459 lines
12 KiB
PHP
459 lines
12 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Illuminate\View;
|
||
|
|
||
|
use Closure;
|
||
|
use Illuminate\Container\Container;
|
||
|
use Illuminate\Contracts\Support\Htmlable;
|
||
|
use Illuminate\Contracts\View\View as ViewContract;
|
||
|
use ReflectionClass;
|
||
|
use ReflectionMethod;
|
||
|
use ReflectionProperty;
|
||
|
|
||
|
abstract class Component
|
||
|
{
|
||
|
/**
|
||
|
* The properties / methods that should not be exposed to the component.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $except = [];
|
||
|
|
||
|
/**
|
||
|
* The component alias name.
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public $componentName;
|
||
|
|
||
|
/**
|
||
|
* The component attributes.
|
||
|
*
|
||
|
* @var \Illuminate\View\ComponentAttributeBag
|
||
|
*/
|
||
|
public $attributes;
|
||
|
|
||
|
/**
|
||
|
* The view factory instance, if any.
|
||
|
*
|
||
|
* @var \Illuminate\Contracts\View\Factory|null
|
||
|
*/
|
||
|
protected static $factory;
|
||
|
|
||
|
/**
|
||
|
* The component resolver callback.
|
||
|
*
|
||
|
* @var (\Closure(string, array): Component)|null
|
||
|
*/
|
||
|
protected static $componentsResolver;
|
||
|
|
||
|
/**
|
||
|
* The cache of blade view names, keyed by contents.
|
||
|
*
|
||
|
* @var array<string, string>
|
||
|
*/
|
||
|
protected static $bladeViewCache = [];
|
||
|
|
||
|
/**
|
||
|
* 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 = [];
|
||
|
|
||
|
/**
|
||
|
* The cache of constructor parameters, keyed by class.
|
||
|
*
|
||
|
* @var array<class-string, array<int, string>>
|
||
|
*/
|
||
|
protected static $constructorParametersCache = [];
|
||
|
|
||
|
/**
|
||
|
* Get the view / view contents that represent the component.
|
||
|
*
|
||
|
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
|
||
|
*/
|
||
|
abstract public function render();
|
||
|
|
||
|
/**
|
||
|
* Resolve the component instance with the given data.
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @return static
|
||
|
*/
|
||
|
public static function resolve($data)
|
||
|
{
|
||
|
if (static::$componentsResolver) {
|
||
|
return call_user_func(static::$componentsResolver, static::class, $data);
|
||
|
}
|
||
|
|
||
|
$parameters = static::extractConstructorParameters();
|
||
|
|
||
|
$dataKeys = array_keys($data);
|
||
|
|
||
|
if (empty(array_diff($parameters, $dataKeys))) {
|
||
|
return new static(...array_intersect_key($data, array_flip($parameters)));
|
||
|
}
|
||
|
|
||
|
return Container::getInstance()->make(static::class, $data);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extract the constructor parameters for the component.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected static function extractConstructorParameters()
|
||
|
{
|
||
|
if (! isset(static::$constructorParametersCache[static::class])) {
|
||
|
$class = new ReflectionClass(static::class);
|
||
|
|
||
|
$constructor = $class->getConstructor();
|
||
|
|
||
|
static::$constructorParametersCache[static::class] = $constructor
|
||
|
? collect($constructor->getParameters())->map->getName()->all()
|
||
|
: [];
|
||
|
}
|
||
|
|
||
|
return static::$constructorParametersCache[static::class];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resolve the Blade view or view file that should be used when rendering the component.
|
||
|
*
|
||
|
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
|
||
|
*/
|
||
|
public function resolveView()
|
||
|
{
|
||
|
$view = $this->render();
|
||
|
|
||
|
if ($view instanceof ViewContract) {
|
||
|
return $view;
|
||
|
}
|
||
|
|
||
|
if ($view instanceof Htmlable) {
|
||
|
return $view;
|
||
|
}
|
||
|
|
||
|
$resolver = function ($view) {
|
||
|
return $this->extractBladeViewFromString($view);
|
||
|
};
|
||
|
|
||
|
return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) {
|
||
|
return $resolver($view($data));
|
||
|
}
|
||
|
: $resolver($view);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a Blade view with the raw component string content.
|
||
|
*
|
||
|
* @param string $contents
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function extractBladeViewFromString($contents)
|
||
|
{
|
||
|
$key = sprintf('%s::%s', static::class, $contents);
|
||
|
|
||
|
if (isset(static::$bladeViewCache[$key])) {
|
||
|
return static::$bladeViewCache[$key];
|
||
|
}
|
||
|
|
||
|
if (strlen($contents) <= PHP_MAXPATHLEN && $this->factory()->exists($contents)) {
|
||
|
return static::$bladeViewCache[$key] = $contents;
|
||
|
}
|
||
|
|
||
|
return static::$bladeViewCache[$key] = $this->createBladeViewFromString($this->factory(), $contents);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a Blade view with the raw component string content.
|
||
|
*
|
||
|
* @param \Illuminate\Contracts\View\Factory $factory
|
||
|
* @param string $contents
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function createBladeViewFromString($factory, $contents)
|
||
|
{
|
||
|
$factory->addNamespace(
|
||
|
'__components',
|
||
|
$directory = Container::getInstance()['config']->get('view.compiled')
|
||
|
);
|
||
|
|
||
|
if (! is_file($viewFile = $directory.'/'.sha1($contents).'.blade.php')) {
|
||
|
if (! is_dir($directory)) {
|
||
|
mkdir($directory, 0755, true);
|
||
|
}
|
||
|
|
||
|
file_put_contents($viewFile, $contents);
|
||
|
}
|
||
|
|
||
|
return '__components::'.basename($viewFile, '.blade.php');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the data that should be supplied to the view.
|
||
|
*
|
||
|
* @author Freek Van der Herten
|
||
|
* @author Brent Roose
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function data()
|
||
|
{
|
||
|
$this->attributes = $this->attributes ?: $this->newAttributeBag();
|
||
|
|
||
|
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 $property->isStatic();
|
||
|
})
|
||
|
->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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a callable variable from the given method.
|
||
|
*
|
||
|
* @param \ReflectionMethod $method
|
||
|
* @return mixed
|
||
|
*/
|
||
|
protected function createVariableFromMethod(ReflectionMethod $method)
|
||
|
{
|
||
|
return $method->getNumberOfParameters() === 0
|
||
|
? $this->createInvokableVariable($method->getName())
|
||
|
: Closure::fromCallable([$this, $method->getName()]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create an invokable, toStringable variable for the given component method.
|
||
|
*
|
||
|
* @param string $method
|
||
|
* @return \Illuminate\View\InvokableComponentVariable
|
||
|
*/
|
||
|
protected function createInvokableVariable(string $method)
|
||
|
{
|
||
|
return new InvokableComponentVariable(function () use ($method) {
|
||
|
return $this->{$method}();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine if the given property / method should be ignored.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function shouldIgnore($name)
|
||
|
{
|
||
|
return str_starts_with($name, '__') ||
|
||
|
in_array($name, $this->ignoredMethods());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the methods that should be ignored.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function ignoredMethods()
|
||
|
{
|
||
|
return array_merge([
|
||
|
'data',
|
||
|
'render',
|
||
|
'resolveView',
|
||
|
'shouldRender',
|
||
|
'view',
|
||
|
'withName',
|
||
|
'withAttributes',
|
||
|
], $this->except);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the component alias name.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function withName($name)
|
||
|
{
|
||
|
$this->componentName = $name;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the extra attributes that the component should make available.
|
||
|
*
|
||
|
* @param array $attributes
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function withAttributes(array $attributes)
|
||
|
{
|
||
|
$this->attributes = $this->attributes ?: $this->newAttributeBag();
|
||
|
|
||
|
$this->attributes->setAttributes($attributes);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a new attribute bag instance.
|
||
|
*
|
||
|
* @param array $attributes
|
||
|
* @return \Illuminate\View\ComponentAttributeBag
|
||
|
*/
|
||
|
protected function newAttributeBag(array $attributes = [])
|
||
|
{
|
||
|
return new ComponentAttributeBag($attributes);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine if the component should be rendered.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function shouldRender()
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the evaluated view contents for the given view.
|
||
|
*
|
||
|
* @param string|null $view
|
||
|
* @param \Illuminate\Contracts\Support\Arrayable|array $data
|
||
|
* @param array $mergeData
|
||
|
* @return \Illuminate\Contracts\View\View
|
||
|
*/
|
||
|
public function view($view, $data = [], $mergeData = [])
|
||
|
{
|
||
|
return $this->factory()->make($view, $data, $mergeData);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the view factory instance.
|
||
|
*
|
||
|
* @return \Illuminate\Contracts\View\Factory
|
||
|
*/
|
||
|
protected function factory()
|
||
|
{
|
||
|
if (is_null(static::$factory)) {
|
||
|
static::$factory = Container::getInstance()->make('view');
|
||
|
}
|
||
|
|
||
|
return static::$factory;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Flush the component's cached state.
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
public static function flushCache()
|
||
|
{
|
||
|
static::$bladeViewCache = [];
|
||
|
static::$constructorParametersCache = [];
|
||
|
static::$methodCache = [];
|
||
|
static::$propertyCache = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Forget the component's factory instance.
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
public static function forgetFactory()
|
||
|
{
|
||
|
static::$factory = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Forget the component's resolver callback.
|
||
|
*
|
||
|
* @return void
|
||
|
*
|
||
|
* @internal
|
||
|
*/
|
||
|
public static function forgetComponentsResolver()
|
||
|
{
|
||
|
static::$componentsResolver = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the callback that should be used to resolve components within views.
|
||
|
*
|
||
|
* @param \Closure(string $component, array $data): Component $resolver
|
||
|
* @return void
|
||
|
*
|
||
|
* @internal
|
||
|
*/
|
||
|
public static function resolveComponentsUsing($resolver)
|
||
|
{
|
||
|
static::$componentsResolver = $resolver;
|
||
|
}
|
||
|
}
|