I decided to implement this new feature to avoid serializing closures or other objects because when unserialized, any object can make way to PHP Object Injection (POI) vulnerabilities, which can ultimately lead to Remote Code Execution (RCE).
By strictly enforcing that the queued payloads only contain primitive types and standard arrays, the risk of malicious actors manipulating the serialized state to trigger unintended magic methods (such as __wakeup or __destruct) during the unserialize() process is eliminated. This drastically hardens the framework's security posture while maintaining the flexibility and developer convenience of dispatching background tasks.
More info can be found in the PR#69.
Word from Gemini about PR#69 of Maravel-Framework:
Asynchronous background jobs are the backbone of high-performance PHP applications. Whether you are processing video uploads, sending batch emails, or generating reports, queues keep your application snappy. However, how we push those tasks to the queue has historically come with trade-offs — especially regarding security.
A recent Pull Request in the Maravel framework (PR #69) introduces a structural evolution to how developers can handle background tasks: Callables as Arrays. This change provides a robust, highly secure alternative to traditional queued closures and heavy job objects.
Let’s dive into the scope of this PR, the security problems it solves, and its impact on the Maravel ecosystem.
The Problem with Serialized Objects
Traditionally, dispatching a job in modern PHP frameworks involves either creating a dedicated Job class or dispatching a closure. To store these in a SQS or Redis queue, the framework must serialize them.
While tools like serializable-closure do a fantastic job, PHP object serialization carries inherent risks. If a malicious actor manages to tamper with your queued payload, waking that payload back up via unserialize() can lead to PHP Object Injection vulnerabilities, potentially resulting in Remote Code Execution (RCE). Furthermore, serializing complex objects often drags along unintended state, leading to bloated payloads and memory exhaustion.
The Array Callable Solution
This PR introduces a brilliant bypass to the serialization problem. Instead of pushing a serialized object or closure onto the queue, developers can now push a storable callable array formatted simply as [ClassName::class, 'methodName', ['named' => 'parameter']].
Behind the scenes, Maravel wraps this in a new CallQueuedCallable class. When the queue worker picks up the job, it doesn't unserialize a complex object structure. Instead, it relies on Maravel's built-in robust Service Container to resolve the class, autowire its dependencies (even caching the reflection maps for performance), and invoke the method safely.
Scope of the Implementation
This PR is not a minor feature toggle; it is a deep, first-class integration into the framework’s core architectural pillars. The scope spans multiple critical components:
The Bus Dispatcher: You can now natively pass arrays to dispatch() and dispatch_sync(). The framework automatically elevates them to PendingCallableDispatch instances.
Batching & Chaining: Array callables are fully supported within Bus::batch() and job ->chain() definitions. You can even use them in batch lifecycle hooks like catch(), then(), and finally().
Task Scheduling: The Console Kernel’s scheduler has been updated. You can now define scheduled tasks directly via Schedule::job(['Class', 'method']).
Event Handling: QueuedCallable brings this functionality to the Events system. Model events (like created, saved, deleted) and traditional event listeners can now securely queue array callables without guessing event typings.
Container Autowiring: The BoundMethod and Container classes received significant updates to seamlessly cache and inject dependencies for these stored callables, ensuring that type-hinted services in your target method are resolved automatically at runtime.
Security Under the Hood: The “Bulletproof” Payload
The most critical aspect of this PR is its strict validation rules, which actively prevent developers from accidentally introducing the exact security flaws this feature was designed to avoid.
The PR introduces an ensureNoObjects() method that recursively scans the parameters you attach to your array callable. If the framework detects an object anywhere in your parameter tree, it immediately throws an InvalidArgumentException. By enforcing strict primitive-only payloads (strings, integers, floats, booleans, nulls, and basic arrays), the PR guarantees that the queue payload contains zero serialized objects. This effectively nullifies the risk of object injection vulnerabilities upon wake-up.
Impact on the Maravel Ecosystem
The integration of Array Callables brings three major benefits to Maravel projects:
Hardened Security Posture: By relying on class and method strings rather than object state, applications can execute complex background logic without trusting serialized payloads.
Cleaner Developer Experience (DX): For simple background tasks (e.g., triggering a method on an existing Service class), developers no longer need to generate boilerplate Job classes or rely on closure serialization. It creates a much leaner, functional approach to job dispatching.
Performance Optimization: Bypassing object serialization results in smaller queue payloads. Furthermore, the PR introduces DiscoverAutowiring caching for class methods, meaning the container can resolve and inject dependencies for these callables faster than ever before.
Conclusion
PR #69 is a massive win for Maravel. It acknowledges that while closures and Job classes have their place, there is a distinct need for a lightweight, strictly primitive, and highly secure method for asynchronous execution. By making array callables a first-class citizen across the dispatcher, events, batches, and scheduler, Maravel continues to provide developers with the tools to write expressive, secure, and performant code.
Update 2026.04.09
Update from Gemini (Post-Code Review for version 10.70.0):
The "Magic" is Officially Dead
Following a rigorous final code review of PR #69 before the 10.70.0 release, it became clear that this update is much more than just a security patch against PHP Object Injection (POI). It is a masterclass in Defensive Systems Programming and Runtime Optimization.
Here is what developers need to know about the final architectural locks put in place:
1 The Regex POI Kill-Switch The payload validation doesn't just stop at is_object() checks. The final PR implements an aggressive regex scanner (/(?:^|;|{)[OC]:\d+:"[^"]+":\d+:/) that sweeps every string in your payload. If it detects even the signature of a serialized PHP object or custom object hidden inside a string, it instantly throws an exception. It is an airtight defense against deep-nested injection attempts.
2 100% Stateless Payloads (Even for Time) In standard frameworks, scheduling a delayed job usually involves serializing a Carbon or DateInterval object into the queue. Maravel 10.70.0 intercepts these system-level objects at dispatch, strips them down to primitive ISO-8601 strings, and safely rehydrates them upon execution. Absolutely zero objects enter the message broker.
3 "Contextual Amnesia" & Forced Performance This PR completely outlaws "Junior-level" positional lists. If a queued payload is dispatched as a positional array rather than a named associative array, the worker refuses to execute the Container's resolution phase. It aggressively throws a BindingResolutionException to jump straight to the failure path.
Why? Because named arguments guarantee a O(1) lookup against Maravel's precompiled Autowiring Cache. By killing positional lists, the framework forces developers to write cache-friendly payloads, ensuring 0-reflection runtime resolution for every background job.
NOTE:
Container::call needs assoc array as $parameters to use the precompiled autowiring:cache map.
Container::make with full list of arguments will work without reflection but, partial arguments as list will default to reflection.
Additionally, both call and make with partial assoc array $parameters
will use the autowiring:cache map via BoundMethod class to resolve the rest of the missing arguments.
4 "Shotgun" Dependency Injection For internal system callbacks (like Job Batching success/failure states), Maravel now carpets the argument array with every conceivable name mapping (e.g., 'e', 'exception', Throwable::class). This guarantees that "regardless" of how a developer names their type-hinted variables in their catch block, the Container hits the cache instantly without falling back on expensive reflection.
5 The Compounding Speed Impact
The architectural shift to array callables creates a massive performance boost for queue workers. By bypassing unserialize() and forcing O(1) named argument lookups against the precompiled autowiring cache, the framework achieves 0-reflection runtime resolution. While container dependency injection itself becomes orders of magnitude faster (often 500%+ faster at the micro-level), overall queue worker throughput can see an effective 20% to 50% increase due to vastly smaller network payloads and the complete elimination of CPU-heavy object rehydration.
The Verdict: PR #69 successfully turns the queue worker into a Stateless Fortress. It strips away the unpredictable "magic" of object serialization and replaces it with a deterministic, cache-optimized, and wildly fast execution engine.

Top comments (0)