In manufacturing, Poka-Yoke is a way of designing a process so that common mistakes are prevented or detected before they cause harm. An asymmetric fixture that accepts a component in only one orientation is a familiar example.
I work with software in operational environments, and I kept returning to a related question: what does mistake-proofing look like at the boundary of a PHP business operation?
Not every mistake needs a new library. Most of the time, an ordinary if statement is the right answer. But when an operation depends on several independent conditions, it can become useful to make those checks explicit, collect the reasons for rejection, and keep the action itself behind a single guard.
That experiment became Novora Poka-Yoke, a small, framework-independent PHP 8.4+ package.
The problem is not just validation
Consider a shipping operation. Before dispatching, we may need to know that payment has been confirmed, a shipping address exists, and sufficient stock is available.
We can write these conditions inline. There is nothing inherently wrong with that. But a sequence of early returns often tells the user only about the first missing condition, while the same requirements can end up scattered across controllers, command handlers, and services.
For some workflows, I wanted an explicit boundary:
- Evaluate application-defined preconditions.
- Collect understandable, machine-readable reasons if any fail.
- Invoke the operation callback only when every check passes.
The important distinction is that this is operation precondition checking, not a general data-validation framework, a state machine, or a transaction manager.
A minimal API
Install the stable package with Composer:
composer require "novora/poka-yoke:^1.0"
A rule implements one method and returns either null (allowed) or a Violation:
<?php
declare(strict_types=1);
use Novora\PokaYoke\Rule;
use Novora\PokaYoke\Violation;
final class PaymentConfirmed implements Rule
{
public function check(mixed $context): ?Violation
{
if (!$context instanceof Order) {
throw new InvalidArgumentException('Expected an Order.');
}
return $context->isPaid()
? null
: new Violation('payment.not_confirmed', 'Payment must be confirmed.');
}
}
Order is a placeholder for an entity or DTO from your application; the package does not define an Order class.
We can combine multiple rules without hiding which ones ran:
use Novora\PokaYoke\Guard;
$guard = new Guard();
$result = $guard->check($order, [
new PaymentConfirmed(),
new ShippingAddressPresent(),
new InventoryAvailable(),
]);
if (!$result->allowed()) {
foreach ($result->violations as $violation) {
// Application decides how to present or translate the message.
echo $violation->code . ': ' . $violation->message . PHP_EOL;
}
}
The two additional rule classes above are application-defined examples too. By default, check() collects violations in rule order. For cases where evaluating later rules would be unnecessary, it supports stopOnFirstViolation: true.
Put the action behind the guard
A separate check followed by an action is useful for displaying diagnostics, but the package also offers a compact operation boundary:
use Novora\PokaYoke\Guard;
use Novora\PokaYoke\OperationBlocked;
try {
$shipmentId = (new Guard())->execute(
$order,
[new PaymentConfirmed(), new ShippingAddressPresent(), new InventoryAvailable()],
fn () => $shippingService->dispatch($order),
);
} catch (OperationBlocked $exception) {
foreach ($exception->result->violations as $violation) {
// Map stable codes to your API, logs, or UI.
}
}
The callback is not invoked if any rule rejects the operation. execute() returns the callback's value if all rules pass. Exceptions thrown by a rule or the callback are not swallowed.
One design decision here is deliberate: a rule expresses why the operation cannot proceed, while the application remains responsible for domain decisions and persistence.
Where this pattern is useful
The same narrow concept appears in quite different applications:
- Shipping: payment, address, and stock checks before dispatch.
- Manufacturing: verifying a scanned part and a completed quality step before recording the next operation. Real machinery safety must remain in dedicated safety systems.
- Bookings: checking consent and requested capacity before submitting a booking.
These are not promises of safe concurrent inventory or seat allocation. They are examples of where explicit preconditions make intent easier to understand.
The repository includes a runnable order example and a discussion of practical scenarios and alternatives.
What Poka-Yoke cannot guarantee here
This is the part I consider especially important.
A successful precondition check does not guarantee the state will still be valid when the callback commits its changes. Inventory, account balance, or booking capacity can change concurrently. The library does not provide database transactions, row locking, idempotency, distributed coordination, or rollback.
If correctness depends on the check and write being atomic, the application must enforce that with suitable database constraints, transactions, locks, or other domain-specific mechanisms. Likewise, rule implementations should avoid side effects, but the library cannot prevent user-supplied PHP code from mutating state.
For data shape and field constraints, an established validator is probably a better choice. For state transitions, a workflow or state-machine component is usually a better abstraction. And for one or two straightforward preconditions, plain PHP conditionals may be simpler.
Testing the contract
The initial v1.0.0 release was validated with 15 PHPUnit tests (27 assertions), PHPStan level 8, Composer validation, and CI on PHP 8.4 and PHP 8.5. I also installed the published package from Packagist in a separate PHP project and exercised its API.
Tests cover failing preconditions, the order of violations, fail-fast checking, invalid rule values, propagation of exceptions, falsy contexts, and the guarantee that a rejected operation does not invoke its callback.
Those checks establish the package's documented behavior. They are not proof that every consuming application's business rules are correct.
A modest conclusion
I do not think Poka-Yoke in PHP requires a large framework. Sometimes it is simply a useful design habit: make the conditions for a consequential operation explicit, and stop before the side effect when those conditions are not met.
Novora Poka-Yoke packages one small expression of that idea. It might be useful when you want structured rejection reasons and a consistent execution boundary; it is not intended to replace a validator, workflow engine, or carefully designed domain model.
If you have solved similar problems differently, I would be interested to learn what worked for you.
Project links: GitHub · Packagist · v1.0.0 release
Top comments (0)