Skip to content

Commit

Permalink
Drastically simplify using Composer Runtime API
Browse files Browse the repository at this point in the history
  • Loading branch information
weitzman committed Nov 11, 2023
1 parent c27bbdf commit 8612833
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 964 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/composer.lock
/vendor/
/.phpunit.result.cache
/web
34 changes: 24 additions & 10 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webflo/drupal-finder",
"description": "Helper class to locate a Drupal installation from a given path.",
"description": "Helper class to locate a Drupal installation.",
"license": "GPL-2.0-or-later",
"type": "library",
"authors": [
Expand All @@ -10,20 +10,34 @@
}
],
"require": {
"ext-json": "*"
},
"autoload": {
"classmap": [
"src/DrupalFinder.php"
]
"composer-runtime-api": "^2.2"
},
"autoload-dev": {
"psr-4": {
"DrupalFinder\\Tests\\": "tests/"
"DrupalFinder\\Tests\\": "tests/",
"DrupalFinder\\": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^8.5.14",
"mikey179/vfsstream": "^1.6"
"composer/installers": "^2",
"drupal/core-recommended": "^10",
"phpunit/phpunit": "^9"
},
"config": {
"allow-plugins": {
"composer/installers": true
},
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"process-timeout": 9600,
"platform": {
"php": "8.1"
}
},
"extra": {
"installer-paths": {
"web/core": ["type:drupal-core"]
}
}
}
267 changes: 9 additions & 258 deletions src/DrupalFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,92 +7,10 @@

namespace DrupalFinder;

use Composer\InstalledVersions;

class DrupalFinder
{
/**
* Drupal root environment variable.
*/
const ENV_DRUPAL_ROOT = 'DRUPAL_FINDER_DRUPAL_ROOT';

/**
* Composer root environment variable.
*/
const ENV_COMPOSER_ROOT = 'DRUPAL_FINDER_COMPOSER_ROOT';

/**
* Vendor directory environment variable.
*/
const ENV_VENDOR_DIR = 'DRUPAL_FINDER_VENDOR_DIR';

/**
* Drupal web public directory.
*
* @var string
*/
private $drupalRoot;

/**
* Drupal package composer directory.
*
* @var bool
*/
private $composerRoot;

/**
* Composer vendor directory.
*
* @var string
*
* @see https://getcomposer.org/doc/06-config.md#vendor-dir
*/
private $vendorDir;

/**
* Initialize finder.
*
* Optionally pass the starting path.
*
* @param string|null $start_path
* The path to begin the search from.
*
* @throws \Exception
* @todo Make $start_path mandatory in v2.
*/
public function __construct($start_path = null) {
// Initialize path variables to false, indicating their locations are
// not yet known.
$this->drupalRoot = false;
$this->composerRoot = false;
$this->vendorDir = false;

// If a starting path was provided, attempt to locate and set path
// variables.
if (!empty($start_path)) {
$this->discoverRoots($start_path);
}
}

/**
* Locate Drupal, Composer, and vendor directory paths.
*
* @param string $start_path
* The path to begin the search from.
*
* @return bool
* True if the Drupal root was identified, false otherwise.
*
* @throws \Exception
*
* @deprecated Will be removed in v2. Future usage should instantiate
* a new DrupalFinder object by passing the starting path to its
* constructor.
*/
public function locateRoot($start_path)
{
$this->discoverRoots($start_path);
return !empty($this->getDrupalRoot());
}

/**
* Get the Drupal root.
*
Expand All @@ -101,9 +19,8 @@ public function locateRoot($start_path)
*/
public function getDrupalRoot()
{
$environment_path = $this->getValidEnvironmentVariablePath(self::ENV_DRUPAL_ROOT);

return !empty($environment_path) ? $environment_path : $this->drupalRoot;
$core = InstalledVersions::getInstallPath('drupal/core');
return $core ? realpath(dirname($core)) : false;
}

/**
Expand All @@ -114,8 +31,7 @@ public function getDrupalRoot()
*/
public function getComposerRoot()
{
$environment_path = $this->getValidEnvironmentVariablePath(self::ENV_COMPOSER_ROOT);
return !empty($environment_path) ? $environment_path : $this->composerRoot;
return realpath(dirname($this->getVendorDir()));
}

/**
Expand All @@ -126,175 +42,10 @@ public function getComposerRoot()
*/
public function getVendorDir()
{
$environment_path = $this->getValidEnvironmentVariablePath(self::ENV_VENDOR_DIR);
return !empty($environment_path) ? $environment_path : $this->vendorDir;
}

/**
* Discover all valid paths.
*
* @param $start_path
* The path to start the search from.
*
* @throws \Exception
*/
protected function discoverRoots($start_path) {
// Since we are discovering, reset all path variables.
$this->drupalRoot = false;
$this->composerRoot = false;
$this->vendorDir = false;

foreach (array(true, false) as $follow_symlinks) {
$path = $start_path;
if ($follow_symlinks && is_link($path)) {
$path = realpath($path);
}

// Check the start path.
if ($this->findAndValidateRoots($path)) {
return;
} else {
// Move up dir by dir and check each.
while ($path = $this->shiftPathUp($path)) {
if ($follow_symlinks && is_link($path)) {
$path = realpath($path);
}
if ($this->findAndValidateRoots($path)) {
return;
}
}
}
}
}

/**
* Determine if a valid Drupal root exists.
*
* In addition, set any valid path properties if they are found.
*
* @param $path
* The starting path to search from.
*
* @return bool
* True if all roots were discovered and validated. False otherwise.
*/
protected function findAndValidateRoots($path)
{

if (!empty($path) && is_dir($path) && file_exists($path . '/autoload.php') && file_exists($path . '/' . $this->getComposerFileName())) {
// Additional check for the presence of core/composer.json to
// grant it is not a Drupal 7 site with a base folder named "core".
$candidate = 'core/includes/common.inc';
if (file_exists($path . '/' . $candidate) && file_exists($path . '/core/core.services.yml')) {
if (file_exists($path . '/core/misc/drupal.js') || file_exists($path . '/core/assets/js/drupal.js')) {
$this->composerRoot = $path;
$this->drupalRoot = $path;
$this->vendorDir = $this->composerRoot . '/vendor';
}
}
}
if (!empty($path) && is_dir($path) && file_exists($path . '/' . $this->getComposerFileName())) {
$json = json_decode(
file_get_contents($path . '/' . $this->getComposerFileName()),
true
);

if (is_null($json)) {
throw new \Exception('Unable to decode ' . $path . '/' . $this->getComposerFileName());
}

if (is_array($json)) {
if (isset($json['extra']['installer-paths']) && is_array($json['extra']['installer-paths'])) {
foreach ($json['extra']['installer-paths'] as $install_path => $items) {
if (in_array('type:drupal-core', $items) ||
in_array('drupal/core', $items) ||
in_array('drupal/drupal', $items)) {
$this->composerRoot = $path;
// @todo: Remove this magic and detect the major version instead.
if (($install_path == 'core') || ((isset($json['name'])) && ($json['name'] == 'drupal/drupal'))) {
$install_path = '';
} elseif (substr($install_path, -5) == '/core') {
$install_path = substr($install_path, 0, -5);
}
$this->drupalRoot = rtrim($path . '/' . $install_path, '/');
$this->vendorDir = $this->composerRoot . '/vendor';
}
}
}
}
}
if ($this->composerRoot && file_exists($this->composerRoot . '/' . $this->getComposerFileName())) {
$json = json_decode(
file_get_contents($path . '/' . $this->getComposerFileName()),
true
);
if (is_array($json) && isset($json['config']['vendor-dir'])) {
$this->vendorDir = $this->composerRoot . '/' . $json['config']['vendor-dir'];
}
}

return $this->allPathsDiscovered();
}

/**
* @return string
*/
protected function getComposerFileName()
{
return trim(getenv('COMPOSER')) ?: 'composer.json';
}

/**
* Helper function to quickly determine whether or not all paths were discovered.
*
* @return bool
* True if all paths have been discovered, false if one or more haven't been found.
*/
protected function allPathsDiscovered() {
return !empty($this->drupalRoot) && !empty($this->composerRoot) && !empty($this->vendorDir);
}

/**
* Helper function to quickly determine whether or not all paths are known.
*
* @return bool
* True if all paths are known, false if one or more paths are unknown.
*/
protected function allPathsKnown() {
return !empty($this->getDrupalRoot()) && !empty($this->getComposerRoot()) && !empty($this->getVendorDir());
}

/**
* Get path stored in environment variable.
*
* @param string $variable
* The name of the environment variable to retrieve the path from.
*
* @return false|string
* A path if it is valid. False otherwise.
*/
protected function getValidEnvironmentVariablePath($variable) {
$path = getenv($variable);
if (is_string($path) && is_dir($path)) {
return $path;
}
return false;
}

/**
* Returns parent directory.
*
* @param string
* Path to start from
*
* @return string|false
* Parent path of given path or false when $path is filesystem root
*/
private function shiftPathUp($path)
{
$parent = dirname($path);

return in_array($parent, ['.', $path]) ? false : $parent;
// See https://getcomposer.org/doc/07-runtime.md#autoloader-path-in-binaries
// PHPUnit replaces $_composer_autoload_path with its constant in vendor/phpunit/phpunit/phpunit
$autoload_path = $GLOBALS['_composer_autoload_path'] ?? PHPUNIT_COMPOSER_INSTALL;
return isset($autoload_path) ? realpath(dirname($autoload_path)) : false;
}

}
Loading

0 comments on commit 8612833

Please sign in to comment.