default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions

View File

@@ -0,0 +1,217 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
/**
* Determine recursively which packages have been allowed to scaffold files.
*
* If the root-level composer.json allows drupal/core, and drupal/core allows
* drupal/assets, then the later package will also implicitly be allowed.
*
* @internal
*/
class AllowedPackages implements PostPackageEventListenerInterface {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* Manager of the options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Composer\Plugin\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The list of new packages added by this Composer command.
*
* @var array
*/
protected $newPackages = [];
/**
* AllowedPackages constructor.
*
* @param \Composer\Composer $composer
* The composer object.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Composer\Plugin\Scaffold\ManageOptions $manage_options
* Manager of the options in the top-level composer.json's 'extra' section.
*/
public function __construct(Composer $composer, IOInterface $io, ManageOptions $manage_options) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = $manage_options;
}
/**
* Gets a list of all packages that are allowed to copy scaffold files.
*
* We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
* and 'drupal/core' to scaffold files, if they are present. Any other
* project must be explicitly whitelisted in the top-level composer.json
* file in order to be allowed to override scaffold files.
* Configuration for packages specified later will override configuration
* specified by packages listed earlier. In other words, the last listed
* package has the highest priority. The root package will always be returned
* at the end of the list.
*
* @return \Composer\Package\PackageInterface[]
* An array of allowed Composer packages.
*/
public function getAllowedPackages() {
$top_level_packages = $this->getTopLevelAllowedPackages();
$allowed_packages = $this->recursiveGetAllowedPackages($top_level_packages);
// If the root package defines any file mappings, then implicitly add it
// to the list of allowed packages. Add it at the end so that it overrides
// all the preceding packages.
if ($this->manageOptions->getOptions()->hasFileMapping()) {
$root_package = $this->composer->getPackage();
unset($allowed_packages[$root_package->getName()]);
$allowed_packages[$root_package->getName()] = $root_package;
}
// Handle any newly-added packages that are not already allowed.
return $this->evaluateNewPackages($allowed_packages);
}
/**
* {@inheritdoc}
*/
public function event(PackageEvent $event) {
$operation = $event->getOperation();
// Determine the package. Later, in evaluateNewPackages(), we will report
// which of the newly-installed packages have scaffold operations, and
// whether or not they are allowed to scaffold by the allowed-packages
// option in the root-level composer.json file.
$operationType = $this->getOperationType($operation);
$package = $operationType === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
if (ScaffoldOptions::hasOptions($package->getExtra())) {
$this->newPackages[$package->getName()] = $package;
}
}
/**
* Gets all packages that are allowed in the top-level composer.json.
*
* We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
* and 'drupal/core' to scaffold files, if they are present. Any other
* project must be explicitly whitelisted in the top-level composer.json
* file in order to be allowed to override scaffold files.
*
* @return array
* An array of allowed Composer package names.
*/
protected function getTopLevelAllowedPackages() {
$implicit_packages = [
'drupal/legacy-scaffold-assets',
'drupal/core',
];
$top_level_packages = $this->manageOptions->getOptions()->allowedPackages();
return array_merge($implicit_packages, $top_level_packages);
}
/**
* Builds a name-to-package mapping from a list of package names.
*
* @param string[] $packages_to_allow
* List of package names to allow.
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function recursiveGetAllowedPackages(array $packages_to_allow, array $allowed_packages = []) {
foreach ($packages_to_allow as $name) {
$package = $this->getPackage($name);
if ($package instanceof PackageInterface && !isset($allowed_packages[$name])) {
$allowed_packages[$name] = $package;
$package_options = $this->manageOptions->packageOptions($package);
$allowed_packages = $this->recursiveGetAllowedPackages($package_options->allowedPackages(), $allowed_packages);
}
}
return $allowed_packages;
}
/**
* Evaluates newly-added packages and see if they are already allowed.
*
* For now we will only emit warnings if they are not.
*
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function evaluateNewPackages(array $allowed_packages) {
foreach ($this->newPackages as $name => $newPackage) {
if (!array_key_exists($name, $allowed_packages)) {
$this->io->write("Not scaffolding files for <comment>{$name}</comment>, because it is not listed in the element 'extra.drupal-scaffold.allowed-packages' in the root-level composer.json file.");
}
else {
$this->io->write("Package <comment>{$name}</comment> has scaffold operations, and is already allowed in the root-level composer.json file.");
}
}
// @todo We could prompt the user and ask if they wish to allow a
// newly-added package. This might be useful if, for example, the user
// might wish to require an installation profile that contains scaffolded
// assets. For more information, see:
// https://www.drupal.org/project/drupal/issues/3064990
return $allowed_packages;
}
/**
* Determine the type of the provided operation.
*
* Adjusts API used for Composer 1 or Composer 2.
*
* @param \Composer\DependencyResolver\Operation\OperationInterface $operation
* The operation object.
*
* @return string
* The operation type.
*/
protected function getOperationType(OperationInterface $operation) {
// Use Composer 2 method.
if (method_exists($operation, 'getOperationType')) {
return $operation->getOperationType();
}
// Fallback to Composer 1 method.
return $operation->getJobType();
}
/**
* Retrieves a package from the current composer process.
*
* @param string $name
* Name of the package to get from the current composer installation.
*
* @return \Composer\Package\PackageInterface|null
* The Composer package.
*/
protected function getPackage($name) {
return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
/**
* List of all commands provided by this package.
*
* @internal
*/
class CommandProvider implements CommandProviderCapability {
/**
* {@inheritdoc}
*/
public function getCommands() {
return [new ComposerScaffoldCommand()];
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Command\BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* The "drupal:scaffold" command class.
*
* Manually run the scaffold operation that normally happens after
* 'composer install'.
*
* @internal
*/
class ComposerScaffoldCommand extends BaseCommand {
/**
* {@inheritdoc}
*/
protected function configure() {
$this
->setName('drupal:scaffold')
->setAliases(['scaffold'])
->setDescription('Update the Drupal scaffold files.')
->setHelp(
<<<EOT
The <info>drupal:scaffold</info> command places the scaffold files in their
respective locations according to the layout stipulated in the composer.json
file.
<info>php composer.phar drupal:scaffold</info>
It is usually not necessary to call <info>drupal:scaffold</info> manually,
because it is called automatically as needed, e.g. after an <info>install</info>
or <info>update</info> command. Note, though, that only packages explicitly
allowed to scaffold in the top-level composer.json will be processed by this
command.
For more information, see https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold.
EOT
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$handler = new Handler($this->getComposer(), $this->getIO());
$handler->scaffold();
return 0;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult;
/**
* Generates an 'autoload.php' that includes the autoloader created by Composer.
*
* @internal
*/
final class GenerateAutoloadReferenceFile {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Generates the autoload file at the specified location.
*
* This only writes a bit of PHP that includes the autoload file that
* Composer generated. Drupal does this so that it can guarantee that there
* will always be an `autoload.php` file in a well-known location.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
* @param string $vendor
* The path to the vendor directory.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The result of the autoload file generation.
*/
public static function generateAutoload(IOInterface $io, $package_name, $web_root, $vendor) {
$autoload_path = static::autoloadPath($package_name, $web_root);
// Calculate the relative path from the webroot (location of the project
// autoload.php) to the vendor directory.
$fs = new Filesystem();
$relative_autoload_path = $fs->findShortestPath($autoload_path->fullPath(), "$vendor/autoload.php");
file_put_contents($autoload_path->fullPath(), static::autoLoadContents($relative_autoload_path));
return new ScaffoldResult($autoload_path, TRUE);
}
/**
* Determines whether or not the autoload file has been committed.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return bool
* True if autoload.php file exists and has been committed to the repository
*/
public static function autoloadFileCommitted(IOInterface $io, $package_name, $web_root) {
$autoload_path = static::autoloadPath($package_name, $web_root);
$autoload_file = $autoload_path->fullPath();
$location = dirname($autoload_file);
if (!file_exists($autoload_file)) {
return FALSE;
}
return Git::checkTracked($io, $autoload_file, $location);
}
/**
* Generates a scaffold file path object for the autoload file.
*
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* Object wrapping the relative and absolute path to the destination file.
*/
protected static function autoloadPath($package_name, $web_root) {
$rel_path = 'autoload.php';
$dest_rel_path = '[web-root]/' . $rel_path;
$dest_full_path = $web_root . '/' . $rel_path;
return new ScaffoldFilePath('autoload', $package_name, $dest_rel_path, $dest_full_path);
}
/**
* Builds the contents of the autoload file.
*
* @param string $relative_autoload_path
* The relative path to the autoloader in vendor.
*
* @return string
* Return the contents for the autoload.php.
*/
protected static function autoLoadContents($relative_autoload_path) {
$relative_autoload_path = preg_replace('#^\./#', '', $relative_autoload_path);
return <<<EOF
<?php
/**
* @file
* Includes the autoloader created by Composer.
*
* This file was generated by drupal-scaffold.
*
* @see composer.json
* @see index.php
* @see core/install.php
* @see core/rebuild.php
* @see core/modules/statistics/statistics.php
*/
return require __DIR__ . '/{$relative_autoload_path}';
EOF;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
/**
* Provide some Git utility operations.
*
* @internal
*/
class Git {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Determines whether the specified scaffold file is already ignored.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already ignored or not (TRUE if ignored).
*/
public static function checkIgnore(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git check-ignore ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Determines whether the specified scaffold file is tracked by git.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already tracked or not (TRUE if tracked).
*/
public static function checkTracked(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git ls-files --error-unmatch ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Checks to see if the project root dir is in a git repository.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $dir
* Base directory for git process.
*
* @return bool
* True if this is a repository.
*/
public static function isRepository(IOInterface $io, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git rev-parse --show-toplevel', $output, $dir);
return $exitCode == 0;
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationData;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationFactory;
use Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection;
/**
* Core class of the plugin.
*
* Contains the primary logic which determines the files to be fetched and
* processed.
*
* @internal
*/
class Handler {
/**
* Composer hook called before scaffolding begins.
*/
const PRE_DRUPAL_SCAFFOLD_CMD = 'pre-drupal-scaffold-cmd';
/**
* Composer hook called after scaffolding completes.
*/
const POST_DRUPAL_SCAFFOLD_CMD = 'post-drupal-scaffold-cmd';
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The scaffold options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Composer\Plugin\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The manager that keeps track of which packages are allowed to scaffold.
*
* @var \Drupal\Composer\Plugin\Scaffold\AllowedPackages
*/
protected $manageAllowedPackages;
/**
* The list of listeners that are notified after a package event.
*
* @var \Drupal\Composer\Plugin\Scaffold\PostPackageEventListenerInterface[]
*/
protected $postPackageListeners = [];
/**
* Handler constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
* @param \Composer\IO\IOInterface $io
* The Composer I/O service.
*/
public function __construct(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = new ManageOptions($composer);
$this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions);
}
/**
* Registers post-package events if the 'require' command was called.
*/
public function requireWasCalled() {
// In order to differentiate between post-package events called after
// 'composer require' vs. the same events called at other times, we will
// only install our handler when a 'require' event is detected.
$this->postPackageListeners[] = $this->manageAllowedPackages;
}
/**
* Posts package command event.
*
* We want to detect packages 'require'd that have scaffold files, but are not
* yet allowed in the top-level composer.json file.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function onPostPackageEvent(PackageEvent $event) {
foreach ($this->postPackageListeners as $listener) {
$listener->event($event);
}
}
/**
* Creates scaffold operation objects for all items in the file mappings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param array $package_file_mappings
* The package file mappings array keyed by destination path and the values
* are operation metadata arrays.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
* A list of scaffolding operation objects
*/
protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) {
$scaffold_op_factory = new OperationFactory($this->composer);
$scaffold_ops = [];
foreach ($package_file_mappings as $dest_rel_path => $data) {
$operation_data = new OperationData($dest_rel_path, $data);
$scaffold_ops[$dest_rel_path] = $scaffold_op_factory->create($package, $operation_data);
}
return $scaffold_ops;
}
/**
* Copies all scaffold files from source to destination.
*/
public function scaffold() {
// Recursively get the list of allowed packages. Only allowed packages
// may declare scaffold files. Note that the top-level composer.json file
// is implicitly allowed.
$allowed_packages = $this->manageAllowedPackages->getAllowedPackages();
if (empty($allowed_packages)) {
$this->io->write("Nothing scaffolded because no packages are allowed in the top-level composer.json file.");
return;
}
// Call any pre-scaffold scripts that may be defined.
$dispatcher = new EventDispatcher($this->composer, $this->io);
$dispatcher->dispatch(self::PRE_DRUPAL_SCAFFOLD_CMD);
// Fetch the list of file mappings from each allowed package and normalize
// them.
$file_mappings = $this->getFileMappingsFromPackages($allowed_packages);
$location_replacements = $this->manageOptions->getLocationReplacements();
$scaffold_options = $this->manageOptions->getOptions();
// Create a collection of scaffolded files to process. This determines which
// take priority and which are combined.
$scaffold_files = new ScaffoldFileCollection($file_mappings, $location_replacements);
// Get the scaffold files whose contents on disk match what we are about to
// write. We can remove these from consideration, as rewriting would be a
// no-op.
$unchanged = $scaffold_files->checkUnchanged();
$scaffold_files->filterFiles($unchanged);
// Process the list of scaffolded files.
$scaffold_results = $scaffold_files->processScaffoldFiles($this->io, $scaffold_options);
// Generate an autoload file in the document root that includes the
// autoload.php file in the vendor directory, wherever that is. Drupal
// requires this in order to easily locate relocated vendor dirs.
$web_root = $this->manageOptions->getOptions()->getLocation('web-root');
if (!GenerateAutoloadReferenceFile::autoloadFileCommitted($this->io, $this->rootPackageName(), $web_root)) {
$scaffold_results[] = GenerateAutoloadReferenceFile::generateAutoload($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath());
}
// Add the managed scaffold files to .gitignore if applicable.
$gitIgnoreManager = new ManageGitIgnore($this->io, getcwd());
$gitIgnoreManager->manageIgnored($scaffold_results, $scaffold_options);
// Call post-scaffold scripts.
$dispatcher->dispatch(self::POST_DRUPAL_SCAFFOLD_CMD);
}
/**
* Gets the path to the 'vendor' directory.
*
* @return string
* The file path of the vendor directory.
*/
protected function getVendorPath() {
$vendor_dir = $this->composer->getConfig()->get('vendor-dir');
$filesystem = new Filesystem();
return $filesystem->normalizePath(realpath($vendor_dir));
}
/**
* Gets a consolidated list of file mappings from all allowed packages.
*
* @param \Composer\Package\Package[] $allowed_packages
* A multidimensional array of file mappings, as returned by
* self::getAllowedPackages().
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[][]
* An array of destination paths => scaffold operation objects.
*/
protected function getFileMappingsFromPackages(array $allowed_packages) {
$file_mappings = [];
foreach ($allowed_packages as $package_name => $package) {
$file_mappings[$package_name] = $this->getPackageFileMappings($package);
}
return $file_mappings;
}
/**
* Gets the array of file mappings provided by a given package.
*
* @param \Composer\Package\PackageInterface $package
* The Composer package from which to get the file mappings.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
* An array of destination paths => scaffold operation objects.
*/
protected function getPackageFileMappings(PackageInterface $package) {
$options = $this->manageOptions->packageOptions($package);
if ($options->hasFileMapping()) {
return $this->createScaffoldOperations($package, $options->fileMapping());
}
// Warn the user if they allow a package that does not have any scaffold
// files. We will ignore drupal/core, though, as it is implicitly allowed,
// but might not have scaffold files (version 8.7.x and earlier).
if (!$options->hasAllowedPackages() && ($package->getName() != 'drupal/core')) {
$this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold.");
}
return [];
}
/**
* Gets the root package name.
*
* @return string
* The package name of the root project
*/
protected function rootPackageName() {
$root_package = $this->composer->getPackage();
return $root_package->getName();
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
/**
* Injects config values from an associative array into a string.
*
* @internal
*/
class Interpolator {
/**
* The character sequence that identifies the start of a token.
*
* @var string
*/
protected $startToken;
/**
* The character sequence that identifies the end of a token.
*
* @var string
*/
protected $endToken;
/**
* The associative array of replacements.
*
* @var array
*/
protected $data = [];
/**
* Interpolator constructor.
*
* @param string $start_token
* The start marker for a token, e.g. '['.
* @param string $end_token
* The end marker for a token, e.g. ']'.
*/
public function __construct($start_token = '\\[', $end_token = '\\]') {
$this->startToken = $start_token;
$this->endToken = $end_token;
}
/**
* Sets the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* Adds to the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function addData(array $data) {
$this->data = array_merge($this->data, $data);
return $this;
}
/**
* Replaces tokens in a string with values from an associative array.
*
* Tokens are surrounded by delimiters, e.g. square brackets "[key]". The
* characters that surround the key may be defined when the Interpolator is
* constructed.
*
* Example:
* If the message is 'Hello, [user.name]', then the value of the user.name
* item is fetched from the array, and the token [user.name] is replaced with
* the result.
*
* @param string $message
* Message containing tokens to be replaced.
* @param array $extra
* Data to use for interpolation in addition to whatever was provided to
* self::setData().
* @param string|bool $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string
* The message after replacements have been made.
*/
public function interpolate($message, array $extra = [], $default = '') {
$data = $extra + $this->data;
$replacements = $this->replacements($message, $data, $default);
return strtr($message, $replacements);
}
/**
* Finds the tokens that exist in a message and builds a replacement array.
*
* All of the replacements in the data array are looked up given the token
* keys from the provided message. Keys that do not exist in the configuration
* are replaced with the default value.
*
* @param string $message
* String with tokens.
* @param array $data
* Data to use for interpolation.
* @param string $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string[]
* An array of replacements to make. Keyed by tokens and the replacements
* are the values.
*/
protected function replacements($message, array $data, $default = '') {
$tokens = $this->findTokens($message);
$replacements = [];
foreach ($tokens as $sourceText => $key) {
$replacement_text = array_key_exists($key, $data) ? $data[$key] : $default;
if ($replacement_text !== FALSE) {
$replacements[$sourceText] = $replacement_text;
}
}
return $replacements;
}
/**
* Finds all of the tokens in the provided message.
*
* @param string $message
* String with tokens.
*
* @return string[]
* map of token to key, e.g. {{key}} => key
*/
protected function findTokens($message) {
$reg_ex = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
if (!preg_match_all($reg_ex, $message, $matches, PREG_SET_ORDER)) {
return [];
}
$tokens = [];
foreach ($matches as $matchSet) {
[$sourceText, $key] = $matchSet;
$tokens[$sourceText] = $key;
}
return $tokens;
}
}

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,127 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
/**
* Manage the .gitignore file.
*
* @internal
*/
class ManageGitIgnore {
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The directory where the project is located.
*
* @var string
*/
protected $dir;
/**
* ManageGitIgnore constructor.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $dir
* The directory where the project is located.
*/
public function __construct(IOInterface $io, $dir) {
$this->io = $io;
$this->dir = $dir;
}
/**
* Manages gitignore files.
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[] $files
* A list of scaffold results, each of which holds a path and whether
* or not that file is managed.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*/
public function manageIgnored(array $files, ScaffoldOptions $options) {
if (!$this->managementOfGitIgnoreEnabled($options)) {
return;
}
// Accumulate entries to add to .gitignore, sorted into buckets based on the
// location of the .gitignore file the entry should be added to.
$add_to_git_ignore = [];
foreach ($files as $scaffoldResult) {
$path = $scaffoldResult->destination()->fullPath();
$is_ignored = Git::checkIgnore($this->io, $path, $this->dir);
if (!$is_ignored) {
$is_tracked = Git::checkTracked($this->io, $path, $this->dir);
if (!$is_tracked && $scaffoldResult->isManaged()) {
$dir = realpath(dirname($path));
$name = basename($path);
$add_to_git_ignore[$dir][] = '/' . $name;
}
}
}
// Write out the .gitignore files one at a time.
foreach ($add_to_git_ignore as $dir => $entries) {
$this->addToGitIgnore($dir, $entries);
}
}
/**
* Determines whether we should manage gitignore files.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*
* @return bool
* Whether or not gitignore files should be managed.
*/
protected function managementOfGitIgnoreEnabled(ScaffoldOptions $options) {
// If the composer.json stipulates whether gitignore is managed or not, then
// follow its recommendation.
if ($options->hasGitIgnore()) {
return $options->gitIgnore();
}
// Do not manage .gitignore if there is no repository here.
if (!Git::isRepository($this->io, $this->dir)) {
return FALSE;
}
// If the composer.json did not specify whether or not .gitignore files
// should be managed, then manage them if the vendor directory is ignored.
return Git::checkIgnore($this->io, 'vendor', $this->dir);
}
/**
* Adds a set of entries to the specified .gitignore file.
*
* @param string $dir
* Path to directory where gitignore should be written.
* @param string[] $entries
* Entries to write to .gitignore file.
*/
protected function addToGitIgnore($dir, array $entries) {
sort($entries);
$git_ignore_path = $dir . '/.gitignore';
$contents = '';
// Appending to existing .gitignore files.
if (file_exists($git_ignore_path)) {
$contents = file_get_contents($git_ignore_path);
if (!empty($contents) && substr($contents, -1) != "\n") {
$contents .= "\n";
}
}
$contents .= implode("\n", $entries);
file_put_contents($git_ignore_path, $contents);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
/**
* Per-project options from the 'extras' section of the composer.json file.
*
* Projects that describe scaffold files do so via their scaffold options.
* This data is pulled from the 'drupal-scaffold' portion of the extras
* section of the project data.
*
* @internal
*/
class ManageOptions {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* ManageOptions constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Gets the root-level scaffold options for this project.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function getOptions() {
return $this->packageOptions($this->composer->getPackage());
}
/**
* Gets the scaffold options for the stipulated project.
*
* @param \Composer\Package\PackageInterface $package
* The package to fetch the scaffold options from.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function packageOptions(PackageInterface $package) {
return ScaffoldOptions::create($package->getExtra());
}
/**
* Creates an interpolator for the 'locations' element.
*
* The interpolator returned will replace a path string with the tokens
* defined in the 'locations' element.
*
* Note that only the root package may define locations.
*
* @return \Drupal\Composer\Plugin\Scaffold\Interpolator
* Interpolator that will do replacements in a string using tokens in
* 'locations' element.
*/
public function getLocationReplacements() {
return (new Interpolator())->setData($this->ensureLocations());
}
/**
* Ensures that all of the locations defined in the scaffold files exist.
*
* Create them on the filesystem if they do not.
*/
protected function ensureLocations() {
$fs = new Filesystem();
$locations = $this->getOptions()->locations() + ['web_root' => './'];
$locations = array_map(function ($location) use ($fs) {
$fs->ensureDirectoryExists($location);
$location = realpath($location);
return $location;
}, $locations);
return $locations;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Provides default behaviors for operations.
*
* @internal
*/
abstract class AbstractOperation implements OperationInterface {
/**
* Cached contents of scaffold file to be written to disk.
*
* @var string
*/
protected $contents;
/**
* {@inheritdoc}
*/
final public function contents() {
if (!isset($this->contents)) {
$this->contents = $this->generateContents();
}
return $this->contents;
}
/**
* Load the scaffold contents or otherwise generate what is needed.
*
* @return string
* The contents of the scaffold file.
*/
abstract protected function generateContents();
/**
* {@inheritdoc}
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
return $this;
}
/**
* {@inheritdoc}
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
return $this;
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to add to the beginning and/or end of a scaffold file.
*
* @internal
*/
class AppendOp extends AbstractOperation {
/**
* Identifies Append operations.
*/
const ID = 'append';
/**
* Path to the source file to prepend, if any.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $prepend;
/**
* Path to the source file to append, if any.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $append;
/**
* Path to the default data to use when appending to an empty file.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $default;
/**
* An indicator of whether the file we are appending to is managed or not.
*/
protected $managed;
/**
* An indicator of whether we are allowed to append to a non-scaffolded file.
*/
protected $forceAppend;
/**
* The contents from the file that we are prepending / appending to.
*
* @var string
*/
protected $originalContents;
/**
* Constructs an AppendOp.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $prepend_path
* The relative path to the prepend file.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $append_path
* The relative path to the append file.
* @param bool $force_append
* TRUE if is okay to append to a file that was not scaffolded.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $default_path
* The relative path to the default data.
*/
public function __construct(ScaffoldFilePath $prepend_path = NULL, ScaffoldFilePath $append_path = NULL, $force_append = FALSE, ScaffoldFilePath $default_path = NULL) {
$this->forceAppend = $force_append;
$this->prepend = $prepend_path;
$this->append = $append_path;
$this->default = $default_path;
$this->managed = TRUE;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
// Fetch the prepend contents, if provided.
$prepend_contents = '';
if (!empty($this->prepend)) {
$prepend_contents = file_get_contents($this->prepend->fullPath()) . "\n";
}
// Fetch the append contents, if provided.
$append_contents = '';
if (!empty($this->append)) {
$append_contents = "\n" . file_get_contents($this->append->fullPath());
}
// Get the original contents, or the default data if the original is empty.
$original_contents = $this->originalContents;
if (empty($original_contents) && !empty($this->default)) {
$original_contents = file_get_contents($this->default->fullPath());
}
// Attach it all together.
return $prepend_contents . $original_contents . $append_contents;
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$destination_path = $destination->fullPath();
$interpolator = $destination->getInterpolator();
// Be extra-noisy of creating a new file or appending to a non-scaffold
// file. Note that if the file already has the append contents, then the
// OperationFactory will make a SkipOp instead, and we will not get here.
if (!$this->managed) {
$message = ' - <info>NOTICE</info> Modifying existing file at <info>[dest-rel-path]</info>.';
if (!file_exists($destination_path)) {
$message = ' - <info>NOTICE</info> Creating a new file at <info>[dest-rel-path]</info>.';
}
$message .= ' Examine the contents and ensure that it came out correctly.';
$io->write($interpolator->interpolate($message));
}
// Notify that we are prepending, if there is prepend data.
if (!empty($this->prepend)) {
$this->prepend->addInterpolationData($interpolator, 'prepend');
$io->write($interpolator->interpolate(" - Prepend to <info>[dest-rel-path]</info> from <info>[prepend-rel-path]</info>"));
}
// Notify that we are appending, if there is append data.
if (!empty($this->append)) {
$this->append->addInterpolationData($interpolator, 'append');
$io->write($interpolator->interpolate(" - Append to <info>[dest-rel-path]</info> from <info>[append-rel-path]</info>"));
}
// Write the resulting data
file_put_contents($destination_path, $this->contents());
// Return a ScaffoldResult with knowledge of whether this file is managed.
return new ScaffoldResult($destination, $this->managed);
}
/**
* {@inheritdoc}
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
$this->originalContents = $existing_target->contents();
return $this;
}
/**
* {@inheritdoc}
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
// If there is no existing scaffold file at the target location, then any
// append we do will be to an unmanaged file.
$this->managed = FALSE;
// Default: do not allow an append over a file that was not scaffolded.
if (!$this->forceAppend) {
$message = " - Skip <info>[dest-rel-path]</info>: cannot append to a path that was not scaffolded unless 'force-append' property is set.";
return new SkipOp($message);
}
// If the target file does not exist, then we will allow the append to
// happen if we have default data to provide for it.
if (!file_exists($destination->fullPath())) {
if (!empty($this->default)) {
return $this;
}
$message = " - Skip <info>[dest-rel-path]</info>: no file exists at the target path, and no default data provided.";
return new SkipOp($message);
}
// If the target file DOES exist, and it already contains the append/prepend
// data, then we will skip the operation.
$existingData = file_get_contents($destination->fullPath());
if ($this->existingFileHasData($existingData, $this->append) || $this->existingFileHasData($existingData, $this->prepend)) {
$message = " - Skip <info>[dest-rel-path]</info>: the file already has the append/prepend data.";
return new SkipOp($message);
}
// Cache the original data to use during append.
$this->originalContents = $existingData;
return $this;
}
/**
* Check to see if the append/prepend data has already been applied.
*
* @param string $contents
* The contents of the target file.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $data_path
* The path to the data to append or prepend
*
* @return bool
* 'TRUE' if the append/prepend data already exists in contents.
*/
protected function existingFileHasData($contents, $data_path) {
if (empty($data_path)) {
return FALSE;
}
$data = file_get_contents($data_path->fullPath());
return strpos($contents, $data) !== FALSE;
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
/**
* Holds parameter data for operation objects during operation creation only.
*
* @internal
*/
class OperationData {
const MODE = 'mode';
const PATH = 'path';
const OVERWRITE = 'overwrite';
const PREPEND = 'prepend';
const APPEND = 'append';
const DEFAULT = 'default';
const FORCE_APPEND = 'force-append';
/**
* The parameter data.
*
* @var array
*/
protected $data;
/**
* The destination path.
*
* @var string
*/
protected $destination;
/**
* OperationData constructor.
*
* @param string $destination
* The destination path.
* @param mixed $data
* The raw data array to wrap.
*/
public function __construct($destination, $data) {
$this->destination = $destination;
$this->data = $this->normalizeScaffoldMetadata($destination, $data);
}
/**
* Gets the destination path that this operation data is associated with.
*
* @return string
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
/**
* Gets operation mode.
*
* @return string
* Operation mode.
*/
public function mode() {
return $this->data[self::MODE];
}
/**
* Checks if path exists.
*
* @return bool
* Returns true if path exists
*/
public function hasPath() {
return isset($this->data[self::PATH]);
}
/**
* Gets path.
*
* @return string
* The path.
*/
public function path() {
return $this->data[self::PATH];
}
/**
* Determines overwrite.
*
* @return bool
* Returns true if overwrite mode was selected.
*/
public function overwrite() {
return !empty($this->data[self::OVERWRITE]);
}
/**
* Determines whether 'force-append' has been set.
*
* @return bool
* Returns true if 'force-append' mode was selected.
*/
public function forceAppend() {
if ($this->hasDefault()) {
return TRUE;
}
return !empty($this->data[self::FORCE_APPEND]);
}
/**
* Checks if prepend path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasPrepend() {
return isset($this->data[self::PREPEND]);
}
/**
* Gets prepend path.
*
* @return string
* Path to prepend data
*/
public function prepend() {
return $this->data[self::PREPEND];
}
/**
* Checks if append path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasAppend() {
return isset($this->data[self::APPEND]);
}
/**
* Gets append path.
*
* @return string
* Path to append data
*/
public function append() {
return $this->data[self::APPEND];
}
/**
* Checks if default path exists.
*
* @return bool
* Returns true if there is default data available.
*/
public function hasDefault() {
return isset($this->data[self::DEFAULT]);
}
/**
* Gets default path.
*
* @return string
* Path to default data
*/
public function default() {
return $this->data[self::DEFAULT];
}
/**
* Normalizes metadata by converting literal values into arrays.
*
* Conversions performed include:
* - Boolean 'false' means "skip".
* - A string means "replace", with the string value becoming the path.
*
* @param string $destination
* The destination path for the scaffold file.
* @param mixed $value
* The metadata for this operation object, which varies by operation type.
*
* @return array
* Normalized scaffold metadata with default values.
*/
protected function normalizeScaffoldMetadata($destination, $value) {
$defaultScaffoldMetadata = [
self::MODE => ReplaceOp::ID,
self::PREPEND => NULL,
self::APPEND => NULL,
self::DEFAULT => NULL,
self::OVERWRITE => TRUE,
];
return $this->convertScaffoldMetadata($destination, $value) + $defaultScaffoldMetadata;
}
/**
* Performs the conversion-to-array step in normalizeScaffoldMetadata.
*
* @param string $destination
* The destination path for the scaffold file.
* @param mixed $value
* The metadata for this operation object, which varies by operation type.
*
* @return array
* Normalized scaffold metadata.
*/
protected function convertScaffoldMetadata($destination, $value) {
if (is_bool($value)) {
if (!$value) {
return [self::MODE => SkipOp::ID];
}
throw new \RuntimeException("File mapping {$destination} cannot be given the value 'true'.");
}
if (empty($value)) {
throw new \RuntimeException("File mapping {$destination} cannot be empty.");
}
if (is_string($value)) {
$value = [self::PATH => $value];
}
// If there is no 'mode', but there is an 'append' or a 'prepend' path,
// then the mode is 'append' (append + prepend).
if (!isset($value[self::MODE]) && (isset($value[self::APPEND]) || isset($value[self::PREPEND]))) {
$value[self::MODE] = AppendOp::ID;
}
return $value;
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Create Scaffold operation objects based on provided metadata.
*
* @internal
*/
class OperationFactory {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* OperationFactory constructor.
*
* @param \Composer\Composer $composer
* Reference to the 'Composer' object, since the Scaffold Operation Factory
* is also responsible for evaluating relative package paths as it creates
* scaffold operations.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Creates a scaffolding operation object as determined by the metadata.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object; varies by operation type.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* The scaffolding operation object (skip, replace, etc.)
*
* @throws \RuntimeException
* Exception thrown when parameter data does not identify a known scaffol
* operation.
*/
public function create(PackageInterface $package, OperationData $operation_data) {
switch ($operation_data->mode()) {
case SkipOp::ID:
return new SkipOp();
case ReplaceOp::ID:
return $this->createReplaceOp($package, $operation_data);
case AppendOp::ID:
return $this->createAppendOp($package, $operation_data);
}
throw new \RuntimeException("Unknown scaffold operation mode <comment>{$operation_data->mode()}</comment>.");
}
/**
* Creates a 'replace' scaffold op.
*
* Replace ops may copy or symlink, depending on settings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createReplaceOp(PackageInterface $package, OperationData $operation_data) {
if (!$operation_data->hasPath()) {
throw new \RuntimeException("'path' component required for 'replace' operations.");
}
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$source = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->path());
$op = new ReplaceOp($source, $operation_data->overwrite());
return $op;
}
/**
* Creates an 'append' (or 'prepend') scaffold op.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createAppendOp(PackageInterface $package, OperationData $operation_data) {
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$prepend_source_file = NULL;
$append_source_file = NULL;
$default_data_file = NULL;
if ($operation_data->hasPrepend()) {
$prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->prepend());
}
if ($operation_data->hasAppend()) {
$append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->append());
}
if ($operation_data->hasDefault()) {
$default_data_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->default());
}
if (!$this->hasContent($prepend_source_file) && !$this->hasContent($append_source_file)) {
$message = ' - Keep <info>[dest-rel-path]</info> unchanged: no content to prepend / append was provided.';
return new SkipOp($message);
}
return new AppendOp($prepend_source_file, $append_source_file, $operation_data->forceAppend(), $default_data_file);
}
/**
* Checks to see if the specified scaffold file exists and has content.
*
* @param Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $file
* Scaffold file to check.
*
* @return bool
* True if the file exists and has content.
*/
protected function hasContent(ScaffoldFilePath $file = NULL) {
if (!$file) {
return FALSE;
}
$path = $file->fullPath();
return is_file($path) && (filesize($path) > 0);
}
/**
* Gets the file path of a package.
*
* Note that if we call getInstallPath on the root package, we get the
* wrong answer (the installation manager thinks our package is in
* vendor). We therefore add special checking for this case.
*
* @param \Composer\Package\PackageInterface $package
* The package.
*
* @return string
* The file path.
*/
protected function getPackagePath(PackageInterface $package) {
if ($package->getName() == $this->composer->getPackage()->getName()) {
// This will respect the --working-dir option if Composer is invoked with
// it. There is no API or method to determine the filesystem path of
// a package's composer.json file.
return getcwd();
}
return $this->composer->getInstallationManager()->getInstallPath($package);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Interface for scaffold operation objects.
*
* @internal
*/
interface OperationInterface {
/**
* Returns the exact data that will be written to the scaffold files.
*
* @return string
* Data to be written to the scaffold location.
*/
public function contents();
/**
* Process this scaffold operation.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file's destination path.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Various options that may alter the behavior of the operation.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* Result of the scaffolding operation.
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options);
/**
* Determines what to do if operation is used at same path as a previous op.
*
* Default behavior is to scaffold this operation at the specified
* destination, ignoring whatever was there before.
*
* @param OperationInterface $existing_target
* Existing file at the destination path that we should combine with.
*
* @return OperationInterface
* The op to use at this destination.
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target);
/**
* Determines what to do if operation is used without a previous operation.
*
* Default behavior is to scaffold this operation at the specified
* destination. Most operations overwrite rather than modify existing files,
* and therefore do not need to do anything special when there is no existing
* file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file's destination path.
*
* @return OperationInterface
* The op to use at this destination.
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination);
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to copy or symlink from source to destination.
*
* @internal
*/
class ReplaceOp extends AbstractOperation {
/**
* Identifies Replace operations.
*/
const ID = 'replace';
/**
* The relative path to the source file.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $source;
/**
* Whether to overwrite existing files.
*
* @var bool
*/
protected $overwrite;
/**
* Constructs a ReplaceOp.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $sourcePath
* The relative path to the source file.
* @param bool $overwrite
* Whether to allow this scaffold file to overwrite files already at
* the destination. Defaults to TRUE.
*/
public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE) {
$this->source = $sourcePath;
$this->overwrite = $overwrite;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
return file_get_contents($this->source->fullPath());
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$fs = new Filesystem();
$destination_path = $destination->fullPath();
// Do nothing if overwrite is 'false' and a file already exists at the
// destination.
if ($this->overwrite === FALSE && file_exists($destination_path)) {
$interpolator = $destination->getInterpolator();
$io->write($interpolator->interpolate(" - Skip <info>[dest-rel-path]</info> because it already exists and overwrite is <comment>false</comment>."));
return new ScaffoldResult($destination, FALSE);
}
// Get rid of the destination if it exists, and make sure that
// the directory where it's going to be placed exists.
$fs->remove($destination_path);
$fs->ensureDirectoryExists(dirname($destination_path));
if ($options->symlink()) {
return $this->symlinkScaffold($destination, $io);
}
return $this->copyScaffold($destination, $io);
}
/**
* Copies the scaffold file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
$this->source->addInterpolationData($interpolator);
$success = file_put_contents($destination->fullPath(), $this->contents());
if (!$success) {
throw new \RuntimeException($interpolator->interpolate("Could not copy source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"));
}
$io->write($interpolator->interpolate(" - Copy <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
/**
* Symlinks the scaffold file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
try {
$fs = new Filesystem();
$fs->relativeSymlink($this->source->fullPath(), $destination->fullPath());
}
catch (\Exception $e) {
throw new \RuntimeException($interpolator->interpolate("Could not symlink source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"), [], $e);
}
$io->write($interpolator->interpolate(" - Link <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\Interpolator;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Collection of scaffold files.
*
* @internal
*/
class ScaffoldFileCollection implements \IteratorAggregate {
/**
* Nested list of all scaffold files.
*
* The top level array maps from the package name to the collection of
* scaffold files provided by that package. Each collection of scaffold files
* is keyed by destination path.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo[][]
*/
protected $scaffoldFilesByProject = [];
/**
* ScaffoldFileCollection constructor.
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[][] $file_mappings
* A multidimensional array of file mappings.
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $location_replacements
* An object with the location mappings (e.g. [web-root]).
*/
public function __construct(array $file_mappings, Interpolator $location_replacements) {
// Collection of all destination paths to be scaffolded. Used to determine
// when two projects scaffold the same file and we have to either replace or
// combine them together.
// @see OperationInterface::scaffoldOverExistingTarget().
$scaffoldFiles = [];
// Build the list of ScaffoldFileInfo objects by project.
foreach ($file_mappings as $package_name => $package_file_mappings) {
foreach ($package_file_mappings as $destination_rel_path => $op) {
$destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements);
// If there was already a scaffolding operation happening at this path,
// allow the new operation to decide how to handle the override.
// Usually, the new operation will replace whatever was there before.
if (isset($scaffoldFiles[$destination_rel_path])) {
$previous_scaffold_file = $scaffoldFiles[$destination_rel_path];
$op = $op->scaffoldOverExistingTarget($previous_scaffold_file->op());
// Remove the previous op so we only touch the destination once.
$message = " - Skip <info>[dest-rel-path]</info>: overridden in <comment>{$package_name}</comment>";
$this->scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path] = new ScaffoldFileInfo($destination, new SkipOp($message));
}
// If there is NOT already a scaffolding operation happening at this
// path, notify the scaffold operation of this fact.
else {
$op = $op->scaffoldAtNewLocation($destination);
}
// Combine the scaffold operation with the destination and record it.
$scaffold_file = new ScaffoldFileInfo($destination, $op);
$scaffoldFiles[$destination_rel_path] = $scaffold_file;
$this->scaffoldFilesByProject[$package_name][$destination_rel_path] = $scaffold_file;
}
}
}
/**
* Removes any item that has a path matching any path in the provided list.
*
* Matching is done via destination path.
*
* @param string[] $files_to_filter
* List of destination paths
*/
public function filterFiles(array $files_to_filter) {
foreach ($this->scaffoldFilesByProject as $project_name => $scaffold_files) {
foreach ($scaffold_files as $destination_rel_path => $scaffold_file) {
if (in_array($destination_rel_path, $files_to_filter, TRUE)) {
unset($scaffold_files[$destination_rel_path]);
}
}
$this->scaffoldFilesByProject[$project_name] = $scaffold_files;
if (!$this->checkListHasItemWithContent($scaffold_files)) {
unset($this->scaffoldFilesByProject[$project_name]);
}
}
}
/**
* Scans through a list of scaffold files and determines if any has contents.
*
* @param Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo[] $scaffold_files
* List of scaffold files, path: ScaffoldFileInfo
*
* @return bool
* TRUE if at least one item in the list has content
*/
protected function checkListHasItemWithContent(array $scaffold_files) {
foreach ($scaffold_files as $scaffold_file) {
$contents = $scaffold_file->op()->contents();
if (!empty($contents)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator() {
return new \ArrayIterator($this->scaffoldFilesByProject);
}
/**
* Processes the files in our collection.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO object.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
* The scaffold options.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
* The results array.
*/
public function processScaffoldFiles(IOInterface $io, ScaffoldOptions $scaffold_options) {
$results = [];
foreach ($this as $project_name => $scaffold_files) {
$io->write("Scaffolding files for <comment>{$project_name}</comment>:");
foreach ($scaffold_files as $scaffold_file) {
$results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
}
}
return $results;
}
/**
* Processes the iterator created by ScaffoldFileCollection::create().
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection $collection
* The iterator to process.
* @param \Composer\IO\IOInterface $io
* The Composer IO object.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
* The scaffold options.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
* The results array.
*
* @deprecated. Called when upgrading from the Core Composer Scaffold plugin
* version 8.8.x due to a bug in the plugin and handler classes. Do not use
* in 8.9.x or 9.x, and remove in Drupal 10.x.
*/
public static function process(ScaffoldFileCollection $collection, IOInterface $io, ScaffoldOptions $scaffold_options) {
$results = [];
foreach ($collection as $project_name => $scaffold_files) {
$io->write("Scaffolding files for <comment>{$project_name}</comment>:");
foreach ($scaffold_files as $scaffold_file) {
$results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
}
}
return $results;
}
/**
* Returns the list of files that have not changed since they were scaffolded.
*
* Note that there are two reasons a file may have changed:
* - The user modified it after it was scaffolded.
* - The package the file came to was updated, and the file is different in
* the new version.
*
* With the current scaffold code, we cannot tell the difference between the
* two. @see https://www.drupal.org/project/drupal/issues/3092563
*
* @return string[]
* List of relative paths to unchanged files on disk.
*/
public function checkUnchanged() {
$results = [];
foreach ($this as $scaffold_files) {
foreach ($scaffold_files as $scaffold_file) {
if (!$scaffold_file->hasChanged()) {
$results[] = $scaffold_file->destination()->relativePath();
}
}
}
return $results;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Record the result of a scaffold operation.
*
* @internal
*/
class ScaffoldResult {
/**
* The path to the scaffold file that was processed.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $destination;
/**
* Indicates if this scaffold file is managed by the scaffold command.
*
* @var bool
*/
protected $managed;
/**
* ScaffoldResult constructor.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* The path to the scaffold file that was processed.
* @param bool $isManaged
* (optional) Whether this result is managed. Defaults to FALSE.
*/
public function __construct(ScaffoldFilePath $destination, $isManaged = FALSE) {
$this->destination = $destination;
$this->managed = $isManaged;
}
/**
* Determines whether this scaffold file is managed.
*
* @return bool
* TRUE if this scaffold file is managed, FALSE if not.
*/
public function isManaged() {
return $this->managed;
}
/**
* Gets the destination scaffold file that this result refers to.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to skip a scaffold file (do nothing).
*
* @internal
*/
class SkipOp extends AbstractOperation {
/**
* Identifies Skip operations.
*/
const ID = 'skip';
/**
* The message to output while processing.
*
* @var string
*/
protected $message;
/**
* SkipOp constructor.
*
* @param string $message
* (optional) A custom message to output while skipping.
*/
public function __construct($message = " - Skip <info>[dest-rel-path]</info>: disabled") {
$this->message = $message;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
return '';
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$interpolator = $destination->getInterpolator();
$io->write($interpolator->interpolate($this->message));
return new ScaffoldResult($destination, FALSE);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\Plugin\Capability\CommandProvider;
use Composer\Plugin\Capable;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Drupal\Composer\Plugin\Scaffold\CommandProvider as ScaffoldCommandProvider;
/**
* Composer plugin for handling drupal scaffold.
*
* @internal
*/
class Plugin implements PluginInterface, EventSubscriberInterface, Capable {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The Composer Scaffold handler.
*
* @var \Drupal\Composer\Plugin\Scaffold\Handler
*/
protected $handler;
/**
* Record whether the 'require' command was called.
*
* @param bool
*/
protected $requireWasCalled;
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->requireWasCalled = FALSE;
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public function getCapabilities() {
return [CommandProvider::class => ScaffoldCommandProvider::class];
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Important note: We only instantiate our handler on "post" events.
return [
ScriptEvents::POST_UPDATE_CMD => 'postCmd',
ScriptEvents::POST_INSTALL_CMD => 'postCmd',
PackageEvents::POST_PACKAGE_INSTALL => 'postPackage',
PluginEvents::COMMAND => 'onCommand',
];
}
/**
* Post command event callback.
*
* @param \Composer\Script\Event $event
* The Composer event.
*/
public function postCmd(Event $event) {
$this->handler()->scaffold();
}
/**
* Post package event behavior.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function postPackage(PackageEvent $event) {
$this->handler()->onPostPackageEvent($event);
}
/**
* Pre command event callback.
*
* @param \Composer\Plugin\CommandEvent $event
* The Composer command event.
*/
public function onCommand(CommandEvent $event) {
if ($event->getCommandName() == 'require') {
if ($this->handler) {
throw new \Error('Core Scaffold Plugin handler instantiated too early. See https://www.drupal.org/project/drupal/issues/3104922');
}
$this->requireWasCalled = TRUE;
}
}
/**
* Lazy-instantiate the handler object. It is dangerous to update a Composer
* plugin if it loads any classes prior to the `composer update` operation,
* and later tries to use them in a post-update hook.
*/
protected function handler() {
if (!$this->handler) {
$this->handler = new Handler($this->composer, $this->io);
// On instantiation of our handler, notify it if the 'require' command
// was executed.
if ($this->requireWasCalled) {
$this->handler->requireWasCalled();
}
}
return $this->handler;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Installer\PackageEvent;
/**
* Interface for post package event listeners.
*
* @see \Drupal\Composer\Plugin\Scaffold\Handler::onPostPackageEvent
*
* @internal
*/
interface PostPackageEventListenerInterface {
/**
* Handles package events during a 'composer require' operation.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function event(PackageEvent $event);
}

View File

@@ -0,0 +1,517 @@
# Drupal Composer Scaffold
This project provides a composer plugin for placing scaffold files (like
`index.php`, `update.php`, …) from the `drupal/core` project into their desired
location inside the web root. Only individual files may be scaffolded with this
plugin.
The purpose of scaffolding files is to allow Drupal sites to be fully managed by
Composer, and still allow individual asset files to be placed in arbitrary
locations. The goal of doing this is to enable a properly configured composer
template to produce a file layout that exactly matches the file layout of a
Drupal 8.7.x and earlier tarball distribution. Other file layouts will also be
possible; for example, a project layout very similar to the current
[drupal-composer/drupal-project](https://github.com/drupal-composer/drupal-scaffold)
template will also be provided. When one of these projects is used, the user
should be able to use `composer require` and `composer update` on a Drupal site
immediately after untarring the downloaded archive.
Note that the dependencies of a Drupal site are only able to scaffold files if
explicitly granted that right in the top-level composer.json file. See
[allowed packages](#allowed-packages), below.
## Usage
Drupal Composer Scaffold is used by requiring `drupal/core-composer-scaffold` in your
project, and providing configuration settings in the `extra` section of your
project's composer.json file. Additional configuration from the composer.json
file of your project's dependencies is also consulted in order to scaffold the
files a project needs. Additional information may be added to the beginning or
end of scaffold files, as is commonly done to `.htaccess` and `robots.txt`
files. See [altering scaffold files](#altering-scaffold-files) for more
information.
Typically, the scaffold operations run automatically as needed, e.g. after
`composer install`, so it is usually not necessary to do anything different
to scaffold a project once the configuration is set up in the project
composer.json file, as described below. To scaffold files directly, run:
```
composer drupal:scaffold
```
### Allowed Packages
Scaffold files are stored inside of projects that are required from the main
project's composer.json file as usual. The scaffolding operation happens after
`composer install`, and involves copying or symlinking the desired assets to
their destination location. In order to prevent arbitrary dependencies from
copying files via the scaffold mechanism, only those projects that are
specifically permitted by the top-level project will be used to scaffold files.
Example: Permit scaffolding from the project `upstream/project`
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"upstream/project"
],
...
}
}
```
Allowing a package to scaffold files also permits it to delegate permission to
scaffold to any project that it requires itself. This allows a package to
organize its scaffold assets as it sees fit. For example, if `upstream/project`
stores its assets in a subproject `upstream/assets`, `upstream/assets` would
implicitly be allowed to scaffold files.
It is possible for a project to obtain scaffold files from multiple projects.
For example, a Drupal project using a distribution, and installing on a specific
web hosting service provider might take its scaffold files from:
- Drupal core
- Its distribution
- A project provided by the hosting provider
- The project itself
Each project allowed to scaffold by the top-level project will be used in turn,
with projects declared later in the `allowed-packages` list taking precedence
over the projects named before. `drupal/core` is implicitly allowed and will be
placed at the top of the list. The top-level composer.json itself is also
implicitly allowed to scaffold files, and its scaffold files have highest
priority.
### Defining Project Locations
The top-level project in turn must define where the web root is located. It does
so via the `locations` mapping, as shown below:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./docroot"
},
...
}
}
```
This makes it possible to configure a project with different file layouts; for
example, either the `drupal/drupal` file layout or the
`drupal-composer/drupal-project` file layout could be used to set up a project.
If a web-root is not explicitly defined, then it will default to `./`.
### Altering Scaffold Files
Sometimes, a project might wish to use a scaffold file provided by a dependency,
but alter it in some way. Two forms of alteration are supported: appending and
patching.
The example below shows a project that appends additional entries onto the end
of the `robots.txt` file provided by `drupal/core`:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": {
"append": "assets/my-robots-additions.txt",
}
}
}
}
```
It is also possible to prepend to a scaffold file instead of, or in addition to
appending by including a "prepend" entry that provides the relative path to the
file to prepend to the scaffold file.
The example below demonstrates the use of the `post-drupal-scaffold-cmd` hook
to patch the `.htaccess` file using a patch.
```
"name": "my/project",
...
"scripts": {
"post-drupal-scaffold-cmd": [
"cd docroot && patch -p1 <../patches/htaccess-ssl.patch"
]
}
```
### Defining Scaffold Files
The placement of scaffold assets is under the control of the project that
provides them, but the location is always relative to some directory defined by
the root project -- usually the web root. For example, the scaffold file
`robots.txt` is copied from its source location, `assets/robots.txt` into the
web root in the snippet below.
```
{
"name": "drupal/assets",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": "assets/robots.txt",
...
}
}
}
}
```
### Excluding Scaffold Files
Sometimes, a project might prefer to entirely replace a scaffold file provided
by a dependency, and receive no further updates for it. This can be done by
setting the value for the scaffold file to exclude to `false`:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": false
}
}
}
```
If possible, use the `append` and `prepend` directives as explained in [altering
scaffold files](#altering-scaffold-files), above. Excluding a file means that
your project will not get any bug fixes or other updates to files that are
modified locally.
### Overwrite
By default, scaffold files overwrite whatever content exists at the target
location. Sometimes a project may wish to provide the initial contents for a
file that will not be changed in subsequent updates. This can be done by setting
the `overwrite` flag to `false`, as shown in the example below:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/sites/default/settings.php": {
"mode": "replace",
"path": "assets/sites/default/settings.php",
"overwrite": false
}
}
}
}
}
```
Note that the `overwrite` directive is intended to be used by starter kits,
service providers, and so on. Individual Drupal sites should exclude the file
by setting its value to false instead.
### Autoload File
The scaffold tool automatically creates the required `autoload.php` file at the
Drupal root as part of the scaffolding operation. This file should not be
modified or customized in any way. If it is committed to the repository, though,
then the scaffold tool will stop managing it. If the location of the `vendor`
directory is changed for any reason, and the `autoload.php` file has been
committed to the repository, manually delete it and then run `composer install`
to update it.
## Specifications
Reference section for the configuration directives for the "drupal-scaffold"
section of the "extra" section of a `composer.json` file appear below.
### allowed-packages
The `allowed-packages` configuration setting contains an ordered list of package
names that will be used during the scaffolding phase.
```
"allowed-packages": [
"example/assets",
],
```
### file-mapping
The `file-mapping` configuration setting consists of a map from the destination
path of the file to scaffold to a set of properties that control how the file
should be scaffolded.
The available properties are as follows:
- mode: One of "replace", "append" or "skip".
- path: The path to the source file to write over the destination file.
- prepend: The path to the source file to prepend to the destination file, which
must always be a scaffold file provided by some other project.
- append: Like `prepend`, but appends content rather than prepends.
- overwrite: If `false`, prevents a `replace` from happening if the destination
already exists.
The mode may be inferred from the other properties. If the mode is not
specified, then the following defaults will be supplied:
- replace: Selected if a `path` property is present, or if the entry's value is
a string rather than a property set.
- append: Selected if a `prepend` or `append` property is present.
- skip: Selected if the entry's value is a boolean `false`.
Examples:
```
"file-mapping": {
"[web-root]/sites/default/default.settings.php": {
"mode": "replace",
"path": "assets/sites/default/default.settings.php",
"overwrite": true
},
"[web-root]/sites/default/settings.php": {
"mode": "replace",
"path": "assets/sites/default/settings.php",
"overwrite": false
},
"[web-root]/robots.txt": {
"mode": "append",
"prepend": "assets/robots-prequel.txt",
"append": "assets/robots-append.txt"
},
"[web-root]/.htaccess": {
"mode": "skip",
}
}
```
The short-form of the above example would be:
```
"file-mapping": {
"[web-root]/sites/default/default.settings.php": "assets/sites/default/default.settings.php",
"[web-root]/sites/default/settings.php": {
"path": "assets/sites/default/settings.php",
"overwrite": false
},
"[web-root]/robots.txt": {
"prepend": "assets/robots-prequel.txt",
"append": "assets/robots-append.txt"
},
"[web-root]/.htaccess": false
}
```
Note that there is no distinct "prepend" mode; "append" mode is used to both
append and prepend to scaffold files. The reason for this is that scaffold file
entries are identified in the file-mapping section keyed by their destination
path, and it is not possible for multiple entries to have the same key. If
"prepend" were a separate mode, then it would not be possible to both prepend
and append to the same file.
By default, append operations may only be applied to files that were scaffolded
by a previously evaluated project. If the `force-append` attribute is added to
an `append` operation, though, then the append will be made to non-scaffolded
files if and only if the append text does not already appear in the file. When
using this mode, it is also possible to provide default contents to use in the
event that the destination file is entirely missing.
The example below demonstrates scaffolding a settings-custom.php file, and
including it from the existing `settings.php` file.
```
"file-mapping": {
"[web-root]/sites/default/settings-custom.php": "assets/settings-custom.php",
"[web-root]/sites/default/settings.php": {
"append": "assets/include-settings-custom.txt",
"force-append": true,
"default": "assets/initial-default-settings.txt"
}
}
```
Note that the example above still works if used with a project that scaffolds
the settings.php file.
### gitignore
The `gitignore` configuration setting controls whether or not this plugin will
manage `.gitignore` files for files written during the scaffold operation.
- true: `.gitignore` files will be updated when scaffold files are written.
- false: `.gitignore` files will never be modified.
- Not set: `.gitignore` files will be updated if the target directory is a local
working copy of a git repository, and the `vendor` directory is ignored
in that repository.
### locations
The `locations` configuration setting contains a list of named locations that
may be used in placing scaffold files. The only required location is `web-root`.
Other locations may also be defined if desired.
```
"locations": {
"web-root": "./docroot"
},
```
### symlink
The `symlink` property causes `replace` operations to make a symlink to the
source file rather than copying it. This is useful when doing core development,
as the symlink files themselves should not be edited. Note that `append`
operations override the `symlink` option, to prevent the original scaffold
assets from being altered.
```
"symlink": true,
```
## Managing Scaffold Files
Scaffold files should be treated the same way that the `vendor` directory is
handled. If you need to commit `vendor` (e.g. in order to deploy your site),
then you should also commit your scaffold files. You should not commit your
`vendor` directory or scaffold files unless it is necessary.
If a dependency provides a scaffold file with `overwrite` set to `false`, that
file should be committed to your repository.
By default, `.gitignore` files will be automatically updated if needed when
scaffold files are written. See the `gitignore` setting in the Specifications
section above.
## Examples
Some full-length examples appear below.
Sample composer.json for a project that relies on packages that use composer-scaffold:
```
{
"name": "my/project",
"require": {
"drupal/core-composer-scaffold": "*",
"composer/installers": "^1.9",
"cweagans/composer-patches": "^1.6.5",
"drupal/core": "^8.8.x-dev",
"service-provider/d8-scaffold-files": "^1"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./docroot"
},
"symlink": true,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/robots-default.txt"
}
}
}
}
```
Sample composer.json for drupal/core, with assets placed in a different project:
```
{
"name": "drupal/core",
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"drupal/assets",
]
}
}
}
```
Sample composer.json for composer-scaffold files in drupal/assets:
```
{
"name": "drupal/assets",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/.csslintrc": "assets/.csslintrc",
"[web-root]/.editorconfig": "assets/.editorconfig",
"[web-root]/.eslintignore": "assets/.eslintignore",
"[web-root]/.eslintrc.json": "assets/.eslintrc.json",
"[web-root]/.gitattributes": "assets/.gitattributes",
"[web-root]/.ht.router.php": "assets/.ht.router.php",
"[web-root]/.htaccess": "assets/.htaccess",
"[web-root]/sites/default/default.services.yml": "assets/default.services.yml",
"[web-root]/sites/default/default.settings.php": "assets/default.settings.php",
"[web-root]/sites/example.settings.local.php": "assets/example.settings.local.php",
"[web-root]/sites/example.sites.php": "assets/example.sites.php",
"[web-root]/index.php": "assets/index.php",
"[web-root]/robots.txt": "assets/robots.txt",
"[web-root]/update.php": "assets/update.php",
"[web-root]/web.config": "assets/web.config"
}
}
}
}
```
Sample composer.json for a library that implements composer-scaffold:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/sites/default/settings.php": "assets/sites/default/settings.php"
}
}
}
}
```
Append to robots.txt:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": {
"append": "assets/my-robots-additions.txt",
}
}
}
}
}
```
Patch a file after it's copied:
```
"post-drupal-scaffold-cmd": [
"cd docroot && patch -p1 <../patches/htaccess-ssl.patch"
]
```
## Related Plugins
### drupal-composer/drupal-scaffold
Previous versions of Drupal Composer Scaffold (see community project,
[drupal-composer/drupal-scaffold](https://github.com/drupal-composer/drupal-project))
downloaded each scaffold file directly from its distribution server (e.g.
`https://git.drupalcode.org`) to the desired destination directory. This was
necessary, because there was no subtree split of the scaffold files available.
Copying the scaffold assets from projects already downloaded by Composer is more
effective, as downloading and unpacking archive files is more efficient than
downloading each scaffold file individually.
### composer/installers
The [composer/installers](https://github.com/composer/installers) plugin is
similar to this plugin in that it allows dependencies to be installed in
locations other than the `vendor` directory. However, Composer and the
`composer/installers` plugin have a limitation that one project cannot be moved
inside of another project. Therefore, if you use `composer/installers` to place
Drupal modules inside the directory `web/modules/contrib`, then you cannot also
use `composer/installers` to place files such as `index.php` and `robots.txt`
into the `web` directory. The drupal-scaffold plugin was created to work around
this limitation.

View File

@@ -0,0 +1,139 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface;
/**
* Data object that keeps track of one scaffold file.
*
* Scaffold files are identified primarily by their destination path. Each
* scaffold file also has an 'operation' object that controls how the scaffold
* file will be placed (e.g. via copy or symlink, or maybe by appending multiple
* files together). The operation may have one or more source files.
*
* @internal
*/
class ScaffoldFileInfo {
/**
* The path to the destination.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $destination;
/**
* The operation used to create the destination.
*
* @var \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
*/
protected $op;
/**
* Constructs a ScaffoldFileInfo object.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* The full and relative paths to the destination file and the package
* defining it.
* @param \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface $op
* Operations object that will handle scaffolding operations.
*/
public function __construct(ScaffoldFilePath $destination, OperationInterface $op) {
$this->destination = $destination;
$this->op = $op;
}
/**
* Gets the Scaffold operation.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* Operations object that handles scaffolding (copy, make symlink, etc).
*/
public function op() {
return $this->op;
}
/**
* Gets the package name.
*
* @return string
* The name of the package this scaffold file info was collected from.
*/
public function packageName() {
return $this->destination->packageName();
}
/**
* Gets the destination.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* The scaffold path to the destination file.
*/
public function destination() {
return $this->destination;
}
/**
* Determines if this scaffold file has been overridden by another package.
*
* @param string $providing_package
* The name of the package that provides the scaffold file at this location,
* as returned by self::findProvidingPackage()
*
* @return bool
* Whether this scaffold file if overridden or removed.
*/
public function overridden($providing_package) {
return $this->packageName() !== $providing_package;
}
/**
* Replaces placeholders in a message.
*
* @param string $message
* Message with placeholders to fill in.
* @param array $extra
* Additional data to merge with the interpolator.
* @param mixed $default
* Default value to use for missing placeholders, or FALSE to keep them.
*
* @return string
* Interpolated string with placeholders replaced.
*/
public function interpolate($message, array $extra = [], $default = FALSE) {
$interpolator = $this->destination->getInterpolator();
return $interpolator->interpolate($message, $extra, $default);
}
/**
* Moves a single scaffold file from source to destination.
*
* @param \Composer\IO\IOInterface $io
* The scaffold file to be processed.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Assorted operational options, e.g. whether the destination should be a
* symlink.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
public function process(IOInterface $io, ScaffoldOptions $options) {
return $this->op()->process($this->destination, $io, $options);
}
/**
* Returns TRUE if the target does not exist or has changed.
*
* @return bool
*/
final public function hasChanged() {
$path = $this->destination()->fullPath();
if (!file_exists($path)) {
return TRUE;
}
return $this->op()->contents() !== file_get_contents($path);
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Util\Filesystem;
/**
* Manage the path to a file to scaffold.
*
* Both the relative and full path to the file is maintained so that the shorter
* name may be used in progress and error messages, as needed. The name of the
* package that provided the file path is also recorded for the same reason.
*
* ScaffoldFilePaths may be used to represent destination scaffold files, or the
* source files used to create them. Static factory methods named
* destinationPath and sourcePath, respectively, are provided to create
* ScaffoldFilePath objects.
*
* @internal
*/
class ScaffoldFilePath {
/**
* The type of scaffold file this is,'autoload', 'dest' or 'src'.
*
* @var string
*/
protected $type;
/**
* The name of the package containing the file.
*
* @var string
*/
protected $packageName;
/**
* The relative path to the file.
*
* @var string
*/
protected $relativePath;
/**
* The full path to the file.
*
* @var string
*/
protected $fullPath;
/**
* ScaffoldFilePath constructor.
*
* @param string $path_type
* The type of scaffold file this is,'autoload', 'dest' or 'src'.
* @param string $package_name
* The name of the package containing the file.
* @param string $rel_path
* The relative path to the file.
* @param string $full_path
* The full path to the file.
*/
public function __construct($path_type, $package_name, $rel_path, $full_path) {
$this->type = $path_type;
$this->packageName = $package_name;
$this->relativePath = $rel_path;
$this->fullPath = $full_path;
// Ensure that the full path really is a full path. We do not use
// 'realpath' here because the file specified by the full path might
// not exist yet.
$fs = new Filesystem();
if (!$fs->isAbsolutePath($this->fullPath)) {
$this->fullPath = getcwd() . '/' . $this->fullPath;
}
}
/**
* Gets the name of the package this source file was pulled from.
*
* @return string
* Name of package.
*/
public function packageName() {
return $this->packageName;
}
/**
* Gets the relative path to the source file (best to use in messages).
*
* @return string
* Relative path to file.
*/
public function relativePath() {
return $this->relativePath;
}
/**
* Gets the full path to the source file.
*
* @return string
* Full path to file.
*/
public function fullPath() {
return $this->fullPath;
}
/**
* Converts the relative source path into an absolute path.
*
* The path returned will be relative to the package installation location.
*
* @param string $package_name
* The name of the package containing the source file. Only used for error
* messages.
* @param string $package_path
* The installation path of the package containing the source file.
* @param string $destination
* Destination location provided as a relative path. Only used for error
* messages.
* @param string $source
* Source location provided as a relative path.
*
* @return self
* Object wrapping the relative and absolute path to the source file.
*/
public static function sourcePath($package_name, $package_path, $destination, $source) {
// Complain if there is no source path.
if (empty($source)) {
throw new \RuntimeException("No scaffold file path given for {$destination} in package {$package_name}.");
}
// Calculate the full path to the source scaffold file.
$source_full_path = $package_path . '/' . $source;
if (!file_exists($source_full_path)) {
throw new \RuntimeException("Scaffold file {$source} not found in package {$package_name}.");
}
if (is_dir($source_full_path)) {
throw new \RuntimeException("Scaffold file {$source} in package {$package_name} is a directory; only files may be scaffolded.");
}
return new self('src', $package_name, $source, $source_full_path);
}
/**
* Converts the relative destination path into an absolute path.
*
* Any placeholders in the destination path, e.g. '[web-root]', will be
* replaced using the provided location replacements interpolator.
*
* @param string $package_name
* The name of the package defining the destination path.
* @param string $destination
* The relative path to the destination file being scaffolded.
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $location_replacements
* Interpolator that includes the [web-root] and any other available
* placeholder replacements.
*
* @return self
* Object wrapping the relative and absolute path to the destination file.
*/
public static function destinationPath($package_name, $destination, Interpolator $location_replacements) {
$dest_full_path = $location_replacements->interpolate($destination);
return new self('dest', $package_name, $destination, $dest_full_path);
}
/**
* Adds data about the relative and full path to the provided interpolator.
*
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $interpolator
* Interpolator to add data to.
* @param string $name_prefix
* (optional) Prefix to add before -rel-path and -full-path item names.
* Defaults to path type provided when constructing this object.
*/
public function addInterpolationData(Interpolator $interpolator, $name_prefix = '') {
if (empty($name_prefix)) {
$name_prefix = $this->type;
}
$data = [
'package-name' => $this->packageName(),
"{$name_prefix}-rel-path" => $this->relativePath(),
"{$name_prefix}-full-path" => $this->fullPath(),
];
$interpolator->addData($data);
}
/**
* Interpolate a string using the data from this scaffold file info.
*
* @param string $name_prefix
* (optional) Prefix to add before -rel-path and -full-path item names.
* Defaults to path type provided when constructing this object.
*
* @return \Drupal\Composer\Plugin\Scaffold\Interpolator
* An interpolator for making string replacements.
*/
public function getInterpolator($name_prefix = '') {
$interpolator = new Interpolator();
$this->addInterpolationData($interpolator, $name_prefix);
return $interpolator;
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Drupal\Composer\Plugin\Scaffold;
/**
* Per-project options from the 'extras' section of the composer.json file.
*
* Projects that describe scaffold files do so via their scaffold options. This
* data is pulled from the 'drupal-scaffold' portion of the extras section of
* the project data.
*
* @internal
*/
class ScaffoldOptions {
/**
* The raw data from the 'extras' section of the top-level composer.json file.
*
* @var array
*/
protected $options = [];
/**
* ScaffoldOptions constructor.
*
* @param array $options
* The scaffold options taken from the 'drupal-scaffold' section.
*/
protected function __construct(array $options) {
$this->options = $options + [
"allowed-packages" => [],
"locations" => [],
"symlink" => FALSE,
"file-mapping" => [],
];
// Define any default locations.
$this->options['locations'] += [
'project-root' => '.',
'web-root' => '.',
];
}
/**
* Determines if the provided 'extras' section has scaffold options.
*
* @param array $extras
* The contents of the 'extras' section.
*
* @return bool
* True if scaffold options have been declared
*/
public static function hasOptions(array $extras) {
return array_key_exists('drupal-scaffold', $extras);
}
/**
* Creates a scaffold options object.
*
* @param array $extras
* The contents of the 'extras' section.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
public static function create(array $extras) {
$options = static::hasOptions($extras) ? $extras['drupal-scaffold'] : [];
return new self($options);
}
/**
* Creates a new scaffold options object with some values overridden.
*
* @param array $options
* Override values.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
protected function override(array $options) {
return new self($options + $this->options);
}
/**
* Creates a new scaffold options object with an overridden 'symlink' value.
*
* @param bool $symlink
* Whether symlinking should be enabled or not.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
public function overrideSymlink($symlink) {
return $this->override(['symlink' => $symlink]);
}
/**
* Determines whether any allowed packages were defined.
*
* @return bool
* Whether there are allowed packages
*/
public function hasAllowedPackages() {
return !empty($this->allowedPackages());
}
/**
* Gets allowed packages from these options.
*
* @return array
* The list of allowed packages
*/
public function allowedPackages() {
return $this->options['allowed-packages'];
}
/**
* Gets the location mapping table, e.g. 'webroot' => './'.
*
* @return array
* A map of name : location values
*/
public function locations() {
return $this->options['locations'];
}
/**
* Determines whether a given named location is defined.
*
* @param string $name
* The location name to search for.
*
* @return bool
* True if the specified named location exist.
*/
protected function hasLocation($name) {
return array_key_exists($name, $this->locations());
}
/**
* Gets a specific named location.
*
* @param string $name
* The name of the location to fetch.
*
* @return string
* The value of the provided named location
*/
public function getLocation($name) {
return $this->hasLocation($name) ? $this->locations()[$name] : FALSE;
}
/**
* Determines if symlink mode is set.
*
* @return bool
* Whether or not 'symlink' mode
*/
public function symlink() {
return $this->options['symlink'];
}
/**
* Determines if there are file mappings.
*
* @return bool
* Whether or not the scaffold options contain any file mappings
*/
public function hasFileMapping() {
return !empty($this->fileMapping());
}
/**
* Returns the actual file mappings.
*
* @return array
* File mappings for just this config type.
*/
public function fileMapping() {
return $this->options['file-mapping'];
}
/**
* Determines if there is defined a value for the 'gitignore' option.
*
* @return bool
* Whether or not there is a 'gitignore' option setting
*/
public function hasGitIgnore() {
return isset($this->options['gitignore']);
}
/**
* Gets the value of the 'gitignore' option.
*
* @return bool
* The 'gitignore' option, or TRUE if undefined.
*/
public function gitIgnore() {
return $this->hasGitIgnore() ? $this->options['gitignore'] : TRUE;
}
}

View File

@@ -0,0 +1,18 @@
HOW-TO: Test this Drupal composer plugin
In order to test this plugin, you'll need to get the entire Drupal repo and
run the tests there.
You'll find the tests under core/tests/Drupal/Tests/Composer/Plugin.
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can find more information about running PHPUnit tests with Drupal here:
https://www.drupal.org/node/2116263
Each component in the Drupal\Composer\Plugin namespace has its own annotated test
group. You can use this group to run only the tests for this component. Like
this:
$ ./vendor/bin/phpunit -c core --group Scaffold

View File

@@ -0,0 +1,32 @@
{
"name": "drupal/core-composer-scaffold",
"description": "A flexible Composer project scaffold builder.",
"type": "composer-plugin",
"keywords": ["drupal"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0-or-later",
"require": {
"composer-plugin-api": "^1 || ^2",
"php": ">=7.3.0"
},
"conflict": {
"drupal-composer/drupal-scaffold": "*"
},
"autoload": {
"psr-4": {
"Drupal\\Composer\\Plugin\\Scaffold\\": ""
}
},
"extra": {
"class": "Drupal\\Composer\\Plugin\\Scaffold\\Plugin",
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"config": {
"sort-packages": true
},
"require-dev": {
"composer/composer": "^1.8@stable"
}
}