Skip to content

Commit

Permalink
Adds ReplaceServiceContainerCallArgRector (#189)
Browse files Browse the repository at this point in the history
* Adds ReplaceServiceContainerCallArgRector

* CS Fix
  • Loading branch information
peterfox authored Feb 29, 2024
1 parent bf0cd91 commit d6da062
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 1 deletion.
19 changes: 18 additions & 1 deletion docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 56 Rules Overview
# 57 Rules Overview

## AddArgumentDefaultValueRector

Expand Down Expand Up @@ -1015,6 +1015,23 @@ Replace `$this->faker` with the `fake()` helper function in Factories

<br>

## ReplaceServiceContainerCallArgRector

Changes the string or class const used for a service container make call

:wrench: **configure it!**

- class: [`RectorLaravel\Rector\MethodCall\ReplaceServiceContainerCallArgRector`](../src/Rector/MethodCall/ReplaceServiceContainerCallArgRector.php)

```diff
-app('encrypter')->encrypt('...');
-\Illuminate\Support\Facades\Application::make('encrypter')->encrypt('...');
+app(Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('...');
+\Illuminate\Support\Facades\Application::make(Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('...');
```

<br>

## ReplaceWithoutJobsEventsAndNotificationsWithFacadeFakeRector

Replace `withoutJobs`, `withoutEvents` and `withoutNotifications` with Facade `fake`
Expand Down
156 changes: 156 additions & 0 deletions src/Rector/MethodCall/ReplaceServiceContainerCallArgRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace RectorLaravel\Rector\MethodCall;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Type\ObjectType;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use Rector\Rector\AbstractRector;
use RectorLaravel\ValueObject\ReplaceServiceContainerCallArg;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

/**
* @see \RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\ReplaceServiceContainerCallArgRectorTest
*/
class ReplaceServiceContainerCallArgRector extends AbstractRector implements ConfigurableRectorInterface
{
/**
* @var ReplaceServiceContainerCallArg[]
*/
private array $replaceServiceContainerCallArgs = [];

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Changes the string or class const used for a service container make call',
[new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
app('encrypter')->encrypt('...');
\Illuminate\Support\Facades\Application::make('encrypter')->encrypt('...');
CODE_SAMPLE,
<<<'CODE_SAMPLE'
app(Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('...');
\Illuminate\Support\Facades\Application::make(Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('...');
CODE_SAMPLE,
[
new ReplaceServiceContainerCallArg(
'encrypter',
new ClassConstFetch(
new Name('Illuminate\Contracts\Encryption\Encrypter'),
'class'
),
),
]
)]
);
}

public function getNodeTypes(): array
{
return [MethodCall::class, StaticCall::class, FuncCall::class];
}

/**
* @param MethodCall|StaticCall|FuncCall $node
*/
public function refactor(Node $node): MethodCall|StaticCall|FuncCall|null
{
if (! $this->validMethodCall($node) &&
! $this->validFuncCall($node)) {
return null;
}

if ($node->args === [] || ! $node->args[0] instanceof Arg) {
return null;
}

$hasChanged = false;

foreach ($this->replaceServiceContainerCallArgs as $replaceServiceContainerCallArg) {
if ($this->isMatchForChangeServiceContainerCallArgValue($node->args[0], $replaceServiceContainerCallArg->getOldService())) {
$this->replaceCallArgValue($node->args[0], $replaceServiceContainerCallArg->getNewService());
$hasChanged = true;
}
}

return $hasChanged ? $node : null;
}

public function configure(array $configuration): void
{
Assert::allIsInstanceOf($configuration, ReplaceServiceContainerCallArg::class);

$this->replaceServiceContainerCallArgs = $configuration;
}

private function isMatchForChangeServiceContainerCallArgValue(Arg $arg, ClassConstFetch|string $oldService): bool
{
if ($arg->value instanceof ClassConstFetch && $oldService instanceof ClassConstFetch) {
if ($arg->value->class instanceof Expr || $oldService->class instanceof Expr) {
return false;
}

return $arg->value->class->toString() === $oldService->class->toString();
} elseif ($arg->value instanceof String_) {
return $arg->value->value === $oldService;
}

return false;
}

private function replaceCallArgValue(Arg $arg, ClassConstFetch|string $newService): void
{
if ($newService instanceof ClassConstFetch) {
$arg->value = $newService;

return;
}

$arg->value = new String_($newService);
}

private function validMethodCall(StaticCall|MethodCall|FuncCall $node): bool
{
if (! $node instanceof MethodCall && ! $node instanceof StaticCall) {
return false;
}

if (! $node->name instanceof Identifier) {
return false;
}

if (! $this->isNames($node->name, ['make', 'get'])) {
return false;
}

[$callObject, $class] = match (true) {
$node instanceof MethodCall => [$node->var, 'Illuminate\Contracts\Container\Container'],
$node instanceof StaticCall => [$node->class, 'Illuminate\Support\Facades\Application'],
};

return $this->isObjectType($callObject, new ObjectType($class));
}

private function validFuncCall(StaticCall|MethodCall|FuncCall $node): bool
{
if (! $node instanceof FuncCall) {
return false;
}

if (! $node->name instanceof Name) {
return false;
}

return $this->isName($node->name, 'app');
}
}
24 changes: 24 additions & 0 deletions src/ValueObject/ReplaceServiceContainerCallArg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace RectorLaravel\ValueObject;

use PhpParser\Node\Expr\ClassConstFetch;

final readonly class ReplaceServiceContainerCallArg
{
public function __construct(
private string|ClassConstFetch $oldService,
private string|ClassConstFetch $newService
) {
}

public function getOldService(): string|ClassConstFetch
{
return $this->oldService;
}

public function getNewService(): string|ClassConstFetch
{
return $this->newService;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\Fixture;

function foo(\Illuminate\Contracts\Container\Container $app) {
$app->make('encrypter')->encrypt('hello world');
}

\Illuminate\Support\Facades\Application::make('encrypter')->encrypt('hello world');

app('encrypter')->encrypt('hello world');

function foo(\Illuminate\Contracts\Container\Container $app) {
$app->make(\Illuminate\Contracts\Session\Session::class)->get('hello world');
}

\Illuminate\Support\Facades\Application::make(\Illuminate\Contracts\Session\Session::class)->get('hello world');

app(\Illuminate\Contracts\Session\Session::class)->get('hello world');

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\Fixture;

function foo(\Illuminate\Contracts\Container\Container $app) {
$app->make(\Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('hello world');
}

\Illuminate\Support\Facades\Application::make(\Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('hello world');

app(\Illuminate\Contracts\Encryption\Encrypter::class)->encrypt('hello world');

function foo(\Illuminate\Contracts\Container\Container $app) {
$app->make('session')->get('hello world');
}

\Illuminate\Support\Facades\Application::make('session')->get('hello world');

app('session')->get('hello world');

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\Fixture;

function foo(\Illuminate\Contracts\Container\Container $app) {
$app->make('foobar')->encrypt('hello world');
}

\Illuminate\Support\Facades\Application::make('foobar')->encrypt('hello world');

app('foobar')->encrypt('foobar');

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\Fixture;

function foo ($app) {
$app->make('encrypter')->encrypt('hello world');
}

Application::make('encrypter')->encrypt('hello world');

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector\Fixture;

function foo (\Illuminate\Contracts\Container\Container $app) {
$app->build('encrypter')->encrypt('hello world');
}

\Illuminate\Support\Facades\Application::build('encrypter')->encrypt('hello world');

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\MethodCall\ReplaceServiceContainerCallArgRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class ReplaceServiceContainerCallArgRectorTest extends AbstractRectorTestCase
{
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

/**
* @test
*/
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name\FullyQualified;
use Rector\Config\RectorConfig;
use RectorLaravel\Rector\MethodCall\ReplaceServiceContainerCallArgRector;
use RectorLaravel\ValueObject\ReplaceServiceContainerCallArg;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');

$rectorConfig->ruleWithConfiguration(
ReplaceServiceContainerCallArgRector::class,
[
new ReplaceServiceContainerCallArg(
'encrypter',
new ClassConstFetch(
new FullyQualified('Illuminate\Contracts\Encryption\Encrypter'),
'class'
),
),
new ReplaceServiceContainerCallArg(
new ClassConstFetch(
new FullyQualified('Illuminate\Contracts\Session\Session'),
'class'
),
'session',
),
]
);
};

0 comments on commit d6da062

Please sign in to comment.