250 lines
8.6 KiB
PHP
250 lines
8.6 KiB
PHP
<?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();
|
|
}
|
|
|
|
}
|