Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"require-dev": {
"phpunit/phpunit": "^5.2",
"symfony/process": "^2.7|^3.0"
"symfony/process": "^2.7|^3.0",
"symfony/options-resolver": "^2.7|^3.0"
},
"repositories": [
{ "type": "vcs", "url": "https://github.com/dantleech/BetterReflection" }
Expand Down
53 changes: 53 additions & 0 deletions lib/Composer/ClassFqn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Phpactor\Composer;

final class ClassFqn
{
private $namespace = '';
private $shortName;

private function __construct()
{
}

public static function fromString(string $classFqn)
{
if (0 === strpos($classFqn, '\\')) {
$classFqn = substr($classFqn, 1);
}

if (substr($classFqn, -1) === '\\') {
throw new \InvalidArgumentException(sprintf(
'Trailing slash detected in class name "%s"',
$classFqn
));
}

$pos = strrpos($classFqn, '\\');

$instance = new self();

if (false === $pos) {
$instance->shortName = $classFqn;
return $instance;
}

$instance->namespace = substr($classFqn, 0, $pos);
$instance->shortName = substr($classFqn, $pos + 1);

return $instance;
}

public function getNamespace()
{
return $this->namespace;
}

public function getShortName()
{
return $this->shortName;
}
}
87 changes: 87 additions & 0 deletions lib/Composer/ClassNameResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Phpactor\Composer;

use Phpactor\Generation\SnippetGeneratorInterface;
use Composer\Autoload\ClassLoader;
use Phpactor\CodeContext;
use Phpactor\Composer\ClassFqn;

class ClassNameResolver
{
/**
* @var ClassLoader
*/
private $classLoader;

public function __construct(ClassLoader $classLoader)
{
$this->classLoader = $classLoader;
}

public function resolve(string $filePath): ClassFqn
{
$prefixes = array_merge(
$this->classLoader->getPrefixes(),
$this->classLoader->getPrefixesPsr4(),
$this->classLoader->getClassMap()
);

$map = [];

if (substr($filePath, 0, 1) === '/') {
throw new \InvalidArgumentException(sprintf(
'Do not support absolute paths.'
));
}

$cwd = getcwd() . '/';

$bestLength = $base = $basePath = null;
$isExact = false;

foreach ($prefixes as $prefix => $files) {
if (is_string($files)) {
$files = [ $files ];
}

foreach ($files as $file) {
$path = str_replace($cwd, '', realpath($file));

if (strpos($filePath, $path) === 0) {
if (null !== $bestLength && strlen($path) < $bestLength) {
continue;
}

$base = $prefix;
$basePath = $path;
$bestLength = strlen($path);

if ($filePath === $path) {
$isExact = true;
break 2; // we are done here
}
}
}
}

if (null === $base) {
throw new \RuntimeException(sprintf(
'Could not resolve base path from Composer autoloader'
));
}

if (false === $isExact && substr($base, -1) !== '\\') {
$base .= '\\';
}

$className = substr($filePath, strlen($basePath) + 1);
$className = str_replace('/', '\\', $className);
$className = $base . $className;
$className = preg_replace('{\.(.+)$}', '', $className);

return ClassFqn::fromString($className);
}
}
29 changes: 22 additions & 7 deletions lib/Console/Command/GenerateSnippetCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,48 @@
use Symfony\Component\Console\Input\InputArgument;
use Phpactor\Util\FileUtil;
use Phpactor\Generation\Snippet\ImplementMissingMethodsGenerator;
use Phpactor\Generation\SnippetGeneratorRegistry;
use Phpactor\Generation\SnippetCreator;
use Symfony\Component\Console\Input\InputOption;

class GenerateSnippetCommand extends Command
{
/**
* @var SnippetGeneratorRegistry
* @var SnippetCreator
*/
private $registry;
private $creator;

public function __construct(SnippetGeneratorRegistry $registry)
public function __construct(SnippetCreator $creator)
{
parent::__construct();
$this->registry = $registry;
$this->creator = $creator;
}

public function configure()
{
$this->setName('generate:snippet');
$this->addArgument('generator', InputArgument::REQUIRED, 'Name of snippet generator');
$this->addOption('options', null, InputOption::VALUE_REQUIRED, 'JSON encoded string of options', []);
Handler\CodeContextHandler::configure($this);
}

public function execute(InputInterface $input, OutputInterface $output)
{
$options = $input->getOption('options');
$context = Handler\CodeContextHandler::contextFromInput($input);
$generator = $this->registry->get($input->getArgument('generator'));
$snippet = $generator->generate($context);

if ($options) {
$decoded = json_decode($options, true);

if (false === $decoded) {
throw new \InvalidArgumentException(sprintf(
'Could not decode JSON option string "%s"', $options
));
}

$options = $decoded;
}

$snippet = $this->creator->create($context, $input->getArgument('generator'), $options);
$output->write($snippet);
}
}
61 changes: 45 additions & 16 deletions lib/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
use Phpactor\Generation\SnippetGeneratorRegistry;
use Phpactor\Generation\Snippet\ImplementMissingMethodsGenerator;
use Phpactor\Generation\Snippet\MissingPropertiesGenerator;
use Phpactor\Generation\Snippet\ClassGenerator;
use Phpactor\Generation\SnippetCreator;
use Phpactor\Composer\ClassNameResolver;

class CoreExtension implements ExtensionInterface
{
Expand All @@ -50,6 +53,7 @@ public function load(Container $container)
$this->registerConsole($container);
$this->registerStorage($container);
$this->registerMisc($container);
$this->registerComposer($container);
}

private function registerComplete(Container $container)
Expand Down Expand Up @@ -95,6 +99,31 @@ private function registerStorage(Container $container)
private function registerMisc(Container $container)
{
$container->register('reflector', function (Container $container) {
$locators = [];

// HACK: for testing purposes ...
if ($source = $container->getParameter('source')) {
$locators[] = new StringSourceLocator($source);
}
$classLoader = $container->get('composer.class_loader');
$locators[] = new ComposerSourceLocator($classLoader);
$locators[] = new PhpInternalSourceLocator($classLoader);

return new ClassReflector(new AggregateSourceLocator($locators));
});

$container->register('util.class', function () {
return new ClassUtil();
});
}

private function registerComposer(Container $container)
{
$container->register('composer.class_name_resolver', function (Container $container) {
return new ClassNameResolver($container->get('composer.class_loader'));
});

$container->register('composer.class_loader', function (Container $container) {
$bootstrap = $container->getParameter('autoload');

if (!file_exists($bootstrap)) {
Expand All @@ -109,21 +138,7 @@ private function registerMisc(Container $container)
throw new \RuntimeException('Autoloader is not an instance of ClassLoader');
}

$locators = [];

// HACK: for testing purposes ...
if ($source = $container->getParameter('source')) {
$locators[] = new StringSourceLocator($source);
}

$locators[] = new ComposerSourceLocator($autoloader);
$locators[] = new PhpInternalSourceLocator($autoloader);

return new ClassReflector(new AggregateSourceLocator($locators));
});

$container->register('util.class', function () {
return new ClassUtil();
return $autoloader;
});
}

Expand All @@ -141,7 +156,7 @@ private function registerConsole(Container $container)
});

$container->register('command.generate', function (Container $container) {
return new GenerateSnippetCommand($container->get('generator.registry.snippet'));
return new GenerateSnippetCommand($container->get('generator.snippet_creator'));
});

$container->register('command.complete', function (Container $container) {
Expand All @@ -158,6 +173,10 @@ private function registerConsole(Container $container)

private function registerGeneration(Container $container)
{
$container->register('generator.snippet_creator', function (Container $container) {
return new SnippetCreator($container->get('generator.registry.snippet'));
});

$container->register('generator.registry.snippet', function (Container $container) {
$registry = new SnippetGeneratorRegistry();
$registry->register(
Expand All @@ -168,6 +187,10 @@ private function registerGeneration(Container $container)
'implement_missing_properties',
$container->get('generator.snippet.implement_missing_properties')
);
$registry->register(
'class',
$container->get('generator.snippet.class')
);

return $registry;
});
Expand All @@ -185,5 +208,11 @@ private function registerGeneration(Container $container)
$container->get('util.class')
);
});

$container->register('generator.snippet.class', function(Container $container) {
return new ClassGenerator(
$container->get('composer.class_name_resolver')
);
});
}
}
50 changes: 50 additions & 0 deletions lib/Generation/Snippet/ClassGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Phpactor\Generation\Snippet;

use Phpactor\Generation\SnippetGeneratorInterface;
use Composer\Autoload\ClassLoader;
use Phpactor\CodeContext;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Phpactor\Composer\ClassNameResolver;
use Phpactor\Composer\ClassFqn;

class ClassGenerator implements SnippetGeneratorInterface
{
/**
* @var ClassLoader
*/
private $resolver;

public function __construct(ClassNameResolver $resolver)
{
$this->resolver = $resolver;
}

public function generate(CodeContext $codeContext, array $options): string
{
$classFqn = $this->resolver->resolve($codeContext->getPath());

return $this->createSnippet($classFqn, $options['type']);
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('type', 'class');
$resolver->setAllowedValues('type', [ 'class', 'trait', 'interface' ]);
}

private function createSnippet(ClassFqn $fqn, string $type)
{
$snippet = [];
$snippet[] = '<?php';
$snippet[] = '';
$snippet[] = 'namespace ' . $fqn->getNamespace() . ';';
$snippet[] = '';
$snippet[] = sprintf('%s %s', $type, $fqn->getShortName());
$snippet[] = '{';
$snippet[] = '}';

return implode(PHP_EOL, $snippet);
}
}
Loading