Skip to content

Commit

Permalink
[DX] Add NamespaceToPSR4Command (#17)
Browse files Browse the repository at this point in the history
* add NamespaceToPSR4Command

* less deps
  • Loading branch information
TomasVotruba authored Feb 8, 2024
1 parent 9c427d5 commit 3945066
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 9 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ vendor/bin/easy-ci check-commented-code packages --line-limit 5
<br>
### 3. Find multiple classes in single file
To make PSR-4 work properly, each class must be in its own file. This command makes it easy to spot multiple classes in single file:
```bash
vendor/bin/easy-ci find-multi-classes src
```
<br>
### 4. Update Namespace to match PSR-4 Root
Is your class in wrong namespace? Make it match your PSR-4 root:
```bash
vendor/bin/easy-ci namespace-to-psr-4 src --namespace-root "App\\"
```
This will update all files in your `/src` directory, to starts with `App\\` and follow full PSR-4 path:
```diff
# file path: src/Repository/TalkRepository.php
-namespace Model;
+namespace App\Repository;
...
```
<br>
## Report Issues
In case you are experiencing a bug or want to request a new feature head over to the [Symplify monorepo issue tracker](https://github.com/symplify/symplify/issues)
Expand Down
10 changes: 4 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,18 @@
"require": {
"php": ">=8.2",
"composer/semver": "^3.3",
"illuminate/container": "^10.42",
"illuminate/container": "^10.43",
"nette/robot-loader": "^3.4",
"nette/utils": "^3.2",
"symfony/console": "^6.3",
"symfony/finder": "^7.0",
"webmozart/assert": "^1.11"
},
"require-dev": {
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10.57",
"phpunit/phpunit": "^10.5",
"rector/rector": "^0.19",
"symplify/easy-coding-standard": "^12.0",
"symplify/phpstan-extensions": "^11.2",
"rector/rector": "^1.0",
"symplify/easy-coding-standard": "^12.1",
"tomasvotruba/class-leak": "^0.2"
},
"autoload": {
Expand Down Expand Up @@ -53,7 +51,7 @@
"scripts": {
"check-cs": "vendor/bin/ecs check --ansi",
"fix-cs": "vendor/bin/ecs check --fix --ansi",
"phpstan": "vendor/bin/phpstan analyse --ansi --error-format symplify",
"phpstan": "vendor/bin/phpstan analyse --ansi",
"rector": "vendor/bin/rector process --dry-run --ansi"
}
}
117 changes: 117 additions & 0 deletions src/Command/NamespaceToPSR4Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCI\Command;

use Nette\Utils\FileSystem;
use Nette\Utils\Strings;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

final class NamespaceToPSR4Command extends Command
{
public function __construct(
private readonly SymfonyStyle $symfonyStyle,
) {
parent::__construct();
}

protected function configure(): void
{
$this->setName('namespace-to-psr-4');

$this->setDescription('Change namespace in your PHP files to match PSR-4 root');

$this->addArgument(
'path',
InputArgument::REQUIRED,
'Single directory path to ensure namespace matches, e.g. "tests"'
);

$this->addOption(
'namespace-root',
null,
InputOption::VALUE_REQUIRED,
'Namespace root for files in provided path, e.g. "App\\Tests"'
);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$path = (string) $input->getArgument('path');
$namespaceRoot = (string) $input->getArgument('namespace-root');

$fileInfos = $this->findFilesInPath($path);

$changedFilesCount = 0;

/** @var SplFileInfo $fileInfo */
foreach ($fileInfos as $fileInfo) {
$expectedNamespace = $this->resolveExpectedNamespace($namespaceRoot, $fileInfo);
$expectedNamespaceLine = 'namespace ' . $expectedNamespace . ';';

// 1. got the correct namespace
if (\str_contains($fileInfo->getContents(), $expectedNamespaceLine)) {
continue;
}

// 2. incorrect namespace found
$this->symfonyStyle->note(sprintf(
'File "%s"%s fixed to expected namespace "%s"',
$fileInfo->getRelativePathname(),
PHP_EOL,
$expectedNamespace
));

// 3. replace
$correctedContents = Strings::replace(
$fileInfo->getContents(),
'#namespace (.*?);#',
$expectedNamespaceLine
);

// 4. print file
FileSystem::write($fileInfo->getRealPath(), $correctedContents);

++$changedFilesCount;
}

if ($changedFilesCount === 0) {
$this->symfonyStyle->success(sprintf('All %d files have correct namespace', count($fileInfos)));
} else {
$this->symfonyStyle->success(sprintf('Fixed %d files', $changedFilesCount));
}

return self::SUCCESS;
}

/**
* @return SplFileInfo[]
*/
private function findFilesInPath(string $path): array
{
$finder = Finder::create()
->files()
->in([$path])
->name('*.php')
->sortByName()
->filter(static fn (SplFileInfo $fileInfo): bool =>
// filter classes
str_contains($fileInfo->getContents(), 'class '));

return iterator_to_array($finder->getIterator());
}

private function resolveExpectedNamespace(string $namespaceRoot, SplFileInfo $fileInfo): string
{
$relativePathNamespace = str_replace('/', '\\', $fileInfo->getRelativePath());
return $namespaceRoot . '\\' . $relativePathNamespace;
}
}
4 changes: 3 additions & 1 deletion src/DependencyInjection/ContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Symplify\EasyCI\Command\CheckCommentedCodeCommand;
use Symplify\EasyCI\Command\CheckConflictsCommand;
use Symplify\EasyCI\Command\FindMultiClassesCommand;
use Symplify\EasyCI\Command\NamespaceToPSR4Command;
use Symplify\EasyCI\Command\ValidateFileLengthCommand;
use Symplify\EasyCI\Testing\Command\DetectUnitTestsCommand;

Expand All @@ -31,14 +32,15 @@ public function create(): Container
);

$container->singleton(Application::class, function (Container $container): Application {
$application = new Application();
$application = new Application('Easy CI toolkit');

$commands = [
$container->make(CheckCommentedCodeCommand::class),
$container->make(CheckConflictsCommand::class),
$container->make(ValidateFileLengthCommand::class),
$container->make(DetectUnitTestsCommand::class),
$container->make(FindMultiClassesCommand::class),
$container->make(NamespaceToPSR4Command::class),
];

$application->addCommands($commands);
Expand Down
2 changes: 1 addition & 1 deletion src/Git/ConflictResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function extractFromFileInfos(array $filePaths): array
}

// test fixtures, that should be ignored
if (str_contains(realpath($filePath), '/tests/Git/ConflictResolver/Fixture')) {
if (str_contains((string) realpath($filePath), '/tests/Git/ConflictResolver/Fixture')) {
continue;
}

Expand Down
5 changes: 4 additions & 1 deletion src/Testing/Command/DetectUnitTestsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return self::SUCCESS;
}

$filesPHPUnitXmlContents = $this->phpunitXmlPrinter->printFiles($unitTestCasesClassesToFilePaths, getcwd());
$filesPHPUnitXmlContents = $this->phpunitXmlPrinter->printFiles(
$unitTestCasesClassesToFilePaths,
(string) getcwd()
);

FileSystem::write(self::OUTPUT_FILENAME, $filesPHPUnitXmlContents);

Expand Down

0 comments on commit 3945066

Please sign in to comment.