In PHP, why can a helper function called inside a generator not yield on the generator's behalf, and what do Fibers change about that?
answer
- yield pauses only its own function frame
- yield in a helper makes the helper a generator
- every layer returns Generator and uses yield from
- Fiber::suspend() from any call depth, PHP 8.1
- generators stay iterable; fibers need a driver
basics
~20 sA yield pauses only the function it is written in, so a yielding helper becomes a separate generator that every caller must yield from. A PHP 8.1 Fiber has its own call stack, so Fiber::suspend() pauses it from any depth.
solid answer
~50 sA generator is **stackless**: `yield` suspends only the frame of the function that contains it. If a helper called from the generator contains `yield`, that helper simply becomes a generator function too; calling it returns a `Generator` and yields nothing to the outer consumer. To pause from deep inside, every function in the chain must be a generator, declare a `Generator`-compatible return type and be called with `yield from`. A callback given to `array_map()` cannot yield for its caller at all. A `Fiber`, available since PHP 8.1, runs a callable on its own call stack, so `Fiber::suspend()` pauses the whole fiber from any depth, including inside such callbacks, without changing any function's signature. Neither runs in parallel. Generators remain the better tool for lazy sequences, because they are directly iterable with keys; a fiber needs explicit `start()` and `resume()` calls from a driver.
code
php · 11 lines<?php
function outer(): Generator
{
$wrapped = array_map(function (int $x) { yield $x; }, [1, 2]);
var_dump($wrapped[0] instanceof Generator); // bool(true)
yield 'only this';
}
foreach (outer() as $v) {
echo $v, "\n"; // only this
}go deeper
Recall that yield pauses only the function it is written in, and that PHP 8.1 added Fibers, which can pause from nested calls.
Explain why a yielding helper becomes its own generator, why every layer then needs yield from and a Generator return type, and why callbacks to built-ins cannot yield outward.
Choose generators for lazy iterable sequences and Fibers for suspending deep I/O-bound code, and explain that neither provides parallelism or speeds up CPU-bound work.
Weigh the cost of spreading Generator return types through an API against adopting a Fiber-based runtime, including debugging, library compatibility and team familiarity.
## What yield can and cannot pause In PHP, `yield` suspends exactly one function: the one whose body contains it. The paused state of a `Generator` is that single frame, its locals and its position. The PHP manual describes generators as **stack-less** for this reason. That has a direct consequence for helper functions. Suppose a generator walks rows and wants to pause inside a helper that waits for something: ```php function process(): Generator { foreach (rows() as $row) { handle($row); // wants to pause inside handle() } } ``` If `handle()` contains `yield`, it does not pause `process()`. It becomes a generator function itself: calling it returns a new `Generator`, and unless someone iterates that object, the body of `handle()` never runs. To make the pause reach the consumer, `process()` must write `yield from handle($row);`, `handle()` must declare a return type compatible with `Generator`, and the same change repeats for every function between the pause point and the outermost generator. ## The places generators cannot reach The rule is strictest where PHP itself calls your code: - **Callbacks passed to built-ins.** A closure given to `array_map()`, `usort()` or `array_walk()` that contains `yield` is a generator closure. `array_map()` receives one `Generator` object per element and returns an array of them; nothing is yielded to the outer generator. - **Methods called by the engine**, such as an `Iterator` implementation's `current()` driven by `foreach`, or a destructor. - **Library code you do not own**, which you cannot convert to generators and `yield from`. ## What Fibers change PHP 8.1 added the `Fiber` class. A fiber wraps a callable and runs it on **its own call stack**: 1. `$fiber = new Fiber($callable);` creates it. 2. `$fiber->start(...$args)` runs the callable until it finishes or something inside calls `Fiber::suspend($value)`. 3. `Fiber::suspend()` may be called at **any depth** in that stack, including inside an `array_map()` callback or an iterator method, and the whole stack pauses. `start()` or `resume()` then returns the suspended value to the driver. 4. `$fiber->resume($value)` continues the fiber, and `Fiber::suspend()` returns `$value` inside it; `$fiber->throw($e)` resumes it by throwing. No function in between changes its signature. Calling `Fiber::suspend()` outside any fiber throws `FiberError`. ## Side by side | Aspect | Generator | Fiber (PHP 8.1+) | |---|---|---| | What pauses | the one function containing `yield` | the fiber's entire call stack | | Pause from nested calls | only if every layer is a generator using `yield from` | yes, from any depth | | Signatures | functions that pause must return `Generator` | unchanged | | Consumption | `foreach`, keys, values | explicit `start()` / `resume()` driver | | Values out, values in | `yield` / `send()`, plus `getReturn()` | `Fiber::suspend()` / `resume()`, plus `getReturn()` | | Parallelism | none: runs in the calling thread | none: cooperative, one thread | ## Choosing between them They solve different problems even though both pause code: - **Lazy sequences** (lines of a file, rows of a query, a range too large for an array) are generator territory. A generator plugs into `foreach`, carries keys, composes with `yield from`, and its intent is obvious from the `Generator` return type. - **Suspending deep application code** while waiting for I/O is what fibers were added for. Event-loop libraries use them so that ordinary-looking functions can wait without every layer becoming a generator. - **They combine.** A generator can run inside a fiber, and a fiber can suspend while a generator is on its stack. Neither gives parallelism: both hand control back and forth within one thread. Code that is CPU-bound runs no faster under either.
- What happens if code calls Fiber::suspend() when it is not running inside a fiber?It throws `FiberError` with the message *Cannot suspend outside of a fiber*. Only code that runs somewhere below a fiber's `start()` or `resume()` can suspend; a plain request script has no fiber to pause.
- Does converting a CPU-heavy loop from a generator to a Fiber make it faster?No. Both are cooperative and run in the calling thread; a fiber only changes where suspension may happen. CPU-bound work runs at the same speed, and gains appear only when the paused time is spent waiting on I/O that a scheduler can overlap.
saying these in an interview costs you the question
- A helper function can yield values on behalf of the generator that called it.
- Fibers run their callables in parallel on separate threads.
- Fibers replace generators, so new code should never use yield.
- Fiber::suspend() may only be called directly inside the callable passed to new Fiber().
- A closure passed to array_map() can yield values to the surrounding generator.