diff --git a/src/Illuminate/View/AnonymousComponent.php b/src/Illuminate/View/AnonymousComponent.php new file mode 100644 index 000000000000..c31221af3138 --- /dev/null +++ b/src/Illuminate/View/AnonymousComponent.php @@ -0,0 +1,55 @@ +view = $view; + $this->data = $data; + } + + /** + * Get the view / view contents that represent the component. + * + * @return string + */ + public function render() + { + return $this->view; + } + + /** + * Get the data that should be supplied to the view. + * + * @return array + */ + public function data() + { + $this->attributes = $this->attributes ?: new ComponentAttributeBag; + + return $this->data + ['attributes' => $this->attributes]; + } +} diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index 6a926724d1cd..fc8e7d52558b 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -4,7 +4,9 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\View\Factory; use Illuminate\Support\Str; +use Illuminate\View\AnonymousComponent; use InvalidArgumentException; use ReflectionClass; @@ -153,7 +155,21 @@ protected function componentString(string $component, array $attributes) [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes); - return " @component('{$class}', [".$this->attributesToString($data->all()).']) + // If the component doesn't exists as a class we'll assume it's a class-less + // component and pass the component as a view parameter to the data so it + // can be accessed within the component and we can render out the view. + if (! class_exists($class)) { + $parameters = [ + 'view' => "'$class'", + 'data' => '['.$this->attributesToString($data->all()).']', + ]; + + $class = AnonymousComponent::class; + } else { + $parameters = $data->all(); + } + + return " @component('{$class}', [".$this->attributesToString($parameters).']) withAttributes(['.$this->attributesToString($attributes->all()).']); ?>'; } @@ -169,11 +185,18 @@ protected function componentClass(string $component) return $this->aliases[$component]; } - if (! class_exists($class = $this->guessClassName($component))) { - throw new InvalidArgumentException("Unable to locate class for component [{$component}]."); + if (class_exists($class = $this->guessClassName($component))) { + return $class; + } + + if (Container::getInstance()->make(Factory::class) + ->exists($view = "components.{$component}")) { + return $view; } - return $class; + throw new InvalidArgumentException( + "Unable to locate a class or view for component [{$component}]." + ); } /** @@ -204,6 +227,13 @@ public function guessClassName(string $component) */ protected function partitionDataAndAttributes($class, array $attributes) { + // If the class doesn't exists, we'll assume it's a class-less component and + // return all of the attributes as both data and attributes since we have + // now way to partition them. The user can exclude attributes manually. + if (! class_exists($class)) { + return [collect($attributes), collect($attributes)]; + } + $constructor = (new ReflectionClass($class))->getConstructor(); $parameterNames = $constructor diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php index 976abdc70f78..6556b95fc4c5 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php @@ -138,4 +138,20 @@ protected function compileEndComponentFirst() { return $this->compileEndComponent(); } + + /** + * Compile the prop statement into valid PHP. + * + * @param string $expression + * @return string + */ + protected function compileProps($expression) + { + return "except{$expression}; ?> + + \$value) { + if (array_key_exists(\$key, \$__defined_vars)) unset(\$\$key); +} ?> +"; + } } diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 819d80c6e4a0..03c7444aa0e2 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -11,6 +11,20 @@ abstract class Component { + /** + * The cache of public property names, keyed by class. + * + * @var array + */ + protected static $propertyCache = []; + + /** + * The cache of public method names, keyed by class. + * + * @var array + */ + protected static $methodCache = []; + /** * That properties / methods that should not be exposed to the component. * @@ -85,25 +99,67 @@ public function data() { $this->attributes = $this->attributes ?: new ComponentAttributeBag; - $class = new ReflectionClass($this); - - $publicProperties = collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) - ->reject(function (ReflectionProperty $property) { - return $this->shouldIgnore($property->getName()); - }) - ->mapWithKeys(function (ReflectionProperty $property) { - return [$property->getName() => $this->{$property->getName()}]; - }); - - $publicMethods = collect($class->getMethods(ReflectionMethod::IS_PUBLIC)) - ->reject(function (ReflectionMethod $method) { - return $this->shouldIgnore($method->getName()); - }) - ->mapWithKeys(function (ReflectionMethod $method) { - return [$method->getName() => $this->createVariableFromMethod($method)]; - }); - - return $publicProperties->merge($publicMethods)->all(); + return array_merge($this->extractPublicProperties(), $this->extractPublicMethods()); + } + + /** + * Extract the public properties for the component. + * + * @return array + */ + protected function extractPublicProperties() + { + $class = get_class($this); + + if (! isset(static::$propertyCache[$class])) { + $reflection = new ReflectionClass($this); + + static::$propertyCache[$class] = collect($reflection->getProperties(ReflectionProperty::IS_PUBLIC)) + ->reject(function (ReflectionProperty $property) { + return $this->shouldIgnore($property->getName()); + }) + ->map(function (ReflectionProperty $property) { + return $property->getName(); + })->all(); + } + + $values = []; + + foreach (static::$propertyCache[$class] as $property) { + $values[$property] = $this->{$property}; + } + + return $values; + } + + /** + * Extract the public methods for the component. + * + * @return array + */ + protected function extractPublicMethods() + { + $class = get_class($this); + + if (! isset(static::$methodCache[$class])) { + $reflection = new ReflectionClass($this); + + static::$methodCache[$class] = collect($reflection->getMethods(ReflectionMethod::IS_PUBLIC)) + ->reject(function (ReflectionMethod $method) { + return $this->shouldIgnore($method->getName()); + }) + ->map(function (ReflectionMethod $method) { + return $method->getName(); + }); + } + + $values = []; + + foreach (static::$methodCache[$class] as $method) { + $values[$method] = $this->createVariableFromMethod(new ReflectionMethod($this, $method)); + } + + return $values; } /** diff --git a/src/Illuminate/View/ComponentAttributeBag.php b/src/Illuminate/View/ComponentAttributeBag.php index 858ec093fed1..8e87e8d4810e 100644 --- a/src/Illuminate/View/ComponentAttributeBag.php +++ b/src/Illuminate/View/ComponentAttributeBag.php @@ -3,11 +3,13 @@ namespace Illuminate\View; use ArrayAccess; +use ArrayIterator; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Support\Arr; use Illuminate\Support\HtmlString; +use IteratorAggregate; -class ComponentAttributeBag implements ArrayAccess, Htmlable +class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate { /** * The raw array of attributes. @@ -85,22 +87,27 @@ public function except($keys) */ public function merge(array $attributeDefaults = []) { - return new static( - array_merge($attributeDefaults, collect($this->attributes)->map(function ($value, $key) use ($attributeDefaults) { - if ($value === true) { - return $key; - } - - if ($key !== 'class') { - return $attributeDefaults[$key] ?? $value; - } - - return collect([$attributeDefaults[$key] ?? '', $value]) - ->filter() - ->unique() - ->join(' '); - })->filter()->all()) - ); + $attributes = []; + + foreach ($this->attributes as $key => $value) { + if ($value === true) { + $attributes[$key] = $key; + + continue; + } + + if ($key !== 'class') { + $attributes[$key] = $attributeDefaults[$key] ?? $value; + + continue; + } + + $attributes[$key] = implode(' ', array_unique( + array_filter([$attributeDefaults[$key] ?? '', $value]) + )); + } + + return new static(array_merge($attributeDefaults, array_filter($attributes))); } /** @@ -180,6 +187,16 @@ public function offsetUnset($offset) unset($this->attributes[$offset]); } + /** + * Get an iterator for the items. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->attributes); + } + /** * Implode the attributes into a single HTML ready string. * @@ -187,10 +204,14 @@ public function offsetUnset($offset) */ public function __toString() { - return collect($this->attributes)->map(function ($value, $key) { - return $value === true - ? $key - : $key.'="'.str_replace('"', '\\"', trim($value)).'"'; - })->implode(' '); + $string = ''; + + foreach ($this->attributes as $key => $value) { + $string .= $value === true + ? ' '.$key + : ' '.$key.'="'.str_replace('"', '\\"', trim($value)).'"'; + } + + return trim($string); } } diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index aca7956d3276..081436f16642 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -4,6 +4,7 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\View\Factory; use Illuminate\View\Compilers\ComponentTagCompiler; use Illuminate\View\Component; use Mockery; @@ -121,6 +122,22 @@ public function testPairedComponentTags() $this->assertEquals("@component('Illuminate\Tests\View\Blade\TestAlertComponent', []) withAttributes([]); ?> +@endcomponentClass", trim($result)); + } + + public function testClasslessComponents() + { + $container = new Container; + $container->instance(Application::class, $app = Mockery::mock(Application::class)); + $container->instance(Factory::class, $factory = Mockery::mock(Factory::class)); + $app->shouldReceive('getNamespace')->andReturn('App\\'); + $factory->shouldReceive('exists')->andReturn(true); + Container::setInstance($container); + + $result = (new ComponentTagCompiler([]))->compileTags(''); + + $this->assertEquals("@component('Illuminate\View\AnonymousComponent', ['view' => 'components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']]) +withAttributes(['name' => 'Taylor','age' => 31,'wire:model' => 'foo']); ?> @endcomponentClass", trim($result)); } }