A PHP 8.5 title pipeline built with |> throws TypeError: strtolower(): Argument #1 ($string) must be of type string, bool given; what went wrong, and how should pipe steps be designed?
answer
- each step's return is the next input
- a validator returned bool, not the title
- no short-circuit on null or false
- validate by returning the value or throwing
- tap steps for logging
basics
~20 sA pipe feeds each step's return value into the next, so a validator returning true or false replaced the title with a bool. Steps must return the value for the next step; validators return their input or throw, since |> never short-circuits.
solid answer
~50 s`|>` has no notion of success or failure: whatever a step **returns** becomes the next step's only argument. The chain `$raw |> trim(...) |> isValidTitle(...) |> strtolower(...)` therefore handed `strtolower()` the `bool` from `isValidTitle()`, and under `strict_types` that is a `TypeError`. The same happens with a `void` step, which passes `null`, or with a function like `preg_replace()` that can return `null` on failure. The fix is a step contract: every step takes one value and **returns the value for the next step**. A validator returns its input unchanged or **throws** a domain exception, which stops the chain because exceptions skip the remaining steps. There is no null-skipping pipe, so optional values need an explicit wrapper. Give steps names, test each as a unary function, and add a pass-through tap step when you need to log an intermediate value.
code
php · 25 lines<?php
declare(strict_types=1);
function isValidTitle(string $t): bool { return $t !== '' && strlen($t) <= 120; }
function assertValidTitle(string $t): string
{
if (!isValidTitle($t)) {
throw new InvalidArgumentException('Invalid article title');
}
return $t;
}
function slugify(string $s): string
{
return trim((string) preg_replace('/[^a-z0-9]+/', '-', strtolower($s)), '-');
}
try {
' Hello World ' |> trim(...) |> isValidTitle(...) |> strtolower(...);
} catch (TypeError $e) {
echo $e->getMessage(), "\n"; // strtolower(): Argument #1 ($string) must be of type string, bool given
}
echo ' Hello World ' |> trim(...) |> assertValidTitle(...) |> slugify(...), "\n"; // hello-worldgo deeper
Recall that each pipe step's return value becomes the next step's input, whatever that value is.
Explain why predicates and void functions break a chain and how guards that return or throw keep it well-typed.
Diagnose a TypeError deep in a chain by finding the step that changed the value's type, and redesign steps as named, typed unary functions with tap steps for logging.
Set conventions for pipe-based code: step contracts, exception types for validation, and when plain sequential code is preferred.
## Reading the error The message `strtolower(): Argument #1 ($string) must be of type string, bool given` says that the step **before** `strtolower(...)` returned a `bool`. With the pipe operator, the value flowing between steps is exactly the previous step's return value; there is no separate channel for "this step passed" or "this step failed". A typical culprit in a pipeline that normalises, validates and slugifies article titles: ```php $slug = $raw |> trim(...) |> isValidTitle(...) // returns bool |> strtolower(...) // receives true or false |> slugify(...); ``` `isValidTitle()` was written as a predicate for `if` statements, and dropping it into the chain replaced the title with its verdict. Under `declare(strict_types=1)` the next typed step throws `TypeError`. Without strict types a bool would be coerced to a string (`'1'` or `''`) for a scalar parameter, which is worse: the chain keeps going with garbage. ## The step contract A chain stays safe when every step obeys one rule: **take one value, return the value the next step needs**. Four kinds of step break it: | Step | What flows on | Fix | |---|---|---| | predicate (`isValidTitle()` returns `bool`) | `true` or `false` | a guard that returns the input or throws | | `void` function (`logTitle()`) | `null` | a tap wrapper that calls it and returns the input | | function returning `null` or `false` on failure | `null` / `false` | a wrapper that converts failure into an exception | | function needing more arguments | `ArgumentCountError` | a parenthesized arrow function supplying them | ## Guards instead of predicates Rewrite validation as a **guard**: it receives the title, returns it unchanged if valid and **throws** otherwise. ```php function assertValidTitle(string $title): string { if ($title === '' || strlen($title) > 120) { throw new InvalidArgumentException('Invalid article title'); } return $title; } ``` Exceptions are the only built-in way to stop a pipe early: when a step throws, the remaining steps are not evaluated and the exception propagates to the caller. The operator has no equivalent of the nullsafe `?->` for "skip the rest if null", so if `null` is a legitimate outcome, handle it explicitly before or around the chain. ## Tap steps for logging and debugging To inspect a value in the middle of a chain without changing it, insert a **tap**: a step that performs a side effect and returns its input. ```php $tap = static function (string $label): Closure { return static function (mixed $value) use ($label): mixed { error_log($label . ': ' . var_export($value, true)); return $value; }; }; $slug = $raw |> trim(...) |> $tap('trimmed') |> assertValidTitle(...) |> slugify(...); ``` `$tap('trimmed')` evaluates to a `Closure`, which is a valid right-hand side. ## Finding the offending step When a chain fails with a type error, locate the step that changed the value's type: - the error names the function that **received** the wrong type, so the culprit is the step immediately before it; - check that step's declared return type, and if it has none, what it returns on its failure path; - look for predicates (`is...()`, `has...()`, `validate...()` returning `bool`), `void` loggers, and built-ins that return `false` or `null` on failure; - insert a tap step just before the failing call to log the value and its type with `get_debug_type()`; - add return types to every step, so the next failure is reported at the step that produced the wrong value. ## Design guidelines 1. **Name the steps.** `assertValidTitle(...)` and `slugify(...)` read better and test better than long inline arrow functions. 2. **Test each step alone** as a unary function: input value in, output value out, and the exception for invalid input. 3. **Keep types honest.** Declare parameter and return types on every step and enable `strict_types`, so a contract break fails at the first wrong step, not three steps later. 4. **Wrap multi-argument functions once.** If `str_replace(' ', '-', ...)` appears in several chains, give it a name, `dashify(...)`, rather than repeating the arrow function. 5. **Know when to stop.** If more than half the steps need wrappers, or the chain needs branching, a few named temporary variables are clearer than a pipe. 6. **Remember by-reference functions cannot be steps.** Functions like `sort()` must be wrapped in a step that sorts a local copy and returns it.
- How do you stop a PHP 8.5 pipe chain early when a step finds invalid input?Throw an exception from that step. When a step throws, PHP does not evaluate the remaining steps and the exception propagates to the surrounding `try` or caller. There is no built-in short-circuit on `null` or `false`, so returning a sentinel just feeds it into the next step.
- Why can a logging function declared void not be a PHP pipe step as-is?A `void` function returns `null`, and the pipe passes that `null` to the next step, discarding the value being processed. Wrap it in a tap step that calls the logger and returns its input unchanged, such as `(function (string $t): string { logTitle($t); return $t; })`.
saying these in an interview costs you the question
- A pipe step returning false stops the rest of the chain.
- A void step leaves the piped value unchanged for the next step.
- Validation functions returning bool are the natural pipe steps.
- The pipe skips remaining steps automatically when a value becomes null.
- Without strict_types a wrong-typed step value always fails loudly.