0

I am creating UI that will allow users to edit the active theme's template files(something like Wordpress does). For this I need to get the list of all templates from the system and to know which templates are already overridden by the front-end theme.

The list of templates can be retrieved via \Drupal::service('theme.registry')->get(). The problem is that this will return templates of the currently used theme therefore this will not return templates that are overridden by the front-end theme(since this is done on the back-end which has different theme).

The class \Drupal\Core\Theme\Registry has option to specify the theme in it's constructor but there is no option to set this via the service. Calling the class directly would require me to take care of DI and it's not what I'd want to do anyway(not very Drupal-way approach).

In the init() method there is logic that would rebuild the registry if there is no global theme key set or the theme name is different from theme information but I don' want to fiddle with global variables.

So what would be the proper way to do this?

  • ... well you don't have a common usecase so live with creating the object for yourself – Daniel Wehner Apr 13 '14 at 5:24
  • What's the point of having the theme argument available when it can never be properly used? – user21641 Apr 13 '14 at 17:37
  • Well, a service exists for itself, but you can always reuse classes in other places, if you like. Specifying the theme manually for example is not a global service. – Daniel Wehner Apr 14 '14 at 9:34
  • I ended up using the class directly(no other choice) and it works like a charm. Though I'd welcome some public method that would allow me to change the theme. – user21641 Apr 14 '14 at 11:37
0

This is a working prototype, although I will be rewritting it a bit later.

<?php

/**
 * @file
 * Contains \Drupal\mymodule\Form\ThemeTemplateForm.
 */

namespace Drupal\mymodule\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Theme\Registry;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Component\Utility\String;


/**
 * Form for editing template files for current theme.
 */
class ThemeTemplateForm extends FormBase {

  /**
   * The cache backend to use for the complete theme registry data.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The lock backend that should be used.
   *
   * @var \Drupal\Core\Lock\LockBackendInterface
   */
  protected $lock;

  /**
   * The module handler to use to load modules.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The theme registry service.
   *
   * @var \Drupal\Core\Theme\Registry
   */
  protected $registry;

  /**
   * The template that is being edited/created.
   *
   * @var string
   */
  protected $template;

  /**
   * The front-end theme that we'll be editing templates for.
   *
   * @var string
   */
  protected $theme;

  /**
   * Creates ThemeTemplateForm object.
   *
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend interface to use for the complete theme registry data.
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
   *   The lock backend.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to use to load modules.
   */
  public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler) {
    $this->cache = $cache;
    $this->lock = $lock;
    $this->moduleHandler = $module_handler;
    $this->theme = $this->config('system.theme')->get('default');
    $this->registry = new Registry($this->cache, $this->lock, $this->moduleHandler, $this->theme);

    $template = $this->getRequest()->query->get('template');
    if (!empty($template) && $this->templateIsAllowed($template)) {
      // Always use machine name of the template
      $this->template = str_replace('-', '_', $template);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('cache.default'),
      $container->get('lock'),
      $container->get('module_handler')
    );
  }

  /**
   * Returns path to selected theme.
   *
   * @return string
   *   Path relative to Drupal root.
   */
  public function themePath() {
    return drupal_get_path('theme', $this->theme);
  }


  /**
   * Returns the list of template files.
   */
  public function templateList() {
    $list = array_filter($this->registry->get(), function($info) {
      return isset($info['template']);
    });
    // Allow modules to alter the list
    \Drupal::moduleHandler()->alter('editable_template_list', $list);
    ksort($list);
    return $list;
  }

  /**
   * Returns list of overridden templates by the front-end theme.
   */
  public function overriddenTemplateList() {
    $list = array_filter($this->templateList(), function($info) {
      // The system will return templates that have preprocess functions
      // implemented by the theme so we need to remove them from the list
      // since they aren't really overridden.
      $theme_path = $this->themePath();
      return $info['theme path'] == $theme_path && (isset($info['path']) && stripos($info['path'], $theme_path) === 0);
    });
    return $list;
  }

  /**
   * See if provided template is allowed.
   *
   * @param string $template
   *   Template name.
   *
   * @return bool
   *   TRUE if template is allowed, FALSE otherwise.
   */
  public function templateIsAllowed($template) {
    $list = $this->templateList();
    $list[$template] = array();
    \Drupal::moduleHandler()->alter('editable_template_list', $list);
    // If no module removed the template the template is allowed
    return isset($list[$template]);
  }

  /**
   * Returns information if the provided template is overridden by the theme.
   *
   * @param string $template
   *   The template name.
   *
   * @return bool
   *   Returns TRUE if the template is overridden, FALSE otherwise.
   */
  public function templateIsOverridden($template) {
    $overridden = $this->overriddenTemplateList();
    return isset($overridden[$template]);
  }

  /**
   * Returns the template information.
   *
   * @param string $template
   *   The template name.
   *
   * @return array
   *   Returns the template information array. Empty array is returned
   *   when template was not found.
   */
  public function templateInfo($template) {
    $templates = $this->templateList();
    if (isset($templates[$template])) {
      return $templates[$template];
    } else {
      $base_hook = $this->registry->getBaseHook($template);
      return $base_hook ? $template[$base_hook] : array();
    }
  }

  /**
   * Returns the source path of the template file.
   *
   * @param string $template
   *   Template name.
   *
   * @return string
   *   Path to the source template file relative to Drupal root.
   */
  public function getTemplateSourcePath($template) {
    $info = $this->templateInfo($template);
    return $info['template'] . '.html.twig';
  }

  /**
   * Returns the path of the template in the theme.
   * If the template does not exist(is not overridden yet)
   * the 'templates' directory within the theme directory will
   * be used as destination for the new template. Otherwise
   * 'path' will be used since themes don't have to explicitly put
   * their templates into the 'templates'directory.
   *
   * @param string $template
   *   Template name.
   *
   * @return string
   *   The path to the template file within the theme directory
   *   relative to Drupal root.
   */
  public function getTemplateThemePath($template) {
    $info = $this->templateInfo($template);
    if (isset($info['path']) && stripos($info['path'], $this->themePath()) === 0) {
      return $info['path'] . '/' . str_replace('_', '-', $template) . '.html.twig';
    } else{
      return $this->themePath() . '/templates/' . str_replace('_', '-', $template) . '.html.twig';
    }
  }

  /**
   * See if provided path is allowed for writing/overriding
   * a template file.
   *
   * @param string $path
   *   Path to the template relative to Drupal root.
   *
   * @return bool
   *   TRUE if template can be created/overridden, FALSE otherwise.
   */
  public function pathIsAllowed($path) {
    // Access is allowed only to theme's directory AND this
    // always has to be located within the 'sites' directory.
    return stripos($path, $this->themePath()) === 0 && stripos($path, 'sites/') === 0;
  }

  /**
   * Returns content of requested template.
   * If the template is not implemented by the theme
   * the content of the original template will be returned.
   *
   * @param string $template
   *   Template name.
   * @return string|false
   *   Returns the code of the template or FALSE if template could not be loaded.
   */
  public function getTemplateCode($template) {
    $info = $this->templateInfo($template);
    if (empty($info)) {
      return FALSE;
    }
    $path = isset($info['path']) ? $info['path'] . '/' . $template : $info['template'];
    $path .= '.html.twig';
    return @file_get_contents('.' . base_path() . '/' . $path);
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'theme_template_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, array &$form_state) {
    $templates = $this->templateList();
    $overridden = $this->overriddenTemplateList();

    if ($this->template) {
      $form['#title'] = $this->t('Template: @template', array('@template' => str_replace('_', '-', $this->template)));
    } else {
      $form['#title'] = $this->t('Create new template');
    }

    $form['left'] = array(
      '#type' => 'container',
      '#attributes' => array(
        'id' => 'template-edit-left'
      )
    );

    $form['left']['edit'] = array(
      '#type' => 'textarea',
      '#rows' => 35,
      '#default_value' => $this->template ? $this->getTemplateCode($this->template) : NULL,
      '#required' => TRUE
    );

    $form['left']['name_wrapper'] = array(
      '#type' => 'fieldset',
      '#title' => $this->t('Template name'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE
    );
    $form['left']['name_wrapper']['name'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Template name'),
      '#required' => !$this->template,
      '#field_prefix' => $this->template ? String::checkPlain(str_replace('_', '-', $this->template)) . '--' : NULL
    );

    $form['left']['actions'] = array(
      '#type' => 'actions',
      'save' => array(
        '#type' => 'submit',
        '#name' => 'save',
        '#value' => $this->t('Save')
      ),
      'delete' => array(
        '#type' => 'submit',
        '#name' => 'delete',
        '#value' => $this->t('Delete'),
        '#access' => $this->template && $this->templateIsOverridden($this->template)
      )
    );

    $form['right'] = array(
      '#type' => 'container',
      '#attributes' => array(
        'id' => 'template-edit-right'
      ),
      'templates' => array(
        '#theme' => 'item_list',
        '#items' => array()
      )
    );
    foreach ($overridden AS $template => $info) {
      $form['right']['templates']['#items'][$template] = array(
        '#type' => 'link',
        '#title' => str_replace('_', '-', $template),
        '#route_name' => 'mymodule.settings.theme.template',
        '#options' => array(
          'query' => array(
            'template' => str_replace('_', '-', $template)
          )
        ),
        '#attributes' => array(
          'class' => array('overridden')
        )
      );
    }
    foreach ($templates AS $template => $info) {
      if (isset($form['right']['templates']['#items'][$template])) {
        continue;
      }
      $form['right']['templates']['#items'][$template] = array(
        '#type' => 'link',
        '#title' => str_replace('_', '-', $template),
        '#route_name' => 'mymodule.settings.theme.template',
        '#options' => array(
          'query' => array(
            'template' => str_replace('_', '-', $template)
          )
        )
      );
    }

    if ($this->template && isset($form['right']['templates']['#items'][$this->template])) {
      $form['right']['templates']['#items'][$this->template]['#attributes']['class'][] = 'active';
    }

    $form['#attached'] = array(
      'css' => array(
        drupal_get_path('module', 'mymodule') . '/css/mymodule.themetemplateform.css.less'
      ),
      'library' => array(
        'mymodule/codemirror.twig'
      ),
      'js' => array(
        drupal_get_path('module', 'mymodule') . '/js/mymodule.themetemplateform.js'
      )
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, array &$form_state) {
    // If there is no template, the 'name' is required so we'll always end up with a value
    // for the name.
    $template = $this->template ? $this->template : '';
    if ((string) $form_state['values']['name'] !== '') {
      $template = (string) $template === '' ? $form_state['values']['name'] : $template . '__' . $form_state['values']['name'];
    }
    $template = str_replace('-', '_', $template);
    if (!$this->templateIsAllowed($template)) {
      $this->setFormError('name', $form_state, $this->t("Template name is not allowed."));
    } elseif (!$this->pathIsAllowed($this->getTemplateThemePath($template))) {
      $this->setFormError('name', $form_state, $this->t("Template cannot be changed."));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, array &$form_state) {
    // If there is no template, the 'name' is required so we'll always end up with a value
    // for the name.
    $template = $this->template ? $this->template : '';
    if ((string) $form_state['values']['name'] !== '') {
      $template = (string) $template === '' ? $form_state['values']['name'] : $template . '__' . $form_state['values']['name'];
    }
    $template = str_replace('-', '_', $template);

    $tplpath = $this->getTemplateThemePath($template);

    switch ($form_state['triggering_element']['#name']) {
      case 'save':
        $write = @file_put_contents('.' . base_path() . $tplpath, $form_state['values']['edit']);
        if ($write === FALSE) {
          $this->setFormError('edit', $form_state, $this->t("Couldn't write data into file. Please try again or contact help desk."));
        } else {
          $form_state['redirect_route'] = array(
            'route_name' => 'mymodule.settings.theme.template',
            'options' => array(
              'query' => array(
                'template' => str_replace('_', '-', $template)
              )
            )
          );
          mymodule_success_message();
        }
        break;

      case 'delete':
        $delete = @unlink('.' . base_path() . $tplpath);
        if ($delete) {
          $form_state['redirect_route'] = array(
            'route_name' => 'mymodule.settings.theme.template'
          );
          mymodule_success_message();
        } else {
          $this->setFormError('edit', $form_state, $this->t("Couldn't delete template. File does not exist or is not accessible."));
        }
        break;
    }

    // Always rebuild cache
    Cache::invalidateTags(array('theme_registry' => TRUE));
  }

}

Your Answer

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