In the first part of the article, we started working on functionality that allows us to select orders and download the dropshipper file. We also added some boilerplate classes and methods for processing order data.

In this part, we’ll focus on generating the content of the file. The file will contain 2 rows per every selected order — a header row and a package row. We’ll define these two kinds of rows using XML documents. But before doing that, we need to define the XML schema to describe the structure of the row itself. Create an edi_order_row.xsd file in the etc directory of the module with the following content:

Magently_Dropshipping/etc/edi_order_row.xsd

<?xml version="1.0"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="items">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="child" maxOccurs="unbounded" minOccurs="0">
                    <xs:complexType>
                        <xs:simpleContent>
                            <xs:extension base="xs:string">
                                <xs:attribute type="xs:string" name="name" use="optional"/>
                                <xs:attribute type="xs:string" name="mapping" use="optional"/>
                                <xs:attribute type="xs:short" name="sort" use="optional"/>
                            </xs:extension>
                        </xs:simpleContent>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
            <xs:attribute type="xs:string" name="name"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

The definition above describes our row documents. You can find more information on how to create XML schemas by going to the url specified in the xs:schema tag: (https://www.w3.org/2001/XMLSchema). A row can contain 0 or more child elements. Each element can have the following optional attributes:

  • name (string)
  • mapping (string)
  • sort (string)

The name and sort attributes are self-explanatory. Let’s create a header row configuration file in the etc directory. Name it edi_order_header.xml and fill it with the following content:

Magently_Dropshipping/etc/edi_order_header.xml

<?xml version="1.0"?>
<items name="fields"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xsi:noNamespaceSchemaLocation="edi_order_row.xsd">
    <child name="code" sort="10">A1</child>
    <child name="account_id" sort="60">123456</child>
    <child name="sender_prefix_code" sort="310"/>
    <child name="sender_first_name" sort="410"/>
    <child name="sender_last_name" sort="510"/>
    <child name="sender_company" sort="610"><![CDATA[The Humble Pixy Co.]]></child>
    <child name="sender_country" sort="810">PL</child>
    <child name="sender_city" sort="910">Wrocław</child>
    <child name="sender_postcode" sort="1010">50-053</child>
    <child name="sender_street" sort="1110">Szewska</child>
    <child name="sender_house_number" sort="1210">3A</child>
    <child name="sender_phone" sort="1410"/>
    <child name="sender_email" sort="1610"/>
    <child name="sender_language_code" sort="1710">pl</child>
    <child name="pickup_hour_from" sort="1820"/>
    <child name="pickup_hour_to" sort="1830"/>
    <!-- Recipient data -->
    <child name="first_name" sort="2010" mapping="shipping_address"/>
    <child name="last_name" sort="2110" mapping="shipping_address"/>
    <child name="company" sort="2210" mapping="shipping_address"/>
    <child name="country" sort="2410">PL</child>
    <child name="city" sort="2510" mapping="shipping_address"/>
    <child name="postcode" sort="2610" mapping="shipping_address"/>
    <child name="phone" sort="3010" mapping="shipping_address"/>
    <child name="mobile" sort="3110"/>
    <child name="notification_sms" sort="3120"/>
    <child name="email" sort="3210" mapping="shipping_address"/>
    <child name="notification_email" sort="3220"/>
    <child name="delivery_hour_from" sort="3460"/>
    <child name="delivery_hour_to" sort="3470"/>
    <child name="order_id" sort="3510"/>
    <child name="notes" sort="3610"/>
    <child name="insurance" sort="4210">10000</child>
</items>

Note that some child elements have already their values set as they will be the same for every order. For those without a value, the name attribute value will be used as a key to get data from the $orderData array. If a particular piece of data lies deeper within the array, you can get it using the mapping attribute.

Now that we have a configuration file (edi_order_header.xml) and a schema file (edi_order_row.xsd), how do we ensure that the former follows the rules we set in the latter? We need a file reader. Luckily Magento provides one — Magento\Framework\Config\Reader\Filesystem. It will read and validate the configuration file, but first we need to adjust the reader itself a bit by using a virtual type. Create a di.xml file in etc/adminhtml directory with a following virtual type definition:

Magently_Dropshipping/etc/adminhtml/di.xml

<virtualType name="Magently\Dropshipping\ContentGenerator\FileGenerator\Edi\FieldReader\OrderHeader"
             type="Magento\Framework\Config\Reader\Filesystem">
    <arguments>
        <argument name="fileName" xsi:type="string">edi_order_header.xml</argument>
        <argument name="converter" 
                  xsi:type="object">Magently\Dropshipping\Config\Edi\Converter</argument>
        <argument name="schemaLocator" 
                  xsi:type="object">Magently\Dropshipping\Config\Edi\OrderRowSchemaLocator</argument>
        </arguments>
</virtualType>

We’re providing the reader class with three arguments there. While the first argument, fileName, is self-explanatory, the other two are more interesting. The second one, Magently\Dropshipping\Config\Edi\Converter, is a class responsible for converting the configuration file to an array. It has to implement Magento\Framework\Config\ConverterInterface with a single method — convert. Here is how it could be implemented:

Magently_Dropshipping/Config/Edi/Converter.php

public function convert($source)
{
    $output = [];
    if (!$source instanceof DOMDocument) {
        return $output;
    }

    /** @var DOMNodeList $sections */
    $items = $source->getElementsByTagName('items');

    /** @var DOMElement $section */
    foreach ($items as $item) {
        $childArray = [];
        $itemsName = $item->getAttribute('name');

        if (!$itemsName) {
            throw new InvalidArgumentException('The attribute "name" of "item" not found.');
        }

        $children = $item->getElementsByTagName('child');
        foreach ($children as $child) {
            /** @var DOMElement $child */
            $childArray[] = [
                'name'    => $child->getAttribute('name'),
                'mapping' => $child->getAttribute('mapping'),
                'sort'    => $child->getAttribute('sort'),
                'value'   => $child->nodeValue
            ];
        }
        $this->sort($childArray);
        $output[$itemsName] = $childArray;
    }
    return $output;
}

The only thing that is missing is sorting. It’s based on the sort attribute values:

Magently_Dropshipping/Config/Edi/Converter.php

private function sort(array &$childArray)
{
    usort($childArray, function ($a, $b) {
        return $a['sort'] - $b['sort'];
    });
}

The third and final argument of our reader virtual type is schemaLocator. Its value, Magently\Dropshipping\Config\Edi\OrderRowSchemaLocator, is yet another virtual type:

Magently_Dropshipping/etc/adminhtml/di.xml

<virtualType name="Magently\Dropshipping\Config\Edi\OrderRowSchemaLocator"
             type="Magently\Dropshipping\Config\Edi\SchemaLocator">
    <arguments>
        <argument name="schemaFileName" xsi:type="string">edi_order_row.xsd</argument>
    </arguments>
</virtualType>

This virtual type is based on a real class Magently\Dropshipping\Config\Edi\SchemaLocator which is the connection between our schema and our configuration file. We’re providing it with a single argument schemaFileName. Magently\Dropshipping\Config\Edi\SchemaLocator must implement Magento\Framework\Config\SchemaLocatorInterface. The interface has two methods: getSchema and getPerFileSchema. Here’s our implementation:

Magently_Dropshipping/Config/Edi/SchemaLocator.php

use Magento\Framework\Config\SchemaLocatorInterface;
use Magento\Framework\Module\Dir;
use Magento\Framework\Module\Dir\Reader;

class SchemaLocator implements SchemaLocatorInterface
{
    protected $schema;
    protected $perFileSchema;

    public function __construct(Reader $moduleReader, string $schemaFileName = '')
    {
        $moduleDir = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magently_Dropshipping');
        $this->schema = $moduleDir . '/' . $schemaFileName;
    }

    public function getSchema(): ?string
    {
        return $this->schema;
    }

    public function getPerFileSchema(): ?string
    {
        return $this->perFileSchema;
    }
}

In part 1, we created a mass action for orders that generates the drop-shipping EDI file. In this part, we created the schema and the configuration for the file together with reading and validating mechanisms. We still need to connect these two parts and turn them into a working feature and that’s what we’re going to do in part 3.