Skip to content

Commit

Permalink
Livewire set and first rule for QueryString to Url attributes (#222)
Browse files Browse the repository at this point in the history
* Working

* Rector and Lint Fixes

* Configure package sets

* Grammar fix

* Docs generated

* Fixes some typos

* Handles keys

* linting

* Upgrade Rector

* Working

* Clean up

* Adds an additional rule to the Livewire30 config

---------

Co-authored-by: Geni Jaho <jahogeni@gmail.com>
  • Loading branch information
peterfox and GeniJaho authored Jul 18, 2024
1 parent 5cafba7 commit 58b57ee
Show file tree
Hide file tree
Showing 17 changed files with 540 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ See available [Laravel rules](/docs/rector_rules_overview.md)

This package is a [Rector](https://github.com/rectorphp/rector) extension developed by the Laravel community.

Rules for additional first party packages are included as well e.g. Cashier and Livewire.

Install the `RectorLaravel` package as dependency:

```bash
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Rector upgrades rules for Laravel Framework",
"require": {
"php": ">=8.2",
"rector/rector": "^1.0"
"rector/rector": "^1.2"
},
"require-dev": {
"nikic/php-parser": "^4.18",
Expand Down
10 changes: 10 additions & 0 deletions config/sets/packages/livewire/level/up-to-livewire-30.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use RectorLaravel\Set\Packages\Livewire\LivewireSetList;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([LivewireSetList::LIVEWIRE_30]);
};
18 changes: 18 additions & 0 deletions config/sets/packages/livewire/livewire-30.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\Class_\RenameAttributeRector;
use Rector\Renaming\ValueObject\RenameAttribute;
use RectorLaravel\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector;

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

$rectorConfig->rule(LivewireComponentQueryStringToUrlAttributeRector::class);

$rectorConfig->ruleWithConfiguration(RenameAttributeRector::class, [
new RenameAttribute('Livewire\Attributes\Rule', 'Livewire\Attributes\Validate'),
]);
};
28 changes: 27 additions & 1 deletion docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 61 Rules Overview
# 62 Rules Overview

## AbortIfRector

Expand Down Expand Up @@ -675,6 +675,32 @@ Change method calls from `$this->json` to `$this->postJson,` `$this->putJson,` e

<br>

## LivewireComponentQueryStringToUrlAttributeRector

Converts the `$queryString` property of a Livewire component to use the Url Attribute

- class: [`RectorLaravel\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector`](../src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php)

```diff
use Livewire\Component;

class MyComponent extends Component
{
+ #[\Livewire\Attributes\Url]
public string $something = '';

+ #[\Livewire\Attributes\Url]
public string $another = '';
-
- protected $queryString = [
- 'something',
- 'another',
- ];
}
```

<br>

## LumenRoutesStringActionToUsesArrayRector

Changes action in rule definitions from string to array notation.
Expand Down
230 changes: 230 additions & 0 deletions src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php

namespace RectorLaravel\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\Type\ObjectType;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\LivewireComponentQueryStringToUrlAttributeRectorTest
*/
final class LivewireComponentQueryStringToUrlAttributeRector extends AbstractRector
{
private const URL_ATTRIBUTE = 'Livewire\Attributes\Url';

private const COMPONENT_CLASS = 'Livewire\Component';

private const QUERY_STRING_PROPERTY_NAME = 'queryString';

public function __construct(private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer)
{

}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Converts the $queryString property of a Livewire component to use the Url Attribute',
[
new CodeSample(<<<'CODE_SAMPLE'
use Livewire\Component;
class MyComponent extends Component
{
public string $something = '';
public string $another = '';
protected $queryString = [
'something',
'another',
];
}
CODE_SAMPLE,
<<<'CODE_SAMPLE'
use Livewire\Component;
class MyComponent extends Component
{
#[\Livewire\Attributes\Url]
public string $something = '';
#[\Livewire\Attributes\Url]
public string $another = '';
}
CODE_SAMPLE
),
]
);
}

public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Class_
{
if (! $this->isObjectType($node, new ObjectType(self::COMPONENT_CLASS))) {
return null;
}

$queryStringProperty = null;

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Property && $this->isName($stmt, self::QUERY_STRING_PROPERTY_NAME)) {
$queryStringProperty = $stmt;
}
}

if (! $queryStringProperty instanceof Property) {
return null;
}

// find the properties and add the attribute
$urlPropertyNames = $this->findQueryStringProperties($queryStringProperty);

if ($urlPropertyNames === []) {
return null;
}

$propertyNodes = [];

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Property && $this->isNames($stmt, array_keys((array) $urlPropertyNames))) {
$propertyNodes[] = $stmt;
}
}

foreach ($propertyNodes as $propertyNode) {
$args = $urlPropertyNames[$this->getName($propertyNode)] ?? [];
$this->addUrlAttributeToProperty($propertyNode, $args);
}

// remove the query string property if now empty
$this->attemptQueryStringRemoval($node, $queryStringProperty);

return $node;
}

/**
* @return array<string, Node\Arg[]>|null
*/
private function findQueryStringProperties(Property $property): ?array
{
if ($property->props === []) {
return null;
}

$array = $property->props[0]->default;

if (! $array instanceof Array_ || $array->items === []) {
return null;
}

$properties = [];
$toFilter = [];

foreach ($array->items as $item) {
if ($item === null) {
continue;
}

if ($item->key instanceof String_ && $item->value instanceof Array_) {
$args = $this->processArrayOptionsIntoArgs($item->value);

if ($args === null) {
continue;
}

$properties[$item->key->value] = $args;
$toFilter[] = $item;

continue;
}

if ($item->value instanceof String_) {
$properties[$item->value->value] = [];
$toFilter[] = $item;
}
}

if ($properties === []) {
return null;
}

// we remove the array properties which will be converted
$array->items = array_filter(
$array->items,
fn (?ArrayItem $arrayItem): bool => ! in_array($arrayItem, $toFilter, true),
);

return $properties;
}

/**
* @param Node\Arg[] $args
*/
private function addUrlAttributeToProperty(Property $property, array $args): void
{
if ($this->phpAttributeAnalyzer->hasPhpAttribute($property, self::URL_ATTRIBUTE)) {
return;
}

$property->attrGroups[] = new AttributeGroup([
new Attribute(
new FullyQualified(self::URL_ATTRIBUTE), args: $args
),
]);
}

/**
* @return Node\Arg[]|null
*/
private function processArrayOptionsIntoArgs(Array_ $array): ?array
{
$args = [];

foreach ($array->items as $item) {
if ($item === null) {
continue;
}
if ($item->key instanceof String_ && $item->value instanceof Scalar && in_array($item->key->value, ['except', 'as'], true)) {
$args[] = new Arg($item->value, name: new Identifier($item->key->value));
}
}

if (count($args) !== count($array->items)) {
return null;
}

return $args;
}

private function attemptQueryStringRemoval(Class_ $class, Property $property): void
{
$array = $property->props[0]->default;

if ($array instanceof Array_ && $array->items === []) {
$class->stmts = array_filter($class->stmts, fn (Node $node) => $node !== $property);
}
}
}
15 changes: 15 additions & 0 deletions src/Set/Packages/Livewire/LivewireLevelSetList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Set\Packages\Livewire;

use Rector\Set\Contract\SetListInterface;

final class LivewireLevelSetList implements SetListInterface
{
/**
* @var string
*/
final public const UP_TO_LIVEWIRE = __DIR__ . '/../../../../config/sets/packages/livewire/level/up-to-livewire-30.php';
}
11 changes: 11 additions & 0 deletions src/Set/Packages/Livewire/LivewireSetList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace RectorLaravel\Set\Packages\Livewire;

final class LivewireSetList
{
/**
* @var string
*/
final public const LIVEWIRE_30 = __DIR__ . '/../../../../config/sets/packages/livewire/livewire-30.php';
}
11 changes: 11 additions & 0 deletions stubs/Livewire/Component.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Livewire;

if (class_exists('Livewire\Component')) {
return;
}

class Component
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\Fixture;

use Livewire\Component;

class DoesNotDoubleApplyAttributes extends Component
{
#[\Livewire\Attributes\Url]
public string $something = '';

public string $another = '';

protected $queryString = [
'something',
'another',
];
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\Fixture;

use Livewire\Component;

class DoesNotDoubleApplyAttributes extends Component
{
#[\Livewire\Attributes\Url]
public string $something = '';

#[\Livewire\Attributes\Url]
public string $another = '';
}

?>
Loading

0 comments on commit 58b57ee

Please sign in to comment.