diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b5084f4..6594e5d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -52,7 +52,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -85,7 +85,6 @@ jobs: strategy: matrix: php-version: - - '7.3' - '7.4' - '8.0' diff --git a/composer.json b/composer.json index aaaa1357..4a542870 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "psr/log": "^1.0", "webmozart/path-util": "^2.3" }, diff --git a/lib/Adapter/Composer/ComposerClassToFile.php b/lib/Adapter/Composer/ComposerClassToFile.php index ded37869..96ca8333 100644 --- a/lib/Adapter/Composer/ComposerClassToFile.php +++ b/lib/Adapter/Composer/ComposerClassToFile.php @@ -11,15 +11,9 @@ class ComposerClassToFile implements ClassToFile { - /** - * @var ClassLoader - */ - private $classLoader; + private ClassLoader $classLoader; - /** - * @var LoggerInterface - */ - private $logger; + private LoggerInterface $logger; public function __construct(ClassLoader $classLoader, LoggerInterface $logger = null) { @@ -31,8 +25,8 @@ public function classToFileCandidates(ClassName $className): FilePathCandidates { $candidates = []; foreach ($this->getStrategies() as $strategy) { - list($prefixes, $inflector) = $strategy; - $this->resolveFile($candidates, $prefixes, $inflector, $className); + list($prefixes, $inflector, $separator) = $strategy; + $this->resolveFile($candidates, $prefixes, $inflector, $className, $separator); } // order with the longest prefixes first @@ -54,30 +48,40 @@ private function getStrategies(): array [ $this->classLoader->getPrefixesPsr4(), new Psr4NameInflector(), + Psr4NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getPrefixes(), new Psr0NameInflector(), + Psr0NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getClassMap(), new ClassmapNameInflector(), + Psr4NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getFallbackDirs(), new Psr0NameInflector(), + Psr0NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getFallbackDirsPsr4(), // PSR0 name inflector works here as there is no prefix new Psr0NameInflector(), + Psr0NameInflector::NAMESPACE_SEPARATOR, ], ]; } - private function resolveFile(&$candidates, array $prefixes, NameInflector $inflector, ClassName $className): void - { - $fileCandidates = $this->getFileCandidates($className, $prefixes); + private function resolveFile( + &$candidates, + array $prefixes, + NameInflector $inflector, + ClassName $className, + string $separator + ): void { + $fileCandidates = $this->getFileCandidates($className, $prefixes, $separator); foreach ($fileCandidates as $prefix => $files) { $prefixCandidates = []; @@ -93,7 +97,7 @@ private function resolveFile(&$candidates, array $prefixes, NameInflector $infle } } - private function getFileCandidates(ClassName $className, array $prefixes) + private function getFileCandidates(ClassName $className, array $prefixes, string $separator) { $candidates = []; @@ -102,7 +106,7 @@ private function getFileCandidates(ClassName $className, array $prefixes) $prefix = ''; } - if ($prefix && false === $className->beginsWith($prefix)) { + if ($prefix && false === $className->beginsWith($prefix, $separator)) { continue; } diff --git a/lib/Adapter/Composer/Psr0NameInflector.php b/lib/Adapter/Composer/Psr0NameInflector.php index bf3669ba..e6162ebd 100644 --- a/lib/Adapter/Composer/Psr0NameInflector.php +++ b/lib/Adapter/Composer/Psr0NameInflector.php @@ -7,10 +7,14 @@ final class Psr0NameInflector implements NameInflector { + public const NAMESPACE_SEPARATOR = '_'; + public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - if (substr($prefix, -1) === '_' && $className->beginsWith($prefix)) { - $elements = explode('_', $className); + if ( + in_array(substr($prefix, -1), [self::NAMESPACE_SEPARATOR, ClassName::DEFAULT_NAMESPACE_SEPARATOR]) + && $className->beginsWith($prefix, self::NAMESPACE_SEPARATOR)) { + $elements = explode(self::NAMESPACE_SEPARATOR, $className); $className = implode('\\', $elements); } diff --git a/lib/Adapter/Composer/Psr4NameInflector.php b/lib/Adapter/Composer/Psr4NameInflector.php index f2d92ff7..02721881 100644 --- a/lib/Adapter/Composer/Psr4NameInflector.php +++ b/lib/Adapter/Composer/Psr4NameInflector.php @@ -7,9 +7,11 @@ final class Psr4NameInflector implements NameInflector { + public const NAMESPACE_SEPARATOR = ClassName::DEFAULT_NAMESPACE_SEPARATOR; + public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - $relativePath = str_replace('\\', '/', substr($className, strlen($prefix))).'.php'; + $relativePath = str_replace(self::NAMESPACE_SEPARATOR, '/', substr($className, strlen($prefix))).'.php'; return FilePath::fromParts([$mappedPath, $relativePath]); } @@ -22,7 +24,7 @@ public function inflectToClassName(FilePath $filePath, string $pathPrefix, strin } $className = substr($filePath, strlen($pathPrefix) + 1); - $className = str_replace('/', '\\', $className); + $className = str_replace('/', self::NAMESPACE_SEPARATOR, $className); $className = $classPrefix.$className; $className = preg_replace('{\.(.+)$}', '', $className); diff --git a/lib/Domain/ClassName.php b/lib/Domain/ClassName.php index 485e10c1..b1e37aee 100644 --- a/lib/Domain/ClassName.php +++ b/lib/Domain/ClassName.php @@ -2,9 +2,13 @@ namespace Phpactor\ClassFileConverter\Domain; +use function in_array; + final class ClassName { - private $fullyQualifiedName; + public const DEFAULT_NAMESPACE_SEPARATOR = '\\'; + + private string $fullyQualifiedName; private function __construct() { @@ -15,7 +19,7 @@ public function __toString() return $this->fullyQualifiedName; } - public static function fromString($string) + public static function fromString(string $string): self { $new = new self(); $new->fullyQualifiedName = $string; @@ -23,22 +27,44 @@ public static function fromString($string) return $new; } - public function namespace() + public function namespace(): string { - return substr($this->fullyQualifiedName, 0, (int) strrpos($this->fullyQualifiedName, '\\')); + return substr($this->fullyQualifiedName, 0, (int) strrpos( + $this->fullyQualifiedName, + self::DEFAULT_NAMESPACE_SEPARATOR, + )); } - public function name() + public function name(): string { - $pos = strrpos($this->fullyQualifiedName, '\\'); + $pos = strrpos($this->fullyQualifiedName, self::DEFAULT_NAMESPACE_SEPARATOR); if (false === $pos) { return $this->fullyQualifiedName; } return substr($this->fullyQualifiedName, $pos + 1); } - public function beginsWith($prefix) + public function beginsWith(string $prefix, string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR): bool { - return 0 === strpos($this->fullyQualifiedName, $prefix); + if ($prefix === $this->fullyQualifiedName) { + return true; + } + + if (0 !== strpos($this->fullyQualifiedName, $prefix)) { + return false; + } + + if ($this->isNamespaceSeparator(mb_substr($prefix, -1, 1), $additionalNseparator)) { + return true; + } + + return mb_substr($this->fullyQualifiedName, mb_strlen($prefix), 1) === $additionalNseparator; + } + + private function isNamespaceSeparator( + string $character, + string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR + ): bool { + return in_array($character, [self::DEFAULT_NAMESPACE_SEPARATOR, $additionalNseparator], true); } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3952583e..8c78cbce 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,41 +95,6 @@ parameters: count: 1 path: lib/Domain/ChainFileToClass.php - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has parameter \\$prefix with no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has parameter \\$string with no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:name\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:namespace\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Property Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:\\$fullyQualifiedName has no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - message: "#^Class Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassNameCandidates implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" count: 1 diff --git a/tests/Integration/Composer/ComposerClassToFileTest.php b/tests/Integration/Composer/ComposerClassToFileTest.php index 004dc20f..7f381800 100644 --- a/tests/Integration/Composer/ComposerClassToFileTest.php +++ b/tests/Integration/Composer/ComposerClassToFileTest.php @@ -42,6 +42,17 @@ public function testPsr4(): void $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class', ['psr4/Foo/Class.php']); } + /** + * @testdox PSR-4 class name to a file path. + */ + public function testPsr4WithClassmapAuthoritative(): void + { + $this->loadExample('psr4-classmap-authoritative.json'); + $this->getClassLoader()->addClassMap(['Acme\\Test\\Foo\\Bar' => $this->workspacePath() . '/psr4/Foo/Bar.php']); + $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Bar2', ['psr4/Foo/Bar2.php']); + $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class2', ['psr4/Foo/Class2.php']); + } + /** * @testdox PSR-4 class in dev namespace */ @@ -118,7 +129,6 @@ public function testPsr0ShortNamePrefix2(): void $this->assertClassNameToFilePath('Twig_Tests_Extension', [ 'psr0/twig/Twig/Tests/Extension.php' ]); } - /** * @testdox PSR-4 fallback */ diff --git a/tests/Integration/Composer/ComposerTestCase.php b/tests/Integration/Composer/ComposerTestCase.php index 0bebea57..a9e9e7dc 100644 --- a/tests/Integration/Composer/ComposerTestCase.php +++ b/tests/Integration/Composer/ComposerTestCase.php @@ -2,6 +2,7 @@ namespace Phpactor\ClassFileConverter\Tests\Integration\Composer; +use Composer\Autoload\ClassLoader; use Phpactor\ClassFileConverter\Tests\Integration\IntegrationTestCase; use Symfony\Component\Filesystem\Filesystem; @@ -26,7 +27,7 @@ protected function loadExample($composerFile): void exec('composer install 2> /dev/null'); } - protected function getClassLoader() + protected function getClassLoader(): ClassLoader { return require $this->workspacePath().'/vendor/autoload.php'; } diff --git a/tests/Integration/Composer/composers/psr4-classmap-authoritative.json b/tests/Integration/Composer/composers/psr4-classmap-authoritative.json new file mode 100644 index 00000000..f4554d88 --- /dev/null +++ b/tests/Integration/Composer/composers/psr4-classmap-authoritative.json @@ -0,0 +1,18 @@ +{ + "name": "dantleech/basic", + "authors": [ + { + "name": "dantleech", + "email": "dan.t.leech@gmail.com" + } + ], + "require": {}, + "config": { + "classmap-authoritative": true + }, + "autoload": { + "psr-4": { + "Acme\\Test\\": "psr4/" + } + } +}