Categories


Archives


Recent Posts


Categories


How Magento Knows if your URLs need `pub` and why all this “Object Orientation” Make us Weep

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

I was helping a developer recently, (Want this sort of help? My Patreon beckons), who wanted to know how Magento determines if /pub needs to be on the end of URLs for static files. The short version? The default folders can be found here

#File: vendor/magento/framework/App/Filesystem/DirectoryList.php
public static function getDefaultConfig()
{
    $result = [
        self::ROOT => [parent::PATH => ''],
        self::APP => [parent::PATH => 'app'],
        self::CONFIG => [parent::PATH => 'app/etc'],
        self::LIB_INTERNAL => [parent::PATH => 'lib/internal'],
        self::VAR_DIR => [parent::PATH => 'var'],
        self::CACHE => [parent::PATH => 'var/cache'],
        self::LOG => [parent::PATH => 'var/log'],
        self::DI => [parent::PATH => 'var/di'],
        self::GENERATION => [parent::PATH => 'var/generation'],
        self::SESSION => [parent::PATH => 'var/session'],
        self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'pub/media'],
        self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'pub/static'],
        self::PUB => [parent::PATH => 'pub', parent::URL_PATH => 'pub'],
        self::LIB_WEB => [parent::PATH => 'lib/web'],
        self::TMP => [parent::PATH => 'var/tmp'],
        self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'pub/media/upload'],
        self::TMP_MATERIALIZATION_DIR => [parent::PATH => 'var/view_preprocessed'],
        self::TEMPLATE_MINIFICATION_DIR => [parent::PATH => 'var/view_preprocessed/html'],
        self::SETUP => [parent::PATH => 'setup/src'],
        self::COMPOSER_HOME => [parent::PATH => 'var/composer_home'],
    ];
    return parent::getDefaultConfig() + $result;
}    

Also, if you’re bootstrapping from pub/index.php instead of the less-secure index.php, Magento sends along a number of other values when it bootstraps

#File: pub/index.php
$params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [
    DirectoryList::PUB => [DirectoryList::URL_PATH => ''],
    DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'],
    DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'],
    DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'],
];
$bootstrap = MagentoFrameworkAppBootstrap::create(BP, $params);    

These values are merged into the values above here

#File: vendor/magento/framework/Filesystem/DirectoryList.php
foreach ($this->directories as $code => $dir) {
    foreach ([self::PATH, self::URL_PATH] as $key) {
        if (isset($config[$code][$key])) {
            $this->directories[$code][$key] = $config[$code][$key];
        }
    }
}    

And it’s this method where Magento plucks the static directory value

#File: vendor/magento/framework/Filesystem/DirectoryList.php
public function getUrlPath($code)
{
    $this->assertCode($code);
    if (!isset($this->directories[$code][self::URL_PATH])) {
        return false;
    }
    return $this->directories[$code][self::URL_PATH];
}

So, relatively straight forward, right? One you know it, and once its been written down, sure. Before that? No really. The rest of this post is a log from the slack conversation where I talked out loud to myself as I tracked this information down for the developer. It took me, a pretty experienced Magento developer, about an hour to find this one piece of information.

Whatever benefits these deeply abstract systems bring, the cost is almost always a sprawling code-base that’s hard to navigate, and hard to intuit things about. If your Magento programmers are grumbling about Magento 2 – chances are it’s an issue like this. If you’re wondering why there’s so many billable hours on your Magento 2 projects, its the programmers needing to stop and spend hours to dig up information that might take minutes in Magento 1.

The following log is mostly me talking to myself, with the original devs name changed to project the innocent.

TheDeveloperWhoseNameHasBeenChanged [3:58 PM]
joined #magento2

TheDeveloperWhoseNameHasBeenChanged [3:58 PM]
How does Magento determine if /pub belongs in the URL or not, when including static files?

Alan Storm [4:42 PM]
via magic? 😉

[4:42]
I think I wrote about this @TheDeveloperWhoseNameHasBeenChanged, one second

[4:44]
hmmm, made note of it here, but not where Magento does its check: http://alanstorm.com/magento-2-frontend-files-serving/

Alan Storm [4:55 PM]
@TheDeveloperWhoseNameHasBeenChanged Looks like the FallbackContextFactory has a baseUrl that’s passed into the constructor

[4:55]
vendor/magento/framework/View/Asset/File/FallbackContext.php

[4:56]
which is created by a factory vendor/magento/framework/View/Asset/File/FallbackContextFactory.php

[4:56]
public function create(array $data = [])
{
return $this->objectManager->create(FallbackContext::class, $data);
}

[4:56]
(Magento core directly uses Object Manager — take a drink)

[4:58]
tracing the call further back, it looks like the fallbackContextFactory is created here

[4:58]
#File: vendor/magento/framework/View/Asset/Repository.php
$url = $this->baseUrl->getBaseUrl(['_type' => $urlType, '_secure' => $isSecure]);
$this->fallbackContext[$id] = $this->fallbackContextFactory->create(
[
'baseUrl' => $url,
'areaType' => $area,
'themePath' => $themePath,
'localeCode' => $locale,
'isSecure' => $isSecure
]
);

(edited)

[5:01]
$urlType is “static”

[5:01]
and the baseUrl property is a MagentoFrameworkUrlInterface

[5:02]
which has a concrete class of MagentoFrameworkUrl

[5:04]
relevant call in
MagentoFrameworkUrl::getBaseUrl
seems to be this

[5:04]
$result = $this->_getScope()->getBaseUrl($this->_getType(), $this->_isSecure());

[5:06]
and _getScope returns a MagentoStoreModelStoreInterceptor

[5:07]
we’ll pray there’s no plugins or preferences in place and look at the store model

[5:07]
vendor/magento/module-store/Model/Store.php

[5:08]
that static URLs seem to come from
case UrlInterface::URL_TYPE_STATIC:
$path = $secure ? self::XML_PATH_SECURE_BASE_STATIC_URL : self::XML_PATH_UNSECURE_BASE_STATIC_URL;
$url = $this->getConfig($path);
if (!$url) {
$url = $this->getBaseUrl(UrlInterface::URL_TYPE_WEB, $secure)
. $this->filesystem->getUri(DirectoryList::STATIC_VIEW);
}
break;

[5:09]
looks to be this code path

[5:09]
$url = $this->getBaseUrl(UrlInterface::URL_TYPE_WEB, $secure)
. $this->filesystem->getUri(DirectoryList::STATIC_VIEW);

[5:11]
and more specifically this part
$this->filesystem->getUri(DirectoryList::STATIC_VIEW)

[5:12]
so back to a __constructor to figure out what filesystem is

[5:12]
public function __construct(
MagentoFrameworkModelContext $context,
MagentoFrameworkRegistry $registry,
MagentoFrameworkApiExtensionAttributesFactory $extensionFactory,
MagentoFrameworkApiAttributeValueFactory $customAttributeFactory,
MagentoStoreModelResourceModelStore $resource,
MagentoMediaStorageHelperFileStorageDatabase $coreFileStorageDatabase,
MagentoFrameworkAppCacheTypeConfig $configCacheType,
MagentoFrameworkUrlInterface $url,
MagentoFrameworkAppRequestInterface $request,
MagentoConfigModelResourceModelConfigData $configDataResource,
MagentoFrameworkFilesystem $filesystem,
MagentoFrameworkAppConfigReinitableConfigInterface $config,
MagentoStoreModelStoreManagerInterface $storeManager,
MagentoFrameworkSessionSidResolverInterface $sidResolver,
MagentoFrameworkAppHttpContext $httpContext,
MagentoFrameworkSessionSessionManagerInterface $session,
MagentoDirectoryModelCurrencyFactory $currencyFactory,
MagentoStoreModelInformation $information,
$currencyInstalled,
MagentoStoreApiGroupRepositoryInterface $groupRepository,
MagentoStoreApiWebsiteRepositoryInterface $websiteRepository,
MagentoFrameworkDataCollectionAbstractDb $resourceCollection = null,
$isCustomEntryPoint = false,
array $data = []
) {
$this->_coreFileStorageDatabase = $coreFileStorageDatabase;
$this->_config = $config;
$this->_url = $url;
$this->_configCacheType = $configCacheType;
$this->_request = $request;
$this->_configDataResource = $configDataResource;
$this->_isCustomEntryPoint = $isCustomEntryPoint;
$this->filesystem = $filesystem;
$this->_storeManager = $storeManager;
$this->_sidResolver = $sidResolver;
$this->_httpContext = $httpContext;
$this->_session = $session;
$this->currencyFactory = $currencyFactory;
$this->information = $information;
$this->_currencyInstalled = $currencyInstalled;
$this->groupRepository = $groupRepository;
$this->websiteRepository = $websiteRepository;
parent::__construct(
$context,
$registry,
$extensionFactory,
$customAttributeFactory,
$resource,
$resourceCollection,
$data
);
}

[5:12]
that’s a MagentoFrameworkFilesystem $filesystem,

[5:13]
and in
Filesystem.php

[5:13]
we have

[5:13]
public function getUri($code)
{
return $this->directoryList->getUrlPath($code);
}

[5:13]
pop to the constructor for a directoryList

[5:13]
public function __construct(
MagentoFrameworkFilesystemDirectoryList $directoryList,
MagentoFrameworkFilesystemDirectoryReadFactory $readFactory,
MagentoFrameworkFilesystemDirectoryWriteFactory $writeFactory
) {
$this->directoryList = $directoryList;
$this->readFactory = $readFactory;
$this->writeFactory = $writeFactory;
}

[5:13]
looks like a
MagentoFrameworkFilesystemDirectoryList

[5:15]
and in DirectoryList.php, the getUrlPath looks like

[5:15]
public function getUrlPath($code)
{
$this->assertCode($code);
if (!isset($this->directories[$code][self::URL_PATH])) {
return false;
}
return $this->directories[$code][self::URL_PATH];
}

[5:15]
so, we get to figure out how directories is populated

[5:16]
looks like a private variable — always a fun time

[5:16]
/**
* Directories configurations
*
* @var array
*/
private $directories;

Alan Storm [5:21 PM]
OK, that’s interesting. $directories gets populated in the constructor in DirectoryList.php

[5:21]
public function __construct($root, array $config = [])
{
static::validate($config);
$this->root = $this->normalizePath($root);
$this->directories = static::getDefaultConfig();

[5:22]
but its values start out as

[5:22]
/path/to/magento/vendor/magento/framework/Filesystem/DirectoryList.php:124:
array (size=2)
'path' => string '/path/to/magento/pub/static' (length=77)
'uri' => string 'pub/static' (length=10)

[5:23]
but by the time Magento calls getUrlPath

[5:23]
the values are

[5:23]
/path/to/magento/vendor/magento/framework/Filesystem/DirectoryList.php:221:
array (size=2)
'path' => string '/path/to/magento/pub/static' (length=77)
'uri' => string 'static' (length=6)

[5:23]
so something strips the pub

Alan Storm [5:24 PM]
makes silent cheer he was running in the right mode to notice this

Alan Storm [5:24 PM]
which means $directories being private is a nice thing (for once), as it has to be some code in this class that does the stripping

[5:25]
hurm, or not

[5:26]
(thanks for the simple question @TheDeveloperWhoseNameHasBeenChanged ! 😉 )

[5:27]
OK, MagentoFilesystemDirectoryList is instantiated three times

[5:27]
the first time the value of uri is pub/static

[5:28]
the second and third time, it’s static

[5:29]
looks like it’s this block that swaps out a uri with pub for one without

[5:29]
foreach ($this->directories as $code => $dir) {
foreach ([self::PATH, self::URL_PATH] as $key) {
if (isset($config[$code][$key])) {
$this->directories[$code][$key] = $config[$code][$key];
}
}
}

TheDeveloperWhoseNameHasBeenChanged [5:29 PM]
/gif popcorn

RightGIF BOT [5:29 PM]
(472KB)

Alan Storm [5:30 PM]
so the value looks like it comes from $config

[5:30]
public function __construct($root, array $config = [])
{

[5:32]
and config looks like this in the three instantiations

[5:32]
/path/to/magento/vendor/magento/framework/Filesystem/DirectoryList.php:96:
array (size=1)
'base' =>
array (size=1)
'path' => string '/path/to/magento' (length=66)
/path/to/magento/vendor/magento/framework/Filesystem/DirectoryList.php:96:
array (size=5)
'base' =>
array (size=1)
'path' => string '/path/to/magento' (length=66)
'pub' =>
array (size=1)
'uri' => string '' (length=0)
'media' =>
array (size=1)
'uri' => string 'media' (length=5)
'static' =>
array (size=1)
'uri' => string 'static' (length=6)
'upload' =>
array (size=1)
'uri' => string 'media/upload' (length=12)
/path/to/magento/vendor/magento/framework/Filesystem/DirectoryList.php:96:
array (size=5)
'base' =>
array (size=1)
'path' => string '/path/to/magento' (length=66)
'pub' =>
array (size=1)
'uri' => string '' (length=0)
'media' =>
array (size=1)
'uri' => string 'media' (length=5)
'static' =>
array (size=1)
'uri' => string 'static' (length=6)
'upload' =>
array (size=1)
'uri' => string 'media/upload' (length=12)

[5:32]
so we get a URI for the static directory, as well as pub

[5:32]
so, what instantiates DirectoryList, and what sets those values

[5:35]
some simple debugging later for a callstack

[5:35]
Called __construct in framework/App/Filesystem/DirectoryList.php at line 145
Called __construct in framework/App/Bootstrap.php at line 169
Called createFilesystemDirectoryList in framework/App/Bootstrap.php at line 136
Called populateAutoloader in framework/App/Bootstrap.php at line 120
Called create in /path/to/magento/pub/index.php at line 34

[5:36]
and in Bootstrap.php at 169 we have
public static function createFilesystemDirectoryList($rootDir, array $initParams)
{
$customDirs = [];
if (isset($initParams[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS])) {
$customDirs = $initParams[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS];
}
return new DirectoryList($rootDir, $customDirs);
}

[5:37]
at 136 we have
$dirList = self::createFilesystemDirectoryList($rootDir, $initParams);

[5:37]
(we’re chasing $initParams at this point)

[5:37]
at 120 we have
public static function create($rootDir, array $initParams, ObjectManagerFactory $factory = null)
{
self::populateAutoloader($rootDir, $initParams);
if ($factory === null) {
$factory = self::createObjectManagerFactory($rootDir, $initParams);
}
return new self($factory, $rootDir, $initParams);
}

[5:38]
ahhhhhhhhh

[5:38]
ha

[5:38]
ha

[5:38]
ha

[5:38]
and back up to pub/index.php

[5:38]
$params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [
DirectoryList::PUB => [DirectoryList::URL_PATH => ''],
DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'],
DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'],
DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'],
];
$bootstrap = MagentoFrameworkAppBootstrap::create(BP, $params);
/** @var MagentoFrameworkAppHttp $app */
$app = $bootstrap->createApplication('MagentoFrameworkAppHttp');
$bootstrap->run($app);

[5:39]
DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static']

[5:39]
that tells Magento the URI of the static asset directory

[5:39]
so if you’re pointing to pub/ as your webroot, that’s the index.php you get

[5:39]
however, if you’re pointing at the “GoDaddy” index.php file

[5:40]
i.e. the one in the root project folder you shouldn’t point at but Magento leaves there because there’s some weird business relationship with a hosting partner that can’t support pub/ (edited)

[5:41]
the create method gets called without the extra values being set

[5:41]
$bootstrap = MagentoFrameworkAppBootstrap::create(BP, $_SERVER);

[5:41]
which means back in DirectoryList.php

[5:42]
the values from
$this->directories = static::getDefaultConfig();
are used

[5:43]
which means

[5:43]
vendor/magento/framework/App/Filesystem/DirectoryList.php

[5:43]
public static function getDefaultConfig()
{
$result = [
self::ROOT => [parent::PATH => ''],
self::APP => [parent::PATH => 'app'],
self::CONFIG => [parent::PATH => 'app/etc'],
self::LIB_INTERNAL => [parent::PATH => 'lib/internal'],
self::VAR_DIR => [parent::PATH => 'var'],
self::CACHE => [parent::PATH => 'var/cache'],
self::LOG => [parent::PATH => 'var/log'],
self::DI => [parent::PATH => 'var/di'],
self::GENERATION => [parent::PATH => 'var/generation'],
self::SESSION => [parent::PATH => 'var/session'],
self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'pub/media'],
self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'pub/static'],
self::PUB => [parent::PATH => 'pub', parent::URL_PATH => 'pub'],
self::LIB_WEB => [parent::PATH => 'lib/web'],
self::TMP => [parent::PATH => 'var/tmp'],
self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'pub/media/upload'],
self::TMP_MATERIALIZATION_DIR => [parent::PATH => 'var/view_preprocessed'],
self::TEMPLATE_MINIFICATION_DIR => [parent::PATH => 'var/view_preprocessed/html'],
self::SETUP => [parent::PATH => 'setup/src'],
self::COMPOSER_HOME => [parent::PATH => 'var/composer_home'],
];
return parent::getDefaultConfig() + $result;
}

[5:44]
does that cover it @TheDeveloperWhoseNameHasBeenChanged?

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 22nd December 2016