In the previous chapter, we learned how to use Pimcore to create entities and render custom web pages. Previously, in Chapter 8, Creating Custom CMS Pages, we discovered how to create CMS pages using the web interface or custom Model View Controller (MVC) pages. In both cases, we'd like to have some reusable components that can be defined once and used in every case by changing some settings. Think about a contact form widget that you can drag and drop on any web page. Well, these kinds of reusable components in Pimcore are called Bricks.
In this chapter, we will learn how to build reusable components that can be placed in CMS or MVC pages and, moreover, can be ported from project to project using bundles.
This is our roadmap:
By the end of this chapter, you will have learned how to create custom interactive widgets to compose pages. This is important to cover all the needs of the users on your website.
Let's start to discover bricks!
As with the previous chapters, there is a demo that you can find on our GitHub repository here: https://github.com/PacktPublishing/Modernizing-Enterprise-CMS-using-Pimcore/.
All you need to run the demo connected to this chapter is to clone it, then navigate to the Full Demo folder and start the Docker environment.
To do so, just follow these instructions:
docker-compose up
docker-compose exec php bash restore.sh
What you will get with this setup is the following:
This project is a good reference, but after all the practice we have had with Pimcore, you could also start a project from scratch and try to replicate all the steps on your own.
Before starting our journey into bricks, we have to learn how to create a bundle. In Chapter 7, Administrating Pimcore Sites, we learned how to install a bundle released from a vendor, but how do we build our own? In this section, we will learn how a bundle is structured and how you can build it. Bundles are very important for creating a portable set of features that you can reuse or distribute across websites. In our demo project, we will create a blog bundle that is self-contained and that you can pick and place on any of your websites.
You have used the main application for many examples in previous chapters. This is good for implementing the specific project but it is not portable. Talking simply, a bundle is a folder that contains both source code and templates. You can get this set of files by adding a composer dependency or by using a local folder. This lets you take your code and reuse it in multiple projects, or simply divide a complex application into modules. For simplicity, in this book, we will use a local folder inside the bundles path. Each subfolder will host a different bundle. In this chapter, we will cover all that is needed to start a blog, so we will create a BlogBundle. This means that we will have the /bundles/BlogBundle folder that will contain all the bundle-related files. This set of files is not discovered automatically; you have to add a specific configuration in your composer.json. In the next piece of code, there is the configuration for the blog bundle:
"autoload": {
"psr-4": {
"App\": "src/",
"BlogBundle\": "bundles/BlogBundle/",
"Pimcore\Model\DataObject\": "var/classes/DataObject",
"Pimcore\Model\Object\": "var/classes/Object",
"Website\": "legacy/website/lib"
}
},
As you can see in the previous snippet, the blog folder is added to the psr-4 definition, just after the standard src that's mapped to the App namespace. In our case, we map the BlogBundle namespace with the bundles/BlogBundle/ folder. Of course, you can play with this configuration and create your own setup to fit your needs. Anyway, we recommend keeping the configuration as close as possible to the Symfony standard.
Here is a list of folders and files inside a bundle:
a) config: Where your YAML files are built.
b) public: Here you can load all the assets that will be published under /bundles/{bundle name}/, so if you add here a file called script.js, you will have it at http://localhost/bundles/blog/script.js.
c) views: You can create here a subfolder containing templates for each controller. This folder also contains the Areas subfolder, which will have all the Brick templates.
Now it's time to create our first bundle!
You could manually create the files and folders by using the naming convention. This is not hard, but it is easy to make some errors while doing it manually. Fortunately, we have a command from Pimcore that does the job for us.
In Symfony 5, this is no longer a built-in feature, so we have to install a bundle from Pimcore and then we can use the console to create a bundle skeleton.
Creating a bundle is very straightforward and will be explained in the next steps:
docker-compose exec php bash
composer require pimcore/bundle-generator
The previous command will add the bundle and configure it for use as a regular console command.
<?php
use PimcoreBundleBundleGeneratorBundle
PimcoreBundleGeneratorBundle;
return [
PimcoreBundleGeneratorBundle::class => ['all' => true],
];
bin/console pimcore:generate:bundle BlogBundle
This will create a set of folders and files for your bundle. The result after you run this command is the creation of the bundle with all the basic subfolders.
"psr-4": {
"App\": "src/",
"BlogBundle\": "bundles/BlogBundle/",
…
}
After this step, you might need to run chmod -R www-data. for a permission fix. In the Docker example we provided, this is mandatory.
In this section, we learned how a bundle is composed and how to create a new one. Now that we have our bundle ready, we can start talking about bricks by using some practical examples.
In simple words, a Brick is composed of a class that takes the place of the controller and a view. Building a Brick is not so different from implementing an MVC page. The most important exception is that, in this case, we do not have the routing part, as the Brick is added to an existing page (it cannot be run standalone). In the following diagram, we have a schema that explains how Bricks work:
In the previous diagram, we can see that a page (My Page) can host many bricks. Each one is composed of a Class and two templates (edit and view).
In the following sections, we will learn how to implement every single component, including classes and templates.
A brick is an instance of PimcoreExtensionDocumentAreabrickAreabrickInterface, but for convenience, we always extend the AbstractTemplateAreabrick class, which implements the interface and gives us some interesting methods. These classes can be loaded manually or autoloaded using YAML files. Even if adding classes to YAML files is easy, it is always an additional step to do. So, we usually prefer the autoloading scenario, which simply requires us to use a default folder (Document/Areabrick) where we place the classes. The namespace of your class must be namespace BlogBundleDocumentAreabrick.
The following class implements a simple brick:
class MyBrick extends AbstractTemplateAreabrick
{
public function getName()
{
return 'The Brick Name';
}
public function getDescription()
{
return 'The Brick description';
}
public function getTemplateSuffix()
{
return static::TEMPLATE_SUFFIX_TWIG;
}
}
As you can see in the preceding snippet, there are some methods that have to be implemented to provide the information for your components. These methods, highlighted in the code, are as follows:
Now that the class part is ready, we can see how to set up the templating in the next section.
Usually, for brick classes, the template follows a naming convention. The place where they have to be located is the Areas folder inside the view folder (Resources/views). Each brick must have its own folder, but the folder name must be in spinal case (all lowercase with hyphens between words, so MyBrick will need a folder named my-brick). The name of the view template has to be view.html.twig.
Some bricks are fully WYSIWYG, and you can change the component's behavior just by entering data. Others have the configuration separated by the rendering. In these cases, you can configure an edit popup that will prompt for data. We will see that configuration in detail with the next sections' examples.
In the next schema, we summarized the naming convention, adding a path example for each case. The Global scenario is the option where you add the brick to the main project (app folder) and the Bundle scenario is where you will add the brick to specific a bundle:
a) Global:
templates/views/Areas/{BrickID}/view.html.(php|twig)
b) Bundle:
{BundleLocation}/Resources/views/Areas/iframe/view.html.(php|twig)
a) Global:
src/Document/Areabrick/{BrickID}
b) Bundle:
{BundleLocation}/Document/Areabrick/{BrickID}
In this section, we learn what a brick is composed of. This was important for understanding the naming convention and the principles for using them. Now it's time to go in depth with some examples! In the next section, we will cover the most important use cases, from easy to complex usage.
In this section, we will implement our first brick. Because the spirit of this book is to learn using real-world examples, we won't limit this to a "hello world" example. In our first example, we will create a widget that could be placed many times on the page and reused. This widget will allow adding text, choosing the header type (h1, h2, and so on), and entering the text.
To complete this goal, we have to follow these steps:
class Header extends AbstractTemplateAreabrick
{
public function getName()
{
return 'Header';
}
public function getDescription()
{
return 'A component for rendering a Header';
}
public function getTemplateLocation()
{
return static::TEMPLATE_LOCATION_BUNDLE;
}
}
The code provided declares a brick called Header with a specific description. Now that we have the brick definition, we have to add a template for it. This will be our next step.
{% if editmode %}
{{pimcore_select('style', {
"store" : [
['h1', 'H1'],
['h2', 'H2'],
['h3', 'H3']
],
"defaultValue" : "h1"
})}}
{{pimcore_input('text')}}
{% else %}
<{{pimcore_select('style')}}>{{pimcore_input('text')}}<{{pimcore_select('style')}}>
{% endif %}
The code is divided into two branches. The first one is activated in edit mode and displays a select component that lets you choose the heading type (h1, h2, and so on) and the text; the second branch of code displays the data wrapping text in the header. All we need to implement our first brick is done; we just have to test it now.
{{ pimcore_areablock("header")}}
This editable will display the following component in edit mode:
This component is just a placeholder that will let us choose a brick from the brick list and will put it inside the page. We will do this in the next step.
As you can see in the preceding screenshot, the data we entered into the class is used to distinguish the component. In fact, we chose Header as the brick name and A component for rendering a Header as the description.
The page displays the text we chose properly.
This first example shows how simple it is to create a reusable component in Pimcore. In fact, we can use the heading brick on every page, giving the user the power of picking it when needed and configuring it. You can use this in conjunction with blocks to allow the user to choose a sequence of custom elements or hardcode it in a template. Moreover, we can also use an interactive brick. We will discover all these features in the next sections.
In this example, we will discover how to create an interactive component where the user can insert data. For this purpose, we will create a contact form. The behavior of this widget will be straightforward: we will have a form with a subject, name, message, and clickable button. An email will be sent to a fixed recipient address once the details are filled in and the button is clicked. This example will also introduce a working example of opening the brick's editor to get parameters not shown in the view. Follow these steps to implement the example:
class ContactForm extends AbstractTemplateAreabrick
{
public function getName()
{
return 'ContactForm';
}
public function getDescription()
{
return 'ContactForm';
}
public function getTemplateLocation()
{
return static::TEMPLATE_LOCATION_BUNDLE;
}
}
class ContactForm extends AbstractTemplateAreabrick implements EditableDialogBoxInterface
{
public function getEditableDialogBoxConfiguration(DocumentEditable $area, ?Info $info): EditableDialogBoxConfiguration
{
$config = new EditableDialogBoxConfiguration();
$config->setItems([
'type' => 'tabpanel',
'items' => [
[
'type' => 'panel',
'title' => 'Contact Form Settings',
'items' => [
[
'type' => 'input',
'label' => 'Email Recipient',
'name' => 'recipient'
]
]
]
]]);
return $config;
}
}
The configuration is, in fact, an array of items that can be grouped in a container. In our case, we used a tab pane, and we placed the input inside it. This array will be used to automatically generate the user's input form. Entered data will be available to the user as regular editables, as in the following snippet:
$recipient=$this->getDocumentEditable($info->getDocument(), 'input', 'recipient')->getData();
{% if alert is defined %}
{{ alert }}
{% endif%}
<form id="contact-form" name="contact-form" method="POST">
<input type="hidden" name="sendEmail" value="true"/>
<input type="text" id="name" name="name" >
<input type="text" id="email" name="email" >
<input type="text" id="subject" name="subject" >
<textarea type="text" id="message" name="message" >
</textarea>
<input type="submit" value="submit" />
</form>
The template contains an alert message, which is a message used to confirm sending the email to the user or to display an error. The form contains the input for getting the field from the user and a submit button. The action is not specified, so this form will submit data to the page itself. The input hidden sendEmail is a flag that will activate the sending procedure.
public function action(Info $info)
{
$request=$info->getRequest();
$id= $info->getEditable()->getName();
$info->setParam('id', $id);
$sendEmail=$request->get("sendEmail");
if($sendEmail==$id)
{
$name=$request->get("name");
$email=$request->get("email");
$subject=$request->get("subject");
$message=$request->get("message");
//send an email here
$sent= $this->sendEmail($name,$email,
$subject,$message, $recipient);
if($sent)
{
$alert="the message is sent!";
}
else
{
$alert="there was an error, try later";
}
$info->setParam('name',$name);
$info->setParam('email',$email);
$info->setParam('subject',$subject);
$info->setParam('message',$message);
$info->setParam('alert',$alert);
}
$recipient=$this->getDocumentEditable($info->
getDocument(), 'input', 'recipient')->getData();
$info->setParam('recipient',$recipient);
}
The preceding code implements the logic for getting parameters and sends an email notifying the user about the result. $request=$info->getRequest(); is used to get the HTTP request that contains the submitted data, and the get method is used to obtain the value of the sendEmail flag, which activates the sending procedure. You can pass variables to the view by using the parameters, as in the following piece of code:
$info->setParam('recipient',$recipient);
Now all the components are in place to test our brick.
You can enter an email address to be used as the recipient for the contact form.
As you have learned from this example, it is quite easy to implement an interactive widget such as a contact form. Anyway, there are some tricks to know to avoid conflicts when you have multiple components on the same page. We will explain this in the next section.
For the contact form example, we have to raise a point about submitting data on a page with multiple bricks. Using the post approach, we send data to the server and manage the request on the backend. This procedure is very easy but it can lead to some issues. In fact, think about a case where you have many components on the same page.
In our example, if we put two contact form widgets on the same page, clicking send will trigger both actions. The same can happen with different components with similar field names.
To avoid such conflicts, follow these troubleshooting steps:
…
$id= $info->getEditable()->getName();
$sendEmail=$request->get("cf-sendEmail");
if($sendEmail==$id)
{
…
The email sending procedure is now processed only if the name of the component is exactly the same that originates the post. Two different instances of the same brick produce different names, so your action will be triggered only once.
The final step is to make a small change in the view template file (created in Step 3 of Implementing a contact form brick). We will be adding a hidden input with the cf-sendEmail name attribute and the ID computed from the action method as value. Cut and paste the next snippet to your view file:
<input type="hidden" name="cf-sendEmail" value="{{id}}"/>
This value will be sent back to our action method with the post argument and we will be comparing it with the one generated on the server side. If they are not equal, the post is not matched to the current component, and we avoid any action.
In this section, we learned how to implement a contact form. The example we have just finished showed us how simple it is to create an interactive brick that can be reused. You are not only able to reuse this component on any page of your website, but you can also copy the bundle to another website and get this feature for free. In the next example, we will discover how to implement a slideshow, mixing controllers and bricks to reuse the code that we might have written for an MVC page.
In this example, we will build a slideshow widget that can be used to display a carousel of images. This will be very easy, and we will use just bootstrap and the tools learned so far. In fact, we will reuse the code used for displaying the image gallery in Chapter 9, Configuring Entities and Rendering Data, but we will integrate it into a brick. To do that, follow these steps:
The previous screenshot shows the properties table. We added a title and subtitle field with some value inside.
{{
pimcore_renderlet('myGallery', {
"controller" : "BlogBundle\Controller\
SlideShowController::galleryAction",
"title" : "Drag an asset folder here to get
a gallery",
"height" : 400
})
}}
The preceding code adds a Pimcore renderlet that lets the user drag a folder on it and uses a controller for implementing the rendering logic. In our case, we will use the gallery action from the SlideShow controller. We are using a controller in a bundle, so we must specify the bundle name also.
<?php
namespace BlogBundleController;
use PimcoreControllerFrontendController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use PimcoreModelAsset;
class SlideShowController extends FrontendController
{
}
public function galleryAction(Request $request)
{
$result=array();
if ('asset' === $request->get('type')) {
$asset = Asset::getById($request->
get('id'));
if ('folder' === $asset->getType()) {
$result['assets'] = $asset->
getChildren();
}
}
return $result;
}
The code is the same as what we used in the gallery example of Chapter 9, Configuring Entities and Rendering Data, so there is no need for more explanation.
<div id="carouselExampleControls" ...>
... omitted bootstrap tags
{% if assets %}
{% set active ='active' %}
{% for asset in assets %}
{% if asset is instanceof('\Pimcore\
Model\Asset\Image') %}
<div class="carousel-item {{ active
}}">
<img src="{{ asset.getThumbnail(
'SlideShow') }}" ... />
<div ...>
<h5>{{ asset.getProperty('title')
}}</h5>
<p>{{ asset.getProperty(
'subtitle') }}</p>
</div>
</div>
{% set active=""%}
{% endif %}
{% endfor %}
{% endif %}
... omitted bootstrap tags for navigation
</div>
In the preceding snippet, all the tags that are not relevant to our explanation are omitted for brevity. Focusing on the part that really matters, we have a simple for loop that prints images following the bootstrap carousel standard. The image thumbnail is extracted from the original image using the getThumbnail('SlideShow') function. The title and subtitle fields are read from the asset properties using the asset.getProperty method.
In the previous screenshot, we highlighted the navigation buttons and the fields printed over the image.
In this section, we discovered how we can integrate controllers and bricks. The same result can be achieved by using the relation editable covered in Chapter 8, Creating Custom CMS Pages, and implementing the template inside the brick itself. Now that we have covered all the topics relating to bricks, it's time to learn how to implement a layout that could let us create any kind of page without writing any additional code.
What we want to do in this section is to find a solution for implementing all kinds of layouts without wasting hours creating custom templates. Theoretically speaking, we could have a set of base objects and then mount them together to create all kinds of websites. In practice, this is what we should do with Pimcore:
The previous diagram shows how a multipurpose layout is structured. We have many horizontal sections (Section 1, …, Section N) that can be divided into columns (col1, col2, col3). In each place, you will be able to add bricks for composing the page in any layout you want.
Theoretically speaking, we need to add a block iteration that will print rows inside another block iteration that will print columns. This lets us create a matrix of elements where we can add an areablock that lets us choose any brick we want. This layout is quite easy to implement in words, and feasible by putting into practice what we learned in the last chapters.
In this example, we will create a generic layout like the one that is shown in the following figure:
In the preceding figure, we can note the three bands (Header, contact form with a description on the left side, and then a full width slideshow). This is, of course, a very simple use case, but with some imagination, you should understand how you can extend this to any kind of web page. Follow these steps to create a custom layout:
<?php
namespace BlogBundleDocumentAreabrick;
class Container extends AbstractTemplateAreabrick implements EditableDialogBoxInterface
{
public function getName()
{
return 'Container';
}
public function getDescription()
{
return 'Container';
}
}
public function getEditableDialogBoxConfiguration(DocumentEditable $area, ?Info $info): EditableDialogBoxConfiguration
{
$config = new EditableDialogBoxConfiguration();
$config->setWidth(600);
$config->setItems([
'type' => 'tabpanel',
'items' => [
[
'type' => 'panel',
'title' => 'Column settings',
'items' => [
[
'type' => 'select',
'label' => 'Layout',
'name' => 'layout',
'config' => [
'store' => [
['one.html.twig', 'One column'],
['two-50-50.html. twig', 'Two column 50-50'],
['two-30-70.html. twig', 'Tre column 30-70'],
]
]
]
]
]
]]);
return $config;
}
Note that, for simplicity, we used the names of a file template as values for the select item, so that the item could be simply related to the content.
public function action(Info $info)
{
$layout=$this->getDocumentEditable($info->
getDocument(), 'select', 'layout')->getData();
$info->setParam('layout',"@Blog/areas/
container/templates/$layout");
}
As you can see from the source code, the relative filename is transformed into a full path and added to the property bag.
<div class="container blog-container">
{% include layout %}
{% if editmode %}
<div class="blog-container-footer">
CONTAINER
<div>
{% endif %}
</div>
The previous piece of code includes the template based on the variable set in the action and simply wraps it in a bootstrap container. Moreover, when you are in edit mode, it adds a bar to the bottom to help the user identify the layout.
<div class="row">
<div class="col-6 blog-col">
{{ pimcore_areablock("content_50501")}}
</div>
<div class="col-6 blog-col">
{{ pimcore_areablock("content_50502")}}
</div>
</div>
As you can see in the previous piece of code, there is a bootstrap row and two columns (col-6; col-6 means the same width). Inside each column, the areablock component will allow you to choose the component to add inside it. Now we are ready to use our general-purpose template in a real-world example!
{# SETTING THE IMAGE URL#}
{% set imageurl=null %}
{% if not editmode %}
{% set image =pimcore_relation("image")%}
{% if image.getElement() is defined and image. getElement() != null %}
{% set imageurl= image.getElement(). getThumbnail("SlideShow") %}
{% endif %}
{% endif %}
{# PRINT HEADER#}
<header class="masthead" style="background-image: url({{imageurl}})">
<div class="overlay"></div>
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<div class="site-heading">
<h1> {{ pimcore_input('headline', {'width': 540}) }}</h1>
<span class="subheading"> {{ pimcore_ input('subheading', {'width': 700}) }}</span>
</div>
</div>
</div>
</div>
</header>
{# IMAGE INPUT #}
{% if editmode %}
{{ pimcore_relation("image",{
"types": ["asset"],
"subtypes": {
"asset": [ "image"],
},
"classes": ["person"]
}) }}
{% endif %}
This snippet of code will render a parametric header that is put on all our pages. You don't want to have it on all the pages? Not a problem. You can always transform this code into a brick and place it only where you really need it.
{% extends 'BlogBundle:Layout:layout.html.twig' %}
{% block content %}
{% include 'BlogBundle:Layout:header.html.twig' %}
{{ pimcore_areablock("content", {'allowed':['container']})}}
{% endblock %}
The preceding script defines a page structure with a header and an area block that will host the containers for our page layout.
The following screenshot shows the area brick that lets us create as many bands as we want. After this step, you should get the following result:
In this section, we learned how to create a template that could suit most situations. The example has been a good opportunity for testing the general-purpose layout in a real-life scenario. This template, in conjunction with all the bricks that you could create, will cover the most common scenarios and will save a lot of time.
In this chapter, we continued our journey with the Pimcore CMS by discovering the bricks engine, another important tool for creating dynamic websites. By creating bricks, it's easy to prepare reusable components that can be used by web page editors to compose any website without asking the developers for customization. This new way to proceed is very important in reducing the development effort, keeping quality standards high, and increasing the speed of implementing the features that users want.
To be more specific, we discovered how bricks work by implementing real-world examples. The contact form and slideshow are components that you will reuse in your projects for sure. Moreover, we also learned how to create a general-purpose template that enables us to produce any layout of a page without writing a single line of code.
In the next chapter, we will learn how to finalize our website by discovering some important details and solutions for everyday Pimcore usage. To list the most important ones, we will learn how to create a bundle's installers to easily recreate our classes and contents after the setup, and we will learn how to create a multisite instance of Pimcore.
3.133.144.217