Chapter 8. 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

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 = MagentoFrameworkAppBootstrap::create(BP, $_SERVER);

//PART-1-3
$app = $bootstrap-> createApplication('MagentoFrameworkAppHttp');

//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 MagentoFrameworkPhrase 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 MagentoFrameworkAppBootstrap 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 MagentoFrameworkAppHttp 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 MagentoFrameworkAppHttp.

PART-1-4 is calling the run method on the $bootstrap object, passing it the instance of the MagentoFrameworkAppHttp class. Although it looks like a simple line of code, this is where things get complicated, as we will soon see.

Let's analyze the (trimmed) version of the MagentoFrameworkAppBootstrap -> run method as follows:

public function run(MagentoFrameworkAppInterface $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 MagentoFrameworkAppHttp instance. Without going into the inner workings of the launch method for the moment, let's just say it returns the instance of the MagentoFrameworkAppResponseHttpInterceptor class defined under var/generation/Magento/Framework/App/Response/Http/Interceptor.php. Note that this is an automatically generated wrapper class, extending the MagentoFrameworkAppResponseHttp class. Effectively, ignoring Interceptor, we can say that $response is an instance the MagentoFrameworkAppResponseHttp class.

Finally, PART-2-3 calls the sendResponse method on $response. Though $response is an instance of the MagentoFrameworkAppResponseHttp class, the actual sendResponse method is found further down the parent tree on the MagentoFrameworkHTTPPhpEnvironmentResponse class. The sendResponse method calls another parent class method called send. The send method can be found under the ZendHttpPhpEnvironmentResponse 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.

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

  • index.php
  • MagentoFrameworkAppBootstrap -> run
  • MagentoFrameworkAppHttp -> launch
  • MagentoFrameworkAppResponseHttp -> 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 MagentoFrameworkAppHttp -> launch method as follows:

public function launch()
{
    //PART-3-1
    $frontController = $this->_objectManager->get ('MagentoFrameworkAppFrontControllerInterface');

    //PART-3-2
    $result = $frontController->dispatch($this->_request);

    if ($result instanceof MagentoFrameworkController ResultInterface) {
        //PART-3-3
        $result->renderResult($this->_response);
    } elseif ($result instanceof MagentoFrameworkApp ResponseHttpInterface) {
        $this->_response = $result;
    } else {
        throw new InvalidArgumentException('Invalid return type');
    }

    //PART-3-4
    return $this->_response;
}

PART-3-1 creates the instance of the object whose class conforms to MagentoFrameworkAppFrontControllerInterface. If we look under app/etc/di.xml, we can see there is a preference for FrontControllerInterface in favor of the MagentoFrameworkAppFrontController class. However, if we were to debug the code and check for the actual instance class, it would show MagentoFrameworkAppFrontControllerInterceptor. This is Magento adding an interceptor wrapper that then extends MagentoFrameworkAppFrontController, 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 MagentoFrameworkViewResultPageInterceptor, defined under the dynamically created var/generation/Magento/Framework/View/Result/Page/Interceptor.php file. Interceptor is the wrapper for the MagentoFrameworkViewResultPage class. Thus, it is safe to say that our $result variable is an instance of the Page class.

The Page class extends MagentoFrameworkViewResultLayout, which further extends MagentoFrameworkControllerAbstractResult and implements MagentoFrameworkControllerResultInterface. Quite a chain we have here, but it is important to understand it.

Notice PART-3-3. Since our $result is an instance of MagentoFrameworkControllerResultInterface, we fall into the first if condition that calls the renderResult method. The renderResult method itself is declared within the MagentoFrameworkViewResultLayout 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
  • MagentoFrameworkAppBootstrap -> run
  • MagentoFrameworkAppHttp -> launch
  • MagentoFrameworkAppFrontController -> dispatch
  • MagentoFrameworkViewResultPage -> renderResult
  • MagentoFrameworkAppResponseHttp -> 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 MagentoFrameworkAppFrontController -> dispatch method as follows:

public function dispatch(MagentoFrameworkAppRequestInterface $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 (MagentoFrameworkException 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:

  • MagentoFrameworkAppRouterBase
  • MagentoUrlRewriteControllerRouter
  • MagentoCmsControllerRouter
  • MagentoFrameworkAppRouterDefaultRouter

All of the listed routers implement MagentoFrameworkAppRouterInterface, 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 MagentoFrameworkAppActionInterface. 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 MagentoFrameworkAppRouterBase and our $actionInstance would be of the class FoggylineOfficeControllerTestCrudInterceptor. 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 FoggylineOfficeControllerTestCrud class file. The Crud class extends FoggylineOfficeControllerTest, which further extends MagentoFrameworkAppActionAction, which implements MagentoFrameworkAppActionInterface. 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 MagentoFrameworkAppActionAction, and is expected to return an object that implements MagentoFrameworkAppResponseInterface. 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 MagentoFrameworkAppResponseHttpInterceptor, which is wrapped around MagentoFrameworkAppResponseHttp.

Let's imagine our Crud class has been defined as follows:

/**
 * @var MagentoFrameworkViewResultPageFactory
 */
protected $resultPageFactory;

public function __construct(
    MagentoFrameworkAppActionContext $context,
    MagentoFrameworkViewResultPageFactory $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 MagentoFrameworkViewResultPageInterceptor. This Interceptor gets dynamically generated by Magento under var/generation/Magento/Framework/View/Result/Page/Interceptor.php and is merely a wrapper for MagentoFrameworkViewResultPage. This Page class further extends the MagentoFrameworkViewResultLayout class, and implements MagentoFrameworkAppResponseInterface.

Finally, PART-4-4 shows the $result object of type MagentoFrameworkViewResultPage 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
  • MagentoFrameworkAppBootstrap -> run
  • MagentoFrameworkAppHttp -> launch
  • MagentoFrameworkAppFrontController -> dispatch
  • MagentoFrameworkAppRouterBase -> match
  • MagentoFrameworkAppActionAction -> dispatch
  • MagentoFrameworkViewResultPage -> renderResult
  • MagentoFrameworkAppResponseHttp -> 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.42.168