bs_lib-8.x-1.0-alpha3/src/Commands/BsLibCommands.php
src/Commands/BsLibCommands.php
<?php namespace Drupal\bs_lib\Commands; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ThemeHandlerInterface; use Drush\Commands\DrushCommands; use Drush\Drush; use Psr\Log\LogLevel; use Exception; use stdClass; /** * Drush commands for bs_lib module. */ class BsLibCommands extends DrushCommands { /** * BsLibCommands constructor. */ public function __construct(ThemeHandlerInterface $theme_handler) { // For easier maintenance we just wrap the methods from bs_base here. $bs_base = $theme_handler->getTheme('bs_base'); include_once "{$bs_base->getPath()}/bs_base.drush.inc"; } /** * Create a new bs_base compatible child theme. * * @command bs:theme-create * * @param string $parent_machine_name Parent theme machine name. * @param string $child_machine_name Child theme machine name. * @param string $child_name Child theme name. * @param string $child_description Child theme description. * * @bootstrap max * @aliases bs-tc,bs-theme-create * * @usage drush bs-tc bs_bootstrap custom_theme 'Custom theme' 'Custom theme description' * Create a new bs_base compatible child theme. * * @throws \Exception */ public function themeCreate($parent_machine_name, $child_machine_name, $child_name, $child_description) { // Verify that the child machine name contains no disallowed characters. if (preg_match('@[^a-z0-9_]+@', $child_machine_name)) { throw new \Exception('The machine-readable name "' . $child_machine_name . '" must contain only lowercase letters, numbers, and hyphens.'); } $this->output()->writeln("Starting $child_machine_name theme creation"); // Parent theme should exist. $parent_path = $this->drupalGetThemePath($parent_machine_name); if (empty($parent_path)) { throw new \Exception('Parent theme does not exist.'); } // Child theme should not exist. if (!empty($child_path = $this->drupalGetThemePath($child_machine_name))) { throw new \Exception("Child theme already exist on $child_path file system."); } // Create child theme directory. // Figure child theme path respecting multisite installation. $boot = Drush::bootstrap(); $site_path = $boot->confPath(); if ($site_path === 'sites/default') { // For default site we are using standard web/themes/custom folder. $child_path = 'themes/custom/' . $child_machine_name; } else { // For multi sites we will use multisite folder. $child_path = $site_path . '/themes/custom/' . $child_machine_name; } drush_log("Creating {$child_path} folder.", LogLevel::INFO); if (!mkdir($child_path, 0755, TRUE)) { throw new \Exception("Failed to create child theme directory on $child_path path."); } $options = [ 'parent_machine_name' => $parent_machine_name, 'parent_path' => $parent_path, 'child_machine_name' => $child_machine_name, 'child_path' => $child_path, 'child_name' => $child_name, 'child_description' => $child_description, ]; // Copy files from parent and change/apply text changes to labels. $this->copyThemeFiles($options); // Replace text in copied files. $this->reconfigureThemeFiles($options); // Copy theme-options.yml from parent theme. Try first to copy template if // it exists, if not copy theme-options.yml. $theme_options_filename = FALSE; if (file_exists($options['parent_path'] . '/template.theme-options.yml')) { $theme_options_filename = 'template.theme-options.yml'; } elseif (file_exists($options['parent_path'] . '/theme-options.yml')) { $theme_options_filename = 'theme-options.yml'; } if ($theme_options_filename && !copy($options['parent_path'] . '/' . $theme_options_filename, $options['child_path'] . '/' . 'theme-options.yml')) { throw new \Exception("Failed to copy $theme_options_filename file from {$options['parent_path']} to {$options['child_path']}."); } // Generate some files from the scratch. $this->generateFile("config/schema/{$child_machine_name}.schema.yml", $options); $this->generateFile('gulp-options.yml', $options); $this->generateFile($child_machine_name . '.info.yml', $options); $this->generateFile($child_machine_name . '.libraries.yml', $options); $this->generateFile('README.md', $options); // Rebuild themes static cache because new theme is created. $this->drupalThemeListInfo(TRUE); // Make sure we are on latest parent theme versions. $update_functions = $this->GetUpdateHooks($child_machine_name); if (!empty($update_functions)) { $bs_versions = []; foreach ($update_functions as $theme_name => $theme_updates) { // Get last update. end($theme_updates['functions']); $last_function = key($theme_updates['functions']); $bs_versions['bs_versions.' . $theme_name] = (int) $last_function; } $all_themes = $this->drupalThemeListInfo(); $this->setYmlValue($all_themes[$child_machine_name]->pathname, $bs_versions, TRUE); } // Update and flatten SASS files. $this->updateSassFiles($child_machine_name); // Rebuild theme assets. $this->themeBuild($child_machine_name); } /** * Update existing bs_lib compatible child theme. * * @command bs:theme-update * * @param string $target_machine_name Theme machine name. * @bootstrap max * @aliases bs-tu,bs-theme-update * * @usage drush bs-tu custom_theme * Create a new bs_base compatible child theme. * @throws \Exception */ public function themeUpdate($target_machine_name) { $this->output()->writeln("Updating a $target_machine_name theme"); $target_path = $this->drupalGetThemePath($target_machine_name); if (empty($target_path)) { throw new \Exception('Target theme does not exist.'); } $parent_themes = $this->getParentThemes($target_machine_name); if (empty($parent_themes)) { throw new \Exception('Parent themes are missing.'); } // Run update hooks. $this->themeRunUpdateHooks($target_machine_name); $all_themes = $this->drupalThemeListInfo(); $first_parent_machine_name = $this->drupalGetParentThemeName($target_machine_name); $this->updateSassFiles($target_machine_name); // Check for any new or removed CSS library in parent theme and update // libraries-override section. $parent_theme_libraries_override = $this->generateLibrariesOverride($first_parent_machine_name); $target_info_array = $all_themes[$target_machine_name]->info; $target_theme_libraries_override = $target_info_array['libraries-override']; // Keep only the libraries from target that are not from parents. foreach ($target_theme_libraries_override as $library_key => $library_value) { // We have it in parent already. if (isset($parent_theme_libraries_override[$library_key])) { unset($target_theme_libraries_override[$library_key]); } // We do not have it in parent, but it does belong to parent themes. We // assume that library was removed from parent theme or that it is in // parent parents. In this case we will remove it from here. elseif ($this->libraryKeyComingFromParents($library_key, $target_machine_name)) { unset($target_theme_libraries_override[$library_key]); } } // We start from parent generated libraries override. $new_libraries_override = array_merge($parent_theme_libraries_override, $target_theme_libraries_override); // Update info file with new libraries override. $info_content = file_get_contents($all_themes[$target_machine_name]->pathname); $info_content = preg_replace('/^libraries-override:\R(^ +.+\R)+/m', Yaml::encode(['libraries-override' => $new_libraries_override]), $info_content); file_put_contents($all_themes[$target_machine_name]->pathname, $info_content); // Rebuild assets. $this->themeBuild($target_machine_name); } /** * Run build script in provided theme. * * @command bs:theme-build * * @param string $theme_machine_name Theme machine name. * * @bootstrap max * @aliases bs-tb,bs-theme-build * * @usage drush bs-tb custom_theme * Download custom_theme build dependencies and build all assets. */ public function themeBuild($theme_machine_name) { $this->output()->writeln("Building asset for a $theme_machine_name theme"); $target_path = $this->drupalGetThemePath($theme_machine_name); if (empty($target_path)) { throw new \Exception("Target theme {$theme_machine_name} does not exist."); } $this->drushBuild($target_path); } /** * Returns array with unique lines. * * This will eliminate all duplicate lines in array, but it will also compare * commented lines with not commented and eliminate that duplicates also. For * example: * * array( * '@import "bs_bootstrap/sass/components/partials/alert";', * '//@import "bs_bootstrap/sass/components/partials/alert";', * ) * * This two values are considered duplicates also and the result will be * * array( * '//@import "bs_bootstrap/sass/components/partials/alert";', * ) * * @param array $lines * Array of lines. * * @return array * Array of unique lines. */ protected function arrayUniqueLines(array $lines) { return _bs_base_array_unique_lines($lines); } /** * Copy general theme files from parent theme to child theme. * * @param array $options * Array of options having next keys: * - parent_machine_name * - parent_path * - child_path * - child_name * - child_description. */ protected function copyThemeFiles(array $options) { _bs_base_copy_theme_files($options); } /** * Copy file from parent theme to child theme. * * If parent file is a directory then it will be created. * * Target subdirectories of a file will be created automatically if they do * not exist. * * @param string $file * File name. * @param array $options * Array of options having next keys: * - parent_machine_name * - parent_path * - child_machine_name * - child_path. * * @return bool * TRUE if the file was copied, FALSE otherwise. */ protected function copyFile($file, array $options) { return _bs_base_copy_file($file, $options); } /** * Finds all the base themes for the specified theme. * * @param array $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 the theme's ancestors including specified theme. */ protected function drupalGetBaseThemes(array $themes, $theme) { return _bs_base_drupal_get_base_themes($themes, $theme); } /** * Returns the first parent theme of passed child theme. * * @param string $theme_name * The name of the child theme whose first parent theme we are looking for. * * @return string|NULL * Returns a theme machine name of first parent theme or NULL if parent does * not exist. */ protected function drupalGetParentThemeName($theme_name) { return _bs_base_drupal_get_parent_theme_name($theme_name); } /** * Returns the path to a Drupal theme. * * @param string $name * Theme machine name. * * @return string * The path to the requested theme or an empty string if the item is not * found. */ protected function drupalGetThemePath($name) { return _bs_base_drupal_get_theme_path($name); } /** * Discovers available extensions of a given type. * * For an explanation of how this work see ExtensionDiscovery::scan(). * * @param string $type * The extension type to search for. One of 'profile', 'module', 'theme', or * 'theme_engine'. * @param bool $reset * Reset internal cache. * * @return stdClass[] * An associative array of stdClass objects, keyed by extension name. */ protected function drupalScan($type, $reset = FALSE) { return _bs_base_drupal_scan($type, $reset); } /** * Recursively scans a base directory for the extensions it contains. * * @see ExtensionDiscovery::scanDirectory() * For an explanation of how this works. * * @param string $dir * A relative base directory path to scan, without trailing slash. * * @return stdClass[] * An associative array of stdClass objects, keyed by extension name. */ protected function drupalScanDirectory($dir) { return _bs_base_drupal_scan_directory($dir); } /** * Get information's for all themes. * * @param bool $reset * Reset internal cache. * * @return array * Array holding themes information's. */ protected function drupalThemeListInfo($reset = FALSE) { return _bs_base_drupal_theme_list_info($reset); } /** * Finds all parent themes for the specified theme. * * @param string $theme_machine_name * The machine name of the theme whose parent themes we are looking for. * * @return array * Returns an array of all the parent themes. */ protected function getParentThemes($theme_machine_name) { return _bs_base_get_parent_themes($theme_machine_name); } /** * Get theme information. * * @param string $theme_machine_name * Theme machine name. * * @return mixed|null * Theme info object or NULL if theme does not exist. */ protected function getThemeInfo($theme_machine_name) { return _bs_base_get_theme_info($theme_machine_name); } /** * Get all child themes for passed parent_theme. * * @todo Remove? Doesn't seem to be used. * * @param string $parent_theme * Machine name of parent theme. * * @return array * Array of all child themes machine names. Empty array if child themes does * not exist. */ protected function findChildThemes($parent_theme) { return _bs_base_find_child_themes($parent_theme); } /** * Check that library is coming from theme parent themes or bs_lib module. * * @param string $library_key * Library key. * @param string $theme_machine_name * Theme machine name. * * @return bool * TRUE if library is coming from parents, FALSE other way. */ protected function libraryKeyComingFromParents($library_key, $theme_machine_name) { return _bs_base_library_key_coming_from_parents($library_key, $theme_machine_name); } /** * Replace text in theme files so all configurations are correct. * * @param array $options * Array of options having next keys: * - parent_machine_name * - parent_path * - child_path * - child_name * - child_description. * * @throws \Exception */ protected function reconfigureThemeFiles(array $options) { _bs_base_reconfigure_theme_files($options); } /** * Get theme update functions. * * @param string $target_machine_name * Target theme machine name. * * @return array * Array of update functions. */ protected function getUpdateHooks($target_machine_name) { return _bs_base_get_update_hooks($target_machine_name); } /** * Run theme update functions. * * @param string $target_machine_name * Target theme machine name. * * @throws \Exception */ protected function themeRunUpdateHooks($target_machine_name) { $update_functions = $this->getUpdateHooks($target_machine_name); if (empty($update_functions)) { $this->output()->writeln("No theme updates required."); return; } // Print a list of pending updates for this module and get confirmation. $this->output()->writeln('The following updates are pending:'); $this->output()->writeln(drush_html_to_text('<h2>')); foreach ($update_functions as $theme_name => $theme_updates) { $this->output()->writeln($theme_name . ' theme : '); foreach ($theme_updates['functions'] as $version => $description) { $this->output()->writeln(' ' . $version . ' - ' . strip_tags($description)); } } $this->output()->writeln(drush_html_to_text('<h2>')); if (!$this->confirm('Do you wish to run all pending updates?', TRUE)) { return; } $this->output()->writeln(drush_html_to_text('<h2>')); $this->output()->writeln('Running next updates:'); // Load install files and execute update functions. $bs_versions = []; foreach ($update_functions as $theme_name => $theme_updates) { include_once $theme_updates['file']; foreach ($theme_updates['functions'] as $version => $description) { $update_function = $theme_name . '_bs_update_' . $version; $this->output()->writeln(' ' . $version . ' - ' . strip_tags($description)); $update_function($target_machine_name); } // Update theme info bs_versions. $bs_versions['bs_versions.' . $theme_name] = (int) $version; } // Update info file with the latest versions. $all_themes = $this->drupalThemeListInfo(); $this->setYmlValue($all_themes[$target_machine_name]->pathname, $bs_versions, TRUE); } /** * Run build script in provided theme path. * * @param string $path * Path to theme folder. */ protected function drushBuild($path) { _bs_base_drush_build($path); } /** * Ensure that theme filename has all directories. * * @param string $filename * Filename with optional subdirectories in path that we ensure they exist. * @param string $path * Base path in which we will check $filename subdirectories. * * @return bool * TRUE on success, FALSE on error. */ protected function ensureDirectory($filename, $path) { return _bs_base_ensure_directory($filename, $path); } /** * Finds all files that match a given mask in a given directory. * * For additional information see file_scan_directory(). * * @param string $dir * The base directory or URI to scan, without trailing slash. * @param string $mask * The preg_match() regular expression for files to be included. * @param array $options * An associative array of additional options, with the following elements: * - 'nomask': The preg_match() regular expression for files to be excluded. * Defaults to the 'file_scan_ignore_directories' setting. * - 'callback': The callback function to call for each match. There is no * default callback. * - 'recurse': When TRUE, the directory scan will recurse the entire tree * starting at the provided directory. Defaults to TRUE. * - 'key': The key to be used for the returned associative array of files. * Possible values are 'uri', for the file's URI; 'filename', for the * basename of the file; and 'name' for the name of the file without the * extension. Defaults to 'uri'. * - 'min_depth': Minimum depth of directories to return files from. Defaults * to 0. * @param int $depth * The current depth of recursion. This parameter is only used internally and * should not be passed in. * * @return * An associative array (keyed on the chosen key) of objects with 'uri', * 'filename', and 'name' properties corresponding to the matched files. */ protected function fileScanDirectory($dir, $mask, $options = [], $depth = 0) { return _bs_base_file_scan_directory($dir, $mask, $options, $depth); } /** * Flatten all parent theme @import directives in a SASS file. * * @param object $target_sass_file * Target SASS file object. * @param string $target_machine_name * Target theme machine name. * @param array $current_themes * Array of current themes. * @param array $parent_themes_sass_files * Array of all SASS files from parent theme. * @param int $depth * Current recursion depth, internally used. * * @return array * Returns array of flattened SASS @import directives. */ protected function flattenSassFileImports($target_sass_file, $target_machine_name, array $current_themes, array $parent_themes_sass_files, $depth = 0) { return _bs_base_flatten_sass_file_imports($target_sass_file, $target_machine_name, $current_themes, $parent_themes_sass_files, $depth); } /** * Generate child SASS file from parent theme. * * @param object $parent_sass_file * Plain PHP object holding SASS file information. * @param array $options * Array of parent/child options. * * @return bool * TRUE on success, FALSE other way. */ protected function generateSassFile($parent_sass_file, array $options) { return _bs_base_generate_sass_file($parent_sass_file, $options); } /** * Generate theme file with a default content. * * @param string $file_name * File name. * @param array $options * Options array. * * @return bool * TRUE on success, FALSE on failure. */ protected function generateFile($file_name, array $options) { return _bs_base_generate_file($file_name, $options); } /** * Get all CSS libraries from passed theme that child theme needs to override. * * @param string $parent_machine_name * Parent theme machine name. * * @return array * Libraries override array. */ protected function generateLibrariesOverride($parent_machine_name) { return _bs_base_generate_libraries_override($parent_machine_name); } /** * Get CSS libraries from theme in libraries override format. * * @param string $theme_machine_name * Theme machine name from which we are getting CSS libraries. * * @return array * Arrays of CSS libraries in libraries override format. */ protected function getCssLibrariesForOverride($theme_machine_name) { return _bs_base_get_css_libraries_for_override($theme_machine_name); } /** * Get array of all SASS files in the given path. * * @param string $path * Theme path. * * @return array * Array of all SASS files in the given path. */ protected function getSassFiles($path) { return _bs_base_get_sass_files($path); } /** * Regular expression search and replace in the text. * * @param string $text * Text to search and replace. * @param array $regexps * Array of regexps searches with it replace values. * @param string $modifiers * PHP regular expression modifiers. * @param string $delimiter * PHP regular expression delimiter. * * @return string * Replaced text. */ protected function regexp($text, array $regexps, $modifiers = 'm', $delimiter = '%') { return _bs_base_regexp($text,$regexps, $modifiers, $delimiter); } /** * Regular expression search and replace in the file. * * @param string $file_name * File path. * @param array $regexps * Array of regexps searches with it replace values. * * @return bool * TRUE on success, FALSE other way. */ protected function regexpFile($file_name, array $regexps) { return _bs_base_regexp_file($file_name, $regexps); } /** * Set primitive values in a yml file. * * Please note that this implementation is not perfect but exist only to * support the needs of this drush implementation. There are a couple of * limitations that are explained in function comments. Most importantly * values in values array can be only primitive types for now. * * @param string $path * Yaml file path. * @param array $values * Array with yaml values to set. The element key is a combination of yaml * keys and element value is yaml value. For example: * * $values['logo:path'] = 'custom/logo/path' * * will do * * logo: * path: 'custom/logo/path'. * @param bool $add * If TRUE then value does not exist, and it will be added. * * @throws Exception * Throws exception in the case that we try to set non-scalar value. */ protected function setYmlValue($path, array $values, $add = FALSE) { _bs_base_set_yml_value($path, $values, $add); } /** * Update SASS files in theme and do SASS import flattening. * * @param string $theme_machine_name * Theme machine name. */ protected function updateSassFiles($theme_machine_name) { _bs_base_update_sass_files($theme_machine_name); } }