Skip to content

Commit

Permalink
Feat: Allow setting extensions directory
Browse files Browse the repository at this point in the history
Via the commandline it is now possible to set the extensions directory
this directory will be used to load extensions.
  • Loading branch information
jaapio committed Aug 25, 2023
1 parent 4310d46 commit 51c21c0
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 132 deletions.
11 changes: 2 additions & 9 deletions bin/phpdoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@
<?php

use phpDocumentor\AutoloaderLocator;
use phpDocumentor\Extension\ExtensionHandler;
use phpDocumentor\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;

set_time_limit(0);

require __DIR__ . '/../src/phpDocumentor/AutoloaderLocator.php';
$loader = AutoloaderLocator::autoload();

$containerFactory = new \phpDocumentor\Console\ContainerFactory();
$container = $containerFactory->create(
AutoloaderLocator::findVendorPath(),
ExtensionHandler::getInstance(getcwd() . '/.phpdoc/extensions')
);
$output = new \Symfony\Component\Console\Output\ConsoleOutput();

$application = $container->get(\phpDocumentor\Console\Application::class);
$application = new Application();
$application->run();
15 changes: 8 additions & 7 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,12 @@ services:
phpDocumentor\Application:
public: true

phpDocumentor\Console\Application:
public: true
arguments:
$commands: !tagged_iterator { tag: 'console.command' }

phpDocumentor\Console\Command\Project\RunCommand:
arguments:
- '@phpDocumentor\Descriptor\ProjectDescriptorBuilder'
- '@phpdoc.pipeline.complete'
tags: [ { name: 'console.command' } ]
public: true

phpDocumentor\Console\Command\Project\ListSettingsCommand:
tags: [ { name: 'console.command' } ]
Expand Down Expand Up @@ -168,11 +164,16 @@ services:
class: phpDocumentor\Parser\Cache\FilesystemAdapter

Monolog\Logger:
public: true
arguments:
- 'app'

Psr\Log\LoggerInterface: '@Monolog\Logger'
Symfony\Component\EventDispatcher\EventDispatcher: ~
Psr\Log\LoggerInterface:
alias: 'Monolog\Logger'
public: true

Symfony\Component\EventDispatcher\EventDispatcher:
public: true
Symfony\Contracts\EventDispatcher\EventDispatcherInterface: '@Symfony\Component\EventDispatcher\EventDispatcher'
Psr\EventDispatcher\EventDispatcherInterface: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'

Expand Down
110 changes: 54 additions & 56 deletions src/phpDocumentor/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,51 @@
namespace phpDocumentor\Console;

use Monolog\Handler\PsrHandler;
use Monolog\Logger;
use phpDocumentor\AutoloaderLocator;
use phpDocumentor\Extension\ExtensionHandler;
use phpDocumentor\Version;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;

use function getcwd;
use function sprintf;
use function strlen;

class Application extends BaseApplication
{
/** @param iterable<Command> $commands */
public function __construct(
iterable $commands,
EventDispatcher $eventDispatcher,
Logger $logger,
ExtensionHandler $extensionHandler,
) {
public function __construct()
{
parent::__construct('phpDocumentor', (new Version())->getVersion());

foreach ($commands as $command) {
$this->add($command);
$this->setDefaultCommand('project:run');
}

public function doRun(InputInterface $input, OutputInterface $output): int
{
$extensionHandler = ExtensionHandler::getInstance(
$input->hasOption('extensions-dir') ? $input->getOption('extensions-dir') : [],
);
$containerFactory = new ContainerFactory();
$container = $containerFactory->create(
AutoloaderLocator::findVendorPath(),
$extensionHandler,
);

$commands = $container->findTaggedServiceIds('console.command');
foreach ($commands as $id => $_command) {
$this->add($container->get($id));
}

$this->setDefaultCommand('project:run');
$eventDispatcher = $container->get(EventDispatcher::class);
$logger = $container->get(LoggerInterface::class);
$this->setDispatcher($eventDispatcher);

$eventDispatcher->addListener(
Expand All @@ -60,43 +72,42 @@ static function (ConsoleEvent $event) use ($logger): void {
ConsoleEvents::COMMAND,
[$extensionHandler, 'onBoot'],
);
}

protected function getCommandName(InputInterface $input): string|null
{
try {
if ($this->looksLikeACommandName($input->getFirstArgument())) {
$this->find($input->getFirstArgument());

return $input->getFirstArgument();
}
} catch (CommandNotFoundException) {
//Empty by purpose
}

// the regular setDefaultCommand option does not allow for options and arguments; with this workaround
// we can have options and arguments when the first element in the argv options is not a recognized
// command name.
return 'project:run';
return parent::doRun($input, $output);
}

protected function getDefaultInputDefinition(): InputDefinition
{
$inputDefinition = parent::getDefaultInputDefinition();

$inputDefinition->addOption(
new InputOption(
'config',
'c',
InputOption::VALUE_OPTIONAL,
'Location of a custom configuration file',
),
);
$inputDefinition->addOption(
new InputOption('log', null, InputOption::VALUE_OPTIONAL, 'Log file to write to'),
// We are replacing the default command argument with a custom one that allows for a default value.
return new InputDefinition(
$inputDefinition->getOptions() +
[
new InputArgument(
'command',
InputArgument::OPTIONAL,
'The command to execute',
'project:run',
),
new InputOption(
'config',
'c',
InputOption::VALUE_OPTIONAL,
'Location of a custom configuration file',
),
new InputOption(
'extensions-dir',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'extensions directory to load extensions from',
[
getcwd() . '/.phpdoc/extensions',
],
),
new InputOption('log', null, InputOption::VALUE_OPTIONAL, 'Log file to write to'),
],
);

return $inputDefinition;
}

/**
Expand All @@ -108,17 +119,4 @@ public function getLongVersion(): string
{
return sprintf('%s <info>v%s</info>', $this->getName(), $this->getVersion());
}

/**
* Only interpret the first argument as a potential command name if it is set and less than 100 characters.
*
* Anything above 255 characters will cause PHP warnings; and 100 should never occur anyway as a single
* command name.
*
* @link https://github.com/phpDocumentor/phpDocumentor/issues/3215
*/
private function looksLikeACommandName(string|null $argument): bool
{
return $argument !== null && strlen($argument) < 100;
}
}
1 change: 1 addition & 0 deletions src/phpDocumentor/Descriptor/Traits/HasMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function setMetadata(array $metadata): void
$this->metadata = $metadata;
}

/** @return Metadata[] */
public function getMetadata(): array
{
return $this->metadata;
Expand Down
1 change: 0 additions & 1 deletion src/phpDocumentor/Extension/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ abstract class Extension extends BaseExtension
* The constructor of extensions should not be used.
*
* Extensions are loaded by the {@see ExtensionHandler}. An extension should not apply any logic in its
* constructor.
*/
final public function __construct()
{
Expand Down
63 changes: 35 additions & 28 deletions src/phpDocumentor/Extension/ExtensionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;

use function array_filter;
use function array_merge;
use function count;
use function file_exists;

Expand All @@ -33,32 +34,30 @@ final class ExtensionHandler
/** @var ExtensionInfo[] */
private array|null $extensions = null;

/** @var string */
private $extensionsDir;

/** @var ExtensionLoader[] */
private $loaders = [];
private array $loaders = [];

/** @var ExtensionInfo[] */
private array $invalidExtensions = [];

/** @var Validator */
private $validator;

private function __construct(string $extensionsDir)
/** @param string[] $extensionsDirs */
private function __construct(private array $extensionsDirs = [])
{
$this->extensionsDir = $extensionsDir;
$this->loaders[] = new DirectoryLoader();
$this->validator = new Validator(
new ApplicationName(PrettyVersions::getRootPackageName()),
new Version(),
);
}

public static function getInstance(string $extensionsDir = ''): self
/** @param string[] $extensionsDirs */
public static function getInstance(array $extensionsDirs = []): self
{
if (isset(self::$instance) === false) {
self::$instance = new self($extensionsDir);
self::$instance = new self($extensionsDirs);
}

return self::$instance;
Expand All @@ -71,29 +70,11 @@ private function getExtensions(): array
return $this->extensions;
}

if (file_exists($this->extensionsDir) === false) {
$this->extensions = [];

return $this->extensions;
}

$extensions = [];
$iterator = new DirectoryIterator($this->extensionsDir);
foreach ($iterator as $dir) {
if ($dir->isDot()) {
continue;
}

foreach ($this->loaders as $loader) {
if (! $loader->supports($dir)) {
continue;
}

$extensions[$dir->getPathName()] = $loader->load(new DirectoryIterator($dir->getPathName()));
}
foreach ($this->extensionsDirs as $extensionsDir) {
$extensions = array_merge($this->collectExtensionsFromDir($extensionsDir), $extensions);
}

$extensions = array_filter($extensions);
$this->extensions = array_filter($extensions, function (ExtensionInfo $extension) {
return $this->validator->isValid($extension);
});
Expand Down Expand Up @@ -136,4 +117,30 @@ public function onBoot(ConsoleCommandEvent $event): void
$io->warning($extension->getName() . ':' . $extension->getVersion());
}
}

/** @return ExtensionInfo[] */
private function collectExtensionsFromDir(string $extensionsDir): array
{
if (file_exists($extensionsDir) === false) {
return [];
}

$extensions = [];
$iterator = new DirectoryIterator($extensionsDir);
foreach ($iterator as $dir) {
if ($dir->isDot()) {
continue;
}

foreach ($this->loaders as $loader) {
if (! $loader->supports($dir)) {
continue;
}

$extensions[$dir->getPathName()] = $loader->load(new DirectoryIterator($dir->getPathName()));
}
}

return array_filter($extensions);
}
}

0 comments on commit 51c21c0

Please sign in to comment.