You are here

class CriticalCssProvider in Critical CSS 8

Critical CSS Provider.

Calculate which CSS file must be used for Critical CSS based on current request (entity id, path, bundle name, etc).

Hierarchy

Expanded class hierarchy of CriticalCssProvider

1 file declares its use of CriticalCssProvider
CriticalCssProviderTest.php in tests/src/Unit/CriticalCssProviderTest.php
1 string reference to 'CriticalCssProvider'
critical_css.services.yml in ./critical_css.services.yml
critical_css.services.yml
1 service uses CriticalCssProvider
critical_css in ./critical_css.services.yml
Drupal\critical_css\Asset\CriticalCssProvider

File

src/Asset/CriticalCssProvider.php, line 20

Namespace

Drupal\critical_css\Asset
View source
class CriticalCssProvider implements CriticalCssProviderInterface {

  /**
   * Critical CSS config.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

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

  /**
   * Request.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * Current Route Match.
   *
   * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface
   */
  protected $currentRouteMatch;

  /**
   * Current path stack.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected $currentPathStack;

  /**
   * Current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Theme Manager.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected $themeManager;

  /**
   * The route admin context to determine whether a route is an admin one.
   *
   * @var \Drupal\Core\Routing\AdminContext
   */
  protected $adminContext;

  /**
   * Flag set when this request has already been processed.
   *
   * @var bool
   */
  protected $isAlreadyProcessed;

  /**
   * Critical CSS data to be inlined.
   *
   * @var string
   */
  protected $criticalCss;

  /**
   * Possible file paths to find CSS contents.
   *
   * @var array
   */
  protected $filePaths = [];

  /**
   * File used for critical CSS.
   *
   * @var string
   */
  protected $matchedFilePath;

  /**
   * CriticalCssProvider constructor.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   Request.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   Config Factory.
   * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $current_route_match
   *   Current route.
   * @param \Drupal\Core\Path\CurrentPathStack $current_path_stack
   *   Current path.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   Current user.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   *   The Theme Manager.
   * @param \Drupal\Core\Routing\AdminContext $admin_context
   *   The route admin context service.
   */
  public function __construct(ModuleHandlerInterface $module_handler, RequestStack $request_stack, ConfigFactoryInterface $config_factory, ResettableStackedRouteMatchInterface $current_route_match, CurrentPathStack $current_path_stack, AccountProxyInterface $current_user, ThemeManagerInterface $theme_manager, AdminContext $admin_context) {
    $this->moduleHandler = $module_handler;
    $this->request = $request_stack
      ->getCurrentRequest();
    $this->config = $config_factory
      ->get('critical_css.settings');
    $this->currentRouteMatch = $current_route_match;
    $this->currentPathStack = $current_path_stack;
    $this->currentUser = $current_user;
    $this->themeManager = $theme_manager;
    $this->adminContext = $admin_context;
  }

  /**
   * {@inheritdoc}
   */
  public function getCriticalCss() {

    // Return previous result, if any.
    if ($this->isAlreadyProcessed) {
      return $this->criticalCss;
    }
    $this->isAlreadyProcessed = TRUE;

    // Get possible file paths and return first match.
    $filePaths = $this
      ->getFilePaths();
    foreach ($filePaths as $filePath) {
      if (is_file($filePath)) {
        $this->matchedFilePath = $filePath;
        $this->criticalCss = trim(file_get_contents($filePath));
        break;
      }
    }
    return $this->criticalCss;
  }

  /**
   * {@inheritdoc}
   *
   * When accessing an admin route, this module is always disabled to avoid
   * multiple problems with Drupal's admin theme. On the other hand, this is a
   * module aimed to be used for a site's frontend, and not for the backend.
   */
  public function isEnabled() {
    $route = $this->currentRouteMatch
      ->getRouteObject();
    if ($route && $this->adminContext
      ->isAdminRoute($route)) {
      return FALSE;
    }
    return (bool) $this->config
      ->get('enabled');
  }

  /**
   * Check if module is enabled for logged-in users.
   *
   * @return bool
   *   True if this module is enabled for logged-in users.
   */
  protected function isEnabledForLoggedInUsers() {
    return (bool) $this->config
      ->get('enabled_for_logged_in_users');
  }

  /**
   * Check if entity id is excluded by configuration.
   *
   * @param int $entityId
   *   Entity ID (integer).
   *
   * @return bool
   *   True if entity is excluded.
   */
  protected function isEntityIdExcluded($entityId) {
    $excludedIds = explode("\n", $this->config
      ->get('excluded_ids'));
    $excludedIds = array_map(function ($item) {
      return trim($item);
    }, $excludedIds);
    return is_array($excludedIds) && in_array($entityId, $excludedIds);
  }

  /**
   * Get critical CSS file path by a key (id, string, etc).
   *
   * @param string $key
   *   Key to search.
   *
   * @return string
   *   Critical CSS string.
   */
  protected function getFilePathByKey($key) {
    if (empty($key)) {
      return NULL;
    }
    $criticalCssDirPath = str_replace('..', '', $this->config
      ->get('dir_path'));
    $themePath = $this->themeManager
      ->getActiveTheme()
      ->getPath();
    $criticalCssDir = $themePath . $criticalCssDirPath;
    return $criticalCssDir . '/' . $key . '.css';
  }

  /**
   * {@inheritdoc}
   */
  public function getFilePaths() {
    if (!$this->filePaths) {
      $this->filePaths = $this
        ->calculateFilePaths();
    }
    return $this->filePaths;
  }

  /**
   * Get all possible paths to search, relatives to theme.
   *
   * @return array
   *   Array with all possible paths.
   */
  protected function calculateFilePaths() {

    // Opt out if module is disabled.
    if (!$this
      ->isEnabled()) {
      return [];
    }

    // Check if module is enabled for logged-in users.
    if (!$this->currentUser
      ->isAnonymous() && !$this
      ->isEnabledForLoggedInUsers()) {
      return [];
    }

    // Get current entity's data.
    $entity = $this
      ->getCurrentEntity();
    $entityId = NULL;
    $bundleName = NULL;
    if (is_object($entity) && method_exists($entity, 'id') && method_exists($entity, 'bundle')) {
      $entityId = $entity
        ->id();
      $bundleName = $entity
        ->bundle();
    }

    // Check if this entity id is excluded.
    if ($entityId && $this
      ->isEntityIdExcluded($entityId)) {
      return [];
    }

    // Get sanitized path, which is something like /node/{X}.
    $sanitizedPath = $this
      ->sanitizePath($this->currentPathStack
      ->getPath());

    // Get sanitized path info, which is something like /article/{title}.
    $sanitizedPathInfo = $this
      ->sanitizePath($this->request
      ->getPathInfo());

    // Get all possible paths in order, starting with the most specific ones
    // (entity id) and finishing with a fallback. Between them, use a "path-"
    // prefix to avoid collisions when there is a node and a bundle with the
    // same name.
    $filePaths[] = $this
      ->getFilePathByKey($entityId);
    $filePaths[] = $this
      ->getFilePathByKey('path-' . $sanitizedPath);
    $filePaths[] = $this
      ->getFilePathByKey($sanitizedPath);
    $filePaths[] = $this
      ->getFilePathByKey('path-' . $sanitizedPathInfo);
    $filePaths[] = $this
      ->getFilePathByKey($sanitizedPathInfo);
    $filePaths[] = $this
      ->getFilePathByKey($bundleName);
    $filePaths[] = $this
      ->getFilePathByKey('default-critical');

    // Remove all null paths (if no callback is supplied to array_filter, all
    // entries of array equal to FALSE are removed)
    $filePaths = array_filter($filePaths);

    // Remove repeated paths.
    $filePaths = array_unique($filePaths);

    // Allow other modules to alter file paths array.
    $this->moduleHandler
      ->alter('critical_css_file_paths_suggestion', $filePaths, $entity);
    return $filePaths;
  }

  /**
   * Get current entity.
   *
   * At this moment, it only works for nodes and taxonomy terms.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface|null
   *   Matched file path, or null if nothing found.
   */
  protected function getCurrentEntity() {
    $entityBundles = [
      'node',
      'taxonomy_term',
    ];
    foreach ($entityBundles as $entityBundle) {
      $entity = $this->currentRouteMatch
        ->getParameter($entityBundle);
      if ($entity) {
        return $entity;
      }
    }
    return NULL;
  }

  /**
   * Sanitizes a path so its usable as a filename.
   *
   * @return string
   *   The sanitized path.
   */
  protected function sanitizePath($path) {
    $path = preg_replace("/^\\//", "", $path);
    $path = preg_replace("/[^a-zA-Z0-9\\/-]/", "", $path);
    $path = str_replace("/", "-", $path);
    if (empty($path)) {
      $path = 'front';
    }
    return $path;
  }

  /**
   * Get matched file path.
   *
   * @return string|null
   *   Matched file path, or null if nothing found.
   */
  public function getMatchedFilePath() {

    // Ensure $this->getCriticalCss() is called before returning anything.
    if (!$this->isAlreadyProcessed) {
      $this
        ->getCriticalCss();
    }
    return $this->matchedFilePath;
  }

  /**
   * {@inheritdoc}
   */
  public function isAlreadyProcessed() {
    return $this->isAlreadyProcessed;
  }

  /**
   * {@inheritdoc}
   */
  public function reset() {
    $this->isAlreadyProcessed = FALSE;
    $this->filePaths = [];
    $this->matchedFilePath = NULL;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CriticalCssProvider::$adminContext protected property The route admin context to determine whether a route is an admin one.
CriticalCssProvider::$config protected property Critical CSS config.
CriticalCssProvider::$criticalCss protected property Critical CSS data to be inlined.
CriticalCssProvider::$currentPathStack protected property Current path stack.
CriticalCssProvider::$currentRouteMatch protected property Current Route Match.
CriticalCssProvider::$currentUser protected property Current user.
CriticalCssProvider::$filePaths protected property Possible file paths to find CSS contents.
CriticalCssProvider::$isAlreadyProcessed protected property Flag set when this request has already been processed.
CriticalCssProvider::$matchedFilePath protected property File used for critical CSS.
CriticalCssProvider::$moduleHandler protected property The module handler.
CriticalCssProvider::$request protected property Request.
CriticalCssProvider::$themeManager protected property Theme Manager.
CriticalCssProvider::calculateFilePaths protected function Get all possible paths to search, relatives to theme.
CriticalCssProvider::getCriticalCss public function Get critical CSS contents. Overrides CriticalCssProviderInterface::getCriticalCss
CriticalCssProvider::getCurrentEntity protected function Get current entity.
CriticalCssProvider::getFilePathByKey protected function Get critical CSS file path by a key (id, string, etc).
CriticalCssProvider::getFilePaths public function Get all possible paths to search, relatives to theme. Overrides CriticalCssProviderInterface::getFilePaths
CriticalCssProvider::getMatchedFilePath public function Get matched file path. Overrides CriticalCssProviderInterface::getMatchedFilePath
CriticalCssProvider::isAlreadyProcessed public function Tells whether this request has been already processed. Overrides CriticalCssProviderInterface::isAlreadyProcessed
CriticalCssProvider::isEnabled public function When accessing an admin route, this module is always disabled to avoid multiple problems with Drupal's admin theme. On the other hand, this is a module aimed to be used for a site's frontend, and not for the backend. Overrides CriticalCssProviderInterface::isEnabled
CriticalCssProvider::isEnabledForLoggedInUsers protected function Check if module is enabled for logged-in users.
CriticalCssProvider::isEntityIdExcluded protected function Check if entity id is excluded by configuration.
CriticalCssProvider::reset public function Reset provider so calculations are made again. Overrides CriticalCssProviderInterface::reset
CriticalCssProvider::sanitizePath protected function Sanitizes a path so its usable as a filename.
CriticalCssProvider::__construct public function CriticalCssProvider constructor.