*** Check out Grokking Magento ***

Vinai Kopp

Magento Expert, Developer & Trainer

  • 09. The Custom Config File Kata

    May 19, 2016

    Mage2Katas

    In this episode we are going to configure all the classes necessary to read custom XML configuration files in Magento 2

    This episode starts where the previous one left off.

    So maybe you want to go through the DI arguments kata one more time so everything is ready.

    To be honest, in this episode there aren’t a whole lot of new concepts, but I still found this kata to be valuable, since custom config files happen in Magento.

    Lets start with the state of the the module at the start of this kata.
    There is the example custom XML configuration file, unit_conversion.xml, together with a matching unit_conversion.xsd schema, which can be found in this gist.
    You can use your own config file, too, it doesn’t matter. For this kata we simply need a custom config file to work with.

    Then there is the test class we created during the DI arguments kata. We will be reusing the custom assertions we created there, namely

    • assertVirtualType($expectedRealType, $virtualType)
    • assertDiArgumentSame($expected, $type, $argumentName)
    • assertDiArgumentInstance($expectedType, $type, $argumentName)

    In the previous kata we used those methods to test the configuration for a config data access virtual type, Mage2Kata\DiConfig\Model\Config\UnitConversion\Virtual.

    To start things off, lets refactor the test a little bit so the virtual types are assigned to class properties ($this->configType and $this->readerType) instead of local variables. That makes it easier to reuse them in the test methods we are about to write.

    Here is the full test class:

    <?php
     
    namespace Mage2Kata\DiConfig;
     
    use Magento\Framework\ObjectManager\ConfigInterface;
    use Magento\TestFramework\ObjectManager;
     
    class UnitConversionDiConfigTest extends \PHPUnit_Framework_TestCase
    {
        private $configType = Model\Config\UnitConversion\Virtual::class;
     
        private $readerType = Model\Config\UnitConversion\Reader\Virtual::class;
     
        /**
         * @return ConfigInterface
         */
        private function getDiConfig()
        {
            return ObjectManager::getInstance()->get(ConfigInterface::class);
        }
     
        /**
         * @param string $expectedRealType
         * @param string $virtualType
         */
        private function assertVirtualType($expectedRealType, $virtualType)
        {
            $this->assertSame($expectedRealType, $this->getDiConfig()->getInstanceType($virtualType));
        }
     
        /**
         * @param mixed $expected
         * @param string $type
         * @param string $argumentName
         */
        private function assertDiArgumentSame($expected, $type, $argumentName)
        {
            $arguments = $this->getDiConfig()->getArguments($type);
            if (!isset($arguments[$argumentName])) {
                $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
            }
            $this->assertSame($expected, $arguments[$argumentName]);
        }
     
        /**
         * @param string $expectedType
         * @param string $type
         * @param string $argumentName
         */
        private function assertDiArgumentInstance($expectedType, $type, $argumentName)
        {
            $arguments = $this->getDiConfig()->getArguments($type);
            if (!isset($arguments[$argumentName])) {
                $this->fail(sprintf('No argument "%s" configured for %s', $argumentName, $type));
            }
            if (!isset($arguments[$argumentName]['instance'])) {
                $this->fail(sprintf('Argument "%s" for %s is not xsi:type="object"', $argumentName, $type));
            }
            $this->assertSame($expectedType, $arguments[$argumentName]['instance']);
        }
     
        public function testUnitConversionConfigDataDiConfig()
        {
            $this->assertVirtualType(\Magento\Framework\Config\Data::class, $this->configType);
            $this->assertDiArgumentSame('mage2kata_unitconversion_map_config', $this->configType, 'cacheId');
            $this->assertDiArgumentInstance($this->readerType, $this->configType, 'reader');
        }
    }

    Now we can add a new test.

    We want to check the mapping of the virtual reader type to a real class.
    If we look at the constuctor signature of \Magento\Framework\Config\Data we can see the required type is \Magento\Framework\Config\ReaderInterface.
    Checking for implementations, there is one generic implementation that can be configured for any XML config file through dependency injection: \Magento\Framework\Config\Reader\Filesystem.

    That is the one we will use for our reader virtual type mapping.

    public function testUnitConversionConfigReaderDiConfig()
    {
        $this->assertVirtualType(\Magento\Framework\Config\Reader\Filesystem::class, $this->readerType);
    }

    Running the test the assertion fails, as expected.
    To make it pass, we need to add the virtual type mapping to our etc/di.xml file, next to the existing one from the previous kata.

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\Virtual" type="Magento\Framework\Config\Data">
            <arguments>
                <argument name="cacheId" xsi:type="string">mage2kata_unitconversion_map_config</argument>
                <argument name="reader" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual</argument>
            </arguments>
        </virtualType>
        <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual" type="Magento\Framework\Config\Reader\Filesystem">
        </virtualType>
    </config>

    Note: If you have the TESTS_CLEANUP constant set to disabled in the phpunit configuration, clear the dev/tests/integration/tmp/sandbox-* directory after every configuration XML change.

    Running the test again shows us that our mapping works as expected: the test passes.

    Now we can configure the constructor arguments for Config\Reader\Filesystem.
    Lets have a look at what arguments are injected.

    public function __construct(
        \Magento\Framework\Config\FileResolverInterface $fileResolver,
        \Magento\Framework\Config\ConverterInterface $converter,
        \Magento\Framework\Config\SchemaLocatorInterface $schemaLocator,
        \Magento\Framework\Config\ValidationStateInterface $validationState,
        $fileName,
        $idAttributes = [],
        $domDocumentClass = 'Magento\Framework\Config\Dom',
        $defaultScope = 'global'
    )

    With some trial and error it turns out, only two arguments are required to be configured explicitly.

    • The $schemaLocator, providing the path to the XSD schema for validation.
    • The $fileName, which is the file name of the XML file to read.

    For now, the other arguments can stay at their default values that are provided by the ObjectManager.

    Lets start with the $fileName argument since it is a simple string. We can add the assertion to the same test method:

    public function testUnitConversionConfigReaderDiConfig()
    {
        $this->assertVirtualType(\Magento\Framework\Config\Reader\Filesystem::class, $this->readerType);
        $this->assertDiArgumentSame('unit_conversion.xml', $this->readerType, 'fileName');
    }

    Of course the test fails, until we add the following argument configuration to our virtual type.

    <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\Reader\Virtual" type="Magento\Framework\Config\Reader\Filesystem">
        <arguments>
            <argument name="fileName" xsi:type="string">unit_conversion.xml</argument>
        </arguments>
    </virtualType>

    Now the first argument is present, we want to assign yet another virtual type as the $schemaLocator.
    The assertion for this argument looks like this:

    $this->assertDiArgumentInstance(Model\Config\UnitConversion\SchemaLocator\Virtual::class, $this->readerType, 'schemaLocator');

    To make the test pass, we add the argument configuration

    <arguments>
        <argument name="fileName" xsi:type="string">unit_conversion.xml</argument>
        <argument name="schemaLocator" xsi:type="object">Mage2Kata\DiConfig\Model\Config\UnitConversion\SchemaLocator\Virtual</argument>
    </arguments>

    …and we are back to green.
    Time to refactor.
    There actually isn’t a lot to do, except assigning the schema to a class property, just like the $configType and the $readerType.

    private $schemaLocatorType = Model\Config\UnitConversion\SchemaLocator\Virtual::class;
     
    ...
     
    $this->assertDiArgumentInstance($this->schemaLocatorType, $this->readerType, 'schemaLocator');

    Besides making the line fit into the 120 character length boundary and making the code more readable and consistent, it also allows us to reuse the value in the next test.

    Now we can move into the red TDD phase again: time to add a test to check the schema locator virtual type mapping.
    Again, we have to look for a class implementing the required type \Magento\Framework\Config\SchemaLocatorInterface.

    There are many implementations, but the one we can configure as needed via DI is \Magento\Framework\Config\GenericSchemaLocator.

    So lets add a test for it just like the others:

    public function testUnitConversionConfigSchemaLocatorDiConfig()
    {
        $this->assertVirtualType(\Magento\Framework\Config\GenericSchemaLocator::class, $this->schemaLocatorType);
    }

    And again, it fails, until we add the configuration to make it pass:

    <virtualType name="Mage2Kata\DiConfig\Model\Config\UnitConversion\SchemaLocator\Virtual" type="Magento\Framework\Config\GenericSchemaLocator">
    </virtualType>

    The GenericSchemaLocator requires us to specify at least the file name of the XSD file and module where it can be found. Both are strings, so they are very straight forward to test.
    Since this test is very similar to the previous ones, I feel confident adding assertions for both arguments in one step:

    $this->assertDiArgumentSame('Mage2Kata_DiConfig', $this->schemaLocatorType, 'moduleName');
    $this->assertDiArgumentSame('unit_conversion.xsd', $this->schemaLocatorType, 'schema');

    Back in red state, we need to add the matching argument configuration to get back into green:

    <argument name="moduleName" xsi:type="string">Mage2Kata_DiConfig</argument>
    <argument name="schema" xsi:type="string">unit_conversion.xsd</argument>

    At this point in time we should be able to read our custom XML configuration using the objects we configured.
    Lets add an integration test to check it actually works!
    At this point in time we are not interested in the actuall array format that will be returned, as long as we get the XML contents in some format it’s all we expect.

    public function testCanReadUnitConversionXmlData()
    {
        /** @var \Magento\Framework\Config\DataInterface $config */
        $config = ObjectManager::getInstance()->create($this->configType);
        $data = $config->get(null);
        $this->assertNotEmpty($data);
    }

    When looking at the implementation of Config\Data::get(), we can see that it will return the full data structure if null is passed as an argument.
    As long as the returned value is not empty, the test will pass.
    Running the test confirms that yes indeed, the configuration is being parsed into an array.

    If you are curious, you can use xdebug to take a peek at the data structure (or just dump it out old school style with print_r()).

    It’s always a good idea to see every test fail at least once. Seeing every test fail and succeed tells us our test works correctly. Without seeing it fail, we can’t really trust it.
    To make the final test fail, all that needs to be done is to rename the unit_conversion.xml file temporarily.

    Once the test fails, we can be assured that any errors would be caught by the test, and can give the file it’s proper name again.

    The final test probably is the most important of all tests in this kata.
    Once we have it in place, all others could be deleted, as they only where stair-step tests to bring us to the point where we can read the configuration file contents.

    In the next kata we will work at massaging the array into a more useful format, so we can actually use to look up unit conversion factory.

    Please leave a comment if you have any questions or comments.

    Thanks for your attention!

    comments powered by Disqus

Read Grokking Magento now!

"The best technical reading I had since years by @VinaiKopp"

Tim Bezhashvyly
Magento Developer