Skip to content

Commit 58b57ee

Browse files
peterfoxGeniJaho
andauthored
Livewire set and first rule for QueryString to Url attributes (#222)
* 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 <[email protected]>
1 parent 5cafba7 commit 58b57ee

17 files changed

+540
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ See available [Laravel rules](/docs/rector_rules_overview.md)
1010

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

13+
Rules for additional first party packages are included as well e.g. Cashier and Livewire.
14+
1315
Install the `RectorLaravel` package as dependency:
1416

1517
```bash

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Rector upgrades rules for Laravel Framework",
66
"require": {
77
"php": ">=8.2",
8-
"rector/rector": "^1.0"
8+
"rector/rector": "^1.2"
99
},
1010
"require-dev": {
1111
"nikic/php-parser": "^4.18",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use RectorLaravel\Set\Packages\Livewire\LivewireSetList;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->sets([LivewireSetList::LIVEWIRE_30]);
10+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Renaming\Rector\Class_\RenameAttributeRector;
7+
use Rector\Renaming\ValueObject\RenameAttribute;
8+
use RectorLaravel\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector;
9+
10+
return static function (RectorConfig $rectorConfig): void {
11+
$rectorConfig->import(__DIR__ . '/../../../config.php');
12+
13+
$rectorConfig->rule(LivewireComponentQueryStringToUrlAttributeRector::class);
14+
15+
$rectorConfig->ruleWithConfiguration(RenameAttributeRector::class, [
16+
new RenameAttribute('Livewire\Attributes\Rule', 'Livewire\Attributes\Validate'),
17+
]);
18+
};

docs/rector_rules_overview.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 61 Rules Overview
1+
# 62 Rules Overview
22

33
## AbortIfRector
44

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

676676
<br>
677677

678+
## LivewireComponentQueryStringToUrlAttributeRector
679+
680+
Converts the `$queryString` property of a Livewire component to use the Url Attribute
681+
682+
- class: [`RectorLaravel\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector`](../src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php)
683+
684+
```diff
685+
use Livewire\Component;
686+
687+
class MyComponent extends Component
688+
{
689+
+ #[\Livewire\Attributes\Url]
690+
public string $something = '';
691+
692+
+ #[\Livewire\Attributes\Url]
693+
public string $another = '';
694+
-
695+
- protected $queryString = [
696+
- 'something',
697+
- 'another',
698+
- ];
699+
}
700+
```
701+
702+
<br>
703+
678704
## LumenRoutesStringActionToUsesArrayRector
679705

680706
Changes action in rule definitions from string to array notation.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
3+
namespace RectorLaravel\Rector\Class_;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Attribute;
8+
use PhpParser\Node\AttributeGroup;
9+
use PhpParser\Node\Expr\Array_;
10+
use PhpParser\Node\Expr\ArrayItem;
11+
use PhpParser\Node\Identifier;
12+
use PhpParser\Node\Name\FullyQualified;
13+
use PhpParser\Node\Scalar;
14+
use PhpParser\Node\Scalar\String_;
15+
use PhpParser\Node\Stmt\Class_;
16+
use PhpParser\Node\Stmt\Property;
17+
use PHPStan\Type\ObjectType;
18+
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
19+
use Rector\Rector\AbstractRector;
20+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
21+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
22+
23+
/**
24+
* @see RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\LivewireComponentQueryStringToUrlAttributeRectorTest
25+
*/
26+
final class LivewireComponentQueryStringToUrlAttributeRector extends AbstractRector
27+
{
28+
private const URL_ATTRIBUTE = 'Livewire\Attributes\Url';
29+
30+
private const COMPONENT_CLASS = 'Livewire\Component';
31+
32+
private const QUERY_STRING_PROPERTY_NAME = 'queryString';
33+
34+
public function __construct(private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer)
35+
{
36+
37+
}
38+
39+
public function getRuleDefinition(): RuleDefinition
40+
{
41+
return new RuleDefinition(
42+
'Converts the $queryString property of a Livewire component to use the Url Attribute',
43+
[
44+
new CodeSample(<<<'CODE_SAMPLE'
45+
use Livewire\Component;
46+
47+
class MyComponent extends Component
48+
{
49+
public string $something = '';
50+
51+
public string $another = '';
52+
53+
protected $queryString = [
54+
'something',
55+
'another',
56+
];
57+
}
58+
CODE_SAMPLE,
59+
<<<'CODE_SAMPLE'
60+
use Livewire\Component;
61+
62+
class MyComponent extends Component
63+
{
64+
#[\Livewire\Attributes\Url]
65+
public string $something = '';
66+
67+
#[\Livewire\Attributes\Url]
68+
public string $another = '';
69+
}
70+
CODE_SAMPLE
71+
),
72+
]
73+
);
74+
}
75+
76+
public function getNodeTypes(): array
77+
{
78+
return [Class_::class];
79+
}
80+
81+
/**
82+
* @param Class_ $node
83+
*/
84+
public function refactor(Node $node): ?Class_
85+
{
86+
if (! $this->isObjectType($node, new ObjectType(self::COMPONENT_CLASS))) {
87+
return null;
88+
}
89+
90+
$queryStringProperty = null;
91+
92+
foreach ($node->stmts as $stmt) {
93+
if ($stmt instanceof Property && $this->isName($stmt, self::QUERY_STRING_PROPERTY_NAME)) {
94+
$queryStringProperty = $stmt;
95+
}
96+
}
97+
98+
if (! $queryStringProperty instanceof Property) {
99+
return null;
100+
}
101+
102+
// find the properties and add the attribute
103+
$urlPropertyNames = $this->findQueryStringProperties($queryStringProperty);
104+
105+
if ($urlPropertyNames === []) {
106+
return null;
107+
}
108+
109+
$propertyNodes = [];
110+
111+
foreach ($node->stmts as $stmt) {
112+
if ($stmt instanceof Property && $this->isNames($stmt, array_keys((array) $urlPropertyNames))) {
113+
$propertyNodes[] = $stmt;
114+
}
115+
}
116+
117+
foreach ($propertyNodes as $propertyNode) {
118+
$args = $urlPropertyNames[$this->getName($propertyNode)] ?? [];
119+
$this->addUrlAttributeToProperty($propertyNode, $args);
120+
}
121+
122+
// remove the query string property if now empty
123+
$this->attemptQueryStringRemoval($node, $queryStringProperty);
124+
125+
return $node;
126+
}
127+
128+
/**
129+
* @return array<string, Node\Arg[]>|null
130+
*/
131+
private function findQueryStringProperties(Property $property): ?array
132+
{
133+
if ($property->props === []) {
134+
return null;
135+
}
136+
137+
$array = $property->props[0]->default;
138+
139+
if (! $array instanceof Array_ || $array->items === []) {
140+
return null;
141+
}
142+
143+
$properties = [];
144+
$toFilter = [];
145+
146+
foreach ($array->items as $item) {
147+
if ($item === null) {
148+
continue;
149+
}
150+
151+
if ($item->key instanceof String_ && $item->value instanceof Array_) {
152+
$args = $this->processArrayOptionsIntoArgs($item->value);
153+
154+
if ($args === null) {
155+
continue;
156+
}
157+
158+
$properties[$item->key->value] = $args;
159+
$toFilter[] = $item;
160+
161+
continue;
162+
}
163+
164+
if ($item->value instanceof String_) {
165+
$properties[$item->value->value] = [];
166+
$toFilter[] = $item;
167+
}
168+
}
169+
170+
if ($properties === []) {
171+
return null;
172+
}
173+
174+
// we remove the array properties which will be converted
175+
$array->items = array_filter(
176+
$array->items,
177+
fn (?ArrayItem $arrayItem): bool => ! in_array($arrayItem, $toFilter, true),
178+
);
179+
180+
return $properties;
181+
}
182+
183+
/**
184+
* @param Node\Arg[] $args
185+
*/
186+
private function addUrlAttributeToProperty(Property $property, array $args): void
187+
{
188+
if ($this->phpAttributeAnalyzer->hasPhpAttribute($property, self::URL_ATTRIBUTE)) {
189+
return;
190+
}
191+
192+
$property->attrGroups[] = new AttributeGroup([
193+
new Attribute(
194+
new FullyQualified(self::URL_ATTRIBUTE), args: $args
195+
),
196+
]);
197+
}
198+
199+
/**
200+
* @return Node\Arg[]|null
201+
*/
202+
private function processArrayOptionsIntoArgs(Array_ $array): ?array
203+
{
204+
$args = [];
205+
206+
foreach ($array->items as $item) {
207+
if ($item === null) {
208+
continue;
209+
}
210+
if ($item->key instanceof String_ && $item->value instanceof Scalar && in_array($item->key->value, ['except', 'as'], true)) {
211+
$args[] = new Arg($item->value, name: new Identifier($item->key->value));
212+
}
213+
}
214+
215+
if (count($args) !== count($array->items)) {
216+
return null;
217+
}
218+
219+
return $args;
220+
}
221+
222+
private function attemptQueryStringRemoval(Class_ $class, Property $property): void
223+
{
224+
$array = $property->props[0]->default;
225+
226+
if ($array instanceof Array_ && $array->items === []) {
227+
$class->stmts = array_filter($class->stmts, fn (Node $node) => $node !== $property);
228+
}
229+
}
230+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Set\Packages\Livewire;
6+
7+
use Rector\Set\Contract\SetListInterface;
8+
9+
final class LivewireLevelSetList implements SetListInterface
10+
{
11+
/**
12+
* @var string
13+
*/
14+
final public const UP_TO_LIVEWIRE = __DIR__ . '/../../../../config/sets/packages/livewire/level/up-to-livewire-30.php';
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace RectorLaravel\Set\Packages\Livewire;
4+
5+
final class LivewireSetList
6+
{
7+
/**
8+
* @var string
9+
*/
10+
final public const LIVEWIRE_30 = __DIR__ . '/../../../../config/sets/packages/livewire/livewire-30.php';
11+
}

stubs/Livewire/Component.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Livewire;
4+
5+
if (class_exists('Livewire\Component')) {
6+
return;
7+
}
8+
9+
class Component
10+
{
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\Fixture;
4+
5+
use Livewire\Component;
6+
7+
class DoesNotDoubleApplyAttributes extends Component
8+
{
9+
#[\Livewire\Attributes\Url]
10+
public string $something = '';
11+
12+
public string $another = '';
13+
14+
protected $queryString = [
15+
'something',
16+
'another',
17+
];
18+
}
19+
20+
?>
21+
-----
22+
<?php
23+
24+
namespace RectorLaravel\Tests\Rector\Class_\LivewireComponentQueryStringToUrlAttributeRector\Fixture;
25+
26+
use Livewire\Component;
27+
28+
class DoesNotDoubleApplyAttributes extends Component
29+
{
30+
#[\Livewire\Attributes\Url]
31+
public string $something = '';
32+
33+
#[\Livewire\Attributes\Url]
34+
public string $another = '';
35+
}
36+
37+
?>

0 commit comments

Comments
 (0)