Skip to content

Commit 331bf9c

Browse files
nunomaduroStyleCIBottaylorotwell
authored
[9.x] Makes blade components blazing fast ⛽️ (#44487)
* 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 <[email protected]> Co-authored-by: Taylor Otwell <[email protected]>
1 parent 7f71ec4 commit 331bf9c

12 files changed

+772
-74
lines changed

src/Illuminate/View/Compilers/Concerns/CompilesComponents.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public static function compileClassComponentOpening(string $component, string $a
6464
{
6565
return implode("\n", [
6666
'<?php if (isset($component)) { $__componentOriginal'.$hash.' = $component; } ?>',
67-
'<?php $component = $__env->getContainer()->make('.Str::finish($component, '::class').', '.($data ?: '[]').' + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?>',
67+
'<?php $component = '.$component.'::resolve('.($data ?: '[]').' + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?>',
6868
'<?php $component->withName('.$alias.'); ?>',
6969
'<?php if ($component->shouldRender()): ?>',
7070
'<?php $__env->startComponent($component->resolveView(), $component->data()); ?>',

src/Illuminate/View/Component.php

+172-21
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,48 @@
1212

1313
abstract class Component
1414
{
15+
/**
16+
* The properties / methods that should not be exposed to the component.
17+
*
18+
* @var array
19+
*/
20+
protected $except = [];
21+
22+
/**
23+
* The component alias name.
24+
*
25+
* @var string
26+
*/
27+
public $componentName;
28+
29+
/**
30+
* The component attributes.
31+
*
32+
* @var \Illuminate\View\ComponentAttributeBag
33+
*/
34+
public $attributes;
35+
36+
/**
37+
* The view factory instance, if any.
38+
*
39+
* @var \Illuminate\Contracts\View\Factory|null
40+
*/
41+
protected static $factory;
42+
43+
/**
44+
* The component resolver callback.
45+
*
46+
* @var (\Closure(string, array): Component)|null
47+
*/
48+
protected static $componentsResolver;
49+
50+
/**
51+
* The cache of blade view names, keyed by contents.
52+
*
53+
* @var array<string, string>
54+
*/
55+
protected static $bladeViewCache = [];
56+
1557
/**
1658
* The cache of public property names, keyed by class.
1759
*
@@ -27,32 +69,61 @@ abstract class Component
2769
protected static $methodCache = [];
2870

2971
/**
30-
* The properties / methods that should not be exposed to the component.
72+
* The cache of constructor parameters, keyed by class.
3173
*
32-
* @var array
74+
* @var array<class-string, array<int, string>>
3375
*/
34-
protected $except = [];
76+
protected static $constructorParametersCache = [];
3577

3678
/**
37-
* The component alias name.
79+
* Get the view / view contents that represent the component.
3880
*
39-
* @var string
81+
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
4082
*/
41-
public $componentName;
83+
abstract public function render();
4284

4385
/**
44-
* The component attributes.
86+
* Resolve the component instance with the given data.
4587
*
46-
* @var \Illuminate\View\ComponentAttributeBag
88+
* @param array $data
89+
* @return static
4790
*/
48-
public $attributes;
91+
public static function resolve($data)
92+
{
93+
if (static::$componentsResolver) {
94+
return call_user_func(static::$componentsResolver, static::class, $data);
95+
}
96+
97+
$parameters = static::extractConstructorParameters();
98+
99+
$dataKeys = array_keys($data);
100+
101+
if (empty(array_diff($parameters, $dataKeys))) {
102+
return new static(...array_intersect_key($data, array_flip($parameters)));
103+
}
104+
105+
return Container::getInstance()->make(static::class, $data);
106+
}
49107

50108
/**
51-
* Get the view / view contents that represent the component.
109+
* Extract the constructor parameters for the component.
52110
*
53-
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string
111+
* @return array
54112
*/
55-
abstract public function render();
113+
protected static function extractConstructorParameters()
114+
{
115+
if (! isset(static::$constructorParametersCache[static::class])) {
116+
$class = new ReflectionClass(static::class);
117+
118+
$constructor = $class->getConstructor();
119+
120+
static::$constructorParametersCache[static::class] = $constructor
121+
? collect($constructor->getParameters())->map->getName()->all()
122+
: [];
123+
}
124+
125+
return static::$constructorParametersCache[static::class];
126+
}
56127

57128
/**
58129
* Resolve the Blade view or view file that should be used when rendering the component.
@@ -72,11 +143,7 @@ public function resolveView()
72143
}
73144

74145
$resolver = function ($view) {
75-
$factory = Container::getInstance()->make('view');
76-
77-
return strlen($view) <= PHP_MAXPATHLEN && $factory->exists($view)
78-
? $view
79-
: $this->createBladeViewFromString($factory, $view);
146+
return $this->extractBladeViewFromString($view);
80147
};
81148

82149
return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) {
@@ -88,13 +155,22 @@ public function resolveView()
88155
/**
89156
* Create a Blade view with the raw component string content.
90157
*
91-
* @param \Illuminate\Contracts\View\Factory $factory
92158
* @param string $contents
93159
* @return string
94160
*/
95-
protected function createBladeViewFromString($factory, $contents)
161+
protected function extractBladeViewFromString($contents)
96162
{
97-
$factory->addNamespace(
163+
$key = sprintf('%s::%s', static::class, $contents);
164+
165+
if (isset(static::$bladeViewCache[$key])) {
166+
return static::$bladeViewCache[$key];
167+
}
168+
169+
if (strlen($contents) <= PHP_MAXPATHLEN && $this->factory()->exists($contents)) {
170+
return static::$bladeViewCache[$key] = $contents;
171+
}
172+
173+
$this->factory()->addNamespace(
98174
'__components',
99175
$directory = Container::getInstance()['config']->get('view.compiled')
100176
);
@@ -107,7 +183,7 @@ protected function createBladeViewFromString($factory, $contents)
107183
file_put_contents($viewFile, $contents);
108184
}
109185

110-
return '__components::'.basename($viewFile, '.blade.php');
186+
return static::$bladeViewCache[$key] = '__components::'.basename($viewFile, '.blade.php');
111187
}
112188

113189
/**
@@ -292,4 +368,79 @@ public function shouldRender()
292368
{
293369
return true;
294370
}
371+
372+
/**
373+
* Get the evaluated view contents for the given view.
374+
*
375+
* @param string|null $view
376+
* @param \Illuminate\Contracts\Support\Arrayable|array $data
377+
* @param array $mergeData
378+
* @return \Illuminate\Contracts\View\View
379+
*/
380+
public function view($view, $data = [], $mergeData = [])
381+
{
382+
return $this->factory()->make($view, $data, $mergeData);
383+
}
384+
385+
/**
386+
* Get the view factory instance.
387+
*
388+
* @return \Illuminate\Contracts\View\Factory
389+
*/
390+
protected function factory()
391+
{
392+
if (is_null(static::$factory)) {
393+
static::$factory = Container::getInstance()->make('view');
394+
}
395+
396+
return static::$factory;
397+
}
398+
399+
/**
400+
* Flush the component's cached state.
401+
*
402+
* @return void
403+
*/
404+
public static function flushCache()
405+
{
406+
static::$bladeViewCache = [];
407+
static::$constructorParametersCache = [];
408+
static::$methodCache = [];
409+
static::$propertyCache = [];
410+
}
411+
412+
/**
413+
* Forget the component's factory instance.
414+
*
415+
* @return void
416+
*/
417+
public static function forgetFactory()
418+
{
419+
static::$factory = null;
420+
}
421+
422+
/**
423+
* Forget the component's resolver callback.
424+
*
425+
* @return void
426+
*
427+
* @internal
428+
*/
429+
public static function forgetComponentsResolver()
430+
{
431+
static::$componentsResolver = null;
432+
}
433+
434+
/**
435+
* Set the callback that should be used to resolve components within views.
436+
*
437+
* @param \Closure(string $component, array $data): Component $resolver
438+
* @return void
439+
*
440+
* @internal
441+
*/
442+
public static function resolveComponentsUsing($resolver)
443+
{
444+
static::$componentsResolver = $resolver;
445+
}
295446
}

src/Illuminate/View/Concerns/ManagesEvents.php

+45-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@
44

55
use Closure;
66
use Illuminate\Contracts\View\View as ViewContract;
7+
use Illuminate\Support\Arr;
78
use Illuminate\Support\Str;
89

910
trait ManagesEvents
1011
{
12+
/**
13+
* An array of views and whether they have registered "creators".
14+
*
15+
* @var array<string, true>|true
16+
*/
17+
protected $shouldCallCreators = [];
18+
19+
/**
20+
* An array of views and whether they have registered "composers".
21+
*
22+
* @var array<string, true>|true
23+
*/
24+
protected $shouldCallComposers = [];
25+
1126
/**
1227
* Register a view creator event.
1328
*
@@ -17,6 +32,16 @@ trait ManagesEvents
1732
*/
1833
public function creator($views, $callback)
1934
{
35+
if (is_array($this->shouldCallCreators)) {
36+
if ($views == '*') {
37+
$this->shouldCallCreators = true;
38+
} else {
39+
foreach (Arr::wrap($views) as $view) {
40+
$this->shouldCallCreators[$this->normalizeName($view)] = true;
41+
}
42+
}
43+
}
44+
2045
$creators = [];
2146

2247
foreach ((array) $views as $view) {
@@ -52,6 +77,16 @@ public function composers(array $composers)
5277
*/
5378
public function composer($views, $callback)
5479
{
80+
if (is_array($this->shouldCallComposers)) {
81+
if ($views == '*') {
82+
$this->shouldCallComposers = true;
83+
} else {
84+
foreach (Arr::wrap($views) as $view) {
85+
$this->shouldCallComposers[$this->normalizeName($view)] = true;
86+
}
87+
}
88+
}
89+
5590
$composers = [];
5691

5792
foreach ((array) $views as $view) {
@@ -174,7 +209,11 @@ protected function addEventListener($name, $callback)
174209
*/
175210
public function callComposer(ViewContract $view)
176211
{
177-
$this->events->dispatch('composing: '.$view->name(), [$view]);
212+
if ($this->shouldCallComposers === true || isset($this->shouldCallComposers[
213+
$this->normalizeName($view->name())
214+
])) {
215+
$this->events->dispatch('composing: '.$view->name(), [$view]);
216+
}
178217
}
179218

180219
/**
@@ -185,6 +224,10 @@ public function callComposer(ViewContract $view)
185224
*/
186225
public function callCreator(ViewContract $view)
187226
{
188-
$this->events->dispatch('creating: '.$view->name(), [$view]);
227+
if ($this->shouldCallCreators === true || isset($this->shouldCallCreators[
228+
$this->normalizeName((string) $view->name())
229+
])) {
230+
$this->events->dispatch('creating: '.$view->name(), [$view]);
231+
}
189232
}
190233
}

src/Illuminate/View/Engines/CompilerEngine.php

+20-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ class CompilerEngine extends PhpEngine
2323
*/
2424
protected $lastCompiled = [];
2525

26+
/**
27+
* The view paths that were compiled or are not expired, keyed by the path.
28+
*
29+
* @var array<string, true>
30+
*/
31+
protected $compiledOrNotExpired = [];
32+
2633
/**
2734
* Create a new compiler engine instance.
2835
*
@@ -51,10 +58,12 @@ public function get($path, array $data = [])
5158
// If this given view has expired, which means it has simply been edited since
5259
// it was last compiled, we will re-compile the views so we can evaluate a
5360
// fresh copy of the view. We'll pass the compiler the path of the view.
54-
if ($this->compiler->isExpired($path)) {
61+
if (! isset($this->compiledOrNotExpired[$path]) && $this->compiler->isExpired($path)) {
5562
$this->compiler->compile($path);
5663
}
5764

65+
$this->compiledOrNotExpired[$path] = true;
66+
5867
// Once we have the path to the compiled file, we will evaluate the paths with
5968
// typical PHP just like any other templates. We also keep a stack of views
6069
// which have been rendered for right exception messages to be generated.
@@ -101,4 +110,14 @@ public function getCompiler()
101110
{
102111
return $this->compiler;
103112
}
113+
114+
/**
115+
* Clear the cache of views that were compiled or not expired.
116+
*
117+
* @return void
118+
*/
119+
public function forgetCompiledOrNotExpired()
120+
{
121+
$this->compiledOrNotExpired = [];
122+
}
104123
}

0 commit comments

Comments
 (0)