305 lines
9.2 KiB
PHP
305 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\Core\Manifest;
|
|
|
|
use RuntimeException;
|
|
use SilverStripe\Assets\FileFinder;
|
|
use SilverStripe\Dev\Deprecation;
|
|
|
|
/**
|
|
* An extension to the default file finder with some extra filters to facilitate
|
|
* autoload and template manifest generation:
|
|
* - Only modules with _config.php files are scanned.
|
|
* - If a _manifest_exclude file is present inside a directory it is ignored.
|
|
* - Assets and module language directories are ignored.
|
|
* - Module tests directories are skipped if either of these conditions is meant:
|
|
* - the `ignore_tests` option is not set to false.
|
|
* - the module PHP CI configuration matches one of the `ignored_ci_configs`
|
|
*/
|
|
class ManifestFileFinder extends FileFinder
|
|
{
|
|
|
|
const CONFIG_FILE = '_config.php';
|
|
const CONFIG_DIR = '_config';
|
|
const EXCLUDE_FILE = '_manifest_exclude';
|
|
const LANG_DIR = 'lang';
|
|
const TESTS_DIR = 'tests';
|
|
const VENDOR_DIR = 'vendor';
|
|
|
|
/**
|
|
* @deprecated 4.4.0:5.0.0 Use global `RESOURCES_DIR` instead.
|
|
*/
|
|
const RESOURCES_DIR = RESOURCES_DIR;
|
|
|
|
protected static $default_options = [
|
|
'include_themes' => false,
|
|
'ignore_tests' => true,
|
|
'min_depth' => 1,
|
|
'ignore_dirs' => ['node_modules'],
|
|
'ignored_ci_configs' => []
|
|
];
|
|
|
|
public function acceptDir($basename, $pathname, $depth)
|
|
{
|
|
// Skip if ignored
|
|
if ($this->isInsideIgnored($basename, $pathname, $depth)) {
|
|
return false;
|
|
}
|
|
|
|
// Keep searching inside vendor
|
|
$inVendor = $this->isInsideVendor($basename, $pathname, $depth);
|
|
if ($inVendor) {
|
|
// Skip nested vendor folders (e.g. vendor/silverstripe/framework/vendor)
|
|
if ($depth == 4 && basename($pathname ?? '') === self::VENDOR_DIR) {
|
|
return false;
|
|
}
|
|
|
|
// Keep searching if we could have a subdir module
|
|
if ($depth < 3) {
|
|
return true;
|
|
}
|
|
|
|
// Stop searching if we are in a non-module library
|
|
$libraryPath = $this->upLevels($pathname, $depth - 3);
|
|
$libraryBase = basename($libraryPath ?? '');
|
|
if (!$this->isDirectoryModule($libraryBase, $libraryPath, 3)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Include themes
|
|
if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
|
|
return true;
|
|
}
|
|
|
|
// Skip if not in module
|
|
if (!$this->isInsideModule($basename, $pathname, $depth)) {
|
|
return false;
|
|
}
|
|
|
|
// Skip if test dir inside vendor module with unexpected CI Configuration
|
|
if ($depth > 3 && $basename === self::TESTS_DIR && $ignoredCIConfig = $this->getOption('ignored_ci_configs')) {
|
|
$ciLib = $this->findModuleCIPhpConfiguration($basename, $pathname, $depth);
|
|
if (in_array($ciLib, $ignoredCIConfig ?? [])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return parent::acceptDir($basename, $pathname, $depth);
|
|
}
|
|
|
|
/**
|
|
* Check if the given dir is, or is inside the vendor folder
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param int $depth
|
|
* @return bool
|
|
*/
|
|
public function isInsideVendor($basename, $pathname, $depth)
|
|
{
|
|
$base = basename($this->upLevels($pathname, $depth - 1) ?? '');
|
|
return $base === self::VENDOR_DIR;
|
|
}
|
|
|
|
/**
|
|
* Check if the given dir is, or is inside the themes folder
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param int $depth
|
|
* @return bool
|
|
*/
|
|
public function isInsideThemes($basename, $pathname, $depth)
|
|
{
|
|
$base = basename($this->upLevels($pathname, $depth - 1) ?? '');
|
|
return $base === THEMES_DIR;
|
|
}
|
|
|
|
/**
|
|
* Check if this folder or any parent is ignored
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param int $depth
|
|
* @return bool
|
|
*/
|
|
public function isInsideIgnored($basename, $pathname, $depth)
|
|
{
|
|
return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
|
|
return $this->isDirectoryIgnored($basename, $pathname, $depth);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if this folder is inside any module
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param int $depth
|
|
* @return bool
|
|
*/
|
|
public function isInsideModule($basename, $pathname, $depth)
|
|
{
|
|
return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
|
|
return $this->isDirectoryModule($basename, $pathname, $depth);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if any parents match the given callback
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param int $depth
|
|
* @param callable $callback
|
|
* @return bool
|
|
*/
|
|
protected function anyParents($basename, $pathname, $depth, $callback)
|
|
{
|
|
// Check all ignored dir up the path
|
|
while ($depth >= 0) {
|
|
$ignored = $callback($basename, $pathname, $depth);
|
|
if ($ignored) {
|
|
return true;
|
|
}
|
|
$pathname = dirname($pathname ?? '');
|
|
$basename = basename($pathname ?? '');
|
|
$depth--;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if the given dir is a module root (not a subdir)
|
|
*
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param string $depth
|
|
* @return bool
|
|
*/
|
|
public function isDirectoryModule($basename, $pathname, $depth)
|
|
{
|
|
// Depth can either be 0, 1, or 3 (if and only if inside vendor)
|
|
$inVendor = $this->isInsideVendor($basename, $pathname, $depth);
|
|
if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
|
|
return false;
|
|
}
|
|
|
|
// True if config file exists
|
|
if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
|
|
return true;
|
|
}
|
|
|
|
// True if config dir exists
|
|
if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get a parent path the given levels above
|
|
*
|
|
* @param string $pathname
|
|
* @param int $depth Number of parents to rise
|
|
* @return string
|
|
*/
|
|
protected function upLevels($pathname, $depth)
|
|
{
|
|
if ($depth < 0) {
|
|
return null;
|
|
}
|
|
while ($depth--) {
|
|
$pathname = dirname($pathname ?? '');
|
|
}
|
|
return $pathname;
|
|
}
|
|
|
|
/**
|
|
* Get all ignored directories
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getIgnoredDirs()
|
|
{
|
|
$ignored = [self::LANG_DIR, 'node_modules'];
|
|
if ($this->getOption('ignore_tests')) {
|
|
$ignored[] = self::TESTS_DIR;
|
|
}
|
|
return $ignored;
|
|
}
|
|
|
|
/**
|
|
* Check if the given directory is ignored
|
|
* @param string $basename
|
|
* @param string $pathname
|
|
* @param string $depth
|
|
* @return bool
|
|
*/
|
|
public function isDirectoryIgnored($basename, $pathname, $depth)
|
|
{
|
|
// Don't ignore root
|
|
if ($depth === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if manifest-ignored is present
|
|
if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
|
|
return true;
|
|
}
|
|
|
|
// Check if directory name is ignored
|
|
$ignored = $this->getIgnoredDirs();
|
|
if (in_array($basename, $ignored ?? [])) {
|
|
return true;
|
|
}
|
|
|
|
// Ignore these dirs in the root only
|
|
if ($depth === 1 && in_array($basename, [ASSETS_DIR, RESOURCES_DIR])) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find out the root of the current module and read the PHP CI configuration from tho composer file
|
|
*
|
|
* @param string $basename Name of the current folder
|
|
* @param string $pathname Full path the parent folder
|
|
* @param string $depth Depth of the current folder
|
|
*/
|
|
private function findModuleCIPhpConfiguration(string $basename, string $pathname, int $depth): string
|
|
{
|
|
if ($depth < 1) {
|
|
// We went all the way back to the root of the project
|
|
return Module::CI_UNKNOWN;
|
|
}
|
|
|
|
// We pop the current folder and use the next entry the pathname
|
|
$newBasename = basename($pathname ?? '');
|
|
$newPathname = dirname($pathname ?? '');
|
|
$newDepth = $depth - 1;
|
|
|
|
if ($this->isDirectoryModule($newBasename, $newPathname, $newDepth)) {
|
|
// We've reached the root of the module folder, we can read the PHP CI config now
|
|
$module = new Module($newPathname, $this->upLevels($newPathname, $newDepth));
|
|
$config = Deprecation::withNoReplacement(function () use ($module) {
|
|
return $module->getCIConfig();
|
|
});
|
|
|
|
if (empty($config['PHP'])) {
|
|
// This should never happen
|
|
throw new RuntimeException('Module::getCIConfig() did not return a PHP CI value');
|
|
}
|
|
|
|
return $config['PHP'];
|
|
}
|
|
|
|
// We haven't reach our module root yet ... let's look up one more level
|
|
return $this->findModuleCIPhpConfiguration($newBasename, $newPathname, $newDepth);
|
|
}
|
|
}
|