Closed and open are obvious: pass through, or fail fast. The third state is what makes it a breaker rather than a switch — half-open lets exactly one request through to find out whether the dependency has recovered.
// closed → calls pass through, failures counted
// open → calls fail immediately, no request made
// half-open → after a cooldown, ONE call is allowed
// success → closed; failure → open again
if ($this->state === 'open' && $this->openedAt + $this->cooldown < time()) {
$this->state = 'half-open';
}
Without half-open, something has to close the breaker and the only candidates are a timer, which reopens the flood, or a human. The subtlety is that half-open must admit one request and not a burst — otherwise the recovering service receives the whole backlog and fails again, which is the thundering herd the breaker was protecting it from.