Magento 2 Developer's Guide

User Manual: Pdf

Open the PDF directly: View PDF PDF.
Page Count: 412

DownloadMagento 2 Developer's Guide
Open PDF In BrowserView PDF
Magento 2 Developer's Guide

Harness the power of Magento 2, the most recent
version of the world's favorite e-commerce platform,
for your online store

Branko Ajzele

BIRMINGHAM - MUMBAI

Magento 2 Developer's Guide
Copyright © 2015 Packt Publishing

All rights reserved. No part of this book may be reproduced, stored in a retrieval
system, or transmitted in any form or by any means, without the prior written
permission of the publisher, except in the case of brief quotations embedded in
critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy
of the information presented. However, the information contained in this book is
sold without warranty, either express or implied. Neither the author, nor Packt
Publishing, and its dealers and distributors will be held liable for any damages
caused or alleged to be caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the
companies and products mentioned in this book by the appropriate use of capitals.
However, Packt Publishing cannot guarantee the accuracy of this information.

First published: December 2015

Production reference: 1171215

Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham B3 2PB, UK.
ISBN 978-1-78588-658-4
www.packtpub.com

Credits
Author
Branko Ajzele
Reviewer
Mitchell Robles, Jr
Commissioning Editor
Neil Alexander
Acquisition Editor
Vinay Argekar
Content Development Editor
Preeti Singh
Technical Editor
Gaurav Suri
Copy Editors
Vedangi Narvekar
Jonathan Todd

Project Coordinator
Shweta H. Birwatkar
Proofreader
Safis Editing
Indexer
Priya Sane
Production Coordinator
Shantanu N. Zagade
Cover Work
Shantanu N. Zagade

About the Author
Branko Ajzele is a husband, father of two, son, brother, author, and a
software developer.

He has a degree in electrical engineering. A lover of all things digital, he makes a
living from software development. He hopes to find enough quality time some day
to seriously dive into hobby electronics; he has his eye on Arduino and Raspberry Pi.
He has years of hands-on experience with full-time software development and team
management, and has specializing in e-commerce platforms. He has been working
with Magento since 2008; he has been knee-deep in it since its very first beta version.
Branko is regularly in touch with everything related to PHP, databases (MySQL/
MongoDB), search/analytics (Solr/Elasticsearch), Node.js, and related technologies.
He has a strong technical knowledge with an ability to communicate those
technicalities frequently and clearly with a strong direction. He feels comfortable
proposing alternatives to demands which he feels can be improved, even when this
means pulling a late shift to meet the deadlines.
He holds several respected IT certifications, such as Zend Certified Engineer (ZCE
PHP), Magento Certified Developer (MCD), Magento Certified Developer Plus
(MCD+), Magento Certified Solution Specialist (MCSS), and JavaScript Certified
Developer.
Instant E-Commerce with Magento: Build a Shop, Packt Publishing, was his first
Magento-related book that was oriented towards Magento newcomers. After writing
this book, he wrote Getting Started with Magento Extension Development for developers.
Currently, he works as a full-time contractor for Lab Lateral Ltd, an award-winning
team of innovative thinkers, artists, and developers who specialize in customercentric websites, digital consultancy, and marketing. He is the Lead Magento
Developer and Head of Lab's Croatia office.
He was awarded the E-Commerce Developer of the Year by Digital Entrepreneur
Awards in October 2014 for his excellent knowledge and expertise in e-commerce
development. His work is second to none. He is truly dedicated to helping the Lab
Lateral Ltd team and his fellow developers across the world.

About the Reviewer
Mitchell Robles, Jr, is a solutions architect and applications engineer who has

worked in various lead roles for several award-winning digital agencies in San
Diego, CA, USA. Through his own entrepreneurial spirit, he founded Mojo Creative
& Technical Solutions (for more information, visit http://www.mojomage.com/),
which specializes in day-to-day Magento support and development for merchants,
agencies, freelancers, and industry partners. As a certified Magento developer,
Mitchell is the brainchild and lead in developing several must-have Magento
extensions, including Mojo Creative & Technical Solutions' Bundled Mojo, a popular,
full-featured Magento extension that gives administrators total control over how
they display and sell their bundled products. When he is not in the digital matrix,
Mitchell enjoys traveling abroad, exploring, skateboarding, scuba diving, and
tinkering with random projects, from woodworking to 3D printing.
You can follow Mitchell on the Mojo Creative & Technical Solutions' blog, which can
be viewed by visiting http://b.mojomage.com/.

www.PacktPub.com
Support files, eBooks, discount offers, and more

For support files and downloads related to your book, please visit www.PacktPub.com.
Did you know that Packt offers eBook versions of every book published, with PDF
and ePub files available? You can upgrade to the eBook version at www.PacktPub.com
and as a print book customer, you are entitled to a discount on the eBook copy. Get in
touch with us at service@packtpub.com for more details.
At www.PacktPub.com, you can also read a collection of free technical articles, sign
up for a range of free newsletters and receive exclusive discounts and offers on Packt
books and eBooks.
TM

https://www2.packtpub.com/books/subscription/packtlib

Do you need instant solutions to your IT questions? PacktLib is Packt's online digital
book library. Here, you can search, access, and read Packt's entire library of books.

Why subscribe?
•

Fully searchable across every book published by Packt

•

Copy and paste, print, and bookmark content

•

On demand and accessible via a web browser

Free access for Packt account holders

If you have an account with Packt at www.PacktPub.com, you can use this to access
PacktLib today and view 9 entirely free books. Simply use your login credentials for
immediate access.

Table of Contents
Preface
Chapter 1: Understanding the Platform Architecture

vii
1

Chapter 2: Managing the Environment

11

The technology stack
The architectural layers
The top-level filesystem structure
The module filesystem structure
Summary

Setting up a development environment
VirtualBox
Vagrant
Vagrant project
Provisioning PHP
Provisioning MySQL
Provisioning Apache
Provisioning Magento installation

Setting up a production environment
Introduction to Amazon Web Services
Setting up access for S3 usage
Creating IAM users
Creating IAM groups

Setting up S3 for database and media files backup
Bash script for automated EC2 setup
Setting up EC2
Setting up Elastic IP and DNS

Summary

2
3
4
8
9

12
12
12
13

16
17
17
18

20
20
22

23
25

28
30

35
43

46

[i]

Table of Contents

Chapter 3: Programming Concepts and Conventions

47

Chapter 4: Models and Collections

61

Composer
Service contracts
Code generation
The var directory
Coding standards
Summary

Creating a miniature module
Creating a simple model
Creating an EAV model
Understanding the flow of schema and data scripts
Creating an install schema script (InstallSchema.php)
Creating an upgrade schema script (UpgradeSchema.php)
Creating an install data script (InstallData.php)
Creating an upgrade data script (UpgradeData.php)
Entity CRUD actions
Creating new entities
Reading existing entities
Updating existing entities
Deleting existing entities
Managing collections
Collection filters
Summary

47
52
55
57
58
59

62
64
66
69
71
78
79
83
85
88
90
91
91
91
98
100

Chapter 5: Using the Dependency Injection

101

Chapter 6: Plugins

113

The object manager
Dependency injection
Configuring class preferences
Using virtual types
Summary
Creating a plugin
Using the before listener
Using the after listener
Using the around listener
The plugin sort order
Summary

102
104
109
110
111

114
117
118
118
119
120

[ ii ]

Table of Contents

Chapter 7: Backend Development

121

Chapter 8: Frontend Development

159

Cron jobs
Notification messages
Session and cookies
Logging
The profiler
Events and observers
Cache(s)
Widgets
Custom variables
i18n
Indexer(s)
Summary

Rendering flow
View elements
Ui components
Containers
Blocks
Block architecture and life cycle
Templates
Layouts
Themes
Creating a new theme

122
124
127
132
136
138
143
146
149
150
155
157

160
167
167
169
172
174
181
183
186

187

JavaScript

190

CSS
Summary

194
196

Creating a custom JS component

193

Chapter 9: The Web API

User types
Authentication methods
REST versus SOAP
Hands-on with token-based authentication
Hands-on with OAuth-based authentication
OAuth-based Web API calls
Hands-on with session-based authentication
Creating custom Web APIs
API call examples
The getById service method call examples
The getList service method call examples
The save (as new) service method call examples
[ iii ]

197

198
201
202
203
207
213
217
218
235

235
238
243

Table of Contents
The save (as update) service method call examples
The deleteById service method call examples

Search Criteria Interface for list filtering
Summary

245
248

250
254

Chapter 10: The Major Functional Areas

255

Chapter 11: Testing

305

CMS management
Managing blocks manually
Managing blocks via code
Managing blocks via API
Managing pages manually
Managing pages via code
Managing pages via API
Catalog management
Managing categories manually
Managing categories via code
Managing categories via API
Managing products manually
Managing products via code
Managing products via API
Customer management
Managing customers manually
Managing customers via code
Managing customers via an API
Managing customer address via code
Managing customers address via an API
Products and customers import
The custom product types
Custom offline shipping methods
Custom offline payment methods
Summary
Types of tests
Unit testing
Integration testing
Static testing
Integrity testing
Legacy testing
Performance testing

255
256
257
259
259
261
261
262
262
264
265
266
267
268
269
269
272
272
273
274
275
280
287
294
303
305
308
309
310
310
311
312

[ iv ]

Table of Contents

Functional testing
Writing a simple unit test
Summary

314
318
325

Chapter 12: Building a Module from Scratch

327

Index

385

Module requirements
Registering a module
Creating a configuration file (config.xml)
Creating e-mail templates (email_templates.xml)
Creating a system configuration file (system.xml)
Creating access control lists (acl.xml)
Creating an installation script (InstallSchema.php)
Managing entity persistence (Model, Resource, Collection)
Building a frontend interface
Creating routes, controllers, and layout handles
Creating blocks and templates
Handling form submissions
Building a backend interface
Linking the access control list and menu
Creating routes, controllers, and layout handles
Utilizing the grid widget
Creating a grid column renderer
Creating grid column options
Creating controller actions
Creating unit tests
Summary

[v]

327
329
331
332
335
339
341
344
348
348
352
356
360
360
361
363
369
371
372
379
384

Preface
Building Magento-powered stores can be a challenging task. It requires a great
range of technical skills that are related to the PHP/JavaScript programing
language, development and production environments, and numerous
Magento-specific features. This book will provide necessary insights
into the building blocks of Magento.
By the end of this book, you should be familiar with configuration files, the
dependency injection, models, collections, blocks, controllers, events, observers,
plugins, cron jobs, shipping methods, payment methods, and a few other things.
All of these should form a solid foundation for your development journey later on.

What this book covers

Chapter 1, Understanding the Platform Architecture, gives a high-level overview of the
technology stack, architectural layers, top-level system structure, and individual
module structure.
Chapter 2, Managing the Environment, gives an introduction to VirtualBox,
Vagrant, and Amazon AWS as platforms to set up development and production
environments. It further provides hands-on examples to set up/script Vagrant and
Amazon EC2 boxes.
Chapter 3, Programing Concepts and Conventions, introduces readers to a few seemingly
unrelated but important parts of Magento, such as composer, service contracts, code
generation, the var directory, and finally, coding standards.
Chapter 4, Models and Collections, takes a look into models, resources, collections,
schemas, and data scripts. It also shows the practical CRUD actions that are applied
to an entity alongside filtering collections.

[ vii ]

Preface

Chapter 5, Using the Dependency Injection, guides readers through the dependency
injection mechanism. It explains the role of an object manager, how to configure class
preferences, and how to use virtual types.
Chapter 6, Plugins, gives a detailed insight into the powerful new concept called
plugins. It shows how easy it is to extend, or add to, an existing functionality using
the before/after/around listeners.
Chapter 7, Backend Development, takes readers through a hands-on approach to what
is mostly considered backend-related development bits. These involve cron jobs,
notification messages, sessions, cookies, logging, profiler, events, cache, widgets,
and so on.
Chapter 8, Frontend Development, uses a higher-level approach to guide the reader
through what is mostly considered frontend-related development. It touches on
rendering the flow, view elements, blocks, templates, layouts, themes, CSS, and
JavaScript in Magento.
Chapter 9, The Web API, takes up a detailed approach to the powerful Web API
provided by Magento. It gives hands-on practical examples to create and use both
REST and SOAP, either through the PHP cURL library, or from the console.
Chapter 10, The Major Functional Areas, adopts a high-level approach towards
introducing readers with some of the most common sections of Magento. These
include CMS, catalog and customer management, and products and customer
import. It even shows how to create a custom product type and a shipping and
payment method.
Chapter 11, Testing, gives an overview of the types of test that are available in
Magento. It further shows how to write and execute a custom test.
Chapter 12, Building a Module from Scratch, shows the entire process of developing
a module, which uses most of the features introduced in the previous chapters.
The final result is a module that has admin and storefront interface, an admin
configuration area, e-mail templates, installed schema scripts, tests, and so on.

What you need for this book

In order to successfully run all the examples provided in this book, you will need
either your own web server or a third-party web hosting solution. The high-level
technology stack includes PHP, Apache/Nginx, and MySQL. The Magento 2
Community Edition platform itself comes with a detailed list of system requirements
that can be found at http://devdocs.magento.com/guides/v2.0/install-gde/
system-requirements.html. The actual environment setup is explained in
Chapter 2, Managing the Environment.
[ viii ]

Preface

Who this book is for

This book is intended primarily for intermediate to professional PHP developers
who are interested in Magento 2 development. For backend developers, several
topics are covered that will enable you to modify and extend your Magento store.
Frontend developers will also find some coverage on how to customize the look of a
site in the frontend.
Given the massive code and structure changes, Magento version 2.x can be described
as a platform that is significantly different from its predecessor. Keeping this in
mind, this book will neither assume nor require previous knowledge of Magento 1.x.

Conventions

In this book, you will find a number of text styles that distinguish between different
kinds of information. Here are some examples of these styles and an explanation of
their meaning.
Code words in text, database table names, folder names, filenames, file extensions,
pathnames, dummy URLs, user input, and Twitter handles are shown as follows:
"The AbstractProductPlugin1 class does not have to be extended from another
class for the plugin to work."
A block of code is set as follows:








[ ix ]

Preface

Any command-line input or output is written as follows:
php bin/magento setup:upgrade

New terms and important words are shown in bold. Words that you see on the
screen, for example, in menus or dialog boxes, appear in the text like this: "In the
Store View drop-down field, we select the store view where we want to apply
the theme."

Warnings or important notes appear in a box like this.

Tips and tricks appear like this.

Reader feedback

Feedback from our readers is always welcome. Let us know what you think about
this book—what you liked or disliked. Reader feedback is important for us as it helps
us develop titles that you will really get the most out of.
To send us general feedback, simply e-mail feedback@packtpub.com, and mention
the book's title in the subject of your message.
If there is a topic that you have expertise in and you are interested in either writing
or contributing to a book, see our author guide at www.packtpub.com/authors.

Customer support

Now that you are the proud owner of a Packt book, we have a number of things to
help you to get the most from your purchase.

Downloading the example code

You can download the example code files from your account at http://www.
packtpub.com for all the Packt Publishing books you have purchased. If you
purchased this book elsewhere, you can visit http://www.packtpub.com/support
and register to have the files e-mailed directly to you.

[x]

Preface

Errata

Although we have taken every care to ensure the accuracy of our content, mistakes
do happen. If you find a mistake in one of our books—maybe a mistake in the text or
the code—we would be grateful if you could report this to us. By doing so, you can
save other readers from frustration and help us improve subsequent versions of this
book. If you find any errata, please report them by visiting http://www.packtpub.
com/submit-errata, selecting your book, clicking on the Errata Submission Form
link, and entering the details of your errata. Once your errata are verified, your
submission will be accepted and the errata will be uploaded to our website or added
to any list of existing errata under the Errata section of that title.
To view the previously submitted errata, go to https://www.packtpub.com/books/
content/support and enter the name of the book in the search field. The required
information will appear under the Errata section.

Piracy

Piracy of copyrighted material on the Internet is an ongoing problem across all
media. At Packt, we take the protection of our copyright and licenses very seriously.
If you come across any illegal copies of our works in any form on the Internet, please
provide us with the location address or website name immediately so that we can
pursue a remedy.
Please contact us at copyright@packtpub.com with a link to the suspected
pirated material.
We appreciate your help in protecting our authors and our ability to bring you
valuable content.

Questions

If you have a problem with any aspect of this book, you can contact us at
questions@packtpub.com, and we will do our best to address the problem.

[ xi ]

Understanding the Platform
Architecture
Magento is a powerful, highly scalable, and highly customizable e-commerce
platform that can be used to build web shops and, if needed, some non-e-commerce
sites. It provides a large number of e-commerce features out of the box.
Features such as product inventory, shopping cart, support for numerous payment
and shipment methods, promotion rules, content management, multiple currencies,
multiple languages, multiple websites, and so on make it a great choice for
merchants. On the other hand, developers enjoy the full set of merchant-relevant
features plus all the things related to actual development. This chapter will touch
upon the topic of robust Web API support, extensible administration interface,
modules, theming, embedded testing frameworks, and much more.
In this chapter, a high-level overview of Magento is provided in the
following sections:
•

The technology stack

•

The architectural layers

•

The top-level filesystem structure

•

The module filesystem structure

[1]

Understanding the Platform Architecture

The technology stack

Magento's highly modular structure is a result of several open source technologies
embedded into a stack. These open source technologies are composed of the
following components:
•

PHP: PHP is a server-side scripting language. This book assumes that you
have advanced knowledge of the object-oriented aspects of PHP, which is
often referred to as PHP OOP.

•

Coding standards: Magento puts a lot of emphasis on coding standards.
These include PSR-0 (the autoloading standard), PSR-1 (the basic coding
standards), PSR-2 (the coding style guide), PSR-3, and PSR-4.

•

Composer: Composer is a dependency management package for PHP.
It is used to pull in all the vendor library requirements.

•

HTML: HTML5 is supported out of the box.

•

CSS: Magento supports CSS3 via its in-built LESS CSS preprocessor.

•

jQuery: jQuery is a mature cross-platform JavaScript library that was
designed to simplify the DOM manipulation. It is one of the most popular
JavaScript frameworks today.

•

RequireJS: RequireJS is a JavaScript file and module loader. Using a modular
script loader such as RequireJS helps improve the speed and quality of code.

•

Third-party libraries: Magento comes packed with lot of third-party
libraries, with the most notable ones being Zend Framework and Symfony.
It is worth noting that Zend Framework comes in two different major
versions, namely version 1.x and version 2.x. Magento uses both of these
versions internally.

•

Apache or Nginx: Both Apache and Nginx are HTTP servers. Each has its
distinct advantages and disadvantages. It would be unfair to say one is better
than another, as their performance widely depends on the entire system's
setup and usage. Magento works with Apache 2.2 and 2.4 and Nginx 1.7.

•

MySQL: MySQL is a mature and widely used relational database
management system (RDBMS) that uses structured query language (SQL).
There are both free community versions and commercial versions of MySQL.
Magento requires at least the of MySQL Community Edition version 5.6.

•

MTF: Magento Testing Framework (MTF) delivers an automated testing
suite. It covers various types of tests, such as performance, functional, and
unit testing. The entire MTF is available on GitHub, which can be viewed by
visiting https://github.com/magento/mtf as an isolated project.

[2]

Chapter 1

Different pieces of technology can be glued into various architectures. There are
different ways to look at the Magento architecture—from the perspective of a
module developer, system integrator, or a merchant, or from some other angle.

The architectural layers

From top to bottom, Magento can be divided into four architectural layers, namely
presentation, service, domain, and persistence.
The presentation layer is the one that we directly interact with through the browser. It
contains layouts, blocks, templates, and even controllers, which process commands
to and from the user interface. Client-side technologies such as jQuery, RequireJS,
CSS, and LESS are also a part of this layer. Usually, three types of users interact with
this layer, namely web users, system administrators, and those making the Web API
calls. Since the Web API calls can be made via HTTP in a manner that is the same as
how a user uses a browser, there's a thin line between the two. While web users and
Web API calls consume the presentation layer as it is, the system administrators have
the power to change it. This change manifests in the form of setting the active theme
and changing the content of the CMS (short for content management system) pages,
blocks, and the products themselves.
When the components of a presentation layer are being interacted with, they usually
make calls to the underlying service layer.
The service layer is the bridge between the presentation and domain layer. It contains
the service contracts, which define the implementation behavior. A service contract
is basically a fancy name for a PHP interface. This layer is where we can find the
REST/SOAP APIs. Most user interaction on the storefront is routed through the
service layer. Similarly, the external applications that make the REST/SOAP API
calls also interact with this layer.
When the components of a service layer are being interacted with, they usually make
calls to the underlying domain layer.
The domain layer is really the business logic of Magento. This layer is all about
generic data objects and models that compose the business logic. The domain layer
models themselves do not contribute to data persistence, but they do contain a
reference to a resource model that is used to retrieve and persist the data to a MySQL
database. A domain layer code from one module can interact with a domain module
code from another module via the use of event observers, plugins, and the di.xml
definitions. We will look into the details of these later on in other chapters. Given
the power of plugins and di.xml, its important to note that this interaction is best
established using service contracts (the PHP interface).
[3]

Understanding the Platform Architecture

When the components of the domain layer are being interacted with, they usually
make calls to the underlying persistence layer.
The persistence layer is where the data gets persisted. This layer is in charge of all the
CRUD (short for create, read, update, and delete) requests. Magento uses an active
record pattern strategy for the persistence layer. The model object contains a resource
model that maps an object to one or more database rows. Here, it is important to
differentiate the cases of simple resource model and the Entity-Attribute-Value
(EAV) resource models. A simple resource model maps to a single table, while the
EAV resource models have their attributes spread out over a number of MySQL
tables. As an example, the Customer and Catalog resource models use EAV
resource models, while the newsletter's Subscriber resource model uses a
simple resource model.

The top-level filesystem structure
The following list depicts the root Magento filesystem structure:
•

.htaccess

•

.htaccess.sample

•

.php_cs

•

.travis.yml

•

CHANGELOG.md

•

CONTRIBUTING.md

•

CONTRIBUTOR_LICENSE_AGREEMENT.html

•

COPYING.txt

•

Gruntfile.js

•

LICENSE.txt

•

LICENSE_AFL.txt

•

app

•

bin

•

composer.json

•

composer.lock

•
•

dev
index.php

[4]

Chapter 1

•

lib

•

nginx.conf.sample

•

package.json

•

php.ini.sample

•

phpserver

•

pub

•

setup

•

update

•

var

•

vendor

The app/etc/di.xml file is one of the most important files that we might often look
into during development. It contains various class mappings or preferences for
individual interfaces.
The var/magento/language-* directories is where the registered languages
reside. Though each module can declare its own translations under app/code/
{VendorName}/{ModuleName}/i18n/, Magento will eventually fall back to its own
individual module named i18n in case translations are not found in the custom
module or within the theme directory.
The bin directory is where we can find the magento file. The magento file is a script
that is intended to be run from a console. Once triggered via the php bin/magento
command, it runs an instance of the Magento\Framework\Console\Cli application,
presenting us with quite a number of console options. We can use the magento script
to enable/disable cache, enable/disable modules, run an indexer, and do many
other things.
The dev directory is where we can find the Magento test scripts. We will have a look
at more of those in later chapters.
The lib directory comprises two major subdirectories, namely the server-side PHP
library code and fonts found under lib/internal and the client-side JavaScript
libraries found in lib/web.
The pub directory is where the publicly exposed files are located. This is the directory
that we should set as root when setting up Apache or Nginx. The pub/index.php file
is what gets triggered when the storefront is opened in a browser.

[5]

Understanding the Platform Architecture

The var directory is where the dynamically generated group type of files such as
cache, log, and a few others get created in. We should be able to delete the content
of this folder at any time and have Magento automatically recreate it.
The vendor directory is where most of the code is located. This is where we can find
various third-party vendor code, Magento modules, themes, and language packs.
Looking further into the vendor directory, you will see the following structure:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•

.htaccess
autoload.php
bin
braintree
composer
doctrine
fabpot
justinrainbow
league
lusitanian
magento
monolog
oyejorge
pdepend
pelago
phpmd
phpseclib
phpunit
psr
sebastian
seld
sjparkinson
squizlabs
symfony
tedivm
tubalmartin
zendframework

[6]

Chapter 1

Within the vendor directory, we can find code from various vendors, such as
phpunit, phpseclib, monolog, symfony, and so on. Magento itself can be found
here. The Magento code is located under vendor/magento directory, listed (partially)
as follows:
•

composer

•

framework

•

language-en_us

•

magento-composer-installer

•

magento2-base

•

module-authorization

•

module-backend

•

module-catalog

•

module-customer

•

module-theme

•

module-translation

•

module-ui

•

module-url-rewrite

•

module-user

•

module-version

•

module-webapi

•

module-widget

•

theme-adminhtml-backend

•

theme-frontend-blank

•

theme-frontend-luma

You will see that the further structuring of directories follows a certain naming
schema, whereas the theme-* directory stores themes, the module-* directory
stores modules, and the language-* directory stores registered languages.

[7]

Understanding the Platform Architecture

The module filesystem structure

Magento identifies itself as a highly modular platform. What this means is that
there is literally a directory location where modules are placed. Let's take a peak at
the individual module structure now. The following structure belongs to one of the
simpler core Magento modules—the Contact module that can be found in vendor/
magento/module-contact:
•
•
•
•

Block
composer.json
Controller
etc

°°
°°

acl.xml
adminhtml

°°
°°
°°
°°

config.xml
email_templates.xml
frontend

°°
°°
°°
°°
•
•
•
•
•
•
•
•

di.xml
page_types.xml
routes.xml

module.xml

Helper
i18n
LICENSE_AFL.txt
LICENSE.txt
Model
README.md
registration.php
Test

°°

Unit

°°
°°
°°
°°
•

system.xml

Block
Controller
Helper
Model

view

°°
°°

adminhtml
frontend
°° layout
[8]

Chapter 1

°°
°°
°°

contact_index_index.xml
default.xml

templates

°°

form.phtml

Even though the preceding structure is for one of the simpler modules, you can see
that it is still quite extensive.
The Block directory is where the view-related block PHP classes are located.
The Controller directory is where the controller-related PHP classes are stored.
This is the code that responds to the storefront POST and GET HTTP actions.
The etc directory is where the module configuration files are present. Here, we can
see files such as module.xml, di.xml, acl.xml, system.xml, config.xml, email_
templates.xml, page_types.xml, routes.xml, and so on. The module.xml file is an
actual module declaration file. We will look into the contents of some of these files in
the later chapters.
The Helper directory is where various helper classes are located. These classes are
usually used to abstract various store configuration values into the getter methods.
The i18n directory is where the module translation package CSV files are stored.
The Module directory is where the entities, resource entities, collections, and various
other business classes can be found.
The Test directory stores the module unit tests.
The view directory contains all the module administrator and storefront template
files (.phtml and .html) and static files (.js and .css).
Finally, the registration.php is a module registration file.

Summary

In this chapter, we took a quick look at the technology stack used in Magento. We
discussed how Magento, being an open source product, takes extensive use of other
open source projects and libraries such as MySQL, Apache, Nginx, Zend Framework,
Symfony, jQuery, and so on. We then learned how these libraries are arranged into
directories. Finally, we explored one of the existing core modules and briefly took a
look at an example of a module's structure.
In the next chapter, we are going to tackle the environment setup so that we can get
Magento installed and ready for development.
[9]

Managing the Environment
Throughout this chapter, we will look into setting up our development and
production environments. The idea is to have a fully automated development
environment, which can be initiated with a single console command. For a
production environment, we will turn our focus to one of the available cloud
services, and see how easy it is to set up Magento for simpler production projects.
We will not be covering any robust environment setups like auto-scaling, caching
servers, content delivery networks, and similar. These are really jobs for System
Administrator or DevOps roles. Our attention here is the bare minimum needed to
get our Magento store up and running; a milestone we will achieve throughout the
following sections would be:
•

•

Setting up a development environment
°°

VirtualBox

°°

Vagrant

°°

Vagrant project
°°

Provisioning PHP

°°

Provisioning MySQL

°°

Provisioning Apache

°°

Provisioning Magento installation

Setting up a production environment
°°

Introduction to Amazon Web Services (AWS)

°°

Setting up access for S3 usage
°°

Creating IAM users

°°

Creating IAM groups

[ 11 ]

Managing the Environment

°°

Setting up S3 for database and media files backup

°°

Bash script for automated EC2 setup
°°

Setting up EC2

°°

Setting up Elastic IP and DNS

Setting up a development environment
In this section, we will build a development environment using VirtualBox
and Vagrant.

Magento official requirements call for Apache 2.2 or 2.4, PHP 5.6.x or 5.5.x
(PHP 5.4 is not supported), and MySQL 5.6.x. We need to keep this in
mind during the environment setup.

VirtualBox

VirtualBox is powerful and feature-rich x86 and AMD64/Intel64 virtualization
software. It is free, runs on a large number of platforms, and supports a large
number of guest operating systems. If we are using Windows, Linux, or OS X in
our daily development, we can use VirtualBox to spin up a virtual machine with an
isolated guest operating system in which we can install our server software needed
to run Magento. This means using MySQL, Apache, and a few other things.

Vagrant

Vagrant is a high-level software wrapper used for virtualization software
management. We can use it to create and configure development environments.
Vagrant supports several types of virtualization software such as VirtualBox,
VMware, Kernel-based Virtual Machine (KVM), Linux Containers (LXC),
and others. It even supports server environments like Amazon EC2.
Before we start, we need to make sure we have VirtualBox and Vagrant
installed already. We can download them and install the following
instructions from their official websites: https://www.virtualbox.
org and https://www.vagrantup.com.

[ 12 ]

Chapter 2

Vagrant project

We start by manually creating an empty directory somewhere within our host
operating system, let's say /Users/branko/www/B05032-Magento-Box/. This is the
directory we will pull in Magento code. We want Magento source code to be external
to Vagrant box, so we can easily work with it in our favorite IDE.
We then create a Vagrant project directory, let's say /Users/branko/www/magentobox/.
Within the magento-box directory, we run the console command vagrant init.
This results in an output as follows:
A 'Vagrantfile' has been placed in this directory. You are now ready
to 'vagrant up' your first virtual environment! Please read the
comments in the Vagrantfile as well as documentation on 'vagrantup.
com' for more information on using Vagrant.

The Vagrantfile is actually a Ruby language source file. If we strip away the
comments, its original content looks like the following:
# -*- mode: ruby -*# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "base"
end

If we were to run vagrant up now within the magento-box directory, this would
start the VirtualBox in headless (no GUI) mode and run the base operating system.
However, let's hold off running that command just now.
The idea is to create a more robust Vagrantfile that covers everything required
for running Magento, from Apache, MySQL, PHP, PHPUnit, composer, and full
Magento installation with performance fixture data.
Though Vagrant does not have a separate configuration file on its own, we
will create one, as we want to store configuration data like a MySQL user and
password in it.
Let's go ahead and create the Vagrantfile.config.yml file, alongside a
Vagrantfile in the same directory, with content as follows:
ip: 192.168.10.10
s3:
access_key: "AKIAIPRNHSWEQNWHLCDQ"
secret_key: "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"
[ 13 ]

Managing the Environment
bucket: "foggy-project-dhj6"
synced_folder:
host_path: "/Users/branko/www/B05032-Magento-Box/"
guest_path: "/vagrant-B05032-Magento-Box/"
mysql:
host: "127.0.0.1"
username: root
password: user123
http_basic:
repo_magento_com:
username: a8adc3ac98245f519ua0d2v2c8770ec8
password: a38488dc908c6d6923754c268vc41bc4
github_oauth:
github_com: "d79fb920d4m4c2fb9d8798b6ce3a043f0b7c2af6"
magento:
db_name: "magento"
admin_firstname: "John"
admin_lastname: "Doe"
admin_password: "admin123"
admin_user: "admin"
admin_email: "email@change.me"
backend_frontname: "admin"
language: "en_US"
currency: "USD"
timezone: "Europe/London"
base_url: "http://magento.box"
fixture: "small"

There is no Vagrant-imposed structure here. This can be any valid YAML file. The
values presented are merely examples of what we can put in.
Magento enables us to generate a pair of 32-character authentication tokens that
can use to access the Git repository. This is done by logging in to Magento Connect
with a user name and password, then going to My Account | Developers | Secure
Keys. The Public Key and Private Key then become our username and password for
accessing Magento GitHub repository.
Having a separate configuration file means we can commit Vagrantfile to version
control with our project, while leaving the Vagrantfile.config.yml out of
version control.
We now edit the Vagrantfile by replacing its content with the following:
# -*- mode: ruby -*# vi: set ft=ruby :
require 'yaml'
vagrantConfig = YAML.load_file 'Vagrantfile.config.yml'
[ 14 ]

Chapter 2
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/vivid64"
config.vm.network "private_network", ip: vagrantConfig['ip']
# Mount local "~/www/B05032-Magento-Box/" path into box's "/vagrantB05032-Magento-Box/" path
config.vm.synced_folder
vagrantConfig['synced_folder']['host_path'],
vagrantConfig['synced_folder']['guest_path'], owner:"vagrant",
group: "www-data", mount_options:["dmode=775, fmode=664"]
# VirtualBox specific settings
config.vm.provider "virtualbox" do |vb|
vb.gui = false
vb.memory = "2048"
vb.cpus = 2
end
# 
end

The preceding code first includes the yaml library, and then reads the content of
the Vagrantfile.config.yml file into a vagrantConfig variable. Then we have a
config block, within which we define the box type, fixed IP address, shared folder
between our host and guest operating system, and a few VirtualBox specific details
such as CPU and memory allocated.
We are using the ubuntu/vivid64 box that stands for the server edition of Ubuntu
15.04 (Vivid Vervet). The reason is that this Ubuntu version gives us the MySQL 5.6.x
and PHP 5.6.x, which stand as requirements for Magento installation, among
other things.
We further have a configuration entry assigning a fixed IP to our virtual machine.
Let's go ahead and add an entry in the hosts file of our host operating system
as follows:
192.168.10.10 magento.box

The reason why we are assigning the fixed IP address to our box is that
we can directly open a URL like http://magento.box within our host
operating system, and then access Apache served page within our guest
operating system.

[ 15 ]

Managing the Environment

Another important part of the preceding code is the one where we defined
synced_folder. Besides source and destination paths, the crucial parts here are
owner, group, and mount_options. We set those to the vagrant user the www-data
user group, and 774 and 664 for directory and file permissions that play nicely
with Magento.
Let's continue editing our Vagrantfile adding several provisioners to it, one below
the other. We do so by replacing the #  from the preceding
example, with content as follows:
config.vm.provision "file", source: "~/.gitconfig", destination:
".gitconfig"
config.vm.provision "shell", inline: "sudo apt-get update"

Here we are instructing Vagrant to pass our .gitconfig file from the host to the
guest operating system. This is so we inherit the host operating system Git setup to
the guest operating system Git. We then call for apt-get update in order to update
the guest operating system.

Provisioning PHP

Further adding to Vagrantfile, we run several provisioners that will install PHP,
required PHP modules, and PHPUnit, as follows:
config.vm.provision "shell", inline: "sudo apt-get -y install php5
php5-dev php5-curl php5-imagick php5-gd php5-mcrypt php5-mhash
php5-mysql php5-xdebug php5-intl php5-xsl"
config.vm.provision "shell", inline: "sudo php5enmod mcrypt"
config.vm.provision "shell", inline: "echo
\"xdebug.max_nesting_level=200\" >> /etc/php5/apache2/php.ini"
config.vm.provision "shell", inline: "sudo apt-get -y install
phpunit"

There is one thing worth pointing out here – the line where we are
writing xdebug.max_nesting_level=200 into the php.ini file. This
is done to exclude the possibility that Magento would not start throwing a
Maximum Functions Nesting Level of '100' reached... error.

[ 16 ]

Chapter 2

Provisioning MySQL

Further adding to Vagrantfile, we run provisioners that will install the MySQL
server, as follows:
config.vm.provision "shell", inline: "sudo debconf-set-selections
<<< 'mysql-server mysql-server/root_password password
#{vagrantConfig['mysql']['password']}'"
config.vm.provision "shell", inline: "sudo debconf-set-selections
<<< 'mysql-server mysql-server/root_password_again password
#{vagrantConfig['mysql']['password']}'"
config.vm.provision "shell", inline: "sudo apt-get -y install
mysql-server"
config.vm.provision "shell", inline: "sudo service mysql start"
config.vm.provision "shell", inline: "sudo update-rc.d mysql
defaults"

What is interesting with the MySQL installation is that it requires a password and a
password confirmation to be provided during installation. This makes it a troubling
part of the provisioning process that expects shell commands to simply execute
without asking for input. To bypass this, we use debconf-set-selections to store
the parameters for input. We read the password from the Vagrantfile.config.yml
file and pass it onto debconf-set-selections.
Once installed, update-rc.d mysql defaults will add MySQL to the operating
system boot process, thus making sure MySQL is running when we reboot the box.

Provisioning Apache

Further adding to Vagrantfile, we run the Apache provisioner as follows:
config.vm.provision "shell", inline: "sudo apt-get -y install
apache2"
config.vm.provision "shell", inline: "sudo update-rc.d apache2
defaults"
config.vm.provision "shell", inline: "sudo service apache2 start"
config.vm.provision "shell", inline: "sudo a2enmod rewrite"
config.vm.provision "shell", inline: "sudo awk '//,/AllowOverride None/{sub(\"None\", \"All\",$0)}{print}'
/etc/apache2/apache2.conf > /tmp/tmp.apache2.conf"
config.vm.provision "shell", inline: "sudo mv
/tmp/tmp.apache2.conf /etc/apache2/apache2.conf"
config.vm.provision "shell", inline: "sudo awk '//,/AllowOverride None/{sub(\"None\",
\"All\",$0)}{print}' /etc/apache2/apache2.conf >
/tmp/tmp.apache2.conf"

[ 17 ]

Managing the Environment
config.vm.provision "shell", inline: "sudo mv
/tmp/tmp.apache2.conf /etc/apache2/apache2.conf"
config.vm.provision "shell", inline: "sudo service apache2 stop"

The preceding code installs Apache, adds it to the boot sequence, starts it, and turns
on the rewrite module. We then have an update to the Apache configuration file,
as we want to replace AllowOverride None with AllowOverride All, or else
our Magento won't work. Once the changes are done, we stop Apache due to
the later processes.

Provisioning Magento installation

Further adding to Vagrantfile, we now turn our attention to Magento installation,
which we split into several steps. First, we link our host folder, /vagrant-B05032Magento-Box/, with the guest, /var/www/html, using Vagrant's synced folder
feature:
config.vm.provision "shell", inline: "sudo rm -Rf /var/www/html"
config.vm.provision "shell", inline: "sudo ln -s
#{vagrantConfig['synced_folder']['guest_path']} /var/www/html"

We then use the composer create-project command to pull the Magento 2 files
from the official repo.magento.com source into the /var/www/html/ director:
config.vm.provision "shell", inline: "curl -sS
https://getcomposer.org/installer | php"
config.vm.provision "shell", inline: "mv composer.phar
/usr/local/bin/composer"
config.vm.provision "shell", inline: "composer clearcache"
config.vm.provision "shell", inline: "echo '{\"http-basic\":
{\"repo.magento.com\": {\"username\": \"#{vagrantConfig
['http_basic']['repo_magento_com']['username']}\",\"password\":
\"#{vagrantConfig['http_basic']['repo_magento_com']['password']}
\"}}, \"github-oauth\": {\"github.com\":
\"#{vagrantConfig['github_oauth']['github_com']}\"}}' >>
/root/.composer/auth.json"
config.vm.provision "shell", inline: "composer create-project -repository-url=https://repo.magento.com/ magento/projectcommunity-edition /var/www/html/"

We then create a database in which Magento will be installed later on:
config.vm.provision "shell", inline: "sudo mysql -user=#{vagrantConfig['mysql']['username']} -password=#{vagrantConfig['mysql']['password']} -e \"CREATE
DATABASE #{vagrantConfig['magento']['db_name']};\""

[ 18 ]

Chapter 2

We then run the Magento installation from the command line:
config.vm.provision "shell", inline: "sudo php
/var/www/html/bin/magento setup:install --baseurl=\"#{vagrantConfig['magento']['base_url']}\" --dbhost=\"#{vagrantConfig['mysql']['host']}\" --dbuser=\"#{vagrantConfig['mysql']['username']}\" --dbpassword=\"#{vagrantConfig['mysql']['password']}\" --dbname=\"#{vagrantConfig['magento']['db_name']}\" --adminfirstname=\"#{vagrantConfig['magento']['admin_firstname']}\" -admin-lastname=\"#{vagrantConfig['magento']['admin_lastname']}\"
--admin-email=\"#{vagrantConfig['magento']['admin_email']}\" -admin-user=\"#{vagrantConfig['magento']['admin_user']}\" -admin-password=\"#{vagrantConfig['magento']['admin_password']}\"
--backendfrontname=\"#{vagrantConfig['magento']['backend_frontname']}\" -language=\"#{vagrantConfig['magento']['language']}\" -currency=\"#{vagrantConfig['magento']['currency']}\" -timezone=\"#{vagrantConfig['magento']['timezone']}\""
config.vm.provision "shell", inline: "sudo php
/var/www/html/bin/magento deploy:mode:set developer"
config.vm.provision "shell", inline: "sudo php
/var/www/html/bin/magento cache:disable"
config.vm.provision "shell", inline: "sudo php
/var/www/html/bin/magento cache:flush"
config.vm.provision "shell", inline: "sudo php
/var/www/html/bin/magento setup:performance:generate-fixtures
/var/www/html/setup/performance-toolkit/profiles/ce/small.xml"

The preceding code shows we are installing the fixtures data as well.
We need to be careful during the Vagrantfile.config.yml file configuration.
Magento installation is quite sensible around provided data. We need to make sure
we provide valid data for fields like mail and password or else the installation will
fail showing errors similar to the following:
SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost'
(using password: NO)
User Name is a required field.
First Name is a required field.
Last Name is a required field.
'magento.box' is not a valid hostname for email address
'john.doe@magento.box'
'magento.box' appears to be a DNS hostname but cannot match TLD
against known list
'magento.box' appears to be a local network name but local network
names are not allowed
Password is required field.
Your password must be at least 7 characters.
Your password must include both numeric and alphabetic characters.
[ 19 ]

Managing the Environment

With this, we conclude our Vagrantfile content.
Running the vagrant up command now within the same directory as Vagrantfile
triggers the box creation process. During this process, all of the previously listed
commands will get executed. The process alone takes up to an hour or so.
Once vagrant up is complete, we can issue another console command, vagrant ssh,
to log in to the box.
At the same time, if we open a URL like http://magento.box in our browser,
we should see the Magento storefront loading.
The preceding Vagrantfile simply pulls from the official Magento Git repository
and installs Magento from the ground up. Vagrantfile and Vagrantfile.config.
yml can be further extended and tailored to suit our individual project needs, like
pulling the code from the private Git repository, restoring the database from the
shared drive, and so on.
This makes for a simple yet powerful scripting process by which we can prepare
fully ready per-project machines for other developers in a team to be able to quickly
spin up.

Setting up a production environment

A production environment is the client-facing environment that focuses on good
performance and availability. Setting up production environments is not really
something we developers tend to do, especially if there are robust requirements
around scaling, load balancing, high availability, and similar. Sometimes, however,
we need to set up a simple production environment. There are various cloud
providers that offer quick and simple solutions to this. For the purpose of this
section, we will turn to Amazon Web Services.

Introduction to Amazon Web Services

Amazon Web Services (AWS) is a collection of remote computing services
frequently referred to as web services. AWS provides on-demand computing
resources and services in the cloud, with pay-as-you-go pricing. Amazon gives a nice
comparison of its AWS resources, saying that using AWS resources instead of your
own is like purchasing electricity from a power company instead of running your
own generator.

[ 20 ]

Chapter 2

If we stop and think about it for a minute, this makes it interesting to not only system
operation roles but also for developers like us. We (developers) are now able to
spin various databases, web application servers, and even complex infrastructures
in a matter of minutes and a few mouse clicks. We can run these services for a few
minutes, hours, or days then shut them down. Meanwhile, we only pay for the
actual usage, not the full monthly or yearly price as we do with most of the hosting
services. Although the overall AWS pricing for certain services might not be the
cheapest out there, it certainly provides a level of commodity and usability unlike
many other services. Commodity comes from things like auto-scaling resources,
a feature that often offers significant cost savings compared to the equivalent onpremises infrastructure.
Quality training and a certification program is another important aspect of the
AWS ecosystem. Certifications are available for Solutions Architect, Developer,
and SysOps Administrator, across associate and professional levels. Though the
certification is not mandatory, if we deal with AWS on a regular basis, we are
encouraged to take one. Earning the certification puts the seal on our expertise to
design, deploy, and operate highly available, cost-effective, and secure applications
on the AWS platform.
We can manage our AWS through a simple and intuitive web-based user interface
called AWS management console, which is available at https://aws.amazon.
com/console. Signing into AWS, we should be able to see a screen similar to the
following one:

[ 21 ]

Managing the Environment

The preceding image shows how the AWS management console groups the AWS
services visually into several major groups, as follows:
•
•
•
•
•
•
•
•
•
•
•

Compute
Developer Tools
Mobile Services
Storage & Content Delivery
Management Tools
Application Services
Database
Security & Identity
Networking
Analytics
Enterprise Applications

As part of this chapter, we will be taking a look at the EC2 service found under
the Compute group and the S3 service found under the Storage & Content
Delivery group.
Amazon Elastic Compute Cloud (Amazon EC2) is a web service that provides a
re-sizable compute capacity in the cloud. We can think of it as a virtual computer
machine in the cloud that we can turn on and off at any time, within minutes.
We can further commission one, hundreds, or even thousands of these machine
instances simultaneously. This makes for the re-sizable compute capacity.
S3 provides secure, durable, and highly scalable object storage. It is designed to
provide durability of 99.99% of objects. The service provides a web service interface
to store and retrieve any amount of data from anywhere on the web. S3 is charged
only per storage that is actually used. S3 can be used alone or together with other
AWS services such as EC2.

Setting up access for S3 usage

As part of our production environment, it is good to have reliable storage where we
can archive database and media files. Amazon S3 stands out as a possible solution.
In order to properly set access to the S3 scalable storage service, we need to take a
quick look into AWS Identity and Access Management (IAM for short). IAM is a
web service that helps us securely control access to AWS resources for our users.
We can use IAM to control authentication (who can use our AWS resources) and
authorization (what resources they can use and in what ways). More specifically,
as we will soon see, we are interested in Users and Groups.
[ 22 ]

Chapter 2

Creating IAM users

This section describes how to create IAM users. An IAM user is an entity that we
create in AWS to represent the person or service using it when interacting with AWS.
Log in to the AWS console.
Under the user menu, click on Security Credentials as shown in the
following screenshot:

This opens up the security dashboard page.
Clicking on the Users menu should open a screen like the following one:

[ 23 ]

Managing the Environment

On the Users menu, we click on Create New User, which opens a page like
the following:

Here, we fill in the desired username for one or more users, something like
foggy_s3_user1, and then click on the Create button.
We should now see a screen like the following one:

Here, we can click on Download Credentials to initiate the CSV format file
download or copy and paste our credentials manually.
[ 24 ]

Chapter 2

Access Key ID and Secret Access Key are the two pieces of information
we will be using to access S3 storage.

Clicking the close link takes us back to the Users menu, showing our newly created
user listed as shown in the following screenshot:

Creating IAM groups

This section describes how to create IAM groups. Groups are collections of IAM
users that we can manage as a single unit. So let's begin:
1. Log in to the AWS console.
2. Under the user menu, click on Security Credentials as shown in the
following screenshot:

[ 25 ]

Managing the Environment

3. This opens up the security dashboard page. Clicking on the Groups menu
should open a screen like the following one:

4. On the Groups menu, we click on Create New Group, which opens a page
like the following:

5. Here, we fill in the desired group name, something like FoggyS3Test.
6. We should now see a screen like the following one, where we need to select
the group Policy Type and click the Next Step button:

[ 26 ]

Chapter 2

7. We select the AmazonS3FullAccess policy type and click the Next
Step button. The Review screen is now shown, asking us to review
the provided information:

8. If the provided information is correct, we confirm it by clicking the Create
Group button. We should now be able to see our group under the Groups
menu as shown in the following screenshot:

9. Mark the checkbox to the left of Group Name, click the Group Actions
dropdown, and then select Add Users to Group as shown in the
following screenshot:

[ 27 ]

Managing the Environment

10. This opens the Add Users to Group page as shown in the following
screenshot:

11. Mark the checkbox to the left of User Name and click on the Add Users
button. This should add the selected user to the group and throw us back to
the Groups listing.
The result of this user and group creation process is a user with Access Key Id,
Secret Access Key, and assigned user group with the AmazonS3FullAccess policy.
We will use this information later on when we demonstrate backing up the database
to S3.

Setting up S3 for database and media files
backup

S3 consists of buckets. We can think of a bucket as the first level directory within our
S3 account. We then set the permissions and other options on that directory (bucket).
In this section, we are going to create our own bucket, with two empty folders called
database and media. We will use these folders later on during our environment
setup in order to back up our MySQL database and our media files.
We start by logging in to the AWS management console.
Under the Storage & Content Delivery group, we click on S3. This opens a screen
similar to the following:

[ 28 ]

Chapter 2

Click on the Create Bucket button. This opens a popup like the one shown in the
following screenshot:

Let's provide a unique Bucket Name, preferably something identifying the project
for which we will be backing up the database and media file, and click the Create
button. For the purpose of this chapter, let's imagine we selected something like
foggy-project-dhj6.
Our bucket should now be visible under the All Buckets list. If we click on it, a new
screen opens like the one shown in the following screenshot:

Here, we click on the Create Folder button and add the necessary database and
media folders.

[ 29 ]

Managing the Environment

While still within the root bucket directory, click on the Properties button and fill in
the Permissions section as shown in the following screenshot:

Here, we are basically assigning all permissions to Authenticated Users.
We should now have an S3 bucket to which we can potentially store our database
and media backups using the s3cmd console tool that we will soon reference.

Bash script for automated EC2 setup

Similar to the Vagrantfile shell provisioners, let's go ahead and create a sequence
of bash shell commands we can use for a production setup.
The first block of commands goes as follows:
#!/bin/bash
apt-get update
apt-get -y install s3cmd

Here, start with the #!/bin/bash expression. This specifies the type of script we are
executing. Then we have a system update and s3cmd tool installation. The s3cmd is
a free command-line tool and client for uploading, retrieving, and managing data in
Amazon S3. We can use it later on for database and media file backups and restores.
We then install the postfix mail server, using the following commands. Since the
postfix installation triggers a graphical interface in the console, asking for mailname
and main_mailer_type, we bypass those using sudo debconf-set-selections.
Once installed, we reload postfix.

[ 30 ]

Chapter 2
sudo debconf-set-selections <<< "postfix postfix/mailname string
magentize.me"
sudo debconf-set-selections <<< "postfix postfix/main_mailer_type
string 'Internet Site'"
sudo apt-get install -y postfix
sudo /etc/init.d/postfix reload

Using mail server directly on the EC2 box is fine for smaller production sites, where
we do not expect high traffic or a large number of customers. For more intensive
production sites, we need to pay attention to Amazon, possibly putting a throttle on
port 25, thus resulting in outgoing e-mail timeouts. In which case we can either ask
Amazon to remove the limitation on our account, or move on to more robust services
like Amazon Simple Email Service.
We then install all things related to PHP. Notice how we even install xdebug, though
immediately turning it off. This might come in handy for those very rare moments
when we really need to debug the live site, then we can turn it off and play with
remote debugging. We further download and set composer to the user path:
apt-get -y install php5 php5-dev php5-curl php5-imagick php5-gd php5mcrypt php5-mhash php5-mysql php5-xdebug php5-intl php5-xsl
php5enmod mcrypt
php5dismod xdebug
service php5-fpm restart
apt-get -y install phpunit
echo "Starting Composer stuff" >> /var/tmp/box-progress.txt
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer

We then move on to MySQL installation. Here, we are also using debconf-setselections to automate the console part of providing input parameters to the
installation. Once installed, MySQL is started and added to the boot process.
debconf-set-selections <<< 'mysql-server mysql-server/root_password
password RrkSBi6VDg6C'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again
password RrkSBi6VDg6C'
apt-get -y install mysql-server
service mysql start
update-rc.d mysql defaults

[ 31 ]

Managing the Environment

Alongside MySQL, another major component is Apache. We install it using the
following commands. With Apache, we need to pay attention to its apache2.
conf file. We need to change AllowOverride None to AllowOverride All for the
Magento directory:
apt-get -y install apache2
update-rc.d apache2 defaults
service apache2 start
a2enmod rewrite
awk '//,/AllowOverride None/{sub("None",
"All",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf
mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf
awk '//,/AllowOverride None/{sub("None",
"All",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf
mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf
service apache2 restart

Now that we have MySQL and Apache installed, we move on to getting the source
code files in place. Next, we are pulling from the official Magento Git repository. This
is not the same as repo.magento.com we used when setting up the vagrant. Though
in this case the Magento Git repository is public, the idea is to be able to pull the code
from the private GitHub repository. Based on the production environment we tend
to set up, we can easily replace the next part with pulling from any other private Git
repository.
sudo rm -Rf /var/www/html/*
git clone https://github.com/magento/magento2.git /var/www/html/.
sudo composer config --global github-oauth.github.com
7d6da6bld50dub454edc27db70db78b1f8997e6
sudo composer install --working-dir="/var/www/html/"
mysql -uroot -pRrkSBi6VDg6C -e "CREATE DATABASE magento;"
PUBLIC_HOSTNAME="'wget -q -O - http://instance-data/latest/metadata/public-hostname'"

To pull the code from a private git repository, we can use a command
of the following form, Git clone: https://:@
github.com//.git.

[ 32 ]

Chapter 2

The PUBLIC_HOSTNAME variable stores the response of the wget command that calls
the http://instance-data/latest/meta-data/public-hostname URL. This URL
is a feature of AWS that allows us to get the current EC2 instance metadata. We then
use the PUBLIC_HOSTNAME variable during Magento installation, passing it as the
--base-url parameter:
php /var/www/html/bin/magento setup:install --baseurl="http://$PUBLIC_HOSTNAME" --db-host="127.0.0.1" --dbuser="root" --db-password="RrkSBi6VDg6C" --db-name="magento" -admin-firstname="John" --admin-lastname="Doe" --adminemail="john.doe@change.me" --admin-user="admin" --adminpassword="pass123" --backend-frontname="admin" -language="en_US" --currency="USD" --timezone="Europe/London"

The preceding command takes a lot of per project specific configuration values, so we
need to be sure to paste in our own information here appropriately before simply
copying and pasting it.
Now we make sure the Magento mode is set to production, and cache is turned on
and flushed, so it regenerates fresh:
php /var/www/html/bin/magento deploy:mode:set production
php /var/www/html/bin/magento cache:enable
php /var/www/html/bin/magento cache:flush

Finally, we reset the permissions on the /var/www/html directory in order for our
Magento to function properly:
chown -R ubuntu:www-data /var/www/html
find /var/www/html -type f -print0 | xargs -r0 chmod 640
find /var/www/html -type d -print0 | xargs -r0 chmod 750
chmod -R g+w /var/www/html/pub
chmod -R g+w /var/www/html/var
chmod -R g+w /var/www/html/app
chmod -R g+w /var/www/html/vendor

We need to take caution with the preceding Git and Magento installation example.
The idea here was to show how we could automatically set Git pull from the public
or private repository. The Magento installation part is a little bonus for this specific
case, not something we would actually do on our production machine. The whole
purpose of this script would be to serve as a blueprint for powering up new AMI
images. So ideally what we would usually do once the code is pulled, is to restore the
database from some private storage like S3 and then attach it to our installation. Thus
making for a complete restore of files, database, and media once the script is finished.
[ 33 ]

Managing the Environment

Putting that thought aside, let's get back to our script, further adding the daily
database backup using the set of command as follows:
CRON_CMD="mysql --user=root --password=RrkSBi6VDg6C magento | gzip -9
> ~/database.sql.gz"
CRON_JOB="30 2 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab CRON_CMD="s3cmd --access_key="AKIAINLIM7M6WGJKMMCQ" -secret_key="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" put
~/database.sql.gz s3://foggy-project-ghj7/database/database_'date
+"%Y-%m-%d_%H-%M"'.sql.gz"
CRON_JOB="30 3 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -

Here, we are adding the 2:30 AM cron job for backing up the database into the home
directory file named database.sql.gz. Then we are adding another cron job that
executes at 3:30 AM, which pushes the database backup to S3 storage.
Similar to the database backup, we can add media backup instructions to our script
using the set of command as follows:
CRON_CMD="tar -cvvzf ~/media.tar.gz /var/www/html/pub/media/"
CRON_JOB="30 2 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab CRON_CMD="s3cmd --access_key="AKIAINLIM7M6WGJKMMCQ" -secret_key="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" put
~/media.tar.gz s3://foggy-project-ghj7/media/media_'date +"%Y-%m%d_%H-%M"'.tar.gz"
CRON_JOB="30 3 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -

The preceding commands have several pieces of information coded in them. We
need to make sure to paste in our access key, secret key, and S3 bucket name
accordingly. For simplicity sake, we are not addressing security implications such as
hardcoding the access tokens into the cron jobs. Amazon provides an extensive AWS
Security Best Practices guide that can be downloaded via the official AWS website.
Now that we have some understanding of what the bash script for automated EC2
setup could look like, let's proceed to setting up the EC2 instance.

[ 34 ]

Chapter 2

Setting up EC2

Follow these steps to get the setting done:
1. Log in to the AWS console
2. Under the Compute group, click on EC2, which should open a screen like
the following:

3. Click on the Launch Instance button, which should open a screen like
the following:

[ 35 ]

Managing the Environment

4. Click on the Community AMIs tab to the left, and type in Ubuntu Vivid into
the search field, as shown in the following screenshot:

The Ubuntu 15.x (Vivid Vervet) server by default supports
MySQL 5.6.x and PHP 5.6.x, which makes it a good
candidate for Magento installation.

We should now see a screen like the following:

5. Choose an instance type and click the Next: Configure Instance Details
button. We should now see a screen like the following:

[ 36 ]

Chapter 2

We won't be getting into the details of each of these options.
Suffice to say that if we are working on smaller production
sites, chances are we can leave most of these options with their
default values.

6. Make sure Shutdown behavior is set to Stop.
7. While still on the Step 3: Configure Instance Details screen, scroll down to
the bottom Advanced Details area and expand it. We should see a screen like
the following:

[ 37 ]

Managing the Environment

8. The User Data input is where we will copy and paste the auto-setup
bash script described in the previous section, as shown in the following
screenshot:

9. Once we copy and paste in the User Data, click on the Next: Add Storage
button. This should bring up the screen as shown in the following screenshot:

10. Within Step 4: Add Storage, we can select one or more volumes to attach to
our EC2 instance. Preferably, we should select the SSD type of storage for
faster performance. Once the volume is set, click on Next: Tag Instance.
We should now see a screen like the following:

[ 38 ]

Chapter 2

11. The Tag Instance screen allows us to assign tags. Tags enable us to categorize
our AWS resource by purpose, owner, environment, or some other way.
Once we have assigned one or more tags, we click on the Next: Configure
Security Group button. We should now see a screen like the following:

[ 39 ]

Managing the Environment

12. The Configure Security Group screen allows us to set rules for inbound and
outbound traffic. We want to be able to access SSH, HTTP, HTTPs, and SMTP
services on the box. Once we add the rules we want, click on the Review and
Launch button. This opens a screen like the following:

13. The Review Instance Launch screen is where we can view the summary of
the box we configured up to this point. If needed, we can go back and edit
individual settings. Once we are satisfied with the summary, we click on the
Launch button. This opens a popup like the following:

[ 40 ]

Chapter 2

14. Here, we get to choose an existing security key, or create a new one. Keys
are provided in PEM format. Once we select the key, we click on the Launch
Instance button.
We should now see the Launch Status screen like the following:

[ 41 ]

Managing the Environment

15. Clicking on the instance name link should throw us back at the EC2
Dashboard like shown in the following screenshot:

With regard to the preceding image, we should now be able to connect to our EC2
box with either one of the following console commands:
ssh -i /path/to/magento-box.pem ubuntu@ec2-52-29-35-49.eu-central-1.
compute.amazonaws.com
ssh -i /path/to/magento-box.pem ubuntu@52.29.35.49

It might take some time for our EC2 box to execute all of the shell commands passed
to it. We can conveniently SSH into the box and then execute the following command
to get an overview of current progress:
sudo tail -f /var/tmp/box-progress.txt

With this, we conclude our instance launch process.

[ 42 ]

Chapter 2

Setting up Elastic IP and DNS

Now that we have an EC2 box in place, let's go ahead and create the so-called Elastic
IP for it. The Elastic IP address is a static IP address designed for dynamic cloud
computing. It is tied to the AWS account, and not some specific instance. This makes
it convenient to easily re-map it from one instance to another.
Let's go ahead and create an Elastic IP as follows:
1. Log in to the AWS console.
2. Under the Compute group, click on EC2, which should get us to the
EC2 Dashboard.
3. Under the EC2 Dashboard, in the left area under Network and Security
grouping, click on Elastic IPs. This should open a screen like the following:

4. Click on the Allocate New Address button, which should open a popup like
the following:

[ 43 ]

Managing the Environment

5. Click on the Yes, Allocate button, which should open another popup like
the following:

6. Now that the Elastic IP address is created, right-clicking on it within
the table listing should bring up the options menu as shown in the
following screenshot:

7. Click on the Associate Address link. This should open a popup like
the following:

[ 44 ]

Chapter 2

8. On the Associate Address popup, we select the Instance to which we want
to assign the Elastic IP address and click on the Associate button.
At this point, our EC2 box has a static (Elastic IP) address assigned. We can now
log in to our domain registrar and point the A-record of our DNS to the Elastic
IP we just created.
Until we wait for the DNS change to kick in, there is one more thing we need to
address. We need to SSH into our box and execute the following set of commands:
mysql -uroot -pRrkSBi6VDg6C -e "USE magento; UPDATE core_config_data
SET value = 'http://our-domain.something/' WHERE path LIKE
"%web/unsecure/base_url%";"
php /var/www/html/bin/magento cache:flush

This will update the Magento URL, so we can access it via a web browser once the
DNS change kicks in. With a little bit of upfront planning, we could have easily made
this bit a part of the user data for our EC2 instance, simply by providing the right
--base-url parameter value in the first place.

[ 45 ]

Managing the Environment

Summary

Throughout this chapter, we focused on two main things: setting up development
and production environments.
As part of the development environment, we embraced free software such as
VirtualBox and Vagrant to manage our environment setup. The setup alone came
down to a single Vagrantfile script that contained the necessary set of commands
to install everything from the Ubuntu server, PHP, Apache, MySQL, and even
Magento itself. We should by no means look at this script as final and only as a
valid script to set up our development environment. Investing time in making the
development environment closer to the project-specific requirements pays off in
terms of team productivity.
We then moved on to the production environment. Here, we looked into Amazon
Web Services, utilizing S3 and EC2 along the way. The production environment also
came with its own scripted installation process that sets most of the things. Similarly,
this script is by no means final and is only a valid way to set things up; it's more of a
base example of how to do it.
In the next chapter, we will take a closer look at some of programming concepts
and conventions.

[ 46 ]

Programming Concepts and
Conventions
With years of experience, the Magento platform grew up to implement a lot of
industry concepts, standards, and conventions. Throughout this chapter, we
will look into several of these independent sections that stand out in day-to-day
interactions with Magento development.
We will go through the following sections in this chapter:
•

Composer

•

Service contracts

•

Code generation

•

The var directory

•

Coding standards

Composer

Composer is a tool that handles dependency management in PHP. It is not a package
manager like Yum and Apt on Linux systems are. Though it deals with libraries
(packages), it does so on a per-project level. It does not install anything globally.
Composer is a multiplatform tool. Therefore, it runs equally well on Windows,
Linux, and OS X.
Installing Composer on a machine is as simple as running the installer in the project
directory by using the following command:
curl -sS https://getcomposer.org/installer | php

[ 47 ]

Programming Concepts and Conventions

More information about the installation of Composer can be found on its official
website, which can be viewed by visiting https://getcomposer.org.
Composer is used to fetch Magento and the third-party components that it uses.
As seen in the previous chapter, the following composer command is what pulls
everything into the specified directory:
composer create-project --repository-url=https://repo.magento.com/
magento/project-enterprise-edition 

Once Magento is downloaded and installed, there are numerous composer.json
files that can be found in its directory. Assuming 
is magento2, if we were to do a quick search executing command such as find
magento2/ -name 'composer.json', that would yield over 100 composer.json
files. Some of these files are (partially) listed here:
/vendor/magento/module-catalog/composer.json
/vendor/magento/module-cms/composer.json
/vendor/magento/module-contact/composer.json
/vendor/magento/module-customer/composer.json
/vendor/magento/module-sales/composer.json
/...
/vendor/magento/theme-adminhtml-backend/composer.json
/vendor/magento/theme-frontend-blank/composer.json
/vendor/magento/theme-frontend-luma/composer.json
/vendor/magento/language-de_de/composer.json
/vendor/magento/language-en_us/composer.json
/...
/composer.json
/dev/tests/...
/vendor/magento/framework/composer.json

The most relevant file is probably the composer.json file in the root of the magento
directory. Its content appears like this:
{
"name": "magento/project-community-edition",
"description": "eCommerce Platform for Growth (Community
Edition)",
"type": "project",
"version": "2.0.0",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"repositories": [
[ 48 ]

Chapter 3
{
"type": "composer",
"url": "https://repo.magento.com/"
}
],
"require": {
"magento/product-community-edition": "2.0.0",
"composer/composer": "@alpha",
"magento/module-bundle-sample-data": "100.0.*",
"magento/module-widget-sample-data": "100.0.*",
"magento/module-theme-sample-data": "100.0.*",
"magento/module-catalog-sample-data": "100.0.*",
"magento/module-customer-sample-data": "100.0.*",
"magento/module-cms-sample-data": "100.0.*",
"magento/module-catalog-rule-sample-data": "100.0.*",
"magento/module-sales-rule-sample-data": "100.0.*",
"magento/module-review-sample-data": "100.0.*",
"magento/module-tax-sample-data": "100.0.*",
"magento/module-sales-sample-data": "100.0.*",
"magento/module-grouped-product-sample-data": "100.0.*",
"magento/module-downloadable-sample-data": "100.0.*",
"magento/module-msrp-sample-data": "100.0.*",
"magento/module-configurable-sample-data": "100.0.*",
"magento/module-product-links-sample-data": "100.0.*",
"magento/module-wishlist-sample-data": "100.0.*",
"magento/module-swatches-sample-data": "100.0.*",
"magento/sample-data-media": "100.0.*",
"magento/module-offline-shipping-sample-data": "100.0.*"
},
"require-dev": {
"phpunit/phpunit": "4.1.0",
"squizlabs/php_codesniffer": "1.5.3",
"phpmd/phpmd": "@stable",
"pdepend/pdepend": "2.0.6",
"sjparkinson/static-review": "~4.1",
"fabpot/php-cs-fixer": "~1.2",
"lusitanian/oauth": "~0.3 <=0.7.0"
},
"config": {
"use-include-path": true
},
"autoload": {
"psr-4": {

[ 49 ]

Programming Concepts and Conventions
"Magento\\Framework\\":
"lib/internal/Magento/Framework/",
"Magento\\Setup\\": "setup/src/Magento/Setup/",
"Magento\\": "app/code/Magento/"
},
"psr-0": {
"": "app/code/"
},
"files": [
"app/etc/NonComposerComponentRegistration.php"
]
},
"autoload-dev": {
"psr-4": {
"Magento\\Sniffs\\":
"dev/tests/static/framework/Magento/Sniffs/",
"Magento\\Tools\\": "dev/tools/Magento/Tools/",
"Magento\\Tools\\Sanity\\":
"dev/build/publication/sanity/
Magento/Tools/Sanity/",
"Magento\\TestFramework\\Inspection\\":
"dev/tests/static/framework/Magento/
TestFramework/Inspection/",
"Magento\\TestFramework\\Utility\\":
"dev/tests/static/framework/Magento/
TestFramework/Utility/"
}
},
"minimum-stability": "alpha",
"prefer-stable": true,
"extra": {
"magento-force": "override"
}
}

Composer's JSON file follows a certain schema. You will find a detailed
documentation of this schema at https://getcomposer.org/doc/04-schema.md.
Applying to the schema ensures validity of the composer file. We can see that all the
listed keys such as name, description, require, config, and so on, are defined by
the schema.

[ 50 ]

Chapter 3

Let's take a look at the individual module's composer.json file. One of the simpler
modules with the least amount of dependencies is the Contact module with its
vendor/magento/module-contact/composer.json content, which looks like this:
{
"name": "magento/module-contact",
"description": "N/A",
"require": {
"php": "~5.5.0|~5.6.0|~7.0.0",
"magento/module-config": "100.0.*",
"magento/module-store": "100.0.*",
"magento/module-backend": "100.0.*",
"magento/module-customer": "100.0.*",
"magento/module-cms": "100.0.*",
"magento/framework": "100.0.*"
},
"type": "magento2-module",
"version": "100.0.2",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Magento\\Contact\\": ""
}
}
}

You will see that the modules define dependencies on the PHP version and other
modules. Furthermore, you will see the use of PSR-4 for autoloading and the direct
loading of the registration.php file.
Next, let's take a look at the contents of vendor/magento/language-en_us/
composer.json from the en_us language module:
{
"name": "magento/language-en_us",
"description": "English (United States) language",
"version": "100.0.2",
"license": [
"OSL-3.0",
[ 51 ]

Programming Concepts and Conventions
"AFL-3.0"
],
"require": {
"magento/framework": "100.0.*"
},
"type": "magento2-language",
"autoload": {
"files": [
"registration.php"
]
}
}

Finally, let's take a look at the contents of vendor/magento/theme-frontend-luma/
composer.json from the luma theme:
{
"name": "magento/theme-frontend-luma",
"description": "N/A",
"require": {
"php": "~5.5.0|~5.6.0|~7.0.0",
"magento/theme-frontend-blank": "100.0.*",
"magento/framework": "100.0.*"
},
"type": "magento2-theme",
"version": "100.0.2",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"autoload": {
"files": [
"registration.php"
]
}
}

As mentioned previously, there are a lot more composer files scattered
around Magento.

Service contracts

A service contract is a set of PHP interfaces that is defined by a module. This contract
comprises data interfaces and service interfaces.
[ 52 ]

Chapter 3

The role of the data interface is to preserve data integrity, while the role of the service
interface is to hide the business logic details from service consumers.
Data interfaces define various functions, such as validation, entity information,
search related functions, and so on. They are defined within the Api/Data directory
of an individual module. To better understand the actual meaning of it, let's take a
look at the data interfaces for the Magento_Cms module. In the vendor/magento/
module-cms/Api/Data/ directory, there are four interfaces defined, as follows:
BlockInterface.php
BlockSearchResultsInterface.php
PageInterface.php
PageSearchResultsInterface.php

The CMS module actually deals with two entities, one being Block and the other one
being Page. Looking at the interfaces defined in the preceding code, we can see that
we have separate data interface for the entity itself and separate data interface for
search results.
Let's take a closer look at the (stripped) contents of the BlockInterface.php file,
which is defined as follows:
namespace Magento\Cms\Api\Data;
interface
{
const
const
const
const
const
const
const

BlockInterface

public
public
public
public
public
public
public
public
public
public
public

BLOCK_ID
IDENTIFIER
TITLE
CONTENT
CREATION_TIME
UPDATE_TIME
IS_ACTIVE
function
function
function
function
function
function
function
function
function
function
function

=
=
=
=
=
=
=

'block_id';
'identifier';
'title';
'content';
'creation_time';
'update_time';
'is_active';

getId();
getIdentifier();
getTitle();
getContent();
getCreationTime();
getUpdateTime();
isActive();
setId($id);
setIdentifier($identifier);
setTitle($title);
setContent($content);
[ 53 ]

Programming Concepts and Conventions
public function setCreationTime($creationTime);
public function setUpdateTime($updateTime);
public function setIsActive($isActive);
}

The preceding interface defines all the getter and setter methods for the entity at
hand along with the constant values that denote entity field names. These data
interfaces do not include management actions, such as delete. The implementation
of this specific interface can be seen in the vendor/magento/module-cms/Model/
Block.php file, where these constants come to use, as follows (partially):
public function getTitle()
{
return $this->getData(self::TITLE);
}
public function setTitle($title)
{
return $this->setData(self::TITLE, $title);
}

Service interfaces are the ones that include management, repository, and metadata
interfaces. These interfaces are defined directly within the module's Api directory.
Looking back at the Magento Cms module, its vendor/magento/module-cms/Api/
directory has two service interfaces, which are defined as follows:
BlockRepositoryInterface.php
PageRepositoryInterface.php

A quick look into the contents of BlockRepositoryInterface.php reveals the
following (partial) content:
namespace Magento\Cms\Api;
use Magento\Framework\Api\SearchCriteriaInterface;
interface BlockRepositoryInterface
{
public function save(Data\BlockInterface $block);
public function getById($blockId);
public function getList(SearchCriteriaInterface
$searchCriteria);
public function delete(Data\BlockInterface $block);
public function deleteById($blockId);
}

[ 54 ]

Chapter 3

Here, we see methods that are used to save, fetch, search, and delete the entity.
These interfaces are then implemented via the Web API definitions, as we will see
later in Chapter 9, The Web API. The result is well-defined and durable API's that
other modules and third-party integrators can consume.

Code generation

One of the neat features of the Magento application is code generation. Code
generation, as implied by its name, generates nonexistent classes. These classes
are generated in Magento's var/generation directory.
The directory structure within var/generation is somewhat similar to that of the
core vendor/magento/module-* and app/code directories. To be more precise, it
follows the module structure. The code is generated for something that is called
Factory, Proxy, and Interceptor classes.
The Factory class creates an instance of a type. For example, a var/generation/
Magento/Catalog/Model/ProductFactory.php file with a Magento\Catalog\
Model\ProductFactory class has been created because somewhere within the
vendor/magento directory and its code, there is a call to the Magento\Catalog\
Model\ProductFactory class, which originally does not exist in Magento. During
runtime, when {someClassName}Factory is called in the code, Magento creates a
Factory class under the var/generation directory if it does not exist. The following
code is an example of the (partial) ProductFactory class:
namespace Magento\Catalog\Model;
/**
* Factory class for @see \Magento\Catalog\Model\Product
*/
class ProductFactory
{
//...
/**
* Create class instance with specified parameters
*
* @param array $data
* @return \Magento\Catalog\Model\Product
*/
public function create(array $data = array())
{

[ 55 ]

Programming Concepts and Conventions
return $this->_objectManager->create($this->_instanceName,
$data);
}
}

Note the create method that creates and returns the Product type instance. Also,
note how the generated code is type safe providing @return annotation for integrated
development environments (IDEs) to support the autocomplete functionality.
Factories are used to isolate an object manager from the business code. Factories can
be dependent on the object manager, unlike business objects.
The Proxy class is a wrapper for some base class. Proxy classes provide better
performance than the base classes because they can be instantiated without
instantiating a base class. A base class is instantiated only when one of its methods
is called. This is highly convenient for cases where the base class is used as a
dependency, but it takes a lot of time to instantiate, and its methods are used
only during some paths of execution.
Like Factory, the Proxy classes are also generated under the var/generation
directory.
If we were to take a look at the var/generation/Magento/Catalog/Model/
Session/Proxy.php file that contains the Magento\Catalog\Model\Session\Proxy
class, we would see that it actually extends \Magento\Catalog\Model\Session. The
wrapping Proxy class implements several magical methods along the way, such as
__sleep, __wakeup, __clone, and __call.

Interceptor is yet another class type that gets autogenerated by Magento. It is related
to the plugins feature, which will be discussed in detail later in Chapter 6, Plugins.
In order to trigger code regeneration, we can use the code compiler that is
available on the console. We can run either the single-tenant compiler or the
multi-tenant compiler.
The single-tenant implies one website and store, and it is executed by using the
following command:
magento setup:di:compile

The multi-tenant implies more than one independent Magento application, and it is
executed by using following command.
magento setup:di:compile-multi-tenant

[ 56 ]

Chapter 3

Code compilation generates factories, proxies, interceptors, and several other classes,
as listed in the setup/src/Magento/Setup/Module/Di/App/Task/Operation/
directory.

The var directory

Magento does a lot of caching and autogeneration of certain class types. These caches
and generated classes are all located in Magento's root var directory. The usual
contents of the var directory is as follows:
cache
composer_home
generation
log
di
view_preprocessed
page_cache

During development, we will most likely need to periodically clear these so that our
changes can kick in.
We can issue the console command as follows to clear individual directories:
rm -rf {Magento root dir}/var/generation/*

Alternatively, we can use the built-in bin/magento console tool to trigger commands
that will delete the proper directories for us, as follows:
•

bin/magento setup:upgrade: This updates the Magento database schema
and data. While doing this, it truncates the var/di and var/generation

directories.

•

bin/magento setup:di:compile: This clears the var/generation

•

bin/magento deploy:mode:set {mode}: This changes the mode from the
developer mode to the production mode and vice versa. While doing this,
it truncates the var/di, var/generation, and var/view_preprocessed
directories.

•

bin/magento cache:clean {type}: This cleans the var/cache and var/
page_cache directories.

directory. After doing this, it compiles the code in it again.

It is important to keep the var directory in mind at all times during development.
Otherwise, the code might encounter exceptions and function improperly.

[ 57 ]

Programming Concepts and Conventions

Coding standards

Coding standards are a result of conventions designed to produce high-quality
code. Adopting certain standards yields better code quality, reduces the time taken
to develop, and minimizes maintenance cost. Following coding standards requires
knowing the standards in question and meticulously applying it to every aspect of
the code that we write.
There are several coding standards that Magento abides by, such as the
following ones:
•

The code demarcation standard

•

The PHP coding standard

•

The JavaScript coding standard

•

The jQuery widget coding standard

•

The DocBlock standard

•

JavaScript DocBlock standard

•

The LESS coding standard

The code demarcation standard speaks of decoupling HTML, CSS, and JS from PHP
classes. By doing so, the backend-related development stays unaffected by frontend
development and vice versa. This means that we can make business logic changes
without fearing a broken frontend.
The PHP coding standard refers to PSR-1: Basic Coding Standard and PSR-2:
Coding Style Guide that are described at http://www.php-fig.org. PSR-1 touches
on PHP filenames, class names, namespaces, class constant, properties, and methods.
PSR-2 extends the PSR-1 by touching upon the actual inners of a class, such as
spaces, braces, method and properties visibility, control structures, and so on.
The JavaScript coding standard is based on the Google JavaScript Style Guide found
at https://google.github.io/styleguide/javascriptguide.xml. This coding
standard touches on the JavaScript language and coding style rules. It is a lot like
PSR-1 and PSR-2 for PHP.
The jQuery widget coding standard is flagged as mandatory for Magento core
developers and recommended for third-party developers. It goes without saying
how important jQuery UI widgets are in Magento. The standard describes several
things, such as widget naming, instantiation, extension, DOM event bubbling,
and so on.

[ 58 ]

Chapter 3

The DocBlock standard touches on the requirements and conventions for the
addition of inline code documentation. The idea is to unify the usage of code
DocBlocks for all files regardless of the programming language in use. However,
a DocBlock standard for that particular language may override it.
The JavaScript DocBlock standard relates to the JavaScript code files and their inline
documentation. It is a subset of Google JavaScript Style Guide and JSDoc, which can
be found at http://usejsdoc.org.
The LESS coding standard defines the formatting and coding style when working
with LESS and CSS files.
You can read more about the actual details of each standard at
http://devdocs.magento.com, as they are too extensive to be
covered in this book.

Summary

In this chapter, we took a look at Composer, which is one of the first things that we
will interact with when installing Magento. We then moved on to service contracts
as one of the strongest Magento architectural parts, which turned out to be good
old PHP interfaces in use. Further, we covered some bits about the Magento code
generation feature. Thus, we have a basic knowledge of the Factory and Proxy
classes. We then had a look at the var directory and explored its role, especially
during development. Finally, we touched upon the coding standards used
in Magento.
In the next chapter, we will discuss the dependency injection, which is one of the
most important architectural parts of Magento.

[ 59 ]

Models and Collections
Like most modern frameworks and platforms, these days Magento embraces an
Object Relational Mapping (ORM) approach over raw SQL queries. Though
the underlying mechanism still comes down to SQL, we are now dealing strictly
with objects. This makes our application code more readable, manageable, and
isolated from vendor-specific SQL differences. Model, resource, and collection are
three types of classes working together to allow us full entity data management,
from loading, saving, deleting, and listing entities. The majority of our data access
and management will be done via PHP classes called Magento models. Models
themselves don't contain any code for communicating with the database.
The database communication part is decoupled into its own PHP class called
resource class. Each model is then assigned a resource class. Calling load, save, or
delete methods on models get delegated to resource classes, as they are the ones to
actually read, write, and delete data from the database. Theoretically, with enough
knowledge, it is possible to write new resource classes for various database vendors.
Next to the model and resource classes, we have collection classes. We can think of
a collection as an array of individual model instances. On a base level, collections
extend from the \Magento\Framework\Data\Collection class, which implements
\IteratorAggregate and \Countable from Standard PHP Library (SPL) and a few
other Magento-specific classes.
More often than not, we look at model and resource as a single unified thing, thus
simply calling it a model. Magento deals with two types of models, which we might
categorize as simple and EAV models.

[ 61 ]

Models and Collections

In this chapter, we will cover the following topics:
•

Creating a miniature module

•

Creating a simple model

•

The EAV model

•

Understanding the flow of schema and data scripts

•

Creating an install schema script (InstallSchema.php)

•

Creating an upgrade schema script (UpgradeSchema.php)

•

Creating an install data script (InstallData.php)

•

Creating an upgrade data script (UpgradeData.php)

•

Entity CRUD actions

•

Managing collections

Creating a miniature module

For the purpose of this chapter, we will create a miniature module called
Foggyline_Office.
The module will have two entities defined as follows:
•

•

Department: a simple model with the following fields:

°°

entity_id: primary key

°°

name: name of department, string value

Employee: an EAV model with the following fields and attributes:

°°

Fields:
°°

entity_id: primary key

°°

department_id: foreign key, pointing to
Department.entity_id

°°

email: unique e-mail of an employee, string value

°°

first_name: first name of an employee, string value

°°

last_name: last name of an employee, string value

[ 62 ]

Chapter 4

°°

Attributes:
°°

service_years: employee's years of service, integer value

°°

dob: employee's date of birth, date-time value

°°

salary – monthly salary, decimal value

°°

vat_number: VAT number, (short) string value

°°

note: possible note on employee, (long) string value

Every module starts with the registration.php and module.xml files. For the
purpose of our chapter module, let's create the app/code/Foggyline/Office/
registration.php file with content as follows:








We will get into more details about the structure of the module.xml file in later
chapters. Right now, we will only focus on the setup_version attribute and module
element within sequence.
The value of setup_version is important because we might use it within our
schema install script (InstallSchema.php) files, effectively turning the install
script into an update script, as we will show soon.
The sequence element is Magento's way of setting dependencies for our module.
Given that our module will make use of EAV entities, we list Magento_Eav as
a dependency.
[ 63 ]

Models and Collections

Creating a simple model

The Department entity, as per requirements, is modeled as a simple model.
We previously mentioned that whenever we talk about models, we implicitly
think of model class, resource class, and collection class forming one unit.
Let's start by first creating a model class, (partially) defined under the app/code/
Foggyline/Office/Model/Department.php file as follows:
namespace Foggyline\Office\Model;
class Department extends \Magento\Framework\Model\AbstractModel
{
protected function _construct()
{
$this-> _init('Foggyline\Office\Model
\ResourceModel\Department');
}
}

All that is happening here is that we are extending from the \Magento\Framework\
Model\AbstractModel class, and triggering the $this->_init method within _
construct passing it our resource class.
The AbstractModel further extends \Magento\Framework\Object. The fact that
our model class ultimately extends from Object means that we do not have to define
a property name on our model class. What Object does for us is that it enables us
to get, set, unset, and check for a value existence on properties magically. To give a
more robust example than name, imagine our entity has a property called employee_
average_salary in the following code:
$department->getData('employee_average_salary');
$department->getEmployeeAverageSalary();
$department->setData('employee_average_salary', 'theValue');
$department->setEmployeeAverageSalary('theValue');
$department->unsetData('employee_average_salary');
$department->unsEmployeeAverageSalary();
$department->hasData('employee_average_salary');
$department->hasEmployeeAverageSalary();

[ 64 ]

Chapter 4

The reason why this works is due to Object implementing the setData,
unsetData, getData, and magic __call methods. The beauty of the
magic __call method implementation is that it understands method
calls like getEmployeeAverageSalary, setEmployeeAverageSalary,
unsEmployeeAverageSalary, and hasEmployeeAverageSalary even if they do not
exist on the Model class. However, if we choose to implement some of these methods
within our Model class, we are free to do so and Magento will pick it up when we
call it.
This is an important aspect of Magento, sometimes confusing to newcomers.
Once we have a model class in place, we create a model resource class, (partially)
defined under the app/code/Foggyline/Office/Model/ResourceModel/
Department.php file as follows:
namespace Foggyline\Office\Model\ResourceModel;
class Department extends \Magento\Framework\Model\ResourceModel\Db\
AbstractDb
{
protected function _construct()
{
$this->_init('foggyline_office_department', 'entity_id');
}
}

Our resource class that extends from \Magento\Framework\Model\ResourceModel\
Db\AbstractDb triggers the $this->_init method call within _construct. $this>_init accepts two parameters. The first parameter is the table name foggyline_
office_department, where our model will persist its data. The second parameter is
the primary column name entity_id within that table.
AbstractDb further extends Magento\Framework\Model\ResourceModel\
AbstractResource.
The resource class is the key to communicating to the database. All it
takes is for us to name the table and its primary key and our models can
save, delete, and update entities.

[ 65 ]

Models and Collections

Finally, we create our collection class, (partially) defined under the app/code/
Foggyline/Office/Model/ResourceModel/Department/Collection.php file
as follows:
namespace Foggyline\Office\Model\ResourceModel\Department;
class Collection extends \Magento\Framework\Model\ResourceModel
\Db\Collection\AbstractCollection
{
protected function _construct()
{
$this->_init(
'Foggyline\Office\Model\Department',
'Foggyline\Office\Model\ResourceModel\Department'
);
}
}

The collection class extends from \Magento\Framework\Model\ResourceModel\
Db\Collection\AbstractCollection and, similar to the model and resource
classes, does a $this->_init method call within _construct. This time, _init
accepts two parameters. The first parameter is the full model class name Foggyline\
Office\Model\Department, and the second parameter is the full resource class
name Foggyline\Office\Model\ResourceModel\Department.
AbstractCollection implements Magento\Framework\App\ResourceConnection\
SourceProviderInterface, and extends \Magento\Framework\Data\Collection\
AbstractDb. AbstractDb further extends \Magento\Framework\Data\Collection.

It is worth taking some time to study the inners of these collection classes, as this
is our go-to place for whenever we need to deal with fetching a list of entities that
match certain search criteria.

Creating an EAV model

The Employee entity, as per requirements, is modeled as an EAV model.
Let's start by first creating an EAV model class, (partially) defined under the app/
code/Foggyline/Office/Model/Employee.php file as follows:
namespace Foggyline\Office\Model;
class Employee extends \Magento\Framework\Model\AbstractModel
{

[ 66 ]

Chapter 4
const ENTITY = 'foggyline_office_employee';
public function _construct()
{
$this-> _init('Foggyline\Office \Model
\ResourceModel\Employee');
}
}

Here, we are extending from the \Magento\Framework\Model\AbstractModel
class, which is the same as with the simple model previously described. The only
difference here is that we have an ENTITY constant defined, but this is merely
syntactical sugar for later on; it bears no meaning for the actual model class.
Next, we create an EAV model resource class, (partially) defined under the app/
code/Foggyline/Office/Model/ResourceModel/Employee.php file as follows:
namespace Foggyline\Office\Model\ResourceModel;
class Employee extends \Magento\Eav\Model\Entity\AbstractEntity
{
protected function _construct()
{
$this->_read = 'foggyline_office_employee_read';
$this->_write = 'foggyline_office_employee_write';
}
public function getEntityType()
{
if (empty($this->_type)) {
$this->setType(\Foggyline\Office\Model
\Employee::ENTITY);
}
return parent::getEntityType();
}
}

Our resource class extends from \Magento\Eav\Model\Entity\AbstractEntity,
and sets the $this->_read, $this->_write class properties through _construct.
These are freely assigned to whatever value we want, preferably following the
naming pattern of our module. The read and write connections need to be named or
else Magento produces an error when using our entities.

[ 67 ]

Models and Collections

The getEntityType method internally sets the _type value to \Foggyline\Office\
Model\Employee::ENTITY, which is the string foggyline_office_employee.
This same value is what's stored in the entity_type_code column within the eav_
entity_type table. At this point, there is no such entry in the eav_entity_type
table. This is because the install schema script will be creating one, as we will be
demonstrating soon.
Finally, we create our collection class, (partially) defined under the app/code/
Foggyline/Office/Model/ResourceModel/Employee/Collection.php file
as follows:
namespace Foggyline\Office\Model\ResourceModel\Employee;
class Collection extends \Magento\Eav\Model\Entity\Collection\
AbstractCollection
{
protected function _construct()
{
$this->_init('Foggyline\Office\Model\Employee',
'Foggyline\Office\Model\ResourceModel\Employee');
}
}

The collection class extends from \Magento\Eav\Model\Entity\Collection\
AbstractCollection and, similar to the model class, does a $this->_init method
call within _construct. _init accepts two parameters: the full model class name
Foggyline\Office\Model\Employee, and the full resource class name Foggyline\
Office\Model\ResourceModel\Employee.
AbstractCollection has the same parent tree as the simple model collection

class, but on its own it implements a lot of EAV collection-specific methods like

addAttributeToFilter, addAttributeToSelect, addAttributeToSort, and so on.
As we can see, EAV models look a lot like simple models. The
difference lies mostly in the resource class and collection class
implementations and their first level parent classes. However, we need
to keep in mind that the example given here is the simplest one possible.
If we look at the eav_entity_type table in the database, we can
see that other entity types make use of attribute_model, entity_
attribute_collection, increment_model, and so on. These are
all advanced properties we can define alongside our EAV model making
it closer to the implementation of the catalog_product entity type,
which is probably the most robust one in Magento. This type of advanced
EAV usage is out of the scope of this book as it is probably worth a book
on its own.

[ 68 ]

Chapter 4

Now that we have simple and EAV models in place, it is time to look into installing
the necessary database schema and possibly pre-fill it with some data. This is done
through schema and data scripts.

Understanding the flow of schema and
data scripts

Simply put, the role of the schema scripts is to create a database structure supporting
your module logic. For example, creating a table where our entities would persist
their data. The role of the data scripts is to manage the data within existing tables,
usually in the form of adding some sample data during module installation.
If we look a few steps back, we can notice how schema_version and data_version
from the database match the setup_version number from our module.xml file.
They all imply the same thing. If we were to now change the setup_version
number in our module.xml file and run the php bin/magento setup:upgrade
console command again, our database schema_version and data_version would
get updated to this new version number.
This is done through module's install and upgrade scripts. If we take a quick
look at the setup/src/Magento/Setup/Model/Installer.php file, we can see a
function, getSchemaDataHandler, with content as follows:
private function getSchemaDataHandler($moduleName, $type)
{
$className = str_replace('_', '\\', $moduleName) . '\Setup';
switch ($type) {
case 'schema-install':
$className .= '\InstallSchema';
$interface = self::SCHEMA_INSTALL;
break;
case 'schema-upgrade':
$className .= '\UpgradeSchema';
$interface = self::SCHEMA_UPGRADE;
break;
case 'schema-recurring':
$className .= '\Recurring';
$interface = self::SCHEMA_INSTALL;
break;
case 'data-install':
$className .= '\InstallData';
$interface = self::DATA_INSTALL;
break;
[ 69 ]

Models and Collections
case 'data-upgrade':
$className .= '\UpgradeData';
$interface = self::DATA_UPGRADE;
break;
default:
throw new \Magento\Setup\Exception("$className does
not exist");
}
return $this->createSchemaDataHandler($className, $interface);
}

This is what tells Magento which classes to pick up and run from the individual
module Setup directory. We will ignore the Recurring case for the moment, as only
the Magento_Indexer module uses it.
For the first time, we run php bin/magento setup:upgrade against our module;
while it still has no entries under the setup_module table, Magento will execute the
files within the module Setup folder in following order:
•

InstallSchema.php

•

UpgradeSchema.php

•

InstallData.php

•

UpgradeData.php

Notice that this is the same order, top to bottom, as in the getSchemaDataHandler
method.
Every subsequent upper module version number change, followed by the console

php bin/magento setup:upgrade command, would result in the following files

being run in the order as listed:
•

UpgradeSchema.php

•

UpgradeData.php

Additionally, Magento would record the upped version number under the
setup_module database. Magento will only trigger install or upgrade scripts
when the version number in the database is less than the version number in the
module.xml file.
We are not required to always provide these install or upgrade scripts, if
ever. They are only needed when we need to add or edit existing tables or
entries in a database.

[ 70 ]

Chapter 4

If we look carefully at the implementation of the install and update
methods within the appropriate scripts, we can see they both accept
ModuleContextInterface $context as a second parameter. Since upgrade
scripts are the ones triggering on every upped version number, we can use
$context->getVersion() to target changes specific to the module version.

Creating an install schema script
(InstallSchema.php)

Now that we understand the flow of schema and data scripts and their relation to the
module version number, let us go ahead and start assembling our InstallSchema.
We start by defining the app/code/Foggyline/Office/Setup/InstallSchema.php
file with (partial) content as follows:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class InstallSchema implements InstallSchemaInterface
{
public function install(SchemaSetupInterface $setup,
ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}

InstallSchema conforms to InstallSchemaInterface, which requires the
implementation of the install method that accepts two parameters of type
SchemaSetupInterface and ModuleContextInterface.

The install method is all that is required here. Within this method, we would add any
relevant code we might have to create the tables and columns we need.
Looking through the code base, we can see that Magento\Setup\Module\Setup
is the one extending \Magento\Framework\Module\Setup and implementing
SchemaSetupInterface. The two methods seen in the preceding code, startSetup
and endSetup, are used to run additional environment setup before and after
our code.
[ 71 ]

Models and Collections

Going further, let's replace the /* #snippet1 */ bit with code that will create our
Department model entity table as follows:
$table = $setup->getConnection()
->newTable($setup->getTable('foggyline_office_department'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' =>
false, 'primary' => true],
'Entity ID'
)
->addColumn(
'name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Name'
)
->setComment('Foggyline Office Department Table');
$setup->getConnection()->createTable($table);
/* #snippet2 */

Here, we are instructing Magento to create a table named foggyline_office_
department, add entity_id and name columns to it, and set the comment on the
table. Assuming we are using the MySQL server, when code executes, the following
SQL gets executed in the database:
CREATE TABLE 'foggyline_office_department' (
'entity_id' int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Entity
ID',
'name' varchar(64) DEFAULT NULL COMMENT 'Name',
PRIMARY KEY ('entity_id')
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
COMMENT='Foggyline Office Department Table';

The addColumn method is the most interesting one here. It takes five parameters,
from column name, column data type, column length, array of additional options,
and column description. However, only column name and column data type
are mandatory! Accepted column data types can be found under the Magento\
Framework\DB\Ddl\Table class, and go as follows:
boolean
float
timestamp
varbinary

smallint
numeric
datetime

integer
decimal
text

bigint
date
blob

[ 72 ]

Chapter 4

An additional options array might contain some of the following keys: unsigned,
precision, scale, unsigned, default, nullable, primary, identity,
auto_increment.
Having gained insight into the addColumn method, let's go ahead and create the
foggyline_office_employee_entity table for the Employee entity as well.
We do so by replacing the /* #snippet2 */ bit from the preceding code with
the following code:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$table = $setup->getConnection()
->newTable($setup->getTable($employeeEntity . '_entity'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' =>
false, 'primary' => true],
'Entity ID'
)
->addColumn(
'department_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false],
'Department Id'
)
->addColumn(
'email',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Email'
)
->addColumn(
'first_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'First Name'
)
->addColumn(
'last_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[ 73 ]

Models and Collections
[],
'Last Name'
)
->setComment('Foggyline Office Employee Table');
$setup->getConnection()->createTable($table);
/* #snippet3 */

Following good database design practices, we might notice one thing here. If we
agree that every employee can be assigned a single department, we should add
a foreign key to this table's department_id column. For the moment, we will
purposely skip this bit, as we want to demonstrate this through the update schema
script later on.
EAV models scatter their data across several tables, three at a minimum. The table
foggyline_office_employee_entity that we just created is one of them. The other
one is the core Magento eav_attribute table. The third table is not a single table,
rather a list of multiple tables; one for each EAV type. These tables are the result of
our install script.
Information stored within the core Magento eav_attribute table is not the value of
an attribute or anything like it; information stored there is an attribute's metadata.
So how does Magento know about our Employee attributes (service_years, dob,
salary, vat_number, note)? It does not; not yet. We need to add the attributes into
that table ourselves. We will do so later on, as we demonstrate the InstallData.
Depending on the EAV attribute data type, we need to create the following tables:
•

foggyline_office_employee_entity_datetime

•

foggyline_office_employee_entity_decimal

•

foggyline_office_employee_entity_int

•

foggyline_office_employee_entity_text

•

foggyline_office_employee_entity_varchar

The names of these attribute value tables come from a simple formula, which says
{name of the entity table}+{_}+{eav_attribute.backend_type value}. If we look at the salary
attribute, we need it to be a decimal value, thus it will get stored in foggyline_
office_employee_entity_decimal.

[ 74 ]

Chapter 4

Given the chunkiness of code behind defining attribute value tables, we will focus
only on a single, decimal type table. We define it by replacing /* #snippet3 */
from the preceding code with the following bit:
$table = $setup->getConnection()
->newTable($setup->getTable($employeeEntity .
'_entity_decimal'))
->addColumn(
'value_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'nullable' => false, 'primary'
true],
'Value ID'
)
->addColumn(
'attribute_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'default'
'0'],
'Attribute ID'
)
->addColumn(
'store_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'default'
'0'],
'Store ID'
)
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false, 'default'
'0'],
'Entity ID'
)

[ 75 ]

=>

=>

=>

=>

Models and Collections
->addColumn(
'value',
\Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL,
'12,4',
[],
'Value'
)
//->addIndex
//->addForeignKey
->setComment('Employee Decimal Attribute Backend Table');
$setup->getConnection()->createTable($table);

Notice the //->addIndex part within code above. Lets replace it with the following
bit.
->addIndex(
$setup->getIdxName(
$employeeEntity . '_entity_decimal',
['entity_id', 'attribute_id', 'store_id'],
\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_
UNIQUE
),
['entity_id', 'attribute_id', 'store_id'],
['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_
TYPE_UNIQUE]
)
->addIndex(
$setup->getIdxName($employeeEntity . '_entity_decimal',
['store_id']),
['store_id']
)
->addIndex(
$setup->getIdxName($employeeEntity . '_entity_decimal',
['attribute_id']),
['attribute_id']
)

The preceding code adds three indexes on the foggyline_office_employee_
entity_decimal table, resulting in a SQL as follows:
•

UNIQUE KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ENTT_ID_ATTR_ID_
STORE_ID' ('entity_id','attribute_id','store_id')

•

KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_ID'
('store_id')

•

KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_ATTRIBUTE_ID'
('attribute_id')
[ 76 ]

Chapter 4

Similarly, we replace the //->addForeignKey part from the preceding code with the
following bit:
->addForeignKey(
$setup->getFkName(
$employeeEntity . '_entity_decimal',
'attribute_id',
'eav_attribute',
'attribute_id'
),
'attribute_id',
$setup->getTable('eav_attribute'),
'attribute_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName(
$employeeEntity . '_entity_decimal',
'entity_id',
$employeeEntity . '_entity',
'entity_id'
),
'entity_id',
$setup->getTable($employeeEntity . '_entity'),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName($employeeEntity . '_entity_decimal',
'store_id', 'store', 'store_id'),
'store_id',
$setup->getTable('store'),
'store_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)

The preceding code adds foreign key relations into the foggyline_office_
employee_entity_decimal table, resulting in a SQL as follows:
•

CONSTRAINT 'FK_D17982EDA1846BAA1F40E30694993801' FOREIGN KEY
('entity_id') REFERENCES 'foggyline_office_employee_entity'
('entity_id') ON DELETE CASCADE,

•

CONSTRAINT 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_
ID_STORE_STORE_ID' FOREIGN KEY ('store_id') REFERENCES 'store'
('store_id') ON DELETE CASCADE,
[ 77 ]

Models and Collections

•

CONSTRAINT 'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ATTR_ID_EAV_
ATTR_ATTR_ID' FOREIGN KEY ('attribute_id') REFERENCES 'eav_
attribute' ('attribute_id') ON DELETE CASCADE

Notice how we added the store_id column to our EAV attribute value tables.
Though our examples won't find use of it, it is a good practice to use store_id
with your EAV entities to scope the data for a possible multi-store setup. To clarify
further, imagine we had a multi-store setup, and with EAV attribute tables set up
like the preceding one, we would be able to store a different attribute value for each
store, since the unique entry in the table is defined as a combination of entity_id,
attribute_id, and store_id columns.
For the reasons of performance and data integrity, it is important to define
indexes and foreign key as per good database design practice. We can do
so within InstallSchema when defining new tables.

Creating an upgrade schema script
(UpgradeSchema.php)

During the first-time module install, an upgrade schema is what gets run
immediately after an install schema. We define upgrade schema within the
app/code/Foggyline/Office/Setup/UpgradeSchema.php file with (partial)
content as follows:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\UpgradeSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class UpgradeSchema implements UpgradeSchemaInterface
{
public function upgrade(SchemaSetupInterface $setup,
ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}

[ 78 ]

Chapter 4

UpgradeSchema conforms to UpgradeSchemaInterface, which requires the
implementation of the upgrade method that accepts two parameters of type
SchemaSetupInterface and ModuleContextInterface.

This is quite similar to InstallSchemaInterface, except the method name.
The update method is run when this schema gets triggered. Within this method,
we would add any relevant code we might want to execute.
Going further, let's replace the /* #snippet1 */ part from the preceding code with
the following code:
$employeeEntityTable = \Foggyline\Office\Model\Employee::ENTITY. '_
entity';
$departmentEntityTable = 'foggyline_office_department';
$setup->getConnection()
->addForeignKey(
$setup->getFkName($employeeEntityTable, 'department_id',
$departmentEntityTable, 'entity_id'),
$setup->getTable($employeeEntityTable),
'department_id',
$setup->getTable($departmentEntityTable),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
);

Here, we are instructing Magento to create a foreign key on the foggyline_office_
employee_entity table, more precisely on its department_id column, pointing to
the foggyline_office_department table and its entity_id column.

Creating an install data script
(InstallData.php)

An install data script is what gets run immediately after upgrade schema. We define
install data schema within the app/code/Foggyline/Office/Setup/InstallData.
php file with (partial) content as follows:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

[ 79 ]

Models and Collections
class InstallData implements InstallDataInterface
{
private $employeeSetupFactory;
public function __construct(
\Foggyline\Office\Setup\EmployeeSetupFactory
$employeeSetupFactory
)
{
$this->employeeSetupFactory = $employeeSetupFactory;
}
public function install(ModuleDataSetupInterface $setup,
ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}

InstallData conforms to InstallDataInterface, which requires the
implementation of the install method that accepts two parameters of type
ModuleDataSetupInterface and ModuleContextInterface.

The install method is run when this script gets triggered. Within this method, we
would add any relevant code we might want to execute.
Going further, let's replace the /* #snippet1 */ part from the preceding code with
the following code:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$employeeSetup = $this->employeeSetupFactory->create(['setup' =>
$setup]);
$employeeSetup->installEntities();
$employeeSetup->addAttribute(
$employeeEntity, 'service_years', ['type' => 'int']
);
$employeeSetup->addAttribute(
$employeeEntity, 'dob', ['type' => 'datetime']
);
[ 80 ]

Chapter 4
$employeeSetup->addAttribute(
$employeeEntity, 'salary', ['type' => 'decimal']
);
$employeeSetup->addAttribute(
$employeeEntity, 'vat_number', ['type' => 'varchar']
);
$employeeSetup->addAttribute(
$employeeEntity, 'note', ['type' => 'text']
);

Using the addAttribute method on the instance of \Foggyline\Office\Setup\
EmployeeSetupFactory, we are instructing Magento to add a number of attributes
(service_years, dob, salary, vat_number, note) to its entity.
We will soon get to the inners of EmployeeSetupFactory, but right now notice the
call to the addAttribute method. Within this method, there is a call to the $this>attributeMapper->map($attr, $entityTypeId) method. attributeMapper
conforms to Magento\Eav\Model\Entity\Setup\PropertyMapperInterface,
which looking at vendor/magento/module-eav/etc/di.xml has a preference for
the Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite class, which
further initializes the following mapper classes:
•

Magento\Eav\Model\Entity\Setup\PropertyMapper

•

Magento\Customer\Model\ResourceModel\Setup\PropertyMapper

•
•

Magento\Catalog\Model\ResourceModel\Setup\PropertyMapper
Magento\ConfigurableProduct\Model\ResourceModel\Setup\
PropertyMapper

Since we are defining our own entity types, the mapper class we are mostly
interested in is Magento\Eav\Model\Entity\Setup\PropertyMapper. A quick
look inside of it reveals the following mapping array in the map method:
[
'backend_model' => 'backend',
'backend_type' => 'type',
'backend_table' => 'table',
'frontend_model' => 'frontend',
'frontend_input' => 'input',
'frontend_label' => 'label',
'frontend_class' => 'frontend_class',
'source_model' => 'source',
'is_required' => 'required',
[ 81 ]

Models and Collections
'is_user_defined' => 'user_defined',
'default_value' => 'default',
'is_unique' => 'unique',
'note' => 'note'
'is_global' => 'global'
]

Looking at the preceding array keys and value strings gives us a clue as to what is
happening. The key strings match the column names in the eav_attribute table,
while the value strings match the keys of our array passed to the addAttribute
method within InstallData.php.
Let's take a look at the EmployeeSetupFactory class within the app/code/
Foggyline/Office/Setup/EmployeeSetup.php file, (partially) defined as follows:
namespace Foggyline\Office\Setup;
use Magento\Eav\Setup\EavSetup;
class EmployeeSetup extends EavSetup
{
public function getDefaultEntities()
{
/* #snippet1 */
}
}

What's happening here is that we are extending from the Magento\Eav\Setup\
EavSetup class, thus effectively telling Magento we are about to create our own
entity. We do so by overriding getDefaultEntities, replacing /* #snippet1 */
with content as follows:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$entities = [
$employeeEntity => [
'entity_model' => 'Foggyline\Office\Model\ResourceModel\
Employee',
'table' => $employeeEntity . '_entity',
'attributes' => [
'department_id' => [
'type' => 'static',
],
'email' => [
'type' => 'static',
],

[ 82 ]

Chapter 4
'first_name' => [
'type' => 'static',
],
'last_name' => [
'type' => 'static',
],
],
],
];
return $entities;

The getDefaultEntities method returns an array of entities we want to register
with Magento. Within our $entities array, the key $employeeEntity becomes an
entry in the eav_entity_type table. Given that our $employeeEntity has a value of
foggyline_office_employee, running the following SQL query should yield
a result:
SELECT * FROM eav_entity_type WHERE entity_type_code =
"foggyline_office_employee";

Only a handful of metadata values are required to make our new entity type
functional. The entity_model value should point to our EAV model resource class,
not the model class. The table value should equal the name of our EAV entity table
in the database. Finally, the attributes array should list any attribute we want created
on this entity. Attributes and their metadata get created in the eav_attribute table.
If we look back at all those foggyline_office_employee_entity_* attribute value
tables we created, they are not the ones that actually create attributes or register a
new entity type in Magento. What creates attributes and a new entity type is the
array we just defined under the getDefaultEntities method. Once Magento
creates the attributes and registers a new entity type, it simply routes the entity
save process to proper attribute value tables depending on the type of attribute.

Creating an upgrade data script
(UpgradeData.php)

The upgrade data script is the last one to execute. We will use it to demonstrate the
example of creating the sample entries for our Department and Employee entities.

[ 83 ]

Models and Collections

We start by creating the app/code/Foggyline/Office/Setup/UpgradeData.php
file with (partial) content as follows:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\UpgradeDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class UpgradeData implements UpgradeDataInterface
{
protected $departmentFactory;
protected $employeeFactory;
public function __construct(
\Foggyline\Office\Model\DepartmentFactory
$departmentFactory,
\Foggyline\Office\Model\EmployeeFactory $employeeFactory
)
{
$this->departmentFactory = $departmentFactory;
$this->employeeFactory = $employeeFactory;
}
public function upgrade(ModuleDataSetupInterface $setup,
ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}

UpgradeData conforms to UpgradeDataInterface, which requires the
implementation of the upgrade method that accepts two parameters of type
ModuleDataSetupInterface and ModuleContextInterface. We are further adding
our own __construct method to which we are passing DepartmentFactory and
EmployeeFactory, as we will be using them within the upgrade method as shown
next, by replacing /* #snippet1 */ with the following code:
$salesDepartment = $this->departmentFactory->create();
$salesDepartment->setName('Sales');
$salesDepartment->save();

[ 84 ]

Chapter 4
$employee = $this->employeeFactory->create();
$employee->setDepartmentId($salesDepartment->getId());
$employee->setEmail('john@sales.loc');
$employee->setFirstName('John');
$employee->setLastName('Doe');
$employee->setServiceYears(3);
$employee->setDob('1983-03-28');
$employee->setSalary(3800.00);
$employee->setVatNumber('GB123456789');
$employee->setNote('Just some notes about John');
$employee->save();

The preceding code creates an instance of the department entity and then saves it.
An instance of employee is then created and saved, passing it the newly created
department ID and other attributes.
A more convenient and professional-looking approach for saving
an entity could be given as follows:
$employee->setDob('1983-03-28')
->setSalary(3800.00)
->setVatNumber('GB123456789')
->save();

Here, we are utilizing the fact that each of the entity setter
methods returns $this (an instance of the entity object itself), so
we can chain the method calls.

Entity CRUD actions

Up to this point, we have learned how to create a simple model, an EAV model, and
install and upgrade types of schema and data script. Now, let us see how we can
create, read, update and delete our entities, operations that are commonly referred to
as CRUD.
Though this chapter is about models, collections, and related things, for the purpose
of demonstration, let's make a tiny detour into routes and controllers. The idea is to
create a simple Test controller with the Crud action we can trigger in the browser via
a URL. Within this Crud action, we will then dump our CRUD-related code.

[ 85 ]

Models and Collections

To make Magento respond to the URL we punch into the browser, we need to define
the route. We do so by creating the app/code/Foggyline/Office/etc/frontend/
routes.xml file with the following content:








Route definition requires a unique ID and frontName attribute values, which in our
case both equal foggyline_office. The frontName attribute value becomes the part
of our URL structure. Simply put, the URL formula for hitting the Crud action goes
like {magento-base-url}/index.php/{route frontName}/{controller name}/{action name}.
For example, if our base URL were http://shop.loc/, the full URL
would be http://shop.loc/index.php/foggyline_office/
test/crud/. If we have URL rewrites turned on, we could omit the
index.php part.

Once the route has been defined, we can go ahead and create the Test controller,
defined in the app/code/Foggyline/Office/Controller/Test.php file with
(partial) code as follows:
namespace Foggyline\Office\Controller;
abstract class Test extends \Magento\Framework\App\Action\Action
{
}

[ 86 ]

Chapter 4

This really is the simplest controller we could have defined. The only thing worth
noting here is that the controller class needs to be defined as abstract and extend the
\Magento\Framework\App\Action\Action class. Controller actions live outside of
the controller itself and can be found under the subdirectory on the same level and
named as controller. Since our controller is called Test, we place our Crud action
under the app/code/Foggyline/Office/Controller/Test/Crud.php file with
content as follows:
namespace Foggyline\Office\Controller\Test;
class Crud extends \Foggyline\Office\Controller\Test
{
protected $employeeFactory;
protected $departmentFactory;
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Foggyline\Office\Model\EmployeeFactory $employeeFactory,
\Foggyline\Office\Model\DepartmentFactory
$departmentFactory
)
{
$this->employeeFactory = $employeeFactory;
$this->departmentFactory = $departmentFactory;
return parent::__construct($context);
}
public function execute()
{
/* CRUD Code Here */
}
}

The Controller action class is basically just an extension of the controller defining
the execute method. Code within the execute method is what gets run when
we hit the URL in the browser. Additionally, we have a __construct method to
which we are passing the EmployeeFactory and DepartmentFactory classes,
which we will soon use for our CRUD examples. Note that EmployeeFactory and
DepartmentFactory are not classes created by us. Magento will autogenerate them
under the DepartmentFactory.php and EmployeeFactory.php files within the
var/generation/Foggyline/Office/Model folder. These are factory classes for
our Employee and Department model classes, generated when requested.
With this, we finish our little detour and focus back on our entities.
[ 87 ]

Models and Collections

Creating new entities

There are three different flavors, if we might call them that, by which we can set
property (field and attribute) values on our entity. They all lead to the same result.
The following few code snippets can be copied and pasted into our Crud class
execute method for testing, simply by replacing /* CRUD Code Here */ with
one of the following code snippets:
//Simple model, creating new entities, flavour #1
$department1 = $this->departmentFactory->create();
$department1->setName('Finance');
$department1->save();
//Simple model, creating new entities, flavour #2
$department2 = $this->departmentFactory->create();
$department2->setData('name', 'Research');
$department2->save();
//Simple model, creating new entities, flavour #3
$department3 = $this->departmentFactory->create();
$department3->setData(['name' => 'Support']);
$department3->save();

The flavour #1 approach from the preceding code is probably the preferred way
of setting properties, as it is using the magic method approach we mentioned
previously. Both flavour #2 and flavour #3 use the setData method, just in a
slightly different manner. All three examples should yield the same result once the
save method is called on an object instance.
Now that we know how to save the simple model, let's take a quick look at doing the
same with the EAV model. The following are analogous code snippets:
//EAV model, creating new entities, flavour #1
$employee1 = $this->employeeFactory->create();
$employee1->setDepartment_id($department1->getId());
$employee1->setEmail('goran@mail.loc');
$employee1->setFirstName('Goran');
$employee1->setLastName('Gorvat');
$employee1->setServiceYears(3);
$employee1->setDob('1984-04-18');
$employee1->setSalary(3800.00);
$employee1->setVatNumber('GB123451234');
$employee1->setNote('Note #1');
$employee1->save();

[ 88 ]

Chapter 4
//EAV model, creating new entities, flavour #2
$employee2 = $this->employeeFactory->create();
$employee2->setData('department_id', $department2->getId());
$employee2->setData('email', 'marko@mail.loc');
$employee2->setData('first_name', 'Marko');
$employee2->setData('last_name', 'Tunukovic');
$employee2->setData('service_years', 3);
$employee2->setData('dob', '1984-04-18');
$employee2->setData('salary', 3800.00);
$employee2->setData('vat_number', 'GB123451234');
$employee2->setData('note', 'Note #2');
$employee2->save();
//EAV model, creating new entities, flavour #3
$employee3 = $this->employeeFactory->create();
$employee3->setData([
'department_id' => $department3->getId(),
'email' => 'ivan@mail.loc',
'first_name' => 'Ivan',
'last_name' => 'Telebar',
'service_years' => 2,
'dob' => '1986-08-22',
'salary' => 2400.00,
'vat_number' => 'GB123454321',
'note' => 'Note #3'
]);
$employee3->save();

As we can see, the EAV code for persisting the data is identical to the simple model.
There is one thing here worth noting. The Employee entity has a relation defined
toward department. Forgetting to specify department_id on a new employee entity
save would result in an error message similar to the following:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add
or update a child row: a foreign key constraint fails
('magento'.'foggyline_office_employee_entity', CONSTRAINT
'FK_E2AEE8BF21518DFA8F02B4E95DC9F5AD' FOREIGN KEY
('department_id') REFERENCES 'foggyline_office_department'
('entity_id') ON), query was: INSERT INTO
'foggyline_office_employee_entity' ('email', 'first_name',
'last_name', 'entity_id') VALUES (?, ?, ?, ?)

Magento saves these types of errors under its var/report directory.

[ 89 ]

Models and Collections

Reading existing entities

Reading an entity based on a provided entity ID value comes down to instantiating
the entity and using the load method to which we pass the entity ID as shown next:
//Simple model, reading existing entities
$department = $this->departmentFactory->create();
$department->load(28);
/*
\Zend_Debug::dump($department->toArray());
array(2) {
["entity_id"] => string(2) "28"
["name"] => string(8) "Research"
}
*/

There is no real difference between loading the simple model or EAV model, as
shown in the following EAV model example:
//EAV model, reading existing entities
$employee = $this->employeeFactory->create();
$employee->load(25);
/*
\Zend_Debug::dump($employee->toArray());
array(10) {
["entity_id"] => string(2) "25"
["department_id"] => string(2) "28"
["email"] => string(14) "marko@mail.loc"
["first_name"] => string(5) "Marko"
["last_name"] => string(9) "Tunukovic"
["dob"] => string(19) "1984-04-18 00:00:00"
["note"] => string(7) "Note #2"
["salary"] => string(9) "3800.0000"
["service_years"] => string(1) "3"
["vat_number"] => string(11) "GB123451234"
}
*/

Notice how the EAV entity loads all of its field and attribute values, which is not
always the case when we obtain the entity through EAV collection, as we will show
later on.
[ 90 ]

Chapter 4

Updating existing entities

Updating entities comes down to using the load method to read an existing entity,
reset its value, and calling the save method in the end, like shown in the following
example:
$department = $this->departmentFactory->create();
$department->load(28);
$department->setName('Finance #2');
$department->save();

Regardless of the entity being the simple model or an EAV, the code is the same.

Deleting existing entities

Calling the delete method on a loaded entity will delete the entity from the
database or throw Exception if it fails. Code to delete the entity looks as follows:
$employee = $this->employeeFactory->create();
$employee->load(25);
$employee->delete();

There is no difference in deleting the simple and EAV entities. We should always use
try/catch blocks when deleting or saving our entities.

Managing collections

Let's start with EAV model collections. We can instantiate the collection either
through the entity factory class like follows:
$collection = $this->employeeFactory->create()
->getCollection();

Or we can use object manager to instantiate the collection as shown next:
$collection = $this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection's
);

There is also a third way, which might be the preferred one, but it requires us to
define APIs so we will skip that one for the moment.

[ 91 ]

Models and Collections

Once we instantiate the collection object, we can loop through it and do some
variable dumps to see the content on individual $employee entities, like shown next:
foreach ($collection as $employee) {
\Zend_Debug::dump($employee->toArray(), '$employee');
}

The preceding would yield results like the following:
$employee array(5) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
}

Notice how the individual $employee only has fields on it, not the attributes.
Let's see what happens when we want to extend our collection by using
addAttributeToSelect to specify the individual attributes to add to it,
like shown next:
$collection->addAttributeToSelect('salary')
->addAttributeToSelect('vat_number');

The preceding would yield results like the following:
$employee array(7) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
["salary"] => string(9) "3800.0000"
["vat_number"] => string(11) "GB123451234"
}

Though we are making progress, imagine if we had tens of attributes, and we want
each and every one to be included into collection. Using addAttributeToSelect
numerous times would make for cluttered code. What we can do is pass '*' as a
parameter to addAttributeToSelect and have collection pick up every attribute,
as shown next:
$collection->addAttributeToSelect('*');

[ 92 ]

Chapter 4

This would yield results like the following:
$employee array(10) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
["dob"] => string(19) "1984-04-18 00:00:00"
["note"] => string(7) "Note #1"
["salary"] => string(9) "3800.0000"
["service_years"] => string(1) "3"
["vat_number"] => string(11) "GB123451234"
}

Though the PHP part of the code looks seemingly simple, what's happening in
the background on the SQL layer is relatively complex. Though Magento executes
several SQL queries prior to fetching the final collection result, let's focus on the last
three queries as shown next:
SELECT COUNT(*) FROM 'foggyline_office_employee_entity' AS 'e'
SELECT 'e'.* FROM 'foggyline_office_employee_entity' AS 'e'
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM 'foggyline_office_employee_entity_datetime'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('349'))
UNION ALL SELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.'
attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM 'foggyline_office_employee_entity_text'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN
('352'))
UNION ALL SELECT
'foggyline_office_employee_entity_decimal'.'
entity_id',
'foggyline_office_employee_entity_decimal'.'
attribute_id',
'foggyline_office_employee_entity_decimal'.'value'
FROM 'foggyline_office_employee_entity_decimal'
[ 93 ]

Models and Collections
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN
('350'))
UNION ALL SELECT
'foggyline_office_employee_entity_int'.'entity_id',
'foggyline_office_employee_entity_int'.'attribute_id',
'foggyline_office_employee_entity_int'.'value'
FROM 'foggyline_office_employee_entity_int'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN
('348'))
UNION ALL SELECT
'foggyline_office_employee_entity_varchar'.'
entity_id',
'foggyline_office_employee_entity_varchar'.'
attribute_id',
'foggyline_office_employee_entity_varchar'.'value'
FROM 'foggyline_office_employee_entity_varchar'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN
('351'))

Before we proceed any further, it is important to know that these queries
are not copy and paste applicable. The reason is that the attribute_id
values will for sure differ from installation to installation. Queries given
here are for us to gain a high-level understanding of what is happening
in the backend on the SQL layer when we use Magento collections on the
PHP application level.

The first query select simply counts the number of entries in the entity table, and
then passes that info to the application layer. The second select fetches all entries
from foggyline_office_employee_entity, then passes that info to the application
layer to use it to pass entity IDs in the third query as part of entity_id IN (24,
25, 26). Second and third queries here can be pretty resource intense if we
have a large amount of entries in our entity and EAV tables. To prevent possible
performance bottlenecks, we should always use the setPageSize and setCurPage
methods on collection, like shown next:
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(5);

This would result in the first COUNT query still being the same, but the second query
would now look like the following:
SELECT 'e'.* FROM 'foggyline_office_employee_entity' AS 'e' LIMIT
25 OFFSET 4
[ 94 ]

Chapter 4

This makes for a much smaller, thus performance-lighter dataset if we have
thousands or tens of thousands of entries. The point here is to always use
setPageSize and setCurPage. If we need to work with a really large set,
then we need to page through it, or walk through it.
Now we know how to limit the size of the result set and fetch the proper page,
let's see how we can further filter the set to avoid overusing PHP loops for the same
purpose. Thus effectively passing the filtering to the database and not the application
layer. To filter the EAV collection, we use its addAttributeToFilter method.
Let's instantiate a clean new collection like shown next:
$collection = $this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection'
);
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(1);
$collection->addAttributeToFilter('email',
array('like'=>'%mail.loc%'))
->addAttributeToFilter('vat_number',
array('like'=>'GB%'))
->addAttributeToFilter('salary', array('gt'=>2400))
->addAttributeToFilter('service_years',
array('lt'=>10));

Notice that we are now using the addAttributeToSelect and
addAttributeToFilter methods on collection. We have already seen the database
impact of addAttributeToSelect on a SQL query. What addAttributeToFilter
does is something completely different.
With the addAttributeToFilter method, the count query now gets transformed
into the following SQL query:
SELECT COUNT(*)
FROM 'foggyline_office_employee_entity' AS 'e'
INNER JOIN 'foggyline_office_employee_entity_varchar' AS
'at_vat_number'
ON ('at_vat_number'.'entity_id' = 'e'.'entity_id') AND
('at_vat_number'.'attribute_id' = '351')
INNER JOIN 'foggyline_office_employee_entity_decimal' AS
'at_salary'
ON ('at_salary'.'entity_id' = 'e'.'entity_id') AND
('at_salary'.'attribute_id' = '350')
[ 95 ]

Models and Collections
INNER JOIN 'foggyline_office_employee_entity_int' AS
'at_service_years'
ON ('at_service_years'.'entity_id' = 'e'.'entity_id') AND
('at_service_years'.'attribute_id' = '348')
WHERE ('e'.'email' LIKE '%mail.loc%') AND (at_vat_number.value
LIKE 'GB%') AND (at_salary.value > 2400) AND
(at_service_years.value < 10)

We can see that this is much more complex than the previous count query, now we
have INNER JOIN stepping in. Notice how we have four addAttributeToFilter
method calls but only three INNER JOIN. This is because one of those four calls
is for e-mail, which is not an attribute but a field within the foggyline_office_
employee_entity table. That is why there is no need for INNER JOIN as the field is
already there. The three INNER JOIN then simply merge the required info into the
query in order to get the select.
The second query also becomes more robust, as shown next:
SELECT
'e'.*,
'at_vat_number'.'value'
AS 'vat_number',
'at_salary'.'value'
AS 'salary',
'at_service_years'.'value' AS 'service_years'
FROM 'foggyline_office_employee_entity' AS 'e'
INNER JOIN 'foggyline_office_employee_entity_varchar' AS
'at_vat_number'
ON ('at_vat_number'.'entity_id' = 'e'.'entity_id') AND
('at_vat_number'.'attribute_id' = '351')
INNER JOIN 'foggyline_office_employee_entity_decimal' AS
'at_salary'
ON ('at_salary'.'entity_id' = 'e'.'entity_id') AND
('at_salary'.'attribute_id' = '350')
INNER JOIN 'foggyline_office_employee_entity_int' AS
'at_service_years'
ON ('at_service_years'.'entity_id' = 'e'.'entity_id') AND
('at_service_years'.'attribute_id' = '348')
WHERE ('e'.'email' LIKE '%mail.loc%') AND (at_vat_number.value
LIKE 'GB%') AND (at_salary.value > 2400) AND
(at_service_years.value < 10)
LIMIT 25

Here, we also see the usage of INNER JOIN. We also have three and not four INNER
JOIN, because one of the conditions is done against email, which is a field. The result
of the query is a flattened piece of rows where the attributes vat_number, salary,
and service_years are present. We can imagine the performance impact if we
haven't used setPageSize to limit the result set.
[ 96 ]

Chapter 4

Finally, the third query is also affected and now looks similar to the following:
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM 'foggyline_office_employee_entity_datetime'
WHERE (entity_id IN (24, 25)) AND (attribute_id IN ('349'))
UNION ALL SELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.'
attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM 'foggyline_office_employee_entity_text'
WHERE (entity_id IN (24, 25)) AND (attribute_id IN
('352'))

Notice here how UNION ALL has been reduced to a single occurrence now, thus
effectively making for two selects. This is because we have a total of five attributes
(service_years, dob, salary, vat_number, note), and three of them have been
pulled in through second query. Out of the preceding three queries demonstrated,
Magento basically pulls the collection data from second and third query. This
seems like a pretty optimized and scalable solution, though we should really give
it some thought on the proper use of setPageSize, addAttributeToSelect, and
addAttributeToFilter methods when creating collection.
During development, if working with collections that have lot of attributes, filters,
and possibly a future large dataset, we might want to use SQL logging to record
actual SQL queries hitting the database server. This might help us spot possible
performance bottlenecks and react on time, either by adding more limiting values
to setPageSize or addAttributeToSelect, or both.
In the preceding examples, the use of addAttributeToSelect results in AND
conditions on the SQL layer. What if we want to filter collection using OR conditions?
addAttributeToSelect can also result in SQL OR conditions if the $attribute
parameter is used in the following way:
$collection->addAttributeToFilter([
['attribute'=>'salary', 'gt'=>2400],
['attribute'=>'vat_number', 'like'=>'GB%']
]);

Without going into the details of actual SQL queries this time, it is suffice to say
that they are near identical to the previous example with the AND condition use of
addAttributeToFilter.
[ 97 ]

Models and Collections

Using collection methods like addExpressionAttributeToSelect,
groupByAttribute, and addAttributeToSort, collections offer further gradient
filtering and even shift some calculations from the PHP application layer to the SQL
layer. Getting into the ins and outs of those and other collection methods is beyond
the scope of this chapter, and would probably require a book on its own.

Collection filters

Looking back at the preceding addAttributeToFilter method call examples,
questions pop out as to where can we see the list of all available collection filters.
If we take a quick look inside the vendor/magento/framework/DB/Adapter/Pdo/
Mysql.php file, we can see the method called prepareSqlCondition (partially)
defined as follows:
public function prepareSqlCondition($fieldName, $condition)
{
$conditionKeyMap = [
'eq'
=> "{{fieldName}} = ?",
'neq'
=> "{{fieldName}} != ?",
'like'
=> "{{fieldName}} LIKE ?",
'nlike'
=> "{{fieldName}} NOT LIKE ?",
'in'
=> "{{fieldName}} IN(?)",
'nin'
=> "{{fieldName}} NOT IN(?)",
'is'
=> "{{fieldName}} IS ?",
'notnull'
=> "{{fieldName}} IS NOT NULL",
'null'
=> "{{fieldName}} IS NULL",
'gt'
=> "{{fieldName}} > ?",
'lt'
=> "{{fieldName}} /* AJZELE */ < ?",
'gteq'
=> "{{fieldName}} >= ?",
'lteq'
=> "{{fieldName}} <= ?",
'finset'
=> "FIND_IN_SET(?, {{fieldName}})",
'regexp'
=> "{{fieldName}} REGEXP ?",
'from'
=> "{{fieldName}} >= ?",
'to'
=> "{{fieldName}} <= ?",
'seq'
=> null,
'sneq'
=> null,
'ntoa'
=> "INET_NTOA({{fieldName}}) LIKE ?",
];
$query = '';
if (is_array($condition)) {
$key = key(array_intersect_key($condition,
$conditionKeyMap));
...
}

[ 98 ]

Chapter 4

This method is what eventually gets called at some point during SQL query
construction. The $condition parameter is expected to have one of the following
(partially listed) forms:
•

array("from" => $fromValue, "to" => $toValue)

•

array("eq" => $equalValue)

•

array("neq" => $notEqualValue)

•

array("like" => $likeValue)

•

array("in" => array($inValues))

•

array("nin" => array($notInValues))

•

array("notnull" => $valueIsNotNull)

•

array("null" => $valueIsNull)

•

array("gt" => $greaterValue)

•

array("lt" => $lessValue)

•

array("gteq" => $greaterOrEqualValue)

•

array("lteq" => $lessOrEqualValue)

•

array("finset" => $valueInSet)

•

array("regexp" => $regularExpression)

•

array("seq" => $stringValue)

•

array("sneq" => $stringValue)

If $condition is passed as an integer or string, then the exact value will be
filtered ('eq' condition). If none of the conditions is matched, then a sequential
array is expected as a parameter and OR conditions will be built using the
preceding structure.
The preceding examples covered EAV model collections, as they are
slightly more complex. Though the approach to filtering more or less
applies to simple model collections as well, the most notable difference is
that there are no addAttributeToFilter, addAttributeToSelect, and
addExpressionAttributeToSelect methods. The simple model collections make
use of addFieldToFilter, addFieldToSelect, and addExpressionFieldToSelect,
among other subtle differences.

[ 99 ]

Models and Collections

Summary

In this chapter, we first learned how to create simple model, its resource, and
collection class. Then we did the same for an EAV model. Once we had the required
model, resource, and collection classes in place, we took a detailed look at the type
and flow of schema and data scripts. Going hands-on, we covered InstallSchema,
UpgradeSchema, InstallData, and UpgradeData scripts. Once the scripts were
run, the database ended up having the required tables and sample data upon which
we based our entity CRUD examples. Finally, we took a quick but focused look at
collection management, mostly comprising filtering collection to get the desired
result set.
The full module code can be downloaded from https://github.com/ajzele/
B05032-Foggyline_Office.

[ 100 ]

Using the Dependency
Injection
Dependency injection is a software design pattern via which one or more
dependencies are injected or passed by reference into an object. What this exactly
means on a practical level is shown in the following two simple examples:
public function getTotalCustomers()
{
$database = new \PDO( … );
$statement = $database->query('SELECT …');
return $statement->fetchColumn();
}

Here, you will see a simplified PHP example, where the $database object is
created in the getTotalCustomers method. This means that the dependency on
the database object is being locked in an object instance method. This makes for
tight coupling, which has several disadvantages such as reduced reusability and a
possible system-wide effect caused by changes made to some parts of the code.
A solution to this problem is to avoid methods with these sorts of dependencies by
injecting a dependency into a method, as follows:
public function getTotalCustomers($database)
{
$statement = $database->query('SELECT ...');
return $statement->fetchColumn();
}

Here, a $database object is passed (injected) into a method. That's all that
dependency injection is—a simple concept that makes code loosely coupled. While
the concept is simple, it may not be easy to implement it across large platforms such
as Magento.
[ 101 ]

Using the Dependency Injection

Magento has its own object manager and dependency injection mechanism that we
will soon look at in detail in the following sections:
•

The object manager

•

Dependency injection

•

Configuring class preferences

•

Using virtual types
To follow and test the code examples given in the following sections, we
can use the code available at https://github.com/ajzele/B05032Foggyline_Di. To install it, we simply need to download it and put it in
the app/code/Foggyline/Di directory. Then, run the following set of
commands on the console within Magento's root directory:
php bin/magento module:enable Foggyline_Di
php bin/magento setup:upgrade
php bin/magento foggy:di

The last command can be used repeatedly when testing the snippets
presented in the following section. When php bin/magento
foggy:di is run, it will run the code within the execute method in
the DiTestCommand class. Therefore, we can use the __construct
and execute methods from within the DiTestCommand class and the
di.xml file itself as a playground for DI.

The object manager

The initializing of objects in Magento is done via what is called the object
manager. The object manager itself is an instance of the Magento\Framework\
ObjectManager\ObjectManager class that implements the Magento\Framework\
ObjectManagerInterface class. The ObjectManager class defines the following
three methods:
•

create($type, array $arguments = []): This creates a new

object instance

•

get($type): This retrieves a cached object instance

•

configure(array $configuration): This configures the di instance

[ 102 ]

Chapter 5

The object manager can instantiate a PHP class, which can be a model,
helper, or block object. Unless the class that we are working with has already
received an instance of the object manager, we can receive it by passing
ObjectManagerInterface into the class constructor, as follows:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager
)
{
$this->_objectManager = $objectManager;
}

Usually, we don't have to take care of the constructor parameter's order in Magento.
The following example will also enable us to fetch an instance of the object manager:
public function __construct(
$var1,
\Magento\Framework\ObjectManagerInterface $objectManager,
$var2 = []
)
{
$this->_objectManager = $objectManager;
}

Though we can still use plain old PHP to instantiate an object such as $object =
new \Foggyline\Di\Model\Object(), by using the object manager, we can take
advantage of Magento's advanced object features such as automatic constructor
dependency injection and object proxying.
Here are a few examples of using object manager's create method to create
new objects:
$this->_objectManager->create('Magento\Sales\Model\Order')
$this->_objectManager->create('Magento\Catalog\Model\Product\Image')
$this->_objectManager->create('Magento\Framework\UrlInterface')
$this->_objectManager->create('SoapServer', ['wsdl' => $url, 'options'
=> $options])

The following are a few examples of using object manager's get method to create
new objects:
$this->_objectManager->get('Magento\Checkout\Model\Session')
$this->_objectManager->get('Psr\Log\LoggerInterface')->critical($e)
$this->_objectManager->get('Magento\Framework\Escaper')
$this->_objectManager->get('Magento\Sitemap\Helper\Data')

[ 103 ]

Using the Dependency Injection

The object manager's create method always returns a new object instance, while the
get method returns a singleton.
Note how some of the string parameters passed to create and get are actually
interface names and not strictly class names. We will soon see why this works
with both class names and interface names. For now, it suffices to say that it
works because of Magento's dependency injection implementation.

Dependency injection

Until now, we have seen how the object manager has control over the instantiation
of dependencies. However, by convention, the object manager isn't supposed to
be used directly in Magento. Rather, it should be used for system-level things that
bootstrap Magento. We are encouraged to use the module's etc/di.xml file to
instantiate objects.
Let's dissect one of the existing di.xml entries, such as the one found under the
vendor/magento/module-admin-notification/etc/adminhtml/di.xml file for
the Magento\Framework\Notification\MessageList type:




Magento\AdminNotification\Model\System
\Message\Baseurl

Magento\AdminNotification\Model\System\
Message\Security

Magento\AdminNotification\Model\System\
Message\CacheOutdated
Magento\AdminNotification\Model\
System\Message\Media\Synchronization\Error
Magento\AdminNotification\Model\
System\Message\Media\Synchronization\Success




[ 104 ]

Chapter 5

Basically, what this means is that whenever an instance of Magento\Framework\
Notification\MessageList is being created, the messages parameter is passed
on to the constructor. The messages parameter is being defined as an array, which
further consists of other string type items. In this case, values of these string type
attributes are class names, as follows:
•

Magento\Framework\ObjectManager\ObjectManager

•

Magento\AdminNotification\Model\System\Message\Baseurl

•

Magento\AdminNotification\Model\System\Message\Security

•
•

Magento\AdminNotification\Model\System\Message\CacheOutdated

•

Magento\AdminNotification\Model\System\Message\Media\
Synchronization\Success

Magento\AdminNotification\Model\System\Message\Media\
Synchronization\Error

If you now take a look at the constructor of MessageList, you will see that it is
defined in the following way:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$messages = []
)
{
//Method body here...
}

If we modify the MessageList constructor as follows, the code will work:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$someVarX = 'someDefaultValueX',
$messages = []
)
{
//Method body here...
}

After modification:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$someVarX = 'someDefaultValueX',
$messages = [],

[ 105 ]

Using the Dependency Injection
$someVarY = 'someDefaultValueY'
)
{
//Method body here...
}

However, if we change the MessageList constructor to one of the following
variations, the code will fail to work:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$Messages = []
)
{
//Method body here...
}

Another variation is as follows:
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$_messages = []
)
{
//Method body here...
}

The name of the $messages parameter in the constructor of the PHP class has to
exactly match the name of the argument within the arguments' list of di.xml.
The order of parameters in the constructor does not really matter as much as
their naming.
Looking further in the MessageList constructor, if we execute func_get_args
somewhere within it, the list of items within the $messages parameter will match
and exceed the one shown in vendor/magento/module-admin-notification/etc/
adminhtml/di.xml. This is so because the list is not final, as Magento collects the DI
definitions from across entire the platform and merges them. So, if another module is
modifying the MessageList type, the modifications will be reflected.
If we perform a string search within all the di.xml files across the entire Magento
code base for ,
this will yield some additional di.xml files that have their own additions to the
MessageList type, as follows:
//vendor/magento/module-indexer/etc/adminhtml/di.xml

[ 106 ]

Chapter 5


Magento\Indexer\Model\Message
\Invalid



//vendor/magento/module-tax/etc/adminhtml/di.xml



Magento
\Tax\Model\System\Message\Notifications




What this means is that the Magento\Indexer\Model\Message\Invalid and
Magento\Tax\Model\System\Message\Notifications string items are being
added to the messages argument and are being made available within the
MessageList constructor.
In the preceding DI example, we only had the $messages parameter defined as one
argument of the array type, and the rest were its array items.
Let's take a look at a DI example for another type definition. This time, it is the one
found under the vendor/magento/module-backend/etc/di.xml file and which is
defined as follows:



Magento\Backend\Model\Url\ScopeResolver

Magento\Backend\Model\Auth\Session\Proxy

Magento\Framework\Data\Form\FormKey\Proxy

Magento\Store\Model\ScopeInterface::SCOPE_STORE


Magento\Backend\Helper\Data\Proxy


[ 107 ]

Using the Dependency Injection

Here, you will see a type with several different arguments passed to the constructor
of the Magento\Backend\Model\Url class. If you now take a look at the constructor
of the Url class, you will see that it is defined in the following way:
public function __construct(
\Magento\Framework\App\Route\ConfigInterface $routeConfig,
\Magento\Framework\App\RequestInterface $request,
\Magento\Framework\Url\SecurityInfoInterface $urlSecurityInfo,
\Magento\Framework\Url\ScopeResolverInterface $scopeResolver,
\Magento\Framework\Session\Generic $session,
\Magento\Framework\Session\SidResolverInterface $sidResolver,
\Magento\Framework\Url\RouteParamsResolverFactory
$routeParamsResolverFactory,
\Magento\Framework\Url\QueryParamsResolverInterface
$queryParamsResolver,
\Magento\Framework\App\Config\ScopeConfigInterface
$scopeConfig,
$scopeType,
\Magento\Backend\Helper\Data $backendHelper,
\Magento\Backend\Model\Menu\Config $menuConfig,
\Magento\Framework\App\CacheInterface $cache,
\Magento\Backend\Model\Auth\Session $authSession,
\Magento\Framework\Encryption\EncryptorInterface $encryptor,
\Magento\Store\Model\StoreFactory $storeFactory,
\Magento\Framework\Data\Form\FormKey $formKey,
array $data = []
) {
//Method body here...
}

The __construct method here clearly has more parameters than what's defined
in the di.xml file. What this means is that the type argument entries in di.xml do
not necessarily cover all the class __construct parameters. The arguments that are
defined in di.xml simply impose the types of individual parameters defined in the
PHP class itself. This works as long as the di.xml parameters are of the same type or
descendants of the same type.
Ideally, we would not pass the class type but interface into the PHP constructor and
then set the type in di.xml. This is where the type, preference, and virtualType
play a major role in di.xml. We have seen the role of type. Now, let's go ahead and
see what preference does.

[ 108 ]

Chapter 5

Configuring class preferences

A great number of Magento's core classes pass interfaces around constructors. The
benefit of this is that the object manager, with the help of di.xml, can decide which
class to actually instantiate for a given interface.
Let's imagine the Foggyline\Di\Console\Command\DiTestCommand class with a
constructor, as follows:
public function __construct(
\Foggyline\Di\Model\TestInterface $myArg1,
$myArg2,
$name = null
)
{
//Method body here...
}

Note how $myArg1 is type hinted as the \Foggyline\Di\Model\TestInterface
interface. The object manager knows that it needs to look into the entire di.xml for
possible preference definitions.
We can define preference within the module's di.xml file, as follows:


Here, we are basically saying that when someone asks for an instance of Foggyline\
Di\Model\TestInterface, give it an instance of the Foggyline\Di\Model\Cart
object. For this to work, the Cart class has to implement TestInterface itself.
Once the preference definition is in place, $myArg1 shown in the preceding example
becomes an object of the Cart class.
Additionally, the preference element is not reserved only to point out the
preferred classes for some interfaces. We can use it to set the preferred class
for some other class.
Now, let's have a look at the Foggyline\Di\Console\Command\DiTestCommand
class with a constructor:
public function __construct(
\Foggyline\Di\Model\User $myArg1,
$myArg2,
$name = null
)
{
//Method body here...
}
[ 109 ]

Using the Dependency Injection

Note how $myArg1 is now type hinted as the \Foggyline\Di\Model\User class.
Like in the previous example, the object manager will look into di.xml for possible
preference definitions.
Let's define the preference element within the module's di.xml file, as follows:


What this preference definition is saying is that whenever an instance of the User
class is requested, pass an instance of the Cart object. This will work only if the Cart
class extends from User. This is a convenient way of rewriting a class, where the
class is being passed directly into another class constructor in place of the interface.
Since the class __construct parameters can be type hinted as either classes or
interfaces and further manipulated via the di.xml preference definition, a question
rises as to what is better. Is it better to use interfaces or specific classes? While the
answer might not be fully clear, it is always preferable to use interfaces to specify
the dependencies we are injecting into the system.

Using virtual types

Along with type and preference, there is another powerful feature of di.xml that
we can use. The virtualType element enables us to define virtual types. Creating a
virtual type is like creating a subclass of an existing class except for the fact that it's
done in di.xml and not in code.
Virtual types are a way of injecting dependencies into some of the existing classes
without affecting other classes. To explain this via a practical example, let's take a
look at the following virtual type defined in the app/etc/di.xml file:



message





Magento\Framework\Message\Session\Storage


[ 110 ]

Chapter 5

The virtualType definition in the preceding example is Magento\Framework\
Message\Session\Storage, which extends from Magento\Framework\Session\
Storage and overwrites the namespace parameter to the message string value. In
virtualType, the name attribute defines the globally unique name of the virtual
type, while the type attribute matches the real PHP class that the virtual type is
based on.
Now, if you look at the type definition, you will see that its storage argument
is set to the object of Magento\Framework\Message\Session\Storage. The
Session\Storage file is actually a virtual type. This allows Message\Session to
be customized without affecting other classes that also declare a dependency on
Session\Storage.
Virtual types allow us to effectively change the behavior of a dependency when it is
used in a specific class.

Summary

In this chapter, we had a look at the object manager and dependency injection, which
are the foundations of Magento object management. We learned the meaning of the
type and preference elements of dependency injection and how to use them to
manipulate class construct parameters. Though there is much more to be said about
dependency injection in Magento, the presented information should suffice and help
us with other aspects of Magento.
In the next chapter, we will extend our journey into di.xml via the concept
of plugins.

[ 111 ]

Plugins
In this chapter, we will take a look at a feature of Magento called plugins. Before we
start with plugins, we first need to understand the term interception because the two
terms are used somewhat interchangeably when dealing with Magento.
Interception is a software design pattern that is used when we want to insert code
dynamically without necessarily changing the original class behavior. This works by
dynamically inserting code between the calling code and the target object.
The interception pattern in Magento is implemented via plugins. They provide
the before, after, and around listeners, which help us extend the observed
method behavior.
In this chapter, we will cover the following topics:
•

Creating a plugin

•

Using the before listener

•

Using the after listener

•

Using the around listener

•

The plugin sort order

Before we start creating a plugin, it is worth noting their limitations. Plugins cannot
be created for just any class or method, as they do not work for the following:
•

Final classes

•

Final methods

•

The classes that are created without a dependency injection

Let's go ahead and create a plugin using a simple module called
Foggyline_Plugged.

[ 113 ]

Plugins

Creating a plugin

Start by creating the app/code/Foggyline/Plugged/registration.php file with
partial content, as follows:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Plugged',
__DIR__
);

Then, create the app/code/Foggyline/Plugged/etc/module.xml file with partial
content, as follows:








The preceding file is simply a new module declaration with the dependency set
against the Magento_Catalog module, as we will be observing its class. We will not
go into the details of module declaration right now, as that will be covered later in
the following chapters.
Now, create the app/code/Foggyline/Plugged/etc/di.xml file with partial
content, as follows:





[ 114 ]

Chapter 6




Plugins are defined within the module di.xml file. To define a plugin, by using
the type element and its name attribute, we first map the class that we want to
observe. In this case, we are observing the Magento\Catalog\Block\Product\
AbstractProduct class. Note that even though the file and class name imply an
abstract type of class, the AbstractProduct class is not abstract.
In the type element, we then define one or more plugins using the plugin element.
The plugin element has the following four attributes assigned to it:
•

name: Using this attribute, you can provide a unique and recognizable name

•

sortOrder: This attribute determines the order of execution when multiple
plugins are observing the same method

•

disabled: The default value of this attribute is set to false, but if it is set to
true, it will disable the plugin

•

type: This attribute points to the class that we will be using to implement the
before, after, or around listener

value that is specific to the plugin

After doing this, create the app/code/Foggyline/Plugged/Block/Catalog/
Product/AbstractProductPlugin1.php file with partial content, as follows:
namespace Foggyline\Plugged\Block\Catalog\Product;
class AbstractProductPlugin1
{
public function beforeGetAddToCartUrl(
$subject,
$product, $additional = []
)
{
var_dump('Plugin1 - beforeGetAddToCartUrl');
}
public function afterGetAddToCartUrl($subject)
{

[ 115 ]

Plugins
var_dump('Plugin1 - afterGetAddToCartUrl');
}
public function aroundGetAddToCartUrl(
$subject,
\Closure $proceed,
$product,
$additional = []
)
{
var_dump('Plugin1 - aroundGetAddToCartUrl');
return $proceed($product, $additional);
}
}

As per the type definition in the di.xml file, the plugin observes the Magento\
Catalog\Block\Product\AbstractProduct class, and this class has a method
called getAddToCartUrl, which is defined as follows:
public function getAddToCartUrl($product, $additional = [])
{
//method body here...
}

The AbstractProductPlugin1 class does not have to be extended from another class
for the plugin to work. We define the before, after and around listeners for the
getAddToCartUrl method by using the naming convention, as follows:
 +  => beforeGetAddToCartUrl
 +  => afterGetAddToCartUrl
 +  => aroundGetAddToCartUrl

We will go into the details of each listener later. Right now we need to
finish the module by creating the AbstractProductPlugin2.php and
AbstractProductPlugin3.php files as a copy of AbstractProductPlugin1.php
and along with that, simply changing all the number values within their code from 1
to 2 or 3.
It's a good practice to organize the listeners into folders matching the structure of
the observed class location. For example, if a module is called Foggyline_Plugged
and we are observing the method in the Magento\Catalog\Block\Product\
AbstractProduct class, we should consider putting the plugin class into the
Foggyline/Plugged/Block/Catalog/Product/AbstractProductPlugin.php file.
This is a not a requirement. Rather, it is a nice convention for other developers to
easily manage the code.
[ 116 ]

Chapter 6

Once the module is in place, we need to execute the following commands on
the console:
php bin/magento module:enable Foggyline_Plugged
php bin/magento setup:upgrade

This will make the module visible to Magento.
If we now open the storefront in a browser for a category page, we will see the
results of all the var_dump function calls.
Let's go ahead and take a look at each and every listener method in detail.

Using the before listener

The before listeners are used when we want to change the arguments of an original
method or add some behavior before an original method is called.
Looking back at the beforeGetAddToCartUrl listener method definition, you will
see that it has three properties assigned in sequence—$subject, $product, and
$additional.
With the before method listener, the first property is always the $subject property,
which contains the instance of the object type being observed. Properties following
the $subject property match the properties of the observed getAddToCartUrl
method in a sequential order.
This simple rule used for transformation is as follows:
getAddToCartUrl($product, $additional = [])
beforeGetAddToCartUrl($subject, $product, $additional = [])

The before listener methods do not need to have a return value.
If we run get_class($subject) in the beforeGetAddToCartUrl listener method
that we previously saw, we will have the following result:
\Magento\Catalog\Block\Product\ListProduct\Interceptor
extends \Magento\Catalog\Block\Product\ListProduct
extends \Magento\Catalog\Block\Product\AbstractProduct

What this shows is that even though we are observing the AbstractProduct class,
the $subject property is not directly of that type. Rather, it is of the ListProduct\
Interceptor type. This is something that you should keep in mind during
development.

[ 117 ]

Plugins

Using the after listener

The after listeners are used when we want to change the values returned by an
original method or add some behavior after an original method is called.
Looking back at the afterGetAddToCartUrl listener method definition, you will see
that it has only one $subject property assigned.
With the after method listener, the first and only property is always the $subject
property, which contains the instance of the object type being observed and not the
return value of the observed method.
This simple rule used for transformation is as follows:
getAddToCartUrl($product, $additional = [])
afterGetAddToCartUrl($subject)

The after listener methods do not need to have a return value.
Like the before interceptor method, the $subject property in this case is not
directly of the AbstractProduct type. Rather, it is of the parent ListProduct\
Interceptor type.

Using the around listener

The around listeners are used when we want to change both the arguments and the
returned values of an original method or add some behavior before and after an
original method is called.
Looking back at the aroundGetAddToCartUrl listener method definition, you will
see that it has four properties assigned in sequence—$subject, $proceed, $product,
and $additional.
With the after method listener, the first property is always the $subject property,
which contains the instance of the object type being observed and not the return
value of the observed method. The second property is always the $proceed property
of \Closure. The properties following the $subject and $proceed match the
properties of the observed getAddToCartUrl method in the sequential order too.
This simple rule used for transformation is as follows:
getAddToCartUrl($product, $additional = [])
aroundGetAddToCartUrl(
$subject,
\Closure $proceed,
$product,
$additional = []
)
[ 118 ]

Chapter 6

The around listener methods must have a return value. The return value is formed
in such way that the parameters following the $closure parameter in the around
listener method definition are passed to the $closure function call in a sequential
order, as follows:
return $proceed($product, $additional);
//or
$result = $proceed($product, $additional);
return $result;

The plugin sort order

Looking back, when we defined a plugin in the di.xml file, one of the attributes that
we set for every plugin definition was sortOrder. It was set to 100, 200 to 300 for
foggyPlugin1, foggyPlugin2 and foggyPlugin3 respectively.
The flow of the code execution for the preceding plugins is as follows:
•
•
•
•
•
•
•
•
•

Plugin1 - beforeGetAddToCartUrl
Plugin1 - aroundGetAddToCartUrl
Plugin2 - beforeGetAddToCartUrl
Plugin2 - aroundGetAddToCartUrl
Plugin3 - beforeGetAddToCartUrl
Plugin3 - aroundGetAddToCartUrl
Plugin3 - afterGetAddToCartUrl
Plugin2 - afterGetAddToCartUrl
Plugin1 - afterGetAddToCartUrl

In other words, if multiple plugins are listening to the same method, the following
execution order is used:
•

The before plugin functions with the lowest sortOrder value

•

The around plugin functions with the lowest sortOrder value

•

The before plugin functions following the sortOrder value from the lowest
to the highest

•

The around plugin functions following the sortOrder value from the lowest
to the highest

•

The after plugin functions with the highest sortOrder value

•

The after plugin functions following the sortOrder value from the highest
to the lowest
[ 119 ]

Plugins

Special care needs to be taken when it comes to the around listener, as
it is the only listener that needs to return a value. If we omit the return
value, we risk breaking the execution flow in such a way that the other
around plugins for the same method won't be executed.

Summary

In this chapter, we had a look at a powerful feature of Magento called plugins. We
created a small module with three plugins; each plugin had a different sort order.
This enabled us to trace the execution flow of multiple plugins that observe the same
method. We explored in detail the before, after, and around listener methods,
while having a strong emphasis on the parameter order. The finalized module used
in this chapter can be found at https://github.com/ajzele/B05032-Foggyline_
Plugged.
In the next chapter, we are going to dive deep into backend development.

[ 120 ]

Backend Development
Backend development is a term that is most commonly used to describe work
closely related to the server side. This usually implies the actual server, application
code, and the database. For example, if we open a storefront of a web shop, add
a few products to the cart, and then check out, the application will store the
information provided. This information is managed on a server with a serverside language, such as PHP, and then saved in a database. In Chapter 4, Models and
Collections, we took a look at the backbone of backend development. In this chapter,
we will explore other backend-related aspects.
We will use the Foggyline_Office module that was defined in one of the previous
chapters as we go through the following topics:
•

Cron jobs

•

Notification messages

•

Sessions and cookies

•

Logging

•

The profiler

•

Events and observers

•

Caches

•

Widgets

•

Custom variables

•

i18n (internationalization)

•

Indexers

These individual isolated units of functionality are mostly used in everyday
backend-related development.

[ 121 ]

Backend Development

Cron jobs

Speaking of cron jobs, it is worth noting one important thing. A Magento cron job
is not the same as an operating system cron job. An operating system cron is driven
by a crontab (short for cron table) file. The crontab file, is a configuration file that
specifies shell commands that need to be run periodically on a given schedule.
A Magento cron job is driven by a periodic execution of PHP code that handles
entries in the cron_schedule table. The cron_schedule table is where Magento
cron jobs are queued once they are picked up from the individual crontab.xml file.
The Magento cron jobs cannot be executed without the operating system cron
job being set to execute the php bin/magento cron:run command. Ideally, an
operating system cron job should be set to trigger Magento's cron:run every
minute. Magento will then internally execute its cron jobs according to the way
an individual cron job is defined in the crontab.xml file.
To define a new cron job in Magento cron, we first need to define a crontab.xml
file in the module. Let's create a app/code/Foggyline/Office/etc/crontab.xml
file with the following content:




*/2 * * * *




Note that the XSD schema location points to crontab.xsd from within the Magento_
Cron module.
The id attribute of a group element is set to the default value. In its modules,
Magento defines two different groups, namely default and index. We used the
default value, as this is the one that gets executed when the standard php bin/
magento cron:run command is triggered on the console.
Within the group element, we have individual jobs defined under the job element.
The job element requires us to specify the name, instance, and method attributes.
The name attribute has to be unique within the group element. The value of the
instance and method attributes should point to the class that will be instantiated
and the method within the class that needs to be executed.
[ 122 ]

Chapter 7

The schedule element nested within the cron job specifies the desired time of job
execution. It uses the same time expression as that of the entries in an operating
system crontab file. The specific example that we will look at defines an expression
(*/2 * * * *) that is executed every two minutes.
Once we have defined the crontab.xml file, we need to define the
Foggyline\Office\Model\Cron class file, as follows:
namespace Foggyline\Office\Model;
class Cron
{
protected $logger;
public function __construct(
\Psr\Log\LoggerInterface $logger
)
{
$this->logger = $logger;
}
public function logHello()
{
$this->logger->info('Hello from Cron job!');
return $this;
}
}

The preceding code simply defines a logHello method used by the cron job. In
the logHello method, we used the logger method that was instantiated via the
constructor. The logger method will make a log entry in the var/log/system.log
file once it is executed.
Once the command is executed, you will see the Ran jobs by schedule message in
the console. Additionally, the cron_schedule table should get filled with all the
Magento cron jobs that were defined.
At this point, we should trigger the php bin/magento cron:run command in
the console.

[ 123 ]

Backend Development

The cron_schedule table contains the following columns:
•
•
•

•
•
•
•
•

schedule_id: The auto-increment primary field.
job_code: The value of the job name attribute, as defined in crontab.xml
file, which equals to foggyline_office_logHello table in our example.
status: Defaults to the pending value for the newly created entries in the
table and allows for a pending, running, success, missed or error value.
Its value changes as the cron job traverses through its life cycle.
messages: Stores the possible exception error message if the exception has
occurred during a job's execution.
created_at: The timestamp value that denotes when a job was created.
scheduled_at: The timestamp value that denotes when a job was scheduled
for execution.
executed_at: The timestamp value that denotes when a job's
execution started.
finished_at: The timestamp value that denotes when a job has
finished executing.

Unless we have already set the operating system cron to trigger the php bin/
magento cron:run command, we need to trigger it on our own a few times every
two minutes in order to actually execute the job. The first time a command is run, if
the job does not exist in the cron_schedule table, Magento will merely queue it, but
it won't execute it. The subsequent cron runs will execute the command. Once we
are sure that the cron job entry in the cron_schedule table has the finished_at
column value filled, we will see an entry that looks like [2015-11-21 09:42:18]
main.INFO: Hello from Cron job! [] [] in the var/log/system.log file.
While developing and testing cron jobs in Magento, we might need to
truncate the cron_schedule table, delete Magento's var/cache
value, and execute the php bin/magento cron:run command
repetitively until we get it tested and working.

Notification messages

Magento implements the notification message mechanism via the Messages
module. The Messages module conforms to \Magento\Framework\Message\
ManagerInterface. Though the interface itself does not impose any session relation,
an implementation adds interface-defined types of messages to a session and allows
access to those messages later. In the app/etc/di.xml file, there is a preference
defined for \Magento\Framework\Message\ManagerInterface towards the
Magento\Framework\Message\Manager class.
[ 124 ]

Chapter 7

Message\ManagerInterface specifies four types of messages, namely error,
warning, notice, and success. The types of messages are followed by several
key methods in the Message\Manager class, such as addSuccess, addNotice,
addWarning, addError, and addException. The addException method is basically
a wrapper for addError that accepts an exception object as a parameter.

Let's try to run the following code in the execute method of app/code/Foggyline/
Office/Controller/Test/Crud.php:
$resultPage = $this->resultPageFactory->create();
$this->messageManager->addSuccess('Success-1');
$this->messageManager->addSuccess('Success-2');
$this->messageManager->addNotice('Notice-1');
$this->messageManager->addNotice('Notice-2');
$this->messageManager->addWarning('Warning-1');
$this->messageManager->addWarning('Warning-2');
$this->messageManager->addError('Error-1');
$this->messageManager->addError('Error-2');
return $resultPage;

Once this code executed, the result, as shown in the following screenshot, will appear
on the page in the browser:

[ 125 ]

Backend Development

Notification messages appear both in the frontend and admin area.
The frontend layout vendor/magento/module-theme/view/frontend/layout/
default.xml file defines it as follows:













The template file that renders the messages is view/frontend/templates/
messages.phtml in the Magento_Theme module. By looking at the Magento\
Framework\View\Element\Messages class, you will see that the _toHtml method
branches into if-else statements, depending on whether template is set or not. In
case the template is not set, _toHtml internally calls the _renderMessagesByType
method, which renders messages in the HTML format that are grouped by type.

The view/adminhtml/layout/default.xml admin layout file in the
Magento_AdminNotification module defines it as follows:







[ 126 ]

Chapter 7

The template file that renders the messages is view/adminhtml/templates/
system/messages.phtml in the Magento_AdminNotification module. When you
look at the Magento\AdminNotification\Block\System\Messages class, you will
see that its _toHtml is calling the _toHtml parent method, where the parent belongs
to the \Magento\Framework\View\Element\Template class. This means that the
output is relying on the view/adminhtml/templates/system/messages.phtml file
in the Magento_AdminNotification module.

Session and cookies

Sessions in Magento conform to Magento\Framework\Session\
SessionManagerInterface. In the app/etc/di.xml file, there is a definition
preference for the SessionManagerInterface class which points to the Magento\
Framework\Session\Generic class type. The Session\Generic class is just an
empty class that extends the Magento\Framework\Session\SessionManager class,
which in turn implements the SessionManagerInterface class.
There is one important object that gets instantiated in the SessionManager instance
that conforms to \Magento\Framework\Session\Config\ConfigInterface. On
looking at app/etc/di.xml file, we can see a preference for ConfigInterface
pointing to a Magento\Framework\Session\Config class type.
To fully understand the session behavior in Magento, we should
study the inner workings of both the SessionManager and
Session\Config classes.
Magento uses cookies to keep track of a session. These cookies have a default lifetime
of 3,600 seconds. When a session is established, a cookie with the name of PHPSESSID
is created in the browser. The value of the cookie equals the session name. By
default, sessions are stored in files in the var/session directory of Magento's
root installation.

[ 127 ]

Backend Development

If you have a look at these session files, you will see that session information
is being stored in serialized strings that are divided into groupings such as _
session_validator_data, _session_hosts, default, customer_website_1,
and checkout, as shown in the following screenshot:

This is not the finite list of grouping. Modules that implement their own session
handling bits can add their own groups.
We can store and retrieve information in a session by simply using expressions like
the following ones:
$this->sessionManager->setFoggylineOfficeVar1('Office1');
$this->sessionManager->getFoggylineOfficeVar1();

The preceding expressions will create and get an entry from the session under the
default group.
We can get the entire content of the default session group simply by using the
$this->sessionManager->getData() expression, which will return an array of
data that is similar to the following one:
array(3) {
["_form_key"] => string(16) "u3sNaa26Ii21nveV"
["visitor_data"] => array(14) {
["last_visit_at"] => string(19) "2015-08-19 07:40:03"
["session_id"] => string(26) "8p82je0dkqq1o00lanlr6bj6m2"
["visitor_id"] => string(2) "35"
["server_addr"] => int(2130706433)
["remote_addr"] => int(2130706433)
["http_secure"] => bool(false)
["http_host"] => string(12) "magento2.loc"
["http_user_agent"] => string(121) "Mozilla/5.0 …"
["http_accept_language"] => string(41) "en-US,en;"
["http_accept_charset"] => string(0) ""
[ 128 ]

Chapter 7
["request_uri"] => string(38)
"/index.php/foggyline_office/test/crud/"
["http_referer"] => string(0) ""
["first_visit_at"] => string(19) "2015-08-19 07:40:03"
["is_new_visitor"] => bool(false)
}
["foggyline_office_var_1"] => string(7) "Office1"
}

As you can see, the foggyline_office_var_1 value is right there among other
session values.
There are several useful methods of ConfigInterface that we can use to fetch
session configuration information; a few of these methods are as follows:
•

getCookieSecure

•

getCookieDomain

•

getCookieHttpOnly

•

getCookieLifetime

•

getName

•

getSavePath

•

getUseCookies

•

getOptions

Here's a result example of the getOptions method call on the Session\Config
instance:
array(9) {
["session.save_handler"] => string(5) "files"
["session.save_path"] => string(39)
"/Users/branko/www/magento2/var/session/"
["session.cookie_lifetime"] => int(3600)
["session.cookie_path"] => string(1) "/"
["session.cookie_domain"] => string(12) "magento2.loc"
["session.cookie_httponly"] => bool(true)
["session.cookie_secure"] => string(0) ""
["session.name"] => string(9) "PHPSESSID"
["session.use_cookies"] => bool(true)
}

Cookies often go hand in hand with sessions. Besides being used to link to a certain
session, cookies are often used to store some information on the client side, thus
tracking or identifying the return users and customers.
[ 129 ]

Backend Development

Besides the pure PHP approach with the setcookie function, we can manage
cookies in Magento through an instance of Magento\Framework\Stdlib\
CookieManagerInterface. When you look at app/etc/di.xml file, you will see
that the preference for CookieManagerInterface points to a class of the Magento\
Framework\Stdlib\Cookie\PhpCookieManager type.
The following restrictions are worth noting when it comes to Magento cookies:
•

We can set maximum of 50 cookies in the system. Otherwise, Magento will
throw an Unable to send the cookie. Maximum number of cookies
would be exceeded exception.

•

We can store a cookie with a maximum size of 4096 bytes. Otherwise,
Magento will throw an Unable to send the cookie. Size of \'%name\'
is %size bytes exception.

By imposing these restrictions, Magento ensures that we are compatible with
most browsers.
The CookieManagerInterface class, among other things, specifies the
setSensitiveCookie method requirement. This method sets a value in a private
cookie with the given $name $value pairing. Sensitive cookies have HttpOnly set to
true and thus cannot be accessed by JavaScript.
As we will soon demonstrate in the following examples, to set a public or private
cookie, we can help ourselves by using instances of the following:
•

\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory

•

\Magento\Framework\Stdlib\CookieManagerInterface

•

\Magento\Framework\Session\Config\ConfigInterface

We can set public cookies in the following way:
$cookieValue = 'Just some value';
$cookieMetadata = $this->cookieMetadataFactory
->createPublicCookieMetadata()
->setDuration(3600)
->setPath($this->sessionConfig->getCookiePath())
->setDomain($this->sessionConfig->getCookieDomain())
->setSecure($this->sessionConfig->getCookieSecure())
->setHttpOnly($this->sessionConfig->getCookieHttpOnly());
$this->cookieManager
->setPublicCookie('cookie_name_1', $cookieValue,
$cookieMetadata);
[ 130 ]

Chapter 7

The preceding code will result in a cookie, as shown in the following screenshot:

We can set private cookies in the following way:
$cookieValue = 'Just some value';
$cookieMetadata = $this->cookieMetadataFactory
->createSensitiveCookieMetadata()
->setPath($this->sessionConfig->getCookiePath())
->setDomain($this->sessionConfig->getCookieDomain());
$this->cookieManager
->setSensitiveCookie('cookie_name_2', $cookieValue,
$cookieMetadata);

[ 131 ]

Backend Development

The preceding code will result in a cookie, as shown in the following screenshot:

Interestingly, both the public and private cookies in the preceding example show
that HttpOnly is checked off because by default, a Magento admin has Stores |
Settings | Configuration | General | Web | Default Cookie Settings | Use HTTP
Only set to Yes. Since we are using the setHttpOnly method in the public cookie
example, we simply picked up the config value via $this->sessionConfig->
getCookieHttpOnly() and passed it on. If we comment out that line, we will see
that the public cookie does not really set HttpOnly by default.

Logging

Magento supports the messages logging mechanism via its \Psr\Log\
LoggerInterface class. The LoggerInterface class has a preference defined
within app/etc/di.xml file for the Magento\Framework\Logger\Monolog class
type. The actual crux of implementation is actually in the Monolog parent class
named Monolog\Logger, which comes from the Monolog vendor.

The LoggerInterface class uses the following eight methods to write logs to the
eight RFC 5424 levels:
•

debug

•
•

info
notice
[ 132 ]

Chapter 7

•

warning

•

error

•

critical

•

alert

•

emergency

To use a logger, we need to pass the LoggerInterface class to a constructor of
a class from within we want to use it and then simply make one of the following
method calls:
$this->logger->log(\Monolog\Logger::DEBUG, 'debug msg');
$this->logger->log(\Monolog\Logger::INFO, 'info msg');
$this->logger->log(\Monolog\Logger::NOTICE, 'notice msg');
$this->logger->log(\Monolog\Logger::WARNING, 'warning msg');
$this->logger->log(\Monolog\Logger::ERROR, 'error msg');
$this->logger->log(\Monolog\Logger::CRITICAL, 'critical msg');
$this->logger->log(\Monolog\Logger::ALERT, 'alert msg');
$this->logger->log(\Monolog\Logger::EMERGENCY, 'emergency msg');

Alternatively, the preferred shorter version through individual log level type
methods is as follows:
$this->logger->debug('debug msg');
$this->logger->info('info msg');
$this->logger->notice('notice msg');
$this->logger->warning('warning msg');
$this->logger->error('error msg');
$this->logger->critical('critical msg');
$this->logger->alert('alert msg');
$this->logger->emergency('emergency msg');

Both approaches result in the same two log files being created in Magento, which are
as follows:
•

var/log/debug.log

•

var/log/system.log

The debug.log file contains only the debug level type of the log, while the rest are
saved under system.log.

[ 133 ]

Backend Development

Entries within these logs will then look like this:
[2015-11-21
[]
[2015-11-21
[2015-11-21
[2015-11-21
[2015-11-21
[2015-11-21
[2015-11-21
[2015-11-21

09:42:18] main.DEBUG: debug msg {"is_exception":false}
09:42:18]
09:42:18]
09:42:18]
09:42:18]
09:42:18]
09:42:18]
09:42:18]

main.INFO: info msg [] []
main.NOTICE: notice msg [] []
main.WARNING: warning msg [] []
main.ERROR: error msg [] []
main.CRITICAL: critical msg [] []
main.ALERT: alert msg [] []
main.EMERGENCY: emergency msg [] []

Each of these logger methods can accept an entire array of arbitrary data called
context, as follows:
$this->logger->info('User logged in.', ['user'=>'Branko',
'age'=>32]);

The preceding expression will produce the following entry in system.log:
[2015-11-21 09:42:18] main.INFO: User logged in.
{"user":"Branko","age":32} []

We can manually delete any of the .log files from the var/log
directory, and Magento will automatically create it again when needed.
Magento also has another logging mechanism in place, where it logs the following
actions in the log_* tables in a database:

•
•
•
•
•
•
•
•
•

log_customer
log_quote
log_summary
log_summary_type
log_url
log_url_info
log_visitorz
log_visitor_info
log_visitor_online

It is worth noting that this database logging is not related in any way to Psr logger
that was described previously. While Psr logger serves developers within the code to
group and log certain messages according to the Psr standard, the database logging
logs the live data that is a result of user/customer interaction in the browser.
[ 134 ]

Chapter 7

By default, Magento keeps database logs for around 180 days. This is a configurable
option that can be controlled in the Magento admin area under the Stores | Settings
| Configuration | Advanced | System | Log Cleaning tab with other log related
options, as shown in the following screenshot:

Configuration options that are shown in the preceding screenshot only bare meaning
operating system cron is triggering Magento cron.
We can execute two commands on terminal: php bin/magento
log:status to get the current state information about log tables and
php bin/magento log:clean to force the clearing of tables.

[ 135 ]

Backend Development

The profiler

Magento has an in-built profiler that can be used to identify performance problems

on the server side. In a nutshell, the profiler can tell us the execution time of certain
chunks of code. There is nothing that great with its behavior. We can only get the
execution time of code blocks or individual expressions that have been wrapped
by the profiler's start and stop methods. On its own, Magento calls for the profiler
extensively across its code. However, we can't see it in effect as the profiler output is
disabled by default.
Magento supports three profiler outputs, namely html, csvfile, and firebug.

To enable the profiler, we can edit .htaccess and add one of the following
expressions:
•

SetEnv MAGE_PROFILER "html"

•

SetEnv MAGE_PROFILER "csvfile"

•

SetEnv MAGE_PROFILER "firebug"

The HTML type of profiler will show its output into the footer area of a page that we
open in the browser, as shown in the following screenshot:

[ 136 ]

Chapter 7

The csv file type of profiler will output into var/log/profiler.csv, as shown in
the following screenshot:

The firebug type of profiler will output into var/log/profiler.csv, as shown in
the following screenshot:

The profiler outputs the following pieces of information:
•

Time profiler shows the time spent from Profiler::start to
Profiler::stop.

•

Avg profiler shows the average time spent from Profiler::start to
Profiler::stop for cases where Cnt is greater than one.

•

Cnt profiler shows the integer value of how many times we have started the
profiler with the same timer name. For example, if we have called \Magento\
Framework\Profiler::start('foggyline:office'); twice somewhere in
the code, then Cnt will show the value of 2.

[ 137 ]

Backend Development

•

Emalloc profiler stands for the amount of memory allocated to PHP. It is a
mix of the core PHP memory_get_usage function without the true parameter
passed to it and the timer values.

•

RealMem profiler also stands for the amount of memory allocated to PHP
whose final value is also obtained via the memory_get_usage function minus
the timer values, but this time with the true parameter passed to it.

We can easily add our own Profiler::start calls anywhere in the code. Every
Profiler::start should be followed by some code expressions and then finalized
with a Profiler::stop call, as follows:
\Magento\Framework\Profiler::start('foggyline:office');
sleep(2); /* code block or single expression here */
\Magento\Framework\Profiler::stop('foggyline:office');

Depending on where we call the profiler in the code, the resulting output should be
similar to the one shown in the following screenshot:

Events and observers

Magento implements the observer pattern through \Magento\Framework\
Event\ManagerInterface. In app/etc/di.xml, there is a preference for
ManagerInterface that points to the Magento\Framework\Event\Manager\Proxy
class type. The Proxy class further extends the \Magento\Framework\Event\
Manager class that implements the actual event dispatch method.

Events are dispatched by calling a dispatch method on the instance of the Event\
Manager class and passing the name and some data, which is optional, to it. Here's
an example of a Magento core event:
$this->eventManager->dispatch(
'customer_customer_authenticated',
['model' => $this->getFullCustomerObject($customer),
'password' => $password]
);

[ 138 ]

Chapter 7

The $this->eventManager is an instance of the previously mentioned Event\
Manager class. In this case, the event name equals to customer_customer_
authenticated, while the data passed to the event is the array with two elements.
The preceding event is fired when the authenticate method is called on \Magento\
Customer\Model\AccountManagement, that is, when a customer logs in.
Dispatching an event only makes sense if we expect someone to observe it and
execute their code when the event is dispatched. Depending on the area from
which we want to observe events, we can define observers in one of the
following XML files:
•

app/code/{vendorName}/{moduleName}/etc/events.xml

•

app/code/{vendorName}/{moduleName}/etc/frontend/events.xml

•

app/code/{vendorName}/{moduleName}/etc/adminhtml/events.xml

Let's define an observer that will log an e-mail address of an authenticated user
into a var/log/system.log file. We can use the Foggyline_Office module and
add some code to it. As we are interested in the storefront, it makes sense to put the
observer in the etc/frontend/events.xml module.
Let's define the app/code/Foggyline/Office/etc/frontend/events.xml file with
content, as follows:






Here, we are specifying a foggyline_office_customer_authenticated
observer for the customer_customer_authenticated event. The observer is
defined in the LogCustomerEmail class that is placed in the Observer module
directory. The Observer class has to implement the Magento\Framework\Event\
ObserverInterface class. The Observer interface defines a single execute method.
The execute method hosts the observer code and is executed when the customer_
customer_authenticated event is dispatched.

[ 139 ]

Backend Development

Let's go ahead and define the Foggyline\Office\Observer\LogCustomerEmail
class in the app/code/Foggyline/Office/Observer/LogCustomerEmail.php file,
as follows:
namespace Foggyline\Office\Observer;
use Magento\Framework\Event\ObserverInterface;
class LogCustomerEmail implements ObserverInterface
{
protected $logger;
public function __construct(
\Psr\Log\LoggerInterface $logger
)
{
$this->logger = $logger;
}
/**
* @param \Magento\Framework\Event\Observer $observer
* @return self
*/
public function execute(\Magento\Framework\Event\Observer
$observer)
{
//$password = $observer->getEvent()->getPassword();
$customer = $observer->getEvent()->getModel();
$this->logger->info('Foggyline\Office: ' . $customer->
getEmail());
return $this;
}
}

The execute method takes a single parameter called $observer of the \Magento\
Framework\Event\Observer type. The event that we are observing is passing two
pieces of data within the array, namely the model and password. We can access
this by using the $observer->getEvent()->get{arrayKeyName} expression.
The $customer object is an instance of the Magento\Customer\Model\Data\
CustomerSecure class, which contains properties such as email, firstname,
lastname, and so on. Thus, we can extract the e-mail address from it and pass it to
logger's info method.

[ 140 ]

Chapter 7

Now that we know how to observe existing events, let's see how we can dispatch
our own events. We can dispatch events from almost anywhere in the code, with or
without data, as shown in the following example:
$this->eventManager->dispatch('foggyline_office_foo');
// or
$this->eventManager->dispatch(
'foggyline_office_bar',
['var1'=>'val1', 'var2'=>'val2']
);

It is worth noting that there are two types of events; we can group them in the
following way according to the way their name is assigned:
•

Static: $this->eventManager->dispatch('event_name', ...)

•

Dynamic: $this->eventManager->dispatch({expression}.'_event_
name', ...)

The static events have a fixed string for a name, while the dynamic ones have a name
that is determined during the runtime. Here's a nice example of the core Magento
functionality from the afterLoad method that is defined under lib/internal/
Magento/Framework/Data/AbstractSearchResult.php, which showcases how to
use both types of events:
protected function afterLoad()
{
$this->eventManager->dispatch
('abstract_search_result_load_after', ['collection' =>
$this]);
if ($this->eventPrefix && $this->eventObject) {
$this->eventManager->dispatch($this->eventPrefix .
'_load_after', [$this->eventObject => $this]);
}
}

We can see a static event (abstract_search_result_load_after) and a dynamic
event ($this->eventPrefix . '_load_after'). The $this->eventPrefix is an
expression that gets evaluated during the runtime. We should be careful when using
dynamic events as they are triggered under multiple situations. Some interesting
dynamic events are the one defined on classes like the following ones:
•

Magento\Framework\Model\AbstractModel

°°

$this->_eventPrefix . '_load_before'

°°
°°

$this->_eventPrefix . '_load_after'
$this->_eventPrefix . '_save_commit_after'
[ 141 ]

Backend Development

•

•

°°

$this->_eventPrefix . '_save_before'

°°

$this->_eventPrefix . '_save_after'

°°

$this->_eventPrefix . '_delete_before'

°°

$this->_eventPrefix . '_delete_after'

°°

$this->_eventPrefix . '_delete_commit_after'

°°

$this->_eventPrefix . '_clear'

\Magento\Framework\Model\ResourceModel\Db\Collection\
AbstractCollection

°°

$this->_eventPrefix . '_load_before'

°°

$this->_eventPrefix . '_load_after'

\Magento\Framework\App\Action\Action

°°

'controller_action_predispatch_' . $request->
getRouteName()

°°

'controller_action_predispatch_' . $request->
getFullActionName()

°°

'controller_action_postdispatch_' . $request->
getFullActionName()

°°

'controller_action_postdispatch_' . $request->
getRouteName()

•

Magento\Framework\View\Result\Layout

•

'layout_render_before_' . $this->request-> getFullActionName()

These events are fired on the model, collection, controller, and layout classes,
which are probably among the most used backend elements that often require
observing and interacting. Even though we can say that the full event name is known
during the runtime along with the dynamic event, this can be assumed even before
the runtime.
For example, assuming that we want to observe 'controller_action_
predispatch_' . $request->getFullActionName() for the Foggyline_Office
module's Crud controller action, the actual full event name will be 'controller_
action_predispatch_foggyline_office_test_crud', given that $request>getFullActionName() will resolve to foggyline_office_test_crud during
the runtime.

[ 142 ]

Chapter 7

Cache(s)

Magento has eleven out-of-the-box cache types, according to the following list.

These are used across many levels within the system:
•
•
•
•
•
•
•
•
•
•
•
•

Configuration: Various XML configurations that were collected across
modules and merged
Layouts: Layout building instructions
Blocks HTML output: Page blocks HTML
Collections data: Collection data files
Reflection data: API interfaces reflection data
Database DDL operations: Results of DDL queries, such as describing tables
or indexes
EAV types and attributes: Entity types declaration cache
Page cache: Full page caching
Translations: Translation files
Integrations configuration: Integration configuration file
Integrations API configuration: Integrations API configuration file
Web services configuration: REST and SOAP configurations, generated
WSDL file

There is also Additional Cache Management that manages the cache for the
following files:
•

Previously generated product image files

•

Themes JavaScript and CSS files combined to one file

•

Preprocessed view files and static files

Each of these caches can be cleared separately.
We can easily define our own cache type. We can do so by first creating an app/

code/Foggyline/Office/etc/cache.xml file with content, as follows:




Example cache from Foggyline Office
module.


[ 143 ]

Backend Development

When defining a new cache type, we need to specify its name and instance
attributes. The name attribute of the type element should be set to foggyline_
office and should be unique across Magento. This value should match the TYPE_
IDENTIFIER constant value on the Foggyline\Office\Model\Cache class, which
will be created soon. The instance attribute holds the class name that we will use
for caching.
Then, we will define the Foggyline\Office\Model\Cache class in the app/code/
Foggyline/Office/Model/Cache.php file with the following content:
namespace Foggyline\Office\Model;
class Cache extends \Magento\Framework\Cache\Frontend\Decorator\
TagScope
{
const TYPE_IDENTIFIER = 'foggyline_office';
const CACHE_TAG = 'OFFICE';
public function __construct(
\Magento\Framework\App\Cache\Type\FrontendPool
$cacheFrontendPool
)
{
parent::__construct(
$cacheFrontendPool->get(self::TYPE_IDENTIFIER),
self::CACHE_TAG
);
}
}

The Cache class extends from TagScope and specifies its own values for TYPE_
IDENTIFIER and CACHE_TAG, passing them along to the parent constructor in the
__construct method. With these two files (cache.xml and Cache), we have

basically defined a new cache type.

Once we have specified the cache.xml file and the referenced cache class, we should
be able to see our cache type in the Magento admin under the System | Tools |
Cache Management menu, as shown in the following screenshot:

[ 144 ]

Chapter 7

On its own, simply defining a new cache does not mean that it will get filled and
used by Magento.
If you would like to use the cache anywhere within your code, you can do so by first
passing the instance of the cache class to the constructor, as follows:
protected $cache;
public function __construct(
\Foggyline\Office\Model\Cache $cache
)
{
$this->cache = $cache;
}

Then, you can execute a chunk of code, as follows:
$cacheId = 'some-specific-id';
$objInfo = null;
$_objInfo = $this->cache->load($cacheId);
if ($_objInfo) {
$objInfo = unserialize($_objInfo);
} else {
$objInfo = [
'var1'=> 'val1',
'var2' => 'val2',
'var3' => 'val3'
];
$this->cache->save(serialize($objInfo), $cacheId);
}

[ 145 ]

Backend Development

The preceding code shows how we first try to load the value from the existing cache
entry, and if there is none, we save it. If the cache type is set to disabled under the
Cache Management menu, then the preceding code will never save and pull the
data from the cache, as it is not in effect.
If you take a look at the var/cache folder of Magento at this point, you will see
something similar to what's shown in the following screenshot:

Magento created two cache entries for us, namely var/cache/mage-tags/mage--a8a_OFFICE and var/cache/mage--f/mage---a8a_SOME_SPECIFIC_ID. The mage--a8a_OFFICE file has only a single line of entry in this specific case, and the entry
is the a8a_SOME_SPECIFIC_ID string, which obviously points to the other file. The
mage---a8a_SOME_SPECIFIC_ID file contains the actual serialized $objInfo array.

The a8a_ prefix and other prefixes in the cache file names are not really relevant
to us; this is something that Magento adds on its own. What is relevant to us is the
passing of proper individual cache tags to the chunks or variables that we want to
cache, like in the preceding example, and the TYPE_IDENTIFIER and CACHE_TAG tags
that we set for the Cache class.

Widgets

Magento provides support for widgets. Though the word "widget" might imply
frontend development skills and activities, we will look at them as a part of the
backend development flow because creating useful and robust widgets requires a
significant amount of backend knowledge.

[ 146 ]

Chapter 7

Magento provides several out-of-the-box widgets; some of them are as follows:

•

CMS page link

•

CMS static block

•

Catalog category link

•

Catalog new products list

•

Catalog product link

•

Catalog products list

•

Orders and returns

•

Recently compared products

•

Recently viewed products

To create a fully custom widget, we start by defining app/code/Foggyline/Office/
etc/widget.xml with content, as follows:



Example Widget









5





[ 147 ]

Backend Development

The id widget has been set to foggyline_office, while the class powering widget
has been set to Foggyline\Office\Block\Widget\Example. the widget class is
basically a block class that extends from \Magento\Framework\View\Element\
AbstractBlock and implements \Magento\Widget\Block\BlockInterface. The
label and description element set values appear under the Magento admin when
we select the widget for use.
The parameters of a widget are its configurable options that translate into HTML
form elements, depending on the type and source_model options that we have
selected. In the following example, we will demonstrate the usage of the select and
text elements to retrieve input from a user, as shown in the following screenshot:

Let's proceed by creating the actual Widget\Example class in the app/code/
Foggyline/Office/Block/Widget/Example.php file with content, as follows:
namespace Foggyline\Office\Block\Widget;
class Example extends \Magento\Framework\View\Element\Text
implements \Magento\Widget\Block\BlockInterface
{
protected function _beforeToHtml()
{
$this->setText(sprintf(
'example widget: var1=%s, var2=%s',
$this->getData('var1'),
$this->getData('var2')
));

[ 148 ]

Chapter 7
return parent::_beforeToHtml();
}
}

What is happening here is that we are using Element\Text as a block type and not
Element\Template because we want to simplify the example, as Element\Template
will require the phtml template to be defined as well. By using Element\Text, we
can simply define _beforeToHtml and call the setText method to set the text string
of the block's output. We will build the output string by picking up the var1 and
var2 variables, which were passed as parameters to the block.
Now, if we open the Magento admin area, go to Content | Elements | Pages, and
select Home Page to edit, we should be able to click on the Insert Frontend App
button and add our widget to the page. Alternatively, if we are not editing the page
content in the WYSIWYG mode, we can also add the widget manually to the page by
using the following expression:
{{widget type="Foggyline\\Office\\Block\\Widget\\Example" var1="1"
var2="5"}}

Finally, we should see the example widget: var1=1, var2=5 string in the browser
while visiting the home page of the storefront.
We can use frontend apps to create highly configurable and embeddable widgets
that users can easily assign to a CMS page or block.

Custom variables

Variables are a handy little feature of a core Magento_Variable module. Magento
allows you to create custom variables and then use them in e-mail templates, the
WYSIWYG editor, or even code expressions.
The following steps outline how we can create a new variable manually:
1. In the Magento admin area, navigate to System | Other Settings | Custom
Variables.
2. Click on the Add New Variable button.
3. While keeping in mind the Store View switcher, fill in the required Variable
Code and Variable Name options, and preferably one of the optional
options, either Variable HTML Value or Variable Plain Value.
4. Click on the Save button.

[ 149 ]

Backend Development

Now that we have created the custom variable, we can use it in an e-mail template or
the WYSIWYG editor by calling it using the following expression:
{{customVar code=foggyline_hello}}

The preceding expression will call for the value of the custom variable with code
foggyline_hello.
Variables can be used within various code expressions, though it is not
recommended to rely on the existence of an individual variable, as an admin user
can delete it at any point. The following example demonstrates how we can use an
existing variable in the code:
$storeId =0;
$variable = $this->_variableFactory->create()->setStoreId(
$storeId
)->loadByCode(
'foggyline_hello'
);
$value = $variable->getValue(
\Magento\Variable\Model\Variable::TYPE_HTML
);

The $this->_variableFactory is an instance of \Magento\Variable\Model\
VariableFactory.
If used in the right way, variables can be useful. Storing information such as
phone numbers or specialized labels that are used in CMS pages, blogs, and
e-mail templates is a nice example of using custom variables.

i18n

i18n is the abbreviation for internationalization. Magento adds i18n support out
of the box, thus adapting to various languages and regions without application
changes. Within app/functions.php, there is a __() translation function, which is
defined as follows:
function __()
{
$argc = func_get_args();
$text = array_shift($argc);
if (!empty($argc) && is_array($argc[0])) {
[ 150 ]

Chapter 7
$argc = $argc[0];
}
return new \Magento\Framework\Phrase($text, $argc);
}

This translation function accepts a variable number of arguments and passes
them to a constructor of the \Magento\Framework\Phrase class and returns its
instance. The Phrase class has the __toString method, which then returns the
translated string.
Here are a few examples of how we can use the __() function:
•

__('Translate me')

•
•

__('Var1 %1, Var2 %2, Var %3', time(), date('Y'), 32)
__('Copyright %1 Magento', date('Y'), 'http://
magento.com')

Strings passed through the translation function are expected to be found under
the local CSV files, such as app/code/{vendorName}/{moduleName}/i18n/
{localeCode}.csv. Let's imagine for a moment that we have two different store
views defined in the Magento admin area under Stores | Settings | All Stores. One
store has Store | Settings | Configuration | General | Locale Options | Locale
set to English (United Kingdom) and the other one to German (Germany). The
local code for English (United Kingdom) is en_GB, and for German (Germany),
it is de_DE.
For the de_DE locale, we will add translation entries in the app/code/Foggyline/
Office/i18n/de_DE.csv file, as follows:
"Translate me","de_DE Translate me"
"Var1 %1, Var2 %2, Var %3","de_DE Var1 %1, Var2 %2, Var %3"
"Copyright %1 Magento","de_DE Copyright %1 Magento"

For the en_GB locale, we will add translation entries in the app/code/Foggyline/
Office/i18n/en_GB.csv file, as follows:
"Translate me","en_GB Translate me"
"Var1 %1, Var2 %2, Var %3", "en_GB Var1 %1, Var2 %2, Var %3"
"Copyright %1 Magento","en_GB Copyright %1 Magento"

[ 151 ]

Backend Development

Looking at the two CSV files, a pattern emerges. We can see that the CSV files
function in the following way:
•

Individual translation strings are provided according to every line of CSV

•

Each line further comprises two individual strings that are separated
by a comma

•

Both individual strings are surrounded by quotes

•

If a string contains quotes, it is escaped by a double quote so that it does not
break translation

•

The %1, %2, %3...%n pattern is used to mark variable placeholders that we
provided during application runtime through the code

Magento supports several commands related to its bin/magento console tool:
i18n
i18n:collect-phrases

Discovers phrases in the codebase

i18n:pack

Saves language package

i18n:uninstall

Uninstalls language packages

If we execute a console command as follows, Magento will recursively look for
translatable expressions within PHP, PHTML, or XML files that have phrases to
translate:
php bin/magento i18n:collect-phrases -o
"/Users/branko/www/magento2/app/code/Foggyline/Office/i18n/en_GB.csv"
/Users/branko/www/magento2/app/code/Foggyline/Office

The output of the preceding command will basically overwrite the app/code/
Foggyline/Office/i18n/en_GB.csv file, which has all the Foggyline/Office
module translatable phrases. This is a nice way of aggregating all the translatable
phrases into appropriate locale files, such as en_GB.csv in this case.
The translation CSV files can also be placed under the individual theme. For
example, let's imagine a situation where we add content to app/design/frontend/
Magento/blank/i18n/en_GB.csv, as follows:
"Translate me","Theme_en_GB Translate me"
"Var1 %1, Var2 %2, Var %3", "Theme_en_GB Var1 %1, Var2 %2, Var %3"
"Copyright %1 Magento","Theme_en_GB Copyright
%1 Magento"

[ 152 ]

Chapter 7

Now, a Translate me string output of the storefront for the en_GB locale would
resolve to Theme_en_GB Translate me and not to the en_GB Translate me string.
Theme CSV translations take higher precedence than module CSV
translations, thus enabling developers to override individual module
translations.

Along with CSV translation files, Magento also supports a feature called inline
translation. We can activate the inline translation in the Magento admin area
by navigating to Store | Settings | Configuration | Advanced | Developer |
Translate Inline. This feature can be turned on separately for admin and storefront,
as shown in the following screenshot:

[ 153 ]

Backend Development

As shown in the preceding screenshot, when a feature is activated, red dotted
borders appear around the HTML elements. Hovering over an individual element
shows a little book icon near the individual element at the bottom left corner.
Clicking on the book icon opens a popup, as shown in the following screenshot:

It is important to note that these red dotted borders and the book icon will only
appear for strings that we passed through the __() translate function.
Here, we can see various pieces of information about the string, such as the Shown,
Translated, and Original string. There is also an input field called Custom,
where we can add a new translation. Inline translation strings are stored in the
translation table in the database.
Inline translation takes higher precedence than theme CSV
translation files.

[ 154 ]

Chapter 7

Indexer(s)

Indexing is the process of transforming data by reducing it to flattened data with less
database tables. This process is run for products, categories, and so on in order to
improve the performance of a web store. Since data constantly changes, this is not a
one-time process. Rather, it is a periodic one. The Magento_Indexer module is a base
of the Magento Indexing functionality.
The Magento console tool supports the following indexer commands.
indexer
indexer:info

Shows allowed Indexers

indexer:reindex

Reindexes Data

indexer:set-mode

Sets index mode type

indexer:show-mode

Shows Index Mode

indexer:status

Shows status of Indexer

On running php bin/magento indexer:info, you will get a list of all the Magento
indexers; the default ones are as follows:
catalog_category_product

Category Products

catalog_product_category

Product Categories

catalog_product_price

Product Price

catalog_product_attribute

Product EAV

foggyline_office_employee

Employee Flat Data

cataloginventory_stock

Stock

catalogrule_rule

Catalog Rule Product

catalogrule_product

Catalog Product Rule

catalogsearch_fulltext

Catalog Search

You will see all the indexers listed in the Magento admin in the System | Tools |
Index Management menu.
From within the admin area, we can only change the indexer mode. There are two
modes of indexers:
•

Update on Save: Index tables are updated right after the dictionary
data is changed

•

Update by Schedule: Index tables are updated by cron jobs according to the
configured schedule

[ 155 ]

Backend Development

Since indexers cannot be run manually from admin, we have to rely either on their
manual execution or the cron execution.
Manual execution is done via the following console command:
php bin/magento indexer:reindex

The preceding command will run all the indexers at once. We can fine-tune it further
to execute individual indexes by running a console command that is similar to the
following line of code:
php bin/magento indexer:reindex catalogsearch_fulltext

Cron-executed indexers are defined via the Magento_Indexer module, as follows:
•

indexer_reindex_all_invalid: This will execute every minute of every
hour every day. It runs the reindexAllInvalid method on an instance of
the Magento\Indexer\Model\Processor class.

•

indexer_update_all_views: This will execute every minute of every hour
every day. It runs the updateMview method on an instance of the Magento\
Indexer\Model\Processor class.

•

indexer_clean_all_changelogs: This will execute the 0th minute of every
hour every day. It runs the clearChangelog method on an instance of the
Magento\Indexer\Model\Processor class.

These cron jobs use an operating system cron job setup in such a way that the
Magento cron job is triggered every minute.
The following three statuses is what an indexer can have:
•

valid: The data is synchronized and no re-indexing is required

•

invalid: The original data was changed and the index should be updated

•

working: The index process is running

While we won't go into the details of actually creating a custom indexer within
this chapter, it is worth noting that Magento defines its indexers in the vendor/
magento/module-*/etc/indexer.xml file. This might come in handy for cases
where we want a deeper understanding of the inner workings of an individual
indexer. For example, the catalog_product_flat indexer is implemented via the
Magento\Catalog\Model\Indexer\Product\Flat class, as defined within the
vendor/magento/module-catalog/etc/indexer.xml file. By studying the Flat
class implementation in depth, you can learn how data is taken from EAV tables and
flattened into a simplified structure.

[ 156 ]

Chapter 7

Summary

In this chapter, we covered some of the most relevant aspects of Magento, which
was beyond models and classes, regarding backend development. We had a look
at crontab.xml, which helps us schedule jobs (commands) so that they can be
run periodically. Then, we tackled notification messages, which enable us to push
styled messages to users via a browser. The Session and cookies section gave us an
understanding of how Magento tracks user information from a browser to a session.
Logging and profiling showed us a simple yet effective mechanism to keep track
of performance and possible issues across code. The Events and observers section
introduced us to a powerful pattern that Magento implements across the code, where
we can trigger custom code execution when a certain event is fired. The section on
caching guided us through the available cache types, and we studied how to create
and use our own cache type. Through the section on frontend apps (widgets), we
learned how to create our own miniature apps that can be called into CMS pages
and blocks. Custom variables gave us an insight into a simple yet interesting feature,
where we can define a variable via the admin interface and then use it within CMS
page, block, or e-mail template. The section on i18n showed us how to use the
Magento translation feature to translate any string on three different levels,
namely the module CSV file, the theme CSV file, and inline translation. Finally,
we had a look at indexers and their mode and status; we learned how to control
their execution.
The next chapter will tackle frontend development. We will learn how create our
own theme and use blocks and layouts to affect the output.

[ 157 ]

Frontend Development
Frontend development is a term most commonly tied to producing HTML, CSS,
and JavaScript for a website or web application. Interchangeably, it addresses
accessibility, usability, and performance toward reaching a satisfying user
experience. Various levels of customization we want to apply to our web store
require different development skill levels. We can make relatively simple changes
to our store using just CSS. These would be the changes where we accept the
structure of the store and focus only on visuals like changing colors and images.
This might be a good starting point for less experienced developers and those new
to the Magento platform. A more involved approach would be to make changes
to the output generated by Magento modules. This usually means tiny bits of PHP
knowledge, mostly copy-paste-modify of existing code fragments. A skill level above
this one would imply knowledge of making structural changes to our store. This
usually means mastering Magento's moderately sophisticated layout engine, where
we make changes through XML definitions. The final and highest skill level for
Magento frontend development implies the modification of existing or new custom
functionality development.
Throughout this chapter, we will take a deep dive through the following sections:
•

Rendering flow

•

View elements

•

Block architecture and life cycle

•

Templates

•

XML layouts

•

Themes

•

JavaScript

•

CSS

[ 159 ]

Frontend Development

Rendering flow

The Magento application entry point is its index.php file. All of the HTTP requests
go through it.
Let's analyze the (trimmed) version of the index.php file as follows:
//PART-1-1
require __DIR__ . '/app/bootstrap.php';
//PART-1-2
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP,
$_SERVER);
//PART-1-3
$app = $bootstrap->
createApplication('Magento\Framework\App\Http');
//PART-1-4
$bootstrap->run($app);

PART-1-1 of the preceding code simply includes /app/bootstrap.php into the
code. What happens inside the bootstrap is the inclusion of app/autoload.php and
app/functions.php. The functions file contains a single __() function, used for
translation purposes, returning an instance of the \Magento\Framework\Phrase

object. Without going into the details of the auto-load file, it is suffice to say it
handles the auto-loading of all our class files across Magento.

PART-1-2 is simply a static create method call to obtain the instance of the \
Magento\Framework\App\Bootstrap object, storing it into the $bootstrap variable.
PART-1-3 is calling the createApplication method on the $bootstrap object. What
is happening within createApplication is nothing more than using object manager
to create and return the object instance of the class we are passing to it. Since we are
passing the \Magento\Framework\App\Http class name to the createApplication
method, our $app variable becomes the instance of that class. What this means,
effectively, is that our web store app is an instance of Magento\Framework\App\
Http.
PART-1-4 is calling the run method on the $bootstrap object, passing it the instance
of the Magento\Framework\App\Http class. Although it looks like a simple line of

code, this is where things get complicated, as we will soon see.

[ 160 ]

Chapter 8

Let's analyze the (trimmed) version of the \Magento\Framework\App\Bootstrap ->
run method as follows:
public function run(\Magento\Framework\AppInterface $application)
{
//PART-2-1
$this->initErrorHandler();
$this->initObjectManager();
$this->assertMaintenance();
$this->assertInstalled();
//PART-2-2
$response = $application->launch();
//PART-2-3
$response->sendResponse();
}

In the preceding code, PART-2-1 handles the sort of housekeeping bits. It initializes
the custom error handler, initializes the object manager, checks if our application is
in maintenance mode, and checks that it is installed.
PART-2-2 looks like a simple line of code. Here, we are calling the launch method
on $application, which is the Magento\Framework\App\Http instance. Without
going into the inner workings of the launch method for the moment, let's just say it
returns the instance of the Magento\Framework\App\Response\Http\Interceptor
class defined under var/generation/Magento/Framework/App/Response/Http/
Interceptor.php. Note that this is an automatically generated wrapper class,
extending the \Magento\Framework\App\Response\Http class. Effectively, ignoring
Interceptor, we can say that $response is an instance the \Magento\Framework\
App\Response\Http class.

Finally, PART-2-3 calls the sendResponse method on $response. Though
$response is an instance of the \Magento\Framework\App\Response\Http class,
the actual sendResponse method is found further down the parent tree on the \
Magento\Framework\HTTP\PhpEnvironment\Response class. The sendResponse
method calls another parent class method called send. The send method can be
found under the Zend\Http\PhpEnvironment\Response class. It triggers the
sendHeaders and sendContent methods. This is where the actual output gets sent to
the browser, as the sendHeaders method is using PHP's header function and echo
construct to push the output.

[ 161 ]

Frontend Development

To reiterate on the preceding, the flow of execution as we understand it comes down
to the following:
•

index.php

•

\Magento\Framework\App\Bootstrap -> run

•

\Magento\Framework\App\Http -> launch

•

\Magento\Framework\App\Response\Http -> sendResponse

Though we have just made it to the end of the bootstrap's run method, it would be
unfair to say we covered the rendering flow, as we barely touched it.
We need to take a step back and take a detailed look at PART-2-2, the inner workings
of the launch method. Let's take a look at the (trimmed) version of the \Magento\
Framework\App\Http -> launch method as follows:
public function launch()
{
//PART-3-1
$frontController = $this->_objectManager->get
('Magento\Framework\App\FrontControllerInterface');
//PART-3-2
$result = $frontController->dispatch($this->_request);
if ($result instanceof \Magento\Framework\Controller
\ResultInterface) {
//PART-3-3
$result->renderResult($this->_response);
} elseif ($result instanceof \Magento\Framework\App
\Response\HttpInterface) {
$this->_response = $result;
} else {
throw new \InvalidArgumentException('Invalid return
type');
}
//PART-3-4
return $this->_response;
}

[ 162 ]

Chapter 8

PART-3-1 creates the instance of the object whose class conforms to \Magento\
Framework\App\FrontControllerInterface. If we look under app/etc/di.xml,
we can see there is a preference for FrontControllerInterface in favor of the \
Magento\Framework\App\FrontController class. However, if we were to debug
the code and check for the actual instance class, it would show Magento\Framework\
App\FrontController\Interceptor. This is Magento adding an interceptor
wrapper that then extends \Magento\Framework\App\FrontController, which we
expected from the di.xml preference entry.

Now that we know the real class behind the $frontController instance, we
know where to look for the dispatch method. The dispatch method is another
important step in understanding the rendering flow process. We will look into its
inner workings in a bit more detail later on. For now, let's focus back on the $result
variable of PART-3-2. If we were to debug the variable, the direct class behind it
would show as Magento\Framework\View\Result\Page\Interceptor, defined
under the dynamically created var/generation/Magento/Framework/View/
Result/Page/Interceptor.php file. Interceptor is the wrapper for the \Magento\
Framework\View\Result\Page class. Thus, it is safe to say that our $result variable
is an instance of the Page class.
The Page class extends \Magento\Framework\View\Result\Layout, which further
extends \Magento\Framework\Controller\AbstractResult and implements \
Magento\Framework\Controller\ResultInterface. Quite a chain we have here,
but it is important to understand it.
Notice PART-3-3. Since our $result is an instance of \Magento\Framework\
Controller\ResultInterface, we fall into the first if condition that calls the
renderResult method. The renderResult method itself is declared within the \
Magento\Framework\View\Result\Layout class. Without going into the details of
renderResult, suffice to say that it adds HTTP headers, and content to the $this->_
response object passed to it. That same response object is what the launch method
returns, as we described before in PART-2-2.
Though PART-3-3 does not depict any return value, the expression $result>renderResult($this->_response) does not do any output on its own. It modifies
$this->_response that we finally return from the launch method as shown in
PART-3-4.
To reiterate on the preceding, the flow of execution as we understand it comes down
to the following:
•

index.php

•

\Magento\Framework\App\Bootstrap -> run

•

\Magento\Framework\App\Http -> launch
[ 163 ]

Frontend Development

•

\Magento\Framework\App\FrontController -> dispatch

•

\Magento\Framework\View\Result\Page -> renderResult

•

\Magento\Framework\App\Response\Http -> sendResponse

As we mentioned while explaining PART-3-2, the dispatch method is another
important step in the rendering flow process. Let's take a look at the (trimmed)
version of the \Magento\Framework\App\FrontController -> dispatch method
as follows:
public function dispatch(\Magento\Framework\App\RequestInterface
$request)
{
//PART-4-1
while (!$request->isDispatched() && $routingCycleCounter++ <
100) {
//PART-4-2
foreach ($this->_routerList as $router) {
try {
//PART-4-3
$actionInstance = $router->match($request);
if ($actionInstance) {
$request->setDispatched(true);
//PART-4-4
$result = $actionInstance->dispatch($request);
break;
}
} catch (\Magento\Framework\Exception
\NotFoundException $e) {}
}
}
//PART-4-4
return $result;
}

PART-4-1 and PART-4-2 in the preceding code shows (almost) the entire dispatch
method body contained within a loop. The loop does 100 iterations, further looping
through all available router types, thus giving each router 100 times to find a
route match.

The router list loop includes routers of the following class types:
•

Magento\Framework\App\Router\Base

•

Magento\UrlRewrite\Controller\Router

•
•

Magento\Cms\Controller\Router
Magento\Framework\App\Router\DefaultRouter

[ 164 ]

Chapter 8

All of the listed routers implement \Magento\Framework\App\RouterInterface,
making them all have the implementation of the match method.
A module can further define new routers if they choose so. As an example, imagine if
we are developing a Blog module. We would want our module catching all requests
on a URL that starts with a /blog/ part. This can be done by specifying the custom
router, which would then show up on the preceding list.
PART-4-3 shows the $actionInstance variable storing the result of the router
match method call. As per RouterInterface requirements, the match method is
required to return an instance whose class implements \Magento\Framework\App\
ActionInterface. Let's imagine we are now hitting the URL /foggyline_office/
test/crud/ from the module we wrote in Chapter 4, Models and Collections. In this
case, our $router class would be \Magento\Framework\App\Router\Base and
our $actionInstance would be of the class \Foggyline\Office\Controller\
Test\Crud\Interceptor. Magento automatically adds Interceptor, through
the dynamically generated var/generation/Foggyline/Office/Controller/
Test/Crud/Interceptor.php file. This Interceptor class further extends our
module \Foggyline\Office\Controller\Test\Crud class file. The Crud class
extends \Foggyline\Office\Controller\Test, which further extends \Magento\
Framework\App\Action\Action, which implements \Magento\Framework\
App\ActionInterface. After a lengthy parent-child tree, we finally got to
ActionInterface, which is what our match method is required to return.
PART-4-4 shows the dispatch method being called on $actionInstance. This
method is implemented within \Magento\Framework\App\Action\Action,
and is expected to return an object that implements \Magento\Framework\App\
ResponseInterface. Internal to dispatch, the execute method is called, thus
running the code within our Crud controller action execute method.

Assuming our Crud controller action execute method does not return nothing, the
$result object becomes an instance of Magento\Framework\App\Response\Http\
Interceptor, which is wrapped around \Magento\Framework\App\Response\
Http.
Let's imagine our Crud class has been defined as follows:
/**
* @var \Magento\Framework\View\Result\PageFactory
*/
protected $resultPageFactory;
public function __construct(
\Magento\Framework\App\Action\Context $context,

[ 165 ]

Frontend Development
\Magento\Framework\View\Result\PageFactory $resultPageFactory
)
{
$this->resultPageFactory = $resultPageFactory;
return parent::__construct($context);
}
public function execute()
{
$resultPage = $this->resultPageFactory->create();
//...
return $resultPage;
}

Debugging the $result variable now shows it's an instance of \Magento\
Framework\View\Result\Page\Interceptor. This Interceptor gets dynamically
generated by Magento under var/generation/Magento/Framework/View/Result/
Page/Interceptor.php and is merely a wrapper for \Magento\Framework\
View\Result\Page. This Page class further extends the \Magento\Framework\
View\Result\Layout class, and implements \Magento\Framework\App\
ResponseInterface.
Finally, PART-4-4 shows the $result object of type \Magento\Framework\View\
Result\Page being returned from the FrontController dispatch method.
To reiterate on the preceding, the flow of execution as we understand it comes down
to the following:
•

index.php

•

\Magento\Framework\App\Bootstrap -> run

•

\Magento\Framework\App\Http -> launch

•

\Magento\Framework\App\FrontController -> dispatch

•

\Magento\Framework\App\Router\Base -> match

•

\Magento\Framework\App\Action\Action -> dispatch

•

\Magento\Framework\View\Result\Page -> renderResult

•

\Magento\Framework\App\Response\Http -> sendResponse

In a nutshell, what we as frontend developers should know is that returning the
Page type object from our controller action will automatically call the renderResult
method on that object. Page and Layout is where all the theme translations, layout,
and template loading are triggering.

[ 166 ]

Chapter 8

View elements

Magento's primary view elements are its UI Components, containers, and blocks.
The following is a brief overview of each of them.

Ui components

Under the vendor/magento/framework/View/Element/ folder, we can find
UiComponentInterface and UiComponentFactory. The full set of Ui components is
located under the vendor/magento/framework/View/Element/ directory. Magento
implements UiComponent through a separate module called Magento_Ui. Thus,
the components themselves are located under the vendor/magento/module-ui/
Component/ directory.
Components implement UiComponentInterface, which is defined under the
vendor/magento/framework/View/Element/UiComponentInterface.php

file as follows:

namespace Magento\Framework\View\Element;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
interface UiComponentInterface extends BlockInterface
{
public function getName();
public function getComponentName();
public function getConfiguration();
public function render();
public function addComponent($name, UiComponentInterface
$component);
public function getComponent($name);
public function getChildComponents();
public function getTemplate();
public function getContext();
public function renderChildComponent($name);
public function setData($key, $value = null);
public function getData($key = '', $index = null);
public function prepare();
public function prepareDataSource(array & $dataSource);
public function getDataSourceData();
}

[ 167 ]

Frontend Development

Notice how BlockInterface extends BlockInterface, whereas BlockInterface
defines only one method requirement as follows:
namespace Magento\Framework\View\Element;
interface BlockInterface
{
public function toHtml();
}

Since Block is an element of the interface, UiComponent can be looked at as an
advanced block. Let's take a quick look at the _renderUiComponent method of the \
Magento\Framework\View\Layout class, (partially) defined as follows:
protected function _renderUiComponent($name)
{
$uiComponent = $this->getUiComponent($name);
return $uiComponent ? $uiComponent->toHtml() : '';
}

This shows that UiComponent is rendered in the same way as block, by calling
the toHtml method on the component. The vendor/magento/module-ui/view/
base/ui_component/etc/definition.xml file contains an extensive list of several
UiComponents as follows:
•

dataSource: Magento\Ui\Component\DataSource

•

listing: Magento\Ui\Component\Listing

•

paging: Magento\Ui\Component\Paging

•

filters: Magento\Ui\Component\Filters

•

container: Magento\Ui\Component\Container

•

form: Magento\Ui\Component\Form

•

price: Magento\Ui\Component\Form\Element\DataType\Price

•

image: Magento\Ui\Component\Form\Element\DataType\Media

•

nav: Magento\Ui\Component\Layout\Tabs\Nav

… and many more

[ 168 ]

Chapter 8

These components are mostly used to construct a listing and filters in the admin
area. If we do a string search for uiComponent across the entire Magento, we would
mostly find entries like the one in vendor/magento/module-cms/view/adminhtml/
layout/cms_block_index.xml with content as follows:








The value cms_block_listing of uiComponent's name attribute refers to the
name of the vendor/magento/module-cms /view/adminhtml/ui_component/
cms_block_listing.xml file. Within the cms_block_listing.xml file, we have a
listing component defined across more than a few hundreds lines of XML. Listing
component then dataSource, container, bookmark, filterSearch, filters, and
so on. We will not go into the details of these declarations, as our focus here is on
more general frontend bits.

Containers

Containers have no block classes related to them. Container renders all of its children
automatically. They allow the configuration of some attributes. Simply attach any
element to a container and it will render it automatically. With a container, we can
define wrapping tags, CSS classes, and more.
We cannot create instances of containers because they are an abstract concept,
whereas we can create instances of blocks.
Containers are rendered via the _renderContainer method of the Magento\
Framework\View\Layout class, defined as follows:
protected function _renderContainer($name)
{
$html = '';
$children = $this->getChildNames($name);
foreach ($children as $child) {
$html .= $this->renderElement($child);
}

[ 169 ]

Frontend Development
if ($html == '' || !$this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_TAG)) {
return $html;
}
$htmlId = $this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_ID);
if ($htmlId) {
$htmlId = ' id="' . $htmlId . '"';
}
$htmlClass = $this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_CLASS);
if ($htmlClass) {
$htmlClass = ' class="' . $htmlClass . '"';
}
$htmlTag = $this->structure->getAttribute($name,
Element::CONTAINER_OPT_HTML_TAG);
$html = sprintf('<%1$s%2$s%3$s>%4$s', $htmlTag,
$htmlId, $htmlClass, $html);
return $html;
}

Containers support the following extra attributes: htmlTag, htmlClass, htmlId,
and label. To make a little demonstration of a container in action, let us make sure
we have a module from Chapter 4, Models and Collections in place, and then create
the view/frontend/layout/foggyline_office_test_crud.xml file within the
module root folder app/code/Foggyline/Office/ with content as follows:


Office CRUD #layout





[ 170 ]

Chapter 8

The Foo

]]>
The Bar

]]>
The preceding XML defines a single container named foobar, and within the container there are two block elements named foo and bar. It should kick in when we open http://{our-shop-url}/index.php/foggyline_office/test/crud/ in the browser. Notice how the container itself is not nested within any other element, rather directly into the body. We could have easily nested into some other container as shown: Either way, we should see the strings The Foo and The Bar shown in the browser, with a full-page layout loaded, as shown in the following screenshot: [ 171 ] Frontend Development Blocks Although containers determine the layout of the page, they do not contain actual content directly. Pieces that contain the content and are nested within containers are called blocks. Each block can contain any number of child content blocks or child containers. Thus, mostly every web page in Magento is formed as a mix of blocks and containers. Layout defines a sequence of blocks on the page, not their location. The look and feel of the blocks is determined by CSS and how the page is rendered. When we speak of blocks, we almost always implicitly refer to templates as well. Templates are the thing that actually draw elements within a page; blocks are the thing that contain the data. In other words, templates are PHTML or HTML files pulling data through variables or methods sent on a linked PHP block class. Magento defines the Magento\Framework\View\Result\Page type under app/etc/ di.xml as follows: pageConfigRenderPool pageLayoutGeneratorPool Magento_Theme::root.phtml Notice the template argument is set to Magento_Theme::root.phtml. When Page gets initialized, it picks up the vendor/magento/module-theme/view/base/ templates/root.phtml file. root.phtml is defined as follows: > > > [ 172 ] Chapter 8 Variables within root.phtml are assigned during the Magento\Framework\View\ Result\Page render method call as (partially) as shown: protected function render(ResponseInterface $response) { $this->pageConfig->publicBuild(); if ($this->getPageLayout()) { $config = $this->getConfig(); $this->addDefaultBodyClasses(); $addBlock = $this->getLayout()->getBlock ('head.additional'); $requireJs = $this->getLayout()->getBlock('require.js'); $this->assign([ 'requireJs' => $requireJs ? $requireJs->toHtml() : null, 'headContent' => $this->pageConfigRenderer-> renderHeadContent(), 'headAdditional' => $addBlock ? $addBlock->toHtml() : null, 'htmlAttributes' => $this->pageConfigRenderer-> renderElementAttributes($config::ELEMENT_TYPE_HTML), 'headAttributes' => $this->pageConfigRenderer-> renderElementAttributes($config::ELEMENT_TYPE_HEAD), 'bodyAttributes' => $this->pageConfigRenderer-> renderElementAttributes($config::ELEMENT_TYPE_BODY), 'loaderIcon' => $this->getViewFileUrl('images/loader2.gif'), ]); $output = $this->getLayout()->getOutput(); $this->assign('layoutContent', $output); $output = $this->renderPage(); $this->translateInline->processResponseBody($output); $response->appendBody($output); } else { parent::render($response); } return $this; } The expression $this->assign is what assigns variables like layoutContent to the root.phtml template. layoutContent is generated based on base layouts, together with all layout updates for the current page. [ 173 ] Frontend Development Whereas base layouts include the following XMLs within vendor/magento/module- theme/view/: • base/page_layout/empty.xml • frontend/page_layout/1column.xml • frontend/page_layout/2columns-left.xml • frontend/page_layout/2columns-right.xml • frontend/page_layout/3columns.xml The expression $this->getLayout()->getOutput() is what gets all blocks marked for output. It basically finds elements in a layout, renders them, and returns the string with its output. Along the way, the event core_layout_render_element gets fired, giving us one possible way of affecting the output result. At this point, most of the elements on the page are rendered. This is important because blocks play a big role here. The rendering system will take empty.xml into account, as it too consists of a list of containers, and every container has some blocks attached to it by other layout updates. In a nutshell, each container has blocks assigned to it. Each block usually (but not always) renders a template. The template itself may or may not call other blocks, and so on. Blocks are rendered when they are called from the template. Block architecture and life cycle Blocks are another one of the primary view elements in Magento. At the root of the parent tree structure, blocks extend from the Magento\Framework\View\Element\ AbstractBlock class and implement Magento\Framework\View\Element\ BlockInterface. BlockInterface sets only one requirement, the implementation of the toHtml method. This method should return blocks HTML output. Looking inside AbstractBlock, we can see it has a number of methods declared. Among the most important ones are the following methods: • _prepareLayout: Prepares a global layout. We can redefine this method in child classes for changing the layout. • addChild: Creates a new block, sets it as a child of the current block, and returns the newly created block. [ 174 ] Chapter 8 • _toHtml: Returns an empty string. We need to override this method in descendants to produce HTML. • _beforeToHtml: Returns $this. Executes before rendering HTML, but after • _afterToHtml: Processing block HTML after rendering. Returns a HTML • toHtml: Produces and returns a block's HTML output. This method should not be overridden. We can override the _toHtml method in descendants if needed. trying to load a cache. string. The AbstractBlock execution flow can be described as follows: • _prepareLayout • toHtml • _beforeToHtml • _toHtml • _afterToHtml It starts with _prepareLayout and flows through a set of methods until it reaches _afterToHtml. This is, in essence, what we need to know about block execution flow. The most important block types are: • Magento\Framework\View\Element\Text • Magento\Framework\View\Element\Text\ListText • Magento\Framework\View\Element\Messages • Magento\Framework\View\Element\Template All of these blocks are basically an implementation of an abstract block. Since the _toHtml method in AbstractBlock returns only an empty string, all of these descendants are implementing their own version of the _toHtml method. To demonstrate the usage of these blocks, we can use our previously created app/ code/Foggyline/Office/view/frontend/layout/foggyline_office_test_ crud.xml file. [ 175 ] Frontend Development The Text block has a setText method we can use to set its content. The way we instantiate the Text block and set its text value through the layout file is shown as follows: Text_1

]]>
The ListText block extends from Text. However, it does not really support the use of setText to set its content. This is obvious just by looking at its code, where the $this->setText('') expression is immediately called within its _toHtml method implementation. Instead, what happens is that the _toHtml method loops through any child blocks it might have and calls the layout's renderElement method on it. Basically, we might compare the ListText block to container, as it has nearly the same purpose. However, unlike container, block is a class so we can manipulate it from PHP. The following is an example of using ListText, containing a few child Text blocks: Text_2A

]]>
Text_2B

]]>
[ 176 ] Chapter 8 The Messages block supports four methods that we can use to add content to output: addSuccess, addNotice, addWarning, and addError. The following is an example instantiating the Messages block through the layout update file: Text_3A: Success

]]>
Text_3B: Notice

]]>
Text_3C: Warning

]]>
Text_3D: Error

]]>
The preceding example should be taken with caution, since calling these setter methods in layout is not the proper way to do it. The default Magento_Theme module already defines the Messages block that uses vendor/magento/module-theme/ view/frontend/templates/messages.phtml for message rendering. Thus, for most of the part there is no need to define our own messages block. Finally, let's look at the example of the Template block as follows: The preceding XML will instantiate the Template type of block and render the content of the view/frontend/templates/office/no4/template.phtml file within the app/code/Foggyline/Office/ directory. On the PHP level, instantiating a new block can be accomplished using the layout object, or directly through the object manager. The layout approach is the preferred way. With regard to the previous examples in XML, let's see their alternatives in PHP (assuming $resultPage is an instance of \Magento\Framework\View\Result\ PageFactory). [ 177 ] Frontend Development The following is an example of instantiating the Text type of block and adding it as a child of the content container: $block = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Text', 'example_1' )->setText( '

Text_1

' ); $resultPage->getLayout()->setChild( 'content', $block->getNameInLayout(), 'example_1_alias' ); The ListText version is done in PHP as follows: $blockLT = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Text\ListText', 'example_2' ); $resultPage->getLayout()->setChild( 'content', $blockLT->getNameInLayout(), 'example_2_alias' ); $block2A = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Text', 'example_2a' )->setText( '

Text_2A

' ); $resultPage->getLayout()->setChild( 'example_2', $block2A->getNameInLayout(), 'example_2a_alias' ); $block2B = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Text', 'example_2b' [ 178 ] Chapter 8 )->setText( '

Text_2B

' ); $resultPage->getLayout()->setChild( 'example_2', $block2B->getNameInLayout(), 'example_2b_alias' ); Notice how we first made an instance of the ListText block and assigned it as a child of an element named content. Then we created two individual Text blocks and assigned them as a child of an element named example_2, which is our ListText. Next, let's define the Messages block as follows: $messagesBlock = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Messages', 'example_3' ); $messagesBlock->addSuccess('Text_3A: Success'); $messagesBlock->addNotice('Text_3B: Notice'); $messagesBlock->addWarning('Text_3C: Warning'); $messagesBlock->addError('Text_3D: Error'); $resultPage->getLayout()->setChild( 'content', $messagesBlock->getNameInLayout(), 'example_3_alias' ); Finally, let's look at the Template block type, which we initiate as follows: $templateBlock = $resultPage->getLayout()->createBlock( 'Magento\Framework\View\Element\Template', 'example_3' )->setTemplate( 'Foggyline_Office::office/no4/template.phtml' ); $resultPage->getLayout()->setChild( 'content', $templateBlock->getNameInLayout(), 'example_4_alias' ); [ 179 ] Frontend Development Whenever possible, we should set our blocks using XML layouts. Now that we know how to utilize the most common types of Magento blocks, let's see how we can create our own block type. Defining our own block class is as simple as creating a custom class file that extends Template. This block class should be placed under our module Block directory. Using our Foggyline_Office module, let's create a file, Block/Hello.php, with content as follows: namespace Foggyline\Office\Block; class Hello extends \Magento\Framework\View\Element\Template { public function helloPublic() { return 'Hello #1'; } protected function helloProtected() { return 'Hello #2'; } private function helloPrivate() { return 'Hello #3'; } } The preceding code simply creates a new custom block class. We can then call this block class through our layout file as follows: Finally, within our module app/code/Foggyline/Office/ directory, we create a template file, view/frontend/templates/office/hello.phtml, with content as follows:

Hello

helloPublic() ?>

helloProtected() ?>

helloPrivate() ?>

[ 180 ] Chapter 8 To further understand what is happening here within the template file, let's take a deeper look at templates themselves. Templates Templates are snippets of HTML mixed with PHP. The PHP part includes elements such as variables, expressions, and class method calls. Magento uses the PHTML file extension for template files. Templates are located under an individual module's view/{_area_}/templates/ directory. In our previous example, we referred to our module template file with an expression like Foggyline_Office::office/hello.phtml. Since templates can belong to different modules, we should prepend the template with the module name as a best practice. This will help us locate template files and avoid file conflicts. A simple naming formula goes like this: we type the name of the module, double single colon, and then the name. Thus making a template path like office/hello. phtml equaling to Foggyline_Office::office/hello.phtml. Within the PHTML template file we often have various PHP expressions like $block->helloPublic(). Notice the block class Foggyline\Office\Block\ Hello in the preceding XML. An instance of this block class becomes available to us in hello.phtml through the $block variable. Thus, an expression like $block>helloPublic() is effectively calling the helloPublic method from an instance of the Hello class. The Hello class is not one of the Magento core classes, but it does extend \Magento\Framework\View\Element\Template. Our hello.phtml template also has two more expressions: $block>helloProtected() and $block->helloPrivate(). However, these are not executed as template files can only see public methods from their $block instances. The $this variable is also available within the PHTML template as an instance of the Magento\Framework\View\TemplateEngine\Php class. In the preceding template code example, we could have easily replaced $block>helloPublic() with the $this->helloPublic() expression. The reason why this would work lies in the template engine Php class, (partially) defined as follows: public function __call($method, $args) { return call_user_func_array([$this->_currentBlock, $method], $args); } public function __isset($name) { [ 181 ] Frontend Development return isset($this->_currentBlock->{$name}); } public function __get($name) { return $this->_currentBlock->{$name}; } Given that templates are included in the context of the engine rather than in the context of the block, __call redirects methods calls to the current block. Similarly, __isset redirects isset calls to the current block and __get allows read access to properties of the current block. Though we can use both $block and $this for the same purpose within the template file, we should really opt for using $block. Another important aspect of templates is their fallback mechanism. Fallback is the process of defining a full template path given only its relative path. For example, office/hello.phtml falls back to the app/code/Foggyline/Office/view/ frontend/templates/office/hello.phtml file. Path resolution starts from the _toHtml method defined on the Magento\ Framework\View\Element\Template class. The _toHtml method then calls getTemplateFile within the same class, which in turn calls getTemplateFileName on resolver, which is an instance of \Magento\Framework\View\Element\ Template\File\Resolver. Looking further, resolver's getTemplateFileName further calls getTemplateFileName on _viewFileSystem, which is an instance of \ Magento\Framework\View\FileSystem. The method getFile is further called on an instance of \Magento\Framework\View\Design\FileResolution\Fallback\ TemplateFile. getFile further triggers the resolve method on the Magento\ Framework\View\Design\FileResolution\Fallback\Resolver\Simple instance, which further calls the getRule method on the Magento\Framework\View\Design\ Fallback\RulePool instance. The RulePoll class is the final class in the chain here. getRule finally calls the createTemplateFileRule method, which creates the rule that detects where the file is located. While running the getRule method, Magento checks against the following types of fallback rules: • file • locale • template • • static email [ 182 ] Chapter 8 It is worth spending some time to study the inner workings of the RulePool class, as it showcases detailed fallbacks for the listed rules. Layouts Up to this point, we briefly touched on layout XMLs. Layout XML is a tool to build the pages of the Magento application in a modular and flexible manner. It enables us to describe the page layout and content placement. Looking at XML root nodes, we differentiate two types of layouts: • layout: XML wrapped in • page: XML wrapped in Page layouts represent a full page in HTML, whereas layout layouts represent a part of a page. The layout type is a subset of the page layout type. Both types of layout XML files are validated by the XSD schema found under the vendor/ magento/framework/View/Layout/etc/ directory: • layout – layout_generic.xsd • page – page_configuration.xsd Based on the application components that provide and elements , we can further section them as base and theme layouts. The base layouts are provided by the modules, usually at the following locations: • • /view/frontend/layout: page configuration and generic layout files /view/frontend/page_layout: page layout files The theme layouts are provided by the themes, usually at the following locations: • /_/layout: page configuration and • /_/page_layout: page layout files generic layout files Magento will load and merge all module and theme XML files on the appropriate page. Once files are merged and XML instructions are processed, the result is rendered and sent to the browser for display. Having two different layout XML files, where both reference the same block, means that the second one with the same name in the sequence will replace the first one. [ 183 ] Frontend Development When the XML files are loaded, Magento applies an inheritance theme at the same time. We can apply a theme and it will look for the parent until a theme without a parent is reached. In addition to the merging of files from each module, layout files from within module directories can also be extended or overridden by themes. Overriding layout XML is not a good practice, but it might be necessary sometimes. To override the base layout files provided by the module within the / view/frontend/layout/directory. We need to create an XML file with the same name in the app/design/frontend////layout/override/base/directory. To override the theme layout files provided by the parent theme within the /_/layout/directory. We need to create an XML file with the same name in the app/design/ frontend////layout/override/ theme///directory. Layouts can be both overridden and extended. The recommended way to customize layout is to extend it through a custom theme. We can do so by simply adding a custom XML layout file with the same name in the app/design/frontend/{vendorName}/{theme}/{vendorName}_{moduleName}/ layout/ directory. Layouts, as we saw in previous examples, support a large number of directives: page page, head, block, and so on. The practical use of these directives and how they mix together is a challenge on its own. Giving full details on each and every directive is beyond the scope of this book. However, what we can do is to show how to figure out the use of an individual directive, which we might need at a given time. For that purpose, it is highly recommended to use an IDE environment like NetBeans PHP or PhpStorm that provide autocomplete on XMLs that include XSD. The following is an example of defining an external schema to PhpStorm, where we are simply saying that the urn:magento:framework:View/Layout/etc/ page_configuration.xsd alias belongs to the vendor/magento/framework/View/ Layout/etc/page_configuration.xsd file: [ 184 ] Chapter 8 This way, PhpStorm will know how to provide autocomplete while we type around XML files. As an example, let's take a look at how we could use the css directive to add an external CSS file to our page. With an IDE that supports autocomplete as soon as we type the css directive within the page | head element, autocomplete might throw out something like the following: A list of available attributes is shown, such as src, sizes, ie_condtion, src_type, and so on. IDEs like PhpStorm will allow us to right-click an element or its attribute and go to the definition. Looking into the definition for the src attribute gets us into the vendor/magento/framework/View/Layout/etc/head.xsd file that defines the css element as follows: [ 185 ] Frontend Development name="hreflang" type="xs:string"/> name="media" type="xs:string"/> name="rel" type="xs:string"/> name="rev" type="xs:string"/> name="sizes" type="xs:string"/> name="target" type="xs:string"/> name="type" type="xs:string"/> name="src_type" type="xs:string"/> All of these are attributes we can set on the css element, and as such get their autocomplete as shown: Although it is not required to use a robust IDE with Magento, it certainly helps to have one that understands XML and XSD files to the level of providing autocomplete and validation. Themes By default, Magento comes with two themes, named Blank and Luma. If we log in to the Magento admin area, we can see a list of available themes under the Content | Design | Themes menu, as shown in the following screenshot: [ 186 ] Chapter 8 Magento themes support a parent-child relationship, something we noted previously, that is visible on the preceding image within the Parent Theme column. Creating a new theme The following steps outline the process of creating our own theme: 1. Under {Magento root directory}/app/design/frontend, create a new directory bearing our vendor name, Foggyline. 2. Within the vendor directory, create a new directory bearing the theme name, jupiter. 3. Within the jupiter directory, create the registration.php file with content as follows: Foggyline Jupiter Magento/blank [ 187 ] Frontend Development media/preview.jpg 5. Create the app/design/frontend/Foggyline/jupiter/media/preview. jpg image file to serve as the theme preview image (the one used in the admin area). 6. Optionally, create separate directories for static files such as styles, fonts, JavaScript, and images. These are stored within the web subdirectory of our theme app/design/frontend/Foggyline/jupiter/ folder like follows: °° web/css/ °° web/css/source/ °° web/css/source/components/ °° web/images/ °° web/js/ Within the theme web directory, we store general theme static files. If our theme contains module-specific static files, these are stored under the corresponding vendor module subdirectories, like app/design/frontend/ Foggyline/jupiter/{vendorName_moduleName}/web/. 7. Optionally, we can create the theme logo.svg image under our theme web/images/ folder. Once we are done with the preceding steps, looking back into the admin area under the Content | Design | Themes menu, we should now see our theme listed as shown in the following screenshot: [ 188 ] Chapter 8 Whereas clicking on the row in the table next to our theme name would open a screen like the following: Notice how the previous two screens do not show any options to apply the theme. They are only listing out available themes and some basic information next to each theme. Our custom theme shows an interesting relationship, where a parent and a child theme can belong to different vendors. Applying the theme requires the following extra steps: 1. Make sure our theme appears in the theme list, under the Content | Design | Themes menu. 2. Go to Stores | Settings | Configuration | General | Design. [ 189 ] Frontend Development 3. In the Store View drop-down field, we select the store view where we want to apply the theme, as shown in the upper-left corner of the following image: 4. On the Design Theme tab, we select our newly created theme in the Design Theme drop-down, as shown on the right-hand side of the preceding image. Click Save Config. 5. Under System | Tools | Cache Management, select and refresh the invalid cache types and click on the Flush Catalog Images Cache, Flush JavaScript/ CSS Cache, and Flush Static Files Cache buttons. 6. Finally, to see our changes applied, reload the storefront pages in the browser. There is a lot more to be said about themes that can fit in a book of its own. However, we will move on to the other important bits. JavaScript Magento makes use of quite a large number of JavaScript libraries, such as: • Knockout: http://knockoutjs.com • Ext JS: https://www.sencha.com/products/extjs/ • jQuery: https://jquery.com/ • jQuery UI: https://jqueryui.com/ • modernizr: http://www.modernizr.com/ • Prototype: http://www.prototypejs.org/ • RequireJS: http://requirejs.org/ [ 190 ] Chapter 8 • script.aculo.us: http://script.aculo.us/ • moment.js: http://momentjs.com/ • Underscore.js: http://underscorejs.org/ • gruntjs: http://gruntjs.com/ • AngularJS: https://angularjs.org/ • jasmine: http://jasmine.github.io/ … and a few others Though a frontend developer is not required to know the ins and outs of every library, it is recommended to at least have a basic insight into most of them. It is worth running find {MAGENTO-DIR}/ -name \*.js > js-list. txt on the console to get a full list of each and every JavaScript file in Magento. Spending a few minutes glossing over the list might serve as a nice future memo when working with JavaScript bits in Magento. The RequireJS and jQuery libraries are probably the most interesting ones, as they often step into the spotlight during frontend development. RequireJS plays a big role in Magento, as it loads other JavaScript files. Using a modular script loader like RequireJS improves the speed of code. Speed improvement comes from removing JavaScript from the header and asynchronously or lazy loading JavaScript resources in the background. JavaScript resources can be specified as follows: • Library level for all libraries in the Magento code base (lib/web). • Module level for all libraries in a module (app/code/{vendorName}/ {moduleName}/view/{area}/web). • Theme for all libraries in a theme (app/design/{area}/{vendorName}/ {theme}/{vendorName}_{moduleName}/web). • All libraries in a theme (app/design/{area}/{vendorName}/{theme}/web). Though possible, it is not recommended using this level to specify JavaScript resources. It is recommended to specify JavaScript resources in the templates rather than in the layout updates. This way, we ensure processing of the resources through RequireJS. To work with the RequireJS library, specify the mapping of JavaScript resources; that is, assign the aliases to resources. Use requires-config.js to create the mapping. [ 191 ] Frontend Development To make our configurations more precise and specific for different modules/ themes, we can identify mapping in the requires-config.js file at several levels depending on our needs. Configurations are collected and executed in the following order: • Library configurations • Configurations at the module level • Configurations at the theme module level for the ancestor themes • Configurations at the theme module level for a current theme • Configurations at the theme level for the ancestor themes • Configurations at the theme level for the current theme When we speak of JavaScript in Magento, we can hear various terms like component and widget. We can easily divide those terms by describing the type of JavaScript in Magento as per the following list: • JavaScript component (JS component): This can be any single JavaScript file decorated as an AMD (short for Asynchronous Module Definition) module • Ui component: A JavaScript component located in the Magento_Ui module • jQuery UI widget: A JavaScript component/widget provided by the jQuery UI library used in Magento • jQuery widget: A custom widget created using jQuery UI Widget Factory and decorated as an AMD module There are two ways we can initialize a JavaScript component in template files: • Using the data-mage-init attribute • Using the Depending on the situation and desired level of expressiveness, we can either opt for usage of data-mage-init or attribute or count()): ?>
getId() ?> escapeHtml($ticket-> getTitle()) ?> getCreatedAt() ?> getSeverityAsLabel() ?> getStatusAsLabel() ?>
Though this is a big chunk of code, it is easily readable as it is divided into a few very different role-playing chunks. The $block variable is actually the same as if we wrote $this, which is a reference to the instance of the Foggyline\Helpdesk\Block\Ticket class where we defined the actual getTickets method. Thus, the $tickets variable is first defined as a collection of tickets that belong to the currently logged-in customer. We then specified a form with a POST method type and an action URL that points to our Save controller action. Within the form, we have a $block>getBlockHtml('formkey') call, which basically returns a hidden input field named form_key whose value is a random string. Form keys in Magento are a means of preventing against Cross-Site Request Forgery (CSRF), so we need to be sure to use them on any form we define. As part of the form, we have also defined a title input field, severity select field, and submit button. Notice the CSS classes tossed around, which guarantee that our form's look will match those of other Magento forms. Right after the closing form tag, we have a RequireJS type of JavaScript inclusion for validation. Given that our form ID value is set to form-validate, the JavaScript dataForm variable binds to it and triggers a validation check when we press the Submit button. We then have a count check and a foreach loop that renders all possibly existing customer tickets. [ 355 ] Building a Module from Scratch The final result of the template code can be seen in the following image: Handling form submissions There is one more piece we are missing in order to complete our frontend functionality – a controller action that will save the New Ticket form once it is posted. We define this action within the app/code/Foggyline/Helpdesk/ Controller/Ticket/Save.php file with content as follows: transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; $this->formKeyValidator = $formKeyValidator; $this->dateTime = $dateTime; $this->ticketFactory = $ticketFactory; $this->messageManager = $context->getMessageManager(); parent::__construct($context, $customerSession); } public function execute() { $resultRedirect = $this->resultRedirectFactory->create(); if (!$this->formKeyValidator->validate($this-> getRequest())) { return $resultRedirect->setRefererUrl(); } $title = $this->getRequest()->getParam('title'); $severity = $this->getRequest()->getParam('severity'); [ 357 ] Building a Module from Scratch try { /* Save ticket */ $ticket = $this->ticketFactory->create(); $ticket->setCustomerId($this->customerSession-> getCustomerId()); $ticket->setTitle($title); $ticket->setSeverity($severity); $ticket->setCreatedAt($this->dateTime-> formatDate(true)); $ticket->setStatus(\Foggyline\Helpdesk\Model\ Ticket::STATUS_OPENED); $ticket->save(); $customer = $this->customerSession->getCustomerData(); /* Send email to store owner */ $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE; $transport = $this->transportBuilder ->setTemplateIdentifier($this->scopeConfig-> getValue('foggyline_helpdesk/email_template/ store_owner', $storeScope)) ->setTemplateOptions( [ 'area' => \Magento\Framework\App\ Area::AREA_FRONTEND, 'store' => $this->storeManager-> getStore()->getId(), ] ) ->setTemplateVars(['ticket' => $ticket]) ->setFrom([ 'name' => $customer->getFirstname() . ' ' . $customer->getLastname(), 'email' => $customer->getEmail() ]) ->addTo($this->scopeConfig->getValue( 'trans_email/ident_general/email', $storeScope)) ->getTransport(); [ 358 ] Chapter 12 $transport->sendMessage(); $this->inlineTranslation->resume(); $this->messageManager->addSuccess(__('Ticket successfully created.')); } catch (Exception $e) { $this->messageManager->addError(__('Error occurred during ticket creation.')); } return $resultRedirect->setRefererUrl(); } } First, we look at __construct to see what parameters are passed to it. Given that the code we run in the execute method needs to check if the form key is valid, create a ticket in the database, pass on the ticket and some customer info to the e-mail that is being sent to the store owner; then, we get an idea of what kind of objects are being passed around. The execute method starts by checking the validity of the form key. If the form key is invalid, we return with a redirection to the referring URL. Passing the form key check, we grab the title and severity variables as passed by the form. We then instantiate the ticket entity by the ticket factory create method and simply set the ticket entity values one by one. Note that the Ticket entity model Foggyline\Helpdesk\Model\Ticket does not really have methods like setSeverity on its own. This is the inherited property of its \Magento\Framework\ Object parent class. Once the ticket entity is saved, we initiate the transport builder object, passing along all of the required parameters for successful e-mail sending. Notice how setTemplateIdentifier uses our system.xml configuration option foggyline_ helpdesk/email_template/store_owner. This, if not specifically set under the admin Store | Configuration | Foggyline | Helpdesk area, has a default value defined under config.xml that points to the e-mail template ID in the email_ templates.xml file. setTemplateVars expects the array or instance of \Magento\Framework\Object to be passed to it. We pass the entire $ticket object to it, just nesting it under the ticket key, thus making the properties of a Ticket entity, like a title, become available in the e-mail HTML template as {{var ticket.title}}. [ 359 ] Building a Module from Scratch When a customer now submits the New Ticket form from My Account | Helpdesk Tickets, the HTTP POST request will hit the save controller action class. If the preceding code is successfully executed, the ticket is saved to the database and redirection back to My Account | Helpdesk Tickets will occur showing a Ticket successfully created message in the browser. Building a backend interface Until now, we have been dealing with setting up general module configuration, e-mail templates, frontend route, frontend layout, block, and template. What remains to complete the module requirements is the admin interface, where the store owner can see submitted tickets and change statuses from open to closed. The following are needed for a fully functional admin interface as per the requirements: • ACL resource used to allow or disallow access to the ticket listing • Menu item linking to tickets listing the controller action • Route that maps to our admin controller • Layout XMLs that map to the ticket listing the controller action • Controller action for listing tickets • Full XML layout grid definition within layout XMLs defining grid, custom column renderers, and custom dropdown filter values • Controller action for closing tickets and sending e-mails to customers Linking the access control list and menu We start by adding a new ACL resource entry to the previously defined app/code/ Foggyline/Helpdesk/etc/acl.xml file, as a child of the Magento_Backend::admin resource as follows: On its own, the defined resource entry does not do anything. This resource will later be used within the menu and controller. [ 360 ] Chapter 12 The menu item linking to the tickets listing the controller action is defined under the app/code/Foggyline/Helpdesk/etc/adminhtml/menu.xml file as follows: We are using the menu | add element to add a new menu item under the Magento admin area. The position of an item within the admin area is defined by the attribute parent, which in our case means under the existing Customer menu. If the parent is omitted, our item would appear as a new item on a menu. The title attribute value is the label we will see in the menu. The id attribute has to uniquely differentiate our menu item from others. The resource attribute references the ACL resource defined in the app/code/Foggyline/Helpdesk/etc/acl.xml file. If a role of a logged-in user does not allow him to use the Foggyline_Helpdesk::ticket_manage resource, the user would not be able to see the menu item. Creating routes, controllers, and layout handles Now we add a route that maps to our admin controller, by defining the app/code/ Foggyline/Helpdesk/etc/adminhtml/routes.xml file as follows: [ 361 ] Building a Module from Scratch The admin route definition is almost identical to the frontend router definition, where the difference primarily lies in the router ID value, which equals to the admin here. With the router definition in place, we can now define our three layout XMLs, under the app/code/Foggyline/Helpdesk/view/adminhtml/layout directory, which map to the ticket listing the controller action: • foggyline_helpdesk_ticket_grid.xml • foggyline_helpdesk_ticket_grid_block.xml • foggyline_helpdesk_ticket_index.xml The reason we define three layout files for a single action controller and not one is because of the way we use the listing in control in the Magento admin area. The content of the foggyline_helpdesk_ticket_index.xml file is defined as follows: Two update handles are specified, one pulling in formkey and the other pulling in foggyline_helpdesk_ticket_grid_block. We then reference the content container and define a new block of the Foggyline\Helpdesk\Block\Adminhtml\Ticket class with it. [ 362 ] Chapter 12 Utilizing the grid widget We could have used Magento\Backend\Block\Widget\Grid\Container as a block class name. However, given that we needed some extra logic, like removing the Add New button, we opted for a custom class that then extends \Magento\Backend\ Block\Widget\Grid\Container and adds the required logic. The Foggyline\Helpdesk\Block\Adminhtml\Ticket class is defined under the app/code/Foggyline/Helpdesk/Block/Adminhtml/Ticket.php file as follows: _controller = 'adminhtml'; $this->_blockGroup = 'Foggyline_Helpdesk'; $this->_headerText = __('Tickets'); parent::_construct(); $this->removeButton('add'); } } Not much is happening in the Ticket block class here. Most importantly, we extend from \Magento\Backend\Block\Widget\Grid\Container and define _controller and _blockGroup, as these serve as a sort of glue for telling our grid where to find other possible block classes. Since we won't have an Add New ticket feature in admin, we are calling the removeButton method to remove the default Add New button from the grid container. Back to our second XML layout file, the foggyline_helpdesk_ticket_grid.xml file, which we define as follows: [ 363 ] Building a Module from Scratch Notice how the content of foggyline_helpdesk_ticket_grid.xml is nearly identical to that of foggyline_helpdesk_ticket_index.xml. The only difference between the two is the value of the block class and the template attribute. The block class is defined as Magento\Backend\Block\Widget\Grid\Container, where we previously defined it as Foggyline\Helpdesk\Block\Adminhtml\Ticket. If we look at the content of the \Magento\Backend\Block\Widget\Grid\Container class, we can see the following property defined: protected $_template = 'Magento_Backend::widget/grid/container.phtml'; If we look at the content of the vendor/magento/module-backend/view/ adminhtml/templates/widget/grid/container.phtml and vendor/magento/ module-backend/view/adminhtml/templates/widget/grid/container/empty. phtml files, the difference can be easily spotted. container/empty.phtml only returns grid HTML, whereas container.phtml returns buttons and grid HTML. Given that foggyline_helpdesk_ticket_grid.xml will be a handle for the AJAX loading grid listing during sorting and filtering, we need it to return only grid HTML upon reload. We now move on to the third and largest of XML's layout files, the app/code/ Foggyline/Helpdesk/view/adminhtml/layout/foggyline_helpdesk_ticket_ grid_block.xml file. Given the size of it, we will split it into two code chunks as we explain them one by one. The first part, or initial content of the foggyline_helpdesk_ticket_grid_block. xml file, is defined as follows: [ 364 ] Chapter 12 ticketGrid Foggyline\Helpdesk\Model\ResourceModel \Ticket\Collection ticket_id desc true true Notice ; we will come back to that soon. For now, let's analyze what is happening here. Right after a body element, we have a reference to admin.block.helpdesk.ticket.grid.container, which is a content block child defined under the foggyline_helpdesk_ticket_grid.xml and foggyline_helpdesk_ticket_index.xml files. Within this reference, we are defining another block of class Magento\Backend\Block\Widget\Grid, passing it a name of our choosing and an alias. Further, this block has an arguments list and another block of class Magento\Backend\Block\Widget\Grid\ColumnSet as child elements. [ 365 ] Building a Module from Scratch Through the arguments list we specify the: • id: Set to the value of ticketGrid, we can set any value we want here, • dataSource: Set to the value of Foggyline\Helpdesk\Model\ ResourceModel\Ticket\Collection, which is the name of our Ticket ideally sticking to formula {entity name}. entity resource class. • default_sort: Set to the value of ticket_id, which is the property of the Ticket entity by which we want to sort. • default_dir: Set to the value of desc, to denote a descending order of sorting. This value functions together with default_sort as a single unit. • save_parameters_in_session: Set to true, this is easiest to explain using the following example: if we do some sorting and filtering on the Ticket grid and then move on to another part of the admin area, then come back to Ticket grid, if this value is set to yes, the grid we see will have those filters and sorting set. • use_ajax: Set to true, when grid filtering and sorting is triggered, an AJAX loader kicks in and reloads only the grid area and not the whole page. Right after the grid blocks argument list, we have the grid column set. This brings us to the second part of foggyline_helpdesk_ticket_grid_block.xml content. We simply replace the comment with the following: ID number ticket_id ticket_id Title string [ 366 ] Chapter 12 title title Severity severity options Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer \Severity col-form_id col-form_id Status status options Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid \Renderer\Status col-form_id col-form_id [ 367 ] Building a Module from Scratch action Action action getId false false Close */*/close id col-actions col-actions Similar to grid, column definitions also have arguments that define its look and behavior: • header: Mandatory, the value we want to see as a label on top of the column. • type: Mandatory, can be anything from: date, datetime, text, longtext, options, store, number, currency, skip-list, wrapline, and country. • id: Mandatory, a unique value that identifies our column, preferably • matching the name of the entity property. index: Mandatory, the database column name. [ 368 ] Chapter 12 • options: Optional, if we are using a type like options, then for the options argument we need to specify the class like Foggyline\Helpdesk\Model\ Ticket\Grid\Severity that implements \Magento\Framework\Option\ ArrayInterface, meaning it provides the toOptionArray method that then fills the values of options during grid rendering. • renderer: Optional, as our Ticket entities store severity and status as integer values in the database, columns would render those integer values into columns, which is not really useful. We want to turn those integer values into labels. In order to do so, we need to rewrite the rendering bit of a single table cell, which we do with the help of the renderer argument. The value we pass to it, Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\ Renderer\Severity, needs to be a class that extends \Magento\Backend\ Block\Widget\Grid\Column\Renderer\AbstractRenderer and does its own implementation of the render method. • header_css_class: Optional, if we prefer to specify a custom header class. • column_css_class: Optional, if we prefer to specify a custom column class. Creating a grid column renderer The Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\Renderer\Severity class, defined in the app/code/Foggyline/Helpdesk/Block/Adminhtml/Ticket/ Grid/Renderer/Severity.php file, is as follows: ticketFactory = $ticketFactory; [ 369 ] Building a Module from Scratch } public function render(\Magento\Framework\DataObject $row) { $ticket = $this->ticketFactory->create()->load($row-> getId()); if ($ticket && $ticket->getId()) { return $ticket->getSeverityAsLabel(); } return ''; } } Here, we are passing the instance of the ticket factory to the constructor and then using that instance within the render method to load a ticket based on the ID value fetched from the current row. Given that $row->getId() returns the ID of the ticket, this is a nice way to reload the entire ticket entity and then fetch the full label from the ticket model by using $ticket->getSeverityAsLabel(). Whatever string we return from this method is what will be shown under the grid row. Another renderer class that is referenced within the foggyline_helpdesk_ticket_ grid_block.xml file is Foggyline\Helpdesk\Block\Adminhtml\Ticket\Grid\ Renderer\Status, and we define its content under the app/code/Foggyline/ Helpdesk/Block/Adminhtml/Ticket/Grid/Renderer/Status.php file as follows: ticketFactory = $ticketFactory; } public function render(\Magento\Framework\DataObject $row) { $ticket = $this->ticketFactory->create()->load($row-> getId()); if ($ticket && $ticket->getId()) { return $ticket->getStatusAsLabel(); } return ''; } } Given that it too is used for a renderer, the content of the Status class is nearly identical to the content of the Severity class. We pass on the ticket factory object via the constructor, so we have it internally for usage within the render method. Then we load the Ticket entity using the ticket factory and ID value fetched from a $row object. As a result, the column will contain the label value of a status and not its integer value. Creating grid column options Besides referencing renderer classes, our foggyline_helpdesk_ticket_grid_ block.xml file also references the options class for the Severity field. We define the Foggyline\Helpdesk\Model\Ticket\Grid\Severity options class under the app/code/Foggyline/Helpdesk/Model/Ticket/Grid/Severity.php file as follows: 'theValue1', 'theLabel1'], ['value'=>'theValue2', 'theLabel2'], ]; Our Severity class simply calls the static method we have defined on the Ticket class, the getSeveritiesOptionArray, and passes along those values. Creating controller actions Up to this point, we have defined the menu item, ACL resource, XML layouts, block, options class, and renderer classes. What remains to connect it all are controllers. We will need three controller actions (Index, Grid, and Close), all extending from the same admin Ticket controller. We define the admin Ticket controller under the app/code/Foggyline/Helpdesk/ Controller/Adminhtml/Ticket.php file as follows: resultPageFactory = $resultPageFactory; $this->resultForwardFactory = $resultForwardFactory; $this->resultRedirectFactory = $context-> getResultRedirectFactory(); [ 372 ] Chapter 12 parent::__construct($context); } protected function _isAllowed() { return $this->_authorization-> isAllowed('Foggyline_Helpdesk::ticket_manage'); } protected function _initAction() { $this->_view->loadLayout(); $this->_setActiveMenu( 'Foggyline_Helpdesk::ticket_manage' )->_addBreadcrumb( __('Helpdesk'), __('Tickets') ); return $this; } } There are a few things to note here. $this->resultPageFactory, $this>resultForwardFactory and $this->resultRedirectFactory are objects to be used on the child (Index, Grid, and Close), so we do not have to initiate them in each child class separately. The _isAllowed() method is extremely important every time we have a customdefined controller or controller action that we want to check against our custom ACL resource. Here, we are the isAllowed method call on the \Magento\Framework\ AuthorizationInterface type of object ($this->_authorization). The parameter passed to the isAllowed method call should be the ID value of our custom ACL resource. We then have the _initAction method, which is used for setting up logic shared across child classes, usually things like loading the entire layout, setting up the active menu flag, and adding breadcrumbs. Moving forward, we define the Index controller action within the app/code/ Foggyline/Helpdesk/Controller/Adminhtml/Ticket/Index.php file as follows: getRequest()->getQuery('ajax')) { $resultForward = $this->resultForwardFactory-> create(); $resultForward->forward('grid'); return $resultForward; } $resultPage = $this->resultPageFactory->create(); $resultPage-> setActiveMenu('Foggyline_Helpdesk::ticket_manage'); $resultPage->getConfig()->getTitle()-> prepend(__('Tickets')); $resultPage->addBreadcrumb(__('Tickets'), __('Tickets')); $resultPage->addBreadcrumb(__('Manage Tickets'), __('Manage Tickets')); return $resultPage; } } Controller actions execute within their own class, within the execute method. Our execute method first checks if the coming request is the AJAX parameter within it. If there is an AJAX parameter, the request is forwarded to the Grid action of the same controller. If there is no AJAX controller, we simply create the instance of the \Magento\ Framework\View\Result\PageFactory object, and set title, active menu item, and breadcrumbs in it. A logical question at this point would be how does all of this work and where can we see it. If we log in to the Magento admin area, under the Customers menu we should be able to see the Helpdesk Tickets menu item. This item, defined previously within app/code/Foggyline/Helpdesk/etc/adminhtml/menu.xml, says the menu action attribute equals to foggyline_helpdesk/ticket/index, which basically translates to the Index action of our Ticket controller. [ 374 ] Chapter 12 Once we click on the Helpdesk Tickets link, Magento will hit the Index action within its Ticket controller and try to find the XML file that has the matching route {id}+{controller name }+{controller action name }+{xml file extension }, which in our case translates to {foggyline_helpdesk}+{ticket}+{index}+{.xml}. At this point, we should be able to see the screen, as shown in the following screenshot: However, if we now try to use sorting or filtering, we would get a broken layout. This is because based on arguments defined under the foggyline_helpdesk_ ticket_grid_block.xml file, we are missing the controller Grid action. When we use sorting or filtering, the AJAX request hits the Index controller and asks to be forwarded to the Grid action, which we haven't defined yet. We now define the Grid action within the app/code/Foggyline/Helpdesk/ Controller/Adminhtml/Ticket/Grid.php file as follows: _view->loadLayout(false); $this->_view->renderLayout(); } } [ 375 ] Building a Module from Scratch There is only one execute method with the Grid controller action class, which is expected. The code within the execute method simply calls the loadLayout(false) method to prevent the entire layout loading, making it load only the bits defined under the foggyline_helpdesk_ticket_grid.xml file. This effectively returns the grid HTML to the AJAX, which refreshes the grid on the page. Finally, we need to handle the Close action link we see on the grid. This link was defined as part of the column definition within the foggyline_helpdesk_ticket_ grid_block.xml file and points to */*/close, which translates to "router as relative from current URL / controller as relative from current URL / close action", which further equals to our Ticket controller Close action. We define the Close controller action under the app/code/Foggyline/Helpdesk/ Controller/Adminhtml/Ticket/Close.php file as follows: ticketFactory = $ticketFactory; $this->customerRepository = $customerRepository; $this->transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; parent::__construct($context, $resultPageFactory, $resultForwardFactory); } public function execute() { $ticketId = $this->getRequest()->getParam('id'); $ticket = $this->ticketFactory->create()->load($ticketId); if ($ticket && $ticket->getId()) { try { $ticket->setStatus(\Foggyline \Helpdesk\Model\Ticket::STATUS_CLOSED); $ticket->save(); $this->messageManager->addSuccess(__('Ticket successfully closed.')); /* Send email to customer */ $customer = $this->customerRepository-> getById($ticket->getCustomerId()); $storeScope = \Magento\Store\Model\ ScopeInterface::SCOPE_STORE; $transport = $this->transportBuilder ->setTemplateIdentifier($this->scopeConfig-> getValue('foggyline_helpdesk/email_template /customer', $storeScope)) ->setTemplateOptions( [ 'area' => \Magento\Framework\App\ Area::AREA_ADMINHTML, 'store' => $this->storeManager>getStore()->getId(), ] ) [ 377 ] Building a Module from Scratch ->setTemplateVars([ 'ticket' => $ticket, 'customer_name' => $customer-> getFirstname() ]) ->setFrom([ 'name' => $this->scopeConfig-> getValue('trans_email/ident_general /name', $storeScope), 'email' => $this->scopeConfig-> getValue('trans_email/ident_general /email', $storeScope) ]) ->addTo($customer->getEmail()) ->getTransport(); $transport->sendMessage(); $this->inlineTranslation->resume(); $this->messageManager->addSuccess(__('Customer notified via email.')); } catch (Exception $e) { $this->messageManager->addError(__('Error with closing ticket action.')); } } $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/index'); return $resultRedirect; } } The Close action controller has two separate roles to fulfill. One is to change the ticket status; the other is to send an e-mail to the customer using the proper e-mail template. The class constructor is being passed a lot of parameters that all instantiate the objects we will be juggling around. Within the execute action, we first check for the existence of the id parameter and then try to load a Ticket entity through the ticket factory, based on the provided ID value. If the ticket exists, we set its status label to \Foggyline\Helpdesk\Model\ Ticket::STATUS_CLOSED and save it. [ 378 ] Chapter 12 Following the ticket save is the e-mail-sending code, which is very similar to the one that we already saw in the customer New Ticket save action. The difference is that the e-mail goes out from the admin user to the customer this time. We are setting the template ID to the configuration value at path foggyline_helpdesk/ email_template/customer. The setTemplateVars method is passed to the member array this time, both ticket and customer_name, as they are both used in the e-mail template. The setFrom method is passed the general store username and e-mail, and the sendMessage method is called on the transport object. Finally, using the resultRedirectFactory object, the user is redirected back to the tickets grid. With this, we finalize our module functional requirement. Though we are done with the functional requirement of a module, what remains for us as developers is to write tests. There are several types of tests, such as unit, functional, integration, and so on. To keep things simple, within this chapter we will cover only unit tests across a single model class. Creating unit tests This chapter assumes that we have PHPUnit configured and available on the command line. If this is not the case, PHPUnit can be installed using instructions from the https://phpunit.de/ website. To build and run tests using the PHPUnit testing framework, we need to define test locations and other configuration options via an XML file. Magento defines this XML configuration file under dev/tests/unit/phpunit.xml.dist. Let's make a copy of that file under dev/tests/unit/phpunit-foggyline-helpdesk.xml, with adjustments as follows: ../../../app/code/Foggyline/Helpdesk/Test/Unit [ 379 ] Building a Module from Scratch ../../../app/code/Foggyline/Helpdesk/* ../../../app/code/Foggyline/Form/Helpdesk We are making a special XML configuration file for our module alone because we want to quickly run a few of the tests contained within our module alone and not the entire Magento app/code folder. Given that the actual art of writing unit tests is beyond the scope of this book and writing the full unit test with 100 percent code coverage for this simple module would require at least a dozen more pages, we will only write a single test, one that covers the Ticket entity model class. We define our Ticket entity model class test under the app/code/Foggyline/ Helpdesk/Test/Unit/Model/TicketTest.php file as follows: objectManager = new \Magento\Framework\TestFramework\Unit\Helper \ObjectManager($this); $this->ticket = $this->objectManager-> getObject('Foggyline\Helpdesk\Model\Ticket'); } public function testGetSeveritiesOptionArray() { $this-> assertNotEmpty(\Foggyline \Helpdesk\Model\Ticket::getSeveritiesOptionArray()); } public function testGetStatusesOptionArray() { $this->assertNotEmpty(\Foggyline \Helpdesk\Model\Ticket::getStatusesOptionArray()); } public function testGetStatusAsLabel() { $this->ticket->setStatus(\Foggyline\Helpdesk \Model\Ticket::STATUS_CLOSED); $this->assertEquals( \Foggyline\Helpdesk\Model\Ticket::$statusesOptions [\Foggyline\Helpdesk\Model\Ticket::STATUS_CLOSED], $this->ticket->getStatusAsLabel() ); } public function testGetSeverityAsLabel() { $this->ticket->setSeverity(\Foggyline \Helpdesk\Model\Ticket::SEVERITY_MEDIUM); $this->assertEquals( \Foggyline\Helpdesk\Model\Ticket::$severitiesOptions [\Foggyline\Helpdesk\Model\Ticket::SEVERITY_MEDIUM], $this->ticket->getSeverityAsLabel() ); } } [ 381 ] Building a Module from Scratch The location of test files should map those of the files being tested. The naming of the test file should also follow the naming of the file being tested with the suffix Test attached to it. This means that if our Ticket model is located under the modules Model/Ticket.php file, then our test should be located under Test/Unit/ TicketTest.php. Our Foggyline\Helpdesk\Test\Unit\Model\TicketTest extends the \PHPUnit_ Framework_TestCase class. There is a setUp method we need to define, which acts like a constructor, where we set up the variables and everything that requires initializing. Using Magento ObjectManager, we instantiate the Ticket model, which is then used within the test methods. The actual test methods follow a simple naming pattern, where the name of the method from the Ticket model matches the {test}+{method name} from the TicketTest class. We defined four test methods: testGetSeveritiesOptionArray, testGetStatusesOptionArray, testGetStatusAsLabel, and testGetSeverityAsLabel. Within the test methods, we are using only assertEquals and assertNotEmpty methods from the PHPUnit testing framework library to do basic checks. We can now open a console, change the directory to our Magento installation directory, and execute the following command: phpunit -c dev/tests/unit/phpunit-foggyline-helpdesk.xml After the command executes, the console should show an output as shown: PHPUnit 4.7.6 by Sebastian Bergmann and contributors. .... Time: 528 ms, Memory: 11.50Mb OK (4 tests, 4 assertions) Generating code coverage report in HTML format ... done Looking back at our dev/tests/unit/phpunit-foggyline-helpdesk.xml file, under the target attribute of the phpunit > logging > log element, we can see that the test report is dumped into the coverage_dir/Foggyline_Helpdesk/testreports/coverage folder relative to the XML file. [ 382 ] Chapter 12 If we open the dev/tests/unit/coverage_dir/Foggyline_Helpdesk/testreports/coverage/ folder, we should see a whole lot of files generated there, as shown in the following screenshot: Opening the index.html file in the browser should give us a page as shown in the following screenshot: We can see the code coverage report showing 60% on lines and methods for our Model folder and 0% for the rest. This is because we only wrote the test for the Ticket entity model class, whereas the rest remain untested. [ 383 ] Building a Module from Scratch Summary This chapter gave a full step-by-step guide to writing a simple yet functional Magento module. Seemingly simple in terms of functionality, we can see that the module code is significantly scattered across multiple PHP, XML, and PHMTL files. With this simple module, we covered quite a lot of various Magento platform parts, from routes, ACLs, controllers, blocks, XML layouts, grids, controller actions, models, resources, collections, install scripts, interactions with session, e-mail templates, e-mail transport, and layout objects. At the end, we wrote a few simple unit tests for our models. Although the practice is to write unit tests for all of our PHP code, we opted for a shorter version or else we would need more pages to cover everything. The full module code is available here: https://github.com/ajzele/B05032Foggyline_Helpdesk. With this being the last chapter, let us look at a short overview of the things we learned throughout the whole book. Our journey started by grasping the Magento platform architecture, where we gained significant insight into the technology stack behind it. We then progressed to environment management. Although it might seem like a wrong order of things, we opted for this next step in order to quickly get us set for development. We then looked into programming concepts and conventions, which served as a precursor to actual hands-on development bits. Details of entity persistence were shown through model, resource, collection classes, and indexers. We further covered the importance and practical details of dependency injection and interception. Backend and frontend-related development was covered in their own two chapters, outlining the most common bits and pieces for making customizations to our Magento platform. We then dug into details of the web API, showing how to make authenticated API calls and even define our own APIs. Along the way, we covered a few major functional areas as well, such as customers, reports, import export, cart, and so on. The testing and QA took up a significant chunk as we briefly covered all forms of available tests. Finally, we used what we learned to build a fully functional module. Although we have covered a significant path on our journey, this is merely a first step. Given its massive code base, diverse technology stacks, and feature list, Magento is not an easy platform to master. Hopefully, this book will give enough incentive to take further steps into profiling ourselves as true Magento experts. [ 384 ] Index A B access control lists (acl.xml) creating 339, 340 after listener using 118 Amazon Elastic Compute Cloud (Amazon EC2) 22 Amazon Web Services (AWS) 20, 22 AngularJS URL 191 Apache JMeter URL 312 API call examples deleteById service method call examples 248-250 getById service method call examples 235-237 getList service method call examples 238-242 references 250 save (as new) service method call examples 243-245 save (as update) service method call examples 245-247 around listener using 118 authentication methods defining 201 OAuth-based authentication 202 session-based authentication 202 token-based authentication 202 AWS management console URL 21 backend development 121 backend interface access control list and menu, linking 360, 361 building 360 controller actions, creating 372-379 controllers, creating 361, 362 grid column options, creating 371, 372 grid column renderer, creating 369-371 grid widget, utilizing 363-369 routes, creating 361, 362 Bearer HTTP authorization scheme 202 before listener using 117 blocks 172-174 C cache(s) defining 143-146 catalog management about 262 categories, managing manually 262-264 categories, managing via API 265 categories, managing via code 264, 265 products, managing manually 266, 267 products, managing via API 268 products, managing via code 267 class preferences configuring 109 CMS management blocks, managing manually 256, 257 blocks, managing via API 259 [ 385 ] blocks, managing via code 257, 258 defining 255 pages, managing manually 259, 260 pages, managing via API 261, 262 pages, managing via code 261 code demarcation 58 code generation 55, 56 coding standards 58 collection filters 98, 99 collections managing 91-98 components, open source technologies Apache or Nginx 2 Coding standards 2 Composer 2 CSS 2 HTML 2 jQuery 2 MTF 2 MySQL 2 PHP 2 RequireJS 2 third-party libraries 2 Composer defining 47-52 references 50 URL 48 configuration file (config.xml) creating 331 cookies 127-132 Cron jobs defining 122-124 CRUD (create, read, update, and delete) 4 CSV files defining 152 customer management about 269 customer address, managing via API 274 customer address, managing via code 273, 274 customers, managing manually 269-271 customers, managing via API 272 customers, managing via code 272 custom offline payment methods defining 294-303 URL 303 custom offline shipping methods defining 287-294 URL 294 custom product types defining 280-286 URL 286 custom variables defining 149, 150 custom Web APIs API call examples 235 creating 218-234 D data interfaces 53 data scripts defining 69, 70 dependency injection 101-108 development environment setting up 12 Vagrant 12 Vagrant project 13-16 VirtualBox 12 DNS setting up 43-45 E EC2 instance setting up 35-42 Elastic IP setting up 43-45 e-mail templates (email_templates.xml) creating 333, 334 Entity-Attribute-Value (EAV) 4 entity CRUD actions defining 85-87 references 86 entity persistence managing 344-347 events defining 138-142 dynamic 141 [ 386 ] static 141 existing entities deleting 91 reading 90 updating 91 Ext JS URL 190 integrated development environments (IDEs) 56 integration testing defining 309 integrity testing defining 310 interception 113 F J factories 56 flow rendering 160-166 frontend development 159 frontend interface blocks, creating 352-355 building 348 controllers, creating 348-351 form submissions, handling 356-359 layout handle, creating 348-351 routes, creating 348-351 templates, creating 352-355 functional testing defining 314-317 jasmine URL 191 JavaScript about 190-192 custom JS component, creating 193, 194 JavaScript coding standard URL 58 JavaScript component (JS component) 192 JavaScript DocBlock standard about 59 URL 59 jQuery URL 190 jQuery UI URL 190 jQuery UI widget 192 G K gruntjs URL 191 I i18n (internationalization) defining 150-154 IAM groups creating 25-28 IAM users creating 23-25 Identity and Access Management (IAM) 22 indexing 155, 156 inline translation 153 installation script (InstallSchema.php) creating 341-344 install data script (InstallData.php) creating 79-83 install schema script (InstallSchema.php) creating 71-78 Kernel-based Virtual Machine (KVM) 12 Knockout URL 190 L layouts 183-186 legacy testing defining 311 LESS coding standard 59 logging 132-135 M Magento about 1 architectural layers 3, 4 domain layer 3 [ 387 ] module filesystem structure 8, 9 persistence layer 4 presentation layer 3 service layer 3 top-level filesystem structure 4-7 Magento Testing Framework (MTF) about 2, 314 requirements 314 URL 3 miniature module creating 62, 63 EAV model, creating 66-68 simple model, creating 64-66 modernizr URL 190 module registering 329-331 module requirements defining 327, 328 moment.js URL 191 N NetBeans PHP 184 new entities creating 88, 89 notification messages defining 124-127 O OAuth 1.0a handshake process 202 OAuth-based authentication defining 207-213 OAuth-based Web API calls defining 213-217 OAuth client URL 209 object manager 102, 103 Object Relational Mapping (ORM) 61 observers defining 138-142 P performance testing defining 312-314 PHP URL 58 PHP coding standard 58 PHP OOP 2 PhpStorm 184 PHPUnit URL 379 PHPUnit testing framework 308 plugin about 113 creating 114-116 plugin sort order 119 production environment access, setting up for S3 usage 22 Amazon Web Services (AWS) 20, 22 bash script, for automated EC2 setup 30-34 S3, setting up for database and media files backup 28-30 setting up 20 products and customers Import defining 275-279 profiler about 136-138 defining 137, 138 enabling 136 Prototype URL 190 R relational database management system (RDBMS) 2 Representational State Transfer. See REST RequireJS URL 190 REST versus SOAP 202, 203 [ 388 ] S schema flow defining 69, 70 Search Criteria Interface used, for list filtering 250-254 service contract 3, 52-54 session 127-132 session-based authentication defining 217, 218 Slide Repository Interface 225 SOAP versus REST 202, 203 SoapClient 203 software testing 305 standards URL 59 static testing defining 310 Symfony 2 system configuration file (system.xml) creating 335-338 T templates 181, 182 tests dynamic 305 static 305 test types defining 305-307 themes, view elements about 186 new theme, creating 187-190 token-based authentication defining 203-206 U Ui component 192 Underscore.js URL 191 unit testing creating 379-383 defining 308, 309 writing 318-325 upgrade data script (UpgradeData.php) creating 83-85 upgrade schema script (UpgradeSchema.php) creating 78, 79 user types about 198-201 administrator or integration 198 customer 198 guest user 198 V Vagrant about 12 URL 12 Vagrant project about 13-16 Apache, provisioning 17 Magento installation, provisioning 18, 19 MySQL, provisioning 17 PHP, provisioning 16 var directory 57 view elements about 167 block architecture 174-180 blocks 172-174 containers 169, 170 CSS 194, 195 JavaScript 190-192 layouts 183-186 life cycle 174-180 templates 181, 182 themes 186 Ui components 167, 168 VirtualBox about 12 URL 12 virtual types about 110 using 110 VMware 12 [ 389 ] W Web Service Definition Language (WSDL) 203 widgets defining 146-149 Y Yum 47 Z Zend Framework 2 [ 390 ] Thank you for buying Magento 2 Developer's Guide About Packt Publishing Packt, pronounced 'packed', published its first book, Mastering phpMyAdmin for Effective MySQL Management, in April 2004, and subsequently continued to specialize in publishing highly focused books on specific technologies and solutions. Our books and publications share the experiences of your fellow IT professionals in adapting and customizing today's systems, applications, and frameworks. Our solution-based books give you the knowledge and power to customize the software and technologies you're using to get the job done. Packt books are more specific and less general than the IT books you have seen in the past. Our unique business model allows us to bring you more focused information, giving you more of what you need to know, and less of what you don't. Packt is a modern yet unique publishing company that focuses on producing quality, cutting-edge books for communities of developers, administrators, and newbies alike. For more information, please visit our website at www.packtpub.com. About Packt Open Source In 2010, Packt launched two new brands, Packt Open Source and Packt Enterprise, in order to continue its focus on specialization. This book is part of the Packt Open Source brand, home to books published on software built around open source licenses, and offering information to anybody from advanced developers to budding web designers. The Open Source brand also runs Packt's Open Source Royalty Scheme, by which Packt gives a royalty to each open source project about whose software a book is sold. Writing for Packt We welcome all inquiries from people who are interested in authoring. Book proposals should be sent to author@packtpub.com. If your book idea is still at an early stage and you would like to discuss it first before writing a formal book proposal, then please contact us; one of our commissioning editors will get in touch with you. We're not just looking for published authors; if you have strong technical skills but no writing experience, our experienced editors can help you develop a writing career, or simply get some additional reward for your expertise. Magento 1.8 Development Cookbook ISBN: 978-1-78216-332-9 Paperback: 274 pages Over 70 recipes to learn Magento development from scratch 1. Customize the look and feel of your Magento shop. 2. Work on theming, catalog configuration, module, and database development. 3. Create modules to modify or extend Magento's standard behavior. Magento PHP Developer's Guide ISBN: 978-1-78216-306-0 Paperback: 256 pages Get started with the flexible and powerful e-commerce framework, Magento 1. Build your first Magento extension, step by step. 2. Extend core Magento functionality, such as the API. 3. Learn how to test your Magento code. Please check www.PacktPub.com for information on our titles Mastering Magento ISBN: 978-1-84951-694-5 Paperback: 300 pages Maximize the power of Magento: for developers, designers, and store owners 1. Learn how to customize your Magento store for maximum performance. 2. Exploit little known techniques for extending and tuning your Magento installation. 3. Step-by-step guides for making your store run faster, better, and more productively. Magento Beginner's Guide Second Edition ISBN: 978-1-78216-270-4 Paperback: 320 pages Learn how to create a fully featured, attractive online store with the most powerful open source solution for e-commerce 1. Install, configure, and manage your own e-commerce store. 2. Extend and customize your store to reflect your brand and personality. 3. Handle tax, shipping, and custom orders. Please check www.PacktPub.com for information on our titles

Source Exif Data:
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.6
Linearized                      : No
Create Date                     : 2015:12:22 08:56:57+05:30
Creator                         : Adobe InDesign CS5.5 (7.5.3)
Modify Date                     : 2015:12:22 13:35:37+05:30
Tagged PDF                      : Yes
XMP Toolkit                     : Adobe XMP Core 5.2-c001 63.139439, 2010/09/27-13:37:26
Instance ID                     : uuid:e754d644-a13b-4fdb-add6-8d16e9ccee4f
Original Document ID            : adobe:docid:indd:4992da54-27df-11de-a18e-f5498a2a904f
Document ID                     : xmp.did:491D60AFEBA3E511A0C0B8280242A2B9
Rendition Class                 : proof:pdf
Derived From Instance ID        : 713ec90e-27db-11de-a18e-f5498a2a904f
Derived From Document ID        : adobe:docid:indd:08d06915-e4d3-11da-8821-8147fa8cbb52
History Action                  : saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved, saved
History Instance ID             : xmp.iid:6A1AF379F4A3E111B5AF96FBD560C3DD, xmp.iid:6B1AF379F4A3E111B5AF96FBD560C3DD, xmp.iid:C408380B1BA6E1119CBDAF783C44C647, xmp.iid:C508380B1BA6E1119CBDAF783C44C647, xmp.iid:007D27201CA6E1119CBDAF783C44C647, xmp.iid:017D27201CA6E1119CBDAF783C44C647, xmp.iid:45048A4FA2A8E111AC02958474B072BE, xmp.iid:8C7F6616A8A8E111AC02958474B072BE, xmp.iid:42195515ABA8E111AC02958474B072BE, xmp.iid:05D0CA20ACA8E111AC02958474B072BE, xmp.iid:C664CDA7B6AFE1118EF4B4604D8FED7A, xmp.iid:F9565F9082C9E111B9EBEA018E6F1527, xmp.iid:80830CECE29FE51196D3961A5F14EC00, xmp.iid:81830CECE29FE51196D3961A5F14EC00, xmp.iid:6E4E3D77E39FE51196D3961A5F14EC00, xmp.iid:714E3D77E39FE51196D3961A5F14EC00, xmp.iid:744E3D77E39FE51196D3961A5F14EC00, xmp.iid:774E3D77E39FE51196D3961A5F14EC00, xmp.iid:7A7D5D00FA9FE51196D3961A5F14EC00, xmp.iid:698CFBEBD5A3E511A252DD5ABE85E759, xmp.iid:6A8CFBEBD5A3E511A252DD5ABE85E759, xmp.iid:6D8CFBEBD5A3E511A252DD5ABE85E759, xmp.iid:65F61B31ECA3E511A0C0B8280242A2B9, xmp.iid:66F61B31ECA3E511A0C0B8280242A2B9, xmp.iid:69F61B31ECA3E511A0C0B8280242A2B9, xmp.iid:6C47C847ECA3E511A0C0B8280242A2B9, xmp.iid:8FECD71479A4E51185F8A242974B4921, xmp.iid:2C8A4C715BA8E5118B3889970B581EAC, xmp.iid:2F8A4C715BA8E5118B3889970B581EAC
History When                    : 2012:05:22 15:54:29+05:30, 2012:05:22 15:54:29+05:30, 2012:05:25 09:12:28+05:30, 2012:05:25 09:12:28+05:30, 2012:05:25 09:15:52+05:30, 2012:05:25 09:16:24+05:30, 2012:05:28 14:51:19+05:30, 2012:05:28 15:07:38+05:30, 2012:05:28 15:29:29+05:30, 2012:05:28 15:42:40+05:30, 2012:06:06 14:35:49+05:30, 2012:07:09 10:32:51+05:30, 2015:12:11 14:14:35+05:30, 2015:12:11 14:14:35+05:30, 2015:12:11 14:15:01+05:30, 2015:12:11 14:16:03+05:30, 2015:12:11 14:18:44+05:30, 2015:12:11 14:19:17+05:30, 2015:12:11 17:15:31+05:30, 2015:12:16 14:48:08+05:30, 2015:12:16 14:48:08+05:30, 2015:12:16 14:48:25+05:30, 2015:12:16 17:27:40+05:30, 2015:12:16 17:27:40+05:30, 2015:12:16 17:27:46+05:30, 2015:12:16 17:28:11+05:30, 2015:12:17 10:16:06+05:30, 2015:12:22 08:54:30+05:30, 2015:12:22 08:55:03+05:30
History Software Agent          : Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.0, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign CS6 (Windows), Adobe InDesign CS6 (Windows), Adobe InDesign CS6 (Windows), Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5, Adobe InDesign 7.5
History Changed                 : /;/metadata, /metadata, /;/metadata, /metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /, /metadata, /, /;/metadata, /metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata, /;/metadata
Metadata Date                   : 2015:12:22 13:35:37+05:30
Creator Tool                    : Adobe InDesign CS5.5 (7.5.3)
Page Image Page Number          : 1, 2
Page Image Format               : JPEG, JPEG
Page Image Width                : 256, 256
Page Image Height               : 256, 256
Page Image                      : (Binary data 5718 bytes, use -b option to extract), (Binary data 5714 bytes, use -b option to extract)
Doc Change Count                : 20
Key Stamp Mp                    : AAAAAA==
Format                          : application/pdf
Producer                        : Adobe PDF Library 9.9
Trapped                         : False
Page Count                      : 412
EXIF Metadata provided by EXIF.tools

Navigation menu