In Magento 2, you can specify a parent theme in a theme's theme.xml file.

<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>Theme Title</title>

The first time Magento sees a theme, it uses this value to set a parent_id in the theme table. This is the source of truth for where a theme's parent is.

However, if you attempt to change this value after a theme's been added to the system, Magento fails to update the parent_id column, and instantiated Magento\Theme\Model\Theme objects will still have the original parent theme. (Even if you clear the cache.)

I can fix this by manually changing the parent_id value -- that seems like a hack. Where is parent_id normally set in Magento's core code, and what user actions trigger this? i.e. is there a way to tell Magento "please re-load this theme"

  • 2
    Yes, I have noticed this too and the only way I have found to modify this after the theme has been registered is to modify the database directly. Possibly a bug? – Gareth Daine Feb 21 '16 at 19:14

UPDATED AT 20160310


It is always set either via updateTheme() or from the collection (via DB) if your appState->getMode() == AppState::MODE_PRODUCTION


To answer the question What is the way to get Magento to reload the theme.xml file the answer is:

Set the application state to developer using SetEnv MAGE_MODE developer in .htaccess (or nginx equivalent) and then log in to the admin area (or refresh any admin route) to trigger Magento\Theme\Model\Theme\Plugin\Registration::beforeDispatch().

The theme table in the database is updated due to


See analysis below for details.


Wow the Magento 2 code seems really complex to me. Have you studied this function beforeDispatch() which calls updateThemeData() but only if ($this->appState->getMode() != AppState::MODE_PRODUCTION)

//namespace: namespace Magento\Theme\Model\Theme\Plugin;
//class: Registration
//file: app/code/Magento/Theme/Model/Theme/Plugin/Registration.php 

     * Add new theme from filesystem and update existing
     * @param AbstractAction $subject
     * @param RequestInterface $request
     * @return void
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
    public function beforeDispatch(
        AbstractAction $subject,
        RequestInterface $request
    ) {
        try {
            if ($this->appState->getMode() != AppState::MODE_PRODUCTION) {
        } catch (LocalizedException $e) {

Probably you have been through this code.

beforeDispatch() is called only via admin routes and not on front-end routes. Here is a trace:

#0 [internal function]: Magento\Theme\Model\Theme\Plugin\Registration->beforeDispatch(Object(Magento\Backend\Controller\Adminhtml\Dashboard\Index\Interceptor), Object(Magento\Framework\App\Request\Http))
#1 \magento2\lib\internal\Magento\Framework\Interception\Interceptor.php(122): call_user_func_array(Array, Array)
#2 \magento2\var\generation\Magento\Backend\Controller\Adminhtml\Dashboard\Index\Interceptor.php(39): Magento\Backend\Controller\Adminhtml\Dashboard\Index\Interceptor->___callPlugins('dispatch', Array, Array)
#3 \magento2\lib\internal\Magento\Framework\App\FrontController.php(55): Magento\Backend\Controller\Adminhtml\Dashboard\Index\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http))
#4 [internal function]: Magento\Framework\App\FrontController->dispatch(Object(Magento\Framework\App\Request\Http))
#5 \magento2\lib\internal\Magento\Framework\Interception\Interceptor.php(74): call_user_func_array(Array, Array)
#6 \magento2\lib\internal\Magento\Framework\Interception\Chain\Chain.php(70): Magento\Framework\App\FrontController\Interceptor->___callParent('dispatch', Array)
#7 \magento2\lib\internal\Magento\Framework\Interception\Interceptor.php(136): Magento\Framework\Interception\Chain\Chain->invokeNext('Magento\\Framewo...', 'dispatch', Object(Magento\Framework\App\FrontController\Interceptor), Array, 'install')
#8 \magento2\lib\internal\Magento\Framework\Module\Plugin\DbStatusValidator.php(69): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#9 [internal function]: Magento\Framework\Module\Plugin\DbStatusValidator->aroundDispatch(Object(Magento\Framework\App\FrontController\Interceptor), Object(Closure), Object(Magento\Framework\App\Request\Http))
#10 \magento2\lib\internal\Magento\Framework\Interception\Interceptor.php(141): call_user_func_array(Array, Array)
#11 \magento2\var\generation\Magento\Framework\App\FrontController\Interceptor.php(26): Magento\Framework\App\FrontController\Interceptor->___callPlugins('dispatch', Array, Array)
#12 \magento2\lib\internal\Magento\Framework\App\Http.php(115): Magento\Framework\App\FrontController\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http))
#13 \magento2\lib\internal\Magento\Framework\App\Bootstrap.php(258): Magento\Framework\App\Http->launch()
#14 \magento2\index.php(39): Magento\Framework\App\Bootstrap->run(Object(Magento\Framework\App\Http))

Actually I see beforeDispatch() calls updateThemeData() which contains this nugget:

//namespace: namespace Magento\Theme\Model\Theme\Plugin;
//class: Registration
//file: app/code/Magento/Theme/Model/Theme/Plugin/Registration.php 
//function: updateThemeData()

            if ($themeData->getParentTheme()) {
                $parentTheme = $this->themeLoader->getThemeByFullPath(

Which seems to actually (finally) refer to a config XML path $themeData->getParentTheme()->getFullPath() but that function still uses $themeData->getParentTheme(). Oh I think the logic is 'If I am updating a registered theme that has a parentId in the collection (via the DB) then seek a parent path in the config and update the collection'. So maybe this is it.

Otherwise I am at a complete loss as to how Magento\Theme\Model\Theme::getParentTheme() implements getParentId() which is declared in the theme interface. Surely it isn't magic. As you say it needs to come either from the DB via the collection or from the theme's config XML path (if it changed or isn't defined yet) but I can't find a definition of getParentId(). Maybe it is always set via updateTheme() OR from the collection (via DB) so too bad if your appState->getMode() == AppState::MODE_PRODUCTION.

I found it useful to glean information from within updateThemeData() by adding some log output:

//namespace: namespace Magento\Theme\Model\Theme\Plugin;
//class: Registration
//file: app/code/Magento/Theme/Model/Theme/Plugin/Registration.php 
//function: updateThemeData()

            if ($themeData->getParentTheme()) {
                $parentTheme = $this->themeLoader->getThemeByFullPath(
            $this->logger->addDebug("Theme parent full path ".$themeData->getParentTheme()->getFullPath());
            $this->logger->addDebug("Theme parent new ID ".$parentTheme->getId());                    $themeData->setParentId($parentTheme->getId());

Which will log to /var/log/debug.log. With application state set to developer I can see the parent ID is always set on every admin page refresh whether it was changed in theme.xml or not. With application state production the function is never run so I conclude:

It is always set via updateTheme() OR from the collection (via DB) so too bad if your appState->getMode() == AppState::MODE_PRODUCTION

I think you probably are all in developer app state. default app state will trigger updateThemeData() too of course. In further debugging I logged the theme full path for the parent theme of Luma which was frontend/Magento/blank. The capital M surprised me so maybe something to watch out for.


The above didn't seem to work for me, so I went with the hack.

Hope it helps someone.

using the command line


 SHOW databases;

 use magento; (or whatever your DB's name is)

 SHOW tables

 SELECT * FROM theme; 

(Check the **parent_id** of theme in question, it should be the same number as **theme_id** of theme you want as the parent)

if it is not, change it.

 UPDATE theme SET parent_id  = '[value]' WHERE theme_title = '[Theme name]';

then quit mysql;

 bin/magento setup:static-content:deploy 


grunt clean:[theme] (For example:  grunt clean:blank)

grunt exec:[theme]

grunt less:[theme]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.