XML Schema Resolution in PHP Storm with URNs (Quick Note)

In a recent Magento 2 code drop into the develop branch on GitHub, support was added for allowing Composer installed modules to stay in the ‘vendor’ directory rather than be copied to ‘app/code’. This however caused problems with XSD references in XML files that used relative paths, as the module could be in different directories at different times. To overcome this problem, new URNs have been introduced for referencing XML Schema declarations. These need to be configured in PHP Storm in order for schema validation to work again. This note is a quick note describing how to do these steps. (Official documentation is coming really soon.)

Some examples of XSD URIs that have changed:

  • “../../../../../lib/internal/Magento/Framework/Config/etc/theme.xsd” can now be replaced with “urn:magento:framework:Config/etc/theme.xsd”.
  • “../../../../../Magento/Webapi/etc/webapi.xsd” can now be replaced with “urn:magento:module:Magento_Webapi:etc/webapi.xsd”.

The new URNs no longer care about the path relative to the other module (which gets messed up when you have some modules under ‘vendor’ and some locally developed modules still under ‘app/code’). Relative paths still work, but are not recommended any more.

If you are using PHP Storm however, it by default does not know how to resolve URNs. The URN resolution code is built into Magento directly. Luckily, it’s pretty easy to associate URNs with real files in your local site.

This is what you will see after loading up the new code. Notice the URN is in red because PHP Storm does not know how to resolve it.

urn-storm-1

Put the cursor in the URN then click on the red light bulb to the left that will appear and select “Manually Setup External Resource” from the list.

urn-storm-2

Scroll down the list of XSD files that PHP Storm has automatically located, and select the appropriate file. The red URN will now be shown in green, and XML validation is back in effect.

urn-storm-3

You could go out on a hunt and destroy mission to find all such references in advance – personally I plan to just register URNs incrementally if I start editing a file with a red URN in it. If you are not editing a file, having the URN not be resolved is not really a problem in PHP Storm.

Oct 30, 2015: Stop the presses!  I just did a git pull and noticed a new CLI command “dev:urn-catalog:generate”. (Okay, you can see Eugene pointed it out in comments below!)  You should probably save your current .idea/misc.xml file, but I just ran the following command from my project root directory

bin/magento dev:urn-catalog:generate .idea/misc.xml

I then restarted PHP Storm, and all the URNs started resolving! Cool bananas! (It took a little while to run, but it found all the schema files and worked out URNs for them all – well all the ones I found so far.)

7 comments

  1. I created an experimental script to make adding the urn to file mapping a bit faster for phpstorm.
    Still needs a bit quality work, but may help some people already.


    <?php
    /**
    * license is MIT, you will find the text of it in the internet
    */
    echo "start generation script\n";
    function getAllSchemaFilePaths()
    {
    $basedir = getcwd().DIRECTORY_SEPARATOR;
    $Directory = new RecursiveDirectoryIterator($basedir);
    $Iterator = new RecursiveIteratorIterator($Directory);
    $Regex = new RegexIterator($Iterator, '/^.+\.xsd$/i', RecursiveRegexIterator::GET_MATCH);
    $paths = [];
    foreach ($Regex as $k => $v) {
    $paths[] = $k;
    }
    return $paths;
    }
    function transformXsdPathToMagentoUri($path)
    {
    $result = null;
    $parts = explode(DIRECTORY_SEPARATOR, $path);
    $parts = array_reverse($parts);
    if (strpos($path, 'app/code') !== false) {
    if ($parts[1] == 'etc') {
    $result = 'urn:magento:module:'.$parts[3].'_'.$parts[2].':etc/'.$parts[0];
    } elseif ($parts[2] == 'etc') {
    $result = 'urn:magento:module:'.$parts[4].'_'.$parts[3].':etc/'.$parts[1].'/'.$parts[0];
    }
    }
    if (strpos($path, 'Magento/Framework') !== false) {
    if ($parts[1] == 'etc') {
    $result = 'urn:magento:framework:'.$parts[2].'/etc/'.$parts[0];
    } elseif ($parts[3] == 'Framework') {
    $result = 'urn:magento:framework:'.$parts[2].'/'.$parts[1].'/'.$parts[0];
    }
    }
    return $result;
    }
    function mapFullArray($paths)
    {
    $result = [];
    foreach ($paths as $path) {
    $transformed = transformXsdPathToMagentoUri($path);
    if ($transformed !== null) {
    $result[$path] = $transformed;
    }
    }
    return $result;
    }
    function unitTest()
    {
    echo "start unitTest:\n";
    $testArray = [
    '/work/sandbox/magento2/lib/internal/Magento/Framework/Event/etc/events.xsd' => 'urn:magento:framework:Event/etc/events.xsd',
    '/work/sandbox/magento2/app/code/Magento/Integration/etc/integration/config.xsd' => 'urn:magento:module:Magento_Integration:etc/integration/config.xsd',
    '/work/sandbox/magento2/app/code/Magento/Store/etc/config.xsd' => 'urn:magento:module:Magento_Store:etc/config.xsd',
    ];
    echo PHP_EOL;
    foreach ($testArray as $input => $expected) {
    $result = transformXsdPathToMagentoUri($input);
    $messages = [];
    if ($result === $expected) {
    echo '.';
    } else {
    echo 'F';
    $messages[] = $result . ' does not match ' . $expected;
    }
    }
    echo PHP_EOL;
    if (!empty($messages)) {
    var_dump($messages);
    }
    echo "finish unitTest:\n";
    }
    function findUnresolvedPaths()
    {
    echo "show unresolved Paths:\n";
    $paths = getAllSchemaFilePaths();
    foreach ($paths as $path) {
    $urn = transformXsdPathToMagentoUri($path);
    if ($urn === null) {
    var_dump($path);
    }
    }
    echo 'paths with "/_files/" and also "argument/types.xsd" seem not to require translation?'."\n";
    foreach (array(
    '/design/adminhtml/Magento/backend/etc/view.xsd',
    ) as $file) {
    echo "is '$file' used somewhere via urn?\n";
    }
    echo "that were all unresolved Paths:\n";
    }
    /**
    * @param $map
    *
    * in phpstorm 8.0 inside .idea/misc.xml
    * the element <project version="4"> contains an <component name="ProjectResources"> element
    * with all the mappings for local xsd files
    *
    * @return string|void
    */
    function transformMapToPhpstormConfigString($map)
    {
    $cwd = getcwd();
    if (!file_exists($cwd.'/.idea')) {
    echo "thats not an phpstorm project directory\n";
    return;
    }
    $xml = '';
    foreach ($map as $path => $urn) {
    $xml .= '<resource url="'.$urn.'" location="'.str_replace($cwd, '$PROJECT_DIR$', $path).'" />'.PHP_EOL;
    }
    $xml = '<component name="ProjectResources">'.PHP_EOL.$xml.PHP_EOL.'</component>';
    return $xml;
    }
    var_dump(transformMapToPhpstormConfigString(mapFullArray(getAllSchemaFilePaths())));
    //unitTest();
    //findUnresolvedPaths();
    echo "finish generation script\n";

    Thought it makes sense to link it here 🙂

    1. Flyingmana · · Reply

      seems Iam not allowed to post an url here <.<

      gist.github.com/Flyingmana
      /c83a09288b666225b5d0

      1. Great idea! We pushed it forward and generated a URN catalog for PhpStorm. Can you please take a look? http://magento.stackexchange.com/questions/88242/magento-2-xml-validation/88413#88413

      2. Replied to your comment below.

  2. Great idea! We moved it forward and generate complete URN catalog in the PhpStorm format. Can you please take a look? http://magento.stackexchange.com/questions/88242/magento-2-xml-validation/88413#88413

    Also would be helpful to add generation for other IDEs.

  3. Just install https://plugins.jetbrains.com/plugin/8024 go to plugin settings and click “Regenerate urn map” button.
    For feature requests/additional information please refer to https://github.com/dkvashninbay/magento2plugin

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.