3 namespace DrupalComposer\DrupalScaffold;
5 use Composer\Script\Event;
6 use Composer\Installer\PackageEvent;
7 use Composer\Plugin\CommandEvent;
9 use Composer\DependencyResolver\Operation\InstallOperation;
10 use Composer\DependencyResolver\Operation\UpdateOperation;
11 use Composer\EventDispatcher\EventDispatcher;
12 use Composer\IO\IOInterface;
13 use Composer\Package\PackageInterface;
14 use Composer\Semver\Semver;
15 use Composer\Util\Filesystem;
16 use Composer\Util\RemoteFilesystem;
17 use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
20 * Core class of the plugin, contains all logic which files should be fetched.
24 const PRE_DRUPAL_SCAFFOLD_CMD = 'pre-drupal-scaffold-cmd';
25 const POST_DRUPAL_SCAFFOLD_CMD = 'post-drupal-scaffold-cmd';
28 * @var \Composer\Composer
33 * @var \Composer\IO\IOInterface
40 * A boolean indicating if progress should be displayed.
45 * @var \Composer\Package\PackageInterface
47 protected $drupalCorePackage;
50 * Handler constructor.
52 * @param \Composer\Composer $composer
53 * @param \Composer\IO\IOInterface $io
55 public function __construct(Composer $composer, IOInterface $io) {
56 $this->composer = $composer;
58 $this->progress = TRUE;
60 // Pre-load all of our sources so that we do not run up
61 // against problems in `composer update` operations.
65 protected function manualLoad() {
70 'DrupalScaffoldCommand',
72 'PrestissimoFileFetcher',
75 foreach ($classes as $src) {
76 if (!class_exists('\\DrupalComposer\\DrupalScaffold\\' . $src)) {
77 include "{$src_dir}/{$src}.php";
86 protected function getCorePackage($operation) {
87 if ($operation instanceof InstallOperation) {
88 $package = $operation->getPackage();
90 elseif ($operation instanceof UpdateOperation) {
91 $package = $operation->getTargetPackage();
93 if (isset($package) && $package instanceof PackageInterface && $package->getName() == 'drupal/core') {
100 * Get the command options.
102 * @param \Composer\Plugin\CommandEvent $event
104 public function onCmdBeginsEvent(CommandEvent $event) {
105 if ($event->getInput()->hasOption('no-progress')) {
106 $this->progress = !($event->getInput()->getOption('no-progress'));
109 $this->progress = TRUE;
114 * Marks scaffolding to be processed after an install or update command.
116 * @param \Composer\Installer\PackageEvent $event
118 public function onPostPackageEvent(PackageEvent $event) {
119 $package = $this->getCorePackage($event->getOperation());
121 // By explicitly setting the core package, the onPostCmdEvent() will
122 // process the scaffolding automatically.
123 $this->drupalCorePackage = $package;
128 * Post install command event to execute the scaffolding.
130 * @param \Composer\Script\Event $event
132 public function onPostCmdEvent(Event $event) {
133 // Only install the scaffolding if drupal/core was installed,
134 // AND there are no scaffolding files present.
135 if (isset($this->drupalCorePackage)) {
136 $this->downloadScaffold();
137 // Generate the autoload.php file after generating the scaffold files.
138 $this->generateAutoload();
143 * Downloads drupal scaffold files for the current process.
145 public function downloadScaffold() {
146 $drupalCorePackage = $this->getDrupalCorePackage();
147 $webroot = realpath($this->getWebRoot());
149 // Collect options, excludes and settings files.
150 $options = $this->getOptions();
151 $files = array_diff($this->getIncludes(), $this->getExcludes());
153 // Call any pre-scaffold scripts that may be defined.
154 $dispatcher = new EventDispatcher($this->composer, $this->io);
155 $dispatcher->dispatch(self::PRE_DRUPAL_SCAFFOLD_CMD);
157 $version = $this->getDrupalCoreVersion($drupalCorePackage);
159 $remoteFs = new RemoteFilesystem($this->io);
161 $fetcher = new PrestissimoFileFetcher($remoteFs, $options['source'], $this->io, $this->progress, $this->composer->getConfig());
162 $fetcher->setFilenames(array_combine($files, $files));
163 $fetcher->fetch($version, $webroot, TRUE);
165 $fetcher->setFilenames($this->getInitial());
166 $fetcher->fetch($version, $webroot, FALSE);
168 // Call post-scaffold scripts.
169 $dispatcher->dispatch(self::POST_DRUPAL_SCAFFOLD_CMD);
173 * Generate the autoload file at the project root. Include the
174 * autoload file that Composer generated.
176 public function generateAutoload() {
177 $vendorPath = $this->getVendorPath();
178 $webroot = $this->getWebRoot();
180 // Calculate the relative path from the webroot (location of the
181 // project autoload.php) to the vendor directory.
182 $fs = new SymfonyFilesystem();
183 $relativeVendorPath = $fs->makePathRelative($vendorPath, realpath($webroot));
185 $fs->dumpFile($webroot . "/autoload.php", $this->autoLoadContents($relativeVendorPath));
189 * Build the contents of the autoload file.
193 protected function autoLoadContents($relativeVendorPath) {
194 $relativeVendorPath = rtrim($relativeVendorPath, '/');
196 $autoloadContents = <<<EOF
201 * Includes the autoloader created by Composer.
203 * This file was generated by drupal-composer/drupal-scaffold.
204 * https://github.com/drupal-composer/drupal-scaffold
208 * @see core/install.php
209 * @see core/rebuild.php
210 * @see core/modules/statistics/statistics.php
213 return require __DIR__ . '/$relativeVendorPath/autoload.php';
216 return $autoloadContents;
220 * Get the path to the 'vendor' directory.
224 public function getVendorPath() {
225 $config = $this->composer->getConfig();
226 $filesystem = new Filesystem();
227 $filesystem->ensureDirectoryExists($config->get('vendor-dir'));
228 $vendorPath = $filesystem->normalizePath(realpath($config->get('vendor-dir')));
234 * Look up the Drupal core package object, or return it from where we cached
235 * it in the $drupalCorePackage field.
237 * @return \Composer\Package\PackageInterface
239 public function getDrupalCorePackage() {
240 if (!isset($this->drupalCorePackage)) {
241 $this->drupalCorePackage = $this->getPackage('drupal/core');
243 return $this->drupalCorePackage;
247 * Returns the Drupal core version for the given package.
249 * @param \Composer\Package\PackageInterface $drupalCorePackage
253 protected function getDrupalCoreVersion(PackageInterface $drupalCorePackage) {
254 $version = $drupalCorePackage->getPrettyVersion();
255 if ($drupalCorePackage->getStability() == 'dev' && substr($version, -4) == '-dev') {
256 $version = substr($version, 0, -4);
263 * Retrieve the path to the web root.
267 public function getWebRoot() {
268 $drupalCorePackage = $this->getDrupalCorePackage();
269 $installationManager = $this->composer->getInstallationManager();
270 $corePath = $installationManager->getInstallPath($drupalCorePackage);
271 // Webroot is the parent path of the drupal core installation path.
272 $webroot = dirname($corePath);
278 * Retrieve a package from the current composer process.
280 * @param string $name
281 * Name of the package to get from the current composer installation.
283 * @return \Composer\Package\PackageInterface
285 protected function getPackage($name) {
286 return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
290 * Retrieve excludes from optional "extra" configuration.
294 protected function getExcludes() {
295 return $this->getNamedOptionList('excludes', 'getExcludesDefault');
299 * Retrieve list of additional settings files from optional "extra" configuration.
303 protected function getIncludes() {
304 return $this->getNamedOptionList('includes', 'getIncludesDefault');
308 * Retrieve list of initial files from optional "extra" configuration.
312 protected function getInitial() {
313 return $this->getNamedOptionList('initial', 'getInitialDefault');
317 * Retrieve a named list of options from optional "extra" configuration.
318 * Respects 'omit-defaults', and either includes or does not include the
319 * default values, as requested.
323 protected function getNamedOptionList($optionName, $defaultFn) {
324 $options = $this->getOptions($this->composer);
326 if (empty($options['omit-defaults'])) {
327 $result = $this->$defaultFn();
329 $result = array_merge($result, (array) $options[$optionName]);
335 * Retrieve excludes from optional "extra" configuration.
339 protected function getOptions() {
340 $extra = $this->composer->getPackage()->getExtra() + ['drupal-scaffold' => []];
341 $options = $extra['drupal-scaffold'] + [
342 'omit-defaults' => FALSE,
346 'source' => 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}',
347 // Github: https://raw.githubusercontent.com/drupal/drupal/{version}/{path}
353 * Holds default excludes.
355 protected function getExcludesDefault() {
360 * Holds default settings files list.
362 protected function getIncludesDefault() {
363 $version = $this->getDrupalCoreVersion($this->getDrupalCorePackage());
364 list($major, $minor) = explode('.', $version, 3);
365 $version = "$major.$minor";
370 * @see https://cgit.drupalcode.org/drupal/tree/?h=8.3.x
380 'sites/default/default.settings.php',
381 'sites/default/default.services.yml',
382 'sites/development.services.yml',
383 'sites/example.settings.local.php',
384 'sites/example.sites.php',
389 // Version specific variations.
390 if (Semver::satisfies($version, '<8.3')) {
391 $common[] = '.eslintrc';
393 if (Semver::satisfies($version, '>=8.3')) {
394 $common[] = '.eslintrc.json';
396 if (Semver::satisfies($version, '>=8.5')) {
397 $common[] = '.ht.router.php';
405 * Holds default initial files.
407 protected function getInitialDefault() {