Repository: laravel/framework (php)

Issue:
New blade syntax breaks passing in static variables as attribute.
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Laravel Version: 9.34.0
- PHP Version: 8.10.0
- Database Driver & Version: N/A

### Description:

There is a backwards compatibility issue with the new blade syntax. https://twitter.com/taylorotwell/status/1577388846806929415

Using static class variables as blade attributes is no longer working. 

A work around:
Use a getter method on the class instead of the static variable.
``:options="\App\Actions\CreateNewUser::userRanges()"``

### Steps To Reproduce:

```blade
        <x-select
          name="users"
          :label="Estimated number users"
          :value="old('users')"
          :options="\App\Actions\CreateNewUser::$userRanges"
        />
```


<!-- If possible, please provide a GitHub repository to demonstrate your issue -->
<!-- laravel new bug-report --github="--public" -->


Pull request:
[9.x] Fixes issue using static class properties as blade attributes
The PR resolves #44465 which raises a breaking change where static class properties can no longer be passed to blade attributes since v9.32.0 when short syntax was introduced.

The fix updates the regex used to determine whether short syntax is being used.

Currently the regex looks for the sequence `:$` within the component string. This successfully determines the short syntax, but will also match calls to a static class property, for example User: _**:$**_ id

To resolve this issue, `\s` has been added to the beginning of the regex to ensure there is whitespace before the `:` character.


Patch:
diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php
index f815b1468b..3870ca95c7 100644
--- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php
+++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php
@@ -567,10 +567,10 @@ protected function getAttributesFromAttributeString(string $attributeString)
      */
     protected function parseShortAttributeSyntax(string $value)
     {
-        $pattern = "/\:\\\$(\w+)/x";
+        $pattern = "/\s\:\\\$(\w+)/x";
 
         return preg_replace_callback($pattern, function (array $matches) {
-            return ":{$matches[1]}=\"\${$matches[1]}\"";
+            return " :{$matches[1]}=\"\${$matches[1]}\"";
         }, $value);
     }
 


Test patch:
diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php
index cb7e3c3312..aaf6d32cf6 100644
--- a/tests/Routing/RoutingRouteTest.php
+++ b/tests/Routing/RoutingRouteTest.php
@@ -575,6 +575,74 @@ public function testNonGreedyMatches()
         $this->assertSame('bar', $route->parameter('foo', 'bar'));
     }
 
+    public function testHasParameters()
+    {
+        $route = new Route('GET', 'images/{id}.{ext}', function () {
+            //
+        });
+        $request1 = Request::create('images/1.png', 'GET');
+        $this->assertFalse($route->hasParameters());
+        $this->assertTrue($route->matches($request1));
+        $route->bind($request1);
+        $this->assertTrue($route->hasParameters());
+    }
+
+    public function testForgetParameter()
+    {
+        $route = new Route('GET', 'images/{id}.{ext}', function () {
+            //
+        });
+        $request1 = Request::create('images/1.png', 'GET');
+        $route->bind($request1);
+        $this->assertTrue($route->hasParameter('id'));
+        $this->assertTrue($route->hasParameter('ext'));
+        $route->forgetParameter('id');
+        $this->assertFalse($route->hasParameter('id'));
+        $this->assertTrue($route->hasParameter('ext'));
+    }
+
+    public function testParameterNames()
+    {
+        $route = new Route('GET', 'images/{id}.{ext}', function () {
+            //
+        });
+        $this->assertSame(['id', 'ext'], $route->parameterNames());
+
+        $route = new Route('GET', 'foo/{bar?}', function () {
+            //
+        });
+        $this->assertSame(['bar'], $route->parameterNames());
+
+        $route = new Route('GET', '/', function () {
+            //
+        });
+        $this->assertSame([], $route->parameterNames());
+    }
+
+    public function testParametersWithoutNulls()
+    {
+        $route = new Route('GET', 'users/{id?}/{name?}/', function () {
+            //
+        });
+        $request1 = Request::create('users/12/amir', 'GET');
+        $route->bind($request1);
+        $this->assertSame(['id' => '12', 'name' => 'amir'], $route->parametersWithoutNulls());
+
+        $route = new Route('GET', 'users/{id?}/{name?}/', function () {
+            //
+        });
+        $request1 = Request::create('users/12', 'GET');
+        $route->bind($request1);
+        $this->assertSame(['id' => '12'], $route->parametersWithoutNulls());
+
+        $route = new Route('GET', 'users/{id?}/{name?}/', function () {
+            //
+        });
+        $request1 = Request::create('users/', 'GET');
+        $route->bind($request1);
+        $this->assertSame([], $route->parametersWithoutNulls());
+    }
+
     public function testRouteParametersDefaultValue()
     {
         $router = $this->getRouter();
diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php
index 50953241f3..781adb9ef5 100644
--- a/tests/View/Blade/BladeComponentTagCompilerTest.php
+++ b/tests/View/Blade/BladeComponentTagCompilerTest.php
@@ -148,6 +148,36 @@ public function testColonDataShortSyntax()
 <?php \$component->withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
     }
 
+    public function testColonDataWithStaticClassProperty()
+    {
+        $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('<x-profile :userId="User::$id"></x-profile>');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => User::\$id])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestProfileComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
+    }
+
+    public function testColonDataWithStaticClassPropertyAndMultipleAttributes()
+    {
+        $result = $this->compiler(['input' => TestInputComponent::class])->compileTags('<x-input :label="Input::$label" :$name value="Joe"></x-input>');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['label' => Input::\$label,'name' => \$name,'value' => 'Joe'])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestInputComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
+
+        $result = $this->compiler(['input' => TestInputComponent::class])->compileTags('<x-input value="Joe" :$name :label="Input::$label"></x-input>');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['value' => 'Joe','name' => \$name,'label' => Input::\$label])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestInputComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
+    }
+
     public function testSelfClosingComponentWithColonDataShortSyntax()
     {
         $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('<x-profile :$userId/>');
@@ -160,6 +190,39 @@ public function testSelfClosingComponentWithColonDataShortSyntax()
 '@endComponentClass##END-COMPONENT-CLASS##', trim($result));
     }
 
+    public function testSelfClosingComponentWithColonDataAndStaticClassPropertyShortSyntax()
+    {
+        $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('<x-profile :userId="User::$id"/>');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => User::\$id])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestProfileComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?>\n".
+'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
+    }
+
+    public function testSelfClosingComponentWithColonDataMultipleAttributesAndStaticClassPropertyShortSyntax()
+    {
+        $result = $this->compiler(['input' => TestInputComponent::class])->compileTags('<x-input :label="Input::$label" value="Joe" :$name />');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['label' => Input::\$label,'value' => 'Joe','name' => \$name])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestInputComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?>\n".
+'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
+
+        $result = $this->compiler(['input' => TestInputComponent::class])->compileTags('<x-input :$name :label="Input::$label" value="Joe" />');
+
+        $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['name' => \$name,'label' => Input::\$label,'value' => 'Joe'])
+<?php if (isset(\$attributes) && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestInputComponent::class))->getConstructor()): ?>
+<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
+<?php endif; ?>
+<?php \$component->withAttributes([]); ?>\n".
+'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
+    }
+
     public function testEscapedColonAttribute()
     {
         $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('<x-profile :user-id="1" ::title="user.name"></x-profile>');
@@ -491,7 +554,9 @@ public function __toString()
             }
         };
 
-        $model = new class extends Model {};
+        $model = new class extends Model
+        {
+        };
 
         $this->assertEquals(e('<hi>'), BladeCompiler::sanitizeComponentAttribute('<hi>'));
         $this->assertEquals(e('1'), BladeCompiler::sanitizeComponentAttribute('1'));
@@ -607,3 +672,20 @@ public function render()
         return 'profile';
     }
 }
+
+class TestInputComponent extends Component
+{
+    public $userId;
+
+    public function __construct($name, $label, $value)
+    {
+        $this->name = $name;
+        $this->label = $label;
+        $this->value = $value;
+    }
+
+    public function render()
+    {
+        return 'input';
+    }
+}