Skip to content

Commit

Permalink
[7.x] Implement anonymous components (#31363)
Browse files Browse the repository at this point in the history
Implements class-less components.
  • Loading branch information
driesvints authored Feb 14, 2020
1 parent 4288dbb commit b1fe9eb
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 45 deletions.
55 changes: 55 additions & 0 deletions src/Illuminate/View/AnonymousComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Illuminate\View;

class AnonymousComponent extends Component
{
/**
* The component view.
*
* @var string
*/
protected $view;

/**
* The component data.
*
* @var array
*/
protected $data = [];

/**
* Create a new class-less component instance.
*
* @param string $view
* @param array $data
* @return void
*/
public function __construct($view, $data)
{
$this->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];
}
}
38 changes: 34 additions & 4 deletions src/Illuminate/View/Compilers/ComponentTagCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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).'])
<?php $component->withAttributes(['.$this->attributesToString($attributes->all()).']); ?>';
}

Expand 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}]."
);
}

/**
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/Illuminate/View/Compilers/Concerns/CompilesComponents.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<?php \$attributes = \$attributes->except{$expression}; ?>
<?php \$__defined_vars = get_defined_vars(); ?>
<?php foreach (\$attributes as \$key => \$value) {
if (array_key_exists(\$key, \$__defined_vars)) unset(\$\$key);
} ?>
<?php unset(\$__defined_vars); ?>";
}
}
94 changes: 75 additions & 19 deletions src/Illuminate/View/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down
65 changes: 43 additions & 22 deletions src/Illuminate/View/ComponentAttributeBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)));
}

/**
Expand Down Expand Up @@ -180,17 +187,31 @@ 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.
*
* @return string
*/
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);
}
}
17 changes: 17 additions & 0 deletions tests/View/Blade/BladeComponentTagCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,6 +122,22 @@ public function testPairedComponentTags()

$this->assertEquals("@component('Illuminate\Tests\View\Blade\TestAlertComponent', [])
<?php \$component->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('<x-anonymous-component name="Taylor" :age="31" wire:model="foo" />');

$this->assertEquals("@component('Illuminate\View\AnonymousComponent', ['view' => 'components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']])
<?php \$component->withAttributes(['name' => 'Taylor','age' => 31,'wire:model' => 'foo']); ?>
@endcomponentClass", trim($result));
}
}
Expand Down

0 comments on commit b1fe9eb

Please sign in to comment.