Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,154 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator.
*/
namespace Drupal\Core\Extension\Discovery;
/**
* Filters a RecursiveDirectoryIterator to discover extensions.
*
* To ensure the best possible performance for extension discovery, this
* filter implementation hard-codes a range of assumptions about directories
* in which Drupal extensions may appear and in which not. Every unnecessary
* subdirectory tree recursion is avoided.
*
* The list of globally ignored directory names is defined in the
* RecursiveExtensionFilterIterator::$blacklist property.
*
* In addition, all 'config' directories are skipped, unless the directory path
* ends with 'modules/config', so as to still find the config module provided by
* Drupal core and still allow that module to be overridden with a custom config
* module.
*
* Lastly, ExtensionDiscovery instructs this filter to additionally skip all
* 'tests' directories at regular runtime, since just with Drupal core only, the
* discovery process yields 4x more extensions when tests are not ignored.
*
* @see ExtensionDiscovery::scan()
* @see ExtensionDiscovery::scanDirectory()
*
* @todo Use RecursiveCallbackFilterIterator instead of the $acceptTests
* parameter forwarding once PHP 5.4 is available.
*/
class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
/**
* List of base extension type directory names to scan.
*
* Only these directory names are considered when starting a filesystem
* recursion in a search path.
*
* @var array
*/
protected $whitelist = array(
'profiles',
'modules',
'themes',
);
/**
* List of directory names to skip when recursing.
*
* These directories are globally ignored in the recursive filesystem scan;
* i.e., extensions (of all types) are not able to use any of these names,
* because their directory names will be skipped.
*
* @var array
*/
protected $blacklist = array(
// Object-oriented code subdirectories.
'src',
'lib',
'vendor',
// Front-end.
'assets',
'css',
'files',
'images',
'js',
'misc',
'templates',
// Legacy subdirectories.
'includes',
// Test subdirectories.
'fixtures',
// @todo ./tests/Drupal should be ./tests/src/Drupal
'Drupal',
);
/**
* Whether to include test directories when recursing.
*
* @var bool
*/
protected $acceptTests = FALSE;
/**
* Controls whether test directories will be scanned.
*
* @param bool $flag
* Pass FALSE to skip all test directories in the discovery. If TRUE,
* extensions in test directories will be discovered and only the global
* directory blacklist in RecursiveExtensionFilterIterator::$blacklist is
* applied.
*/
public function acceptTests($flag = FALSE) {
$this->acceptTests = $flag;
if (!$this->acceptTests) {
$this->blacklist[] = 'tests';
}
}
/**
* Overrides \RecursiveFilterIterator::getChildren().
*/
public function getChildren() {
$filter = parent::getChildren();
// Pass the $acceptTests flag forward to child iterators.
$filter->acceptTests($this->acceptTests);
return $filter;
}
/**
* Implements \FilterIterator::accept().
*/
public function accept() {
$name = $this->current()->getFilename();
// FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden
// directories (like '.git').
if ($name[0] == '.') {
return FALSE;
}
if ($this->isDir()) {
// If this is a subdirectory of a base search path, only recurse into the
// fixed list of expected extension type directory names. Required for
// scanning the top-level/root directory; without this condition, we would
// recurse into the whole filesystem tree that possibly contains other
// files aside from Drupal.
if ($this->current()->getSubPath() == '') {
return in_array($name, $this->whitelist, TRUE);
}
// 'config' directories are special-cased here, because every extension
// contains one. However, those default configuration directories cannot
// contain extensions. The directory name cannot be globally skipped,
// because core happens to have a directory of an actual module that is
// named 'config'. By explicitly testing for that case, we can skip all
// other config directories, and at the same time, still allow the core
// config module to be overridden/replaced in a profile/site directory
// (whereas it must be located directly in a modules directory).
if ($name == 'config') {
return substr($this->current()->getPathname(), -14) == 'modules/config';
}
// Accept the directory unless the name is blacklisted.
return !in_array($name, $this->blacklist, TRUE);
}
else {
// Only accept extension info files.
return substr($name, -9) == '.info.yml';
}
}
}

View file

@ -0,0 +1,205 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\Extension.
*/
namespace Drupal\Core\Extension;
/**
* Defines an extension (file) object.
*/
class Extension implements \Serializable {
/**
* The type of the extension (e.g., 'module').
*
* @var string
*/
protected $type;
/**
* The relative pathname of the extension (e.g., 'core/modules/node/node.info.yml').
*
* @var string
*/
protected $pathname;
/**
* The filename of the main extension file (e.g., 'node.module').
*
* @var string|null
*/
protected $filename;
/**
* An SplFileInfo instance for the extension's info file.
*
* Note that SplFileInfo is a PHP resource and resources cannot be serialized.
*
* @var \SplFileInfo
*/
protected $splFileInfo;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new Extension object.
*
* @param string $root
* The app root.
* @param string $type
* The type of the extension; e.g., 'module'.
* @param string $pathname
* The relative path and filename of the extension's info file; e.g.,
* 'core/modules/node/node.info.yml'.
* @param string $filename
* (optional) The filename of the main extension file; e.g., 'node.module'.
*/
public function __construct($root, $type, $pathname, $filename = NULL) {
$this->root = $root;
$this->type = $type;
$this->pathname = $pathname;
$this->filename = $filename;
}
/**
* Returns the type of the extension.
*
* @return string
*/
public function getType() {
return $this->type;
}
/**
* Returns the internal name of the extension.
*
* @return string
*/
public function getName() {
return basename($this->pathname, '.info.yml');
}
/**
* Returns the relative path of the extension.
*
* @return string
*/
public function getPath() {
return dirname($this->pathname);
}
/**
* Returns the relative path and filename of the extension's info file.
*
* @return string
*/
public function getPathname() {
return $this->pathname;
}
/**
* Returns the filename of the extension's info file.
*
* @return string
*/
public function getFilename() {
return basename($this->pathname);
}
/**
* Returns the relative path of the main extension file, if any.
*
* @return string|null
*/
public function getExtensionPathname() {
if ($this->filename) {
return $this->getPath() . '/' . $this->filename;
}
}
/**
* Returns the name of the main extension file, if any.
*
* @return string|null
*/
public function getExtensionFilename() {
return $this->filename;
}
/**
* Loads the main extension file, if any.
*
* @return bool
* TRUE if this extension has a main extension file, FALSE otherwise.
*/
public function load() {
if ($this->filename) {
include_once $this->root . '/' . $this->getPath() . '/' . $this->filename;
return TRUE;
}
return FALSE;
}
/**
* Re-routes method calls to SplFileInfo.
*
* Offers all SplFileInfo methods to consumers; e.g., $extension->getMTime().
*/
public function __call($method, array $args) {
if (!isset($this->splFileInfo)) {
$this->splFileInfo = new \SplFileInfo($this->pathname);
}
return call_user_func_array(array($this->splFileInfo, $method), $args);
}
/**
* Implements Serializable::serialize().
*
* Serializes the Extension object in the most optimized way.
*/
public function serialize() {
$data = array(
'root' => $this->root,
'type' => $this->type,
'pathname' => $this->pathname,
'filename' => $this->filename,
);
// @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and
// system_list() are adding custom properties to the Extension object.
$info = new \ReflectionObject($this);
foreach ($info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$data[$property->getName()] = $property->getValue($this);
}
return serialize($data);
}
/**
* Implements Serializable::unserialize().
*/
public function unserialize($data) {
$data = unserialize($data);
$this->root = $data['root'];
$this->type = $data['type'];
$this->pathname = $data['pathname'];
$this->filename = $data['filename'];
// @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and
// system_list() are adding custom properties to the Extension object.
foreach ($data as $property => $value) {
if (!isset($this->$property)) {
$this->$property = $value;
}
}
}
}

View file

@ -0,0 +1,488 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ExtensionDiscovery.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\Request;
/**
* Discovers available extensions in the filesystem.
*/
class ExtensionDiscovery {
/**
* Origin directory weight: Core.
*/
const ORIGIN_CORE = 0;
/**
* Origin directory weight: Installation profile.
*/
const ORIGIN_PROFILE = 1;
/**
* Origin directory weight: sites/all.
*/
const ORIGIN_SITES_ALL = 2;
/**
* Origin directory weight: Site-wide directory.
*/
const ORIGIN_ROOT = 3;
/**
* Origin directory weight: Parent site directory of a test site environment.
*/
const ORIGIN_PARENT_SITE = 4;
/**
* Origin directory weight: Site-specific directory.
*/
const ORIGIN_SITE = 5;
/**
* Regular expression to match PHP function names.
*
* @see http://php.net/manual/functions.user-defined.php
*/
const PHP_FUNCTION_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
/**
* InfoParser instance for parsing .info.yml files.
*
* @var \Drupal\Core\Extension\InfoParser
*/
protected $infoParser;
/**
* Previously discovered files keyed by origin directory and extension type.
*
* @var array
*/
protected static $files = array();
/**
* List of installation profile directories to additionally scan.
*
* @var array
*/
protected $profileDirectories;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* The file cache object.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected $fileCache;
/**
* Constructs a new ExtensionDiscovery object.
*
* @param string $root
* The app root.
*/
public function __construct($root) {
$this->root = $root;
$this->fileCache = FileCacheFactory::get('extension_discovery');
}
/**
* Discovers available extensions of a given type.
*
* Finds all extensions (modules, themes, etc) that exist on the site. It
* searches in several locations. For instance, to discover all available
* modules:
* @code
* $listing = new ExtensionDiscovery(\Drupal::root());
* $modules = $listing->scan('module');
* @endcode
*
* The following directories will be searched (in the order stated):
* - the core directory; i.e., /core
* - the installation profile directory; e.g., /core/profiles/standard
* - the legacy site-wide directory; i.e., /sites/all
* - the site-wide directory; i.e., /
* - the site-specific directory; e.g., /sites/example.com
*
* The information is returned in an associative array, keyed by the extension
* name (without .info.yml extension). Extensions found later in the search
* will take precedence over extensions found earlier - unless they are not
* compatible with the current version of Drupal core.
*
* @param string $type
* The extension type to search for. One of 'profile', 'module', 'theme', or
* 'theme_engine'.
* @param bool $include_tests
* (optional) Whether to explicitly include or exclude test extensions. By
* default, test extensions are only discovered when in a test environment.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array of Extension objects, keyed by extension name.
*/
public function scan($type, $include_tests = NULL) {
// Determine the installation profile directories to scan for extensions,
// unless explicit profile directories have been set. Exclude profiles as we
// cannot have profiles within profiles.
if (!isset($this->profileDirectories) && $type != 'profile') {
$this->setProfileDirectoriesFromSettings();
}
// Search the core directory.
$searchdirs[static::ORIGIN_CORE] = 'core';
// Search the legacy sites/all directory.
$searchdirs[static::ORIGIN_SITES_ALL] = 'sites/all';
// Search for contributed and custom extensions in top-level directories.
// The scan uses a whitelist to limit recursion to the expected extension
// type specific directory names only.
$searchdirs[static::ORIGIN_ROOT] = '';
// Simpletest uses the regular built-in multi-site functionality of Drupal
// for running web tests. As a consequence, extensions of the parent site
// located in a different site-specific directory are not discovered in a
// test site environment, because the site directories are not the same.
// Therefore, add the site directory of the parent site to the search paths,
// so that contained extensions are still discovered.
// @see \Drupal\simpletest\WebTestBase::setUp()
if ($parent_site = Settings::get('test_parent_site')) {
$searchdirs[static::ORIGIN_PARENT_SITE] = $parent_site;
}
// Find the site-specific directory to search. Since we are using this
// method to discover extensions including profiles, we might be doing this
// at install time. Therefore Kernel service is not always available, but is
// preferred.
if (\Drupal::hasService('kernel')) {
$searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
}
else {
$searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
}
// Unless an explicit value has been passed, manually check whether we are
// in a test environment, in which case test extensions must be included.
// Test extensions can also be included for debugging purposes by setting a
// variable in settings.php.
if (!isset($include_tests)) {
$include_tests = drupal_valid_test_ua() || Settings::get('extension_discovery_scan_tests');
}
$files = array();
foreach ($searchdirs as $dir) {
// Discover all extensions in the directory, unless we did already.
if (!isset(static::$files[$dir][$include_tests])) {
static::$files[$dir][$include_tests] = $this->scanDirectory($dir, $include_tests);
}
// Only return extensions of the requested type.
if (isset(static::$files[$dir][$include_tests][$type])) {
$files += static::$files[$dir][$include_tests][$type];
}
}
// If applicable, filter out extensions that do not belong to the current
// installation profiles.
$files = $this->filterByProfileDirectories($files);
// Sort the discovered extensions by their originating directories.
$origin_weights = array_flip($searchdirs);
$files = $this->sort($files, $origin_weights);
// Process and return the list of extensions keyed by extension name.
return $this->process($files);
}
/**
* Sets installation profile directories based on current site settings.
*
* @return $this
*/
public function setProfileDirectoriesFromSettings() {
$this->profileDirectories = array();
$profile = drupal_get_profile();
// For SimpleTest to be able to test modules packaged together with a
// distribution we need to include the profile of the parent site (in
// which test runs are triggered).
if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
$testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
if ($testing_profile && $testing_profile != $profile) {
$this->profileDirectories[] = drupal_get_path('profile', $testing_profile);
}
}
// In case both profile directories contain the same extension, the actual
// profile always has precedence.
if ($profile) {
$this->profileDirectories[] = drupal_get_path('profile', $profile);
}
return $this;
}
/**
* Gets the installation profile directories to be scanned.
*
* @return array
* A list of installation profile directory paths relative to the system
* root directory.
*/
public function getProfileDirectories() {
return $this->profileDirectories;
}
/**
* Sets explicit profile directories to scan.
*
* @param array $paths
* A list of installation profile directory paths relative to the system
* root directory (without trailing slash) to search for extensions.
*
* @return $this
*/
public function setProfileDirectories(array $paths = NULL) {
$this->profileDirectories = $paths;
return $this;
}
/**
* Filters out extensions not belonging to the scanned installation profiles.
*
* @param \Drupal\Core\Extension\Extension[] $all_files.
* The list of all extensions.
*
* @return \Drupal\Core\Extension\Extension[]
* The filtered list of extensions.
*/
protected function filterByProfileDirectories(array $all_files) {
if (empty($this->profileDirectories)) {
return $all_files;
}
$all_files = array_filter($all_files, function ($file) {
if (strpos($file->subpath, 'profiles') !== 0) {
// This extension doesn't belong to a profile, ignore it.
return TRUE;
}
foreach ($this->profileDirectories as $weight => $profile_path) {
if (strpos($file->getPath(), $profile_path) === 0) {
// Parent profile found.
return TRUE;
}
}
return FALSE;
});
return $all_files;
}
/**
* Sorts the discovered extensions.
*
* @param \Drupal\Core\Extension\Extension[] $all_files.
* The list of all extensions.
* @param array $weights
* An array of weights, keyed by originating directory.
*
* @return \Drupal\Core\Extension\Extension[]
* The sorted list of extensions.
*/
protected function sort(array $all_files, array $weights) {
$origins = array();
$profiles = array();
foreach ($all_files as $key => $file) {
// If the extension does not belong to a profile, just apply the weight
// of the originating directory.
if (strpos($file->subpath, 'profiles') !== 0) {
$origins[$key] = $weights[$file->origin];
$profiles[$key] = NULL;
}
// If the extension belongs to a profile but no profile directories are
// defined, then we are scanning for installation profiles themselves.
// In this case, profiles are sorted by origin only.
elseif (empty($this->profileDirectories)) {
$origins[$key] = static::ORIGIN_PROFILE;
$profiles[$key] = NULL;
}
else {
// Apply the weight of the originating profile directory.
foreach ($this->profileDirectories as $weight => $profile_path) {
if (strpos($file->getPath(), $profile_path) === 0) {
$origins[$key] = static::ORIGIN_PROFILE;
$profiles[$key] = $weight;
continue 2;
}
}
}
}
// Now sort the extensions by origin and installation profile(s).
// The result of this multisort can be depicted like the following matrix,
// whereas the first integer is the weight of the originating directory and
// the second is the weight of the originating installation profile:
// 0 core/modules/node/node.module
// 1 0 profiles/parent_profile/modules/parent_module/parent_module.module
// 1 1 core/profiles/testing/modules/compatible_test/compatible_test.module
// 2 sites/all/modules/common/common.module
// 3 modules/devel/devel.module
// 4 sites/default/modules/custom/custom.module
array_multisort($origins, SORT_ASC, $profiles, SORT_ASC, $all_files);
return $all_files;
}
/**
* Processes the filtered and sorted list of extensions.
*
* Extensions discovered in later search paths override earlier, unless they
* are not compatible with the current version of Drupal core.
*
* @param \Drupal\Core\Extension\Extension[] $all_files
* The sorted list of all extensions that were found.
*
* @return \Drupal\Core\Extension\Extension[]
* The filtered list of extensions, keyed by extension name.
*/
protected function process(array $all_files) {
$files = array();
// Duplicate files found in later search directories take precedence over
// earlier ones; they replace the extension in the existing $files array.
foreach ($all_files as $file) {
$files[$file->getName()] = $file;
}
return $files;
}
/**
* Recursively scans a base directory for the requested extension type.
*
* @param string $dir
* A relative base directory path to scan, without trailing slash.
* @param bool $include_tests
* Whether to include test extensions. If FALSE, all 'tests' directories are
* excluded in the search.
*
* @return array
* An associative array whose keys are extension type names and whose values
* are associative arrays of \Drupal\Core\Extension\Extension objects, keyed
* by absolute path name.
*
* @see \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator
*/
protected function scanDirectory($dir, $include_tests) {
$files = array();
// In order to scan top-level directories, absolute directory paths have to
// be used (which also improves performance, since any configured PHP
// include_paths will not be consulted). Retain the relative originating
// directory being scanned, so relative paths can be reconstructed below
// (all paths are expected to be relative to $this->root).
$dir_prefix = ($dir == '' ? '' : "$dir/");
$absolute_dir = ($dir == '' ? $this->root : $this->root . "/$dir");
if (!is_dir($absolute_dir)) {
return $files;
}
// Use Unix paths regardless of platform, skip dot directories, follow
// symlinks (to allow extensions to be linked from elsewhere), and return
// the RecursiveDirectoryIterator instance to have access to getSubPath(),
// since SplFileInfo does not support relative paths.
$flags = \FilesystemIterator::UNIX_PATHS;
$flags |= \FilesystemIterator::SKIP_DOTS;
$flags |= \FilesystemIterator::FOLLOW_SYMLINKS;
$flags |= \FilesystemIterator::CURRENT_AS_SELF;
$directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags);
// Filter the recursive scan to discover extensions only.
// Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator
// would recurse into the entire filesystem directory tree without any kind
// of limitations.
$filter = new RecursiveExtensionFilterIterator($directory_iterator);
$filter->acceptTests($include_tests);
// The actual recursive filesystem scan is only invoked by instantiating the
// RecursiveIteratorIterator.
$iterator = new \RecursiveIteratorIterator($filter,
\RecursiveIteratorIterator::LEAVES_ONLY,
// Suppress filesystem errors in case a directory cannot be accessed.
\RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $key => $fileinfo) {
// All extension names in Drupal have to be valid PHP function names due
// to the module hook architecture.
if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) {
continue;
}
if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
$files[$cached_extension->getType()][$key] = $cached_extension;
continue;
}
// Determine extension type from info file.
$type = FALSE;
$file = $fileinfo->openFile('r');
while (!$type && !$file->eof()) {
preg_match('@^type:\s*(\'|")?(\w+)\1?\s*$@', $file->fgets(), $matches);
if (isset($matches[2])) {
$type = $matches[2];
}
}
if (empty($type)) {
continue;
}
$name = $fileinfo->getBasename('.info.yml');
$pathname = $dir_prefix . $fileinfo->getSubPathname();
// Determine whether the extension has a main extension file.
// For theme engines, the file extension is .engine.
if ($type == 'theme_engine') {
$filename = $name . '.engine';
}
// For profiles/modules/themes, it is the extension type.
else {
$filename = $name . '.' . $type;
}
if (!file_exists(dirname($pathname) . '/' . $filename)) {
$filename = NULL;
}
$extension = new Extension($this->root, $type, $pathname, $filename);
// Track the originating directory for sorting purposes.
$extension->subpath = $fileinfo->getSubPath();
$extension->origin = $dir;
$files[$type][$key] = $extension;
$this->fileCache->set($fileinfo->getPathName(), $extension);
}
return $files;
}
/**
* Returns a parser for .info.yml files.
*
* @return \Drupal\Core\Extension\InfoParser
* The InfoParser instance.
*/
protected function getInfoParser() {
if (!isset($this->infoParser)) {
$this->infoParser = new InfoParser();
}
return $this->infoParser;
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ExtensionNameLengthException.
*/
namespace Drupal\Core\Extension;
/**
* Exception thrown when the extension's name length exceeds the allowed maximum.
*/
class ExtensionNameLengthException extends \Exception { }

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\InfoParser.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Utility\SafeMarkup;
/**
* Parses extension .info.yml files.
*/
class InfoParser implements InfoParserInterface {
/**
* Array of all info keyed by filename.
*
* @var array
*/
protected static $parsedInfos = array();
/**
* {@inheritdoc}
*/
public function parse($filename) {
if (!isset(static::$parsedInfos[$filename])) {
if (!file_exists($filename)) {
static::$parsedInfos[$filename] = array();
}
else {
try {
static::$parsedInfos[$filename] = Yaml::decode(file_get_contents($filename));
}
catch (InvalidDataTypeException $e) {
$message = SafeMarkup::format("Unable to parse !file: !error", array('!file' => $filename, '!error' => $e->getMessage()));
throw new InfoParserException($message);
}
$missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename]));
if (!empty($missing_keys)) {
$message = SafeMarkup::format('Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename));
throw new InfoParserException($message);
}
if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') {
static::$parsedInfos[$filename]['version'] = \Drupal::VERSION;
}
}
}
return static::$parsedInfos[$filename];
}
/**
* Returns an array of keys required to exist in .info.yml file.
*
* @return array
* An array of required keys.
*/
protected function getRequiredKeys() {
return array('type', 'core', 'name');
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\InfoParserException.
*/
namespace Drupal\Core\Extension;
/**
* An exception thrown by the InfoParser class whilst parsing info.yml files.
*/
class InfoParserException extends \RuntimeException {
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\InfoParserInterface.
*/
namespace Drupal\Core\Extension;
/**
* Interface for classes that parses Drupal's info.yml files.
*/
interface InfoParserInterface {
/**
* Parses Drupal module, theme and profile .info.yml files.
*
* Info files are NOT for placing arbitrary theme and module-specific
* settings. Use Config::get() and Config::set()->save() for that. Info files
* are formatted as YAML. If the 'version' key is set to 'VERSION' in any info
* file, then the value will be substituted with the current version of Drupal
* core.
*
* Information stored in all .info.yml files:
* - name: The real name of the module for display purposes. (Required)
* - description: A brief description of the module.
* - type: whether it is for a module or theme. (Required)
*
* Information stored in a module .info.yml file:
* - dependencies: An array of dependency strings. Each is in the form
* 'project:module (versions)'; with the following meanings:
* - project: (optional) Project shortname, recommended to ensure
* uniqueness, if the module is part of a project hosted on drupal.org.
* If omitted, also omit the : that follows. The project name is currently
* ignored by Drupal core but is used for automated testing.
* - module: (required) Module shortname within the project.
* - (versions): Version information, consisting of one or more
* comma-separated operator/value pairs or simply version numbers, which
* can contain "x" as a wildcard. Examples: (>=8.22, <8.28), (8.x-3.x).
* - package: The name of the package of modules this module belongs to.
*
* See forum.info.yml for an example of a module .info.yml file.
*
* Information stored in a theme .info.yml file:
* - screenshot: Path to screenshot relative to the theme's .info.yml file.
* - engine: Theme engine; typically twig.
* - base theme: Name of a base theme, if applicable.
* - regions: Listed regions.
* - features: Features available.
* - stylesheets: Theme stylesheets.
* - scripts: Theme scripts.
*
* See bartik.info.yml for an example of a theme .info.yml file.
*
* @param string $filename
* The file we are parsing. Accepts file with relative or absolute path.
*
* @return array
* The info array.
*
* @throws \Drupal\Core\Extension\InfoParserException
* Exception thrown if there is a parsing error or the .info.yml file does
* not contain a required key.
*/
public function parse($filename);
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\MissingDependencyException.
*/
namespace Drupal\Core\Extension;
/**
* Exception class to throw when modules are missing on install.
*
* @see \Drupal\Core\Extension\ModuleInstaller::install()
*/
class MissingDependencyException extends \Exception {}

View file

@ -0,0 +1,717 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleHandler.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
/**
* Class that manages modules in a Drupal installation.
*/
class ModuleHandler implements ModuleHandlerInterface {
/**
* List of loaded files.
*
* @var array
* An associative array whose keys are file paths of loaded files, relative
* to the application's root directory.
*/
protected $loadedFiles;
/**
* List of installed modules.
*
* @var \Drupal\Core\Extension\Extension[]
*/
protected $moduleList;
/**
* Boolean indicating whether modules have been loaded.
*
* @var bool
*/
protected $loaded = FALSE;
/**
* List of hook implementations keyed by hook name.
*
* @var array
*/
protected $implementations;
/**
* List of hooks where the implementations have been "verified".
*
* @var true[]
* Associative array where keys are hook names.
*/
protected $verified;
/**
* Information returned by hook_hook_info() implementations.
*
* @var array
*/
protected $hookInfo;
/**
* Cache backend for storing module hook implementation information.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Whether the cache needs to be written.
*
* @var bool
*/
protected $cacheNeedsWriting = FALSE;
/**
* List of alter hook implementations keyed by hook name(s).
*
* @var array
*/
protected $alterFunctions;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a ModuleHandler object.
*
* @param string $root
* The app root.
* @param array $module_list
* An associative array whose keys are the names of installed modules and
* whose values are Extension class parameters. This is normally the
* %container.modules% parameter being set up by DrupalKernel.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend for storing module hook implementation information.
*
* @see \Drupal\Core\DrupalKernel
* @see \Drupal\Core\CoreServiceProvider
*/
public function __construct($root, array $module_list = array(), CacheBackendInterface $cache_backend) {
$this->root = $root;
$this->moduleList = array();
foreach ($module_list as $name => $module) {
$this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
}
$this->cacheBackend = $cache_backend;
}
/**
* {@inheritdoc}
*/
public function load($name) {
if (isset($this->loadedFiles[$name])) {
return TRUE;
}
if (isset($this->moduleList[$name])) {
$this->moduleList[$name]->load();
$this->loadedFiles[$name] = TRUE;
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function loadAll() {
if (!$this->loaded) {
foreach ($this->moduleList as $name => $module) {
$this->load($name);
}
$this->loaded = TRUE;
}
}
/**
* {@inheritdoc}
*/
public function reload() {
$this->loaded = FALSE;
$this->loadAll();
}
/**
* {@inheritdoc}
*/
public function isLoaded() {
return $this->loaded;
}
/**
* {@inheritdoc}
*/
public function getModuleList() {
return $this->moduleList;
}
/**
* {@inheritdoc}
*/
public function getModule($name) {
if (isset($this->moduleList[$name])) {
return $this->moduleList[$name];
}
throw new \InvalidArgumentException(sprintf('The module %s does not exist.', $name));
}
/**
* {@inheritdoc}
*/
public function setModuleList(array $module_list = array()) {
$this->moduleList = $module_list;
// Reset the implementations, so a new call triggers a reloading of the
// available hooks.
$this->resetImplementations();
}
/**
* {@inheritdoc}
*/
public function addModule($name, $path) {
$this->add('module', $name, $path);
}
/**
* {@inheritdoc}
*/
public function addProfile($name, $path) {
$this->add('profile', $name, $path);
}
/**
* Adds a module or profile to the list of currently active modules.
*
* @param string $type
* The extension type; either 'module' or 'profile'.
* @param string $name
* The module name; e.g., 'node'.
* @param string $path
* The module path; e.g., 'core/modules/node'.
*/
protected function add($type, $name, $path) {
$pathname = "$path/$name.info.yml";
$filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
$this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
$this->resetImplementations();
}
/**
* {@inheritdoc}
*/
public function buildModuleDependencies(array $modules) {
foreach ($modules as $module) {
$graph[$module->getName()]['edges'] = array();
if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
foreach ($module->info['dependencies'] as $dependency) {
$dependency_data = static::parseDependency($dependency);
$graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
}
}
}
$graph_object = new Graph($graph);
$graph = $graph_object->searchAndSort();
foreach ($graph as $module_name => $data) {
$modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : array();
$modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : array();
$modules[$module_name]->sort = $data['weight'];
}
return $modules;
}
/**
* {@inheritdoc}
*/
public function moduleExists($module) {
return isset($this->moduleList[$module]);
}
/**
* {@inheritdoc}
*/
public function loadAllIncludes($type, $name = NULL) {
foreach ($this->moduleList as $module => $filename) {
$this->loadInclude($module, $type, $name);
}
}
/**
* {@inheritdoc}
*/
public function loadInclude($module, $type, $name = NULL) {
if ($type == 'install') {
// Make sure the installation API is available
include_once $this->root . '/core/includes/install.inc';
}
$name = $name ?: $module;
if (isset($this->moduleList[$module])) {
$file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
if (is_file($file)) {
require_once $file;
return $file;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getHookInfo() {
if (!isset($this->hookInfo)) {
if ($cache = $this->cacheBackend->get('hook_info')) {
$this->hookInfo = $cache->data;
}
else {
$this->buildHookInfo();
$this->cacheBackend->set('hook_info', $this->hookInfo);
}
}
return $this->hookInfo;
}
/**
* Builds hook_hook_info() information.
*
* @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
*/
protected function buildHookInfo() {
$this->hookInfo = array();
// Make sure that the modules are loaded before checking.
$this->reload();
// $this->invokeAll() would cause an infinite recursion.
foreach ($this->moduleList as $module => $filename) {
$function = $module . '_hook_info';
if (function_exists($function)) {
$result = $function();
if (isset($result) && is_array($result)) {
$this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
}
}
}
}
/**
* {@inheritdoc}
*/
public function getImplementations($hook) {
$implementations = $this->getImplementationInfo($hook);
return array_keys($implementations);
}
/**
* {@inheritdoc}
*/
public function writeCache() {
if ($this->cacheNeedsWriting) {
$this->cacheBackend->set('module_implements', $this->implementations);
$this->cacheNeedsWriting = FALSE;
}
}
/**
* {@inheritdoc}
*/
public function resetImplementations() {
$this->implementations = NULL;
$this->hookInfo = NULL;
$this->alterFunctions = NULL;
// We maintain a persistent cache of hook implementations in addition to the
// static cache to avoid looping through every module and every hook on each
// request. Benchmarks show that the benefit of this caching outweighs the
// additional database hit even when using the default database caching
// backend and only a small number of modules are enabled. The cost of the
// $this->cacheBackend->get() is more or less constant and reduced further
// when non-database caching backends are used, so there will be more
// significant gains when a large number of modules are installed or hooks
// invoked, since this can quickly lead to
// \Drupal::moduleHandler()->implementsHook() being called several thousand
// times per request.
$this->cacheBackend->set('module_implements', array());
$this->cacheBackend->delete('hook_info');
}
/**
* {@inheritdoc}
*/
public function implementsHook($module, $hook) {
$function = $module . '_' . $hook;
if (function_exists($function)) {
return TRUE;
}
// If the hook implementation does not exist, check whether it lives in an
// optional include file registered via hook_hook_info().
$hook_info = $this->getHookInfo();
if (isset($hook_info[$hook]['group'])) {
$this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
if (function_exists($function)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function invoke($module, $hook, array $args = array()) {
if (!$this->implementsHook($module, $hook)) {
return;
}
$function = $module . '_' . $hook;
return call_user_func_array($function, $args);
}
/**
* {@inheritdoc}
*/
public function invokeAll($hook, array $args = array()) {
$return = array();
$implementations = $this->getImplementations($hook);
foreach ($implementations as $module) {
$function = $module . '_' . $hook;
$result = call_user_func_array($function, $args);
if (isset($result) && is_array($result)) {
$return = NestedArray::mergeDeep($return, $result);
}
elseif (isset($result)) {
$return[] = $result;
}
}
return $return;
}
/**
* {@inheritdoc}
*/
public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
// Most of the time, $type is passed as a string, so for performance,
// normalize it to that. When passed as an array, usually the first item in
// the array is a generic type, and additional items in the array are more
// specific variants of it, as in the case of array('form', 'form_FORM_ID').
if (is_array($type)) {
$cid = implode(',', $type);
$extra_types = $type;
$type = array_shift($extra_types);
// Allow if statements in this function to use the faster isset() rather
// than !empty() both when $type is passed as a string, or as an array
// with one item.
if (empty($extra_types)) {
unset($extra_types);
}
}
else {
$cid = $type;
}
// Some alter hooks are invoked many times per page request, so store the
// list of functions to call, and on subsequent calls, iterate through them
// quickly.
if (!isset($this->alterFunctions[$cid])) {
$this->alterFunctions[$cid] = array();
$hook = $type . '_alter';
$modules = $this->getImplementations($hook);
if (!isset($extra_types)) {
// For the more common case of a single hook, we do not need to call
// function_exists(), since $this->getImplementations() returns only
// modules with implementations.
foreach ($modules as $module) {
$this->alterFunctions[$cid][] = $module . '_' . $hook;
}
}
else {
// For multiple hooks, we need $modules to contain every module that
// implements at least one of them.
$extra_modules = array();
foreach ($extra_types as $extra_type) {
$extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
}
// If any modules implement one of the extra hooks that do not implement
// the primary hook, we need to add them to the $modules array in their
// appropriate order. $this->getImplementations() can only return
// ordered implementations of a single hook. To get the ordered
// implementations of multiple hooks, we mimic the
// $this->getImplementations() logic of first ordering by
// $this->getModuleList(), and then calling
// $this->alter('module_implements').
if (array_diff($extra_modules, $modules)) {
// Merge the arrays and order by getModuleList().
$modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
// Since $this->getImplementations() already took care of loading the
// necessary include files, we can safely pass FALSE for the array
// values.
$implementations = array_fill_keys($modules, FALSE);
// Let modules adjust the order solely based on the primary hook. This
// ensures the same module order regardless of whether this if block
// runs. Calling $this->alter() recursively in this way does not
// result in an infinite loop, because this call is for a single
// $type, so we won't end up in this code block again.
$this->alter('module_implements', $implementations, $hook);
$modules = array_keys($implementations);
}
foreach ($modules as $module) {
// Since $modules is a merged array, for any given module, we do not
// know whether it has any particular implementation, so we need a
// function_exists().
$function = $module . '_' . $hook;
if (function_exists($function)) {
$this->alterFunctions[$cid][] = $function;
}
foreach ($extra_types as $extra_type) {
$function = $module . '_' . $extra_type . '_alter';
if (function_exists($function)) {
$this->alterFunctions[$cid][] = $function;
}
}
}
}
}
foreach ($this->alterFunctions[$cid] as $function) {
$function($data, $context1, $context2);
}
}
/**
* Provides information about modules' implementations of a hook.
*
* @param string $hook
* The name of the hook (e.g. "help" or "menu").
*
* @return mixed[]
* An array whose keys are the names of the modules which are implementing
* this hook and whose values are either a string identifying a file in
* which the implementation is to be found, or FALSE, if the implementation
* is in the module file.
*/
protected function getImplementationInfo($hook) {
if (!isset($this->implementations)) {
$this->implementations = array();
$this->verified = array();
if ($cache = $this->cacheBackend->get('module_implements')) {
$this->implementations = $cache->data;
}
}
if (!isset($this->implementations[$hook])) {
// The hook is not cached, so ensure that whether or not it has
// implementations, the cache is updated at the end of the request.
$this->cacheNeedsWriting = TRUE;
// Discover implementations.
$this->implementations[$hook] = $this->buildImplementationInfo($hook);
// Implementations are always "verified" as part of the discovery.
$this->verified[$hook] = TRUE;
}
elseif (!isset($this->verified[$hook])) {
if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
// One or more of the implementations did not exist and need to be
// removed in the cache.
$this->cacheNeedsWriting = TRUE;
}
$this->verified[$hook] = TRUE;
}
return $this->implementations[$hook];
}
/**
* Builds hook implementation information for a given hook name.
*
* @param string $hook
* The name of the hook (e.g. "help" or "menu").
*
* @return mixed[]
* An array whose keys are the names of the modules which are implementing
* this hook and whose values are either a string identifying a file in
* which the implementation is to be found, or FALSE, if the implementation
* is in the module file.
*
* @throws \RuntimeException
* Exception thrown when an invalid implementation is added by
* hook_module_implements_alter().
*
* @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
*/
protected function buildImplementationInfo($hook) {
$implementations = array();
$hook_info = $this->getHookInfo();
foreach ($this->moduleList as $module => $extension) {
$include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
// Since $this->implementsHook() may needlessly try to load the include
// file again, function_exists() is used directly here.
if (function_exists($module . '_' . $hook)) {
$implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
}
}
// Allow modules to change the weight of specific implementations, but avoid
// an infinite loop.
if ($hook != 'module_implements_alter') {
// Remember the original implementations, before they are modified with
// hook_module_implements_alter().
$implementations_before = $implementations;
// Verify implementations that were added or modified.
$this->alter('module_implements', $implementations, $hook);
// Verify new or modified implementations.
foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
// If drupal_alter('module_implements') changed or added a $group, the
// respective file needs to be included.
if ($group) {
$this->loadInclude($module, 'inc', "$module.$group");
}
// If a new implementation was added, verify that the function exists.
if (!function_exists($module . '_' . $hook)) {
throw new \RuntimeException(SafeMarkup::format('An invalid implementation @function was added by hook_module_implements_alter()', array('@function' => $module . '_' . $hook)));
}
}
}
return $implementations;
}
/**
* Verifies an array of implementations loaded from the cache, by including
* the lazy-loaded $module.$group.inc, and checking function_exists().
*
* @param string[] $implementations
* Implementation "group" by module name.
* @param string $hook
* The hook name.
*
* @return bool
* TRUE, if all implementations exist.
* FALSE, if one or more implementations don't exist and need to be removed
* from the cache.
*/
protected function verifyImplementations(&$implementations, $hook) {
$all_valid = TRUE;
foreach ($implementations as $module => $group) {
// If this hook implementation is stored in a lazy-loaded file, include
// that file first.
if ($group) {
$this->loadInclude($module, 'inc', "$module.$group");
}
// It is possible that a module removed a hook implementation without
// the implementations cache being rebuilt yet, so we check whether the
// function exists on each request to avoid undefined function errors.
// Since ModuleHandler::implementsHook() may needlessly try to
// load the include file again, function_exists() is used directly here.
if (!function_exists($module . '_' . $hook)) {
// Clear out the stale implementation from the cache and force a cache
// refresh to forget about no longer existing hook implementations.
unset($implementations[$module]);
// One of the implementations did not exist and needs to be removed in
// the cache.
$all_valid = FALSE;
}
}
return $all_valid;
}
/**
* Parses a dependency for comparison by drupal_check_incompatibility().
*
* @param $dependency
* A dependency string, which specifies a module dependency, and optionally
* the project it comes from and versions that are supported. Supported
* formats include:
* - 'module'
* - 'project:module'
* - 'project:module (>=version, version)'
*
* @return
* An associative array with three keys:
* - 'name' includes the name of the thing to depend on (e.g. 'foo').
* - 'original_version' contains the original version string (which can be
* used in the UI for reporting incompatibilities).
* - 'versions' is a list of associative arrays, each containing the keys
* 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
* '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
* Callers should pass this structure to drupal_check_incompatibility().
*
* @see drupal_check_incompatibility()
*/
public static function parseDependency($dependency) {
$value = array();
// Split out the optional project name.
if (strpos($dependency, ':') !== FALSE) {
list($project_name, $dependency) = explode(':', $dependency);
$value['project'] = $project_name;
}
// We use named subpatterns and support every op that version_compare
// supports. Also, op is optional and defaults to equals.
$p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
// Core version is always optional: 8.x-2.x and 2.x is treated the same.
$p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
$p_major = '(?<major>\d+)';
// By setting the minor version to x, branches can be matched.
$p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
$parts = explode('(', $dependency, 2);
$value['name'] = trim($parts[0]);
if (isset($parts[1])) {
$value['original_version'] = ' (' . $parts[1];
foreach (explode(',', $parts[1]) as $version) {
if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
$op = !empty($matches['operation']) ? $matches['operation'] : '=';
if ($matches['minor'] == 'x') {
// Drupal considers "2.x" to mean any version that begins with
// "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
// on the other hand, treats "x" as a string; so to
// version_compare(), "2.x" is considered less than 2.0. This
// means that >=2.x and <2.x are handled by version_compare()
// as we need, but > and <= are not.
if ($op == '>' || $op == '<=') {
$matches['major']++;
}
// Equivalence can be checked by adding two restrictions.
if ($op == '=' || $op == '==') {
$value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x');
$op = '>=';
}
}
$value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']);
}
}
}
return $value;
}
/**
* {@inheritdoc}
*/
public function getModuleDirectories() {
$dirs = array();
foreach ($this->getModuleList() as $name => $module) {
$dirs[$name] = $this->root . '/' . $module->getPath();
}
return $dirs;
}
/**
* {@inheritdoc}
*/
public function getName($module) {
$info = system_get_info('module', $module);
return isset($info['name']) ? $info['name'] : $module;
}
}

View file

@ -0,0 +1,315 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleHandlerInterface.
*/
namespace Drupal\Core\Extension;
/**
* Interface for classes that manage a set of enabled modules.
*
* Classes implementing this interface work with a fixed list of modules and are
* responsible for loading module files and maintaining information about module
* dependencies and hook implementations.
*/
interface ModuleHandlerInterface {
/**
* Includes a module's .module file.
*
* This prevents including a module more than once.
*
* @param string $name
* The name of the module to load.
*
* @return bool
* TRUE if the item is loaded or has already been loaded.
*/
public function load($name);
/**
* Loads all enabled modules.
*/
public function loadAll();
/**
* Returns whether all modules have been loaded.
*
* @return bool
* A Boolean indicating whether all modules have been loaded. This means all
* modules; the load status of bootstrap modules cannot be checked.
*/
public function isLoaded();
/**
* Reloads all enabled modules.
*/
public function reload();
/**
* Returns the list of currently active modules.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array whose keys are the names of the modules and whose
* values are Extension objects.
*/
public function getModuleList();
/**
* Returns a module extension object from the currently active modules list.
*
* @param string $name
* The name of the module to return.
*
* @return \Drupal\Core\Extension\Extension
* An extension object.
*
* @throws \InvalidArgumentException
* Thrown when the requested module does not exist.
*/
public function getModule($name);
/**
* Sets an explicit list of currently active modules.
*
* @param \Drupal\Core\Extension\Extension[] $module_list
* An associative array whose keys are the names of the modules and whose
* values are Extension objects.
*/
public function setModuleList(array $module_list = array());
/**
* Adds a module to the list of currently active modules.
*
* @param string $name
* The module name; e.g., 'node'.
* @param string $path
* The module path; e.g., 'core/modules/node'.
*/
public function addModule($name, $path);
/**
* Adds an installation profile to the list of currently active modules.
*
* @param string $name
* The profile name; e.g., 'standard'.
* @param string $path
* The profile path; e.g., 'core/profiles/standard'.
*/
public function addProfile($name, $path);
/**
* Determines which modules require and are required by each module.
*
* @param array $modules
* An array of module objects keyed by module name. Each object contains
* information discovered during a Drupal\Core\Extension\ExtensionDiscovery
* scan.
*
* @return
* The same array with the new keys for each module:
* - requires: An array with the keys being the modules that this module
* requires.
* - required_by: An array with the keys being the modules that will not work
* without this module.
*
* @see \Drupal\Core\Extension\ExtensionDiscovery
*/
public function buildModuleDependencies(array $modules);
/**
* Determines whether a given module is enabled.
*
* @param string $module
* The name of the module (without the .module extension).
*
* @return bool
* TRUE if the module is both installed and enabled.
*/
public function moduleExists($module);
/**
* Loads an include file for each enabled module.
*
* @param string $type
* The include file's type (file extension).
* @param string $name
* (optional) The base file name (without the $type extension). If omitted,
* each module's name is used; i.e., "$module.$type" by default.
*/
public function loadAllIncludes($type, $name = NULL);
/**
* Loads a module include file.
*
* Examples:
* @code
* // Load node.admin.inc from the node module.
* $this->loadInclude('node', 'inc', 'node.admin');
* // Load content_types.inc from the node module.
* $this->loadInclude('node', 'inc', ''content_types');
* @endcode
*
* @param string $module
* The module to which the include file belongs.
* @param string $type
* The include file's type (file extension).
* @param string $name
* (optional) The base file name (without the $type extension). If omitted,
* $module is used; i.e., resulting in "$module.$type" by default.
*
* @return string|false
* The name of the included file, if successful; FALSE otherwise.
*/
public function loadInclude($module, $type, $name = NULL);
/**
* Retrieves a list of hooks that are declared through hook_hook_info().
*
* @return array
* An associative array whose keys are hook names and whose values are an
* associative array containing a group name. The structure of the array
* is the same as the return value of hook_hook_info().
*
* @see hook_hook_info()
*/
public function getHookInfo();
/**
* Determines which modules are implementing a hook.
*
* @param string $hook
* The name of the hook (e.g. "help" or "menu").
*
* @return array
* An array with the names of the modules which are implementing this hook.
*/
public function getImplementations($hook);
/**
* Write the hook implementation info to the cache.
*/
public function writeCache();
/**
* Resets the cached list of hook implementations.
*/
public function resetImplementations();
/**
* Returns whether a given module implements a given hook.
*
* @param string $module
* The name of the module (without the .module extension).
* @param string $hook
* The name of the hook (e.g. "help" or "menu").
*
* @return bool
* TRUE if the module is both installed and enabled, and the hook is
* implemented in that module.
*/
public function implementsHook($module, $hook);
/**
* Invokes a hook in a particular module.
*
* @param string $module
* The name of the module (without the .module extension).
* @param string $hook
* The name of the hook to invoke.
* @param ...
* Arguments to pass to the hook implementation.
*
* @return mixed
* The return value of the hook implementation.
*/
public function invoke($module, $hook, array $args = array());
/**
* Invokes a hook in all enabled modules that implement it.
*
* @param string $hook
* The name of the hook to invoke.
* @param array $args
* Arguments to pass to the hook.
*
* @return array
* An array of return values of the hook implementations. If modules return
* arrays from their implementations, those are merged into one array.
*/
public function invokeAll($hook, array $args = array());
/**
* Passes alterable variables to specific hook_TYPE_alter() implementations.
*
* This dispatch function hands off the passed-in variables to type-specific
* hook_TYPE_alter() implementations in modules. It ensures a consistent
* interface for all altering operations.
*
* A maximum of 2 alterable arguments is supported. In case more arguments need
* to be passed and alterable, modules provide additional variables assigned by
* reference in the last $context argument:
* @code
* $context = array(
* 'alterable' => &$alterable,
* 'unalterable' => $unalterable,
* 'foo' => 'bar',
* );
* $this->alter('mymodule_data', $alterable1, $alterable2, $context);
* @endcode
*
* Note that objects are always passed by reference in PHP5. If it is absolutely
* required that no implementation alters a passed object in $context, then an
* object needs to be cloned:
* @code
* $context = array(
* 'unalterable_object' => clone $object,
* );
* $this->alter('mymodule_data', $data, $context);
* @endcode
*
* @param string|array $type
* A string describing the type of the alterable $data. 'form', 'links',
* 'node_content', and so on are several examples. Alternatively can be an
* array, in which case hook_TYPE_alter() is invoked for each value in the
* array, ordered first by module, and then for each module, in the order of
* values in $type. For example, when Form API is using $this->alter() to
* execute both hook_form_alter() and hook_form_FORM_ID_alter()
* implementations, it passes array('form', 'form_' . $form_id) for $type.
* @param mixed $data
* The variable that will be passed to hook_TYPE_alter() implementations to be
* altered. The type of this variable depends on the value of the $type
* argument. For example, when altering a 'form', $data will be a structured
* array. When altering a 'profile', $data will be an object.
* @param mixed $context1
* (optional) An additional variable that is passed by reference.
* @param mixed $context2
* (optional) An additional variable that is passed by reference. If more
* context needs to be provided to implementations, then this should be an
* associative array as described above.
*/
public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL);
/**
* Returns an array of directories for all enabled modules. Useful for
* tasks such as finding a file that exists in all module directories.
*
* @return array
*/
public function getModuleDirectories();
/**
* Gets the human readable name of a given module.
*
* @param string $module
* The machine name of the module which title should be shown.
*
* @return string
* Returns the human readable name of the module or the machine name passed
* in if no matching module is found.
*/
public function getName($module);
}

View file

@ -0,0 +1,519 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleInstaller.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Default implementation of the module installer.
*
* It registers the module in config, installs its own configuration,
* installs the schema, updates the Drupal kernel and more.
*/
class ModuleInstaller implements ModuleInstallerInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* The uninstall validators.
*
* @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
*/
protected $uninstallValidators;
/**
* Constructs a new ModuleInstaller instance.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The drupal kernel.
*
* @see \Drupal\Core\DrupalKernel
* @see \Drupal\Core\CoreServiceProvider
*/
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
$this->root = $root;
$this->moduleHandler = $module_handler;
$this->kernel = $kernel;
}
/**
* {@inheritdoc}
*/
public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
$this->uninstallValidators[] = $uninstall_validator;
}
/**
* {@inheritdoc}
*/
public function install(array $module_list, $enable_dependencies = TRUE) {
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
if ($enable_dependencies) {
// Get all module data so we can find dependencies and sort.
$module_data = system_rebuild_module_data();
$module_list = $module_list ? array_combine($module_list, $module_list) : array();
if ($missing_modules = array_diff_key($module_list, $module_data)) {
// One or more of the given modules doesn't exist.
throw new MissingDependencyException(SafeMarkup::format('Unable to install modules %modules due to missing modules %missing.', array(
'%modules' => implode(', ', $module_list),
'%missing' => implode(', ', $missing_modules),
)));
}
// Only process currently uninstalled modules.
$installed_modules = $extension_config->get('module') ?: array();
if (!$module_list = array_diff_key($module_list, $installed_modules)) {
// Nothing to do. All modules already installed.
return TRUE;
}
// Add dependencies to the list. The new modules will be processed as
// the while loop continues.
while (list($module) = each($module_list)) {
foreach (array_keys($module_data[$module]->requires) as $dependency) {
if (!isset($module_data[$dependency])) {
// The dependency does not exist.
throw new MissingDependencyException(SafeMarkup::format('Unable to install modules: module %module is missing its dependency module %dependency.', array(
'%module' => $module,
'%dependency' => $dependency,
)));
}
// Skip already installed modules.
if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
$module_list[$dependency] = $dependency;
}
}
}
// Set the actual module weights.
$module_list = array_map(function ($module) use ($module_data) {
return $module_data[$module]->sort;
}, $module_list);
// Sort the module list by their weights (reverse).
arsort($module_list);
$module_list = array_keys($module_list);
}
// Required for module installation checks.
include_once $this->root . '/core/includes/install.inc';
/** @var \Drupal\Core\Config\ConfigInstaller $config_installer */
$config_installer = \Drupal::service('config.installer');
$sync_status = $config_installer->isSyncing();
if ($sync_status) {
$source_storage = $config_installer->getSourceStorage();
}
$modules_installed = array();
foreach ($module_list as $module) {
$enabled = $extension_config->get("module.$module") !== NULL;
if (!$enabled) {
// Throw an exception if the module name is too long.
if (strlen($module) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
throw new ExtensionNameLengthException(format_string('Module name %name is over the maximum allowed length of @max characters.', array(
'%name' => $module,
'@max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
)));
}
// Check the validity of the default configuration. This will throw
// exceptions if the configuration is not valid.
$config_installer->checkConfigurationToInstall('module', $module);
// Save this data without checking schema. This is a performance
// improvement for module installation.
$extension_config
->set("module.$module", 0)
->set('module', module_config_sort($extension_config->get('module')))
->save(TRUE);
// Prepare the new module list, sorted by weight, including filenames.
// This list is used for both the ModuleHandler and DrupalKernel. It
// needs to be kept in sync between both. A DrupalKernel reboot or
// rebuild will automatically re-instantiate a new ModuleHandler that
// uses the new module list of the kernel. However, DrupalKernel does
// not cause any modules to be loaded.
// Furthermore, the currently active (fixed) module list can be
// different from the configured list of enabled modules. For all active
// modules not contained in the configured enabled modules, we assume a
// weight of 0.
$current_module_filenames = $this->moduleHandler->getModuleList();
$current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
$current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
$module_filenames = array();
foreach ($current_modules as $name => $weight) {
if (isset($current_module_filenames[$name])) {
$module_filenames[$name] = $current_module_filenames[$name];
}
else {
$module_path = drupal_get_path('module', $name);
$pathname = "$module_path/$name.info.yml";
$filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
$module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
}
}
// Update the module handler in order to load the module's code.
// This allows the module to participate in hooks and its existence to
// be discovered by other modules.
// The current ModuleHandler instance is obsolete with the kernel
// rebuild below.
$this->moduleHandler->setModuleList($module_filenames);
$this->moduleHandler->load($module);
module_load_install($module);
// Clear the static cache of system_rebuild_module_data() to pick up the
// new module, since it merges the installation status of modules into
// its statically cached list.
drupal_static_reset('system_rebuild_module_data');
// Update the kernel to include it.
$this->updateKernel($module_filenames);
// Allow modules to react prior to the installation of a module.
$this->moduleHandler->invokeAll('module_preinstall', array($module));
// Now install the module's schema if necessary.
drupal_install_schema($module);
// Clear plugin manager caches.
\Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
// Set the schema version to the number of the last update provided by
// the module, or the minimum core schema version.
$version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
$versions = drupal_get_schema_versions($module);
if ($versions) {
$version = max(max($versions), $version);
}
// Notify interested components that this module's entity types are new.
// For example, a SQL-based storage handler can use this as an
// opportunity to create the necessary database tables.
// @todo Clean this up in https://www.drupal.org/node/2350111.
$entity_manager = \Drupal::entityManager();
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$entity_manager->onEntityTypeCreate($entity_type);
}
}
// Install default configuration of the module.
$config_installer = \Drupal::service('config.installer');
if ($sync_status) {
$config_installer
->setSyncing(TRUE)
->setSourceStorage($source_storage);
}
\Drupal::service('config.installer')->installDefaultConfig('module', $module);
// If the module has no current updates, but has some that were
// previously removed, set the version to the value of
// hook_update_last_removed().
if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
$version = max($version, $last_removed);
}
drupal_set_installed_schema_version($module, $version);
// Record the fact that it was installed.
$modules_installed[] = $module;
// file_get_stream_wrappers() needs to re-register Drupal's stream
// wrappers in case a module-provided stream wrapper is used later in
// the same request. In particular, this happens when installing Drupal
// via Drush, as the 'translations' stream wrapper is provided by
// Interface Translation module and is later used to import
// translations.
\Drupal::service('stream_wrapper_manager')->register();
// Update the theme registry to include it.
drupal_theme_rebuild();
// Modules can alter theme info, so refresh theme data.
// @todo ThemeHandler cannot be injected into ModuleHandler, since that
// causes a circular service dependency.
// @see https://www.drupal.org/node/2208429
\Drupal::service('theme_handler')->refreshInfo();
// Allow the module to perform install tasks.
$this->moduleHandler->invoke($module, 'install');
// Record the fact that it was installed.
\Drupal::logger('system')->info('%module module installed.', array('%module' => $module));
}
}
// If any modules were newly installed, invoke hook_modules_installed().
if (!empty($modules_installed)) {
\Drupal::service('router.builder')->setRebuildNeeded();
$this->moduleHandler->invokeAll('modules_installed', array($modules_installed));
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
// Get all module data so we can find dependencies and sort.
$module_data = system_rebuild_module_data();
$module_list = $module_list ? array_combine($module_list, $module_list) : array();
if (array_diff_key($module_list, $module_data)) {
// One or more of the given modules doesn't exist.
return FALSE;
}
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
$installed_modules = $extension_config->get('module') ?: array();
if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
// Nothing to do. All modules already uninstalled.
return TRUE;
}
if ($uninstall_dependents) {
// Add dependent modules to the list. The new modules will be processed as
// the while loop continues.
$profile = drupal_get_profile();
while (list($module) = each($module_list)) {
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
if (!isset($module_data[$dependent])) {
// The dependent module does not exist.
return FALSE;
}
// Skip already uninstalled modules.
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
$module_list[$dependent] = $dependent;
}
}
}
}
// Use the validators and throw an exception with the reasons.
if ($reasons = $this->validateUninstall($module_list)) {
foreach ($reasons as $reason) {
$reason_message[] = implode(', ', $reason);
}
throw new ModuleUninstallValidatorException(format_string('The following reasons prevents the modules from being uninstalled: @reasons', array(
'@reasons' => implode('; ', $reason_message),
)));
}
// Set the actual module weights.
$module_list = array_map(function ($module) use ($module_data) {
return $module_data[$module]->sort;
}, $module_list);
// Sort the module list by their weights.
asort($module_list);
$module_list = array_keys($module_list);
// Only process modules that are enabled. A module is only enabled if it is
// configured as enabled. Custom or overridden module handlers might contain
// the module already, which means that it might be loaded, but not
// necessarily installed.
foreach ($module_list as $module) {
// Clean up all entity bundles (including fields) of every entity type
// provided by the module that is being uninstalled.
// @todo Clean this up in https://www.drupal.org/node/2350111.
$entity_manager = \Drupal::entityManager();
foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->getProvider() == $module) {
foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
$entity_manager->onBundleDelete($bundle, $entity_type_id);
}
}
}
// Allow modules to react prior to the uninstallation of a module.
$this->moduleHandler->invokeAll('module_preuninstall', array($module));
// Uninstall the module.
module_load_install($module);
$this->moduleHandler->invoke($module, 'uninstall');
// Remove all configuration belonging to the module.
\Drupal::service('config.manager')->uninstall('module', $module);
// Notify interested components that this module's entity types are being
// deleted. For example, a SQL-based storage handler can use this as an
// opportunity to drop the corresponding database tables.
// @todo Clean this up in https://www.drupal.org/node/2350111.
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$entity_manager->onEntityTypeDelete($entity_type);
}
}
// Remove the schema.
drupal_uninstall_schema($module);
// Remove the module's entry from the config. Don't check schema when
// uninstalling a module since we are only clearing a key.
\Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
// Update the module handler to remove the module.
// The current ModuleHandler instance is obsolete with the kernel rebuild
// below.
$module_filenames = $this->moduleHandler->getModuleList();
unset($module_filenames[$module]);
$this->moduleHandler->setModuleList($module_filenames);
// Remove any potential cache bins provided by the module.
$this->removeCacheBins($module);
// Clear the static cache of system_rebuild_module_data() to pick up the
// new module, since it merges the installation status of modules into
// its statically cached list.
drupal_static_reset('system_rebuild_module_data');
// Clear plugin manager caches.
\Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
// Update the kernel to exclude the uninstalled modules.
$this->updateKernel($module_filenames);
// Update the theme registry to remove the newly uninstalled module.
drupal_theme_rebuild();
// Modules can alter theme info, so refresh theme data.
// @todo ThemeHandler cannot be injected into ModuleHandler, since that
// causes a circular service dependency.
// @see https://www.drupal.org/node/2208429
\Drupal::service('theme_handler')->refreshInfo();
\Drupal::logger('system')->info('%module module uninstalled.', array('%module' => $module));
$schema_store = \Drupal::keyValue('system.schema');
$schema_store->delete($module);
}
\Drupal::service('router.builder')->setRebuildNeeded();
drupal_get_installed_schema_version(NULL, TRUE);
// Let other modules react.
$this->moduleHandler->invokeAll('modules_uninstalled', array($module_list));
// Flush all persistent caches.
// Any cache entry might implicitly depend on the uninstalled modules,
// so clear all of them explicitly.
$this->moduleHandler->invokeAll('cache_flush');
foreach (Cache::getBins() as $service_id => $cache_backend) {
$cache_backend->deleteAll();
}
return TRUE;
}
/**
* Helper method for removing all cache bins registered by a given module.
*
* @param string $module
* The name of the module for which to remove all registered cache bins.
*/
protected function removeCacheBins($module) {
// Remove any cache bins defined by a module.
$service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
if (file_exists($service_yaml_file)) {
$definitions = Yaml::decode(file_get_contents($service_yaml_file));
if (isset($definitions['services'])) {
foreach ($definitions['services'] as $id => $definition) {
if (isset($definition['tags'])) {
foreach ($definition['tags'] as $tag) {
// This works for the default cache registration and even in some
// cases when a non-default "super" factory is used. That should
// be extremely rare.
if ($tag['name'] == 'cache.bin' && isset($definition['factory_service']) && isset($definition['factory_method']) && !empty($definition['arguments'])) {
try {
$factory = \Drupal::service($definition['factory_service']);
if (method_exists($factory, $definition['factory_method'])) {
$backend = call_user_func_array(array($factory, $definition['factory_method']), $definition['arguments']);
if ($backend instanceof CacheBackendInterface) {
$backend->removeBin();
}
}
}
catch (\Exception $e) {
watchdog_exception('system', $e, 'Failed to remove cache bin defined by the service %id.', array('%id' => $id));
}
}
}
}
}
}
}
}
/**
* Updates the kernel module list.
*
* @param string $module_filenames
* The list of installed modules.
*/
protected function updateKernel($module_filenames) {
// This reboots the kernel to register the module's bundle and its services
// in the service container. The $module_filenames argument is taken over as
// %container.modules% parameter, which is passed to a fresh ModuleHandler
// instance upon first retrieval.
$this->kernel->updateModules($module_filenames, $module_filenames);
// After rebuilding the container we need to update the injected
// dependencies.
$container = $this->kernel->getContainer();
$this->moduleHandler = $container->get('module_handler');
}
/**
* {@inheritdoc}
*/
public function validateUninstall(array $module_list) {
$reasons = array();
foreach ($module_list as $module) {
foreach ($this->uninstallValidators as $validator) {
$validation_reasons = $validator->validate($module);
if (!empty($validation_reasons)) {
if (!isset($reasons[$module])) {
$reasons[$module] = array();
}
$reasons[$module] = array_merge($reasons[$module], $validation_reasons);
}
}
}
return $reasons;
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleInstallerInterface.
*/
namespace Drupal\Core\Extension;
/**
* Provides the installation of modules with creating the db schema and more.
*/
interface ModuleInstallerInterface {
/**
* Installs a given list of modules.
*
* Order of events:
* - Gather and add module dependencies to $module_list (if applicable).
* - For each module that is being installed:
* - Invoke hook_module_preinstall().
* - Install module schema and update system registries and caches.
* - Invoke hook_install() and add it to the list of installed modules.
* - Invoke hook_modules_installed().
*
* @param string[] $module_list
* An array of module names.
* @param bool $enable_dependencies
* (optional) If TRUE, dependencies will automatically be installed in the
* correct order. This incurs a significant performance cost, so use FALSE
* if you know $module_list is already complete.
*
* @return bool
* TRUE if the modules were successfully installed.
*
* @throws \Drupal\Core\Extension\MissingDependencyException
* Thrown when a requested module, or a dependency of one, can not be found.
*
* @see hook_module_preinstall()
* @see hook_install()
* @see hook_modules_installed()
*/
public function install(array $module_list, $enable_dependencies = TRUE);
/**
* Uninstalls a given list of modules.
*
* @param string[] $module_list
* The modules to uninstall.
* @param bool $uninstall_dependents
* (optional) If TRUE, dependent modules will automatically be uninstalled
* in the correct order. This incurs a significant performance cost, so use
* FALSE if you know $module_list is already complete.
*
* @return bool
* FALSE if one or more dependencies are missing, TRUE otherwise.
*
* @see hook_module_preuninstall()
* @see hook_uninstall()
* @see hook_modules_uninstalled()
*/
public function uninstall(array $module_list, $uninstall_dependents = TRUE);
/**
* Adds module a uninstall validator.
*
* @param \Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator
* The uninstall validator to add.
*/
public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator);
/**
* Determines whether a list of modules can be uninstalled.
*
* @param string[] $module_list
* An array of module names.
*
* @return string[]
* An array of reasons the module can not be uninstalled, empty if it can.
*/
public function validateUninstall(array $module_list);
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleUninstallValidatorException.
*/
namespace Drupal\Core\Extension;
/**
* Defines an exception thrown when uninstalling a module that did not validate.
*/
class ModuleUninstallValidatorException extends \InvalidArgumentException { }

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleUninstallValidatorInterface.
*/
namespace Drupal\Core\Extension;
/**
* Common interface for module uninstall validators.
*/
interface ModuleUninstallValidatorInterface {
/**
* Determines the reasons a module can not be uninstalled.
*
* @param string $module
* A module name.
*
* @return string[]
* An array of reasons the module can not be uninstalled, empty if it can.
* Each reason should not end with any punctuation since multiple reasons
* can be displayed together.
*
* @see theme_system_modules_uninstall()
*/
public function validate($module);
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\RequiredModuleUninstallValidator.
*/
namespace Drupal\Core\Extension;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Ensures that required modules cannot be uninstalled.
*/
class RequiredModuleUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* Constructs a new RequiredModuleUninstallValidator.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(TranslationInterface $string_translation) {
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];
$module_info = $this->getModuleInfoByModule($module);
if (!empty($module_info['required'])) {
$reasons[] = $this->t('The @module module is required', ['@module' => $module_info['name']]);
}
return $reasons;
}
/**
* Returns the module info for a specific module.
*
* @param string $module
* The name of the module.
*
* @return array
* The module info, or NULL if that module does not exist.
*/
protected function getModuleInfoByModule($module) {
$modules = system_rebuild_module_data();
return isset($modules[$module]->info) ? $modules[$module]->info : [];
}
}

View file

@ -0,0 +1,484 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ThemeHandler.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\State\StateInterface;
/**
* Default theme handler using the config system to store installation statuses.
*/
class ThemeHandler implements ThemeHandlerInterface {
/**
* Contains the features enabled for themes by default.
*
* @var array
*/
protected $defaultFeatures = array(
'logo',
'favicon',
'name',
'slogan',
'node_user_picture',
'comment_user_picture',
'comment_user_verification',
);
/**
* A list of all currently available themes.
*
* @var array
*/
protected $list;
/**
* The config factory to get the installed themes.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The module handler to fire themes_installed/themes_uninstalled hooks.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The state backend.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The config installer to install configuration.
*
* @var \Drupal\Core\Config\ConfigInstallerInterface
*/
protected $configInstaller;
/**
* The info parser to parse the theme.info.yml files.
*
* @var \Drupal\Core\Extension\InfoParserInterface
*/
protected $infoParser;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The route builder to rebuild the routes if a theme is installed.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* An extension discovery instance.
*
* @var \Drupal\Core\Extension\ExtensionDiscovery
*/
protected $extensionDiscovery;
/**
* The CSS asset collection optimizer service.
*
* @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
*/
protected $cssCollectionOptimizer;
/**
* The config manager used to uninstall a theme.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new ThemeHandler.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory to get the installed themes.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to fire themes_installed/themes_uninstalled hooks.
* @param \Drupal\Core\State\StateInterface $state
* The state store.
* @param \Drupal\Core\Extension\InfoParserInterface $info_parser
* The info parser to parse the theme.info.yml files.
* @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
* (optional) A extension discovery instance (for unit tests).
*/
public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
$this->root = $root;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->state = $state;
$this->infoParser = $info_parser;
$this->extensionDiscovery = $extension_discovery;
}
/**
* {@inheritdoc}
*/
public function getDefault() {
return $this->configFactory->get('system.theme')->get('default');
}
/**
* {@inheritdoc}
*/
public function setDefault($name) {
$list = $this->listInfo();
if (!isset($list[$name])) {
throw new \InvalidArgumentException("$name theme is not installed.");
}
$this->configFactory->getEditable('system.theme')
->set('default', $name)
->save();
return $this;
}
/**
* {@inheritdoc}
*/
public function install(array $theme_list, $install_dependencies = TRUE) {
// We keep the old install() method as BC layer but redirect directly to the
// theme installer.
\Drupal::service('theme_installer')->install($theme_list, $install_dependencies);
}
/**
* {@inheritdoc}
*/
public function uninstall(array $theme_list) {
// We keep the old uninstall() method as BC layer but redirect directly to
// the theme installer.
\Drupal::service('theme_installer')->uninstall($theme_list);
}
/**
* {@inheritdoc}
*/
public function listInfo() {
if (!isset($this->list)) {
$this->list = array();
$themes = $this->systemThemeList();
// @todo Ensure that systemThemeList() does not contain an empty list
// during the batch installer, see https://www.drupal.org/node/2322619.
if (empty($themes)) {
$this->refreshInfo();
$this->list = $this->list ?: array();
$themes = \Drupal::state()->get('system.theme.data', array());
}
foreach ($themes as $theme) {
$this->addTheme($theme);
}
}
return $this->list;
}
/**
* {@inheritdoc}
*/
public function addTheme(Extension $theme) {
foreach ($theme->info['libraries'] as $library => $name) {
$theme->libraries[$library] = $name;
}
if (isset($theme->info['engine'])) {
$theme->engine = $theme->info['engine'];
}
if (isset($theme->info['base theme'])) {
$theme->base_theme = $theme->info['base theme'];
}
$this->list[$theme->getName()] = $theme;
}
/**
* {@inheritdoc}
*/
public function refreshInfo() {
$this->reset();
$extension_config = $this->configFactory->get('core.extension');
$installed = $extension_config->get('theme');
// @todo Avoid re-scanning all themes by retaining the original (unaltered)
// theme info somewhere.
$list = $this->rebuildThemeData();
foreach ($list as $name => $theme) {
if (isset($installed[$name])) {
$this->addTheme($theme);
}
}
$this->state->set('system.theme.data', $this->list);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->systemListReset();
$this->list = NULL;
}
/**
* {@inheritdoc}
*/
public function rebuildThemeData() {
$listing = $this->getExtensionDiscovery();
$themes = $listing->scan('theme');
$engines = $listing->scan('theme_engine');
$extension_config = $this->configFactory->get('core.extension');
$installed = $extension_config->get('theme') ?: array();
// Set defaults for theme info.
$defaults = array(
'engine' => 'twig',
'regions' => array(
'sidebar_first' => 'Left sidebar',
'sidebar_second' => 'Right sidebar',
'content' => 'Content',
'header' => 'Header',
'primary_menu' => 'Primary menu',
'secondary_menu' => 'Secondary menu',
'footer' => 'Footer',
'highlighted' => 'Highlighted',
'help' => 'Help',
'page_top' => 'Page top',
'page_bottom' => 'Page bottom',
'breadcrumb' => 'Breadcrumb',
),
'description' => '',
'features' => $this->defaultFeatures,
'screenshot' => 'screenshot.png',
'php' => DRUPAL_MINIMUM_PHP,
'libraries' => array(),
);
$sub_themes = array();
$files_theme = array();
$files_theme_engine = array();
// Read info files for each theme.
foreach ($themes as $key => $theme) {
// @todo Remove all code that relies on the $status property.
$theme->status = (int) isset($installed[$key]);
$theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
// Add the info file modification time, so it becomes available for
// contributed modules to use for ordering theme lists.
$theme->info['mtime'] = $theme->getMTime();
// Invoke hook_system_info_alter() to give installed modules a chance to
// modify the data in the .info.yml files if necessary.
// @todo Remove $type argument, obsolete with $theme->getType().
$type = 'theme';
$this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
if (!empty($theme->info['base theme'])) {
$sub_themes[] = $key;
// Add the base theme as a proper dependency.
$themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
}
// Defaults to 'twig' (see $defaults above).
$engine = $theme->info['engine'];
if (isset($engines[$engine])) {
$theme->owner = $engines[$engine]->getExtensionPathname();
$theme->prefix = $engines[$engine]->getName();
$files_theme_engine[$engine] = $engines[$engine]->getPathname();
}
// Prefix screenshot with theme path.
if (!empty($theme->info['screenshot'])) {
$theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
}
$files_theme[$key] = $theme->getPathname();
}
// Build dependencies.
// @todo Move into a generic ExtensionHandler base class.
// @see https://www.drupal.org/node/2208429
$themes = $this->moduleHandler->buildModuleDependencies($themes);
// Store filenames to allow system_list() and drupal_get_filename() to
// retrieve them for themes and theme engines without having to scan the
// filesystem.
$this->state->set('system.theme.files', $files_theme);
$this->state->set('system.theme_engine.files', $files_theme_engine);
// After establishing the full list of available themes, fill in data for
// sub-themes.
foreach ($sub_themes as $key) {
$sub_theme = $themes[$key];
// The $base_themes property is optional; only set for sub themes.
// @see ThemeHandlerInterface::listInfo()
$sub_theme->base_themes = $this->getBaseThemes($themes, $key);
// empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
// the key of a base theme with a value of NULL in case it is not found,
// in order to prevent needless iterations.
if (!current($sub_theme->base_themes)) {
continue;
}
// Determine the root base theme.
$root_key = key($sub_theme->base_themes);
// Build the list of sub-themes for each of the theme's base themes.
foreach (array_keys($sub_theme->base_themes) as $base_theme) {
$themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
}
// Add the theme engine info from the root base theme.
if (isset($themes[$root_key]->owner)) {
$sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
$sub_theme->owner = $themes[$root_key]->owner;
$sub_theme->prefix = $themes[$root_key]->prefix;
}
}
return $themes;
}
/**
* {@inheritdoc}
*/
public function getBaseThemes(array $themes, $theme) {
return $this->doGetBaseThemes($themes, $theme);
}
/**
* Finds the base themes for the specific theme.
*
* @param array $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.
* @param array $used_themes
* (optional) A recursion parameter preventing endless loops. Defaults to
* an empty array.
*
* @return array
* An array of base themes.
*/
protected function doGetBaseThemes(array $themes, $theme, $used_themes = array()) {
if (!isset($themes[$theme]->info['base theme'])) {
return array();
}
$base_key = $themes[$theme]->info['base theme'];
// Does the base theme exist?
if (!isset($themes[$base_key])) {
return array($base_key => NULL);
}
$current_base_theme = array($base_key => $themes[$base_key]->info['name']);
// Is the base theme itself a child of another theme?
if (isset($themes[$base_key]->info['base theme'])) {
// Do we already know the base themes of this theme?
if (isset($themes[$base_key]->base_themes)) {
return $themes[$base_key]->base_themes + $current_base_theme;
}
// Prevent loops.
if (!empty($used_themes[$base_key])) {
return array($base_key => NULL);
}
$used_themes[$base_key] = TRUE;
return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
}
// If we get here, then this is our parent theme.
return $current_base_theme;
}
/**
* Returns an extension discovery object.
*
* @return \Drupal\Core\Extension\ExtensionDiscovery
* The extension discovery object.
*/
protected function getExtensionDiscovery() {
if (!isset($this->extensionDiscovery)) {
$this->extensionDiscovery = new ExtensionDiscovery($this->root);
}
return $this->extensionDiscovery;
}
/**
* {@inheritdoc}
*/
public function getName($theme) {
$themes = $this->listInfo();
if (!isset($themes[$theme])) {
throw new \InvalidArgumentException(SafeMarkup::format('Requested the name of a non-existing theme @theme', array('@theme' => $theme)));
}
return SafeMarkup::checkPlain($themes[$theme]->info['name']);
}
/**
* Wraps system_list_reset().
*/
protected function systemListReset() {
system_list_reset();
}
/**
* Wraps system_list().
*
* @return array
* A list of themes keyed by name.
*/
protected function systemThemeList() {
return system_list('theme');
}
/**
* {@inheritdoc}
*/
public function getThemeDirectories() {
$dirs = array();
foreach ($this->listInfo() as $name => $theme) {
$dirs[$name] = $this->root . '/' . $theme->getPath();
}
return $dirs;
}
/**
* {@inheritdoc}
*/
public function themeExists($theme) {
$themes = $this->listInfo();
return isset($themes[$theme]);
}
/**
* {@inheritdoc}
*/
public function getTheme($name) {
$themes = $this->listInfo();
if (isset($themes[$name])) {
return $themes[$name];
}
throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name));
}
}

View file

@ -0,0 +1,211 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ThemeHandlerInterface.
*/
namespace Drupal\Core\Extension;
/**
* Manages the list of available themes.
*/
interface ThemeHandlerInterface {
/**
* Installs a given list of themes.
*
* @param array $theme_list
* An array of theme names.
* @param bool $install_dependencies
* (optional) If TRUE, dependencies will automatically be installed in the
* correct order. This incurs a significant performance cost, so use FALSE
* if you know $theme_list is already complete and in the correct order.
*
* @return bool
* Whether any of the given themes have been installed.
*
* @throws \Drupal\Core\Extension\ExtensionNameLengthException
* Thrown when the theme name is to long
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use the theme_installer service instead.
*
* @see \Drupal\Core\Extension\ThemeInstallerInterface::install
*/
public function install(array $theme_list, $install_dependencies = TRUE);
/**
* Uninstalls a given list of themes.
*
* Uninstalling a theme removes all related configuration (like blocks) and
* invokes the 'themes_uninstalled' hook.
*
* @param array $theme_list
* The themes to uninstall.
*
* @throws \InvalidArgumentException
* Thrown when you uninstall an not installed theme.
*
* @see hook_themes_uninstalled()
*
* @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0.
* Use the theme_installer service instead.
*
* @see \Drupal\Core\Extension\ThemeInstallerInterface::uninstall
*/
public function uninstall(array $theme_list);
/**
* Returns a list of currently installed themes.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array of the currently installed themes. The keys are the
* themes' machine names and the values are objects having the following
* properties:
* - filename: The filepath and name of the .info.yml file.
* - name: The machine name of the theme.
* - status: 1 for installed, 0 for uninstalled themes.
* - info: The contents of the .info.yml file.
* - stylesheets: A two dimensional array, using the first key for the
* media attribute (e.g. 'all'), the second for the name of the file
* (e.g. style.css). The value is a complete filepath (e.g.
* themes/bartik/style.css). Not set if no stylesheets are defined in the
* .info.yml file.
* - scripts: An associative array of JavaScripts, using the filename as key
* and the complete filepath as value. Not set if no scripts are defined
* in the .info.yml file.
* - prefix: The base theme engine prefix.
* - engine: The machine name of the theme engine.
* - base_theme: If this is a sub-theme, the machine name of the base theme
* defined in the .info.yml file. Otherwise, the element is not set.
* - base_themes: If this is a sub-theme, an associative array of the
* base-theme ancestors of this theme, starting with this theme's base
* theme, then the base theme's own base theme, etc. Each entry has an
* array key equal to the theme's machine name, and a value equal to the
* human-readable theme name; if a theme with matching machine name does
* not exist in the system, the value will instead be NULL (and since the
* system would not know whether that theme itself has a base theme, that
* will end the array of base themes). This is not set if the theme is not
* a sub-theme.
* - sub_themes: An associative array of themes on the system that are
* either direct sub-themes (that is, they declare this theme to be
* their base theme), direct sub-themes of sub-themes, etc. The keys are
* the themes' machine names, and the values are the themes'
* human-readable names. This element is not set if there are no themes on
* the system that declare this theme as their base theme.
*/
public function listInfo();
/**
* Adds a theme extension to the internal listing.
*
* @param \Drupal\Core\Extension\Extension $theme
* The theme extension.
*/
public function addTheme(Extension $theme);
/**
* Refreshes the theme info data of currently installed themes.
*
* Modules can alter theme info, so this is typically called after a module
* has been installed or uninstalled.
*/
public function refreshInfo();
/**
* Resets the internal state of the theme handler.
*/
public function reset();
/**
* Scans and collects theme extension data and their engines.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array of theme extensions.
*/
public function rebuildThemeData();
/**
* Finds all the base themes for the specified theme.
*
* Themes can inherit templates and function implementations from earlier
* themes.
*
* @param \Drupal\Core\Extension\Extension[] $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.
*
* @return array
* Returns an array of all of the theme's ancestors; the first element's
* value will be NULL if an error occurred.
*/
public function getBaseThemes(array $themes, $theme);
/**
* Gets the human readable name of a given theme.
*
* @param string $theme
* The machine name of the theme which title should be shown.
*
* @return string
* Returns the human readable name of the theme.
*/
public function getName($theme);
/**
* Returns the default theme.
*
* @return string
* The default theme.
*/
public function getDefault();
/**
* Sets a new default theme.
*
* @param string $theme
* The new default theme.
*
* @return $this
*/
public function setDefault($theme);
/**
* Returns an array of directories for all installed themes.
*
* Useful for tasks such as finding a file that exists in all theme
* directories.
*
* @return array
*/
public function getThemeDirectories();
/**
* Determines whether a given theme is installed.
*
* @param string $theme
* The name of the theme (without the .theme extension).
*
* @return bool
* TRUE if the theme is installed.
*/
public function themeExists($theme);
/**
* Returns a theme extension object from the currently active theme list.
*
* @param string $name
* The name of the theme to return.
*
* @return \Drupal\Core\Extension\Extension
* An extension object.
*
* @throws \InvalidArgumentException
* Thrown when the requested theme does not exist.
*/
public function getTheme($name);
}

View file

@ -0,0 +1,312 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ThemeInstaller.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigInstallerInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\State\StateInterface;
use Psr\Log\LoggerInterface;
/**
* Manages theme installation/uninstallation.
*/
class ThemeInstaller implements ThemeInstallerInterface {
/**
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* @var \Drupal\Core\Config\ConfigInstallerInterface
*/
protected $configInstaller;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
*/
protected $cssCollectionOptimizer;
/**
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new ThemeInstaller.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory to get the installed themes.
* @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
* (optional) The config installer to install configuration. This optional
* to allow the theme handler to work before Drupal is installed and has a
* database.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to fire themes_installed/themes_uninstalled hooks.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The config manager used to uninstall a theme.
* @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
* The CSS asset collection optimizer service.
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* (optional) The route builder service to rebuild the routes if a theme is
* installed.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\State\StateInterface $state
* The state store.
*/
public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
$this->themeHandler = $theme_handler;
$this->configFactory = $config_factory;
$this->configInstaller = $config_installer;
$this->moduleHandler = $module_handler;
$this->configManager = $config_manager;
$this->cssCollectionOptimizer = $css_collection_optimizer;
$this->routeBuilder = $route_builder;
$this->logger = $logger;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function install(array $theme_list, $install_dependencies = TRUE) {
$extension_config = $this->configFactory->getEditable('core.extension');
$theme_data = $this->themeHandler->rebuildThemeData();
if ($install_dependencies) {
$theme_list = array_combine($theme_list, $theme_list);
if ($missing = array_diff_key($theme_list, $theme_data)) {
// One or more of the given themes doesn't exist.
throw new \InvalidArgumentException(SafeMarkup::format('Unknown themes: !themes.', array(
'!themes' => implode(', ', $missing),
)));
}
// Only process themes that are not installed currently.
$installed_themes = $extension_config->get('theme') ?: array();
if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
// Nothing to do. All themes already installed.
return TRUE;
}
while (list($theme) = each($theme_list)) {
// Add dependencies to the list. The new themes will be processed as
// the while loop continues.
foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
if (!isset($theme_data[$dependency])) {
// The dependency does not exist.
return FALSE;
}
// Skip already installed themes.
if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
$theme_list[$dependency] = $dependency;
}
}
}
// Set the actual theme weights.
$theme_list = array_map(function ($theme) use ($theme_data) {
return $theme_data[$theme]->sort;
}, $theme_list);
// Sort the theme list by their weights (reverse).
arsort($theme_list);
$theme_list = array_keys($theme_list);
}
else {
$installed_themes = $extension_config->get('theme') ?: array();
}
$themes_installed = array();
foreach ($theme_list as $key) {
// Only process themes that are not already installed.
$installed = $extension_config->get("theme.$key") !== NULL;
if ($installed) {
continue;
}
// Throw an exception if the theme name is too long.
if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
throw new ExtensionNameLengthException(SafeMarkup::format('Theme name %name is over the maximum allowed length of @max characters.', array(
'%name' => $key,
'@max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
)));
}
// Validate default configuration of the theme. If there is existing
// configuration then stop installing.
$this->configInstaller->checkConfigurationToInstall('theme', $key);
// The value is not used; the weight is ignored for themes currently. Do
// not check schema when saving the configuration.
$extension_config
->set("theme.$key", 0)
->save(TRUE);
// Add the theme to the current list.
// @todo Remove all code that relies on $status property.
$theme_data[$key]->status = 1;
$this->themeHandler->addTheme($theme_data[$key]);
// Update the current theme data accordingly.
$current_theme_data = $this->state->get('system.theme.data', array());
$current_theme_data[$key] = $theme_data[$key];
$this->state->set('system.theme.data', $current_theme_data);
// Reset theme settings.
$theme_settings = &drupal_static('theme_get_setting');
unset($theme_settings[$key]);
// @todo Remove system_list().
$this->systemListReset();
// Only install default configuration if this theme has not been installed
// already.
if (!isset($installed_themes[$key])) {
// Install default configuration of the theme.
$this->configInstaller->installDefaultConfig('theme', $key);
}
$themes_installed[] = $key;
// Record the fact that it was installed.
$this->logger->info('%theme theme installed.', array('%theme' => $key));
}
$this->cssCollectionOptimizer->deleteAll();
$this->resetSystem();
// Invoke hook_themes_installed() after the themes have been installed.
$this->moduleHandler->invokeAll('themes_installed', array($themes_installed));
return !empty($themes_installed);
}
/**
* {@inheritdoc}
*/
public function uninstall(array $theme_list) {
$extension_config = $this->configFactory->getEditable('core.extension');
$theme_config = $this->configFactory->getEditable('system.theme');
$list = $this->themeHandler->listInfo();
foreach ($theme_list as $key) {
if (!isset($list[$key])) {
throw new \InvalidArgumentException("Unknown theme: $key.");
}
if ($key === $theme_config->get('default')) {
throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
}
if ($key === $theme_config->get('admin')) {
throw new \InvalidArgumentException("The current admin theme $key cannot be uninstalled.");
}
// Base themes cannot be uninstalled if sub themes are installed, and if
// they are not uninstalled at the same time.
// @todo https://www.drupal.org/node/474684 and
// https://www.drupal.org/node/1297856 themes should leverage the module
// dependency system.
if (!empty($list[$key]->sub_themes)) {
foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
}
}
}
}
$this->cssCollectionOptimizer->deleteAll();
$current_theme_data = $this->state->get('system.theme.data', array());
foreach ($theme_list as $key) {
// The value is not used; the weight is ignored for themes currently.
$extension_config->clear("theme.$key");
// Update the current theme data accordingly.
unset($current_theme_data[$key]);
// Reset theme settings.
$theme_settings = &drupal_static('theme_get_setting');
unset($theme_settings[$key]);
// Remove all configuration belonging to the theme.
$this->configManager->uninstall('theme', $key);
}
// Don't check schema when uninstalling a theme since we are only clearing
// keys.
$extension_config->save(TRUE);
$this->state->set('system.theme.data', $current_theme_data);
// @todo Remove system_list().
$this->themeHandler->refreshInfo();
$this->resetSystem();
$this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
}
/**
* Resets some other systems like rebuilding the route information or caches.
*/
protected function resetSystem() {
if ($this->routeBuilder) {
$this->routeBuilder->setRebuildNeeded();
}
$this->systemListReset();
// @todo It feels wrong to have the requirement to clear the local tasks
// cache here.
Cache::invalidateTags(array('local_task'));
$this->themeRegistryRebuild();
}
/**
* Wraps drupal_theme_rebuild().
*/
protected function themeRegistryRebuild() {
drupal_theme_rebuild();
}
/**
* Wraps system_list_reset().
*/
protected function systemListReset() {
system_list_reset();
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ThemeInstallerInterface.
*/
namespace Drupal\Core\Extension;
/**
* Manages theme installation/uninstallation.
*/
interface ThemeInstallerInterface {
/**
* Installs a given list of themes.
*
* @param array $theme_list
* An array of theme names.
* @param bool $install_dependencies
* (optional) If TRUE, dependencies will automatically be installed in the
* correct order. This incurs a significant performance cost, so use FALSE
* if you know $theme_list is already complete and in the correct order.
*
* @return bool
* Whether any of the given themes have been installed.
*
* @throws \Drupal\Core\Extension\ExtensionNameLengthException
* Thrown when the theme name is to long
*/
public function install(array $theme_list, $install_dependencies = TRUE);
/**
* Uninstalls a given list of themes.
*
* Uninstalling a theme removes all related configuration (like blocks) and
* invokes the 'themes_uninstalled' hook.
*
* @param array $theme_list
* The themes to uninstall.
*
* @throws \InvalidArgumentException
* Thrown when you uninstall an not installed theme.
*
* @see hook_themes_uninstalled()
*/
public function uninstall(array $theme_list);
}

View file

@ -0,0 +1,784 @@
<?php
/**
* @file
* Hooks related to module and update systems.
*/
use Drupal\Core\Utility\UpdateException;
use Drupal\Core\Url;
/**
* @addtogroup hooks
* @{
*/
/**
* Defines one or more hooks that are exposed by a module.
*
* Normally hooks do not need to be explicitly defined. However, by declaring a
* hook explicitly, a module may define a "group" for it. Modules that implement
* a hook may then place their implementation in either $module.module or in
* $module.$group.inc. If the hook is located in $module.$group.inc, then that
* file will be automatically loaded when needed.
* In general, hooks that are rarely invoked and/or are very large should be
* placed in a separate include file, while hooks that are very short or very
* frequently called should be left in the main module file so that they are
* always available.
*
* @return
* An associative array whose keys are hook names and whose values are an
* associative array containing:
* - group: A string defining the group to which the hook belongs. The module
* system will determine whether a file with the name $module.$group.inc
* exists, and automatically load it when required.
*
* See system_hook_info() for all hook groups defined by Drupal core.
*
* @see hook_hook_info_alter().
*/
function hook_hook_info() {
$hooks['token_info'] = array(
'group' => 'tokens',
);
$hooks['tokens'] = array(
'group' => 'tokens',
);
return $hooks;
}
/**
* Alter the registry of modules implementing a hook.
*
* This hook is invoked during \Drupal::moduleHandler()->getImplementations().
* A module may implement this hook in order to reorder the implementing
* modules, which are otherwise ordered by the module's system weight.
*
* Note that hooks invoked using \Drupal::moduleHandler->alter() can have
* multiple variations(such as hook_form_alter() and hook_form_FORM_ID_alter()).
* \Drupal::moduleHandler->alter() will call all such variants defined by a
* single module in turn. For the purposes of hook_module_implements_alter(),
* these variants are treated as a single hook. Thus, to ensure that your
* implementation of hook_form_FORM_ID_alter() is called at the right time,
* you will have to change the order of hook_form_alter() implementation in
* hook_module_implements_alter().
*
* @param $implementations
* An array keyed by the module's name. The value of each item corresponds
* to a $group, which is usually FALSE, unless the implementation is in a
* file named $module.$group.inc.
* @param $hook
* The name of the module hook being implemented.
*/
function hook_module_implements_alter(&$implementations, $hook) {
if ($hook == 'form_alter') {
// Move my_module_form_alter() to the end of the list.
// \Drupal::moduleHandler()->getImplementations()
// iterates through $implementations with a foreach loop which PHP iterates
// in the order that the items were added, so to move an item to the end of
// the array, we remove it and then add it.
$group = $implementations['my_module'];
unset($implementations['my_module']);
$implementations['my_module'] = $group;
}
}
/**
* Alter the information parsed from module and theme .info.yml files
*
* This hook is invoked in _system_rebuild_module_data() and in
* \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). A module
* may implement this hook in order to add to or alter the data generated by
* reading the .info.yml file with \Drupal\Core\Extension\InfoParser.
*
* @param array $info
* The .info.yml file contents, passed by reference so that it can be altered.
* @param \Drupal\Core\Extension\Extension $file
* Full information about the module or theme.
* @param string $type
* Either 'module' or 'theme', depending on the type of .info.yml file that
* was passed.
*/
function hook_system_info_alter(array &$info, \Drupal\Core\Extension\Extension $file, $type) {
// Only fill this in if the .info.yml file does not define a 'datestamp'.
if (empty($info['datestamp'])) {
$info['datestamp'] = $file->getMTime();
}
}
/**
* Perform necessary actions before a module is installed.
*
* @param string $module
* The name of the module about to be installed.
*/
function hook_module_preinstall($module) {
mymodule_cache_clear();
}
/**
* Perform necessary actions after modules are installed.
*
* This function differs from hook_install() in that it gives all other modules
* a chance to perform actions when a module is installed, whereas
* hook_install() is only called on the module actually being installed. See
* \Drupal\Core\Extension\ModuleHandler::install() for a detailed description of
* the order in which install hooks are invoked.
*
* @param $modules
* An array of the modules that were installed.
*
* @see \Drupal\Core\Extension\ModuleHandler::install()
* @see hook_install()
*/
function hook_modules_installed($modules) {
if (in_array('lousy_module', $modules)) {
\Drupal::state()->set('mymodule.lousy_module_compatibility', TRUE);
}
}
/**
* Perform setup tasks when the module is installed.
*
* If the module implements hook_schema(), the database tables will
* be created before this hook is fired.
*
* Implementations of this hook are by convention declared in the module's
* .install file. The implementation can rely on the .module file being loaded.
* The hook will only be called when a module is installed. The module's schema
* version will be set to the module's greatest numbered update hook. Because of
* this, any time a hook_update_N() is added to the module, this function needs
* to be updated to reflect the current version of the database schema.
*
* See the @link https://www.drupal.org/node/146843 Schema API documentation
* @endlink for details on hook_schema and how database tables are defined.
*
* Note that since this function is called from a full bootstrap, all functions
* (including those in modules enabled by the current page request) are
* available when this hook is called. Use cases could be displaying a user
* message, or calling a module function necessary for initial setup, etc.
*
* Please be sure that anything added or modified in this function that can
* be removed during uninstall should be removed with hook_uninstall().
*
* @see hook_schema()
* @see \Drupal\Core\Extension\ModuleHandler::install()
* @see hook_uninstall()
* @see hook_modules_installed()
*/
function hook_install() {
// Create the styles directory and ensure it's writable.
$directory = file_default_scheme() . '://styles';
$mode = isset($GLOBALS['install_state']['mode']) ? $GLOBALS['install_state']['mode'] : NULL;
file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS, $mode);
}
/**
* Perform necessary actions before a module is uninstalled.
*
* @param string $module
* The name of the module about to be uninstalled.
*/
function hook_module_preuninstall($module) {
mymodule_cache_clear();
}
/**
* Perform necessary actions after modules are uninstalled.
*
* This function differs from hook_uninstall() in that it gives all other
* modules a chance to perform actions when a module is uninstalled, whereas
* hook_uninstall() is only called on the module actually being uninstalled.
*
* It is recommended that you implement this hook if your module stores
* data that may have been set by other modules.
*
* @param $modules
* An array of the modules that were uninstalled.
*
* @see hook_uninstall()
*/
function hook_modules_uninstalled($modules) {
if (in_array('lousy_module', $modules)) {
\Drupal::state()->delete('mymodule.lousy_module_compatibility');
}
mymodule_cache_rebuild();
}
/**
* Remove any information that the module sets.
*
* The information that the module should remove includes:
* - state that the module has set using \Drupal::state()
* - modifications to existing tables
*
* The module should not remove its entry from the module configuration.
* Database tables defined by hook_schema() will be removed automatically.
*
* The uninstall hook must be implemented in the module's .install file. It
* will fire when the module gets uninstalled but before the module's database
* tables are removed, allowing your module to query its own tables during
* this routine.
*
* @see hook_install()
* @see hook_schema()
* @see hook_modules_uninstalled()
*/
function hook_uninstall() {
// Remove the styles directory and generated images.
file_unmanaged_delete_recursive(file_default_scheme() . '://styles');
}
/**
* Return an array of tasks to be performed by an installation profile.
*
* Any tasks you define here will be run, in order, after the installer has
* finished the site configuration step but before it has moved on to the
* final import of languages and the end of the installation. This is invoked
* by install_tasks(). You can have any number of custom tasks to perform
* during this phase.
*
* Each task you define here corresponds to a callback function which you must
* separately define and which is called when your task is run. This function
* will receive the global installation state variable, $install_state, as
* input, and has the opportunity to access or modify any of its settings. See
* the install_state_defaults() function in the installer for the list of
* $install_state settings used by Drupal core.
*
* At the end of your task function, you can indicate that you want the
* installer to pause and display a page to the user by returning any themed
* output that should be displayed on that page (but see below for tasks that
* use the form API or batch API; the return values of these task functions are
* handled differently). You should also use #title within the task
* callback function to set a custom page title. For some tasks, however, you
* may want to simply do some processing and pass control to the next task
* without ending the page request; to indicate this, simply do not send back
* a return value from your task function at all. This can be used, for
* example, by installation profiles that need to configure certain site
* settings in the database without obtaining any input from the user.
*
* The task function is treated specially if it defines a form or requires
* batch processing; in that case, you should return either the form API
* definition or batch API array, as appropriate. See below for more
* information on the 'type' key that you must define in the task definition
* to inform the installer that your task falls into one of those two
* categories. It is important to use these APIs directly, since the installer
* may be run non-interactively (for example, via a command line script), all
* in one page request; in that case, the installer will automatically take
* care of submitting forms and processing batches correctly for both types of
* installations. You can inspect the $install_state['interactive'] boolean to
* see whether or not the current installation is interactive, if you need
* access to this information.
*
* Remember that a user installing Drupal interactively will be able to reload
* an installation page multiple times, so you should use \Drupal::state() to
* store any data that you may need later in the installation process. Any
* temporary state must be removed using \Drupal::state()->delete() before
* your last task has completed and control is handed back to the installer.
*
* @param array $install_state
* An array of information about the current installation state.
*
* @return array
* A keyed array of tasks the profile will perform during the final stage of
* the installation. Each key represents the name of a function (usually a
* function defined by this profile, although that is not strictly required)
* that is called when that task is run. The values are associative arrays
* containing the following key-value pairs (all of which are optional):
* - display_name: The human-readable name of the task. This will be
* displayed to the user while the installer is running, along with a list
* of other tasks that are being run. Leave this unset to prevent the task
* from appearing in the list.
* - display: This is a boolean which can be used to provide finer-grained
* control over whether or not the task will display. This is mostly useful
* for tasks that are intended to display only under certain conditions;
* for these tasks, you can set 'display_name' to the name that you want to
* display, but then use this boolean to hide the task only when certain
* conditions apply.
* - type: A string representing the type of task. This parameter has three
* possible values:
* - normal: (default) This indicates that the task will be treated as a
* regular callback function, which does its processing and optionally
* returns HTML output.
* - batch: This indicates that the task function will return a batch API
* definition suitable for batch_set() or an array of batch definitions
* suitable for consecutive batch_set() calls. The installer will then
* take care of automatically running the task via batch processing.
* - form: This indicates that the task function will return a standard
* form API definition (and separately define validation and submit
* handlers, as appropriate). The installer will then take care of
* automatically directing the user through the form submission process.
* - run: A constant representing the manner in which the task will be run.
* This parameter has three possible values:
* - INSTALL_TASK_RUN_IF_NOT_COMPLETED: (default) This indicates that the
* task will run once during the installation of the profile.
* - INSTALL_TASK_SKIP: This indicates that the task will not run during
* the current installation page request. It can be used to skip running
* an installation task when certain conditions are met, even though the
* task may still show on the list of installation tasks presented to the
* user.
* - INSTALL_TASK_RUN_IF_REACHED: This indicates that the task will run on
* each installation page request that reaches it. This is rarely
* necessary for an installation profile to use; it is primarily used by
* the Drupal installer for bootstrap-related tasks.
* - function: Normally this does not need to be set, but it can be used to
* force the installer to call a different function when the task is run
* (rather than the function whose name is given by the array key). This
* could be used, for example, to allow the same function to be called by
* two different tasks.
*
* @see install_state_defaults()
* @see batch_set()
* @see hook_install_tasks_alter()
* @see install_tasks()
*/
function hook_install_tasks(&$install_state) {
// Here, we define a variable to allow tasks to indicate that a particular,
// processor-intensive batch process needs to be triggered later on in the
// installation.
$myprofile_needs_batch_processing = \Drupal::state()->get('myprofile.needs_batch_processing', FALSE);
$tasks = array(
// This is an example of a task that defines a form which the user who is
// installing the site will be asked to fill out. To implement this task,
// your profile would define a function named myprofile_data_import_form()
// as a normal form API callback function, with associated validation and
// submit handlers. In the submit handler, in addition to saving whatever
// other data you have collected from the user, you might also call
// \Drupal::state()->set('myprofile.needs_batch_processing', TRUE) if the
// user has entered data which requires that batch processing will need to
// occur later on.
'myprofile_data_import_form' => array(
'display_name' => t('Data import options'),
'type' => 'form',
),
// Similarly, to implement this task, your profile would define a function
// named myprofile_settings_form() with associated validation and submit
// handlers. This form might be used to collect and save additional
// information from the user that your profile needs. There are no extra
// steps required for your profile to act as an "installation wizard"; you
// can simply define as many tasks of type 'form' as you wish to execute,
// and the forms will be presented to the user, one after another.
'myprofile_settings_form' => array(
'display_name' => t('Additional options'),
'type' => 'form',
),
// This is an example of a task that performs batch operations. To
// implement this task, your profile would define a function named
// myprofile_batch_processing() which returns a batch API array definition
// that the installer will use to execute your batch operations. Due to the
// 'myprofile.needs_batch_processing' variable used here, this task will be
// hidden and skipped unless your profile set it to TRUE in one of the
// previous tasks.
'myprofile_batch_processing' => array(
'display_name' => t('Import additional data'),
'display' => $myprofile_needs_batch_processing,
'type' => 'batch',
'run' => $myprofile_needs_batch_processing ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP,
),
// This is an example of a task that will not be displayed in the list that
// the user sees. To implement this task, your profile would define a
// function named myprofile_final_site_setup(), in which additional,
// automated site setup operations would be performed. Since this is the
// last task defined by your profile, you should also use this function to
// call \Drupal::state()->delete('myprofile.needs_batch_processing') and
// clean up the state that was used above. If you want the user to pass
// to the final Drupal installation tasks uninterrupted, return no output
// from this function. Otherwise, return themed output that the user will
// see (for example, a confirmation page explaining that your profile's
// tasks are complete, with a link to reload the current page and therefore
// pass on to the final Drupal installation tasks when the user is ready to
// do so).
'myprofile_final_site_setup' => array(
),
);
return $tasks;
}
/**
* Alter the full list of installation tasks.
*
* You can use this hook to change or replace any part of the Drupal
* installation process that occurs after the installation profile is selected.
*
* This hook is invoked on the install profile in install_tasks().
*
* @param $tasks
* An array of all available installation tasks, including those provided by
* Drupal core. You can modify this array to change or replace individual
* steps within the installation process.
* @param $install_state
* An array of information about the current installation state.
*
* @see hook_install_tasks()
* @see install_tasks()
*/
function hook_install_tasks_alter(&$tasks, $install_state) {
// Replace the entire site configuration form provided by Drupal core
// with a custom callback function defined by this installation profile.
$tasks['install_configure_form']['function'] = 'myprofile_install_configure_form';
}
/**
* Perform a single update.
*
* For each change that requires one or more actions to be performed when
* updating a site, add a new hook_update_N(), which will be called by
* update.php. The documentation block preceding this function is stripped of
* newlines and used as the description for the update on the pending updates
* task list. Schema updates should adhere to the
* @link https://www.drupal.org/node/150215 Schema API. @endlink
*
* Implementations of hook_update_N() are named (module name)_update_(number).
* The numbers are composed of three parts:
* - 1 digit for Drupal core compatibility.
* - 1 digit for your module's major release version (e.g., is this the 8.x-1.*
* (1) or 8.x-2.* (2) series of your module).
* - 2 digits for sequential counting, starting with 01.
*
* Examples:
* - mymodule_update_8100(): This is the first update to get the database ready
* to run mymodule 8.x-1.*.
* - mymodule_update_8200(): This is the first update to get the database ready
* to run mymodule 8.x-2.*.
*
* As of Drupal 8.0, the database upgrade system no longer supports updating a
* database from an earlier major version of Drupal: update.php can be used to
* upgrade from 7.x-1.x to 7.x-2.x, or 8.x-1.x to 8.x-2.x, but not from 7.x to
* 8.x. Therefore, only update hooks numbered 8001 or later will run for
* Drupal 8. 8000 is reserved for the minimum core schema version and defining
* mymodule_update_8000() will result in an exception. Use the
* @link https://www.drupal.org/node/2127611 Migration API @endlink instead to
* migrate data from an earlier major version of Drupal.
*
* For further information about releases and release numbers see:
* @link https://www.drupal.org/node/711070 Maintaining a drupal.org project
* with Git @endlink
*
* Never renumber update functions.
*
* Implementations of this hook should be placed in a mymodule.install file in
* the same directory as mymodule.module. Drupal core's updates are implemented
* using the system module as a name and stored in database/updates.inc.
*
* Not all module functions are available from within a hook_update_N() function.
* In order to call a function from your mymodule.module or an include file,
* you need to explicitly load that file first.
*
* During database updates the schema of any module could be out of date. For
* this reason, caution is needed when using any API function within an update
* function - particularly CRUD functions, functions that depend on the schema
* (for example by using drupal_write_record()), and any functions that invoke
* hooks.
*
* The $sandbox parameter should be used when a multipass update is needed, in
* circumstances where running the whole update at once could cause PHP to
* timeout. Each pass is run in a way that avoids PHP timeouts, provided each
* pass remains under the timeout limit. To signify that an update requires
* at least one more pass, set $sandbox['#finished'] to a number less than 1
* (you need to do this each pass). The value of $sandbox['#finished'] will be
* unset between passes but all other data in $sandbox will be preserved. The
* system will stop iterating this update when $sandbox['#finished'] is left
* unset or set to a number higher than 1. It is recommended that
* $sandbox['#finished'] is initially set to 0, and then updated each pass to a
* number between 0 and 1 that represents the overall % completed for this
* update, finishing with 1.
*
* See the @link batch Batch operations topic @endlink for more information on
* how to use the Batch API.
*
* @param array $sandbox
* Stores information for multipass updates. See above for more information.
*
* @throws \Drupal\Core\Utility\UpdateException|PDOException
* In case of error, update hooks should throw an instance of
* Drupal\Core\Utility\UpdateException with a meaningful message for the user.
* If a database query fails for whatever reason, it will throw a
* PDOException.
*
* @return string|null
* Optionally, update hooks may return a translated string that will be
* displayed to the user after the update has completed. If no message is
* returned, no message will be presented to the user.
*
* @see batch
* @see schemaapi
* @see hook_update_last_removed()
* @see update_get_update_list()
*/
function hook_update_N(&$sandbox) {
// For non-multipass updates, the signature can simply be;
// function hook_update_N() {
// For most updates, the following is sufficient.
db_add_field('mytable1', 'newcol', array('type' => 'int', 'not null' => TRUE, 'description' => 'My new integer column.'));
// However, for more complex operations that may take a long time,
// you may hook into Batch API as in the following example.
// Update 3 users at a time to have an exclamation point after their names.
// (They're really happy that we can do batch API in this hook!)
if (!isset($sandbox['progress'])) {
$sandbox['progress'] = 0;
$sandbox['current_uid'] = 0;
// We'll -1 to disregard the uid 0...
$sandbox['max'] = db_query('SELECT COUNT(DISTINCT uid) FROM {users}')->fetchField() - 1;
}
$users = db_select('users', 'u')
->fields('u', array('uid', 'name'))
->condition('uid', $sandbox['current_uid'], '>')
->range(0, 3)
->orderBy('uid', 'ASC')
->execute();
foreach ($users as $user) {
$user->setUsername($user->getUsername() . '!');
db_update('users')
->fields(array('name' => $user->getUsername()))
->condition('uid', $user->id())
->execute();
$sandbox['progress']++;
$sandbox['current_uid'] = $user->id();
}
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
if ($some_error_condition_met) {
// In case of an error, simply throw an exception with an error message.
throw new UpdateException('Something went wrong; here is what you should do.');
}
// To display a message to the user when the update is completed, return it.
// If you do not want to display a completion message, simply return nothing.
return t('The update did what it was supposed to do.');
}
/**
* Return an array of information about module update dependencies.
*
* This can be used to indicate update functions from other modules that your
* module's update functions depend on, or vice versa. It is used by the update
* system to determine the appropriate order in which updates should be run, as
* well as to search for missing dependencies.
*
* Implementations of this hook should be placed in a mymodule.install file in
* the same directory as mymodule.module.
*
* @return
* A multidimensional array containing information about the module update
* dependencies. The first two levels of keys represent the module and update
* number (respectively) for which information is being returned, and the
* value is an array of information about that update's dependencies. Within
* this array, each key represents a module, and each value represents the
* number of an update function within that module. In the event that your
* update function depends on more than one update from a particular module,
* you should always list the highest numbered one here (since updates within
* a given module always run in numerical order).
*
* @see update_resolve_dependencies()
* @see hook_update_N()
*/
function hook_update_dependencies() {
// Indicate that the mymodule_update_8001() function provided by this module
// must run after the another_module_update_8003() function provided by the
// 'another_module' module.
$dependencies['mymodule'][8001] = array(
'another_module' => 8003,
);
// Indicate that the mymodule_update_8002() function provided by this module
// must run before the yet_another_module_update_8005() function provided by
// the 'yet_another_module' module. (Note that declaring dependencies in this
// direction should be done only in rare situations, since it can lead to the
// following problem: If a site has already run the yet_another_module
// module's database updates before it updates its codebase to pick up the
// newest mymodule code, then the dependency declared here will be ignored.)
$dependencies['yet_another_module'][8005] = array(
'mymodule' => 8002,
);
return $dependencies;
}
/**
* Return a number which is no longer available as hook_update_N().
*
* If you remove some update functions from your mymodule.install file, you
* should notify Drupal of those missing functions. This way, Drupal can
* ensure that no update is accidentally skipped.
*
* Implementations of this hook should be placed in a mymodule.install file in
* the same directory as mymodule.module.
*
* @return
* An integer, corresponding to hook_update_N() which has been removed from
* mymodule.install.
*
* @see hook_update_N()
*/
function hook_update_last_removed() {
// We've removed the 8.x-1.x version of mymodule, including database updates.
// The next update function is mymodule_update_8200().
return 8103;
}
/**
* Provide information on Updaters (classes that can update Drupal).
*
* Drupal\Core\Updater\Updater is a class that knows how to update various parts
* of the Drupal file system, for example to update modules that have newer
* releases, or to install a new theme.
*
* @return
* An associative array of information about the updater(s) being provided.
* This array is keyed by a unique identifier for each updater, and the
* values are subarrays that can contain the following keys:
* - class: The name of the PHP class which implements this updater.
* - name: Human-readable name of this updater.
* - weight: Controls what order the Updater classes are consulted to decide
* which one should handle a given task. When an update task is being run,
* the system will loop through all the Updater classes defined in this
* registry in weight order and let each class respond to the task and
* decide if each Updater wants to handle the task. In general, this
* doesn't matter, but if you need to override an existing Updater, make
* sure your Updater has a lighter weight so that it comes first.
*
* @see drupal_get_updaters()
* @see hook_updater_info_alter()
*/
function hook_updater_info() {
return array(
'module' => array(
'class' => 'Drupal\Core\Updater\Module',
'name' => t('Update modules'),
'weight' => 0,
),
'theme' => array(
'class' => 'Drupal\Core\Updater\Theme',
'name' => t('Update themes'),
'weight' => 0,
),
);
}
/**
* Alter the Updater information array.
*
* An Updater is a class that knows how to update various parts of the Drupal
* file system, for example to update modules that have newer releases, or to
* install a new theme.
*
* @param array $updaters
* Associative array of updaters as defined through hook_updater_info().
* Alter this array directly.
*
* @see drupal_get_updaters()
* @see hook_updater_info()
*/
function hook_updater_info_alter(&$updaters) {
// Adjust weight so that the theme Updater gets a chance to handle a given
// update task before module updaters.
$updaters['theme']['weight'] = -1;
}
/**
* Check installation requirements and do status reporting.
*
* This hook has three closely related uses, determined by the $phase argument:
* - Checking installation requirements ($phase == 'install').
* - Checking update requirements ($phase == 'update').
* - Status reporting ($phase == 'runtime').
*
* Note that this hook, like all others dealing with installation and updates,
* must reside in a module_name.install file, or it will not properly abort
* the installation of the module if a critical requirement is missing.
*
* During the 'install' phase, modules can for example assert that
* library or server versions are available or sufficient.
* Note that the installation of a module can happen during installation of
* Drupal itself (by install.php) with an installation profile or later by hand.
* As a consequence, install-time requirements must be checked without access
* to the full Drupal API, because it is not available during install.php.
* If a requirement has a severity of REQUIREMENT_ERROR, install.php will abort
* or at least the module will not install.
* Other severity levels have no effect on the installation.
* Module dependencies do not belong to these installation requirements,
* but should be defined in the module's .info.yml file.
*
* The 'runtime' phase is not limited to pure installation requirements
* but can also be used for more general status information like maintenance
* tasks and security issues.
* The returned 'requirements' will be listed on the status report in the
* administration section, with indication of the severity level.
* Moreover, any requirement with a severity of REQUIREMENT_ERROR severity will
* result in a notice on the administration configuration page.
*
* @param $phase
* The phase in which requirements are checked:
* - install: The module is being installed.
* - update: The module is enabled and update.php is run.
* - runtime: The runtime requirements are being checked and shown on the
* status report page.
*
* @return
* An associative array where the keys are arbitrary but must be unique (it
* is suggested to use the module short name as a prefix) and the values are
* themselves associative arrays with the following elements:
* - title: The name of the requirement.
* - value: The current value (e.g., version, time, level, etc). During
* install phase, this should only be used for version numbers, do not set
* it if not applicable.
* - description: The description of the requirement/status.
* - severity: The requirement's result/severity level, one of:
* - REQUIREMENT_INFO: For info only.
* - REQUIREMENT_OK: The requirement is satisfied.
* - REQUIREMENT_WARNING: The requirement failed with a warning.
* - REQUIREMENT_ERROR: The requirement failed with an error.
*/
function hook_requirements($phase) {
$requirements = array();
// Report Drupal version
if ($phase == 'runtime') {
$requirements['drupal'] = array(
'title' => t('Drupal'),
'value' => \Drupal::VERSION,
'severity' => REQUIREMENT_INFO
);
}
// Test PHP version
$requirements['php'] = array(
'title' => t('PHP'),
'value' => ($phase == 'runtime') ? \Drupal::l(phpversion(), new Url('system.php')) : phpversion(),
);
if (version_compare(phpversion(), DRUPAL_MINIMUM_PHP) < 0) {
$requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version.', array('%version' => DRUPAL_MINIMUM_PHP));
$requirements['php']['severity'] = REQUIREMENT_ERROR;
}
// Report cron status
if ($phase == 'runtime') {
$cron_last = \Drupal::state()->get('system.cron_last');
if (is_numeric($cron_last)) {
$requirements['cron']['value'] = t('Last run !time ago', array('!time' => \Drupal::service('date.formatter')->formatTimeDiffSince($cron_last)));
}
else {
$requirements['cron'] = array(
'description' => t('Cron has not run. It appears cron jobs have not been setup on your system. Check the help pages for <a href="@url">configuring cron jobs</a>.', array('@url' => 'https://www.drupal.org/cron')),
'severity' => REQUIREMENT_ERROR,
'value' => t('Never run'),
);
}
$requirements['cron']['description'] .= ' ' . t('You can <a href="@cron">run cron manually</a>.', array('@cron' => \Drupal::url('system.run_cron')));
$requirements['cron']['title'] = t('Cron maintenance tasks');
}
return $requirements;
}
/**
* @} End of "addtogroup hooks".
*/