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

[2.x] Support vendor class aliasing #88

Merged
merged 2 commits into from
Feb 5, 2020
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
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"symfony/var-dumper": "^4.0|^5.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0"
"mockery/mockery": "^1.3.1",
"phpunit/phpunit": "^8.0|^9.0"
},
"suggest": {
"illuminate/database": "The Illuminate Database package (^6.0|^7.0)."
Expand All @@ -30,7 +31,9 @@
},
"autoload-dev": {
"psr-4": {
"Laravel\\Tinker\\Tests\\": "tests/"
"Laravel\\Tinker\\Tests\\": "tests/",
"App\\": "tests/fixtures/app",
"One\\Two\\": "tests/fixtures/vendor/one/two"
}
},
"extra": {
Expand Down
15 changes: 15 additions & 0 deletions config/tinker.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,19 @@
'App\Nova',
],

/*
|--------------------------------------------------------------------------
| Alias Whitelist
|--------------------------------------------------------------------------
|
| Tinker will not automatically alias classes in your vendor namespaces.
| However, you may wish to allow this for certain classes, which you
| may accomplish by listing the classes in the following array.
|
*/

'alias' => [
//
],

];
81 changes: 64 additions & 17 deletions src/ClassAliasAutoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,39 @@ class ClassAliasAutoloader
*/
protected $classes = [];

/**
* Path to the vendor directory.
*
* @var string
*/
protected $vendorPath;

/**
* Explicitly included namespaces/classes.
*
* @var \Illuminate\Support\Collection
*/
protected $includedAliases;

/**
* Excluded namespaces/classes.
*
* @var \Illuminate\Support\Collection
*/
protected $excludedAliases;

/**
* Register a new alias loader instance.
*
* @param \Psy\Shell $shell
* @param string $classMapPath
* @param array $includedAliases
* @param array $excludedAliases
* @return static
*/
public static function register(Shell $shell, $classMapPath)
public static function register(Shell $shell, $classMapPath, array $includedAliases = [], array $excludedAliases = [])
{
return tap(new static($shell, $classMapPath), function ($loader) {
return tap(new static($shell, $classMapPath, $includedAliases, $excludedAliases), function ($loader) {
spl_autoload_register([$loader, 'aliasClass']);
});
}
Expand All @@ -40,26 +63,21 @@ public static function register(Shell $shell, $classMapPath)
*
* @param \Psy\Shell $shell
* @param string $classMapPath
* @param array $includedAliases
* @param array $excludedAliases
* @return void
*/
public function __construct(Shell $shell, $classMapPath)
public function __construct(Shell $shell, $classMapPath, array $includedAliases = [], array $excludedAliases = [])
{
$this->shell = $shell;

$vendorPath = dirname(dirname($classMapPath));
$this->vendorPath = dirname(dirname($classMapPath));
$this->includedAliases = collect($includedAliases);
$this->excludedAliases = collect($excludedAliases);

$classes = require $classMapPath;

$excludedAliases = collect(config('tinker.dont_alias', []));

foreach ($classes as $class => $path) {
if (! Str::contains($class, '\\') || Str::startsWith($path, $vendorPath)) {
continue;
}

if (! $excludedAliases->filter(function ($alias) use ($class) {
return Str::startsWith($class, $alias);
})->isEmpty()) {
if (! $this->isAliasable($class, $path)) {
continue;
}

Expand All @@ -83,9 +101,7 @@ public function aliasClass($class)
return;
}

$fullName = isset($this->classes[$class])
? $this->classes[$class]
: false;
$fullName = $this->classes[$class] ?? false;

if ($fullName) {
$this->shell->writeStdout("[!] Aliasing '{$class}' to '{$fullName}' for this Tinker session.\n");
Expand Down Expand Up @@ -113,4 +129,35 @@ public function __destruct()
{
$this->unregister();
}

/**
* Whether a class may be aliased.
*
* @param string $class
* @param string $path
*/
public function isAliasable($class, $path)
{
if (! Str::contains($class, '\\')) {
return false;
}

if (! $this->includedAliases->filter(function ($alias) use ($class) {
return Str::startsWith($class, $alias);
})->isEmpty()) {
return true;
}

if (Str::startsWith($path, $this->vendorPath)) {
return false;
}

if (! $this->excludedAliases->filter(function ($alias) use ($class) {
return Str::startsWith($class, $alias);
})->isEmpty()) {
return false;
}

return true;
}
}
10 changes: 8 additions & 2 deletions src/Console/TinkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ public function handle()

$path .= '/composer/autoload_classmap.php';

$loader = ClassAliasAutoloader::register($shell, $path);
$config = $this->getLaravel()->make('config');

$loader = ClassAliasAutoloader::register(
$shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', [])
);

try {
$shell->run();
Expand All @@ -88,7 +92,9 @@ protected function getCommands()
}
}

foreach (config('tinker.commands', []) as $command) {
$config = $this->getLaravel()->make('config');

foreach ($config->get('tinker.commands', []) as $command) {
$commands[] = $this->getApplication()->resolve($command);
}

Expand Down
78 changes: 78 additions & 0 deletions tests/ClassAliasAutoloaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Laravel\Tinker\Tests;

use Laravel\Tinker\ClassAliasAutoloader;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psy\Shell;

class ClassAliasAutoloaderTest extends TestCase
{
public function setUp(): void
{
$this->classmapPath = __DIR__.'/fixtures/vendor/composer/autoload_classmap.php';
}

public function tearDown(): void
{
$this->loader->unregister();
}

public function testCanAliasClasses()
{
$this->loader = ClassAliasAutoloader::register(
$shell = Mockery::mock(Shell::class),
$this->classmapPath
);

$shell->shouldReceive('writeStdout')
->with("[!] Aliasing 'Bar' to 'App\Foo\Bar' for this Tinker session.\n")
->once();

$this->assertTrue(class_exists('Bar'));
$this->assertInstanceOf(\App\Foo\Bar::class, new \Bar);
}

public function testCanExcludeNamespacesFromAliasing()
{
$this->loader = ClassAliasAutoloader::register(
$shell = Mockery::mock(Shell::class),
$this->classmapPath,
[],
['App\Baz']
);

$shell->shouldNotReceive('writeStdout');

$this->assertFalse(class_exists('Qux'));
}

public function testVendorClassesAreExcluded()
{
$this->loader = ClassAliasAutoloader::register(
$shell = Mockery::mock(Shell::class),
$this->classmapPath
);

$shell->shouldNotReceive('writeStdout');

$this->assertFalse(class_exists('Three'));
}

public function testVendorClassesCanBeWhitelisted()
{
$this->loader = ClassAliasAutoloader::register(
$shell = Mockery::mock(Shell::class),
$this->classmapPath,
['One\Two']
);

$shell->shouldReceive('writeStdout')
->with("[!] Aliasing 'Three' to 'One\Two\Three' for this Tinker session.\n")
->once();

$this->assertTrue(class_exists('Three'));
$this->assertInstanceOf(\One\Two\Three::class, new \Three);
}
}
8 changes: 8 additions & 0 deletions tests/fixtures/app/Baz/Qux.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace App\Baz;

class Qux
{
//
}
8 changes: 8 additions & 0 deletions tests/fixtures/app/Foo/Bar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace App\Foo;

class Bar
{
//
}
9 changes: 9 additions & 0 deletions tests/fixtures/vendor/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'App\\Foo\\Bar' => $baseDir.'/app/Foo/Bar.php',
'App\\Baz\\Qux' => $baseDir.'/app/Baz/Qux.php',
'One\\Two\\Three' => $vendorDir.'/one/two/src/Three.php',
'Four\\Five\\Six' => $vendorDir.'/four/five/src/Six.php',
);
8 changes: 8 additions & 0 deletions tests/fixtures/vendor/one/two/Three.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace One\Two;

class Three
{
//
}