This is a consolidated reference for AI coding assistants. It contains everything needed to write correct SmartArray code in a single file, and covers SmartArray 3.0. For human-friendly docs with tutorials and explanations, see Getting Started on GitHub (this is the only docs file shipped in the Composer package, so links here are absolute).
Contents:
- What is SmartArray
- Class Hierarchy and Type Hints
- Creating Collections
- Reading and Writing Fields
- Missing Keys and SmartNull
- Iteration and Keys
- Mode Conversion and Plain Arrays
- Single Elements - first(), last(), at()
- Collection Checks - count(), isEmpty(), isNotEmpty(), contains()
- Row Position - isFirst(), isLast(), position()
- Filtering and Sorting - where(), whereNot(), whereInList(), filter(), sort(), sortBy(), unique()
- Transforming and Grouping - column(), columnAt(), indexBy(), groupBy(), keys(), values(), map(), merge(), implode()
- Guards - or404(), orDie(), orThrow(), orRedirect()
- Database Metadata - mysqli(), load()
- Debugging - debug()
- Errors and Exceptions
- Deprecated Names
- Gotchas Quick Reference
SmartArray wraps PHP arrays (usually database rows) in chainable collection methods. Two concrete classes share one API; the ONLY difference is what a field read returns:
SmartArrayHtml(HTML mode): fields returnSmartStringobjects that HTML-encode in every string context. Use whenever output is a web page. ZenDB and CMS Builder query results arrive in this mode.SmartArray(raw mode): fields return plain PHP values in their original types. Use for JSON, CSV, email, CLI, and data processing.
use Itools\SmartArray\SmartArray;
use Itools\SmartArray\SmartArrayHtml;
$users = SmartArrayHtml::new([
['name' => "Jean O'Brien", 'city' => 'Vancouver'],
['name' => 'Tom & Jerry Inc', 'city' => 'Ottawa'],
]);
foreach ($users as $user) {
echo "<li>$user->name from $user->city</li>\n"; // <li>Jean O'Brien from Vancouver</li>
}
echo $users->where('city', 'Ottawa')->first()->name; // Tom & Jerry IncKey definitions used throughout:
- row = a nested SmartArray inside a parent collection (created
automatically for nested input arrays). Rows know their
position()(1-based); top-level and derived collections report position 0. - field = a scalar element read off a collection:
SmartStringin HTML mode, plain PHP value in raw mode. Data is stored raw and wrapped on access, never modified. - SmartNull = chainable placeholder returned for missing keys and empty
lookups. Echoes as
"", counts as 0, iterates as nothing; SmartArray and SmartString methods on it keep working. - Collection methods behave identically in both modes: callbacks, matching, and sorting always operate on original raw values. Methods returning a collection return it in the calling object's mode.
- Transformation methods return NEW collections; the original is never modified.
SmartBase (interface) every collection type plus SmartNull (not SmartString)
├── SmartArrayBase abstract base - type-hint this to accept either mode
│ ├── SmartArray raw mode
│ └── SmartArrayHtml HTML mode
└── SmartNull returned for missing keys and empty lookups
SmartArrayHtml is NOT instanceof SmartArray; they are siblings. Hint
SmartArrayBase for functions accepting either mode, SmartBase to also
accept SmartNull.
SmartArrayHtml::new(array $array = [], array $properties = []): SmartArrayHtml
SmartArray::new(array $array = [], array $properties = []): SmartArray- Nested arrays become child rows (recursively); scalars and null store
as-is; Smart values unwrap; other objects/resources throw
InvalidArgumentException. $propertiesis for database layers:['mysqli' => [...metadata...], 'loadHandler' => callable]. Normal code omits it.new SmartArray($data)works too;::new()exists becausenew SmartArray($data)->method()is a syntax error before PHP 8.4.
$row->name // property syntax is canonical
$row->{'users.id'} // braces for keys property syntax can't type (dots, dashes, numeric)
$row->{0} // numeric keys
$row->name = 'Jean'; // writes use the same syntax
$row->{'sort-order'} = 5;- HTML mode wraps scalar reads in
SmartString; nested rows come back as collections (never wrapped). Raw mode returns everything as-is. - Writes unwrap Smart values (SmartString stores its raw value, SmartNull stores null, SmartArray children convert to the target's mode).
- SmartString fields used inside braces stringify to their HTML-ENCODED
output. Digits are unaffected, so numeric id keys work as-is; for text
keys pass the original:
$map->{$field->value()}. isset($row->key)/empty($row->key)/$row->key ?? $defaultuse plain-array semantics: stored NULL reads as missing (so??fires on missing keys AND stored NULLs) and none of them ever warn.- The
??fallback is a plain value, output with NO encoding: keep??fallbacks to literals. For display fallbacks use the field'sor(), which also covers""and keeps the result encoded. - Empty-string keys exist but property syntax can't reach them; only the
deprecated
get('')/set('')can (and$arr[null]reads key''). unset($row->key)removes a key.
Reading a key that doesn't exist returns a SmartNull. Warning behavior
depends on WHERE you read (changed in 3.0):
- Rows inside a result set (position 1+): echoes
Warning: keyname is undefined in file.php:LINEand triggersE_USER_WARNING(caller's file:line, key HTML-encoded, plus a wrap-methods-in-braces hint when the key matches a method name). Row keys are column names, so a miss is treated as a typo. - Everywhere else (top-level collections,
indexBy()/column()lookup maps, standalone arrays, empty collections): silent. A miss is a normal no-match, so fallbacks chain cleanly:$authorById->{$id}->or('Unknown').
Separate rule for method ARGUMENTS: a field name passed to where(),
whereNot(), whereInList(), sortBy(), indexBy(), groupBy(), or
column() that doesn't exist in the first row warns from ANY nested
collection, result set or not, with the format
funcName(): 'field' doesn't exist (caller's file:line appended).
SmartNull behavior: echo → ""; value() → null; count() → 0;
foreach iterates zero times; toArray() → []; json_encode() → null;
SmartArray methods return empty results; SmartString methods (HTML mode)
behave as on null, except transforms return the same SmartNull (chain stays
missing, accepts value or collection endings) and map() skips its callback
(a NULL value in an existing key still runs it); guards (or404() etc.)
FIRE (empty = missing); one-argument set($value) produces that value in
HTML mode only (raw mode throws like any write); all other writes throw
RuntimeException ("Cannot set values on SmartNull").
It carries the source's mysqli metadata and load handler.
foreach ($collection as $key => $value): values follow the mode (rows as collections, scalars as fields); keys are always raw plain values, never encoded, in both modes. When keys came from user data (groupBy()on a user-entered field) and get echoed, iterate$collection->keys()instead - keys() returns them as fields that encode on output.- Iteration order is insertion/array order. Nested rows yield as-is.
foreachover aSmartStringfield throws; overSmartNullyields nothing.
| Method | Returns |
|---|---|
asHtml(): SmartArrayHtml |
Collection in HTML mode; same object if already HTML, else a new collection (original unchanged). Rows keep position metadata |
asRaw(): SmartArray |
Collection in raw mode; same object if already raw |
toArray(): array |
Plain nested PHP array, original values, both modes |
SmartArray::getRawValue(mixed $value) (static) |
SmartString → value, SmartArray → array, SmartNull → null, scalar/null/array pass through (arrays unwrapped recursively); other objects throw InvalidArgumentException |
json_encode($collection)(JsonSerializable) emits RAW original values in both modes (JSON is a data format; HTML encoding is output-only). Malformed UTF-8 in keys or values becomes � (U+FFFD) instead of returning false.(array)$collectionexposes internal object properties (PHP has no cast hook) - never use it; usetoArray(). Spread[...$collection]works for flat lists (top level only) but keeps element mode: SmartString objects in HTML mode, plain values in raw mode. For plain original values, usetoArray().
first(): row|field|SmartNull // first element
last(): row|field|SmartNull // last element
at(int|SmartString|SmartNull $index): row|field|SmartNull // by position, ignoring keys: 0 first, -1 lastAll three return SmartNull silently when there is no such element
(empty collection, out-of-range index, SmartNull index).
count(): int // also works via count($collection) (Countable)
isEmpty(): bool // no elements
isNotEmpty(): bool // any elements
contains(mixed $value): bool // any element matches $value (where() rules; Smart args unwrap)Field-level checks (isMissing(), isEmpty(), or(), ...) are SmartString
methods, available on fields in HTML mode; see the
SmartString AI reference.
isFirst(): bool // true for the first row in its parent collection
isLast(): bool // true for the last row
position(): int // 1-based position in parent; 0 on top-level/derived collectionsComputed on first call and then kept; preserved through asHtml()/asRaw().
Rows in a DERIVED collection (a where() result) get fresh positions in
the new collection.
All return a new collection. Methods marked "Rows only" work on the rows
(elements that are arrays) and ignore other elements (scalars/null); a
non-empty array with no rows throws InvalidArgumentException
(empty arrays pass). Methods marked "Flat only" throw on nested input.
| Method | Behavior |
|---|---|
where(string $field, mixed $value = null): static |
Rows only. Keeps rows where $field matches $value: strings match as exact text ('0e12' never matches '0e99'), numbers match numerically in either direction ('1' matches 1, 1 matches '1.00'), null matches only null (SQL IS NULL), bools compare as 1/0 on either side. Smart args unwrap. Rows without the field are dropped. Chain calls for AND. Warns when $field is missing from the first row. Single-arg where($field) keeps rows where the field is non-empty (PHP empty() rule: NULL, false, 0, "0", "", missing are empty). NOTE: where($f) and where($f, null) differ - the latter matches only stored NULLs |
whereNot(string $field, mixed $value = null): static |
Rows only. Drops rows where $field matches $value (same matching rules as where()); rows WITHOUT the field are kept. Single-arg whereNot($field) keeps rows where the field is empty or missing (exact complement of where($field)) |
whereInList(string $field, mixed $value): static |
Rows only. Keeps rows where tab-separated $field contains $value as a whole value ("\tmenu\tfooter\t" format, CMS Builder checkbox/multi-select fields) or equals it as a plain single value. Never substring matching |
filter(?callable $callback = null): static |
Both shapes. Callback receives raw ($value, $key), keeps on true. No callback: removes falsy ("", "0", 0, null, false). Keys preserved like array_filter() - chain values() for a clean JSON array |
sort(int $flags = SORT_REGULAR): static |
Flat only. Sorts ascending by value, renumbers keys. $flags choose comparison only; SORT_ASC/SORT_DESC throw InvalidArgumentException (sort descending in SQL) |
sortBy(string $field, int $flags = SORT_REGULAR): static |
Rows only. Ascending by $field; rows missing the field sort first (like MySQL ORDER BY) and are kept unchanged. Numeric row keys renumber, string keys preserved. SORT_NATURAL for human number order; SORT_ASC/SORT_DESC throw |
unique(): static |
Flat only. Removes duplicates keeping the first, keys preserved; compares as strings (array_unique()), so 1 and '1' are duplicates |
"Rows only" and "Flat only" carry the same contract as in Filtering and Sorting above.
| Method | Behavior |
|---|---|
column(int|string|null $columnKey, int|string|null $indexKey = null): static |
Rows only. Like array_column(): one field per row; $indexKey keys results by another field using indexBy() rules (missing field keys under '', floats throw, bools key as 1/0); column(null, $indexKey) keys whole rows, same as indexBy() |
columnAt(int $index): static |
Rows only. The column at a position from each row, ignoring key names (0 first, -1 last) |
indexBy(string $field): static |
Rows only. Whole rows keyed by $field; duplicate keys keep the LAST row. Null/missing field keys under ''; floats throw (convert to strings first), booleans key as 1/0 |
groupBy(string $field): static |
Rows only. Rows grouped by $field: one child collection per distinct value; same keying rules as indexBy() |
keys(): static |
The keys as a new collection (encode on output in HTML mode) |
values(): static |
The values, keys renumbered from 0 |
map(callable $callback): static |
New collection from $callback per element: closures receive raw ($value, $key), PHP built-ins receive $value only; rows arrive as plain arrays; returned arrays become rows again |
merge(array|SmartArrayBase|SmartNull ...$arrays): static |
Appends: numeric keys renumber, string keys overwrite (later wins); SmartNull merges as empty |
implode(string $separator = ''): SmartString|string |
Flat only. Joins values; returns SmartString in HTML mode (encodes on output), plain string in raw mode |
Fire when the COLLECTION IS EMPTY (no rows/elements; contrast SmartString's
field guards, which fire on missing values). Non-empty: return $this
unchanged, so they chain inline. $text is HTML-encoded automatically
(messages often interpolate user input).
| Method | On empty |
|---|---|
or404(?string $text = null): static |
HTTP 404 + minimal HTML page + exit(1). Default text "The requested URL was not found on this server." |
orDie(string $text): static |
Echo encoded text + exit(1) |
orThrow(string $text): static |
throw new RuntimeException($encodedText) |
orRedirect(string $url): static |
302 + Location: $url + exit. Checks headers_sent() immediately (throws even when non-empty) |
$article = $articles->where('num', $num)->first()->or404('Article not found');(first() on empty returns SmartNull; its or404() delegates and fires.)
Set via the $properties constructor argument; ZenDB and CMS Builder do
this automatically.
mysqli(?string $property = null): int|string|null|array- all metadata as an array with no argument ([]when none), or one value by name ('affected_rows','insert_id','query','baseTable', ...); unknown or unset properties return null.load(string $field): static|SmartNull- loads related records for$fieldvia the configuredloadHandler. ReturnsSmartNullwhen the collection is empty; throwsRuntimeExceptionwhen no handler is set or when called on a record set (call it on a row). Handler contract: return[rows, mysqliProperties], orfalseto reject the field, which throws a PHP nativeErrornaming it. Any other return throws a PHP nativeErrordescribing what came back instead.
$collection->debug(); // contents, current mode, mysqli metadata; debug(1) adds types and internals
print_r($collection); // element data only; the class name identifies the modedebug() output is <xmp>-wrapped in the browser and plain text on the
command line. PHP's print_r()/var_dump() output raw values with no wrapper -
in a browser use debug().
- InvalidArgumentException: unsupported value types in constructor or
writes (objects/resources), flat/nested shape mismatches (
sort()on nested,where()on flat),SORT_ASC/SORT_DESCpassed tosort()/sortBy(),getRawValue()on unsupported objects, invalidload()field names. - RuntimeException:
orThrow()(message HTML-encoded),orRedirect()with headers already sent, writes toSmartNull,load()without a handler or called on a record set. - Error (PHP native): undefined method calls (caller's file:line, plus
a did-you-mean suggestion when the name matches a known old method,
otherwise a docs pointer);
load()handler returning anything but[rows, mysqliProperties], includingfalse. - TypeError: a strict callback passed to
map()orfilter()throws PHP's own TypeError when an element doesn't match its signature (e.g.strtoupper(...)on a null or int element). - E_USER_WARNING (echoed + trigger_error): missing key on a result-set
row; missing field-name argument to
where()/sortBy()/indexBy()and friends (any nested collection, see Missing Keys above); string conversion of a collection (echo "$users"yields"", page continues, message suggests"{$var->method()}"braces). - E_USER_DEPRECATED:
$arr['key']array syntax, and deprecated names that have reached the notice stage (see below).
Old names still work. Some log deprecation notices naming the replacement; the rest are flagged only by IDEs and static analysis, with no runtime signal. When reading old code, translate:
| Deprecated | Use instead |
|---|---|
$arr['key'], $arr['key'] = $v (array syntax) |
$arr->key, $arr->key = $v (braces for odd keys) |
get($key) / get($key, $default) |
->key / ->{'key'}; for defaults use ->or($default) in HTML mode (encoded; ?? fallbacks skip encoding) or ?? $default in raw mode (NOTE: both fire on stored NULLs too; get()'s default only fired on missing keys) |
set($key, $value) |
->key = $value / ->{'key'} = $value |
pluck($field) / pluck($field, $keyField) |
column($field) / column($field, $keyField) |
pluckNth($index) |
columnAt($index) |
nth($index) |
at($index) |
toRaw(), noSmartStrings(), disableSmartStrings() |
asRaw() |
toHtml(), withSmartStrings(), enableSmartStrings() |
asHtml() or SmartArrayHtml::new() |
smartMap($callback) |
map($callback) |
each($callback) |
a foreach loop |
sprintf($format) |
map() with an inline format string |
where(['field' => $value, ...]) (array arg) |
chained where('field', $value) calls |
isMultipleOf($n) |
->position() % $n === 0 |
chunk($size) |
deprecated, no replacement planned |
help() |
retired; read the docs on GitHub |
SmartArrayRaw, SmartArrayRaw::new() |
SmartArray, SmartArray::new() (it extends SmartArray, so instanceof checks still pass) |
How the deprecated array syntax is reported is configurable via
SmartArrayBase::$onOffsetAccess: 'notify' (default) echoes a notice into
the page and passes it to your error handler, 'log' passes it to your error
handler only (for legacy sites mid-migration), 'throw' throws a
RuntimeException (strict mode for new installs).
SmartArrayHtmlis not instanceofSmartArray; type-hintSmartArrayBaseto accept both.- Foreach KEYS are never encoded, even in HTML mode; output keys via
keys()or encode manually. - Comparisons on HTML-mode fields compare the object: unwrap with
->value()/->int()first.empty($row->field)is false for stored""/0(objects are truthy); use$row->field->isEmpty(). - A stored NULL is not PHP null in HTML mode (
$row->x === nullis false); use->value() === nullor->isMissing(). Raw mode returns real null. - Braces stringify SmartString keys HTML-encoded:
$map->{$field}misses on text with'/&; use$map->{$field->value()}. Numeric ids are safe. (array)$collectionreturns internal object data; usetoArray().filter()keeps keys;json_encode()of a gapped array is an object, not an array - chainvalues().- Collection guards fire on EMPTY collections; SmartString field guards
fire on missing VALUES.
$row->or404()(row) vs$row->num->or404()(field) differ. where()drops rows missing the field;whereNot()keeps them.implode()in HTML mode returns a SmartString: interpolating it into a raw-SQL string would encode the joined text; call->string()first or use raw mode for SQL.- Missing-key READS warn only on result-set rows; standalone lookups are
silent by design. A missing field-name ARGUMENT (
where(),sortBy(),indexBy(), ...) warns from any nested collection. - In raw mode a MISSING field returns
SmartNull, an object, and objects are always truthy: bareif ($user->is_admin)runs when the field is absent or misspelled. PHP'sisset()and??see missing keys correctly; use them or->value()for logic. - HTML encoding makes values safe as HTML text and quoted attribute
values only. It does NOT make them safe as
javascript:-scheme hrefs (check the scheme first), inside<script>or<style>blocks (usejsonEncode()), or as URL query parameters (useurlEncode(), which returns""on a NULL field whereurlencode($x->value())raises a deprecation; for a path segment userawurlencode($x->value())). echo $collection/"$users"never works (collections have no string form); echo fields orimplode().