Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composer-specific source locator builder #461

Merged
merged 29 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5f629d6
Spec for an API that, given a set of psr-4 mappings, can look up all …
Ocramius Oct 28, 2018
976e256
The constructor of the `Psr4Mapping` definition must produce equivale…
Ocramius Oct 28, 2018
2b3129c
Verify PSR-4 class lookups against a data-provider
Ocramius Mar 20, 2019
73b1719
PSR-4 lookups for non-class identifiers should fail
Ocramius Mar 20, 2019
fa64531
Abstraction for a PSR-4 mapping data structure
Ocramius Mar 20, 2019
a9879ce
Implemented a PSR-4-based source locator
Ocramius Mar 20, 2019
bc46762
PSR-0 mapping definitions abstraction
Ocramius Mar 20, 2019
f6b2cc0
Common abstraction for lookups via PSR autoloader mappings
Ocramius Mar 20, 2019
c153a9e
Rewrote PSR-4 locator to work with any PSR autoload mappings
Ocramius Mar 20, 2019
0f410a6
Prototype for an utility that builds a composer autoloader given a pr…
Ocramius Mar 20, 2019
132dca5
Corrected callback parameter order
Ocramius Mar 20, 2019
9d04388
Corrected reference to `installed.json`
Ocramius Mar 20, 2019
adb8291
Prefix all autoload paths with their installation path
Ocramius Mar 20, 2019
a10d2ee
Implemented a source locator factory for `installed.json` (only)
Ocramius Mar 20, 2019
e46a99b
Packages (generally) live under `vendor/`, which I forgot to add to p…
Ocramius Mar 20, 2019
1f3e44f
Corrected `SingleFileSourceLocator` instantiation/merging with parent…
Ocramius Mar 20, 2019
2ce44a5
Stub source locator builder for `composer.json` paths *ONLY*
Ocramius Apr 14, 2019
8f83091
Need `beberlei/assert` for these added monstruosity to work
Ocramius Apr 14, 2019
da0a1b3
Clarifying on why a `PsrAutoloaderLocator` has an empty try-catch on …
Ocramius Apr 22, 2019
2420548
Moved composer-locator utilities to a `Factory\` sub-namespace
Ocramius Apr 22, 2019
e11272a
Extensive test coverage and exception specificity for `MakeLocatorFor…
Ocramius Apr 22, 2019
a114863
Added `ext-json` requirement to `composer.json` - needed for `json_de…
Ocramius Apr 22, 2019
da0e599
Extensive tests for `MakeLocatorForComposerJson` factory
Ocramius Apr 22, 2019
0364914
Extensive tests for `MakeLocatorForInstalledJson` factory
Ocramius Apr 22, 2019
e1e50dd
Removing the need for `beberlei/assert` through specific exception types
Ocramius Apr 22, 2019
1d5ed9e
Applied CS checks as per doctrine/cs 6.x
Ocramius Apr 22, 2019
76b0350
Refactored factory code to remove union types in internal parameters
Ocramius Apr 22, 2019
5d91c93
Removing reliance on filesystem sorting in test (only)
Ocramius Apr 22, 2019
3730d70
Usage example: reflecting sources from a `composer.json`-based project
Ocramius Apr 22, 2019
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/.phpcs-cache
/.phpunit.result.cache
/phpcs.xml
/vendor
./vendor
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"license": "MIT",
"require": {
"php": ">=7.2.0,<7.4.0",
"ext-json": "*",
"jetbrains/phpstorm-stubs": "2019.1",
"nikic/php-parser": "^4.0.4",
"phpdocumentor/reflection-docblock": "^4.1.1",
Expand Down
5 changes: 3 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,39 @@ $classInfo = ReflectionClass::createFromName('MyClass');
$functionInfo = ReflectionFunction::createFromName('foo');
```

### Inspecting code and dependencies of a composer-based project

If you need to inspect code from a project that has a `composer.json` and
its associated `vendor/` directory populated, this package offers some
factories that ease the setup of the source locator. These are:

* `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJsonAndInstalledJson` - if
you need to inspect project and dependencies
* `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJson` - if you only want to
inspect project sources
* `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForInstalledJson` - if you only want
to inspect project dependencies

Here's an example of `MakeLocatorForComposerJsonAndInstalledJson` usage:

```php
<?php

use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\ClassReflector;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJsonAndInstalledJson;

$astLocator = (new BetterReflection())->astLocator();
$reflector = new ClassReflector(new AggregateSourceLocator([
(new MakeLocatorForComposerJsonAndInstalledJson)('path/to/the/project', $astLocator),
new PhpInternalSourceLocator($astLocator)
]));

$classes = $reflector->getAllClasses();
```

### Using the Composer autoloader directly

```php
Expand Down
11 changes: 11 additions & 0 deletions src/SourceLocator/Type/Composer/Factory/Exception/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception;

use Throwable;

interface Exception extends Throwable
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception;

use UnexpectedValueException;
use function sprintf;

final class FailedToParseJson extends UnexpectedValueException implements Exception
{
public static function inFile(string $file) : self
{
return new self(sprintf(
'Could not parse JSON file "%s"',
$file
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception;

use InvalidArgumentException;
use function sprintf;

final class InvalidProjectDirectory extends InvalidArgumentException implements Exception
{
public static function atPath(string $path) : self
{
return new self(sprintf(
'Could not locate project directory "%s"',
$path
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception;

use UnexpectedValueException;
use function sprintf;

final class MissingComposerJson extends UnexpectedValueException implements Exception
{
public static function inProjectPath(string $path) : self
{
return new self(sprintf(
'Could not locate a "composer.json" file in "%s"',
$path
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception;

use UnexpectedValueException;
use function sprintf;

final class MissingInstalledJson extends UnexpectedValueException implements Exception
{
public static function inProjectPath(string $path) : self
{
return new self(sprintf(
'Could not locate a "vendor/composer/installed.json" file in "%s"',
$path
));
}
}
145 changes: 145 additions & 0 deletions src/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type\Composer\Factory;

use Roave\BetterReflection\SourceLocator\Ast\Locator;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\FailedToParseJson;
use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\InvalidProjectDirectory;
use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\MissingComposerJson;
use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping;
use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping;
use Roave\BetterReflection\SourceLocator\Type\Composer\PsrAutoloaderLocator;
use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\SourceLocator;
use function array_filter;
use function array_map;
use function array_merge;
use function file_exists;
use function file_get_contents;
use function is_array;
use function is_dir;
use function json_decode;
use function realpath;

final class MakeLocatorForComposerJson
{
public function __invoke(string $installationPath, Locator $astLocator) : SourceLocator
{
$realInstallationPath = (string) realpath($installationPath);

if (! is_dir($realInstallationPath)) {
throw InvalidProjectDirectory::atPath($installationPath);
}

$composerJsonPath = $realInstallationPath . '/composer.json';

if (! file_exists($composerJsonPath)) {
throw MissingComposerJson::inProjectPath($installationPath);
}

$composer = json_decode((string) file_get_contents($composerJsonPath), true);

if (! is_array($composer)) {
throw FailedToParseJson::inFile($composerJsonPath);
}

$pathPrefix = $realInstallationPath . '/';
$classMapPaths = $this->prefixPaths($this->packageToClassMapPaths($composer), $pathPrefix);
$classMapFiles = array_filter($classMapPaths, 'is_file');
$classMapDirectories = array_filter($classMapPaths, 'is_dir');
$filePaths = $this->prefixPaths($this->packageToFilePaths($composer), $pathPrefix);

return new AggregateSourceLocator(array_merge(
[
new PsrAutoloaderLocator(
Psr4Mapping::fromArrayMappings(
$this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $pathPrefix)
),
$astLocator
),
new PsrAutoloaderLocator(
Psr0Mapping::fromArrayMappings(
$this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $pathPrefix)
),
$astLocator
),
new DirectoriesSourceLocator($classMapDirectories, $astLocator),
],
...array_map(static function (string $file) use ($astLocator) : array {
return [new SingleFileSourceLocator($file, $astLocator)];
}, array_merge($classMapFiles, $filePaths))
));
}

/**
* @param mixed[] $package
*
* @return array<string, array<int, string>>
*/
private function packageToPsr4AutoloadNamespaces(array $package) : array
{
return array_map(static function ($namespacePaths) : array {
return (array) $namespacePaths;
}, $package['autoload']['psr-4'] ?? []);
}

/**
* @param mixed[] $package
*
* @return array<string, array<int, string>>
*/
private function packageToPsr0AutoloadNamespaces(array $package) : array
{
return array_map(static function ($namespacePaths) : array {
return (array) $namespacePaths;
}, $package['autoload']['psr-0'] ?? []);
}

/**
* @param mixed[] $package
*
* @return array<int, string>
*/
private function packageToClassMapPaths(array $package) : array
{
return $package['autoload']['classmap'] ?? [];
}

/**
* @param mixed[] $package
*
* @return array<int, string>
*/
private function packageToFilePaths(array $package) : array
{
return $package['autoload']['files'] ?? [];
}

/**
* @param array<string, array<int, string>> $paths
*
* @return array<string, array<int, string>>
*/
private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath) : array
{
return array_map(function (array $paths) use ($trimmedInstallationPath) : array {
return $this->prefixPaths($paths, $trimmedInstallationPath);
}, $paths);
}

/**
* @param array<int, string> $paths
*
* @return array<int, string>
*/
private function prefixPaths(array $paths, string $prefix) : array
{
return array_map(static function (string $path) use ($prefix) {
return $prefix . $path;
}, $paths);
}
}
Loading