15 min read

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:

  • The EAV approach
  • Store relation table
  • Translation of template interface texts

(For more resources related to this topic, see here.)

The EAV approach

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.

Store relation table

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.

Creating the new table

The first step is to create the new table to store the new data:

  1. Create the [extension_path]/Setup/UpgradeSchema.php file and add the following code:
    <?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.

  2. Open the [extension_path]/etc/module.xml file and change the version number 1.2.0 to 1.3.0:
    <?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>
  3. In your terminal, run the upgrade by typing the following command:

    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.

Adding the new input to the edit form

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.

Saving the new data in the new table

Now we have the form and the database table, we have to write the code to save the data from the form:

  1. Open the [extension_path]/Model/Event.php file and add the following method at its end:
    /**
         * Receive page store ids
         *
         * @return int[]
         */
        public function getStores()
        {
            return $this->hasData('stores') ? $this->getData('stores') : $this->getData('store_id');
        }
  2. Open the [extension_path]/Model/ResourceModel/Event.php file and replace all the code with the following code:
    <?php
    
    namespace BlackbirdTicketBlasterModelResourceModel;
    
    class Event extends MagentoFrameworkModelResourceModelDbAbstractDb
    {
    
    [...]
  3. The afterSave() method is handling our insert queries in the new table. The afterload() and getLoadSelect() methods are handling the new load mode to select the right events.
  4. Your new table is now filled when you save your events; they are also properly loaded when you go back to your edit form.

Showing the store views in the admin grid

In order to inform admin users of the selected store views for one event, we will add a new column in the admin grid:

  1. Open the [extension_path]/Model/ResourceModel/Event/Collection.php file and replace all the code with the following code:
    <?php
    
    namespace BlackbirdTicketBlasterModelResourceModelEvent;
    
    class Collection extends MagentoFrameworkModelResourceModelDbCollectionAbstractCollection
    {
    
    [...]
  2. Open the [extention_path]/view/adminhtml/ui_component/ticketblaster_event_listing.xml file and add the following XML instructions before the end of the </filters> tag:
    <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>
  3. Before the actionsColumn tag, add the new column:
    <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>
  4. You can refresh your grid page and see the new column added at the end.

    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.

Modifying the frontend event list

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:

  1. Edit the [extension_path]/Block/EventList.php file and replace the code with the following code:
    <?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();
        }
    }
  2. Note that we have a new property available and instantiated in our constructor: storeManager. Thanks to this class, we can filter our collection with the store view ID by calling the addStoreFilter() method on our events collection.

Restricting the frontend access by store view

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:

  1. Open the [extention_path]/Helper/Event.php file and replace the code with the following code:
    <?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;
        }
    }
  2. The setStoreId() method called on our model will load the model only for the given ID. The events are no longer available through their direct URL if we are not on their available store view.

Translation of template interface texts

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:

  1. Create the [extension_path]/i18n folder.
  2. Create [extension_path]/i18n/en_US.csv and add the following code:
    "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.

Translation of e-mail templates: creating and translating the e-mails

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.

  1. To declare your e-mail template, create a new [extension_path]/etc/email_templates.xml file and add the following code:
    <?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).

  2. Next, create the corresponding template by creating the [extension_path]/view/adminhtml/email/share_form.html file and add the following code:
    <!--@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:

  1. Create the [extension_path]/etc/adminhtml/system.xml file and add the following code:
    <?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>
  2. Create the [extension_path]/etc/config.xml file and add the following code:
    <?xml version="1.0"?>
    <config xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
    <ticketblaster>
    <email>
    <recipient_email>
    <![CDATA[[email protected]]]>
    </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:

  1. Open the existing [extension_path]/view/frontend/templates/view.phtml file and add the following code at the end:
    <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>
  2. Create the [extension_path]/Controller/View/Share.php file and add the following code:
    <?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.

Testing our code by sending the e-mail

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.

Summary

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.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here