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');
+};