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 30 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
168 changes: 159 additions & 9 deletions src/Illuminate/View/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@

abstract class Component
{
/**
* The components resolver used withing views.
*
* @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 view factory instance, if any.
*
* @var \Illuminate\Contracts\View\Factory|null
*/
protected static $factory;

/**
* The cache of public property names, keyed by class.
*
Expand All @@ -26,6 +47,13 @@ abstract class Component
*/
protected static $methodCache = [];

/**
* The cache of constructor parameters, keyed by class.
*
* @var array<class-string, array<int, string>>
*/
protected static $constructorParametersCache = [];

/**
* The properties / methods that should not be exposed to the component.
*
Expand Down Expand Up @@ -72,11 +100,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 +112,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 +140,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 +325,121 @@ public function shouldRender()
{
return true;
}

/**
* Flush the components cache.
*
* @return void
*/
public static function flushCache()
{
static::$bladeViewCache = [];
static::$constructorParametersCache = [];
static::$methodCache = [];
static::$propertyCache = [];
}

/**
* 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.
*
* @return \Illuminate\Contracts\View\Factory
*/
protected function factory()
{
if (is_null(static::$factory)) {
static::$factory = Container::getInstance()->make('view');
}

return static::$factory;
}

/**
* Forget the component's factory.
*
* @return void
*/
public static function forgetFactory()
{
static::$factory = null;
}

/**
* Forget the component resolver.
*
* @return void
*
* @internal
*/
public static function forgetComponentsResolver()
{
static::$componentsResolver = null;
}

/**
* Acts as static factory for the component.
*
* @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);
}

/**
* Set the callback to 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;
}

/**
* 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];
}
}
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
{
/**
* If the factory should call the creators.
*
* @var array<string, true>|true
*/
protected $shouldCallCreators = [];

/**
* If the factory should call the 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 @@ -16,6 +16,13 @@ class CompilerEngine extends PhpEngine
*/
protected $compiler;

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

/**
* A stack of the last compiled templates.
*
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;
}

/**
* Forgets the views that were compiled or not expired.
*
* @return void
*/
public function forgetCompiledOrNotExpired()
{
$this->compiledOrNotExpired = [];
}
}
16 changes: 15 additions & 1 deletion src/Illuminate/View/ViewServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public function register()
$this->registerViewFinder();
$this->registerBladeCompiler();
$this->registerEngineResolver();

$this->app->terminating(static function () {
Component::flushCache();
});
}

/**
Expand All @@ -48,6 +52,10 @@ public function registerFactory()

$factory->share('app', $app);

$app->terminating(static function () {
Component::forgetFactory();
});

return $factory;
});
}
Expand Down Expand Up @@ -153,7 +161,13 @@ public function registerPhpEngine($resolver)
public function registerBladeEngine($resolver)
{
$resolver->register('blade', function () {
return new CompilerEngine($this->app['blade.compiler'], $this->app['files']);
$compiler = new CompilerEngine($this->app['blade.compiler'], $this->app['files']);

$this->app->terminating(static function () use ($compiler) {
$compiler->forgetCompiledOrNotExpired();
});

return $compiler;
});
}
}
Loading