diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5583e8a..5a74b2dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,3 +60,37 @@ jobs: cache-to: type=gha,mode=max - name: Run analysis run: docker compose run --rm actions-tester composer test:phpstan + mess-detect: + runs-on: ubuntu-latest + name: Mess detector + steps: + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + - uses: actions/checkout@v4 + - name: Build Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: "{{defaultContext}}" + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Run mess detector + run: docker compose run --rm actions-tester composer test:phpmd + doc-check: + runs-on: ubuntu-latest + name: Documentation check + steps: + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + - uses: actions/checkout@v4 + - name: Build Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: "{{defaultContext}}" + push: false + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Run doc check + run: docker compose run --rm actions-tester composer test:docs diff --git a/Dockerfile b/Dockerfile index 06ec0fdb..175e1e3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM composer:2.7.7 -FROM php:8.3.9-cli-alpine AS base +FROM composer:2.7.9 +FROM php:8.3.11-cli-alpine AS base FROM base AS builder RUN apk update && apk add git diff --git a/composer.json b/composer.json index 05f06aa5..140cc2fb 100644 --- a/composer.json +++ b/composer.json @@ -40,13 +40,17 @@ "symfony/console": "^5.4", "symfony/finder": "^5.4", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "phpcompatibility/php-compatibility": "dev-develop" + "phpcompatibility/php-compatibility": "dev-develop", + "phpmd/phpmd": "^2.15", + "niels-de-blaauw/php-doc-check": "^0.4.0" }, "scripts": { "test": [ "@test:lint", "@test:phpunit", - "@test:phpstan" + "@test:phpstan", + "@test:phpmd", + "@test:docs" ], "test:lint": [ "composer validate", @@ -57,6 +61,12 @@ ], "test:phpstan": [ "./vendor/bin/phpstan analyse -c phpstan.neon.dist --memory-limit=2G" + ], + "test:phpmd": [ + "./vendor/bin/phpmd src ansi phpmd.xml.dist" + ], + "test:docs": [ + "./vendor/bin/php-doc-check src" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a83ef6c4..c260bdaa 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,8 +1,8 @@ - Coding standard ruleset based on the PSR-2 coding standard. - + Coding standard ruleset based on the PSR-12 coding standard. + diff --git a/phpmd.xml.dist b/phpmd.xml.dist new file mode 100644 index 00000000..185b7f28 --- /dev/null +++ b/phpmd.xml.dist @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index eb0b0284..00000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,211 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Autoloader\\:\\:getSearchNamespace\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Composer/Autoload/Autoloader.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Autoloader\\:\\:processConfig\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Composer/Autoload/Autoloader.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Autoloader\\:\\:processConfig\\(\\) has parameter \\$autoloadConfig with no type specified\\.$#" - count: 1 - path: src/Composer/Autoload/Autoloader.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Classmap\\:\\:processConfig\\(\\) has parameter \\$autoloadConfig with no type specified\\.$#" - count: 1 - path: src/Composer/Autoload/Classmap.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Classmap\\:\\:\\$files type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Composer/Autoload/Classmap.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Classmap\\:\\:\\$paths type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Composer/Autoload/Classmap.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\NamespaceAutoloader\\:\\:processConfig\\(\\) has parameter \\$autoloadConfig with no type specified\\.$#" - count: 1 - path: src/Composer/Autoload/NamespaceAutoloader.php - - - - message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\*/\", expected type at offset 9$#" - count: 1 - path: src/Composer/Package.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: src/Composer/Package.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Composer\\\\Package\\:\\:\\$config has no type specified\\.$#" - count: 1 - path: src/Composer/Package.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Composer\\\\Package\\:\\:\\$dependencies type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Composer/Package.php - - - - message: "#^Access to an undefined property object\\:\\:\\$dep_namespace\\.$#" - count: 2 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:getAllDependenciesOfPackage\\(\\) has parameter \\$dependencies with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:getAllDependenciesOfPackage\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:movePackage\\(\\) has parameter \\$package with no type specified\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:movePackages\\(\\) has parameter \\$packages with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:replacePackage\\(\\) has parameter \\$package with no type specified\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:replacePackages\\(\\) has parameter \\$packages with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:replaceParentInTree\\(\\) has parameter \\$packages with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\*/\", expected type at offset 9$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 2 - path: src/Console/Commands/Compose.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:\\$config has no type specified\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Console\\\\Commands\\\\Compose\\:\\:\\$workingDir \\(string\\) does not accept string\\|false\\.$#" - count: 1 - path: src/Console/Commands/Compose.php - - - - message: "#^Access to an undefined property CoenJacobs\\\\Mozart\\\\Composer\\\\Autoload\\\\Autoloader\\:\\:\\$namespace\\.$#" - count: 1 - path: src/Mover.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Mover\\:\\:__construct\\(\\) has parameter \\$config with no type specified\\.$#" - count: 1 - path: src/Mover.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Mover\\:\\:__construct\\(\\) has parameter \\$workingDir with no type specified\\.$#" - count: 1 - path: src/Mover.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Mover\\:\\:\\$movedPackages type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Mover.php - - - - message: "#^Anonymous function has an unused use \\$contents\\.$#" - count: 1 - path: src/Replace/ClassmapReplacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\ClassmapReplacer\\:\\:replace\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/Replace/ClassmapReplacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\ClassmapReplacer\\:\\:saveReplacedClass\\(\\) has parameter \\$classname with no type specified\\.$#" - count: 1 - path: src/Replace/ClassmapReplacer.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and non\\-falsy\\-string will always evaluate to false\\.$#" - count: 1 - path: src/Replace/ClassmapReplacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\NamespaceReplacer\\:\\:replace\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: src/Replace/NamespaceReplacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\Replacer\\:\\:replace\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Replace/Replacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\Replacer\\:\\:replace\\(\\) has parameter \\$contents with no type specified\\.$#" - count: 1 - path: src/Replace/Replacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replace\\\\Replacer\\:\\:setAutoloader\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Replace/Replacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replacer\\:\\:__construct\\(\\) has parameter \\$config with no type specified\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replacer\\:\\:__construct\\(\\) has parameter \\$workingDir with no type specified\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Method CoenJacobs\\\\Mozart\\\\Replacer\\:\\:replaceInFile\\(\\) has parameter \\$targetFile with no type specified\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Parameter \\#2 \\$contents of method League\\\\Flysystem\\\\Filesystem\\:\\:put\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Parameter \\#3 \\$subject of function preg_replace_callback expects array\\|string, string\\|null given\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Property CoenJacobs\\\\Mozart\\\\Replacer\\:\\:\\$replacedClasses type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Replacer.php - - - - message: "#^Strict comparison using \\=\\=\\= between false and non\\-falsy\\-string will always evaluate to false\\.$#" - count: 2 - path: src/Replacer.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index dcc32dd4..38df4a92 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: 8 reportUnmatchedIgnoredErrors: false diff --git a/src/Composer/Autoload/AbstractAutoloader.php b/src/Composer/Autoload/AbstractAutoloader.php new file mode 100644 index 00000000..05b8c379 --- /dev/null +++ b/src/Composer/Autoload/AbstractAutoloader.php @@ -0,0 +1,27 @@ +package; + } + + public function setPackage(Package $package): void + { + $this->package = $package; + } + + public function getOutputDir(string $basePath, string $autoloadPath): string + { + $outputDir = $basePath . $autoloadPath; + $outputDir = str_replace('\\', DIRECTORY_SEPARATOR, $outputDir); + return $outputDir; + } +} diff --git a/src/Composer/Autoload/Autoloader.php b/src/Composer/Autoload/Autoloader.php index 15f190a9..92a10ebf 100644 --- a/src/Composer/Autoload/Autoloader.php +++ b/src/Composer/Autoload/Autoloader.php @@ -2,6 +2,9 @@ namespace CoenJacobs\Mozart\Composer\Autoload; +use CoenJacobs\Mozart\FilesHandler; +use Symfony\Component\Finder\SplFileInfo; + interface Autoloader { /** @@ -9,4 +12,17 @@ interface Autoloader */ public function processConfig($autoloadConfig): void; public function getSearchNamespace(): string; + public function getOutputDir(string $basePath, string $autoloadPath): string; + /** + * @return array + */ + public function getFiles(FilesHandler $files): array; + /** + * Returns the intended target path of a file, where it should be moved by + * the Mover class. This requires access to the Mozart configuration, for it + * to determine the target directory. This is done by checking the paths + * that are being registered for this autoloader, to see if they can be + * matched with the full path name of the provided file. + */ + public function getTargetFilePath(SplFileInfo $file): string; } diff --git a/src/Composer/Autoload/NamespaceAutoloader.php b/src/Composer/Autoload/NamespaceAutoloader.php index 62ef6328..fab3d286 100644 --- a/src/Composer/Autoload/NamespaceAutoloader.php +++ b/src/Composer/Autoload/NamespaceAutoloader.php @@ -2,24 +2,28 @@ namespace CoenJacobs\Mozart\Composer\Autoload; -abstract class NamespaceAutoloader implements Autoloader +use CoenJacobs\Mozart\FilesHandler; +use Symfony\Component\Finder\SplFileInfo; + +abstract class NamespaceAutoloader extends AbstractAutoloader { - /** @var string */ - public $namespace = ''; + public string $namespace = ''; /** - * The subdir of the vendor/domain/package directory that contains the files for this autoloader type. - * - * e.g. src/ + * The subdir of the vendor/domain/package directory that contains the files + * for this autoloader type. e.g. src/ * - * @var array + * @var string[] */ public $paths = []; + private FilesHandler $fileHandler; + /** - * A package's composer.json config autoload key's value, where $key is `psr-1`|`psr-4`|`classmap`. + * A package's composer.json config autoload key's value, where $key is + * `psr-0`|`psr-4`|`classmap`. * - * @param $autoloadConfig + * @inheritdoc */ public function processConfig($autoloadConfig): void { @@ -27,9 +31,15 @@ public function processConfig($autoloadConfig): void foreach ($autoloadConfig as $path) { array_push($this->paths, $path); } - } else { - array_push($this->paths, $autoloadConfig); + + return; } + array_push($this->paths, $autoloadConfig); + } + + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; } public function getNamespace(): string @@ -46,4 +56,53 @@ public function getNamespacePath(): string { return ''; } + + public function getFiles(FilesHandler $fileHandler): array + { + $this->fileHandler = $fileHandler; + $filesToMove = array(); + + foreach ($this->paths as $path) { + $sourcePath = $fileHandler->getConfig()->getWorkingDir() . 'vendor' . DIRECTORY_SEPARATOR + . $this->getPackage()->getName() . DIRECTORY_SEPARATOR . $path; + + $sourcePath = str_replace('/', DIRECTORY_SEPARATOR, $sourcePath); + + + $files = $fileHandler->getFilesFromPath($sourcePath); + + foreach ($files as $foundFile) { + $filePath = $foundFile->getRealPath(); + $filesToMove[ $filePath ] = $foundFile; + } + } + + return $filesToMove; + } + + /** + * @inheritdoc + */ + public function getTargetFilePath(SplFileInfo $file): string + { + $suffix = ''; + foreach ($this->paths as $path) { + if (! empty(strstr($file->getPathname(), $this->getPackage()->getName() . DIRECTORY_SEPARATOR . $path))) { + $suffix = $path; + break; + } + } + + $replaceWith = $this->fileHandler->getConfig()->getDepDirectory() . $this->getNamespacePath(); + $targetFile = str_replace($this->fileHandler->getConfig()->getWorkingDir(), $replaceWith, $file->getPathname()); + + $packageVendorPath = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $this->getPackage()->getName(); + + if (! empty($suffix)) { + $packageVendorPath = $packageVendorPath . DIRECTORY_SEPARATOR . $suffix; + } + + $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); + return str_replace($packageVendorPath, DIRECTORY_SEPARATOR, $targetFile); + } } diff --git a/src/Config/Autoload.php b/src/Config/Autoload.php index 55fd1aae..e6a0cddc 100644 --- a/src/Config/Autoload.php +++ b/src/Config/Autoload.php @@ -10,7 +10,13 @@ class Autoload /** @var array */ public array $autoloaders = []; - public function setupAutoloaders(stdClass $autoloadData): void + /** + * Loads the autoloaders provided in the loaded composer.json file, which is + * then passed to this method as a stdClass. It registers each autoloader, + * which are then used to access the paths to read and replace contents of + * files that these autoloaders allow access to. + */ + public function setupAutoloaders(stdClass $autoloadData, Package $package): void { $autoloaders = []; @@ -18,8 +24,9 @@ public function setupAutoloaders(stdClass $autoloadData): void $psr4Autoloaders = (array) $autoloadData->{'psr-4'}; foreach ($psr4Autoloaders as $key => $value) { $autoloader = new Psr4(); - $autoloader->namespace = $key; + $autoloader->setNamespace($key); $autoloader->processConfig($value); + $autoloader->setPackage($package); $autoloaders[] = $autoloader; } } @@ -28,8 +35,9 @@ public function setupAutoloaders(stdClass $autoloadData): void $psr0Autoloaders = (array) $autoloadData->{'psr-0'}; foreach ($psr0Autoloaders as $key => $value) { $autoloader = new Psr0(); - $autoloader->namespace = $key; + $autoloader->setNamespace($key); $autoloader->processConfig($value); + $autoloader->setPackage($package); $autoloaders[] = $autoloader; } } @@ -37,6 +45,7 @@ public function setupAutoloaders(stdClass $autoloadData): void if (isset($autoloadData->classmap)) { $autoloader = new Classmap(); $autoloader->processConfig($autoloadData->classmap); + $autoloader->setPackage($package); $autoloaders[] = $autoloader; } diff --git a/src/Config/Classmap.php b/src/Config/Classmap.php index c53529ed..5826db59 100644 --- a/src/Config/Classmap.php +++ b/src/Config/Classmap.php @@ -2,10 +2,12 @@ namespace CoenJacobs\Mozart\Config; -use CoenJacobs\Mozart\Composer\Autoload\Autoloader; +use CoenJacobs\Mozart\Composer\Autoload\AbstractAutoloader; +use CoenJacobs\Mozart\FilesHandler; use Exception; +use Symfony\Component\Finder\SplFileInfo; -class Classmap implements Autoloader +class Classmap extends AbstractAutoloader { /** @var string[] */ public $files = []; @@ -13,6 +15,8 @@ class Classmap implements Autoloader /** @var string[] */ public $paths = []; + private FilesHandler $fileHandler; + /** * @inheritdoc */ @@ -21,9 +25,10 @@ public function processConfig($autoloadConfig): void foreach ($autoloadConfig as $value) { if ('.php' == substr($value, -4, 4)) { array_push($this->files, $value); - } else { - array_push($this->paths, $value); + continue; } + + array_push($this->paths, $value); } } @@ -34,4 +39,67 @@ public function getSearchNamespace(): string { throw new Exception('Classmap autoloaders do not contain a namespace and this method can not be used.'); } + + /** + * @return array + */ + public function getFiles(FilesHandler $fileHandler): array + { + $this->fileHandler = $fileHandler; + $filesToMove = array(); + + foreach ($this->files as $file) { + $sourcePath = $fileHandler->getConfig()->getWorkingDir() . 'vendor' + . DIRECTORY_SEPARATOR . $this->getPackage()->getName(); + + $files = $fileHandler->getFile($sourcePath, $file); + + foreach ($files as $foundFile) { + $filePath = $foundFile->getRealPath(); + $filesToMove[ $filePath ] = $foundFile; + } + } + + foreach ($this->paths as $path) { + $sourcePath = $fileHandler->getConfig()->getWorkingDir() . 'vendor' + . DIRECTORY_SEPARATOR . $this->getPackage()->getName() . DIRECTORY_SEPARATOR . $path; + + $files = $fileHandler->getFilesFromPath($sourcePath); + foreach ($files as $foundFile) { + $filePath = $foundFile->getRealPath(); + $filesToMove[ $filePath ] = $foundFile; + } + } + + return $filesToMove; + } + + /** + * @inheritdoc + */ + public function getTargetFilePath(SplFileInfo $file): string + { + $suffix = ''; + foreach ($this->paths as $path) { + if (! empty(strstr($file->getPathname(), $this->getPackage()->getName() . DIRECTORY_SEPARATOR . $path))) { + $suffix = $path; + break; + } + } + + $namespacePath = $this->getPackage()->getName(); + $replaceWith = $this->fileHandler->getConfig()->getClassmapDirectory() . $namespacePath . DIRECTORY_SEPARATOR; + + $targetFile = str_replace($this->fileHandler->getConfig()->getWorkingDir(), $replaceWith, $file->getPathname()); + + $packageVendorPath = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $this->getPackage()->getName() + . DIRECTORY_SEPARATOR; + + if (! empty($suffix)) { + $packageVendorPath = $packageVendorPath . DIRECTORY_SEPARATOR . $suffix; + } + + $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); + return str_replace($packageVendorPath, DIRECTORY_SEPARATOR, $targetFile); + } } diff --git a/src/Config/Mozart.php b/src/Config/Mozart.php index a07c5b5b..38f04a7f 100644 --- a/src/Config/Mozart.php +++ b/src/Config/Mozart.php @@ -11,22 +11,42 @@ class Mozart { use ReadsConfig; - public string $dep_namespace; - public string $dep_directory; - public string $classmap_directory; - public string $classmap_prefix; + public string $depNamespace; + public string $depDirectory; + public string $classmapDir; + public string $classmapPrefix; /** @var string[] */ public array $packages = []; /** @var string[] */ - public array $excluded_packages = []; + public array $excludedPackages = []; - public OverrideAutoload $override_autoload; - public bool $delete_vendor_directories; + public OverrideAutoload $overrideAutoload; + public bool $deleteVendorDir = true; public string $workingDir = ''; + public function setDepNamespace(string $depNamespace): void + { + $this->depNamespace = $depNamespace; + } + + public function setDepDirectory(string $depDirectory): void + { + $this->depDirectory = $depDirectory; + } + + public function setClassmapDirectory(string $classmapDirectory): void + { + $this->classmapDir = $classmapDirectory; + } + + public function setClassmapPrefix(string $classmapPrefix): void + { + $this->classmapPrefix = $classmapPrefix; + } + /** * @return string[] */ @@ -44,21 +64,26 @@ public function setPackages(array $packages): void } /** - * @param string[] $excluded_packages + * @param string[] $excludedPackages */ - public function setExcludedPackages(array $excluded_packages): void + public function setExcludedPackages(array $excludedPackages): void { - $this->excluded_packages = $excluded_packages; + $this->excludedPackages = $excludedPackages; } public function setOverrideAutoload(stdClass $object): void { - $this->override_autoload = new OverrideAutoload($object); + $this->overrideAutoload = new OverrideAutoload($object); + } + + public function setDeleteVendorDir(bool $deleteVendorDir): void + { + $this->deleteVendorDir = $deleteVendorDir; } public function isValidMozartConfig(): bool { - $required = [ 'dep_namespace', 'dep_directory', 'classmap_directory', 'classmap_prefix' ]; + $required = [ 'depNamespace', 'depDirectory', 'classmapDir', 'classmapPrefix' ]; foreach ($required as $requiredProp) { if (empty($this->$requiredProp)) { @@ -80,22 +105,22 @@ public function isExcludedPackage(Package $package): bool */ public function getDepDirectory(): string { - return rtrim($this->dep_directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + return trim($this->depDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function getClassmapDirectory(): string { - return rtrim($this->classmap_directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + return trim($this->classmapDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function getDeleteVendorDirectories(): bool { - return $this->delete_vendor_directories; + return $this->deleteVendorDir; } public function getDependencyNamespace(): string { - $namespace = preg_replace("/\\\{2,}$/", "\\", $this->dep_namespace."\\"); + $namespace = preg_replace("/\\\{2,}$/", "\\", $this->depNamespace . "\\"); if (empty($namespace)) { throw new Exception('Could not get target dependency namespace'); @@ -106,12 +131,12 @@ public function getDependencyNamespace(): string public function getClassmapPrefix(): string { - return $this->classmap_prefix; + return $this->classmapPrefix; } public function getOverrideAutoload(): OverrideAutoload { - return $this->override_autoload; + return $this->overrideAutoload; } /** @@ -119,7 +144,7 @@ public function getOverrideAutoload(): OverrideAutoload */ public function getExcludedPackages(): array { - return $this->excluded_packages; + return $this->excludedPackages; } public function setWorkingDir(string $workingDir): void @@ -129,6 +154,6 @@ public function setWorkingDir(string $workingDir): void public function getWorkingDir(): string { - return $this->workingDir; + return rtrim($this->workingDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } } diff --git a/src/Config/Package.php b/src/Config/Package.php index c466ee9f..9794c2e5 100644 --- a/src/Config/Package.php +++ b/src/Config/Package.php @@ -25,10 +25,12 @@ class Package public ?Autoload $autoload = null; public ?Extra $extra = null; + private bool $dependenciesLoaded = false; + public function setAutoload(stdClass $data): void { $autoload = new Autoload(); - $autoload->setupAutoloaders($data); + $autoload->setupAutoloaders($data, $this); $this->autoload = $autoload; } @@ -83,9 +85,19 @@ public function getDependencies(): array return $this->dependencies; } - public function loadDependencies(): void + /** + * Loads and registers all dependencies of this package, by checking the + * require-object of the composer.json file of this package. Each package + * listed as a dependency is then loaded and registered as being a + * dependency of this package. Also flags this package for having its + * dependencies already loaded, so it doesn't duplicate dependencies. + */ + public function loadDependencies(PackageFinder $finder): void { - $finder = PackageFinder::instance(); + if ($this->dependenciesLoaded) { + return; + } + if ($this->isValidMozartConfig() && !empty($this->getExtra())) { $mozart = $this->getExtra()->getMozart(); @@ -98,6 +110,7 @@ public function loadDependencies(): void $dependencies = $finder->getPackagesBySlugs($this->getRequire()); $this->registerDependencies($dependencies); + $this->dependenciesLoaded = true; } public function registerDependency(Package $package): void diff --git a/src/Config/ReadsConfig.php b/src/Config/ReadsConfig.php index 4e7f3d30..22f7a83e 100644 --- a/src/Config/ReadsConfig.php +++ b/src/Config/ReadsConfig.php @@ -8,7 +8,7 @@ trait ReadsConfig { - public static function loadFromFile(string $filePath): self + public function loadFromFile(string $filePath): self { $fileContents = file_get_contents($filePath); @@ -16,13 +16,13 @@ public static function loadFromFile(string $filePath): self throw new Exception('Could not read config from provided file.'); } - return self::loadFromString($fileContents); + return $this->loadFromString($fileContents); } /** * @param array $config */ - public static function loadFromArray(array $config): self + public function loadFromArray(array $config): self { $encoded = json_encode($config); @@ -36,10 +36,10 @@ public static function loadFromArray(array $config): self throw new Exception('Could not read config from provided array.'); } - return self::loadFromStdClass($config); + return $this->loadFromStdClass($config); } - public static function loadFromStdClass(stdClass $config): self + public function loadFromStdClass(stdClass $config): self { $mapper = new JsonMapper(); $mapper->bEnforceMapType = false; @@ -52,7 +52,7 @@ public static function loadFromStdClass(stdClass $config): self return $object; } - public static function loadFromString(string $config): self + public function loadFromString(string $config): self { $config = json_decode($config); diff --git a/src/Console/Commands/Compose.php b/src/Console/Commands/Compose.php index aad21e6a..98fa1c56 100644 --- a/src/Console/Commands/Compose.php +++ b/src/Console/Commands/Compose.php @@ -17,15 +17,8 @@ class Compose extends Command private Mover $mover; private Replacer $replacer; private Mozart $config; - private PackageFinder $finder; private string $workingDir; - public function __construct() - { - $this->workingDir = getcwd(); - parent::__construct(); - } - protected function configure(): void { $this->setName('compose'); @@ -33,15 +26,23 @@ protected function configure(): void $this->setHelp(''); } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ protected function execute(InputInterface $input, OutputInterface $output): int { - if (! $this->workingDir) { - throw new Exception('Could not determine working directory.'); + $workingDir = getcwd(); + + if (! $workingDir) { + throw new Exception('Unable to determine the working directory.'); } - $composerFile = $this->workingDir . DIRECTORY_SEPARATOR. 'composer.json'; + $this->workingDir = $workingDir; + + $composerFile = $this->workingDir . DIRECTORY_SEPARATOR . 'composer.json'; try { - $package = PackageFactory::createPackage($composerFile, null, false); + $factory = new PackageFactory(); + $package = $factory->createPackage($composerFile); } catch (Exception $e) { $output->write('Unable to read the composer.json file'); return 1; @@ -67,16 +68,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $require = $package->getRequire(); } - $this->finder = PackageFinder::instance(); - $this->finder->setConfig($this->config); - - $package->loadDependencies(); + $finder = new PackageFinder(); + $finder->setConfig($this->config); - $packages = $this->finder->getPackagesBySlugs($require); - $packages = $this->finder->findPackages($packages); + $package->loadDependencies($finder); + $packages = $finder->findPackages($package->getDependencies()); - $this->mover = new Mover($this->workingDir, $this->config); - $this->replacer = new Replacer($this->workingDir, $this->config); + $this->mover = new Mover($this->config); + $this->replacer = new Replacer($this->config); $this->mover->deleteTargetDirs($packages); $this->mover->movePackages($packages); @@ -84,6 +83,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->replacer->replaceParentInTree($packages); $this->replacer->replaceParentClassesInDirectory($this->config->getClassmapDirectory()); + if ($this->config->getDeleteVendorDirectories()) { + $this->mover->deletePackageVendorDirectories(); + } + return 0; } } diff --git a/src/FilesHandler.php b/src/FilesHandler.php new file mode 100644 index 00000000..46787b55 --- /dev/null +++ b/src/FilesHandler.php @@ -0,0 +1,81 @@ +config = $config; + + $adapter = new LocalFilesystemAdapter( + $this->config->getWorkingDir() + ); + + // The FilesystemOperator + $this->filesystem = new Filesystem($adapter); + } + + public function readFile(string $path): string + { + try { + $contents = $this->filesystem->read($path); + } catch (UnableToReadFile $e) { + $contents = ''; + } + + return $contents; + } + + public function getConfig(): Mozart + { + return $this->config; + } + + public function writeFile(string $path, string $contents): void + { + $this->filesystem->write($path, $contents); + } + + public function getFilesFromPath(string $path): Iterator + { + $finder = new Finder(); + return $finder->files()->in($path)->getIterator(); + } + + public function getFile(string $path, string $fileName): Iterator + { + $finder = new Finder(); + return $finder->files()->name($fileName)->in($path)->getIterator(); + } + + public function createDirectory(string $path): void + { + $this->filesystem->createDirectory($path); + } + + public function deleteDirectory(string $path): void + { + $this->filesystem->deleteDirectory($path); + } + + public function isDirectoryEmpty(string $path): bool + { + return count($this->filesystem->listContents($path, true)->toArray()) === 0; + } + + public function copyFile(string $origin, string $destination): void + { + $this->filesystem->copy($origin, $destination); + } +} diff --git a/src/Mover.php b/src/Mover.php index 4f3d65d6..3c928902 100644 --- a/src/Mover.php +++ b/src/Mover.php @@ -3,57 +3,41 @@ namespace CoenJacobs\Mozart; use CoenJacobs\Mozart\Composer\Autoload\Autoloader; -use CoenJacobs\Mozart\Composer\Autoload\NamespaceAutoloader; use CoenJacobs\Mozart\Config\Classmap; use CoenJacobs\Mozart\Config\Mozart; use CoenJacobs\Mozart\Config\Package; use CoenJacobs\Mozart\Config\Psr0; use CoenJacobs\Mozart\Config\Psr4; -use League\Flysystem\Local\LocalFilesystemAdapter; -use League\Flysystem\Filesystem; -use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; class Mover { - /** @var string */ - protected $workingDir; - - /** @var string */ - protected $targetDir; - - /** @var Mozart */ - protected $config; - - /** @var Filesystem */ - protected $filesystem; + protected Mozart $config; + protected FilesHandler $files; /** @var array */ protected $movedPackages = []; - public function __construct(string $workingDir, Mozart $config) + /** @var array */ + protected $movedFiles = []; + + public function __construct(Mozart $config) { $this->config = $config; - $this->workingDir = $workingDir; - $this->targetDir = $this->config->getDepDirectory(); - - $adapter = new LocalFilesystemAdapter( - $this->workingDir - ); - - // The FilesystemOperator - $this->filesystem = new Filesystem($adapter); + $this->files = new FilesHandler($config); } /** - * Create the required `dep_directory` and `classmap_directory` and delete targetDirs of packages about to be moved. + * Create the required `dep_directory` and `classmap_directory` and delete + * targetDirs of packages about to be moved. * - * @param Package[] $packages The packages that, in the next step, will be moved. + * @param Package[] $packages The packages to delete the target directories + * for, which will be moved in the next step. */ public function deleteTargetDirs($packages): void { - $this->filesystem->createDirectory($this->config->getDepDirectory()); - $this->filesystem->createDirectory($this->config->getClassmapDirectory()); + $this->files->createDirectory($this->config->getDepDirectory()); + $this->files->createDirectory($this->config->getClassmapDirectory()); foreach ($packages as $package) { $this->deleteDepTargetDirs($package); @@ -61,43 +45,52 @@ public function deleteTargetDirs($packages): void } /** - * Delete the directories about to be used for packages earmarked for Mozart namespacing. - * - * @visibility private to allow recursion through packages and subpackages. + * Delete the directories about to be used for packages earmarked for Mozart + * namespacing. */ private function deleteDepTargetDirs(Package $package): void { - foreach ($package->getAutoloaders() as $packageAutoloader) { - $autoloaderType = get_class($packageAutoloader); + foreach ($package->getAutoloaders() as $autoloader) { + $autoloaderType = get_class($autoloader); + $outputDir = ''; switch ($autoloaderType) { case Psr0::class: case Psr4::class: - $outputDir = $this->config->getDepDirectory() . $packageAutoloader->namespace; - $outputDir = str_replace('\\', DIRECTORY_SEPARATOR, $outputDir); - $this->filesystem->deleteDirectory($outputDir); + $outputDir = $autoloader->getOutputDir( + $this->config->getDepDirectory(), + $autoloader->getSearchNamespace() + ); break; case Classmap::class: - $outputDir = $this->config->getClassmapDirectory() . $package->getName(); - $outputDir = str_replace('\\', DIRECTORY_SEPARATOR, $outputDir); - $this->filesystem->deleteDirectory($outputDir); + $outputDir = $autoloader->getOutputDir( + $this->config->getClassmapDirectory(), + $package->getName() + ); break; } + + if (empty($outputDir)) { + continue; + } + + $this->files->deleteDirectory($outputDir); } + foreach ($package->getDependencies() as $subPackage) { $this->deleteDepTargetDirs($subPackage); } } - public function deleteEmptyDirs(): void + private function deleteEmptyDirs(): void { - if (count($this->filesystem->listContents($this->config->getDepDirectory(), true)->toArray()) === 0) { - $this->filesystem->deleteDirectory($this->config->getDepDirectory()); + if ($this->files->isDirectoryEmpty($this->config->getDepDirectory())) { + $this->files->deleteDirectory($this->config->getDepDirectory()); } - if (count($this->filesystem->listContents($this->config->getClassmapDirectory(), true)->toArray()) === 0) { - $this->filesystem->deleteDirectory($this->config->getClassmapDirectory()); + if ($this->files->isDirectoryEmpty($this->config->getClassmapDirectory())) { + $this->files->deleteDirectory($this->config->getClassmapDirectory()); } } @@ -107,112 +100,71 @@ public function deleteEmptyDirs(): void public function movePackages($packages): void { foreach ($packages as $package) { - $this->movePackages($package->getDependencies()); $this->movePackage($package); } $this->deleteEmptyDirs(); } - public function movePackage(Package $package): void + /** + * Moves each file for each autoloader, for the provided package. Each + * package will only be moved once, to prevent duplicates, so the package + * name is registered at the end of the method. + */ + private function movePackage(Package $package): void { - if (in_array($package->getName(), $this->movedPackages)) { - return; - } - - if ($this->config->isExcludedPackage($package)) { + if (!$this->shouldPackageBeMoved($package)) { return; } + /** + * @todo: This maybe even warrants its own 'File' class, where stuff + * like the SplFileInfo etc can be stored in. + */ foreach ($package->getAutoloaders() as $autoloader) { - if ($autoloader instanceof NamespaceAutoloader) { - $finder = new Finder(); - - foreach ($autoloader->paths as $path) { - $source_path = $this->workingDir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR - . $package->getName() . DIRECTORY_SEPARATOR . $path; - - $source_path = str_replace('/', DIRECTORY_SEPARATOR, $source_path); - - $finder->files()->in($source_path); - - foreach ($finder as $file) { - $this->moveFile($package, $autoloader, $file, $path); - } - } - } elseif ($autoloader instanceof Classmap) { - $finder = new Finder(); + $filesToMove = $autoloader->getFiles($this->files); - $files_to_move = array(); - - foreach ($autoloader->files as $file) { - $source_path = $this->workingDir . DIRECTORY_SEPARATOR . 'vendor' - . DIRECTORY_SEPARATOR . $package->getName(); - $finder->files()->name($file)->in($source_path); - - foreach ($finder as $foundFile) { - $filePath = $foundFile->getRealPath(); - $files_to_move[ $filePath ] = $foundFile; - } - } - - $finder = new Finder(); - - foreach ($autoloader->paths as $path) { - $source_path = $this->workingDir . DIRECTORY_SEPARATOR . 'vendor' - . DIRECTORY_SEPARATOR . $package->getName() . DIRECTORY_SEPARATOR . $path; - - $finder->files()->in($source_path); - - foreach ($finder as $foundFile) { - $filePath = $foundFile->getRealPath(); - $files_to_move[ $filePath ] = $foundFile; - } - } - - foreach ($files_to_move as $foundFile) { - $this->moveFile($package, $autoloader, $foundFile); - } + foreach ($filesToMove as $foundFile) { + $this->moveFile($autoloader, $foundFile); } + } - if (!in_array($package->getName(), $this->movedPackages)) { - $this->movedPackages[] = $package->getName(); - } + if (!in_array($package->getName(), $this->movedPackages)) { + $this->movedPackages[] = $package->getName(); } + } - if ($this->config->getDeleteVendorDirectories()) { - $this->deletePackageVendorDirectories(); + private function shouldPackageBeMoved(Package $package): bool + { + if (in_array($package->getName(), $this->movedPackages)) { + return false; } + + if ($this->config->isExcludedPackage($package)) { + return false; + } + + return true; } - public function moveFile(Package $package, Autoloader $autoloader, SplFileInfo $file, string $path = ''): string + private function moveFile(Autoloader $autoloader, SplFileInfo $file): void { - if ($autoloader instanceof NamespaceAutoloader) { - $namespacePath = $autoloader->getNamespacePath(); - $replaceWith = $this->config->getDepDirectory() . $namespacePath; - $targetFile = str_replace($this->workingDir, $replaceWith, $file->getPathname()); - - $packageVendorPath = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package->getName() - . DIRECTORY_SEPARATOR . $path; - $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); - $targetFile = str_replace($packageVendorPath, '', $targetFile); - } else { - $namespacePath = $package->getName(); - $replaceWith = $this->config->getClassmapDirectory() . $namespacePath; - $targetFile = str_replace($this->workingDir, $replaceWith, $file->getPathname()); - - $packageVendorPath = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package->getName() - . DIRECTORY_SEPARATOR; - $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); - $targetFile = str_replace($packageVendorPath, DIRECTORY_SEPARATOR, $targetFile); + if (in_array($file->getRealPath(), $this->movedFiles)) { + return; } - $this->filesystem->copy( - str_replace($this->workingDir, '', $file->getPathname()), + $targetFile = $autoloader->getTargetFilePath($file); + $this->copyFile($file, $targetFile); + + array_push($this->movedFiles, $file->getRealPath()); + } + + private function copyFile(SplFileInfo $file, string $targetFile): void + { + $this->files->copyFile( + str_replace($this->config->getWorkingDir(), '', $file->getPathname()), $targetFile ); - - return $targetFile; } /** @@ -220,7 +172,7 @@ public function moveFile(Package $package, Autoloader $autoloader, SplFileInfo $ * prevent packages that are prefixed/namespaced from being used or * influencing the output of the code. They just need to be gone. */ - protected function deletePackageVendorDirectories(): void + public function deletePackageVendorDirectories(): void { foreach ($this->movedPackages as $movedPackage) { $packageDir = 'vendor' . DIRECTORY_SEPARATOR . $movedPackage; @@ -228,20 +180,16 @@ protected function deletePackageVendorDirectories(): void continue; } - $this->filesystem->deleteDirectory($packageDir); + $this->files->deleteDirectory($packageDir); - //Delete parent directory too if it became empty - //(because that package was the only one from that vendor) + /** + * Delete parent directory too if it became empty (because that + * package was the only one from that vendor). + */ $parentDir = dirname($packageDir); - if ($this->dirIsEmpty($parentDir)) { - $this->filesystem->deleteDirectory($parentDir); + if ($this->files->isDirectoryEmpty($parentDir)) { + $this->files->deleteDirectory($parentDir); } } } - - private function dirIsEmpty(string $dir): bool - { - $di = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); - return iterator_count($di) === 0; - } } diff --git a/src/PackageFactory.php b/src/PackageFactory.php index da67d61e..4591181a 100644 --- a/src/PackageFactory.php +++ b/src/PackageFactory.php @@ -8,28 +8,22 @@ class PackageFactory { /** @var array */ - public static array $cache = []; + public array $cache = []; - public static function createPackage( - string $path, - stdClass $overrideAutoload = null, - bool $loadDependencies = true - ): Package { - if (isset(self::$cache[$path])) { - return self::$cache[$path]; + public function createPackage(string $path, stdClass $overrideAutoload = null): Package + { + if (isset($this->cache[$path])) { + return $this->cache[$path]; } - $package = Package::loadFromFile($path); + $package = new Package(); + $package = $package->loadFromFile($path); if (! empty($overrideAutoload)) { $package->setAutoload($overrideAutoload); } - if ($loadDependencies) { - $package->loadDependencies(); - } - - self::$cache[$path] = $package; + $this->cache[$path] = $package; return $package; } } diff --git a/src/PackageFinder.php b/src/PackageFinder.php index 9f162c63..1c6e268b 100644 --- a/src/PackageFinder.php +++ b/src/PackageFinder.php @@ -9,16 +9,11 @@ class PackageFinder { private ?Mozart $config; + public PackageFactory $factory; - public static function instance(): self + public function __construct() { - static $instance; - - if (! is_object($instance) || ! $instance instanceof self) { - $instance = new self(); - } - - return $instance; + $this->factory = new PackageFactory(); } public function setConfig(Mozart $config): void @@ -26,6 +21,12 @@ public function setConfig(Mozart $config): void $this->config = $config; } + /** + * Returns a Package object for the package based on the provided slug (in + * vendor/package format). The data of the package is loaded if a valid + * installed package could be found based on the slug, which is then being + * used to read the composer.json file of the package. + */ public function getPackageBySlug(string $slug): ?Package { /** @@ -48,15 +49,20 @@ public function getPackageBySlug(string $slug): ?Package } $autoloaders = null; - $override_autoload = $this->config->getOverrideAutoload(); - if ($override_autoload !== false && isset($override_autoload->$slug)) { - $autoloaders = $override_autoload->$slug; + $overrideAutoload = $this->config->getOverrideAutoload(); + if ($overrideAutoload !== false && isset($overrideAutoload->$slug)) { + $autoloaders = $overrideAutoload->$slug; } - return PackageFactory::createPackage($packageDir . 'composer.json', $autoloaders, true); + $package = $this->factory->createPackage($packageDir . 'composer.json', $autoloaders); + $package->loadDependencies($this); + return $package; } /** + * Returns Package objects which are loaded based on the provided array of + * slugs (in vendor/package format). + * * @param string[] $slugs * @return Package[] */ @@ -72,11 +78,10 @@ public function getPackagesBySlugs(array $slugs): array } /** - * Loops through all dependencies and their dependencies and so on... - * will eventually return a list of all packages required by the full tree. + * Loops through all dependencies and their dependencies and so on... will + * eventually return a list of all packages required by the full tree. * * @param Package[] $packages - * * @return Package[] */ public function findPackages(array $packages): array @@ -84,8 +89,9 @@ public function findPackages(array $packages): array foreach ($packages as $package) { $dependencies = $package->getDependencies(); - $package->registerDependencies($this->findPackages($dependencies)); - $packages[$package->getName()] = $package; + if (! empty($dependencies)) { + $packages = array_merge($packages, $this->findPackages($dependencies)); + } } return $packages; diff --git a/src/Replace/ClassmapReplacer.php b/src/Replace/ClassmapReplacer.php index 41323fa3..27117217 100644 --- a/src/Replace/ClassmapReplacer.php +++ b/src/Replace/ClassmapReplacer.php @@ -1,30 +1,30 @@ classmap_prefix . $matches[1]; + $replace = $this->classmapPrefix . $matches[1]; $this->saveReplacedClass($matches[1], $replace); return str_replace($matches[1], $replace, $matches[0]); }, $contents ); + + if (empty($replaced)) { + throw new Exception('Failed to replace contents of the file.'); + } + + return $replaced; } - public function saveReplacedClass($classname, string $replacedName): void + public function saveReplacedClass(string $classname, string $replacedName): void { $this->replacedClasses[ $classname ] = $replacedName; } diff --git a/src/Replace/NamespaceReplacer.php b/src/Replace/NamespaceReplacer.php index 680fb413..e2540b74 100644 --- a/src/Replace/NamespaceReplacer.php +++ b/src/Replace/NamespaceReplacer.php @@ -2,25 +2,21 @@ namespace CoenJacobs\Mozart\Replace; +use Exception; + class NamespaceReplacer extends BaseReplacer { /** - * The prefix to add to existing namespaces. - * - * @var string "My\Mozart\Prefix". + * The prefix to add to existing namespaces, for example: "My\Mozart\Prefix" */ - public $dep_namespace = ''; + public string $depNamespace = ''; - /** - * @param string $contents The text to make replacements in. - * @param null $file Only used in ClassmapReplacer (for recording which files were changed). - */ - public function replace($contents, $file = null): string + public function replace(string $contents): string { $searchNamespace = preg_quote($this->autoloader->getSearchNamespace(), '/'); - $dependencyNamespace = preg_quote($this->dep_namespace, '/'); + $dependencyNamespace = preg_quote($this->depNamespace, '/'); - return preg_replace_callback( + $replaced = preg_replace_callback( " / # Start the pattern ([^a-zA-Z0-9_\x7f-\xff]) # Match the non-class character before the namespace @@ -33,9 +29,15 @@ public function replace($contents, $file = null): string ) # End the namespace matcher /Ux", function ($matches) { - return $matches[1] . $this->dep_namespace . $matches[2]; + return $matches[1] . $this->depNamespace . $matches[2]; }, $contents ); + + if (empty($replaced)) { + throw new Exception('Failed to replace contents of the file.'); + } + + return $replaced; } } diff --git a/src/Replace/Replacer.php b/src/Replace/Replacer.php index 33b1a150..2266169c 100644 --- a/src/Replace/Replacer.php +++ b/src/Replace/Replacer.php @@ -6,6 +6,9 @@ interface Replacer { - public function setAutoloader(Autoloader $autoloader); - public function replace($contents); + public function setAutoloader(Autoloader $autoloader): void; + /** + * @param string $contents The text to make replacements in. + */ + public function replace(string $contents): string; } diff --git a/src/Replacer.php b/src/Replacer.php index bc2668b2..2404d718 100644 --- a/src/Replacer.php +++ b/src/Replacer.php @@ -9,46 +9,30 @@ use CoenJacobs\Mozart\Config\Package; use CoenJacobs\Mozart\Replace\ClassmapReplacer; use CoenJacobs\Mozart\Replace\NamespaceReplacer; -use League\Flysystem\Local\LocalFilesystemAdapter; -use League\Flysystem\UnableToReadFile; -use League\Flysystem\Filesystem; -use Symfony\Component\Finder\Finder; +use CoenJacobs\Mozart\Replace\Replacer as ReplacerInterface; +use Exception; class Replacer { - /** @var string */ - protected $workingDir; - - /** @var string */ - protected $targetDir; - /** @var Mozart */ protected $config; - /** @var array */ + /** @var array */ protected $replacedClasses = []; - /** @var Filesystem */ - protected $filesystem; + /** @var FilesHandler */ + protected $files; - public function __construct(string $workingDir, Mozart $config) + public function __construct(Mozart $config) { - $this->workingDir = $workingDir; $this->config = $config; - $this->targetDir = $this->config->getDepDirectory(); - - $adapter = new LocalFilesystemAdapter( - $this->workingDir - ); - - // The FilesystemOperator - $this->filesystem = new Filesystem($adapter); + $this->files = new FilesHandler($config); } /** * @param Package[] $packages */ - public function replacePackages($packages): void + public function replacePackages(array $packages): void { foreach ($packages as $package) { $this->replacePackages($package->getDependencies()); @@ -65,35 +49,42 @@ public function replacePackage(Package $package): void public function replaceInFile(string $targetFile, Autoloader $autoloader): void { - $targetFile = str_replace($this->workingDir, '', $targetFile); - try { - $contents = $this->filesystem->read($targetFile); - } catch (UnableToReadFile $e) { - return; - } + $targetFile = str_replace($this->config->getWorkingDir(), '', $targetFile); + $contents = $this->files->readFile($targetFile); if (!$contents) { return; } - if ($autoloader instanceof NamespaceAutoloader) { - $replacer = new NamespaceReplacer(); - $replacer->dep_namespace = $this->config->getDependencyNamespace(); - } else { - $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = $this->config->getClassmapPrefix(); - } - - $replacer->setAutoloader($autoloader); + $replacer = $this->getReplacerByAutoloader($autoloader); $contents = $replacer->replace($contents); if ($replacer instanceof ClassmapReplacer) { $this->replacedClasses = array_merge($this->replacedClasses, $replacer->replacedClasses); } - $this->filesystem->write($targetFile, $contents); + $this->files->writeFile($targetFile, $contents); } + public function getReplacerByAutoloader(Autoloader $autoloader): ReplacerInterface + { + if ($autoloader instanceof NamespaceAutoloader) { + $replacer = new NamespaceReplacer(); + $replacer->depNamespace = $this->config->getDependencyNamespace(); + $replacer->setAutoloader($autoloader); + return $replacer; + } + + $replacer = new ClassmapReplacer(); + $replacer->classmapPrefix = $this->config->getClassmapPrefix(); + $replacer->setAutoloader($autoloader); + return $replacer; + } + + /** + * Fetches the files or directories to perform a replace action on, based + * on the provided autoloader, for the provided package. + */ public function replacePackageByAutoloader(Package $package, Autoloader $autoloader): void { if ($this->config->isExcludedPackage($package)) { @@ -101,15 +92,14 @@ public function replacePackageByAutoloader(Package $package, Autoloader $autoloa } if ($autoloader instanceof NamespaceAutoloader) { - $source_path = $this->workingDir . $this->targetDir + $sourcePath = $this->config->getWorkingDir() . $this->config->getDepDirectory() . str_replace('\\', DIRECTORY_SEPARATOR, $autoloader->getNamespace()); - $this->replaceInDirectory($autoloader, $source_path); + $this->replaceInDirectory($autoloader, $sourcePath); } elseif ($autoloader instanceof Classmap) { - $finder = new Finder(); - $source_path = $this->workingDir . $this->config->getClassmapDirectory() . $package->getName(); - $finder->files()->in($source_path); + $sourcePath = $this->config->getWorkingDir() . $this->config->getClassmapDirectory() . $package->getName(); + $files = $this->files->getFilesFromPath($sourcePath); - foreach ($finder as $foundFile) { + foreach ($files as $foundFile) { $targetFile = $foundFile->getRealPath(); if ('.php' == substr($targetFile, -4, 4)) { @@ -119,60 +109,55 @@ public function replacePackageByAutoloader(Package $package, Autoloader $autoloa } } + /** + * Replaces all occurances of previously replaced classes, in the provided + * directory. This to ensure that each package has its parents package + * classes also replaced in its own files. + */ public function replaceParentClassesInDirectory(string $directory): void { - if (count($this->replacedClasses)===0) { + if (count($this->replacedClasses) === 0) { return; } $directory = trim($directory, '//'); - $finder = new Finder(); - $finder->files()->in($directory); + $files = $this->files->getFilesFromPath($directory); $replacedClasses = $this->replacedClasses; - foreach ($finder as $file) { + foreach ($files as $file) { $targetFile = $file->getPathName(); if ('.php' == substr($targetFile, -4, 4)) { - try { - $contents = $this->filesystem->read($targetFile); - } catch (UnableToReadFile $e) { - continue; - } - - if (!$contents) { - continue; - } + $contents = $this->files->readFile($targetFile); foreach ($replacedClasses as $original => $replacement) { $contents = preg_replace_callback( - '/(.*)([^a-zA-Z0-9_\x7f-\xff])'. $original . '([^a-zA-Z0-9_\x7f-\xff])/U', + '/(.*)([^a-zA-Z0-9_\x7f-\xff])' . $original . '([^a-zA-Z0-9_\x7f-\xff])/U', function ($matches) use ($replacement) { - if (preg_match('/(include|require)/', $matches[0], $output_array)) { + if (preg_match('/(include|require)/', $matches[0])) { return $matches[0]; } return $matches[1] . $matches[2] . $replacement . $matches[3]; }, $contents ); - } - if (empty($contents)) { - continue; + if (empty($contents)) { + throw new Exception('Failed to replace parent classes in directory.'); + } } - $this->filesystem->write($targetFile, $contents); + $this->files->writeFile($targetFile, $contents); } } } public function replaceInDirectory(NamespaceAutoloader $autoloader, string $directory): void { - $finder = new Finder(); - $finder->files()->in($directory); + $files = $this->files->getFilesFromPath($directory); - foreach ($finder as $file) { + foreach ($files as $file) { $targetFile = $file->getPathName(); if ('.php' == substr($targetFile, -4, 4)) { @@ -196,32 +181,36 @@ public function replaceParentPackage(Package $package, Package $parent): void foreach ($package->getAutoloaders() as $autoloader) { if ($parentAutoloader instanceof NamespaceAutoloader) { $namespace = str_replace('\\', DIRECTORY_SEPARATOR, $parentAutoloader->namespace); - $directory = $this->workingDir . $this->config->getDepDirectory() . $namespace + $directory = $this->config->getWorkingDir() . $this->config->getDepDirectory() . $namespace . DIRECTORY_SEPARATOR; if ($autoloader instanceof NamespaceAutoloader) { $this->replaceInDirectory($autoloader, $directory); - } else { - $directory = str_replace($this->workingDir, '', $directory); - $this->replaceParentClassesInDirectory($directory); + return; } - } else { - $directory = $this->workingDir . - $this->config->getClassmapDirectory() . $parent->getName(); - if ($autoloader instanceof NamespaceAutoloader) { - $this->replaceInDirectory($autoloader, $directory); - } else { - $directory = str_replace($this->workingDir, '', $directory); - $this->replaceParentClassesInDirectory($directory); - } + $directory = str_replace($this->config->getWorkingDir(), '', $directory); + $this->replaceParentClassesInDirectory($directory); + return; } + + $directory = $this->config->getWorkingDir() . + $this->config->getClassmapDirectory() . $parent->getName(); + + if ($autoloader instanceof NamespaceAutoloader) { + $this->replaceInDirectory($autoloader, $directory); + return; + } + + $directory = str_replace($this->config->getWorkingDir(), '', $directory); + $this->replaceParentClassesInDirectory($directory); } } } /** - * Get an array containing all the dependencies and dependencies + * Get an array containing all the dependencies and dependencies. + * * @param Package $package * @param Package[] $dependencies * @return Package[] diff --git a/tests/Config/ConfigMapperTest.php b/tests/Config/ConfigMapperTest.php index 42e1ed7a..46d5553f 100644 --- a/tests/Config/ConfigMapperTest.php +++ b/tests/Config/ConfigMapperTest.php @@ -5,6 +5,7 @@ use CoenJacobs\Mozart\Config\Mozart; use CoenJacobs\Mozart\Config\Package; use CoenJacobs\Mozart\PackageFactory; +use CoenJacobs\Mozart\PackageFinder; use PHPUnit\Framework\TestCase; class ConfigMapperTest extends TestCase @@ -15,7 +16,10 @@ class ConfigMapperTest extends TestCase #[Test] public function it_creates_a_valid_config_object_based_on_composer_file() { - $package = PackageFactory::createPackage(__DIR__ . '/config-mapper-test.json'); + $finder = new PackageFinder(); + $factory = new PackageFactory(); + $package = $factory->createPackage(__DIR__ . '/config-mapper-test.json'); + $package->loadDependencies($finder); $this->assertInstanceOf(Package::class, $package); $this->assertInstanceOf(Mozart::class, $package->getExtra()->getMozart()); $this->assertCount(4, $package->autoload->getAutoloaders()); diff --git a/tests/Console/Commands/ComposeTest.php b/tests/Console/Commands/ComposeTest.php index 325ea3bc..6ec1d9aa 100644 --- a/tests/Console/Commands/ComposeTest.php +++ b/tests/Console/Commands/ComposeTest.php @@ -18,9 +18,8 @@ public static function setUpBeforeClass(): void } /** - * Before each test ensure the current working directory is this one. - * - * Record the previous PHPUnit cwd to restore after. + * Before each test ensure the current working directory is this one. Record + * the previous PHPUnit cwd to restore after. */ public function setUp(): void { @@ -30,9 +29,9 @@ public function setUp(): void } /** - * When composer.json is absent, instead of failing with: - * "failed to open stream: No such file or directory" - * a better message should be written to the OutputInterface. + * When composer.json is absent, instead of failing with: "failed to open + * stream: No such file or directory" a better message should be written to + * the OutputInterface. * * @test */ @@ -56,9 +55,8 @@ public function __construct($inputInterfaceMock, $outputInterfaceMock) } /** - * When json_decode fails, instead of - * "Trying to get property 'extra' of non-object" - * a better message should be written to the OutputInterface. + * When json_decode fails, instead of "Trying to get property 'extra' of + * non-object" a better message should be written to the OutputInterface. * * @test */ @@ -86,9 +84,9 @@ public function __construct($inputInterfaceMock, $outputInterfaceMock) } /** - * When composer.json->extra is absent, instead of - * "Undefined property: stdClass::$extra" - * a better message should be written to the OutputInterface. + * When composer.json->extra is absent, instead of "Undefined property: + * stdClass::$extra" a better message should be written to the + * OutputInterface. * * @test */ @@ -117,9 +115,9 @@ public function __construct($inputInterfaceMock, $outputInterfaceMock) /** - * When composer.json->extra is not an object, instead of - * "Trying to get property 'mozart' of non-object" - * a better message should be written to the OutputInterface. + * When composer.json->extra is not an object, instead of "Trying to get + * property 'mozart' of non-object" a better message should be written to + * the OutputInterface. * * @test */ @@ -147,9 +145,9 @@ public function __construct($inputInterfaceMock, $outputInterfaceMock) } /** - * When composer.json->extra->mozart is absent, instead of - * "Undefined property: stdClass::$mozart" - * a better message should be written to the OutputInterface. + * When composer.json->extra->mozart is absent, instead of "Undefined + * property: stdClass::$mozart" a better message should be written to the + * OutputInterface. * * @test */ @@ -177,11 +175,9 @@ public function __construct($inputInterfaceMock, $outputInterfaceMock) } /** - * When composer.json->extra->mozart is malformed, instead of - * "Undefined property: stdClass::$mozart" - * a better message should be written to the OutputInterface. - * - * is_object() added. + * When composer.json->extra->mozart is malformed, instead of "Undefined + * property: stdClass::$mozart" a better message should be written to the + * OutputInterface. * * @test */ diff --git a/tests/MoverTest.php b/tests/MoverTest.php index 8945f49e..ac922d31 100644 --- a/tests/MoverTest.php +++ b/tests/MoverTest.php @@ -5,6 +5,7 @@ use CoenJacobs\Mozart\PackageFactory; use CoenJacobs\Mozart\Console\Commands\Compose; use CoenJacobs\Mozart\Mover; +use CoenJacobs\Mozart\PackageFinder; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Input\InputInterface; @@ -54,18 +55,21 @@ public function setUp(): void ), ); - $this->config = Mozart::loadFromString( json_encode($configArgs) ); + $mozart = new Mozart(); + $this->config = $mozart->loadFromString( json_encode($configArgs) ); + $this->config->setWorkingDir($this->testsWorkingDir); } /** - * If the specified `dep_directory` or `classmap_directory` are absent, create them. + * If the specified `dep_directory` or `classmap_directory` are absent, + * create them. * * @test */ #[Test] public function it_creates_absent_dirs(): void { - $mover = new Mover($this->testsWorkingDir, $this->config); + $mover = new Mover($this->config); $packages = array(); @@ -78,14 +82,15 @@ public function it_creates_absent_dirs(): void } /** - * If the specified `dep_directory` or `classmap_directory` already exists with contents, it is not an issue. + * If the specified `dep_directory` or `classmap_directory` already exists + * with contents, it is not an issue. * * @test */ #[Test] public function it_is_unpertrubed_by_existing_dirs(): void { - $mover = new Mover($this->testsWorkingDir, $this->config); + $mover = new Mover($this->config); if (!file_exists($this->testsWorkingDir . $this->config->getDepDirectory())) { mkdir($this->testsWorkingDir . $this->config->getDepDirectory()); @@ -109,8 +114,10 @@ public function it_is_unpertrubed_by_existing_dirs(): void } /** - * If the specified `dep_directory` or `classmap_directory` contains a subdir we are going to need when moving, - * delete the subdir. aka: If subfolders exist for dependencies we are about to manage, delete those subfolders. + * If the specified `dep_directory` or `classmap_directory` contains a + * subdir we are going to need when moving, delete the subdir. aka: If + * subfolders exist for dependencies we are about to manage, delete those + * subfolders. * * @test */ @@ -137,11 +144,14 @@ public function it_deletes_subdirs_for_packages_about_to_be_moved(): void if ( ! empty( $overrideAutoload ) ) { $overrideAutoload = $overrideAutoload->getByKey( $packageString ); } - $parsedPackage = PackageFactory::createPackage($testDummyComposerPath, $overrideAutoload); + $factory = new PackageFactory(); + $finder = new PackageFinder(); + $parsedPackage = $factory->createPackage($testDummyComposerPath, $overrideAutoload); + $parsedPackage->loadDependencies($finder); $packages[] = $parsedPackage; } - $mover = new Mover($this->testsWorkingDir, $this->config); + $mover = new Mover($this->config); $mover->deleteTargetDirs($packages); $this->assertDirectoryDoesNotExist($this->testsWorkingDir . $this->config->getDepDirectory() . 'Pimple'); @@ -149,14 +159,16 @@ public function it_deletes_subdirs_for_packages_about_to_be_moved(): void } /** - * If a file is specified more than once in an autoloader, e.g. is explicitly listed and is also in a folder listed, - * a "File already exists at path" error occurs. + * If a file is specified more than once in an autoloader, e.g. is + * explicitly listed and is also in a folder listed, a "File already exists + * at path" error occurs. * - * To fix this, we enumerate the files to be copied using a dictionary indexed with the source file path, then loop - * and copy, thus only copying each one once. + * To fix this, we list the files being moved/copied by their absolute path + * resulting in only copying each file only once. * * Original error: - * "League\Flysystem\FileExistsException : File already exists at path: lib/classes/tecnickcom/tcpdf/tcpdf.php" + * "League\Flysystem\FileExistsException : File already exists at path: + * lib/classes/tecnickcom/tcpdf/tcpdf.php" * * Test is using a known problematic autoloader: * "iio/libmergepdf": { diff --git a/tests/replacers/ClassMapReplacerTest.php b/tests/replacers/ClassMapReplacerTest.php index 10b10ce8..131d2089 100644 --- a/tests/replacers/ClassMapReplacerTest.php +++ b/tests/replacers/ClassMapReplacerTest.php @@ -13,7 +13,7 @@ public function it_replaces_class_declarations(): void { $contents = 'class Hello_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals('class Mozart_Hello_World {', $contents); } @@ -24,7 +24,7 @@ public function it_replaces_abstract_class_declarations(): void { $contents = 'abstract class Hello_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals('abstract class Mozart_Hello_World {', $contents); } @@ -35,7 +35,7 @@ public function it_replaces_interface_class_declarations(): void { $contents = 'interface Hello_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals('interface Mozart_Hello_World {', $contents); } @@ -46,7 +46,7 @@ public function it_replaces_class_declarations_that_extend_other_classes(): void { $contents = 'class Hello_World extends Bye_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals('class Mozart_Hello_World extends Bye_World {', $contents); } @@ -57,7 +57,7 @@ public function it_replaces_class_declarations_that_implement_interfaces(): void { $contents = 'class Hello_World implements Bye_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals('class Mozart_Hello_World implements Bye_World {', $contents); } @@ -68,7 +68,7 @@ public function it_stores_replaced_class_names(): void { $contents = 'class Hello_World {'; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $replacer->replace($contents); $this->assertArrayHasKey('Hello_World', $replacer->replacedClasses); } @@ -79,7 +79,7 @@ public function it_replaces_class_declarations_psr2(): void { $contents = "class Hello_World\n{"; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals("class Mozart_Hello_World\n{", $contents); } @@ -94,7 +94,7 @@ public function it_replaces_class(): void { $contents = "class Hello_World"; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $contents = $replacer->replace($contents); $this->assertEquals("class Mozart_Hello_World", $contents); } @@ -114,7 +114,7 @@ public function it_does_not_replace_inside_namespace_multiline(): void class Hello_World "; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $result = $replacer->replace($input); $this->assertEquals($input, $result); @@ -131,7 +131,7 @@ public function it_does_not_replace_inside_namespace_singleline(): void { $input = "namespace Mozart; class Hello_World"; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $result = $replacer->replace($input); $this->assertEquals($input, $result); @@ -140,7 +140,8 @@ public function it_does_not_replace_inside_namespace_singleline(): void /** * It's possible to have multiple namespaces inside one file. * - * To have two classes in one file, one in a namespace and the other not, the global namespace needs to be explicit. + * To have two classes in one file, one in a namespace and the other not, + * the global namespace needs to be explicit. * * @test */ @@ -158,7 +159,7 @@ class B_Class { } "; $replacer = new ClassmapReplacer(); - $replacer->classmap_prefix = 'Mozart_'; + $replacer->classmapPrefix = 'Mozart_'; $result = $replacer->replace($input); $this->assertStringNotContainsString('Mozart_A_Class', $result); diff --git a/tests/replacers/NamespaceReplacerTest.php b/tests/replacers/NamespaceReplacerTest.php index b0651bc7..ea88c53e 100644 --- a/tests/replacers/NamespaceReplacerTest.php +++ b/tests/replacers/NamespaceReplacerTest.php @@ -24,10 +24,11 @@ protected function setUp(): void protected static function createReplacer(string $namespace, string $prefix = self::PREFIX) : NamespaceReplacer { $autoloader = new Psr0; - $autoloader->namespace = $namespace; + $autoloader->setNamespace($namespace); + $replacer = new NamespaceReplacer(); $replacer->setAutoloader($autoloader); - $replacer->dep_namespace = $prefix; + $replacer->depNamespace = $prefix; return $replacer; } @@ -83,8 +84,10 @@ public function it_doesnt_double_replace_namespaces_that_also_exist_inside_anoth $chickenReplacer = self::createReplacer('Chicken'); $eggReplacer = self::createReplacer('Egg'); - // This is a tricky situation. We are referencing Chicken\Egg, - // but Egg *also* exists as a separate top level class. + /** + * This is a tricky situation. We are referencing Chicken\Egg,but Egg + * *also* exists as a separate top level class. + */ $contents = 'use Chicken\\Egg;'; $expected = 'use My\\Mozart\\Prefix\\Chicken\\Egg;';