diff --git a/.gitignore b/.gitignore index 3d6c7cac..46161589 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor/ .DS_Store composer.lock .phpunit.result.cache + +temptestdir \ No newline at end of file diff --git a/src/Composer/Autoload/NamespaceAutoloader.php b/src/Composer/Autoload/NamespaceAutoloader.php index f8393995..b073eb69 100644 --- a/src/Composer/Autoload/NamespaceAutoloader.php +++ b/src/Composer/Autoload/NamespaceAutoloader.php @@ -2,6 +2,8 @@ namespace CoenJacobs\Mozart\Composer\Autoload; +use stdClass; + abstract class NamespaceAutoloader implements Autoloader { /** @var string */ @@ -17,7 +19,7 @@ abstract class NamespaceAutoloader implements Autoloader public $paths = []; /** - * 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 * @@ -39,11 +41,5 @@ public function getSearchNamespace() return $this->namespace; } - /** - * @return string - */ - public function getNamespacePath() - { - return ''; - } + abstract public function getNamespacePath(): string; } diff --git a/src/Composer/Autoload/Psr0.php b/src/Composer/Autoload/Psr0.php index 2a009821..1fba42b4 100644 --- a/src/Composer/Autoload/Psr0.php +++ b/src/Composer/Autoload/Psr0.php @@ -1,7 +1,15 @@ namespace, '\\'); } - /** - * @return string - */ - public function getNamespacePath() + public function getNamespacePath(): string { return str_replace('\\', DIRECTORY_SEPARATOR, $this->namespace); } diff --git a/src/Mover.php b/src/Mover.php index d92b63bb..1e39aeb6 100644 --- a/src/Mover.php +++ b/src/Mover.php @@ -18,11 +18,18 @@ class Mover { - /** @var string */ + /** + * The only path variable with a leading slash. + * + * @var string + */ protected $workingDir; /** @var string */ - protected $targetDir; + protected $dep_directory; + + /** @var string */ + protected $classmap_directory; /** @var MozartConfig */ protected $config; @@ -39,6 +46,11 @@ public function __construct($workingDir, MozartConfig $config) $this->workingDir = $workingDir; $this->targetDir = $this->config->getDepDirectory(); + $this->workingDir = $workingDir; + + $this->dep_directory = $this->clean($config->getDepDirectory()); + $this->classmap_directory = $this->clean($config->getClassmapDirectory()); + $this->filesystem = new Filesystem(new Local($this->workingDir)); } @@ -77,13 +89,12 @@ private function deleteDepTargetDirs($package): void switch ($autoloaderType) { case Psr0::class: case Psr4::class: - $outputDir = $this->config->getDepDirectory() . $packageAutoloader->namespace; - $outputDir = str_replace('\\', DIRECTORY_SEPARATOR, $outputDir); + $outputDir = $this->dep_directory . DIRECTORY_SEPARATOR . + $this->clean($packageAutoloader->namespace); $this->filesystem->deleteDir($outputDir); break; case Classmap::class: - $outputDir = $this->config->getClassmapDirectory() . $package->config->name; - $outputDir = str_replace('\\', DIRECTORY_SEPARATOR, $outputDir); + $outputDir = $this->classmap_directory . DIRECTORY_SEPARATOR . $this->clean($package->config->name); $this->filesystem->deleteDir($outputDir); break; } @@ -96,12 +107,12 @@ private function deleteDepTargetDirs($package): void public function deleteEmptyDirs(): void { - if (count($this->filesystem->listContents($this->config->getDepDirectory(), true)) === 0) { - $this->filesystem->deleteDir($this->config->getDepDirectory()); + if (count($this->filesystem->listContents($this->dep_directory, true)) === 0) { + $this->filesystem->deleteDir($this->dep_directory); } - if (count($this->filesystem->listContents($this->config->getClassmapDirectory(), true)) === 0) { - $this->filesystem->deleteDir($this->config->getClassmapDirectory()); + if (count($this->filesystem->listContents($this->classmap_directory, true)) === 0) { + $this->filesystem->deleteDir($this->classmap_directory); } } @@ -121,10 +132,7 @@ public function movePackage(ComposerPackageConfig $package) $finder = new Finder(); foreach ($autoloader->paths as $path) { - $source_path = $this->workingDir . 'vendor' . DIRECTORY_SEPARATOR - . $package->config->name . DIRECTORY_SEPARATOR . $path; - - $source_path = str_replace('/', DIRECTORY_SEPARATOR, $source_path); + $source_path = DIRECTORY_SEPARATOR . $this->clean($package->path . DIRECTORY_SEPARATOR . $path); $finder->files()->in($source_path); @@ -142,8 +150,7 @@ public function movePackage(ComposerPackageConfig $package) foreach ($autoloader->files as $file) { - $source_path = $this->workingDir . 'vendor' - . DIRECTORY_SEPARATOR . $package->config->name; + $source_path = DIRECTORY_SEPARATOR . $this->clean($package->path); $finder->files()->name($file)->in($source_path); @@ -156,8 +163,7 @@ public function movePackage(ComposerPackageConfig $package) $finder = new Finder(); foreach ($autoloader->paths as $path) { - $source_path = $this->workingDir . 'vendor' - . DIRECTORY_SEPARATOR . $package->config->name . DIRECTORY_SEPARATOR . $path; + $source_path = DIRECTORY_SEPARATOR . $this->clean($package->path . DIRECTORY_SEPARATOR . $path); $finder->files()->in($source_path); @@ -193,47 +199,31 @@ public function movePackage(ComposerPackageConfig $package) */ public function moveFile(ComposerPackageConfig $package, $autoloader, $file, $path = '') { - $sourceFileAbsolutePath = $file->getPathname(); - if ($autoloader instanceof NamespaceAutoloader) { - $namespacePath = strtolower($autoloader->getNamespacePath()); - $replaceWith = $this->config->getDepDirectory() . $namespacePath; + // The relative path to the file from the project root. + $sourceFilePath = $this->clean(str_replace($this->workingDir, '', $file->getPathname())); + + $packagePath = $this->clean(str_replace($this->workingDir, '', $package->path)); - $findString = $this->workingDir . 'vendor' . DIRECTORY_SEPARATOR . $namespacePath . $path; + if ($autoloader instanceof NamespaceAutoloader) { + $namespacePath = $this->clean($autoloader->getNamespacePath()); - $targetFileRelativePath = str_replace($findString, $replaceWith, $sourceFileAbsolutePath); + // TODO: Should $path come from the NameSpaceAutoloader object? + $sourceVendorPath = $this->clean($packagePath . DIRECTORY_SEPARATOR . $path); + $destinationMozartPath = $this->dep_directory . DIRECTORY_SEPARATOR . $namespacePath; - $packageVendorPath = $this->workingDir . 'vendor' . DIRECTORY_SEPARATOR . $package->config->name - . DIRECTORY_SEPARATOR . $path; - $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); - $targetFileRelativePath = str_replace($packageVendorPath, '', $targetFileRelativePath); + $targetFilePath = str_ireplace($sourceVendorPath, $destinationMozartPath, $sourceFilePath); } else { - $namespacePath = $package->config->name; - $replaceWith = $this->config->getClassmapDirectory() . $namespacePath; - $targetFileRelativePath = str_replace($this->workingDir, $replaceWith, $file->getPathname()); + $packageName = $this->clean($package->config->name); - $packageVendorPath = 'vendor' . DIRECTORY_SEPARATOR . $package->config->name - . DIRECTORY_SEPARATOR; + $destinationMozartPath = $this->classmap_directory . DIRECTORY_SEPARATOR . $packageName; - $packageVendorPath = str_replace('/', DIRECTORY_SEPARATOR, $packageVendorPath); + $targetFilePath = str_ireplace($packagePath, $destinationMozartPath, $sourceFilePath); } - $sourceFileRelativePath = str_replace($this->workingDir, '', $sourceFileAbsolutePath); - $targetFileRelativePath = str_replace($packageVendorPath, DIRECTORY_SEPARATOR, $targetFileRelativePath); - - try { - $this->filesystem->copy($sourceFileRelativePath, $targetFileRelativePath); - } catch (FileNotFoundException $e) { - throw $e; - } catch (FileExistsException $e) { - if (md5_file($sourceFileAbsolutePath) === md5_file($this->workingDir . $targetFileRelativePath)) { - return $targetFileRelativePath; - } else { - throw $e; - } - } + $this->filesystem->copy($sourceFilePath, $targetFilePath); - return $targetFileRelativePath; + return $targetFilePath; } /** @@ -246,7 +236,7 @@ public function moveFile(ComposerPackageConfig $package, $autoloader, $file, $pa protected function deletePackageVendorDirectories(): void { foreach ($this->movedPackages as $movedPackage) { - $packageDir = 'vendor' . DIRECTORY_SEPARATOR . $movedPackage; + $packageDir = 'vendor' . DIRECTORY_SEPARATOR . $this->clean($movedPackage); if (!is_dir($packageDir) || is_link($packageDir)) { continue; } @@ -267,4 +257,20 @@ private function dirIsEmpty(string $dir): bool $di = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); return iterator_count($di) === 0; } + + /** + * For Windows & Unix file paths' compatibility. + * + * * Removes duplicate `\` and `/`. + * * Trims them from each end. + * * Replaces them with the OS agnostic DIRECTORY_SEPARATOR. + * + * @param string $path A full or partial filepath. + * + * @return string + */ + protected function clean($path) + { + return trim(preg_replace('/[\/\\\\]+/', DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR); + } } diff --git a/tests/MoverIntegrationTest.php b/tests/MoverIntegrationTest.php new file mode 100644 index 00000000..30e0979f --- /dev/null +++ b/tests/MoverIntegrationTest.php @@ -0,0 +1,214 @@ +testsWorkingDir = __DIR__ . '/temptestdir'; + if (file_exists($this->testsWorkingDir)) { + $this->delete_dir($this->testsWorkingDir); + } + mkdir($this->testsWorkingDir); + + $mozart_config = new class() { + public $dep_namespace = "Mozart"; + public $classmap_prefix = "Mozart_"; + public $dep_directory = "/dep_directory/"; + public $classmap_directory = "/classmap_directory/"; + + }; + + $composer = new class() { + public $require = array(); + public $extra; + }; + + $composer->extra = new class() { + public $mozart; + }; + + $composer->extra->mozart = $mozart_config; + + $this->composer = $composer; + } + + + /** + * Issue 43. Needs "aws/aws-sdk-php". + * + * League\Flysystem\FileExistsException : File already exists at path: + * dep_directory/vendor/guzzle/guzzle/src/Guzzle/Cache/Zf1CacheAdapter.php + */ + public function testAwsSdkSucceeds() + { + + $composer = $this->composer; + + $composer->require["aws/aws-sdk-php"] = "2.8.31"; + + $composer->extra->mozart->override_autoload = new class() { + public $guzzle_guzzle; + + public function __construct() + { + $this->guzzle_guzzle = new class() { + public $psr_4 = array( + "Guzzle"=>"src/" + ); + }; + } + }; + + $composer_json_string = json_encode($composer); + + $composer_json_string = str_replace("psr_4", "psr-4", $composer_json_string); + $composer_json_string = str_replace("guzzle_guzzle", "guzzle/guzzle", $composer_json_string); + + + file_put_contents($this->testsWorkingDir . '/composer.json', $composer_json_string); + + chdir($this->testsWorkingDir); + + exec('composer install'); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $mozartCompose = new Compose(); + + $result = $mozartCompose->run($inputInterfaceMock, $outputInterfaceMock); + + $this->assertEquals(0, $result); + + $this->assertFileExists($this->testsWorkingDir . '/dep_directory/Aws/Common/Aws.php'); + } + + + /** + * Issue 90. Needs "iio/libmergepdf". + * + * Error: "File already exists at path: classmap_directory/tecnickcom/tcpdf/tcpdf.php". + */ + public function testLibpdfmergeSucceeds() + { + + $composer = $this->composer; + + $composer->require["iio/libmergepdf"] = "4.0.4"; + + file_put_contents($this->testsWorkingDir . '/composer.json', json_encode($composer)); + + chdir($this->testsWorkingDir); + + exec('composer install'); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $mozartCompose = new Compose(); + + $result = $mozartCompose->run($inputInterfaceMock, $outputInterfaceMock); + + $this->assertEquals(0, $result); + + // This test would only fail on Windows? + $this->assertDirectoryNotExists($this->testsWorkingDir .'classmap_directory/iio/libmergepdf/vendor/iio/libmergepdf/tcpdi'); + + $this->assertFileExists($this->testsWorkingDir .'/classmap_directory/iio/libmergepdf/tcpdi/tcpdi.php'); + } + + + + /** + * Issue 97. Package named "crewlabs/unsplash" is downloaded to `vendor/crewlabs/unsplash` but their composer.json + * has the package name as "unsplash/unsplash". + * + * "The "/Users/BrianHenryIE/Sites/mozart-97/vendor/unsplash/unsplash/src" directory does not exist." + */ + public function testCrewlabsUnsplashSucceeds() + { + + $composer = $this->composer; + + $composer->require["crewlabs/unsplash"] = "3.1.0"; + + file_put_contents($this->testsWorkingDir . '/composer.json', json_encode($composer)); + + chdir($this->testsWorkingDir); + + exec('composer install'); + + $inputInterfaceMock = $this->createMock(InputInterface::class); + $outputInterfaceMock = $this->createMock(OutputInterface::class); + + $mozartCompose = new Compose(); + + $result = $mozartCompose->run($inputInterfaceMock, $outputInterfaceMock); + + $this->assertEquals(0, $result); + + } + + + /** + * Delete $this->testsWorkingDir after each test. + * + * @see https://stackoverflow.com/questions/3349753/delete-directory-with-files-in-it + */ + public function tearDown(): void + { + parent::tearDown(); + + $dir = $this->testsWorkingDir; + + $this->delete_dir($dir); + } + + protected function delete_dir($dir) + { + if (!file_exists($dir)) { + return; + } + + $it = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator( + $it, + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($files as $file) { + if ($file->isDir()) { + rmdir($file->getRealPath()); + } else { + unlink($file->getRealPath()); + } + } + rmdir($dir); + } +}