package and depencies

This commit is contained in:
RafficMohammed
2023-01-08 02:57:24 +05:30
parent d5332eb421
commit 1d54b8bc7f
4309 changed files with 193331 additions and 172289 deletions

View File

@@ -51,7 +51,7 @@ class AnonymousComponent extends Component
$this->attributes = $this->attributes ?: $this->newAttributeBag();
return array_merge(
optional($this->data['attributes'] ?? null)->getAttributes() ?: [],
($this->data['attributes'] ?? null)?->getAttributes() ?: [],
$this->attributes->getAttributes(),
$this->data,
['attributes' => $this->attributes]

View File

@@ -21,6 +21,7 @@ class BladeCompiler extends Compiler implements CompilerInterface
Concerns\CompilesConditionals,
Concerns\CompilesEchos,
Concerns\CompilesErrors,
Concerns\CompilesFragments,
Concerns\CompilesHelpers,
Concerns\CompilesIncludes,
Concerns\CompilesInjections,
@@ -122,6 +123,20 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected $rawBlocks = [];
/**
* The array of anonymous component paths to search for components in.
*
* @var array
*/
protected $anonymousComponentPaths = [];
/**
* The array of anonymous component namespaces to autoload from.
*
* @var array
*/
protected $anonymousComponentNamespaces = [];
/**
* The array of class component aliases and their class names.
*
@@ -241,7 +256,7 @@ class BladeCompiler extends Compiler implements CompilerInterface
);
foreach ($this->precompilers as $precompiler) {
$value = call_user_func($precompiler, $value);
$value = $precompiler($value);
}
// Here we will loop through all of the tokens returned by the Zend lexer and
@@ -340,11 +355,11 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected function storeUncompiledBlocks($value)
{
if (strpos($value, '@verbatim') !== false) {
if (str_contains($value, '@verbatim')) {
$value = $this->storeVerbatimBlocks($value);
}
if (strpos($value, '@php') !== false) {
if (str_contains($value, '@php')) {
$value = $this->storePhpBlocks($value);
}
@@ -504,12 +519,14 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected function compileStatement($match)
{
if (Str::contains($match[1], '@')) {
if (str_contains($match[1], '@')) {
$match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1];
} elseif (isset($this->customDirectives[$match[1]])) {
$match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3));
} elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) {
$match[0] = $this->$method(Arr::get($match, 3));
} else {
return $match[0];
}
return isset($match[3]) ? $match[0] : $match[0].$match[2];
@@ -524,9 +541,9 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected function callCustomDirective($name, $value)
{
$value = $value ?? '';
$value ??= '';
if (Str::startsWith($value, '(') && Str::endsWith($value, ')')) {
if (str_starts_with($value, '(') && str_ends_with($value, ')')) {
$value = Str::substr($value, 1, -1);
}
@@ -625,12 +642,12 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
public function component($class, $alias = null, $prefix = '')
{
if (! is_null($alias) && Str::contains($alias, '\\')) {
if (! is_null($alias) && str_contains($alias, '\\')) {
[$class, $alias] = [$alias, $class];
}
if (is_null($alias)) {
$alias = Str::contains($class, '\\View\\Components\\')
$alias = str_contains($class, '\\View\\Components\\')
? collect(explode('\\', Str::after($class, '\\View\\Components\\')))->map(function ($segment) {
return Str::kebab($segment);
})->implode(':')
@@ -672,6 +689,45 @@ class BladeCompiler extends Compiler implements CompilerInterface
return $this->classComponentAliases;
}
/**
* Register a new anonymous component path.
*
* @param string $path
* @param string|null $prefix
* @return void
*/
public function anonymousComponentPath(string $path, string $prefix = null)
{
$prefixHash = md5($prefix ?: $path);
$this->anonymousComponentPaths[] = [
'path' => $path,
'prefix' => $prefix,
'prefixHash' => $prefixHash,
];
Container::getInstance()
->make(ViewFactory::class)
->addNamespace($prefixHash, $path);
}
/**
* Register an anonymous component namespace.
*
* @param string $directory
* @param string|null $prefix
* @return void
*/
public function anonymousComponentNamespace(string $directory, string $prefix = null)
{
$prefix ??= $directory;
$this->anonymousComponentNamespaces[$prefix] = Str::of($directory)
->replace('/', '.')
->trim('. ')
->toString();
}
/**
* Register a class-based component namespace.
*
@@ -684,6 +740,26 @@ class BladeCompiler extends Compiler implements CompilerInterface
$this->classComponentNamespaces[$prefix] = $namespace;
}
/**
* Get the registered anonymous component paths.
*
* @return array
*/
public function getAnonymousComponentPaths()
{
return $this->anonymousComponentPaths;
}
/**
* Get the registered anonymous component namespaces.
*
* @return array
*/
public function getAnonymousComponentNamespaces()
{
return $this->anonymousComponentNamespaces;
}
/**
* Get the registered class component namespaces.
*

View File

@@ -3,34 +3,64 @@
namespace Illuminate\View\Compilers;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use InvalidArgumentException;
abstract class Compiler
{
/**
* The Filesystem instance.
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* Get the cache path for the compiled views.
* The cache path for the compiled views.
*
* @var string
*/
protected $cachePath;
/**
* The base path that should be removed from paths before hashing.
*
* @var string
*/
protected $basePath;
/**
* Determines if compiled views should be cached.
*
* @var bool
*/
protected $shouldCache;
/**
* The compiled view file extension.
*
* @var string
*/
protected $compiledExtension = 'php';
/**
* Create a new compiler instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $cachePath
* @param string $basePath
* @param bool $shouldCache
* @param string $compiledExtension
* @return void
*
* @throws \InvalidArgumentException
*/
public function __construct(Filesystem $files, $cachePath)
public function __construct(
Filesystem $files,
$cachePath,
$basePath = '',
$shouldCache = true,
$compiledExtension = 'php')
{
if (! $cachePath) {
throw new InvalidArgumentException('Please provide a valid cache path.');
@@ -38,6 +68,9 @@ abstract class Compiler
$this->files = $files;
$this->cachePath = $cachePath;
$this->basePath = $basePath;
$this->shouldCache = $shouldCache;
$this->compiledExtension = $compiledExtension;
}
/**
@@ -48,7 +81,7 @@ abstract class Compiler
*/
public function getCompiledPath($path)
{
return $this->cachePath.'/'.sha1('v2'.$path).'.php';
return $this->cachePath.'/'.sha1('v2'.Str::after($path, $this->basePath)).'.'.$this->compiledExtension;
}
/**
@@ -59,6 +92,10 @@ abstract class Compiler
*/
public function isExpired($path)
{
if (! $this->shouldCache) {
return true;
}
$compiled = $this->getCompiledPath($path);
// If the compiled file doesn't exist we will indicate that the view is expired

View File

@@ -111,10 +111,18 @@ class ComponentTagCompiler
(?:
\s+
(?:
(?:
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
|
(?:
(\:\\\$)(\w+)
)
|
(?:
[\w\-:.@]+
(
@@ -164,10 +172,18 @@ class ComponentTagCompiler
(?:
\s+
(?:
(?:
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
|
(?:
(\:\\\$)(\w+)
)
|
(?:
[\w\-:.@]+
(
@@ -216,12 +232,16 @@ class ComponentTagCompiler
return [Str::camel($key) => $value];
});
// If the component doesn't exists as a class we'll assume it's a class-less
// If the component doesn't exist as a class, we'll assume it's a class-less
// component and pass the component as a view parameter to the data so it
// can be accessed within the component and we can render out the view.
if (! class_exists($class)) {
$view = Str::startsWith($component, 'mail::')
? "\$__env->getContainer()->make(Illuminate\\View\\Factory::class)->make('{$component}')"
: "'$class'";
$parameters = [
'view' => "'$class'",
'view' => $view,
'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']',
];
@@ -231,6 +251,9 @@ class ComponentTagCompiler
}
return "##BEGIN-COMPONENT-CLASS##@component('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).'])
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag && $constructor = (new ReflectionClass('.$class.'::class))->getConstructor()): ?>
<?php $attributes = $attributes->except(collect($constructor->getParameters())->map->getName()->all()); ?>
<?php endif; ?>
<?php $component->withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>';
}
@@ -268,12 +291,13 @@ class ComponentTagCompiler
return $class;
}
if ($viewFactory->exists($view = $this->guessViewName($component))) {
return $view;
if (! is_null($guess = $this->guessAnonymousComponentUsingNamespaces($viewFactory, $component)) ||
! is_null($guess = $this->guessAnonymousComponentUsingPaths($viewFactory, $component))) {
return $guess;
}
if ($viewFactory->exists($view = $this->guessViewName($component).'.index')) {
return $view;
if (Str::startsWith($component, 'mail::')) {
return $component;
}
throw new InvalidArgumentException(
@@ -281,6 +305,72 @@ class ComponentTagCompiler
);
}
/**
* Attempt to find an anonymous component using the registered anonymous component paths.
*
* @param \Illuminate\Contracts\View\Factory $viewFactory
* @param string $component
* @return string|null
*/
protected function guessAnonymousComponentUsingPaths(Factory $viewFactory, string $component)
{
$delimiter = ViewFinderInterface::HINT_PATH_DELIMITER;
foreach ($this->blade->getAnonymousComponentPaths() as $path) {
try {
if (str_contains($component, $delimiter) &&
! str_starts_with($component, $path['prefix'].$delimiter)) {
continue;
}
$formattedComponent = str_starts_with($component, $path['prefix'].$delimiter)
? Str::after($component, $delimiter)
: $component;
if (! is_null($guess = match (true) {
$viewFactory->exists($guess = $path['prefixHash'].$delimiter.$formattedComponent) => $guess,
$viewFactory->exists($guess = $path['prefixHash'].$delimiter.$formattedComponent.'.index') => $guess,
default => null,
})) {
return $guess;
}
} catch (InvalidArgumentException $e) {
//
}
}
}
/**
* Attempt to find an anonymous component using the registered anonymous component namespaces.
*
* @param \Illuminate\Contracts\View\Factory $viewFactory
* @param string $component
* @return string|null
*/
protected function guessAnonymousComponentUsingNamespaces(Factory $viewFactory, string $component)
{
return collect($this->blade->getAnonymousComponentNamespaces())
->filter(function ($directory, $prefix) use ($component) {
return Str::startsWith($component, $prefix.'::');
})
->prepend('components', $component)
->reduce(function ($carry, $directory, $prefix) use ($component, $viewFactory) {
if (! is_null($carry)) {
return $carry;
}
$componentName = Str::after($component, $prefix.'::');
if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory))) {
return $view;
}
if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory).'.index')) {
return $view;
}
});
}
/**
* Find the class for the given component using the registered namespaces.
*
@@ -293,7 +383,7 @@ class ComponentTagCompiler
$prefix = $segments[0];
if (! isset($this->namespaces[$prefix]) || ! isset($segments[1])) {
if (! isset($this->namespaces[$prefix], $segments[1])) {
return;
}
@@ -338,15 +428,18 @@ class ComponentTagCompiler
* Guess the view name for the given component.
*
* @param string $name
* @param string $prefix
* @return string
*/
public function guessViewName($name)
public function guessViewName($name, $prefix = 'components.')
{
$prefix = 'components.';
if (! Str::endsWith($prefix, '.')) {
$prefix .= '.';
}
$delimiter = ViewFinderInterface::HINT_PATH_DELIMITER;
if (Str::contains($name, $delimiter)) {
if (str_contains($name, $delimiter)) {
return Str::replaceFirst($delimiter, $delimiter.$prefix, $name);
}
@@ -362,7 +455,7 @@ class ComponentTagCompiler
*/
public function partitionDataAndAttributes($class, array $attributes)
{
// If the class doesn't exists, we'll assume it's a class-less component and
// If the class doesn't exist, we'll assume it is a class-less component and
// return all of the attributes as both data and attributes since we have
// now way to partition them. The user can exclude attributes manually.
if (! class_exists($class)) {
@@ -403,12 +496,16 @@ class ComponentTagCompiler
<
\s*
x[\-\:]slot
\s+
(:?)name=(?<name>(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+))
(?:\:(?<inlineName>\w+(?:-\w+)*))?
(?:\s+(:?)name=(?<name>(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+)))?
(?<attributes>
(?:
\s+
(?:
(?:
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
@@ -435,9 +532,13 @@ class ComponentTagCompiler
/x";
$value = preg_replace_callback($pattern, function ($matches) {
$name = $this->stripQuotes($matches['name']);
$name = $this->stripQuotes($matches['inlineName'] ?: $matches['name']);
if ($matches[1] !== ':') {
if (Str::contains($name, '-') && ! empty($matches['inlineName'])) {
$name = Str::camel($name);
}
if ($matches[2] !== ':') {
$name = "'{$name}'";
}
@@ -459,8 +560,9 @@ class ComponentTagCompiler
*/
protected function getAttributesFromAttributeString(string $attributeString)
{
$attributeString = $this->parseShortAttributeSyntax($attributeString);
$attributeString = $this->parseAttributeBag($attributeString);
$attributeString = $this->parseComponentTagClassStatements($attributeString);
$attributeString = $this->parseBindAttributes($attributeString);
$pattern = '/
@@ -495,7 +597,7 @@ class ComponentTagCompiler
$value = $this->stripQuotes($value);
if (Str::startsWith($attribute, 'bind:')) {
if (str_starts_with($attribute, 'bind:')) {
$attribute = Str::after($attribute, 'bind:');
$this->boundAttributes[$attribute] = true;
@@ -503,7 +605,7 @@ class ComponentTagCompiler
$value = "'".$this->compileAttributeEchos($value)."'";
}
if (Str::startsWith($attribute, '::')) {
if (str_starts_with($attribute, '::')) {
$attribute = substr($attribute, 1);
}
@@ -511,6 +613,21 @@ class ComponentTagCompiler
})->toArray();
}
/**
* Parses a short attribute syntax like :$foo into a fully-qualified syntax like :foo="$foo".
*
* @param string $value
* @return string
*/
protected function parseShortAttributeSyntax(string $value)
{
$pattern = "/\s\:\\\$(\w+)/x";
return preg_replace_callback($pattern, function (array $matches) {
return " :{$matches[1]}=\"\${$matches[1]}\"";
}, $value);
}
/**
* Parse the attribute bag in a given attribute string into its fully-qualified syntax.
*
@@ -527,6 +644,27 @@ class ComponentTagCompiler
return preg_replace($pattern, ' :attributes="$1"', $attributeString);
}
/**
* Parse @class statements in a given attribute string into their fully-qualified syntax.
*
* @param string $attributeString
* @return string
*/
protected function parseComponentTagClassStatements(string $attributeString)
{
return preg_replace_callback(
'/@(class)(\( ( (?>[^()]+) | (?2) )* \))/x', function ($match) {
if ($match[1] === 'class') {
$match[2] = str_replace('"', "'", $match[2]);
return ":class=\"\Illuminate\Support\Arr::toCssClasses{$match[2]}\"";
}
return $match[0];
}, $attributeString
);
}
/**
* Parse the "bind" attributes in a given attribute string into their fully-qualified syntax.
*

View File

@@ -23,7 +23,7 @@ trait CompilesComponents
*/
protected function compileComponent($expression)
{
[$component, $alias, $data] = strpos($expression, ',') !== false
[$component, $alias, $data] = str_contains($expression, ',')
? array_map('trim', explode(',', trim($expression, '()'), 3)) + ['', '', '']
: [trim($expression, '()'), '', ''];
@@ -64,7 +64,7 @@ trait CompilesComponents
{
return implode("\n", [
'<?php if (isset($component)) { $__componentOriginal'.$hash.' = $component; } ?>',
'<?php $component = $__env->getContainer()->make('.Str::finish($component, '::class').', '.($data ?: '[]').'); ?>',
'<?php $component = '.$component.'::resolve('.($data ?: '[]').' + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? (array) $attributes->getIterator() : [])); ?>',
'<?php $component->withName('.$alias.'); ?>',
'<?php if ($component->shouldRender()): ?>',
'<?php $__env->startComponent($component->resolveView(), $component->data()); ?>',
@@ -149,7 +149,11 @@ trait CompilesComponents
*/
protected function compileProps($expression)
{
return "<?php \$attributes = \$attributes->exceptProps{$expression}; ?>
return "<?php \$attributes ??= new \\Illuminate\\View\\ComponentAttributeBag; ?>
<?php foreach(\$attributes->onlyProps{$expression} as \$__key => \$__value) {
\$\$__key = \$\$__key ?? \$__value;
} ?>
<?php \$attributes = \$attributes->exceptProps{$expression}; ?>
<?php foreach (array_filter({$expression}, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) {
\$\$__key = \$\$__key ?? \$__value;
} ?>
@@ -182,7 +186,7 @@ trait CompilesComponents
*/
public static function sanitizeComponentAttribute($value)
{
if (is_object($value) && $value instanceof CanBeEscapedWhenCastToString) {
if ($value instanceof CanBeEscapedWhenCastToString) {
return $value->escapeWhenCastingToString();
}

View File

@@ -304,4 +304,82 @@ trait CompilesConditionals
{
return '<?php endif; ?>';
}
/**
* Compile a selected block into valid PHP.
*
* @param string $condition
* @return string
*/
protected function compileSelected($condition)
{
return "<?php if{$condition}: echo 'selected'; endif; ?>";
}
/**
* Compile a checked block into valid PHP.
*
* @param string $condition
* @return string
*/
protected function compileChecked($condition)
{
return "<?php if{$condition}: echo 'checked'; endif; ?>";
}
/**
* Compile a disabled block into valid PHP.
*
* @param string $condition
* @return string
*/
protected function compileDisabled($condition)
{
return "<?php if{$condition}: echo 'disabled'; endif; ?>";
}
/**
* Compile a required block into valid PHP.
*
* @param string $condition
* @return string
*/
protected function compileRequired($condition)
{
return "<?php if{$condition}: echo 'required'; endif; ?>";
}
/**
* Compile a readonly block into valid PHP.
*
* @param string $condition
* @return string
*/
protected function compileReadonly($condition)
{
return "<?php if{$condition}: echo 'readonly'; endif; ?>";
}
/**
* Compile the push statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePushIf($expression)
{
$parts = explode(',', $this->stripParentheses($expression), 2);
return "<?php if({$parts[0]}): \$__env->startPush({$parts[1]}); ?>";
}
/**
* Compile the end-push statements into valid PHP.
*
* @return string
*/
protected function compileEndPushIf()
{
return '<?php $__env->stopPush(); endif; ?>';
}
}

View File

@@ -143,7 +143,7 @@ trait CompilesEchos
{
$value = Str::of($value)
->trim()
->when(Str::endsWith($value, ';'), function ($str) {
->when(str_ends_with($value, ';'), function ($str) {
return $str->beforeLast(';');
});

View File

@@ -0,0 +1,36 @@
<?php
namespace Illuminate\View\Compilers\Concerns;
trait CompilesFragments
{
/**
* The last compiled fragment.
*
* @var string
*/
protected $lastFragment;
/**
* Compile the fragment statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileFragment($expression)
{
$this->lastFragment = trim($expression, "()'\" ");
return "<?php \$__env->startFragment{$expression}; ?>";
}
/**
* Compile the end-fragment statements into valid PHP.
*
* @return string
*/
protected function compileEndfragment()
{
return '<?php echo $__env->stopFragment(); ?>';
}
}

View File

@@ -2,6 +2,8 @@
namespace Illuminate\View\Compilers\Concerns;
use Illuminate\Foundation\Vite;
trait CompilesHelpers
{
/**
@@ -46,4 +48,31 @@ trait CompilesHelpers
{
return "<?php echo method_field{$method}; ?>";
}
/**
* Compile the "vite" statements into valid PHP.
*
* @param ?string $arguments
* @return string
*/
protected function compileVite($arguments)
{
$arguments ??= '()';
$class = Vite::class;
return "<?php echo app('$class'){$arguments}; ?>";
}
/**
* Compile the "viteReactRefresh" statements into valid PHP.
*
* @return string
*/
protected function compileViteReactRefresh()
{
$class = Vite::class;
return "<?php echo app('$class')->reactRefresh(); ?>";
}
}

View File

@@ -2,6 +2,8 @@
namespace Illuminate\View\Compilers\Concerns;
use Illuminate\Contracts\View\ViewCompilationException;
trait CompilesLoops
{
/**
@@ -16,12 +18,18 @@ trait CompilesLoops
*
* @param string $expression
* @return string
*
* @throws \Illuminate\Contracts\View\ViewCompilationException
*/
protected function compileForelse($expression)
{
$empty = '$__empty_'.++$this->forElseCounter;
preg_match('/\( *(.*) +as *(.*)\)$/is', $expression, $matches);
preg_match('/\( *(.+) +as +(.+)\)$/is', $expression ?? '', $matches);
if (count($matches) === 0) {
throw new ViewCompilationException('Malformed @forelse statement.');
}
$iteratee = trim($matches[1]);
@@ -87,10 +95,16 @@ trait CompilesLoops
*
* @param string $expression
* @return string
*
* @throws \Illuminate\Contracts\View\ViewCompilationException
*/
protected function compileForeach($expression)
{
preg_match('/\( *(.*) +as *(.*)\)$/is', $expression, $matches);
preg_match('/\( *(.+) +as +(.*)\)$/is', $expression ?? '', $matches);
if (count($matches) === 0) {
throw new ViewCompilationException('Malformed @foreach statement.');
}
$iteratee = trim($matches[1]);

View File

@@ -2,6 +2,8 @@
namespace Illuminate\View\Compilers\Concerns;
use Illuminate\Support\Str;
trait CompilesStacks
{
/**
@@ -26,6 +28,24 @@ trait CompilesStacks
return "<?php \$__env->startPush{$expression}; ?>";
}
/**
* Compile the push-once statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePushOnce($expression)
{
$parts = explode(',', $this->stripParentheses($expression), 2);
[$stack, $id] = [$parts[0], $parts[1] ?? ''];
$id = trim($id) ?: "'".(string) Str::uuid()."'";
return '<?php if (! $__env->hasRenderedOnce('.$id.')): $__env->markAsRenderedOnce('.$id.');
$__env->startPush('.$stack.'); ?>';
}
/**
* Compile the end-push statements into valid PHP.
*
@@ -36,6 +56,16 @@ trait CompilesStacks
return '<?php $__env->stopPush(); ?>';
}
/**
* Compile the end-push-once statements into valid PHP.
*
* @return string
*/
protected function compileEndpushOnce()
{
return '<?php $__env->stopPush(); endif; ?>';
}
/**
* Compile the prepend statements into valid PHP.
*
@@ -47,6 +77,24 @@ trait CompilesStacks
return "<?php \$__env->startPrepend{$expression}; ?>";
}
/**
* Compile the prepend-once statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePrependOnce($expression)
{
$parts = explode(',', $this->stripParentheses($expression), 2);
[$stack, $id] = [$parts[0], $parts[1] ?? ''];
$id = trim($id) ?: "'".(string) Str::uuid()."'";
return '<?php if (! $__env->hasRenderedOnce('.$id.')): $__env->markAsRenderedOnce('.$id.');
$__env->startPrepend('.$stack.'); ?>';
}
/**
* Compile the end-prepend statements into valid PHP.
*
@@ -56,4 +104,14 @@ trait CompilesStacks
{
return '<?php $__env->stopPrepend(); ?>';
}
/**
* Compile the end-prepend-once statements into valid PHP.
*
* @return string
*/
protected function compileEndprependOnce()
{
return '<?php $__env->stopPrepend(); endif; ?>';
}
}

View File

@@ -6,27 +6,12 @@ use Closure;
use Illuminate\Container\Container;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View as ViewContract;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
abstract class Component
{
/**
* 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 properties / methods that should not be exposed to the component.
*
@@ -48,6 +33,48 @@ abstract class Component
*/
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.
*
@@ -55,6 +82,49 @@ abstract class Component
*/
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.
*
@@ -73,11 +143,7 @@ abstract class Component
}
$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) {
@@ -86,6 +152,27 @@ abstract class Component
: $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.
*
@@ -223,7 +310,7 @@ abstract class Component
*/
protected function shouldIgnore($name)
{
return Str::startsWith($name, '__') ||
return str_starts_with($name, '__') ||
in_array($name, $this->ignoredMethods());
}
@@ -293,4 +380,79 @@ abstract class Component
{
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;
}
}

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;
use IteratorAggregate;
use Traversable;
class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
{
@@ -68,6 +69,17 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
return array_key_exists($key, $this->attributes);
}
/**
* Determine if a given attribute is missing from the attribute array.
*
* @param string $key
* @return bool
*/
public function missing($key)
{
return ! $this->has($key, $this->attributes);
}
/**
* Only include the given attribute from the attribute array.
*
@@ -154,6 +166,17 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
return $this->whereStartsWith($needles);
}
/**
* Only include the given attribute from the attribute array.
*
* @param mixed|array $keys
* @return static
*/
public function onlyProps($keys)
{
return $this->only($this->extractPropNames($keys));
}
/**
* Exclude the given attribute from the attribute array.
*
@@ -161,6 +184,17 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
* @return static
*/
public function exceptProps($keys)
{
return $this->except($this->extractPropNames($keys));
}
/**
* Extract prop names from given keys.
*
* @param mixed|array $keys
* @return array
*/
protected function extractPropNames($keys)
{
$props = [];
@@ -171,7 +205,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
$props[] = Str::kebab($key);
}
return $this->except($props);
return $props;
}
/**
@@ -323,8 +357,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
* @param string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
public function offsetExists($offset): bool
{
return isset($this->attributes[$offset]);
}
@@ -335,8 +368,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
* @param string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
public function offsetGet($offset): mixed
{
return $this->get($offset);
}
@@ -348,8 +380,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{
$this->attributes[$offset] = $value;
}
@@ -360,8 +391,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
* @param string $offset
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
public function offsetUnset($offset): void
{
unset($this->attributes[$offset]);
}
@@ -371,8 +401,7 @@ class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
*
* @return \ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Traversable
{
return new ArrayIterator($this->attributes);
}

View File

@@ -55,7 +55,7 @@ trait ManagesEvents
$composers = [];
foreach ((array) $views as $view) {
$composers[] = $this->addViewEvent($view, $callback, 'composing: ');
$composers[] = $this->addViewEvent($view, $callback);
}
return $composers;
@@ -145,7 +145,7 @@ trait ManagesEvents
*/
protected function classEventMethodForPrefix($prefix)
{
return Str::contains($prefix, 'composing') ? 'compose' : 'create';
return str_contains($prefix, 'composing') ? 'compose' : 'create';
}
/**
@@ -157,7 +157,7 @@ trait ManagesEvents
*/
protected function addEventListener($name, $callback)
{
if (Str::contains($name, '*')) {
if (str_contains($name, '*')) {
$callback = function ($name, array $data) use ($callback) {
return $callback($data[0]);
};

View File

@@ -0,0 +1,88 @@
<?php
namespace Illuminate\View\Concerns;
use InvalidArgumentException;
trait ManagesFragments
{
/**
* All of the captured, rendered fragments.
*
* @var array
*/
protected $fragments = [];
/**
* The stack of in-progress fragment renders.
*
* @var array
*/
protected $fragmentStack = [];
/**
* Start injecting content into a fragment.
*
* @param string $fragment
* @return void
*/
public function startFragment($fragment)
{
if (ob_start()) {
$this->fragmentStack[] = $fragment;
}
}
/**
* Stop injecting content into a fragment.
*
* @return string
*
* @throws \InvalidArgumentException
*/
public function stopFragment()
{
if (empty($this->fragmentStack)) {
throw new InvalidArgumentException('Cannot end a fragment without first starting one.');
}
$last = array_pop($this->fragmentStack);
$this->fragments[$last] = ob_get_clean();
return $this->fragments[$last];
}
/**
* Get the contents of a fragment.
*
* @param string $name
* @param string|null $default
* @return mixed
*/
public function getFragment($name, $default = null)
{
return $this->getFragments()[$name] ?? $default;
}
/**
* Get the entire array of rendered fragments.
*
* @return array
*/
public function getFragments()
{
return $this->fragments;
}
/**
* Flush all of the fragments.
*
* @return void
*/
public function flushFragments()
{
$this->fragments = [];
$this->fragmentStack = [];
}
}

View File

@@ -2,8 +2,8 @@
namespace Illuminate\View\Concerns;
use Countable;
use Illuminate\Support\Arr;
use Illuminate\Support\LazyCollection;
trait ManagesLoops
{
@@ -22,7 +22,9 @@ trait ManagesLoops
*/
public function addLoop($data)
{
$length = is_array($data) || $data instanceof Countable ? count($data) : null;
$length = is_countable($data) && ! $data instanceof LazyCollection
? count($data)
: null;
$parent = Arr::last($this->loopsStack);

View File

@@ -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.
*
@@ -51,14 +58,31 @@ class CompilerEngine extends PhpEngine
// 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);
}
// 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.
$results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
try {
$results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
} catch (ViewException $e) {
if (! str($e->getMessage())->contains(['No such file or directory', 'File does not exist at path'])) {
throw $e;
}
if (! isset($this->compiledOrNotExpired[$path])) {
throw $e;
}
$this->compiler->compile($path);
$results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
}
$this->compiledOrNotExpired[$path] = true;
array_pop($this->lastCompiled);
@@ -101,4 +125,14 @@ class CompilerEngine extends PhpEngine
{
return $this->compiler;
}
/**
* Clear the cache of views that were compiled or not expired.
*
* @return void
*/
public function forgetCompiledOrNotExpired()
{
$this->compiledOrNotExpired = [];
}
}

View File

@@ -7,7 +7,6 @@ use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\View\Factory as FactoryContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Illuminate\View\Engines\EngineResolver;
use InvalidArgumentException;
@@ -17,6 +16,7 @@ class Factory implements FactoryContract
use Macroable,
Concerns\ManagesComponents,
Concerns\ManagesEvents,
Concerns\ManagesFragments,
Concerns\ManagesLayouts,
Concerns\ManagesLoops,
Concerns\ManagesStacks,
@@ -231,7 +231,7 @@ class Factory implements FactoryContract
// view. Alternatively, the "empty view" could be a raw string that begins
// with "raw|" for convenience and to let this know that it is a string.
else {
$result = Str::startsWith($empty, 'raw|')
$result = str_starts_with($empty, 'raw|')
? substr($empty, 4)
: $this->make($empty)->render();
}
@@ -321,7 +321,7 @@ class Factory implements FactoryContract
$extensions = array_keys($this->extensions);
return Arr::first($extensions, function ($value) use ($path) {
return Str::endsWith($path, '.'.$value);
return str_ends_with($path, '.'.$value);
});
}
@@ -482,6 +482,7 @@ class Factory implements FactoryContract
$this->flushSections();
$this->flushStacks();
$this->flushComponents();
$this->flushFragments();
}
/**

View File

@@ -145,9 +145,7 @@ class FileViewFinder implements ViewFinderInterface
*/
protected function getPossibleViewFiles($name)
{
return array_map(function ($extension) use ($name) {
return str_replace('.', '/', $name).'.'.$extension;
}, $this->extensions);
return array_map(fn ($extension) => str_replace('.', '/', $name).'.'.$extension, $this->extensions);
}
/**

View File

@@ -7,6 +7,7 @@ use Closure;
use Illuminate\Contracts\Support\DeferringDisplayableValue;
use Illuminate\Support\Enumerable;
use IteratorAggregate;
use Traversable;
class InvokableComponentVariable implements DeferringDisplayableValue, IteratorAggregate
{
@@ -43,8 +44,7 @@ class InvokableComponentVariable implements DeferringDisplayableValue, IteratorA
*
* @return \ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Traversable
{
$result = $this->__invoke();

View File

@@ -77,6 +77,19 @@ class View implements ArrayAccess, Htmlable, ViewContract
$this->data = $data instanceof Arrayable ? $data->toArray() : (array) $data;
}
/**
* Get the evaluated contents of a given fragment.
*
* @param string $fragment
* @return string
*/
public function fragment($fragment)
{
return $this->render(function () use ($fragment) {
return $this->factory->getFragment($fragment);
});
}
/**
* Get the string contents of the view.
*
@@ -112,7 +125,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
*/
protected function renderContents()
{
// We will keep track of the amount of views being rendered so we can flush
// We will keep track of the number of views being rendered so we can flush
// the section after the complete rendering operation is done. This will
// clear out the sections for any separate views that may be rendered.
$this->factory->incrementRender();
@@ -122,7 +135,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
$contents = $this->getContents();
// Once we've finished rendering the view, we'll decrement the render count
// so that each sections get flushed out next time a view is created and
// so that each section gets flushed out next time a view is created and
// no old sections are staying around in the memory of an environment.
$this->factory->decrementRender();
@@ -306,8 +319,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
public function offsetExists($key): bool
{
return array_key_exists($key, $this->data);
}
@@ -318,8 +330,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
public function offsetGet($key): mixed
{
return $this->data[$key];
}
@@ -331,8 +342,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
public function offsetSet($key, $value): void
{
$this->with($key, $value);
}
@@ -343,8 +353,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
* @param string $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
public function offsetUnset($key): void
{
unset($this->data[$key]);
}
@@ -409,7 +418,7 @@ class View implements ArrayAccess, Htmlable, ViewContract
return $this->macroCall($method, $parameters);
}
if (! Str::startsWith($method, 'with')) {
if (! str_starts_with($method, 'with')) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));

View File

@@ -28,7 +28,7 @@ class ViewException extends ErrorException
* Render the exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\Response|null
*/
public function render($request)
{

View File

@@ -14,7 +14,7 @@ class ViewName
{
$delimiter = ViewFinderInterface::HINT_PATH_DELIMITER;
if (strpos($name, $delimiter) === false) {
if (! str_contains($name, $delimiter)) {
return str_replace('/', '.', $name);
}

View File

@@ -22,6 +22,10 @@ class ViewServiceProvider extends ServiceProvider
$this->registerViewFinder();
$this->registerBladeCompiler();
$this->registerEngineResolver();
$this->app->terminating(static function () {
Component::flushCache();
});
}
/**
@@ -48,6 +52,10 @@ class ViewServiceProvider extends ServiceProvider
$factory->share('app', $app);
$app->terminating(static function () {
Component::forgetFactory();
});
return $factory;
});
}
@@ -85,7 +93,13 @@ class ViewServiceProvider extends ServiceProvider
public function registerBladeCompiler()
{
$this->app->singleton('blade.compiler', function ($app) {
return tap(new BladeCompiler($app['files'], $app['config']['view.compiled']), function ($blade) {
return tap(new BladeCompiler(
$app['files'],
$app['config']['view.compiled'],
$app['config']->get('view.relative_hash', false) ? $app->basePath() : '',
$app['config']->get('view.cache', true),
$app['config']->get('view.compiled_extension', 'php'),
), function ($blade) {
$blade->component('dynamic-component', DynamicComponent::class);
});
});
@@ -147,7 +161,13 @@ class ViewServiceProvider extends ServiceProvider
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;
});
}
}

View File

@@ -14,15 +14,15 @@
}
],
"require": {
"php": "^7.3|^8.0",
"php": "^8.0.2",
"ext-json": "*",
"illuminate/collections": "^8.0",
"illuminate/container": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/events": "^8.0",
"illuminate/filesystem": "^8.0",
"illuminate/macroable": "^8.0",
"illuminate/support": "^8.0"
"illuminate/collections": "^9.0",
"illuminate/container": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/events": "^9.0",
"illuminate/filesystem": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
@@ -31,7 +31,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"config": {