Skip to content
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ return static function (Config $config): void {

# Available rules

**Hint**: If you want to test how a Rule work, you can use the command like `phparkitect debug:expression <RuleName> <arguments>` to check which class satisfy the rule in your current folder.

For example: `phparkitect debug:expression ResideInOneOfTheseNamespaces App`

---

Currently, you can check if a class:

### Depends on a namespace
Expand Down
2 changes: 2 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

allowStringToStandInForClass="true"
>
<projectFiles>
<directory name="src" />
Expand Down
119 changes: 119 additions & 0 deletions src/CLI/Command/DebugExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);

namespace Arkitect\CLI\Command;

use Arkitect\Analyzer\FileParserFactory;
use Arkitect\ClassSet;
use Arkitect\CLI\TargetPhpVersion;
use Arkitect\Rules\ParsingError;
use Arkitect\Rules\Violations;
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;

class DebugExpression extends Command
{
/** @var string|null */
public static $defaultName = 'debug:expression';

/** @var string|null */
public static $defaultDescription = <<< 'EOT'
Check which classes respect an expression
EOT;

/** @var string */
public static $help = <<< 'EOT'
Check which classes respect an expression
EOT;

protected function configure(): void
{
$this
->setHelp(self::$help)
->addArgument('expression', InputArgument::REQUIRED)
->addArgument('arguments', InputArgument::IS_ARRAY)
->addOption(
'from-dir',
'd',
InputOption::VALUE_REQUIRED,
'The folder in which to search the classes',
'.'
)
->addOption(
'target-php-version',
't',
InputOption::VALUE_OPTIONAL,
'Target php version to use for parsing'
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$phpVersion = $input->getOption('target-php-version');
$targetPhpVersion = TargetPhpVersion::create($phpVersion);
$fileParser = FileParserFactory::createFileParser($targetPhpVersion);

$classSet = ClassSet::fromDir($input->getOption('from-dir'));
foreach ($classSet as $file) {
$fileParser->parse($file->getContents(), $file->getRelativePathname());
$parsedErrors = $fileParser->getParsingErrors();

if (\count($parsedErrors) > 0) {
$output->writeln('WARNING: Some files could not be parsed for these errors:');
/** @var ParsingError $parsedError */
foreach ($parsedErrors as $parsedError) {
$output->writeln(' - '.$parsedError->getError().': '.$parsedError->getRelativeFilePath());
}
$output->writeln('');
}

$ruleName = $input->getArgument('expression');
/** @var class-string $ruleFQCN */
$ruleFQCN = 'Arkitect\Expression\ForClasses\\'.$ruleName;
$arguments = $input->getArgument('arguments');

try {
$expressionReflection = new \ReflectionClass($ruleFQCN);
} catch (\ReflectionException $exception) {
$output->writeln("Error: Expression '$ruleName' not found.");

return 2;
}

$constructorReflection = $expressionReflection->getConstructor();
if (null === $constructorReflection) {
$maxNumberOfArguments = 0;
$minNumberOfArguments = 0;
} else {
$maxNumberOfArguments = $constructorReflection->getNumberOfParameters();
$minNumberOfArguments = $constructorReflection->getNumberOfRequiredParameters();
}

if (\count($arguments) < $minNumberOfArguments) {
$output->writeln("Error: Too few arguments for '$ruleName'.");

return 2;
}

if (\count($arguments) > $maxNumberOfArguments) {
$output->writeln("Error: Too many arguments for '$ruleName'.");

return 2;
}

$rule = new $ruleFQCN(...$arguments);
foreach ($fileParser->getClassDescriptions() as $classDescription) {
$violations = new Violations();
$rule->evaluate($classDescription, $violations, '');
if (0 === $violations->count()) {
$output->writeln($classDescription->getFQCN());
}
}
}

return 0;
}
}
2 changes: 2 additions & 0 deletions src/CLI/PhpArkitectApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Arkitect\CLI;

use Arkitect\CLI\Command\Check;
use Arkitect\CLI\Command\DebugExpression;
use Arkitect\CLI\Command\Init;

class PhpArkitectApplication extends \Symfony\Component\Console\Application
Expand All @@ -22,6 +23,7 @@ public function __construct()
parent::__construct('PHPArkitect', Version::get());
$this->add(new Check());
$this->add(new Init());
$this->add(new DebugExpression());
}

public function getLongVersion()
Expand Down
90 changes: 90 additions & 0 deletions tests/E2E/Cli/DebugExpressionCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\E2E\Cli;

use Arkitect\CLI\PhpArkitectApplication;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\ApplicationTester;

class DebugExpressionCommandTest extends TestCase
{
public function test_you_need_to_specify_the_expression(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression']);
$this->assertEquals(1, $appTester->getStatusCode());
}

public function test_zero_results(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'Extend', 'arguments' => ['NotFound'], '--from-dir' => __DIR__]);
$this->assertEquals('', $appTester->getDisplay());
$this->assertEquals(0, $appTester->getStatusCode());
}

public function test_some_classes_found(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'NotExtend', 'arguments' => ['NotFound'], '--from-dir' => __DIR__.'/../_fixtures/mvc/Domain']);
$this->assertEquals("App\Domain\Model\n", $appTester->getDisplay());
$this->assertEquals(0, $appTester->getStatusCode());
}

public function test_meaningful_errors_for_too_few_arguments_for_the_expression(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'NotExtend', 'arguments' => [], '--from-dir' => __DIR__.'/../_fixtures/mvc/Domain']);
$this->assertEquals("Error: Too few arguments for 'NotExtend'.\n", $appTester->getDisplay());
$this->assertEquals(2, $appTester->getStatusCode());
}

public function test_meaningful_errors_for_too_many_arguments_for_the_expression(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'NotExtend', 'arguments' => ['First', 'Second'], '--from-dir' => __DIR__.'/../_fixtures/mvc/Domain']);
$this->assertEquals("Error: Too many arguments for 'NotExtend'.\n", $appTester->getDisplay());
$this->assertEquals(2, $appTester->getStatusCode());
}

public function test_optional_argument_for_expression_can_be_avoided(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'NotHaveDependencyOutsideNamespace', 'arguments' => ['NotFound'], '--from-dir' => __DIR__]);
$this->assertEquals('', $appTester->getDisplay());
$this->assertEquals(0, $appTester->getStatusCode());
}

public function test_expression_not_found(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'blabla', 'arguments' => ['NotFound'], '--from-dir' => __DIR__]);
$this->assertEquals("Error: Expression 'blabla' not found.\n", $appTester->getDisplay());
$this->assertEquals(2, $appTester->getStatusCode());
}

public function test_parse_error_dont_stop_execution(): void
{
$appTester = $this->createAppTester();
$appTester->run(['debug:expression', 'expression' => 'NotExtend', 'arguments' => ['NotFound'], '--from-dir' => __DIR__.'/../_fixtures/parse_error']);
$errorMessage = <<<END
WARNING: Some files could not be parsed for these errors:
- Syntax error, unexpected T_STRING, expecting '{' on line 8: Services/CartService.php

App\Services\UserService

END;
$this->assertEquals($errorMessage, $appTester->getDisplay());
$this->assertEquals(0, $appTester->getStatusCode());
}

private function createAppTester(): ApplicationTester
{
$app = new PhpArkitectApplication();
$app->setAutoExit(false);

return new ApplicationTester($app);
}
}