Important
You are browsing the documentation for version 3.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get the updated information.
See our Release Process documentation for more information on the currently supported and upcoming releases.
Customize Product View Page
In this example we look at how to customize product with different product types, and change product page by category.
Every product view page contains the current product_type in the layout context. You can use it in your layout update conditions. When you customize any page, remember to use Symfony Profiler and look into the Layout section, where the current layout context data and actual layout tree can be found.
Please see the Debug Information section for more details.
First, create an import and a template that will be used in a Simple Product and a Configurable Product.
Import:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | layout:
actions:
- '@setBlockTheme':
themes: 'AcmeProductBundle:layouts:default/imports/oro_product_view/layout.html.twig'
- '@addTree':
items:
product_view_page:
blockType: container
product_container:
blockType: product_view_container
options:
product: '=data["product"]'
product_title_mobile:
blockType: text
options:
text: '=data["locale"].getLocalizedValue(data["product"].getNames())'
product_image:
blockType: product_image
options:
product: '=data["product"]'
product_description_container:
blockType: container
product_title_container:
blockType: container
product_specification_container:
blockType: container
product_specification:
blockType: container
product_specification_sku:
blockType: text
options:
text: '=data["product"].getSku()'
product_description_content:
blockType: container
product_description:
blockType: text
options:
text: '=data["locale"].getLocalizedValue(data["product"].getDescriptions())'
escape: false
tree:
page_content:
product_view_page:
product_container:
product_image: ~
product_description_container:
product_title_container: ~
product_specification_container:
product_specification:
product_specification_sku: ~
product_description_content:
product_description: ~
- '@setOption':
id: title
optionName: params
optionValue:
'%name%': '=data["locale"].getLocalizedValue(data["product"].getNames())'
- '@setOption':
id: page_title
optionName: defaultValue
optionValue: '=data["locale"].getLocalizedValue(data["product"].getNames())'
- '@move':
id: page_title
parentId: product_title_container
|
Template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | {% block _product_container_widget %}
{% set attr = layout_attr_defaults(attr, {
'data-page-component-module': 'oroui/js/app/components/view-component',
'~data-page-component-options': {
view: 'oroproduct/js/app/views/base-product-view',
modelAttr: product.jsonSerialize()
},
'data-layout': 'separate',
'~class': ' product clearfix'
}) %}
<div {{ block('block_attributes') }}>
{{ block_widget(block) }}
</div>
{% endblock %}
{% block _page_title_widget %}
<h3 class="product-title">{{ text }}</h3>
{% endblock %}
{% block _product_image_widget %}
<div class="product-gallery-widget product-gallery-widget_vertical product-gallery-widget_l_floated">
<div class="product-gallery product-gallery_vertical">
<div class="product-gallery__image-holder">
<div class="product-gallery__image-holder__carousel" data-product-gallery>
<div class="product-gallery__image-holder__container">
{% set productImage = product.imagesByType('main')|length > 0 ? product.imagesByType('main').first.image : null %}
<img src="{{ product_filtered_image(productImage, 'product_extra_large') }}"
alt="{{ product.names|localized_value }}"
width="378"
height="378"
{% if productImage and isDesktopVersion() %}
data-zoom-image="{{ product_filtered_image(productImage, 'product_original') }}"
{% set options = {
widgetModule: 'jquery-elevatezoom',
widgetName: 'elevateZoom',
scrollZoom: true,
zoomWindowWidth: 630,
zoomWindowHeight: 376,
borderSize: 1,
borderColour: '#ebebeb',
lensBorderColour: '#7d7d7d',
lensColour: '#000',
lensOpacity: 0.22
}|json_encode() %}
data-page-component-module="oroui/js/app/components/jquery-widget-component"
data-page-component-options="{{ options }}"
{% endif %}
/>
</div>
{% for productImage in product.imagesByType('additional') %}
<img src="{{ product_filtered_image(productImage.image, 'product_small') }}" width="82" height="82"/>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block _product_description_container_widget %}
<div class="product__description-container">
{{ block_widget(block) }}
</div>
{% endblock %}
{% block _product_title_container_widget %}
{% set attr = attr|merge({
class: attr.class|default('') ~ " product__title-container"
}) %}
<div {{ block('block_attributes') }}>
{{ block_widget(block) }}
</div>
{% endblock %}
{% block _product_specification_container_widget %}
<div class="product__specification-container product__specification-container_l_floated">
{{ block_widget(block) }}
</div>
{% endblock %}
{% block _product_specification_widget %}
<div class="product__specification">
{{ block_widget(block) }}
</div>
{% endblock %}
{% block _product_specification_sku_widget %}
<div>
{{ 'oro.product.frontend.index.item'|trans }} <span class="red">{{ block_widget(block) }}</span>
</div>
{% endblock %}
{% block _product_description_content_widget %}
<div class="product__description-content product__description-content_l_floated">
<div class="product__description">
{{ block_widget(block) }}
</div>
</div>
{% endblock %}
|
Simple Product
In our case a simple product inherits all import properties.
Create a layout update that includes the oro_product_view import and has the conditions to check if the current product has a simple product type.
Our simple product has the following look:

1 2 3 4 5 6 7 | layout:
imports:
- oro_product_view
actions: []
conditions: 'context["product_type"] == "simple"'
|
Configurable Product
Create a layout update that includes the oro_product_view import and has the conditions to check if the current product has a configurable product type.
Add the product variants block and the template.
Our configurable product has the following look:

Product Variants Block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | layout:
imports:
- oro_product_view
actions:
- '@setBlockTheme':
themes: 'AcmeProductBundle:layouts:default/oro_product_frontend_product_view/configurable_product.html.twig'
- '@add':
id: product_variants
blockType: product_variants
parentId: product_specification_container
siblingId: product_specification
options:
variants: '=data["product_variants"].getVariants(data["product"])'
conditions: 'context["product_type"] == "configurable"'
|
Template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {% block _product_variants_widget %}
{% for variant in variants %}
{% set key = variant.name|lower %}
<div class="product__{{ key }}s">
<label for="product_variants_{{ key }}">Select {{ variant.name }}</label>
{% if key == 'color' %}
{% for key, name in variant.elements %}
<span class="badge badge_sm product__colors__item color-item_{{ key }}">
<input type="checkbox" name="{{ key }}" alt="{{ name }}" />
</span>
{% endfor %}
{% else %}
<select id="product_variants_{{ key }}" class="select select--size-s">
{% for key, name in variant.elements %}
<option value="{{ key }}">{{ name }}</option>
{% endfor %}
</select>
{% endif %}
</div>
{% endfor %}
{% endblock %}
|
Block Types
For this example we need to create a product_variants block type used in the configurable product layout update.
1 2 3 4 5 6 7 8 9 10 | services:
...
acme_product.layout.type.product_variants:
parent: oro_layout.block_type.abstract_configurable_container
calls:
- [setOptionsConfig, [{variants: {required: true}}]]
- [setName, ['product_variants']]
tags:
- { name: layout.block_type, alias: product_variants }
...
|
Data Providers
Also, we need to create a product_variants data provider used in the configurable product layout update.
1 2 3 4 5 6 7 | services:
...
acme_product.layout.data_provider.product_variants:
class: Acme\Bundle\ProductBundle\Layout\DataProvider\ProductVariantsProvider
tags:
- { name: layout.data_provider, alias: product_variants }
...
|
The following is an example of the data provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?php
namespace Acme\Bundle\ProductBundle\Layout\DataProvider;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Oro\Bundle\EntityExtendBundle\Entity\AbstractEnumValue;
use Oro\Bundle\ProductBundle\Entity\Product;
class ProductVariantsProvider
{
/**
* @param Product $product
*
* @return array
*/
public function getVariants(Product $product)
{
$variants = [];
$variantFields = $product->getVariantFields();
foreach ($variantFields as $variantField) {
$variants[strtolower($variantField)]['name'] = $variantField;
}
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$variantLinks = $product->getVariantLinks();
foreach ($variantLinks as $variantLink) {
$childProduct = $variantLink->getProduct();
foreach ($variants as $key => $variant) {
/** @var AbstractEnumValue $enumValue */
$enumValue = $propertyAccessor->getValue($childProduct, $key);
$variants[$key]['elements'][$enumValue->getId()] = $enumValue->getName();
}
}
return $variants;
}
}
|
Change Product Page by Category
Every product view page contains the current category_id and the category_ids in the layout context. You can use these values to evaluate the layout conditions. When you customize any page, remember to use Symfony Profiler and look into the Layout section, where the current layout context data and actual layout tree can be found.
Please see the Debug Information section for more details.
Example 1 (by category ID)
We have a “Headlamps” category and we want to add some static html to all products in this category.
The condition is: conditions: ‘context[“category_id”] == 4’.
The result is:

1 2 3 4 5 6 7 8 9 10 11 12 13 | layout:
actions:
- '@setBlockTheme':
themes: 'AcmeProductBundle:layouts:default/oro_product_frontend_product_view/headlamps.html.twig'
- '@add':
id: product_sale_banner
blockType: block
parentId: product_view_main_container
siblingId: ~
prepend: false
conditions: 'context["category_id"] == 4'
|
Template:
1 2 3 4 5 6 | {% block _product_sale_banner_widget %}
<div class="text-right">
<img src="{{ asset('/bundles/oroproduct/default/images/headlamps.jpg') }}"/>
</div>
<br />
{% endblock %}
|
Example 2 (by parent category ID)
As an example, we want to add a sale banner to all products in first level category and their children. We have a category called “Furniture” with id = 6.
The condition is: conditions: ‘6 in context[“category_ids”]’.
The result is:

1 2 3 4 5 6 7 8 9 10 11 12 13 | layout:
actions:
- '@setBlockTheme':
themes: 'AcmeProductBundle:layouts:default/oro_product_frontend_product_view/furniture.html.twig'
- '@add':
id: product_sale_banner
blockType: block
parentId: page_content
siblingId: ~
prepend: true
conditions: '6 in context["category_ids"]'
|
Template:
1 2 3 4 5 6 | {% block _product_sale_banner_widget %}
<div class="text-center">
<img src="{{ asset('/bundles/oroproduct/default/images/furniture_sale.jpg') }}"/>
</div>
<br />
{% endblock %}
|
Product Page Templates
Please see the Page Templates section for more details.
You can modify the visual presentation of the product view page for every product, or choose a page template for all of them by default.
First, create a config for the page_templates in our theme.
1 2 3 4 5 6 7 8 9 10 11 12 13 | templates:
-
label: Custom page template
description: Custom page template description
route_name: oro_product_frontend_product_view
key: custom
-
label: Parent Additional page template
description: Additional page template description
route_name: oro_product_frontend_product_view
key: additional
titles:
oro_product_frontend_product_view: Product Page
|
Next, add some layout updates:
1 2 3 4 5 6 7 | layout:
actions:
- '@remove':
id: product_view_attribute_group_images
- '@move':
id: product_view_specification_container
parentId: product_view_aside_container
|
1 2 3 4 5 6 7 8 | layout:
actions:
- '@setBlockTheme':
themes: 'AcmeProductBundle:layouts:default/oro_product_frontend_product_view/page_template/additional/layout.html.twig'
- '@add':
id: product_view_banner
blockType: block
parentId: product_view_content_container
|
Add some templates:
1 2 3 4 5 6 | {% block _product_view_banner_widget %}
<div class="text-center">
<img src="{{ asset('/bundles/oroproduct/default/images/flashlights.png') }}"/>
</div>
<br />
{% endblock %}
|
Global Level
To apply a custom page template to all products, go to System > Configuration > Commerce > Design > Theme.
In the Page Templates section, choose Custom page template in the Product Page select.
In the storefront, here is what it will look like:

Entity level
To apply a custom page template to the selected products, go to Products > Products, find your product and click edit.
In the Design section, choose Additional page template in the Page Template select.
In the storefront, here is what it will look like:
