Creating a Custom Widget in Magento 2

Sometimes you need to change the functionality of a widget or increase the selection of customization options, and in order to do this you need to create your own. In this article we will be looking at how to create a widget that extends the functionality of a core widget.

Dynamic Store Configuration Fields

Magento’s store configuration functionality allows developers to quickly and efficiently define config fields for their modules. This efficiency promotes flexible and configurable modules, saving developers and merchants time and money.

In some cases, however, a fixed list of defined config fields isn’t sufficient to configure a more dynamic feature. Fortunately, it’s relatively straightforward to implement dynamically generated store config fields, allowing developers to support complicated flexibility.

Example Use Case

The requirement for this example is that, for countries which are configured to require a state/region, only certain regions are allowed.

These countries are configured in store config at General -> General -> State Options -> State is Required for. In order to configure which regions are allowed for each country, new store config fields will need to be added, one for each configured country. Since the configured countries could change at any time, it will not be possible to hard code the allowed regions fields in system.xml. Instead, a field will need to be dynamically added to the State Options group for each configured country.

Dynamic Fields

Approach

Each element in the store config hierarchy (tab, section, and group) extends MagentoConfigModelConfigStructureElementAbstractComposite. When setting its data, AbstractComposite specifically looks for an array at key children, and uses this value to populate the children elements.

public function setData(array $data, $scope)
{
    parent::setData($data, $scope);
    $children = array_key_exists(
        'children',
        $this->_data
    ) && is_array(
        $this->_data['children']
    ) ? $this->_data['children'] : [];
    $this->_childrenIterator->setElements($children, $scope);
}

This provides the opportunity to craft a plugin to add (or remove) children, which will eventually be used to populate the store config UI. The parent element of fields is a config group, so MagentoConfigModelConfigStructureElementGroup is the specific class where calls to setData() should be intercepted.




    
        
    



    
        
    
<?php

// EW/DynamicConfigFields/Model/Config/Config/Structure/Element/Section.php

namespace EWDynamicConfigFieldsModelConfigConfigStructureElement;

use MagentoConfigModelConfigStructureElementSection as OriginalSection;
use MagentoDirectoryApiCountryInformationAcquirerInterface;
use MagentoDirectoryApiDataCountryInformationInterface;
use EWDynamicConfigFieldsHelperConfig as ConfigHelper;
use MagentoDirectoryHelperData as DirectoryHelper;

/**
 * Plugin to add dynamically generated groups to
 * General -> General section.
 *
 * @package EWDynamicConfigFieldsModelConfigConfigStructureElement
 */
class Section
{
    /**
     * Config path of target section
     */
    const CONFIG_GENERAL_SECTION_ID = 'general';

    /**
     * @var MagentoDirectoryHelperData
     */
    protected $directoryHelper;
    /**
     * @var CountryInformationAcquirerInterface
     */
    protected $countryInformationAcquirer;

    /**
     * Group constructor.
     * @param DirectoryHelper $directoryHelper
     * @param CountryInformationAcquirerInterface $countryInformationAcquirer
     */
    public function __construct(
        DirectoryHelper $directoryHelper,
        CountryInformationAcquirerInterface $countryInformationAcquirer
    )
    {
        $this->directoryHelper = $directoryHelper;
        $this->countryInformationAcquirer = $countryInformationAcquirer;
    }

    /**
     * Get config options array of regions for given country
     *
     * @param CountryInformationInterface $countryInfo
     * @return array
     */
    protected function getRegionsForCountry(CountryInformationInterface $countryInfo) : array {
        $options = [];

        $availableRegions = $countryInfo->getAvailableRegions() ?: [];

        foreach($availableRegions as $region) {
            $options[$region->getCode()] = [
                'value' => $region->getCode(),
                'label' => $region->getName()
            ];
        }

        return $options;
    }

    /**
     * Get dynamic config groups (if any)
     *
     * @return array
     */
    protected function getDynamicConfigGroups() : array {
        $countriesWithStatesRequired = $this->directoryHelper->getCountriesWithStatesRequired();

        $dynamicConfigGroups = [];
        foreach($countriesWithStatesRequired as $index => $country) {
            // Use a consistent prefix for dynamically generated fields
            // to allow them to be deterministic but not collide with any
            // preexisting fields.
            // ConfigHelper::ALLOWED_REGIONS_CONFIG_PATH_PREFIX == 'regions-allowed-'.
            $configId = ConfigHelper::ALLOWED_REGIONS_CONFIG_PATH_PREFIX . $country;

            $countryInfo = $this->countryInformationAcquirer->getCountryInfo($country);
            $regionOptions = $this->getRegionsForCountry($countryInfo);

            // Use type multiselect if fixed list of regions; otherwise, use textarea.
            $configType = !empty($regionOptions) ? 'multiselect' : 'textarea';

            $dynamicConfigFields = [];
            switch($configType) {
                case 'multiselect':
                    $dynamicConfigFields[$configId] = [
                        'id' => $configId,
                        'type' => 'multiselect',
                        'sortOrder' => ($index * 10), // Generate unique and deterministic sortOrder values
                        'showInDefault' => '1',       // In this case, only show fields at default scope
                        'showInWebsite' => '0',
                        'showInStore' => '0',
                        'label' => __('Allowed Regions: %1', $countryInfo->getFullNameEnglish()),
                        'options' => [                // Since this is a multiselect, generate options dynamically.
                            'option' => $this->getRegionsForCountry($countryInfo)
                        ],
                        'comment' => __(
                            'Select allowed regions for %1.',
                            $countryInfo->getFullNameEnglish()
                        ),
                        '_elementType' => 'field',
                        'path' => implode(            // Compute group path from section ID and dynamic group ID
                            '/',
                            [
                                self::CONFIG_GENERAL_SECTION_ID,
                                ConfigHelper::ALLOWED_REGIONS_SECTION_CONFIG_PATH_PREFIX . $country
                            ]
                        )
                    ];
                    break;
                case 'textarea':
                    $dynamicConfigFields[$configId] = [
                        'id' => $configId,
                        'type' => 'textarea',
                        'sortOrder' => ($index * 10), // Generate unique and deterministic sortOrder values
                        'showInDefault' => '1',       // In this case, only show fields at default scope
                        'showInWebsite' => '0',
                        'showInStore' => '0',
                        'label' => __('Allowed Regions: %1', $countryInfo->getFullNameEnglish()),
                        'comment' => __(
                            'Enter allowed regions for %1, one per line.',
                            $countryInfo->getFullNameEnglish()
                        ),
                        '_elementType' => 'field',
                        'path' => implode(            // Compute group path from section ID and dynamic group ID
                            '/',
                            [
                                self::CONFIG_GENERAL_SECTION_ID,
                                ConfigHelper::ALLOWED_REGIONS_SECTION_CONFIG_PATH_PREFIX . $country
                            ]
                        )
                    ];
                    break;
            }

            $dynamicConfigGroups[$country] = [    // Declare group information
                'id' => $country,                   // Use dynamic group ID
                'label' => __(
                    '%1 Allowed Regions',
                    $countryInfo->getFullNameEnglish()
                ),
                'showInDefault' => '1',             // Show in default scope
                'showInWebsite' => '0',             // Don't show in website scope
                'showInStore' => '0',               // Don't show in store scope
                'sortOrder' => ($index * 10),       // Generate unique and deterministic sortOrder values
                'children' => $dynamicConfigFields  // Use dynamic fields generated above
            ];
        }

        return $dynamicConfigGroups;
    }

    /**
     * Add dynamic region config groups for each country configured
     *
     * @param OriginalSection $subject
     * @param callable $proceed
     * @param array $data
     * @param $scope
     * @return mixed
     */
    public function aroundSetData(OriginalSection $subject, callable $proceed, array $data, $scope) {
        // This method runs for every section.
        // Add a condition to check for the one to which we're
        // interested in adding groups.
        if($data['id'] == self::CONFIG_GENERAL_SECTION_ID) {
            $dynamicGroups = $this->getDynamicConfigGroups();

            if(!empty($dynamicGroups)) {
                $data['children'] += $dynamicGroups;
            }
        }

        return $proceed($data, $scope);
    }
}

Results

Similar to dynamic fields, after this plugin is implemented a dynamic group is shown in the General -> General tab, one for each selected country. (Click for full page screenshot.)

Screenshot of Dynamic Groups in Admin UI

Additionally, values of fields in dynamic groups are correctly saved to core_config_data.

core_config_data dynamic groups screenshot

Where to Go From Here

Retrieving Values

Retrieving values of dynamic fields or fields of dynamic groups is the same as getting any other store config field, except that the path is computed. Below is an example of a config helper to look up the values from this example.

<?php

// EW/DynamicConfigFields/Helper/Config.php

namespace EWDynamicConfigFieldsHelper;

use MagentoFrameworkAppConfigScopeConfigInterface;
use MagentoFrameworkAppHelperAbstractHelper;

class Config extends AbstractHelper
{
    const ALLOWED_REGIONS_GROUP_PATH_PREFIX = 'general/region';
    const ALLOWED_REGIONS_CONFIG_PATH_PREFIX = 'regions-allowed-';

    const ALLOWED_REGIONS_TAB_ID = 'general';
    const ALLOWED_REGIONS_SECTION_CONFIG_PATH_PREFIX = 'allowed-states-section-';

    /**
     * Get configured allowed regions from dynamic fields by country code
     *
     * @param string $countryCode
     * @param string $scopeType
     * @param null $scopeCode
     * @return array
     */
    public function getAllowedRegionsByDynamicField(
        string $countryCode,
        $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT,
        $scopeCode = null
    ) : array {
        $configPath = implode(
            '/',
            [
                self::ALLOWED_REGIONS_GROUP_PATH_PREFIX,
                self::ALLOWED_REGIONS_CONFIG_PATH_PREFIX . $countryCode
            ]
        );

        $rawValue = $this->scopeConfig->getValue($configPath, $scopeType, $scopeCode);

        // Split on either comma or newline to accommodate both multiselect
        // and textarea field types.
        $parsedValues = preg_split('/[,n]/', $rawValue);

        return $parsedValues;
    }

    /**
     * Get configured allowed regions from fields in dynamic groups
     *
     * @param string $countryCode
     * @param string $scopeType
     * @param null $scopeCode
     * @return array
     */
    public function getAllowedRegionsByDynamicGroup(
        string $countryCode,
        $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT,
        $scopeCode = null
    ) : array {
        $configPath = implode(
            '/',
            [
                self::ALLOWED_REGIONS_TAB_ID,
                self::ALLOWED_REGIONS_SECTION_CONFIG_PATH_PREFIX . $countryCode,
                self::ALLOWED_REGIONS_CONFIG_PATH_PREFIX . $countryCode
            ]
        );

        $rawValue = $this->scopeConfig->getValue($configPath, $scopeType, $scopeCode);

        // Split on either comma or newline to accommodate both multiselect
        // and textarea field types.
        $parsedValues = preg_split('/[,n]/', $rawValue);

        return $parsedValues;
    }
}

Dynamic Sections and Tabs

Similar to fields and groups, it’s possible to create an around plugin on the setData() method of MagentoConfigModelConfigStructureElementTab and add children one level higher than a group. These dynamic sections will show in the admin as expected. However, clicking on them redirects back to one of the hard-coded sections. Since each section has its own URL, there are probably additional routing concerns for dynamic sections.

Example Module

A complete module demonstrating these code examples is available here: https://github.com/ericthehacker/example-dynamicconfigfields. Use it wisely.

Be aware of the following module notes.

  • The module implements example dynamic fields and groups, as well as methods to retrieve their values. Actually using values to restrict available regions (as expressed in the example use case), however, is left as an exercise for the reader.
  • There is duplicated code between the dynamic fields and groups plugins. This is intentional to ensure that each example plugin is easy to read.

How to Change Magento 2 Increment ID for Orders, Invoices, Credit Memos and Shipments

Some merchants want to customize order numbers or invoice numbers to be different than what Magento 2 produces by default. They might want the numbers to contain more information, or they might have an existing format that shouldn’t be changed. Perhaps the numbers need to pick up where they left off from a previous website (or maybe they just don’t look pretty enough). These numbers each have an Increment ID, and the values used to create them are stored in the database and are not configurable from admin, so we’ll need a little SQL to make our customizations. Without further ado…

We can individually adjust the following properties of increment IDs for Orders, Invoices, Credit memos, & Shipments:

  • Prefix
  • Suffix
  • Step
  • Start-value
  • Pad-length

The Prefix, Suffix, Start-value, and Step are stored in the database, while the Pad-length is set in the code. Before changing them, let’s see how they’re all used to generate increments IDs:

Formula

In Magento 2, the method for determining the increment ID is in MagentoSalesSequenceModelSequence.
The pattern is set as: a string, plus a 9-digit number (padded with 0s), plus another string.
The getCurrentValue() method returns a new increment ID according to the pattern as: The Prefix, plus the return value of the calculateCurrentValue() method, plus the Suffix. The calculateCurrentValue() returns the main number:

return ($this->lastIncrementId - $this->meta->getActiveProfile()->getStartValue()) * $this->meta->getActiveProfile()->getStep() + $this->meta->getActiveProfile()->getStartValue();

For the sake of demonstration, we’ll work with the increment ID for orders, so $this->lastIncrementId is the last value in the sequence_value column of the sequence_order_1 table in the database. (If we were working with the invoice increment ID, $this->lastIncrementId would come from the sequence_invoice_1 table.) The 1 in the table name is the store ID for the first store view. If you have another store view, you would use the sequence_order_n table (where n is your store ID).
So, calculateCurrentValue() subtracts start_value from the last sequence_value, multiplies by step, and then adds start_value. The result is added between the Prefix and Suffix.
We can express the whole method as a mathematical formula:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix

sequence_value starts at 1 and always increases by 1 when a new order is created [or invoice, etc].
Initially, start_value, and step are each 1, and prefix and suffix are undefined.
If we plug these values into our formula, we can predict the initial order increment ID:

increment_id = '' + ((11) * 1 + 1) {padded to 9 digits} + ''
increment ID = ((1 – 1) * 1 + 1) {padded to 9 digits}
increment ID = 1 {padded to 9 digits}
increment ID = 000000001

(This is consistent with the first order increment ID shown in sales_order.increment_id in the database: 000000001.)

Prefix/Suffix

The Prefix and Suffix are simple. They prepend and append the increment-ID number with the values stored in sales_sequence_profile.prefix & sales_sequence_profile.suffix, respectively. Changing the Prefix can be an easy way to lengthen the increment ID or make it start with something besides 0. The Suffix could be used to add 0s, to make the number appear to increase by 10 or 100 each time (as an example). Alternatively, we could use these values to stylize the increment ID or make it proprietary, so in this case, let’s just add a Prefix of “CL-” and use “-M2” for our Suffix.
(When we create the next order, sequence_order_1.sequence_value increases from 1 to 2, but start_value and step are still each 1.)

Plugging it into our formula:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix
increment ID = ‘CL-‘ + ((2 – 1) * 1 + 1) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 2 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000000002-M2’

(When we create this order, the last row of sales_order.increment_id should be consistent with our calculation.)

Step

The Step is stored in sales_sequence_profile.step. It is 1 by default and should not be less than 1.
We can use it to increase our order increment-ID number by a certain amount each time a new order is created (or “step” it up). For example, because step is 1 by default, our last increment-ID number “stepped” up by 1 from 000000001 to 000000002. However, when we change the step, the increment ID will “shuffle” one time before it follows the new pattern.
To demonstrate, let’s set the step to 100. (When we create the next order, sequence_order_1.sequence_value increases from 2 to 3, but start_value is still 1.)

Plugging it in:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix
increment ID = ‘CL-‘ + ((3 – 1) * 100 + 1) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 201 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000000201-M2’

As you can see, the increment-ID number did not “step” up by 100 from the previous value (000000002). This is because it shuffles on the first change. However, the next increment ID should follow the new pattern. (The next order increases sequence_value from 3 to 4.)

increment ID = ‘CL-‘ + ((4 – 1) * 100 + 1) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 301 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000000301-M2’

This time, the increment-ID number “stepped” up by 100 from the previous value, as we hoped (000000201 to 000000301).

Start-value

The Start-value is stored in sales_sequence_profile.start_value. It is 1 by default and cannot be less than 0. (Also, it should not be both greater than the last sequence_value and less than step, because that would make the increment-ID number negative.)
The Start-value is somewhat unintuitively named, as it reduces the amount by which step is multiplied, and then is added back to the increment-ID number. For example, because start_value is 1 by default, our last two increment-ID numbers (000000201 and 000000301) effectively have a 1 added to a multiple of the step value (which is 100).
When we change the Start-value, the increment ID will “shift” one time before it follows the pattern again. To demonstrate, we’ll use a Start-value of 3. (When we create the next order, sequence_order_1.sequence_value increases from 4 to 5, and step is still 100.)

When we plug it in:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix
increment ID = ‘CL-‘ + ((5 – 3) * 100 + 3) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 203 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000000203-M2’

As you can see, the amount by which step was being multiplied got reduced, and the amount added to the end of the increment-ID number increased (instead of “stepping” from 000000301 to 000000401, it “shifts” to 000000203).

The next increment ID should “step” up as usual. (The next order increases sequence_value from 5 to 6.)

increment ID = ‘CL-‘ + ((6 – 3) * 100 + 3) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 303 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000000303-M2’

This time, the increment-ID number “stepped” up as expected, and the Start-value of 3 was added to the end.
(If this change seems like an unusual way to set your increment IDs, don’t worry about it. The intention was just to demonstrate how start_value affects the pattern.)

Minimum increment-ID number

If we want to set a “starting” number to make it seem like the increment ID was originally higher than 1 and counted up from there (like numbering your first bank check 1000), we won’t actually use the start_value property. (However, because it has that name, it seemed helpful to address the matter under this section.)
To set a minimum number for our increment IDs, we need to add a value to sequence_order_1.sequence_value.
To demonstrate, let’s add a sequence_value of 1006 and put step and start_value back to 1. (When we create the next order, sequence_order_1.sequence_value increases from 1006 to 1007, and step and start_value are now 1 again.)

According to the formula:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix
increment ID = ‘CL-‘ + ((1007 – 1) * 1 + 1) {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-‘ + 1007 {padded to 9 digits} + ‘-M2’
increment ID = ‘CL-000001007-M2’

Now it’s as if the first order increment ID was 1000 and we “stepped” up from there.

Note: The sequence_order tables have an AUTO_INCREMENT value, so the above method only works if the inserted sequence_value is higher than the previous. If you’ve tried to shorten the order ID this way unsuccessfully, you should check the AUTO_INCREMENT value for the related sequence table:

SHOW CREATE TABLE sequence_order_1;

If the AUTO_INCREMENT value in the returned query is higher than the sequence_value number you’re wanting to change to, you’ll need to run a query like this:

ALTER TABLE sequence_order_1 AUTO_INCREMENT = 1006;

Pad-length

The pad length of the increment-ID number is determined in the code base, and it’s not affected by any of the database properties.
In MagentoSalesSequenceModelSequence, the getCurrentValue() method uses a formatted-string-print function to set the pattern of the increment-ID number:

return sprintf(
    $this->pattern,
    $this->meta->getActiveProfile()->getPrefix(),
    $this->calculateCurrentValue(),
    $this->meta->getActiveProfile()->getSuffix()
);

“$this->pattern” comes from the constant DEFAULT_PATTERN, which is initially: %s%'.09d%s.
The increment-ID number is determined by the middle section: %'.09d. The '.0 sets “0” as the padding character and sets the number of digits to display as the value that follows, which in this case is 9. The d presents the number as a [signed] decimal. This means that by default, the increment-ID number will be a signed decimal with 9 digits, padded with 0s. To demonstrate, we’ll set the pad-length to 6. (When we create the next order, sequence_order_1.sequence_value increases from 1007 to 1008, while start_value and step are still 1.)

Using our formula:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix
increment ID = ‘CL-‘ + ((1008 – 1) * 1 + 1) {padded to 6 digits} + ‘-M2’
increment ID = ‘CL-‘ + 1008 {padded to 6 digits} + ‘-M2’
increment ID = ‘CL-001008-M2’

As you can see, the increment-ID number is only 6 digits long now, instead of 9.

Make it happen

In the database, the sales_sequence_profile table sets the pattern for the increment ID on each entity type (order, invoice, creditmemo, and shipment) at each store view. We need to make our changes for store view 1, which is set on rows 5–8. (These rows set the 4 entity types respectively.) For the sake of demonstration, we’ll work with the order increment ID, so we’ll be changing row 5 of sales_sequence_profile (meta_id = 5).

Here’s what sales_sequence_profile looks like by default:

sales_sequence_profile: Default state

The following are the SQL queries (plus the line of code) to set each property to the value used in our examples. (Because we worked with the increment ID for Orders, the meta_id in each query below is set to 5, but you could also use 6, 7, or 8, to change the increment IDs for Invoices, Creditmemos, and Shipments, respectively.)

Prefix:
UPDATE `sales_sequence_profile` SET `prefix` = 'CL-' WHERE `meta_id` = 5;
Suffix:
UPDATE `sales_sequence_profile` SET `suffix` = '-M2' WHERE `meta_id` = 5;
Step:
UPDATE `sales_sequence_profile` SET `step` = 100 WHERE `meta_id` = 5;
Start-value:
UPDATE `sales_sequence_profile` SET `start_value` = 3 WHERE `meta_id` = 5;
Minimum increment-ID number:
INSERT INTO `sequence_order_1` (`sequence_value`) VALUES ('1000');
Pad-length:

The constant DEFAULT_PATTERN is set in: /vendor/magento/module-sales-sequence/Model/Sequence.php, on line 19.
We can change this in a custom module by creating etc/di.xml with the following contents:



    
        
            %s%'.06d%s
        
    

Conclusion

You can now have full control over your increment IDs. Although they sometimes seem to change unexpectedly when you adjust certain values, the formula should help you predict the pattern:

increment_id = prefix + ((sequence_valuestart_value) * step + start_value) {padded to X digits} + suffix

Happy incrementing!

Google Announces Mobile-First Indexing

We all knew it was coming, it was only a matter of time before Google officially announced mobile-first indexing. This means that Google will first look at the mobile version of a website for its various ranking markers. If a site doesn’t have a mobile version, then it will look at the site’s desktop version.

Why is this change needed?

The change comes from Google hearing feedback for quite some time. From a user’s perspective, it will make mobile searches much more efficient and useful. Google gets more searches from mobile devices than desktop on a daily basis, but the ranking system only looks at the desktop version of content to decide if it is relevant to the user’s search. If the mobile version of the site has less content than the desktop page, then the mobile user may not get the information they are looking for. Currently, while Google searches are mobile the actual index is based upon desktops, and this change will align indexing with many user’s primary search devices.

Why it matters to you

Based off of the information we’ve gotten from Google thus far it appears the SEO landscape will change drastically. However, we don’t know to what extent at this point. Google searches are highly dependent on content and links. Given that mobile versions often have less content and fewer links, we can safely assume SEO changes are coming.  For now, there will be two indexing types: desktop and mobile. Eventually, however, there will only be one: mobile. So what we know for sure is that Google will start to build new factors when evaluating relevance and ranking authority.

What do you need to do?

You can use these tips from Google to increase your mobile rankings:

  • If you have a site configuration where the primary content and markup is different across mobile and desktop, you should consider making some changes to your site.
    • Make sure to serve structured markup for both the desktop and mobile version.
    • Sites can verify the equivalence of their structured markup across desktop and mobile by typing the URLs of both versions into the Structured Data Testing Tool and comparing the output.
    • When adding structured data to a mobile site, avoid adding large amounts of markup that isn’t relevant to the specific information content of each document.
    • Use the robots.txt testing tool to verify that your mobile version is accessible to Googlebot.
    • Sites do not have to make changes to their canonical links; we’ll continue to use these links as guides to serve the appropriate results to a user searching on desktop or mobile.
  • If you are a site owner who has only verified their desktop site in Search Console, please add and verify your mobile version.
  • If you only have a desktop site, we’ll continue to index your desktop site just fine, even if we’re using a mobile user agent to view your site.
  • If you are building a mobile version of your site, keep in mind that a functional desktop-oriented site can be better than a broken or incomplete mobile version of the site. It’s better for you to build up your mobile site and launch it when ready.

What is the timeline for this?

It is still in testing mode, but this change will be rolled out soon, though a set date hasn’t been specified by Google. They have stated that it will be slow, so you will not see a drastic drop/raise in your rankings at this time.

Have questions, or need assistance making the changes listed above? Contact us, we’d love to help!

How to Override a Template in Magento 2

One of the most common tasks for a Magento 2 developer is overriding a template. Magento 2 makes this easy in most cases, but there are some cases that will leave you wondering if it’s even possible. This article will cover which methods for overriding a template are appropriate for a variety of cases and when you should consider pursuing an alternative to a template override.

In Magento 2, there are two main methods for overriding a template:

  • Theme file path
  • Layout Block Argument

The template path method is used when building a theme and the layout method is used when building a module. They are the simplest, most straightforward, and easiest to understand methods. You will almost always be using one of these two methods. For the very few times, these two methods are not available, you have two other methods to choose from:

  • Class preference
  • Plugin

If you would like to avoid confusion or information overload, it is acceptable to skip the other two options, then come back in the future when you run into a template that you cannot override with the two conventional methods.

Theme File Path

In Magento 2, themes can override any module’s or parent theme’s layout, template, or web (css, js, etc.) file simply by placing it in <theme_dir>/<Vendor>_<Module>/path/to/file. For example, If you want to override the template located at <theme_dir>/<Vendor>_<Module>/view/html/header.phtml for the Magento_Theme module, you would place your template in <theme_dir>/Magento_Theme/templates/html/header.phtml.

There are several block definitions in Magento 2 that do not have the Vendor_Module prefix to specify which module the template belongs to. In these cases, the block’s class attribute will define what module the template belongs to. For example if you were to find a block definition in the Magento_Checkout module like this you would place your template inside the Magento_Checkout module directory inside your theme.

This Magento 2 devdoc on theme-inheritance goes into more detail on how to override a template in a theme.

Layout Block Argument

The layout method should be used when building a module. To override a template using layout XML, you only need to override the block’s template argument. Using the template Magento_Wishlist/view/frontend/templates/view.phtml as an example, to override view.phtml with your own template, you first have to create a new layout file. <Vendor>_<Module>/view/frontend/layout/wishlist_index_index.xml

There are currently two methods of overriding a block argument.

(New method)

wishlist_index_index.xml



    
        
            
                Vendor_Module::view.phtml
            
        
    

(Old deprecated method)

wishlist_index_index.xml



    
        
            
                Vendor_Module::view.phtml
            
        
    

The devdocs say that the new method is the appropriate method to override a template with layout XML but according to issue #3356 on the Magento 2 GitHub repo, there are a number of cases where the new method won’t work. In these cases, it is okay to use the old deprecated method until the issue is resolved.

Now you have to place your new custom template in the location you specified in your layout file. For this example, that is <Vendor>_<Module>/view/frontend/templates/view.phtml

The path of the template in a module does not matter so long as it matches the path you set in your template attribute. I like to put the template in the same path you found it in its original module starting from the module’s templates directory but add a directory for the module name of the template you’re overriding. For example, in your module, you would put a wishlist template in <Vendor>_<Module>/view/frontend/templates/wishlist/view.phtml or for a catalog template it would be <Vendor>_<Module>/view/frontend/template/catalog/view.phtml.

There is one additional step for your override to take effect. You must add a sequence to your module.xml file for the module containing the layout file you are modifying. For this example, your etc/module.xml file will look like this.

module.xml



    
        
            
        
    

This will ensure that the Magento_Wishlist module will be loaded and added to the merged layout file before your module. This is necessary as it ensures your module’s layout override will be parsed after the layout XML it is referencing has been parsed. Otherwise, your layout could be referencing something that doesn’t exist yet, so your layout override will not apply.

Class Preference

You may have found that there are some block definitions that contain a template attribute but do not contain a name attribute. This means you can’t override the block from layout XML like you normally would in a module. If the block’s template attribute does not contain the Vendor_Module prefix you can utilize the class preference method to override the template.

In this example, we will be overriding the block containing the cart/item/default.phtml template in found in Magento/Checkout/view/frontend/layout/checkout_cart_item_renderers.xml.


The class attribute on the block sets the scope for the template path so a Vendor_Module prefix is unnecessary if the template is in the same module as the block’s class. This means that all it takes to change the template’s module scope is to change the block’s class with a class preference in your di.xml file like this.

di.xml



    

Now you have to make sure that block actually exists even though you don’t actually need to do anything with it. To do this you can just create a skeleton block.

Renderer.php


To complete the template override you just need to add your template like you would if you were doing doing a normal template override from layout. In this case, that path will be Vendor/Module/view/frontend/templates/cart/item/default.phtml

At this point, it should be obvious how big of a hack this is. It is only prudent in an extremely small number of cases. If you think you might have such a case, consider if there are any alternatives like the ones I will list later in this article. Consider the ramifications for other modules that may want to do something similar to what you are doing. Hopefully, this issue will eventually be fixed and this sort of hack will no longer be necessary.

Plugin

The class preference is an acceptable option when you are wanting to override the template for all instances of a class and the Vendor_Namespace prefix is missing from the template attribute on block definition. However, there may be times when you need to be more targeted. The class may be used for multiple templates or the Vendor_Module prefix might be set. In these cases, a plugin is the best option. For this example we will override Magento_Catalog::category/products.phtml with our own template using a plugin. I won't be going into great detail on plugins so if you aren't already familiar with them, please read through the Magento 2 devdoc on plugins before continuing.

The original block definition for the category view template looks like this


So our plugin will need to hook into the class MagentoCatalogBlockCategoryView. The toHtml() method, inherited by MagentoFrameworkViewElementTemplate, retrieves the template and turns it into html. To override the template that gets retrieved by that method, the $_template variable needs to be changed to your own template. Fortunately, the Template class already has a method to do that. With this understanding, we can create our plugin and di.xml files.

di.xml



    
        
    

View.php

setTemplate('Vendor_>Module::catalog/category/products.phtml');
        }
    }
}

The plugin could be set on a few other methods but I chose toHtml because it is the method in which the template is first used. This method of overriding a template is not recommended except when necessary. It should only be used if the block does not have a name, has a Vendor_Module prefix and/or has a class that handles multiple templates. To complete the template override just place your template in your module in the appropriate location,<Vendor>/<Module>/view/frontend/templates/catalog/category/products.phtml

Which method should you use?

There is no one particular method that should be used in all scenarios. If you are building a theme, use the template path method. If you are building a module, use the layout method. For developers trying to figure out where a core Magento template has been overridden, these are the two places they will look. If you are building a module and you encounter a block that cannot be referenced by name, you can use one of the two unconventional methods to override that block's template.

Alternatives

To modify something on a page, overriding a template is not always the best option. Another module could override your override in any of the four methods, the name on a block could change with an update, or the class assigned to a block could change. Of course in many situations, it is the best, if not the only option. However, there are some occasions when it would be better to go another route.

The layout structure allows for inserting child blocks

Many times, the only change required to a template is an addition to the beginning or end of that template. In these cases, if the block defining the template you want to override is nested inside a container, you can simply create your own block and place it before or after the block you previously wanted to override. One of the distinguishing characteristics between blocks and containers is that children of blocks must be explicitly called in order to be rendered while children of containers will render all of their children blocks and containers. So all that is required to add your new markup to the wishlist page is to create your own layout/wishlist_index_index.xml file, like this:



    
        
            
        
    

You only need to remove an element

This isn't recommended but for cases where you really want to avoid overriding a template to remove an element, you can remove the element with JS or CSS. It's easy to think this is a good idea in terms of ease of implementation but it usually is not the best solution. If used too frequently, you can have large chunks of HTML that get loaded but aren't displayed to the user. This can increase page load times and lead to messy, hard to maintain HTML and CSS. There is really no clear line for what is "too frequent" so it is best to avoid this option unless absolutely necessary.

You need to replace a jQuery widget with your own

Overriding JS in Magento 2 doesn't always require overriding the template where it gets initialized. You can override functions and objects on a jQuery UI widget much like you would override a variable or method on a PHP class you are extending. To do this, first create your JS file in your theme or module:

  • Theme: <Vendor>/<Theme>/web/js/customAccordion.js
  • Module: <Vendor>/<Module>/view/frontend/web/js/customAccordion.js

customAccordion.js

define([
  'jquery',
  'jquery/ui',
  'mage/accordion'
], function($){
  $.widget('Vendor_Module.customAccordion', $.mage.accordion, {
      _create: function() {
          // custom code here
      }
  });

  return $.Vendor_Module.customAccordion;
});

Now create a mapping for your customAccordion widget:

  • Theme: <Vendor>/<Theme>/requirejs-config.js
  • Module: <Vendor>/<Module>/view/frontend/requirejs-config.js

requirejs-config.js

var config = {
    map: {
        '*': {
            'accordion': 'Vendor_Module/js/customAccordion'
        }
    }
};

Now, anywhere accordion is included or initialized, your custom accordion will be loaded. For more information. Check out this Magento 2 devdoc: Customize a default Magento jQuery widget

You want to change a line of text

Magento 2 has a very easy method of overriding strings of text. You can do this by adding a translation to your theme or module that matches the line of text you want to change and replaces it with any string you set. This approach isn't considered a best practice, but sometimes it is worth the tradeoff as opposed to having to override a large template to make a simple text change. To learn more about this, read this guide on the Magento 2 devdocs: Use translation dictionary to customize strings. You need to be careful when using this method in case a string you're translating is used in multiple locations.

Wrapping up

You've learned the good, the bad, and the ugly of Magento 2 template overrides. The two main methods of overriding a template are "good", but not always necessary. The two "bad" methods of overriding a template are almost never necessary and even then should be considered a hack that needs to be replaced when it becomes possible. Template overrides are a quick and easy way to make changes in Magento 2 they can be easily abused. When you have the option, consider the alternatives to overriding a template.

What Magento Merchants need to know about sales tax in 2017

Where are we headed with remote sales tax legislation? Repost from Avalara

If tax-free online shopping ends in 2017, what are the implications to your business? Classy Llama and Avalara want to make sure cart compliance makes your list for year-end planning meetings.

This blog post will give you a quick snapshot of information concerning the Sales Simplification Act and what it means for your business. Our free eBook: The Online Sales Tax Showdown provides a more detailed look at the sales tax situation for online retailers today, the proposals in play, and their likely impact for retailers of all types and sizes next year.

The issue of remote sales tax remains a divisive one among federal lawmakers. The 2015 Online Sales Simplification Act (OSSA) draft never became a bill, and OSSA 2016 exists in draft form only. OSSA could go the way of the Marketplace Fairness Act and the Remote Transactions Parity Act (thus far, nowhere). Or it could drum up support for the No Regulation without Representation Act of 2016, an anti-remote sales tax bill that was introduced in July.

Yet change in remote sales tax policy doesn’t necessarily depend on federal legislators. Numerous states have enacted affiliate and click-through nexus laws, under which certain remote retailers trigger a sales and use tax obligation through affiliate relationships and/or links on websites owned by state residents. In addition, new economic nexus laws in Alabama and South Dakota are challenging the physical requirement precedent upheld by the 1992 United States Supreme Court decision in Quill Corp. v. North Dakota. Both states are being sued over their policies and both hope to take their cases to Supreme Court, which they hope will overturn Quill and grant states the authority to tax certain remote sales.

Instead of challenging or expanding the definition of nexus, Colorado and Vermont have imposed use tax reporting requirements on remote retailers to attempt to increase use tax compliance. States are entitled to tax revenue from taxable remote sales — when sales tax isn’t collected by a retailer, consumers are supposed to remit use tax to the state. Yet individual use tax compliance is low, and enforcement is difficult. It remains to be seen whether the policies in Colorado and Vermont will be effective.

In short, a majority of states want remote sales tax revenue, and lawmakers will continue to push for it in state capitols and on Capitol Hill. There is no guarantee their efforts will be successful; if they are, what remote sales tax compliance will look like from state to state is unknown.

For a more in-depth look at this topic, download a copy of The Online Sales Tax Showdown, a special report by Internet Retailer with support from Avalara that looks at what federal sales tax legislation could mean for US retailers and other sellers.

GET MORE INFO

User Acceptance Testing a Magento 2 Site

During our website builds, some of our clients have asked about our quality assurance process. We commonly get questions like “What do you check during testing?” and “What should our testing include?” We can give answers to the first question during the course of the project. In response to the second question, we recently wrote a document that provides an initial set of answers. A primary factor that influenced the direction of the document is that we’ve found that a merchant tends to focus on the features of a site that are unique to them, while overlooking aspects that are standard or only lightly customized. And having a published procedure which we can give to clients means they can get started sooner with their testing because they have a roadmap to follow. A benefit of an earlier start on user acceptance testing is more available time for changes and bug fixes prior to launching.

The first version of the procedure guides a merchant through testing a “happy path” through the site. A “happy path” is a path through a system or site where given normal, expected inputs, everything works as expected without errors or exceptions. Using a “happy path” approach doesn’t ensure that a site is bug-free since things do go wrong in the real world. Our internal testing process is much more comprehensive and detailed, and hopefully, we have delivered a nearly defect-free site to the client. Our intention with the procedure is to achieve two objectives during user acceptance testing:

  1. To prompt user acceptance testing when there’s enough time to act on the client’s feedback without delaying the launch.
  2. To ensure that the client sees what most of the site looks like, and experiences how the key features of the site work.

The Scenario

The scenario we chose to emulate with the test is that of a first-time guest user of the site. We thought about the actions that a first-time visitor would most likely take, and created a diagram depicting what that user might do.

Note that the above diagram isn’t a rigorously correct process flow diagram. It was sufficient for the purpose of guiding the writing of the test procedure. And it probably includes more actions than a typical first-time guest user would actually do. It can be thought of as a “happy path with detours”. We wanted to cover those behaviors that were highly likely to be done by first-time visitors, although any single user is unlikely to do all of them.

Test Coverage

The procedure covers the following aspects of testing the store as a first-time guest user.

  • Testing with both desktop and mobile devices.
  • Visiting the homepage.
  • Adding a product to the cart from a “featured products” or “hot sellers” block.
  • Navigating to a category page and adding a product on it to the cart.
  • Searching for a product and adding a product to the cart from the search results.
  • Adding a product to the cart from a product details page, including at least one product with options.
  • Viewing the minicart and using its features.
  • Viewing the shopping cart and using its features.
  • Checking out.
  • Reviewing the order confirmation.
  • Opening an account.
  • Using account page features.
  • Viewing the 404 page.

The Document

The document includes step by step instructions and many screenshots so that it is usable by testers who are new to Magento-based sites. It is available as a PDF file.

Classy Llama’s clients will be provided with the document at the appropriate point in their project and may also obtain it by contacting their project manager and requesting a copy of User Acceptance Testing a Magento 2 Site.

Utilizing XSLT with Xtento’s Order Export Magento Module

Overview Example

When working with an ERP integration or another system that requires Magento data to be in a very specific format there are a couple of options. You could write a custom module to handle converting the Magento data directly into the format that the end product requires, or you could use an already built module for it.

Recently I found myself in a situation where a client had a custom ERP integration that needed re-formatted data. Additionally, the format was quite a bit outside the format of the Magento data. For a situation like this, I turn to using XTENTO Order Export as it comes with an XSLT option.

This will only be covering the XSLT portion of this. XTENTO has excellent documentation on how to utilize their product.

XSLT in it’s simplest form is a language to convert XML into other formats such as HTML or XML formatted differently.

If you have some XML structured like the following:

<?xml version="1.0" encoding="UTF-8"?>
		<buster>
			<first_name>Jamie</first_name>
			<last_name>Hyneman</last_name>
			<pet_name>Zero</pet_name>
		</buster>
		<buster>
			<first_name>Karie</first_name>
			<last_name>Byron</last_name>
			<pet_name>Gertie</pet_name>
		</buster>

Maybe you just want to get the pet names from this document. The following XSLT code would accomplish that goal.

 <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="1.0" xmlns_xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/">
        <ul>
            <xsl:for-each select="buster">
                <li>
                    <xsl:value-of select="pet_name" />
                </li>   
            </xsl:for-each>
        </ul>   
    </xsl:template>
    </xsl:stylesheet>

The output from the XSLT above is below, considering the size of the data sample we could have done this manually and it would not have been an overwhelming amount of work.

         Zero
        Gertie

Now that we have a working knowledge of what XSLT is used for, as well as an idea of what kind of results we can achieve, let us move onto something a bit more useful. We will be converting a Magento order XML from Cute Cats Inc. into an XML format that their ERP is expecting.

This is business critical as people need their cute cat pictures to go out, so the faster the better from the client perspective.

For the purposes of the following examples, we will be working with an order, of the following products:

 Product Name        Product Sku      Product Qty         Product Price
Cat, Awesome        150-sku-01          1                   $699.00
Cat, Cool           150-sku-02          1                   $555.00

The following is an example of the XML structure we will be working with. Magento stores its orders in a predictable fashion via XML. This example is not exact but it is close enough for the purposes of our examples.

     <!-- simplification of how magento stores order information in an xml -->
    <!-- Cute Cats Incorporated - We specialize in cuteness -->
    <orders>
        <order>
            <increment_id>100000034</increment_id>
            <order_item>
                <name>Cat, Awesome</name>
                <sku>150-sku-01</sku>
                <qty>1</qty>
                <price>699.000</qty>
            </order_item>
            <order_item>
                <name>Cat, Awesome</name>
                <sku>150-sku-01</sku>
                <qty>1</qty>
                <price>699.000</qty>
            </order_item>
        </order>
    </orders>

The format that we are trying to reach is like this example:

     <!-- example ERP format for orders -->
    <!-- Business as Usual LLC - ERP Division -->
    100000034,1 
    <!-- Order Id, number of order (if you had multiple orders this would increment) -->
    Cat Awesome,150-sku-01,1.0000,699.0000, <!-- Item Name, Sku, QTY, Price -->
    Cat Cool,150-sku-02,1.0000,555.0000, <!-- <!-- Item Name, Sku, QTY, Price -->

To accomplish this we will create an XSL template which will be used for all the orders from Magento. An XSLT template is defined in order to give the XML predefined locations to be output within the document.

The basic structure of an XSL Template is like this:

     <?xml version="1.0"?>
    <!-- the files tag is to hold every file we work with, its one in most cases with XTENTO -->
    <files>
        <!-- the file tag is for each individual file -->
        <!-- you can control the name of the file output here as seen in the filename attribute -->
    <file filename="%lastorderincrementid%.so">
    <!-- the xsl:stylesheet is where the actual xsl begins it is important to include the xmlns to php for XTENTO -->
    <xsl:stylesheet version="1.0" xmlns_xsl="http://www.w3.org/1999/XSL/Transform" xmlns_php="http://php.net/xsl">
        <!-- the output method text tells the xsl what kind of output it will be  -->
        <xsl:output method="text"/>
        <!-- constants are declared if you plan to use them for more than one thing I generally create one for separators -->
        <xsl:variable name="sepstart" select="'"'"/>
        <xsl:variable name="sepend" select="','"/>
        <!-- I also create one for an empty value if my csv will have one -->
        <xsl:variable name="emptyval" select="'~'" />
        <!-- xsl template match this tells it what character to match on for the template -->
        <xsl:template match="/">
            <!-- Template will go here -->
            <!-- This is where we will be doing most of the work in this blog -->        
        </xsl:template>
    </xsl:stylesheet>
    </file>
    </files>

We can define constant variables, these can hold field separators as I have done, or any value you want to remain constant, you can then refer to these throughout the XSLT template.

     <xsl:variable name="sepstart" select="'"'"/>
    <xsl:variable name="sepend" select="','"/>
    <xsl:variable name="emptyval" select="'~'" />

Using Simple Fields

I plan to examine three cases with this system that are useful, a simple example, an example using PHP native functionality, and a more complex solution that uses custom built static methods to compare data to an admin field. This multi-layered complexity allows you to change the data as needed to fit practically any requirements.

For the purpose of our simple example we are going to look at a case where we need a simple CSV output with: the sku of every item ordered, the quantity ordered, and the price.

Using XTENTO we will have some basic structure already in place allowing us to loop over order items, and gain access to the data contained within.

         <!-- Loop Example -->
    <xsl:for-each select="orders/order">
        <!-- loop over orders here -->
    </xsl:for-each>

Simple Fields – Example 1

First let’s look at how we work with pulling basic field data off of an xml node.

     <xsl:value-of select="sku"/>

This would return the following:

     150-sku-01,

This is not a lot of code, and it is fairly readable in that form as well. You will get the value of whatever node is selected (in this case, “SKU”).

So to put together some data using the looping we have looked at and how to pull data from nodes would look like:

     <xsl:for-each select="orders/order">
        <!-- order id -->
        <xsl:value-of select="increment_id"/>
        <xsl:value-of select="$sepend" />
        <xsl:text>
</xsl:text> <!-- this inserts a line break -->
        
        <xsl:for-each select="items/item">
            <!-- loop over order items -->

            <!-- sku -->
            <xsl:value-of select="sku"/>
            <xsl:value-of select="$sepend" />

            <!-- qty ordered -->
            <xsl:value-of select="qty_ordered"/>
            <xsl:value-of select="$sepend" />

            <!-- price -->
            <xsl:value-of select="price"/>
            <xsl:value-of select="$sepend" />
            <xsl:text>
</xsl:text>
    
        </xsl:for-each>
    </xsl:for-each>

Which would output the following:

     100000034, <!-- order id -->
    150-sku-01,1.0000,699.0000, <!-- sku, qty ordered, price -->
    150-sku-02,1.0000,555.0000, <!-- sku, qty ordered, price -->

With only a short template in XSLT, the order has already been changed from a long XML document into a short little piece of CSV data, with exactly the details desired.

PHP Functions – Example 2

Now for a more complex example, where we can call to some PHP native functionality. It is possible to use any PHP function directly from the XSL Template. In this case, I want to include the title or name of my products, but they include commas so that is an issue as it would break the CSV structure. There are a couple of ways to solve this, one will address the entire document as a whole and then this more focused approach.

First, let’s look at calling a PHP Function by itself:

 <xsl:value-of select="php:functionString('preg_replace','/[,]/', '',name)"/>

This allows us to call the PHP function “preg_replace”, pass in a regex to strip out commas, and then replace those. We pass in the field id that we are wanting to pass in as a parameter to it, much as we would when using the function within our regular use of PHP.

So if we were to plug that into the XSL template we already have from above it would look like:

 ...
<xsl:for-each select="items/item">
        <!-- loop over order items -->

        <!-- name -->
        <xsl:value-of select="php:functionString('preg_replace','/[,]/', '',name)"/>
        <xsl:value-of select="$sepend" />

        <!-- sku -->
        ...

This will output the following:

     100000034,
    Cat Awesome,150-sku-01,1.0000,699.0000,
    Cat Cool,150-sku-02,1.0000,555.0000,

As you can see we now have the names of those two very important products, “Cat Awesome” and “Cat Cool” which is the backbone of Cute Cats Inc.

Advanced Functionality – Example 3

Let’s say for the purposes of our example that our client Cute Cats Inc. wants to offer free shipping to wholesale customers, who have a specific wholesale_id, which they can set in the admin (once built in another module). These people are after all selling their most important products so let’s take special care of them. In order to accomplish this, we will need to create a static method to perform the task.

To do this first we have to create an XSL.php helper in a module following Magento best practices, like so:

     /**
    *
    * app/code/local/CLS/OrderExport/Helper/Xsl.php
    */
    class CLS_OrderExport_Helper_Xsl extends Xtento_OrderExport_Helper_Xsl
    {
        const XML_FREE_SHIPPING_WHOLESALE_ID = 'path/to/admin/config/field';
        
        /**
        * Return if free shipping is available based on the wholesale id in the admin
        * @param $orderId
        *
        * @return int
        */
        
        static function getIsFreeShippingAvailable($orderId)
        {
            $freeShippingWholesaleId =        
                 Mage::getStoreConfig(self::XML_FREE_SHIPPING_WHOLESALE_ID);
            /** @var  $order Mage_Sales_Model_Order*/
                $order = Mage::getModel('sales/order')->load($orderId);
                $customer =   
                 Mage::getModel('customer/customer')->load($order->getCustomerId());

                if ($customer->getWholesaleId() == $freeShippingWholesaleId) {
                return 1;
            }

                return 0;
        }   
        
    }

Then we need to call that function and pass in the entity_id from the order to the method so it can accomplish the logic work.

 ...
    <!-- free shipping 1 for yes, 0 for no -->
    <xsl:value-of select="php:functionString('CLS_OrderExport_Helper_Xsl::getIsFreeShippingAvailable',entity_id)"/>
    <xsl:value-of select="$sepend" />
    
    <xsl:text>
</xsl:text>
    
    <xsl:for-each select="items/item">
...

With that done we are now able to use the custom functionality giving us an output of:

     100000034,1
    Cat Awesome,150-sku-01,1.0000,699.0000,
    Cat Cool,150-sku-02,1.0000,555.0000,

This lines up exactly with what the ERP integration is expecting from our client. Not only have we accomplished the task, we did not have to manually edit any XML, or write more than one short method in PHP.

Summary

As you can see using the XTENTO module gives you a lot of speed to hit the ground running with, you can use it out of the box with a few lines of XSLT to format the text in any format required, and with a little extra engineering you can perform complex logic to make the output mimic what you need it to say. Below we have the entire XSLT template we have written all compiled into one entry.


 <?xml version="1.0"?>
    <!-- the files tag is to hold every file we work with, its one in most cases with XTENTO -->
    <files>
        <!-- the file tag is for each individual file -->
        <!-- you can control the name of the file output here as seen in the filename attribute -->
    <file filename="%lastorderincrementid%.so">
    <!-- the xsl:stylesheet is where the actual xsl begins it is important to include the xmlns to php for XTENTO -->
    <xsl:stylesheet version="1.0" xmlns_xsl="http://www.w3.org/1999/XSL/Transform" xmlns_php="http://php.net/xsl">
        <!-- the output method text tells the xsl what kind of output it will be I have never used anything other than text -->
        <xsl:output method="text"/>
        <!-- constants are declared if you plan to use them for more than one thing I generally create one for seperators -->
        <xsl:variable name="sepstart" select="'"'"/>
        <xsl:variable name="sepend" select="','"/>
        <!-- I also create one for an empty value if my csv will have one -->
        <xsl:variable name="emptyval" select="'~'" />
        <!-- xsl template match this tells it what character to match on for the template -->
        <xsl:template match="/">
            <!-- Template will go here -->
            <xsl:for-each select="orders/order">
        
                <!-- order id -->
                <xsl:value-of select="increment_id"/>
                <xsl:value-of select="$sepend" />
                
                <!-- free shipping 1 for yes, 0 for no -->
                <xsl:value-of select="php:functionString('CLS_OrderExport_Helper_Xsl::getIsFreeShippingAvailable',entity_id)"/>
                <xsl:value-of select="$sepend" />
                <xsl:text>
</xsl:text> <!-- this inserts a line break -->
        
                <xsl:for-each select="items/item">
                    <!-- loop over order items -->
                    
                    <!-- name -->
                    <xsl:value-of select="php:functionString('preg_replace','/[,]/', '',name)"/>
                    <xsl:value-of select="$sepend" />

                    <!-- sku -->
                    <xsl:value-of select="sku"/>
                    <xsl:value-of select="$sepend" />

                    <!-- qty ordered -->
                    <xsl:value-of select="qty_ordered"/>
                    <xsl:value-of select="$sepend" />

                    <!-- price -->
                    <xsl:value-of select="price"/>
                    <xsl:value-of select="$sepend" />
                    <xsl:text>
</xsl:text>
                </xsl:for-each>
    
            </xsl:for-each>
        </xsl:template>
    </xsl:stylesheet>
    </file>
    </files>

The XTENTO module links up seamlessly with the Magento interface, giving you a good place to drop in your XSL Template. It also brings with it some other functionality you can leverage to make use of Magento’s data.

Some of those other features that are of use within the XTENTO Export XSL are:

     <!-- static output -->
    <xsl:text>Whatever you want to output goes here.</xsl:text>
    
    <!-- mapping if/then/else -->
    <xsl:choose>
        <xsl:when test="shipping_method='abc'"><xsl:text>output this text</xsl:text></xsl:when>
        <xsl:when test="shipping_method='xyz'"><xsl:text>output something else</xsl:text></xsl:when>
        <xsl:otherwise><xsl:text>nothing matches, output this</xsl:text></xsl:otherwise>
    </xsl:choose>
    
    <!-- replace all characters in fields -->
    <file filename="file_name.csv" search=",;" replace="--"> 
    <!-- this is actually across all fields so much wider than the preg_replace example above which is only a single field-->

You can find purchase information at Xtento Order Export Module & their Documentation is also available.

Icon Fonts in Magento 2: A Foundation

Icons have a storied history on the web. In the bad old days, they were treated like any other images on a web page, implemented using individual image elements. This carried several negative implications, among them poor semantics and excessive HTTP requests as every icon image on a page were loaded separately. A much-improved method emerged with image replacement using CSS, pseudo-elements, and sprites. A CSS-based approach left HTML semantics undisturbed, and sprite images cut down on unnecessary asset requests with single large images combined with background positioning, instead of an image per icon.

Of course, even the CSS sprite technique still has the chief downfall that it’s image-based. This means that image editing software is required to create or edit sprites, whether or not you are creating your own icons or pulling them in from elsewhere. It also means that at the end of the day your icons are a flat set of pixels, subject to resolution limitations. (Note the Magento 1 responsive theme’s use of two distinct sizes of sprite image, in order to accommodate high pixel density displays like Retina devices.) An approach fast gaining wide adoption today, which cuts images out of the story entirely, is that of icon fonts. Magento 2’s native theme fully embraces this technique.

Using fonts for icons makes sense for a lot of reasons. In reality, iconography has always had more in common with text than content images; icons are glyphs that communicate meaning to the audience. And in technical terms, font glyphs are vector information that can scale up to any resolution. Of course, if you’re a web developer used to working with image-based icons, it’s likely that you have no small degree of discomfort with generating your own font files. Luckily, the growing adoption of icon fonts means there are great tools available for you. The native Magento theme provides the framework you need for implementation, and in this article, I’ll introduce you to a foundation that will leave you even better equipped for customizing Magento’s icons easily.

A Brief Overview of Magento’s Icon CSS

You’ll find the icon font files for the Magento Blank theme in lib/web/fonts/Blank-Theme-Icons (in the typical multiple file formats to support all user agents). lib/web/css/source/lib/variables/_typography.less defines the font icon path and name, and in the Magento Blank theme web/css/source/_icons.less uses these to define the icon font face itself, to be used in all CSS declarations.

The final relevant piece to be aware of is lib/web/css/source/lib/variables/_icons.less, where LESS variables are defined to store the Unicode characters that correspond to the right font glyphs for each icon. This will be a handy reference for you when needing to re-assign said character codes to new icons.

The defined icon font face and the character variables are used throughout the theme LESS code to define icons, usually through a call to the lib-icon-font mix-in.

Armed with this info about the CSS structure for customizing icon fonts, you’ve got a great starting point; change the variables defining the icon font path and name, and you’ve successfully replaced the native icons with your own. But that still leaves the question of how to generate your own icon font in the first place.

Font Libraries and Tools

You’ve got two main options for defining custom icons for your Magento theme: Create your own, or cherry pick some from available libraries. There are online tools that support either approach and allow you to wrap up your custom creation in a final font. A great example, and the one covered here is IcoMoon.

This font generation tool has a number of free and paid icon sets from which you can pick and choose. It also supports other features important for your Magento icon customization: importing of existing fonts, importing of your own custom icon SVGs, and importing/exporting your selections as JSON-based configuration, so that you can pick up straight from an already configured icon set. IcoMoon also features a fairly robust editing interface for adjusting the scaling and positioning of individual icons.

Using IcoMoon, you can start with an imported font like Magento’s native icons, make your desired tweaks by removing some and adding others, and then re-export a finished font. Of the five file types that Magento makes available natively, IcoMoon’s are missing only woff2. That’s not a problem, though, since there are plenty of tools out there for font file type conversion as well. Try Everything Fonts. IcoMoon’s final package handily includes JSON configuration and example CSS as well, though you’ll be eschewing said CSS in favor of Magento’s native structure.

Caveats and an Enhanced Starter Package

I said earlier that, once you’ve got your custom icon font in hand, replacing Magento’s native icons would be as simple as setting the right LESS variables for the font path and name. It would be great if that were the case. Unfortunately, if you’re using Magento Blank as your theme starting point and plan to customize it with available libraries, you’re likely to run into a snag: the size of the icons in the Blank font relative to their bounding box is significantly smaller than the typical fonts you’ll find on IcoMoon. Combining drop-in replacements with the font sizes and line heights in the Blank theme will result in unusually large icons compared with the spaces where they’re used.

There are two ways to approach this problem. The first is to scale down any icons you are using in your custom set using IcoMoon’s aforementioned editing interface. With this approach, you avoid any CSS changes, but you’ll have to touch every icon you wish to include in your custom font.

The second approach is to customize CSS font sizes and line heights to match the more typical icon sizes. This has the benefit of not needing to worry about sizing icons every time you drop a new one in, but it carries some up front overhead. It would be nice if a few typical icon sizes were defined in LESS variables for just such a customization. Unfortunately, the definition of these sizes is considerably more scattershot throughout the various module LESS source files in Magento. It’s a lucky break, then, that the Magento Luma example theme contains a more typically sized icon font, along with corresponding font sizes.

If you want to extend the leaner, more vanilla Magento Blank with your theme instead of extending Luma, the latter at least provides an appropriate boilerplate from which to extract the right styles to normalize icon sizes. And that’s exactly what I’ve done in this starter theme package. This theme extends Blank but uses the Luma icon font, containing appropriate CSS resets for sizing. You will note that these resets are contained within the theme’s _extend.less, meaning that it supersedes rather than replaces the baseline styles. While this leads to slightly more bloated compiled CSS, it’s a trade-off to avoid entirely duplicating a myriad of core theme files, since the vast majority of styles in said files won’t change from Blank. (The resulting _extend.less also provides a precise reference for which relevant styles should be tweaked if further re-sizing is needed.)

Note that this package is a work in progress, and you may find areas I’ve missed where further size tweaking is needed.

Pulling it All Together

Because of the sizing issue with the Blank theme, you should first decide which of the following approaches you want to take with your custom icons:

  • Approach A: Size your icons to match the CSS sizing in the Blank theme
  • Approach B: Directly extend the Luma theme
  • Approach C: Implement CSS to match the more typical icon size

Which approach is best? That probably depends on how many icons you intend to customize with drop-in replacements. If you’re replacing only a few, Approach A may work best for you. If you’re really giving your theme a distinct flavor with a wholesale new icon set, though, Approach B or C may be the way to go.

If you’re using Approach C, obtain this starter icon theme. You can install and extend this theme, or copy its styles directly into your own theme.

  1. In (IcoMoon, import the SVG font file for your starting icon font (e.g., Luma-Icons). Navigate to the IcoMoon App and use Import Icons. Conveniently, your imported font will be collected into its own set.
  2. For the icons you want to replace, remove the existing glyphs from the set.
  3. Choose your replacement icons. Use the Icon Library to add other free or paid icon sets if desired. Then select your replacements and choose Copy Selection to Set from the settings menu of your custom set.
  4. If you’ve created your own custom icons, use Import to Set from the settings menu of your custom set.
  5. Switch to the Generate Font tab at the bottom of the IcoMoon interface and make sure to set the correct Unicode character values for all new icons. Use the variable reference in lib/web/css/source/lib/variables/_icons.less. (If you’re uncertain which character codes match which icons, you can take note of them in your initial imported font before removing the originals.)
  6. If you’re using Approach A (i.e., sizing icons to match the CSS of the Blank theme), switch to Edit mode in IcoMoon, choose each new icon, and use the “Scale” feature to size the icon down to match the default Blank icons.
  7. Name your font. Use “Edit Info” from the settings menu of your icon set.
  8. Download the font package by switching to the Generate Font tab at the bottom of the interface, then choose “Download” from the same tab.
  9. Get the woff2 file format to round out the five formats Magento makes available for its default fonts. Head to a resource like Everything Fonts and use the conversion tools.
  10. Copy all font file types from the font directory of your downloaded package into your theme at web/fonts.
  11. In web/css/source/_theme.less in your theme, set @icons__font-path and @icons__font-name appropriately with the file path and name of your new icon font.
  12. If you discover you need any further tweaks to icon CSS sizing, use web/css/source/_extend.less to implement these tweaks.

Why We Still Do Tradeshows

When you think of “traditional” marketing, you probably think of television commercials, newspaper ads, billboards, etc. Trade shows are generally lumped in that category as well. And traditional marketing is dead, right? Content is king! Digital marketing reigns supreme! For the most part, I would agree with that sentiment. Content and digital marketing are trackable, measurable and less expensive. And we are living in a digital world.

With all of that being said – we still do trade shows. We do ‘em, and we love ‘em. Yes, they are a very large expense and you cannot get the level of data you can with digital marketing.

But – we love them because in this hyper-digital world it gives us the opportunity to connect with people face to face. We love people. We love spreading joy and building relationships. I go more in-depth about this here, but the gist is – People. Connections. Real Conversations. Through those connections, we grow and strengthen our brand presence in the digital commerce ecosystem.

We also love them because, for us, they generate more leads than any other source. Tradeshows put us in front of a captive audience that is already within the scope of a specified industry. While you can directly target potential customers with digital marketing, an ad only says WHAT we do, not who we are. And if you do try to say who you are in an ad, it is very difficult not to come off as pretentious or cliche. A lot of companies do what we do, but only we do things the Classy way. As our name suggests, we are a unique company and we can convey that much more easily in person.

In addition, trade shows give us the opportunity to network with our peers. If you take the opportunity to talk to people who are doing what you’re doing, you can learn so much as well as build valuable partnerships.

As the world continues to move more and more online, we like to have our foot in both the digital and physical world. We’ll help get your business online, but we want to do it as face-to-face as we can. As we make our plans for 2017, we are actively adding more and more conferences and trade shows. So keep watching our website and social media for where you can find us, and score some sweet swag (for you and your kids).

Contact Us