Real-world data has gaps: empty fields, nulls, records that don't exist. This page covers what the library considers "missing", the methods that care about missing values, and other conditional/error checking features.
Contents:
- What "Missing" Means
- Fallbacks -
or() - Targeted Replacements -
ifNull(),ifZero(),ifEquals(),ifTrue(),set() - Run Conditionals Before Formatting
- True/False Checks -
isEmpty(),isNotEmpty(),isMissing(),isNull() - Requiring a Value -
or404(),orDie(),orThrow(),orRedirect() - Putting It Together
All the methods on this page react to missing values: or() fills them
in, prepend()
won't put a label on one, and the guards stop the page
rather than run without one. Missing simply means there is no value: null
or an empty string "", and nothing else. Zero is not missing. False is
not missing:
| Value | or("fallback") |
isEmpty() |
isMissing() |
|---|---|---|---|
null |
"fallback" |
true | true |
"" |
"fallback" |
true | true |
0 |
0 |
true | false |
"0" |
"0" |
true | false |
false |
false |
true | false |
"hello" |
"hello" |
false | false |
Both 0 and "0" come through or() unchanged: they are real
values, not missing ones. PHP's empty() considers them empty, but
SmartString's conditional methods do not treat them as missing. A price of
zero (what your template shows as $0.00) is real data, not a missing value.
Replaces missing values with $fallback. This is the most used conditional
method; reach for it whenever a field might be blank:
echo SmartString::new('')->or('N/A'); // N/A
echo SmartString::new(null)->or('Unknown'); // Unknown
echo SmartString::new(0)->or('N/A'); // 0 (zero counts as present)
echo "Hello, {$user->name->or('Guest')}!"; // reads naturally in templatesBecause failed dates, numbers, and math all produce null (see
Text and Formatting), one or() at the end of a
chain covers every failure in it:
echo $article->date->dateFormat('M j, Y')->or('Date not set');
echo $order->total->add($order->shipping)->numberFormat(2)->or('n/a');Each replaces the value when one specific condition matches, for finer
control than or().
The ifNull() method replaces null only, leaving "" alone. Its everyday
job is turning null into a real value before math:
echo SmartString::new(null)->ifNull(0)->add(50); // 50 (null would have made it null)The ifZero() method replaces numeric zero (0, 0.0, "0", "0.00",
"-0"). Non-numeric values never match, so text and missing values pass
through untouched:
echo $account->balance->ifZero('No balance'); // "No balance" when 0
echo SmartString::new(null)->ifZero('No balance'); // "" (null is not zero)The ifEquals() method replaces the value when it matches the one you
specify. Matching is loose (==), so the string "5" matches the number
5 - handy, since numbers often arrive as text, like form input or numbers
stored in text fields:
// ifEquals($match, $newValue)
// legacy zero-dates aren't real dates: null them, then fall back
echo $record->date->ifEquals('0000-00-00', null)->dateFormat('M j, Y')->or('Not set');
echo $plan->maxUsers->ifEquals(-1, 'Unlimited'); // fires on -1 and "-1"For null, use ifNull() instead; PHP treats null == 0 and null == ""
as true, so ifEquals(null) would match those too.
The ifTrue() method replaces the value when a condition you computed is
truthy. The condition is a plain value, not a callback, and it replaces the
value only; the rest of the chain still runs:
// ifTrue($condition, $newValue)
$qty = SmartString::new(150);
echo $qty->ifTrue($qty->int() > 99, '99+'); // 99+None of these methods take an else value; when the condition doesn't match,
the original value passes through. For a different value in each branch,
write a ternary or match - and set() (below) if the expression has to
sit inside an interpolated string.
set() replaces the value unconditionally, putting the result of any PHP
expression into a chain. It works best in heredocs and double-quoted strings:
{$...} interpolation accepts property and method access but not operators,
so a ternary or match written directly inside {$...} is a parse error.
Inside a method call's arguments it parses fine:
// set($newValue)
echo "Gift wrap: {$order->giftWrap->set($order->giftWrap->bool() ? 'Yes' : 'No')}";
// Gift wrap: Yes
echo <<<__HTML__
<span class="badge">{$order->status->set(match($order->status->string()) {
'P' => 'Pending',
'S' => 'Shipped',
default => 'Unknown',
})}</span>
__HTML__;
// <span class="badge">Shipped</span>The result re-enters the chain, so it encodes on output and can take a
further conditional or formatting calls. That is what set() adds
over computing the label first: a precomputed $label is a plain string
and echoes unencoded, while a set() result stays HTML-safe.
When the expression interpolates a SmartString, use ->value() inside
it to avoid double-encoding.
Formatted output is display text, not a number: once numberFormat(2) has
produced "1,234.00" or percent() has produced "50%", math methods and
zero checks can no longer read it. Anything that needs to read the number
(math, percentOf(), ifZero()) goes before formatting in the chain:
// WRONG - "$0.00" is not numeric, so ifZero never fires; prints $0.00
echo $price->numberFormat(2)->prepend('$')->ifZero('Free!');When the replacement is display text like "Free!", it can't simply move
before the formatter either: numberFormat() returns null for non-numeric
values, so "Free!" would become null and display nothing. Match the
finished text instead with ifEquals(), using exactly what your format
produces:
// RIGHT - match the formatted text
echo $price->numberFormat(2)->prepend('$')->ifEquals('$0.00', 'Free!');
// $0 → Free!
// $19.99 → $19.99The one formatter that needs a zero rule often enough gets it as a
parameter: percent(2, ifZero: 'N/A') (see
Percentages).
Placement also changes what or() means. Before formatting, it supplies a
fallback number; after formatting it supplies fallback display text:
$value = SmartString::new(null);
echo $value->or(0)->numberFormat(2); // 0.00 (fallback number, then formatted)
echo $value->numberFormat(2)->or('n/a'); // n/a (format failed, then display text)These return a plain bool for use in if statements. You need them
because a SmartString is an object, and PHP objects are always truthy:
testing the object itself always gives the same answer, no matter what
value is inside:
// WRONG - an object is always truthy; this branch runs even when bio is null
if ($user->bio) {
echo "<h2>About</h2><p>{$user->bio->nl2br()}</p>";
}
// RIGHT - ask the object about its value
if ($user->bio->isNotEmpty()) {
echo "<h2>About</h2><p>{$user->bio->nl2br()}</p>";
}The same applies to empty($user->bio) and !$user->bio, which are always
false on an object; use the check methods instead, or ->value() to test the
raw value with plain PHP. The difference between the checks is what happens
to zero:
$balance = SmartString::new(0);
$balance->isEmpty(); // true (PHP empty() rules: 0, "0", false, "", null)
$balance->isMissing(); // false (zero is a real value; matches or() and the guards)
$balance->isNull(); // false (null only)Where or() quietly substitutes a fallback, the guards stop the page. Use
them for values that must exist, like a record ID from the URL. If the value
is missing they act; otherwise they do nothing and return the same object,
so the chain continues. Zero counts as present.
$article = DB::selectOne('articles', ['num' => $articleNum]);
$article->num->or404("Article not found"); // 404 header + message, then exits
$article->num->orDie("Article not found"); // prints message, then exits
$article->num->orThrow("Article not found"); // throws RuntimeException
$article->num->orRedirect("/articles/"); // 302 redirect, then exitsWhen the query returns no row, $article is an empty
SmartArray record and
->num reads as missing (same missing rules), so the guard fires.
The messages are HTML-encoded automatically. They often interpolate user
input (->orDie("Bad id: $id")) and may be echoed into a page, so the
guards encode rather than trusting every error handler to do it.
How each guard differs:
or404($text)sends the 404 status and a minimal HTML error page. The default message is "The requested URL was not found on this server." The page is deliberately plain; to show your own, see Using Your Site's 404 Page.orDie($text)exits with code 1, so CLI scripts and cron jobs see a failure instead of success.orThrow($text)throws aRuntimeExceptionfor your error handler. Handlers that want plain text (CLI, logs) can decode the message withhtmlspecialchars_decode($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5).orRedirect($url)checksheaders_sent()and a blank$url(null or"") immediately and throws if either fails, even when the value is present, so a misplaced redirect fails on the first request instead of only when a value goes missing. Pass a URL you control: a URL taken from user input can send visitors to another site.
or404() prints a plain built-in page. To show your site's own 404 page
instead, set a handler once in your init file:
use Itools\SmartArray\SmartArray;
use Itools\SmartString\SmartString;
$show404 = function (?string $text): void {
$message = SmartString::new($text ?? "We couldn't find that page."); // encodes itself when echoed
include __DIR__ . '/404.php'; // your page template, which echoes $message
};
SmartString::set404Handler($show404);
SmartArray::set404Handler($show404);SmartString and SmartArray
each keep their own handler, so set both. Field guards like
$article->num->or404(), and ->first()->or404() with no match, use
SmartString's; or404() on a whole result uses SmartArray's. Without
SmartArray, leave out its two lines.
Guards pass the value through untouched, so one chain can distinguish "no row matched" from "row found, but the column is empty" and report which one happened:
$memberId = DB::selectOne('users', ['email' => $email])
->orThrow("No user found for $email")
->memberId
->orThrow("User $email has no member ID")
->int();The first orThrow() fires when the query returns no row; the second fires
when the row exists but the column is null or "". (Row-level guards like the
first one are SmartArray methods; same names, same missing rules.)
A record detail page: guard first, then fallbacks for every optional field:
$product = SmartArrayHtml::new([
'name' => 'Deluxe Widget',
'price' => 0,
'summary' => '',
'updated' => '2026-09-10',
]);
$product->name->or404("Product not found"); // guard: required field
echo <<<__HTML__
<h1>$product->name</h1>
<p>Price: {$product->price->numberFormat(2)->prepend('$')->ifEquals('$0.00', 'Free!')}</p>
<p>{$product->summary->textOnly()->maxChars(120)->or('No description yet.')}</p>
<p>Updated: {$product->updated->dateFormat('M j, Y')->or('never')}</p>
__HTML__;Output:
<h1>Deluxe Widget</h1>
<p>Price: Free!</p>
<p>No description yet.</p>
<p>Updated: Sep 10, 2026</p>
A passing guard leaves no trace in the output; a missing name would have
ended the page with a 404 before any of it printed. The empty summary stays
"" through textOnly()->maxChars(), which is what lets the trailing or()
fire.
← Documentation Index | ← Prev: Text and Formatting | Next: Common Patterns →