Skip to content

Commit

Permalink
[9.x] Makes blade components blazing fast ⛽️ (#44487)
Browse files Browse the repository at this point in the history
* Improves performance of inline components

* Keys by content

* Uses class on key too

* Improves blade components performance

* Apply fixes from StyleCI

* Fixes cache

* Forgets factory in tests

* Apply fixes from StyleCI

* Fixes tests

* Apply fixes from StyleCI

* Avoids usage of container in regular components

* Apply fixes from StyleCI

* Removes todo

* Avoids extra calls checking if a view is expired

* Removes non needed include

* Avoids calling creators / composers when is not necessary

* Apply fixes from StyleCI

* Minor changes

* Improves tests

* Adds more tests

* More tests

* More tests

* Apply fixes from StyleCI

* Flushes parameters cache too

* Makes forget static

* More tests

* Docs

* More tests

* formatting

* Apply fixes from StyleCI

Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
3 people authored Oct 12, 2022
1 parent 7f71ec4 commit 331bf9c
Show file tree
Hide file tree
Showing 12 changed files with 772 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static function compileClassComponentOpening(string $component, string $a
{
return implode("\n", [
'<?php if (isset($component)) { $__componentOriginal'.$hash.' = $component; } ?>',
'<?php $component = $__env->getContainer()->make('.Str::finish($component, '::class').', '.($data ?: '[]').' + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?>',
'<?php $component = '.$component.'::resolve('.($data ?: '[]').' + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?>',
'<?php $component->withName('.$alias.'); ?>',
'<?php if ($component->shouldRender()): ?>',
'<?php $__env->startComponent($component->resolveView(), $component->data()); ?>',
Expand Down
193 changes: 172 additions & 21 deletions src/Illuminate/View/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@

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.
*
Expand All @@ -27,32 +69,61 @@ abstract class Component
protected static $methodCache = [];

/**
* The properties / methods that should not be exposed to the component.
* The cache of constructor parameters, keyed by class.
*
* @var array
* @var array<class-string, array<int, string>>
*/
protected $except = [];
protected static $constructorParametersCache = [];

/**
* The component alias name.
* Get the view / view contents that represent the component.
*
* @var string
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
*/
public $componentName;
abstract public function render();

/**
* The component attributes.
* Resolve the component instance with the given data.
*
* @var \Illuminate\View\ComponentAttributeBag
* @param array $data
* @return static
*/
public $attributes;
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);
}

/**
* Get the view / view contents that represent the component.
* Extract the constructor parameters for the component.
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
* @return array
*/
abstract public function render();
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.
Expand All @@ -72,11 +143,7 @@ public function resolveView()
}

$resolver = function ($view) {
$factory = Container::getInstance()->make('view');

return strlen($view) <= PHP_MAXPATHLEN && $factory->exists($view)
? $view
: $this->createBladeViewFromString($factory, $view);
return $this->extractBladeViewFromString($view);
};

return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) {
Expand All @@ -88,13 +155,22 @@ public function resolveView()
/**
* 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)
protected function extractBladeViewFromString($contents)
{
$factory->addNamespace(
$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;
}

$this->factory()->addNamespace(
'__components',
$directory = Container::getInstance()['config']->get('view.compiled')
);
Expand All @@ -107,7 +183,7 @@ protected function createBladeViewFromString($factory, $contents)
file_put_contents($viewFile, $contents);
}

return '__components::'.basename($viewFile, '.blade.php');
return static::$bladeViewCache[$key] = '__components::'.basename($viewFile, '.blade.php');
}

/**
Expand Down Expand Up @@ -292,4 +368,79 @@ 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;
}
}
47 changes: 45 additions & 2 deletions src/Illuminate/View/Concerns/ManagesEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@

use Closure;
use Illuminate\Contracts\View\View as ViewContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

trait ManagesEvents
{
/**
* An array of views and whether they have registered "creators".
*
* @var array<string, true>|true
*/
protected $shouldCallCreators = [];

/**
* An array of views and whether they have registered "composers".
*
* @var array<string, true>|true
*/
protected $shouldCallComposers = [];

/**
* Register a view creator event.
*
Expand All @@ -17,6 +32,16 @@ trait ManagesEvents
*/
public function creator($views, $callback)
{
if (is_array($this->shouldCallCreators)) {
if ($views == '*') {
$this->shouldCallCreators = true;
} else {
foreach (Arr::wrap($views) as $view) {
$this->shouldCallCreators[$this->normalizeName($view)] = true;
}
}
}

$creators = [];

foreach ((array) $views as $view) {
Expand Down Expand Up @@ -52,6 +77,16 @@ public function composers(array $composers)
*/
public function composer($views, $callback)
{
if (is_array($this->shouldCallComposers)) {
if ($views == '*') {
$this->shouldCallComposers = true;
} else {
foreach (Arr::wrap($views) as $view) {
$this->shouldCallComposers[$this->normalizeName($view)] = true;
}
}
}

$composers = [];

foreach ((array) $views as $view) {
Expand Down Expand Up @@ -174,7 +209,11 @@ protected function addEventListener($name, $callback)
*/
public function callComposer(ViewContract $view)
{
$this->events->dispatch('composing: '.$view->name(), [$view]);
if ($this->shouldCallComposers === true || isset($this->shouldCallComposers[
$this->normalizeName($view->name())
])) {
$this->events->dispatch('composing: '.$view->name(), [$view]);
}
}

/**
Expand All @@ -185,6 +224,10 @@ public function callComposer(ViewContract $view)
*/
public function callCreator(ViewContract $view)
{
$this->events->dispatch('creating: '.$view->name(), [$view]);
if ($this->shouldCallCreators === true || isset($this->shouldCallCreators[
$this->normalizeName((string) $view->name())
])) {
$this->events->dispatch('creating: '.$view->name(), [$view]);
}
}
}
21 changes: 20 additions & 1 deletion src/Illuminate/View/Engines/CompilerEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ class CompilerEngine extends PhpEngine
*/
protected $lastCompiled = [];

/**
* The view paths that were compiled or are not expired, keyed by the path.
*
* @var array<string, true>
*/
protected $compiledOrNotExpired = [];

/**
* Create a new compiler engine instance.
*
Expand Down Expand Up @@ -51,10 +58,12 @@ public function get($path, array $data = [])
// If this given view has expired, which means it has simply been edited since
// it was last compiled, we will re-compile the views so we can evaluate a
// fresh copy of the view. We'll pass the compiler the path of the view.
if ($this->compiler->isExpired($path)) {
if (! isset($this->compiledOrNotExpired[$path]) && $this->compiler->isExpired($path)) {
$this->compiler->compile($path);
}

$this->compiledOrNotExpired[$path] = true;

// Once we have the path to the compiled file, we will evaluate the paths with
// typical PHP just like any other templates. We also keep a stack of views
// which have been rendered for right exception messages to be generated.
Expand Down Expand Up @@ -101,4 +110,14 @@ public function getCompiler()
{
return $this->compiler;
}

/**
* Clear the cache of views that were compiled or not expired.
*
* @return void
*/
public function forgetCompiledOrNotExpired()
{
$this->compiledOrNotExpired = [];
}
}
Loading

0 comments on commit 331bf9c

Please sign in to comment.