From c7e51eab009fea310ee8fb8e799e2ca391de609b Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sun, 29 Mar 2026 19:57:51 +0200 Subject: [PATCH] Add fast paths for class/style string arguments in BlazeAttributeBag When merge(), class(), or style() receive a simple string argument (the most common usage), skip the general-purpose loops and handle the merge directly. The shared class-merging logic is extracted into withMergedClass() to avoid duplication between merge() and class(). Uses array union to place the merged key first, matching the attribute ordering of the general merge() path's array_merge($defaults, $attrs). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Runtime/BlazeAttributeBag.php | 43 +++++++++++++++++++ tests/ComparisonTest.php | 5 +++ .../views/components/merge-class.blade.php | 3 ++ 3 files changed, 51 insertions(+) create mode 100644 tests/fixtures/views/components/merge-class.blade.php diff --git a/src/Runtime/BlazeAttributeBag.php b/src/Runtime/BlazeAttributeBag.php index fb7c0fe..641650b 100644 --- a/src/Runtime/BlazeAttributeBag.php +++ b/src/Runtime/BlazeAttributeBag.php @@ -49,6 +49,11 @@ public static function make(array $attributes, array $boundKeys = [], array $ori /** {@inheritdoc} */ public function merge(array $attributeDefaults = [], $escape = true): static { + // Fast path: merge(['class' => 'string']) — the most common pattern. + if (count($attributeDefaults) === 1 && isset($attributeDefaults['class']) && is_string($attributeDefaults['class'])) { + return $this->withMergedClass($escape ? e($attributeDefaults['class']) : $attributeDefaults['class']); + } + if ($escape) { foreach ($attributeDefaults as $key => $value) { if ($this->shouldEscapeAttributeValue($escape, $value)) { @@ -103,6 +108,10 @@ public function merge(array $attributeDefaults = [], $escape = true): static /** {@inheritdoc} */ public function class($classList): static { + if (is_string($classList)) { + return $this->withMergedClass(e($classList)); + } + $classes = $this->toCssClasses(Arr::wrap($classList)); return $this->merge(['class' => $classes]); @@ -111,11 +120,45 @@ public function class($classList): static /** {@inheritdoc} */ public function style($styleList): static { + if (is_string($styleList)) { + $default = e(rtrim($styleList, ';').';'); + $current = $this->attributes['style'] ?? ''; + + if ($current) { + $current = rtrim((string) $current, ';').';'; + $style = $current === $default ? $default : $default.' '.$current; + } else { + $style = $default; + } + + return new static(['style' => $style] + $this->attributes); + } + $styles = $this->toCssStyles((array) $styleList); return $this->merge(['style' => $styles]); } + /** + * Return a new bag with the given class default merged into the current class attribute. + */ + protected function withMergedClass(string $default): static + { + $current = $this->attributes['class'] ?? ''; + + if (! $current || $current === $default) { + $class = $default; + } elseif (! $default) { + $class = $current ?: ''; + } else { + $class = $default.' '.$current; + } + + // Array union places class first, matching merge()'s + // array_merge($attributeDefaults, $attributes) key ordering. + return new static(['class' => $class] + $this->attributes); + } + /** * Convert class list to CSS classes string. */ diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index 165b09c..14f776a 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -95,6 +95,11 @@ BLADE )); +test('merge preserves attribute ordering', fn () => compare(<<<'BLADE' + + BLADE +)); + test('deeply nested same component with different components interleaved', fn () => compare(<<<'BLADE' diff --git a/tests/fixtures/views/components/merge-class.blade.php b/tests/fixtures/views/components/merge-class.blade.php new file mode 100644 index 0000000..dba7365 --- /dev/null +++ b/tests/fixtures/views/components/merge-class.blade.php @@ -0,0 +1,3 @@ +@blaze + +
merge(['class' => 'default-class']) }}>
\ No newline at end of file