diff --git a/README.md b/README.md index 02157503..20350548 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/composer.json b/composer.json index 2fa4fbe4..e3071c8c 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/config/sets/packages/livewire/level/up-to-livewire-30.php b/config/sets/packages/livewire/level/up-to-livewire-30.php new file mode 100644 index 00000000..b3febbd6 --- /dev/null +++ b/config/sets/packages/livewire/level/up-to-livewire-30.php @@ -0,0 +1,10 @@ +sets([LivewireSetList::LIVEWIRE_30]); +}; diff --git a/config/sets/packages/livewire/livewire-30.php b/config/sets/packages/livewire/livewire-30.php new file mode 100644 index 00000000..174e6952 --- /dev/null +++ b/config/sets/packages/livewire/livewire-30.php @@ -0,0 +1,18 @@ +import(__DIR__ . '/../../../config.php'); + + $rectorConfig->rule(LivewireComponentQueryStringToUrlAttributeRector::class); + + $rectorConfig->ruleWithConfiguration(RenameAttributeRector::class, [ + new RenameAttribute('Livewire\Attributes\Rule', 'Livewire\Attributes\Validate'), + ]); +}; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 3452bc27..a2180aa8 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 61 Rules Overview +# 62 Rules Overview ## AbortIfRector @@ -675,6 +675,32 @@ Change method calls from `$this->json` to `$this->postJson,` `$this->putJson,` e
+## 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', +- ]; + } +``` + +
+ ## LumenRoutesStringActionToUsesArrayRector Changes action in rule definitions from string to array notation. diff --git a/src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php b/src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php new file mode 100644 index 00000000..03f9a786 --- /dev/null +++ b/src/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector.php @@ -0,0 +1,230 @@ +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|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); + } + } +} diff --git a/src/Set/Packages/Livewire/LivewireLevelSetList.php b/src/Set/Packages/Livewire/LivewireLevelSetList.php new file mode 100644 index 00000000..36c3852a --- /dev/null +++ b/src/Set/Packages/Livewire/LivewireLevelSetList.php @@ -0,0 +1,15 @@ + +----- + diff --git a/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/fixture.php.inc b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..a75cbbae --- /dev/null +++ b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/fixture.php.inc @@ -0,0 +1,51 @@ + ['except' => 1, 'as' => 'foo'], + self::FOO_BAR, + ]; +} + +?> +----- + diff --git a/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/skip_non_component_class.php.inc b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/skip_non_component_class.php.inc new file mode 100644 index 00000000..aa9f8ee3 --- /dev/null +++ b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/Fixture/skip_non_component_class.php.inc @@ -0,0 +1,17 @@ + diff --git a/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/LivewireComponentQueryStringToUrlAttributeRectorTest.php b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/LivewireComponentQueryStringToUrlAttributeRectorTest.php new file mode 100644 index 00000000..8e8153ab --- /dev/null +++ b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/LivewireComponentQueryStringToUrlAttributeRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/config/configured_rule.php b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/config/configured_rule.php new file mode 100644 index 00000000..6d220b79 --- /dev/null +++ b/tests/Rector/Class_/LivewireComponentQueryStringToUrlAttributeRector/config/configured_rule.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../../../../../config/config.php'); + + $rectorConfig->rule(LivewireComponentQueryStringToUrlAttributeRector::class); +}; diff --git a/tests/Sets/Livewire30/Fixture/fixture.php.inc b/tests/Sets/Livewire30/Fixture/fixture.php.inc new file mode 100644 index 00000000..17f46299 --- /dev/null +++ b/tests/Sets/Livewire30/Fixture/fixture.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/tests/Sets/Livewire30/Livewire30Test.php b/tests/Sets/Livewire30/Livewire30Test.php new file mode 100644 index 00000000..2752ffe0 --- /dev/null +++ b/tests/Sets/Livewire30/Livewire30Test.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Sets/Livewire30/config/configured_rule.php b/tests/Sets/Livewire30/config/configured_rule.php new file mode 100644 index 00000000..deae720e --- /dev/null +++ b/tests/Sets/Livewire30/config/configured_rule.php @@ -0,0 +1,9 @@ +import(__DIR__ . '/../../../../config/sets/packages/livewire/livewire-30.php'); +};