Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Makes blade components blazing fast ⛽️ #44487

Merged
merged 32 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f5c6eba
Improves performance of inline components
nunomaduro Oct 5, 2022
981b684
Keys by content
nunomaduro Oct 5, 2022
3836f5c
Uses class on key too
nunomaduro Oct 5, 2022
0f6d05c
Improves blade components performance
nunomaduro Oct 6, 2022
a80dae1
Apply fixes from StyleCI
StyleCIBot Oct 6, 2022
22e821c
Merge branch 'feat/view-components-performance' into feat/performance…
nunomaduro Oct 6, 2022
fbd6811
Merge pull request #44480 from laravel/feat/performance-improvement-o…
nunomaduro Oct 6, 2022
9c3c416
Fixes cache
nunomaduro Oct 6, 2022
12a299e
Forgets factory in tests
nunomaduro Oct 6, 2022
ac1da93
Apply fixes from StyleCI
StyleCIBot Oct 6, 2022
1553bdd
Fixes tests
nunomaduro Oct 6, 2022
616b191
Apply fixes from StyleCI
StyleCIBot Oct 6, 2022
c70ec4c
Avoids usage of container in regular components
nunomaduro Oct 6, 2022
1ff8c82
Apply fixes from StyleCI
StyleCIBot Oct 6, 2022
f8fe3a5
Removes todo
nunomaduro Oct 6, 2022
b6ab56f
Avoids extra calls checking if a view is expired
nunomaduro Oct 6, 2022
7ead72d
Removes non needed include
nunomaduro Oct 6, 2022
cdbb413
Avoids calling creators / composers when is not necessary
nunomaduro Oct 7, 2022
7965920
Apply fixes from StyleCI
StyleCIBot Oct 7, 2022
10bbc60
Minor changes
nunomaduro Oct 7, 2022
547f31d
Improves tests
nunomaduro Oct 7, 2022
f0e397d
Adds more tests
nunomaduro Oct 7, 2022
83595e4
More tests
nunomaduro Oct 7, 2022
c859513
More tests
nunomaduro Oct 7, 2022
df1ac30
Apply fixes from StyleCI
StyleCIBot Oct 7, 2022
68e1d3c
Flushes parameters cache too
nunomaduro Oct 7, 2022
6b69ca2
Makes forget static
nunomaduro Oct 7, 2022
84948d5
More tests
nunomaduro Oct 7, 2022
cb2fc23
Docs
nunomaduro Oct 7, 2022
0631ccb
More tests
nunomaduro Oct 7, 2022
993f698
formatting
taylorotwell Oct 12, 2022
0745b1c
Apply fixes from StyleCI
StyleCIBot Oct 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = [])
nunomaduro marked this conversation as resolved.
Show resolved Hide resolved
{
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)) {
nunomaduro marked this conversation as resolved.
Show resolved Hide resolved
$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