In this article by Jérémie Bouchet author of the book Magento Extensions Development. We will see how to handle this aspect of our extension and how it is handled in a complex extension using an EAV table structure.
In this article, we will cover the following topics:
(For more resources related to this topic, see here.)
The EAV structure in Magento is used for complex models, such as customer and product entities. In our extension, if we want to add a new field for our events, we would have to add a new column in the main table. With the EAV structure, each attribute is stored in a separate table depending on its type. For example, catalog_product_entity, catalog_product_entity_varchar and catalog_product_entity_int.
Each row in the subtables has a foreign key reference to the main table. In order to handle multiple store views in this structure, we will add a column for the store ID in the subtables.
Let's see an example for a product entity, where our main table contains only the main attribute:
The varchar table structure is as follows:
The 70 attribute corresponds to the product name and is linked to our 1 entity.
There is a different product name for the store view, 0 (default) and 2 (in French in this example).
In order to create an EAV model, you will have to extend the right class in your code. You can inspire your development on the existing modules, such as customers or products.
In our extension, we will handle the store views scope by using a relation table. This behavior is also used for the CMS pages or blocks, reviews, ratings, and all the models that are not EAV-based and need to be store views-related.
The first step is to create the new table to store the new data:
<?php
namespace BlackbirdTicketBlasterSetup;
use MagentoEavSetupEavSetup;
use MagentoEavSetupEavSetupFactory;
use MagentoFrameworkSetupUpgradeSchemaInterface;
use MagentoFrameworkSetupModuleContextInterface;
use MagentoFrameworkSetupSchemaSetupInterface;
/**
* @codeCoverageIgnore
*/
class UpgradeSchema implements UpgradeSchemaInterface
{
/**
* EAV setup factory
*
* @varEavSetupFactory
*/
private $eavSetupFactory;
/**
* Init
*
* @paramEavSetupFactory $eavSetupFactory
*/
public function __construct(EavSetupFactory $eavSetupFactory)
{
$this->eavSetupFactory = $eavSetupFactory;
}
public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
if (version_compare($context->getVersion(), '1.3.0', '<')) {
$installer = $setup;
$installer->startSetup();
/**
* Create table 'blackbird_ticketblaster_event_store'
*/
$table = $installer->getConnection()->newTable(
$installer->getTable('blackbird_ticketblaster_event_store')
)->addColumn(
'event_id',
MagentoFrameworkDBDdlTable::TYPE_SMALLINT,
null,
['nullable' => false, 'primary' => true],
'Event ID'
)->addColumn(
'store_id',
MagentoFrameworkDBDdlTable::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'primary' => true],
'Store ID'
)->addIndex(
$installer->getIdxName('blackbird_ticketblaster_event_store', ['store_id']),
['store_id']
)->addForeignKey(
$installer->getFkName('blackbird_ticketblaster_event_store', 'event_id', 'blackbird_ticketblaster_event', 'event_id'),
'event_id',
$installer->getTable('blackbird_ticketblaster_event'),
'event_id',
MagentoFrameworkDBDdlTable::ACTION_CASCADE
)->addForeignKey(
$installer->getFkName('blackbird_ticketblaster_event_store', 'store_id', 'store', 'store_id'),
'store_id',
$installer->getTable('store'),
'store_id',
MagentoFrameworkDBDdlTable::ACTION_CASCADE
)->setComment(
'TicketBlaster Event To Store Linkage Table'
);
$installer->getConnection()->createTable($table);
$installer->endSetup();
}
}
}
The upgrade method will handle all the necessary updates in our database for our extension. In order to differentiate the update for a different version of the extension, we surround the script with a version_compare() condition.
Once this code is set, we need to tell Magento that our extension has new database upgrades to process.
<?xml version="1.0"?>
<config xsi_noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
<module name="Blackbird_TicketBlaster" setup_version="1.3.0">
<sequence>
<module name="Magento_Catalog"/>
<module name="Blackbird_AnotherModule"/>
</sequence>
</module>
</config>
php bin/magentosetup:upgrade
The new table structure now contains two columns: event_id and store_id. This table will store which events are available for store views:
If you have previously created events, we recommend emptying the existing blackbird_ticketblaster_event table, because they won't have a default store view and this may trigger an error output.
In order to select the store view for the content, we will need to add the new input to the edit form. Before running this code, you should add a new store view:
Here's how to do that:
Open the [extension_path]/Block/Adminhtml/Event/Edit/Form.php file and add the following code in the _prepareForm() method, below the last addField() call:
/* Check is single store mode */
if (!$this->_storeManager->isSingleStoreMode()) {
$field = $fieldset->addField(
'store_id',
'multiselect',
[
'name' => 'stores[]',
'label' => __('Store View'),
'title' => __('Store View'),
'required' => true,
'values' => $this->_systemStore->getStoreValuesForForm(false, true)
]
);
$renderer = $this->getLayout()->createBlock(
'MagentoBackendBlockStoreSwitcherFormRendererFieldsetElement'
);
$field->setRenderer($renderer);
} else {
$fieldset->addField(
'store_id',
'hidden',
['name' => 'stores[]', 'value' => $this->_storeManager->getStore(true)->getId()]
);
$model->setStoreId($this->_storeManager->getStore(true)->getId());
}
This results in a new multiselect field in the form.
Now we have the form and the database table, we have to write the code to save the data from the form:
/**
* Receive page store ids
*
* @return int[]
*/
public function getStores()
{
return $this->hasData('stores') ? $this->getData('stores') : $this->getData('store_id');
}
<?php
namespace BlackbirdTicketBlasterModelResourceModel;
class Event extends MagentoFrameworkModelResourceModelDbAbstractDb
{
[...]
In order to inform admin users of the selected store views for one event, we will add a new column in the admin grid:
<?php
namespace BlackbirdTicketBlasterModelResourceModelEvent;
class Collection extends MagentoFrameworkModelResourceModelDbCollectionAbstractCollection
{
[...]
<filterSelect name="store_id">
<argument name="optionsProvider" xsi_type="configurableObject">
<argument name="class" xsi_type="string">MagentoCmsUiComponentListingColumnCmsOptions</argument>
</argument>
<argument name="data" xsi_type="array">
<item name="config" xsi_type="array">
<item name="dataScope" xsi_type="string">store_id</item>
<item name="label" xsi_type="string" translate="true">Store View</item>
<item name="captionValue" xsi_type="string">0</item>
</item>
</argument>
</filterSelect>
<column name="store_id" class="MagentoStoreUiComponentListingColumnStore">
<argument name="data" xsi_type="array">
<item name="config" xsi_type="array">
<item name="bodyTmpl" xsi_type="string">ui/grid/cells/html</item>
<item name="sortable" xsi_type="boolean">false</item>
<item name="label" xsi_type="string" translate="true">Store View</item>
</item>
</argument>
</column>
Magento remembers the previous column's order. If you add a new column, it will always be added at the end of the table. You will have to manually reorder them by dragging and dropping them.
Our frontend list (/events) is still listing all the events. In order to list only the events available for our current store view, we need to change a file:
<?php
namespace BlackbirdTicketBlasterBlock;
use BlackbirdTicketBlasterApiDataEventInterface;
use BlackbirdTicketBlasterModelResourceModelEventCollection as EventCollection;
use MagentoCustomerModelContext;
class EventList extends MagentoFrameworkViewElementTemplate implements MagentoFrameworkDataObjectIdentityInterface
{
/**
* Store manager
*
* @var MagentoStoreModelStoreManagerInterface
*/
protected $_storeManager;
/**
* @var MagentoCustomerModelSession
*/
protected $_customerSession;
/**
* Construct
*
* @param MagentoFrameworkViewElementTemplateContext $context
* @param BlackbirdTicketBlasterModelResourceModelEventCollectionFactory $eventCollectionFactory,
* @param array $data
*/
public function __construct(
MagentoFrameworkViewElementTemplateContext $context,
BlackbirdTicketBlasterModelResourceModelEventCollectionFactory $eventCollectionFactory,
MagentoStoreModelStoreManagerInterface $storeManager,
MagentoCustomerModelSession $customerSession,
array $data = []
) {
parent::__construct($context, $data);
$this->_storeManager = $storeManager;
$this->_eventCollectionFactory = $eventCollectionFactory;
$this->_customerSession = $customerSession;
}
/**
* @return BlackbirdTicketBlasterModelResourceModelEventCollection
*/
public function getEvents()
{
if (!$this->hasData('events')) {
$events = $this->_eventCollectionFactory
->create()
->addOrder(
EventInterface::CREATION_TIME,
EventCollection::SORT_ORDER_DESC
)
->addStoreFilter($this->_storeManager->getStore()->getId());
$this->setData('events', $events);
}
return $this->getData('events');
}
/**
* Return identifiers for produced content
*
* @return array
*/
public function getIdentities()
{
return [BlackbirdTicketBlasterModelEvent::CACHE_TAG . '_' . 'list'];
}
/**
* Is logged in
*
* @return bool
*/
public function isLoggedIn()
{
return $this->_customerSession->isLoggedIn();
}
}
The events will not be listed in our list page if they are not available for the current store view, but they can still be accessed with their direct URL, for example http://[magento_url]/events/view/index/event_id/2.
We will change this to restrict the frontend access by store view:
<?php
namespace BlackbirdTicketBlasterHelper;
use BlackbirdTicketBlasterApiDataEventInterface;
use BlackbirdTicketBlasterModelResourceModelEventCollection as EventCollection;
use MagentoFrameworkAppActionAction;
class Event extends MagentoFrameworkAppHelperAbstractHelper
{
/**
* @var BlackbirdTicketBlasterModelEvent
*/
protected $_event;
/**
* @var MagentoFrameworkViewResultPageFactory
*/
protected $resultPageFactory;
/**
* Store manager
*
* @var MagentoStoreModelStoreManagerInterface
*/
protected $_storeManager;
/**
* Constructor
*
* @param MagentoFrameworkAppHelperContext $context
* @param BlackbirdTicketBlasterModelEvent $event
* @param MagentoFrameworkViewResultPageFactory $resultPageFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
MagentoFrameworkAppHelperContext $context,
BlackbirdTicketBlasterModelEvent $event,
MagentoFrameworkViewResultPageFactory $resultPageFactory,
MagentoStoreModelStoreManagerInterface $storeManager,
)
{
$this->_event = $event;
$this->_storeManager = $storeManager;
$this->resultPageFactory = $resultPageFactory;
$this->_customerSession = $customerSession;
parent::__construct($context);
}
/**
* Return an event from given event id.
*
* @param Action $action
* @param null $eventId
* @return MagentoFrameworkViewResultPage|bool
*/
public function prepareResultEvent(Action $action, $eventId = null)
{
if ($eventId !== null && $eventId !== $this->_event->getId()) {
$delimiterPosition = strrpos($eventId, '|');
if ($delimiterPosition) {
$eventId = substr($eventId, 0, $delimiterPosition);
}
$this->_event->setStoreId($this->_storeManager->getStore()->getId());
if (!$this->_event->load($eventId)) {
return false;
}
}
if (!$this->_event->getId()) {
return false;
}
/** @var MagentoFrameworkViewResultPage $resultPage */
$resultPage = $this->resultPageFactory->create();
// We can add our own custom page handles for layout easily.
$resultPage->addHandle('ticketblaster_event_view');
// This will generate a layout handle like: ticketblaster_event_view_id_1
// giving us a unique handle to target specific event if we wish to.
$resultPage->addPageLayoutHandles(['id' => $this->_event->getId()]);
// Magento is event driven after all, lets remember to dispatch our own, to help people
// who might want to add additional functionality, or filter the events somehow!
$this->_eventManager->dispatch(
'blackbird_ticketblaster_event_render',
['event' => $this->_event, 'controller_action' => $action]
);
return $resultPage;
}
}
In order to translate the texts written directly in the template file, for the interface or in your PHP class, you need to use the __('Your text here') method. Magento looks for a corresponding match within all the translation CSV files.
There is nothing to be declared in XML; you simply have to create a new folder at the root of your module and create the required CSV:
"Event time:","Event time:"
"Please sign in to read more details.","Please sign in to read more details."
"Read more","Read more"
Create [extension_path]/i18n/en_US.csv and add the following code:
"Event time:","Date de l'évènement :"
"Pleasesign in to read more details.","Merci de vous inscrire pour plus de détails."
"Read more","Lire la suite"
The CSV file contains the correspondences between the key used in the code and the value in its final language.
We will add a new form in the Details page to share the event to a friend. The first step is to declare your e-mail template.
<?xml version="1.0"?>
<config xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
<template id="ticketblaster_email_email_template" label="Share Form" file="share_form.html" type="text" module="Blackbird_TicketBlaster" area="adminhtml"/>
</config>
This XML line declares a new template ID, label, file path, module, and area (frontend or adminhtml).
<!--@subject Share Form@-->
<!--@vars {
"varpost.email":"Sharer Email",
"varevent.title":"Event Title",
"varevent.venue":"Event Venue"
} @-->
<p>{{trans "Your friend %email is sharing an event with you:" email=$post.email}}</p>
{{trans "Title: %title" title=$event.title}}<br/>
{{trans "Venue: %venue" venue=$event.venue}}<br/>
<p>{{trans "View the detailed page: %url" url=$event.url}}</p>
Note that in order to translate texts within the HTML file, we use the trans function, which works like the default PHP printf() function. The function will also use our i18n CSV files to find a match for the text.
Your e-mail template can also be overridden directly from the backoffice: Marketing | Email templates.
The e-mail template is ready; we will also add the ability to change it in the system configuration and allow users to determine the sender's e-mail and name:
<?xml version="1.0"?>
<config xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="ticketblaster" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Ticket Blaster</label>
<tab>general</tab>
<resource>Blackbird_TicketBlaster::event</resource>
<group id="email" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Email Options</label>
<field id="recipient_email" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Send Emails To</label>
<validate>validate-email</validate>
</field>
<field id="sender_email_identity" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Email Sender</label>
<source_model>MagentoConfigModelConfigSourceEmailIdentity</source_model>
</field>
<field id="email_template" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Email Template</label>
<comment>Email template chosen based on theme fallback when "Default" option is selected.</comment>
<source_model>MagentoConfigModelConfigSourceEmailTemplate</source_model>
</field>
</group>
</section>
</system>
</config>
<?xml version="1.0"?>
<config xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<ticketblaster>
<email>
<recipient_email>
<![CDATA[hello@example.com]]>
</recipient_email>
<sender_email_identity>custom2</sender_email_identity>
<email_template>ticketblaster_email_email_template</email_template>
</email>
</ticketblaster>
</default>
</config>
Thanks to these two files, you can change the configuration for the e-mail template in the Admin panel (Stores | Configuration).
Let's create our HTML form and the controller that will handle our submission:
<form action="<?php echo $block->getUrl('events/view/share', array('event_id' => $event->getId())); ?>" method="post" id="form-validate" class="form">
<h3>
<?php echo __('Share this event to my friend'); ?>
</h3>
<input type="email" name="email" class="input-text" placeholder="email" />
<button type="submit" class="button"><?php echo __('Share'); ?></button>
</form>
<?php
namespace BlackbirdTicketBlasterControllerView;
use MagentoFrameworkExceptionNotFoundException;
use MagentoFrameworkAppRequestInterface;
use MagentoStoreModelScopeInterface;
use BlackbirdTicketBlasterApiDataEventInterface;
class Share extends MagentoFrameworkAppActionAction {
[...]
This controller will get the necessary configuration entirely from the admin and generate the e-mail to be sent.
Go to the page of an event and fill in the form we prepared. When you submit it, Magento will send the e-mail immediately.
In this article, we addressed all the main processes that are run for internationalization. We can now create and control the availability of our events with regards to Magento's stores and translate the contents of our pages and e-mails.
Further resources on this subject: