Skip to content

Conversation

@dmitrijivanenko
Copy link

Optimize MessageBag::all() performance from O(n²) to O(n)

Summary

This PR optimizes the MessageBag::all() method by replacing the O(n²) array_merge() approach with O(n) direct array building, resulting in significant performance improvements for applications with large validation error sets.

Problem

The current implementation uses array_merge() in a loop, which creates a new array and copies all previous elements on each iteration:

foreach ($this->messages as $key => $messages) {
    $all = array_merge($all, $this->transform($messages, $format, $key));
}

This results in O(n²) complexity where n is the total number of messages.

Solution

Replace the array_merge() approach with direct array building:

foreach ($this->messages as $key => $messages) {
    $transformed = $this->transform($messages, $format, $key);
    foreach ($transformed as $message) {
        $all[] = $message;
    }
}

This reduces complexity to O(n) while maintaining identical behavior.

MessageBag::all() Performance Benchmark

MessageBag::all() Performance Benchmark
=====================================

Scalability Test:
================
 10 fields:   0.10 ms →   0.08 ms ( 18.0% improvement)
 50 fields:   0.92 ms →   0.31 ms ( 66.3% improvement)
100 fields:   3.16 ms →   0.61 ms ( 80.8% improvement)
200 fields:  11.25 ms →   1.24 ms ( 88.9% improvement)
500 fields:  73.79 ms →   3.20 ms ( 95.7% improvement)
Benchmark Code (Click to expand)

You can reproduce these results with the following code:

<?php
// Create test data with realistic validation messages
$messages = [];
for ($i = 0; $i < 500; $i++) {
    $messages["field_$i"] = [
        "The field_$i field is required.",
        "The field_$i field must be a string.",
        "The field_$i field must be at least 3 characters."
    ];
}

// Current implementation (O(n²)) - using array_merge
function currentAll($messages, $format) {
    $all = [];
    foreach ($messages as $key => $messageArray) {
        $transformed = $messageArray; // Simulate transform() returning array
        $all = array_merge($all, $transformed);
    }
    return $all;
}

// Optimized implementation (O(n)) - direct array building
function optimizedAll($messages, $format) {
    $all = [];
    foreach ($messages as $key => $messageArray) {
        $transformed = $messageArray; // Simulate transform() returning array
        foreach ($transformed as $message) {
            $all[] = $message;
        }
    }
    return $all;
}

// Benchmark
$format = ":message";

echo "MessageBag::all() Performance Benchmark\n";
echo "=====================================\n\n";

// Scalability test
echo "Scalability Test:\n";
echo "================\n";

$sizes = [10, 50, 100, 200, 500];
foreach ($sizes as $size) {
    $testMessages = array_slice($messages, 0, $size, true);
    
    $start = microtime(true);
    for ($i = 0; $i < 100; $i++) {
        currentAll($testMessages, $format);
    }
    $current = microtime(true) - $start;
    
    $start = microtime(true);
    for ($i = 0; $i < 100; $i++) {
        optimizedAll($testMessages, $format);
    }
    $optimized = microtime(true) - $start;
    
    $improvement = (($current - $optimized) / $current) * 100;
    
    printf("%3d fields: %6.2f ms → %6.2f ms (%5.1f%% improvement)\n", 
        $size, $current * 1000, $optimized * 1000, $improvement);
}
?>

Real-World Impact

This optimization particularly benefits:

  • Complex forms with many validation rules
  • API responses with extensive error aggregation
  • High-traffic applications processing large validation error sets
  • View error bags combining multiple error sources

Testing

  • All existing MessageBag tests pass
  • Integration tests pass
  • Behavior is identical - only performance improves

Backward Compatibility

This change is fully backward compatible with no breaking changes to the public API or behavior.

@dmitrijivanenko dmitrijivanenko changed the title Optimize MessageBag::all() [12.x] Optimize MessageBag::all() Oct 16, 2025
@NickSdot
Copy link
Contributor

The problem with these kind of optimisations is that we win on one and and lose on the other end. 10 messages are surely not unlikely, but I would argue that 50+ concurrent messages are not as likely to be found in applications than <=10.

We probably don't want to make exceptions faster and the common cases slower. Below the full picture that also takes <10 messages into account.

MessageBag::all() Performance Benchmark 
===================================== 

Scalability Test: ================
  1 fields:   0.03 ms →   0.05 ms (-116.2% improvement)
  2 fields:   0.02 ms →   0.03 ms (-29.8% improvement)
  3 fields:   0.03 ms →   0.04 ms (-26.2% improvement)
  4 fields:   0.04 ms →   0.05 ms ( -6.5% improvement)
  5 fields:   0.06 ms →   0.06 ms ( -0.4% improvement)
 10 fields:   0.14 ms →   0.11 ms ( 25.8% improvement)
 50 fields:   1.74 ms →   0.52 ms ( 70.3% improvement)
100 fields:   5.73 ms →   0.98 ms ( 83.0% improvement)
200 fields:  21.07 ms →   1.98 ms ( 90.6% improvement)
500 fields: 130.02 ms →   4.97 ms ( 96.2% improvement)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants