Skip to content

Commit 00e6a21

Browse files
authored
Merge pull request #12 from navarr/bin-command
Add "depanno" CLI command
2 parents 607ba1d + 328c05a commit 00e6a21

File tree

14 files changed

+428
-35
lines changed

14 files changed

+428
-35
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"main": "2.x-dev"
4747
}
4848
},
49+
"bin": [
50+
"depanno"
51+
],
4952
"archive": {
5053
"exclude": [
5154
".github",

depanno

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/**
5+
* @copyright 2021 Navarr Barnier. All Rights Reserved.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
use Navarr\Depends\Controller\CliApplication;
11+
12+
if (version_compare('7.1.0', PHP_VERSION, '>')) {
13+
fwrite(
14+
STDERR,
15+
"This version of DepAnno requires PHP 7.1.0 or greater." . PHP_EOL .
16+
"You are currently using PHP " . PHP_VERSION . PHP_EOL
17+
);
18+
die(1);
19+
}
20+
21+
if (!ini_get('date.timezone')) {
22+
ini_set('date.timezone', 'UTC');
23+
}
24+
25+
$found = false;
26+
foreach (
27+
[
28+
__DIR__ . '/../../autoload.php',
29+
__DIR__ . '/../vendor/autoload.php',
30+
__DIR__ . '/vendor/autoload.php',
31+
] as $autoload
32+
) {
33+
if (file_exists($autoload)) {
34+
require_once($autoload);
35+
$found = true;
36+
break;
37+
}
38+
}
39+
40+
if (!$found) {
41+
fwrite(STDERR, "You must install DepAnno using Composer");
42+
die(1);
43+
}
44+
45+
die(CliApplication::execute());

src/Controller/CliApplication.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/**
4+
* @copyright 2021 Navarr Barnier. All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Navarr\Depends\Controller;
10+
11+
use Navarr\Attribute\Dependency;
12+
use Symfony\Component\Console\Application;
13+
14+
#[Dependency('symfony/console', '^5', 'Creates a Symfony Application')]
15+
class CliApplication
16+
{
17+
private const VERSION = '2.1.0';
18+
19+
public static function execute(): int
20+
{
21+
$application = new Application('DepAnno', static::VERSION);
22+
$application->add(new WhyBlockCommandController());
23+
return $application->run();
24+
}
25+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
/**
4+
* @copyright 2021 Navarr Barnier. All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Navarr\Depends\Controller;
10+
11+
use DI\ContainerBuilder;
12+
use InvalidArgumentException;
13+
use Navarr\Attribute\Dependency;
14+
use Navarr\Depends\Command\WhyBlockCommand;
15+
use Navarr\Depends\Command\WhyBlockCommand\CsvOutputHandler;
16+
use Navarr\Depends\Command\WhyBlockCommand\JsonOutputHandler;
17+
use Navarr\Depends\Command\WhyBlockCommand\OutputHandlerInterface;
18+
use Navarr\Depends\Command\WhyBlockCommand\StandardOutputHandler;
19+
use Navarr\Depends\Command\WhyBlockCommand\XmlOutputHandler;
20+
use Navarr\Depends\IssueHandler\FailOnIssueHandler;
21+
use Navarr\Depends\IssueHandler\IssueHandlerInterface;
22+
use Navarr\Depends\IssueHandler\NotifyOnIssueHandler;
23+
use Navarr\Depends\Parser\AstParser;
24+
use Navarr\Depends\Parser\LegacyParser;
25+
use Navarr\Depends\Parser\ParserInterface;
26+
use Navarr\Depends\Parser\ParserPool;
27+
use Navarr\Depends\Proxy\StdOutWriter;
28+
use Navarr\Depends\Proxy\WriterInterface;
29+
use Navarr\Depends\ScopeDeterminer\ComposerScopeDeterminer;
30+
use Navarr\Depends\ScopeDeterminer\DirectoryScopeDeterminer;
31+
use Navarr\Depends\ScopeDeterminer\PhpFileFinder;
32+
use Navarr\Depends\ScopeDeterminer\ScopeDeterminerInterface;
33+
use Psr\Container\ContainerInterface;
34+
use Symfony\Component\Console\Command\Command;
35+
use Symfony\Component\Console\Input\InputArgument;
36+
use Symfony\Component\Console\Input\InputInterface;
37+
use Symfony\Component\Console\Input\InputOption;
38+
use Symfony\Component\Console\Output\OutputInterface;
39+
40+
use function DI\autowire;
41+
use function DI\create;
42+
43+
class WhyBlockCommandController extends Command
44+
{
45+
private const LEGACY_ANNOTATION = 'include-legacy-annotations';
46+
private const FAIL_ON_ERROR = 'fail-on-error';
47+
private const OUTPUT_FORMAT = 'format';
48+
49+
private const FORMAT_TEXT = 'text';
50+
private const FORMAT_CSV = 'csv';
51+
private const FORMAT_JSON = 'json';
52+
private const FORMAT_XML = 'xml';
53+
54+
private const ACCEPTABLE_FORMATS = [
55+
self::FORMAT_CSV,
56+
self::FORMAT_TEXT,
57+
self::FORMAT_JSON,
58+
self::FORMAT_XML,
59+
];
60+
61+
private const FORMAT_MAPPER = [
62+
self::FORMAT_CSV => CsvOutputHandler::class,
63+
self::FORMAT_TEXT => StandardOutputHandler::class,
64+
self::FORMAT_JSON => JsonOutputHandler::class,
65+
self::FORMAT_XML => XmlOutputHandler::class,
66+
];
67+
68+
// phpcs:disable Generic.Files.LineLength.TooLong -- Attribute support pre PHP 8
69+
#[Dependency('symfony/console', '^5', 'Command\'s setName, addArgument and addOption methods as well as InputArgument\'s constants of REQUIRED and VALUE_NONE')]
70+
#[Dependency('php-di/php-di', '^6', 'DI\ContainerBuilder::addDefinitions and the existence of the DI\autowire function')]
71+
// phpcs:enable Generic.Files.LineLength.TooLong
72+
protected function configure(): void
73+
{
74+
$this->setName('why-block')
75+
->addArgument('package', InputArgument::REQUIRED, 'Package to inspect')
76+
->addArgument('version', InputArgument::REQUIRED, 'Version you want to update it to')
77+
->addArgument('directory', InputArgument::REQUIRED, 'Directory to search in')
78+
->addOption(
79+
self::OUTPUT_FORMAT,
80+
['f'],
81+
InputOption::VALUE_OPTIONAL,
82+
'Format to output results in. Accepted values: text, csv, json, xml',
83+
'text'
84+
)
85+
->addOption(
86+
self::FAIL_ON_ERROR,
87+
['e'],
88+
InputOption::VALUE_NONE,
89+
'Immediately fail on parsing errors'
90+
)
91+
->addOption(
92+
self::LEGACY_ANNOTATION,
93+
['l'],
94+
InputOption::VALUE_NONE,
95+
'Include old @dependency/@composerDependency annotations in search'
96+
);
97+
}
98+
99+
#[Dependency('symfony/console', '^5', 'InputInterface::getOption and OutputInterface::writeln')]
100+
protected function execute(
101+
InputInterface $input,
102+
OutputInterface $output
103+
): int {
104+
$packageToSearchFor = $input->getArgument('package');
105+
$versionToCompareTo = $input->getArgument('version');
106+
$directory = $input->getArgument('directory');
107+
$outputFormat = $input->getOption(self::OUTPUT_FORMAT);
108+
109+
if (!is_string($directory)) {
110+
throw new InvalidArgumentException('Only one directory is allowed');
111+
}
112+
if (!is_string($packageToSearchFor)) {
113+
throw new InvalidArgumentException('Only one package is allowed');
114+
}
115+
if (!is_string($versionToCompareTo)) {
116+
throw new InvalidArgumentException('Only one version is allowed');
117+
}
118+
if (!is_string($outputFormat)) {
119+
throw new InvalidArgumentException('Only one output format is allowed');
120+
}
121+
122+
$outputFormat = strtolower($outputFormat);
123+
if (!in_array($outputFormat, static::ACCEPTABLE_FORMATS)) {
124+
$outputFormat = 'text';
125+
}
126+
127+
$containerBuilder = new ContainerBuilder();
128+
$containerBuilder->addDefinitions(
129+
[
130+
InputInterface::class => $input,
131+
OutputInterface::class => $output,
132+
IssueHandlerInterface::class => $input->getOption(static::FAIL_ON_ERROR)
133+
? FailOnIssueHandler::class
134+
: NotifyOnIssueHandler::class,
135+
ParserInterface::class => static function (ContainerInterface $container) use ($input) {
136+
$parsers = [$container->get(AstParser::class)];
137+
if ($input->getOption(static::LEGACY_ANNOTATION)) {
138+
$parsers[] = $container->get(LegacyParser::class);
139+
}
140+
return new ParserPool($parsers);
141+
},
142+
WriterInterface::class => autowire(StdOutWriter::class),
143+
ScopeDeterminerInterface::class => static function (ContainerInterface $container) use ($directory) {
144+
return new DirectoryScopeDeterminer(
145+
$container->get(PhpFileFinder::class),
146+
$directory
147+
);
148+
},
149+
OutputHandlerInterface::class => autowire(static::FORMAT_MAPPER[$outputFormat]),
150+
]
151+
);
152+
$container = $containerBuilder->build();
153+
154+
/** @var WhyBlockCommand $command */
155+
$command = $container->get(WhyBlockCommand::class);
156+
return $command->execute($packageToSearchFor, $versionToCompareTo);
157+
}
158+
}

src/ScopeDeterminer/ComposerScopeDeterminer.php

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ class ComposerScopeDeterminer implements ScopeDeterminerInterface
2626
/** @var Composer */
2727
private $composer;
2828

29-
/** @var PhpFileDeterminer */
30-
private $phpFileDeterminer;
29+
/** @var PhpFileFinder */
30+
private $phpFileFinder;
3131

3232
/** @var int */
3333
private $scope;
3434

3535
public function __construct(
3636
Composer $composer,
37-
PhpFileDeterminer $phpFileDeterminer,
37+
PhpFileFinder $phpFileFinder,
3838
#[ExpectedValues(valuesFromClass: ComposerScopeDeterminer::class)]
3939
int $scope = self::SCOPE_PROJECT_ONLY
4040
) {
4141
$this->composer = $composer;
42-
$this->phpFileDeterminer = $phpFileDeterminer;
42+
$this->phpFileFinder = $phpFileFinder;
4343
$this->scope = $scope;
4444
}
4545

@@ -133,40 +133,10 @@ private function getAllFilesForAutoload(
133133
continue;
134134
}
135135
if (is_dir($realDir)) {
136-
$results = $this->getAllPhpFiles($realDir, $results);
136+
$results = $this->phpFileFinder->findAll($dir, $results);
137137
}
138138
}
139139
}
140140
return $results;
141141
}
142-
143-
/**
144-
* Find all PHP files by recursively searching a directory
145-
*
146-
* @param string $dir Directory to search recursively
147-
* @param string[] $results Array of file paths to merge with
148-
* @return string[] File paths
149-
*/
150-
private function getAllPhpFiles(string $dir, array $results = []): array
151-
{
152-
$files = scandir($dir);
153-
if ($files === false) {
154-
return $results;
155-
}
156-
157-
foreach ($files as $value) {
158-
$path = realpath($dir . DIRECTORY_SEPARATOR . $value);
159-
if ($path === false) {
160-
continue;
161-
}
162-
163-
if (is_file($path) && $this->phpFileDeterminer->isPhp($path)) {
164-
$results[] = $path;
165-
} elseif (is_dir($path) && !in_array($value, ['.', '..'])) {
166-
$results = $this->getAllPhpFiles($path, $results);
167-
}
168-
}
169-
170-
return $results;
171-
}
172142
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* @copyright 2021 Navarr Barnier. All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Navarr\Depends\ScopeDeterminer;
10+
11+
class DirectoryScopeDeterminer implements ScopeDeterminerInterface
12+
{
13+
/** @var string */
14+
private $directory;
15+
16+
/** @var PhpFileFinder */
17+
private $phpfileFinder;
18+
19+
public function __construct(PhpFileFinder $phpfileFinder, string $directory = '.')
20+
{
21+
$this->directory = $directory;
22+
$this->phpfileFinder = $phpfileFinder;
23+
}
24+
25+
public function getFiles(): array
26+
{
27+
return $this->phpfileFinder->findAll($this->directory);
28+
}
29+
}

src/ScopeDeterminer/PhpFileFinder.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* @copyright 2021 Navarr Barnier. All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Navarr\Depends\ScopeDeterminer;
10+
11+
class PhpFileFinder
12+
{
13+
/** @var PhpFileDeterminer */
14+
private $phpFileDeterminer;
15+
16+
public function __construct(PhpFileDeterminer $phpFileDeterminer)
17+
{
18+
$this->phpFileDeterminer = $phpFileDeterminer;
19+
}
20+
21+
/**
22+
* Find all PHP files by recursively searching a directory
23+
*
24+
* @param string $dir Directory to search recursively
25+
* @param string[] $results Array of file paths to merge with
26+
* @return string[] File paths
27+
*/
28+
public function findAll(string $dir, array $results = []): array
29+
{
30+
// Directories is ever expanding by the loop. We do this instead of recursion b/c I have an unhealthy fear
31+
// of recursion limits
32+
$directories = [$dir];
33+
for ($i = 0; $i < count($directories); ++$i) {
34+
$files = scandir($directories[$i]);
35+
if ($files !== false) {
36+
foreach ($files as $value) {
37+
$path = realpath($directories[$i] . DIRECTORY_SEPARATOR . $value);
38+
if ($path === false) {
39+
continue;
40+
}
41+
42+
if (is_file($path) && $this->phpFileDeterminer->isPhp($path)) {
43+
$results[] = $path;
44+
} elseif (is_dir($path) && !in_array($value, ['.', '..'])) {
45+
$directories[] = $path;
46+
}
47+
}
48+
}
49+
}
50+
51+
return $results;
52+
}
53+
}

0 commit comments

Comments
 (0)