Skip to content

Commit 7aa89a4

Browse files
committed
docs: tighten CHANGELOG and UPGRADING for the unreleased version
- CHANGELOG: one bullet per change users would actually notice, each with a bold plain-words lead-in; everything smaller rolled up into a closing Minor section, with the diff as the full record - UPGRADING: shorter intro - breaking changes announce themselves, everything worth checking is listed here with a search that finds affected code
1 parent 6568330 commit 7aa89a4

2 files changed

Lines changed: 83 additions & 215 deletions

File tree

CHANGELOG.md

Lines changed: 76 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,33 @@ Everything else is hardening and fixes.
1414

1515
### Requirements
1616

17-
- Now requires SmartString 3.0+ (previously any version). Composer picks it
18-
up on `composer update`; only a pinned `itools/smartstring` constraint in
19-
your own composer.json needs changing.
17+
- Now requires SmartString 3.0+ (previously any version); Composer picks it
18+
up automatically unless your own composer.json pins `itools/smartstring`
19+
lower
2020

2121
### Security
2222

23-
- Missing-key warnings and array-syntax deprecation notices HTML-encode the
24-
key before echoing it. With a dynamic key (`->get($_GET['sort'])`,
25-
`$arr[$_GET['sort']]`), a key containing HTML reached the browser
26-
unencoded - a reflected XSS vector. The `trigger_error()` copy carries the
27-
same encoded key.
28-
- `json_encode($smartArray)` substitutes malformed UTF-8 in keys with �
29-
(U+FFFD), not just values - one corrupt byte in a key no longer makes it
30-
return false and lose the whole document.
31-
- `debug()` and `help()` escape a literal `</xmp` in web output as `<\/xmp`,
32-
same escaping as CMSB's `xmp_safe()`. A stored value containing `</xmp>`
33-
ended the `<xmp>` block early, so the rest of the value parsed as live
34-
HTML on any page that called `debug()` on it.
23+
- **Warnings HTML-encode dynamic keys** - missing-key warnings and
24+
deprecation notices echoed keys like `$_GET['sort']` unencoded, a
25+
reflected XSS vector
26+
- **`json_encode()` survives malformed UTF-8 in keys** - bad bytes become
27+
� instead of the whole document returning false (values were already
28+
handled)
29+
- **`debug()` and `help()` escape `</xmp`** - a stored value containing
30+
`</xmp>` ended the debug block early, so the rest of it parsed as live
31+
HTML
3532

3633
### Added
3734

38-
- `where($field)` and `whereNot($field)` with just a field name filter by
35+
- **`where($field)` / `whereNot($field)`** - one-argument forms filter by
3936
PHP's `empty()` rule: `where('featured')` keeps rows where the field is
40-
non-empty, `whereNot('featured')` keeps the rest, and every row lands in
41-
exactly one of the two. The one-argument form previously ran as
42-
`where($field, null)`, a loose match against NULL.
37+
non-empty, `whereNot('featured')` keeps the rest
4338

4439
### Performance
4540

46-
- Building arrays is ~3.4x faster (25-row record set: 15.5 → 4.5
47-
microseconds), `toArray()` is ~4.7x faster (flat 4-field row: 108 → 23 ns),
48-
and `foreach` is up to ~1.3x faster when no value needs wrapping (the gain
49-
grows with row count). Numbers and how the speedups work:
50-
[docs/performance.md](docs/performance.md)
41+
- **Building arrays is ~3.4x faster** (25-row record set: 15.5 → 4.5
42+
microseconds), `toArray()` ~4.7x, and `foreach` up to ~1.3x. Numbers and
43+
how the speedups work: [docs/performance.md](docs/performance.md)
5144

5245
### Renamed
5346

@@ -62,201 +55,80 @@ strikethrough and offer a one-click rename.
6255

6356
### Parameter renames (named arguments only)
6457

65-
These only matter if you write parameter names in calls - calls using an old
66-
name fail with a clear "Unknown named parameter" Error:
67-
68-
- `sortBy(flags:)` was `type:` - it always held PHP sort flags, and now
69-
matches `sort()` and PHP's own sort functions
58+
- `sortBy(flags:)` was `type:` - named-argument calls only; the old name
59+
fails with a clear "Unknown named parameter" Error
7060

7161
### Deprecated
7262

7363
These still work with no runtime notice, they're just no longer featured in
7464
the docs - IDEs show a strikethrough with the replacement.
7565

76-
- `get($key, $default)` and `set($key, $value)` - use property access
77-
instead: `$row->name`, `$row->{'users.id'}` for keys property syntax can't
78-
type, `$row->name ?? 'n/a'` for missing-key defaults, and
79-
`$row->name = $value` to write. `get('')` and `set('', $value)` remain the
80-
only way to reach an empty-string key - the brace form is a fatal error.
81-
- `each($callback)` - use a `foreach` loop instead, same behavior in plain PHP
82-
- `sprintf($format)` - use `map()` with an inline format string instead:
83-
`$list->map(fn($v) => "<li>$v</li>")`. On SmartArrayHtml, convert to raw
84-
mode and encode explicitly so the finished HTML isn't re-encoded on output:
85-
`$row->asRaw()->map(fn($v) => "<td>" . htmlspecialchars((string)$v) . "</td>")->implode("\n")`
86-
- `help()` - read the docs on GitHub instead; the guide pages and method
87-
reference replaced the built-in cheat sheet (`src/help.txt` is deleted).
88-
Until removed, `help()` prints links to both, and runtime messages point at
89-
"the SmartArray docs" rather than suggesting it.
66+
- **`get()` and `set()`** - use property access: `$row->name`,
67+
`$row->name ?? 'n/a'`, `$row->name = $value`
68+
- **`each()`** - a `foreach` loop does the same in plain PHP
69+
- **`sprintf()`** - use `map()` with an inline format string:
70+
`$list->map(fn($v) => "<li>$v</li>")`
71+
- **`help()`** - the docs on GitHub replaced the built-in cheat sheet
9072

9173
### Removed
9274

93-
- `usingSmartStrings()` - use `instanceof SmartArrayHtml` to check the mode;
94-
the class is the mode. Never documented, no found uses.
95-
- `setLoadHandler()` - pass the handler as the `loadHandler` constructor
96-
property, which is how the database layer (ZenDB) has always set it. It
97-
couldn't work on record sets anyway: rows snapshot the handler during
98-
construction, so one set afterward never reached them.
99-
- `newSmartNull()` - now protected; it is the internal factory for
100-
missing-value placeholders. Never documented, no found uses.
75+
- **`usingSmartStrings()`, `setLoadHandler()`, `newSmartNull()`** - never
76+
documented, no found uses; replacements in [UPGRADING.md](UPGRADING.md)
10177

10278
### Behavior changes
10379

104-
- `where()`, `whereNot()`, `whereInList()`, and `contains()` match values
105-
more precisely. Most code sees no difference: numbers still match numeric
106-
strings, so `where('id', 5)` matches `'5'` and `where('price', 1)` matches
107-
`'1.00'`. Three edge cases changed:
108-
- Two strings must match exactly: `'01'` no longer matches `'1'`, and
109-
`where('code', '0e123')` no longer matches `'0e999'` (PHP's loose `==`
110-
read both as numbers - a wrong-row risk for hash and token lookups)
111-
- null only matches null, like SQL IS NULL (it used to match `''`, 0,
112-
and false too)
113-
- true/false mean 1/0 (true used to match any truthy value, even `'abc'`)
114-
115-
See [UPGRADING.md](UPGRADING.md).
116-
- Row-only methods (`where()`, `whereNot()`, `whereInList()`, `sortBy()`,
117-
`indexBy()`, `groupBy()`, `column()`, `columnAt()`) throw
118-
`InvalidArgumentException` naming the offending element when the array
119-
mixes rows and scalar values, instead of silently skipping the scalars
120-
(`sortBy()` kept them). A scalar next to rows means the array was built
121-
wrong - usually a wrapped API response (`['count' => 5, 'items' => [...]]`)
122-
or a value assigned onto a result set - and skipping it hid the mistake.
123-
Database results and empty arrays are unaffected. See
124-
[UPGRADING.md](UPGRADING.md).
125-
- A missing field stays a SmartNull through the whole chain instead of
126-
becoming an empty SmartString at the first method call. Same output as
127-
before (echoes `""`, `or()` still fires), but chains no longer dead-end:
128-
`$row->missing->trim()->implode(', ')` works where it previously threw.
129-
`map()` skips its callback on a missing key; NULL values in existing
130-
keys still run it.
131-
- `isset()`, `empty()`, and `??` treat a stored null as missing, matching
132-
plain PHP arrays: on a NULL column `isset($row->field)` is now false and
133-
`$row->field ?? 'none'` returns `'none'`. Previously they answered "does
134-
the column exist", so in HTML mode `??` fallbacks never fired on NULL
135-
columns. Bracket syntax matches, and direct access is unchanged. See
136-
[UPGRADING.md](UPGRADING.md).
137-
- Missing-key warnings fire only on rows inside a result set, where keys are
138-
column names and a miss is almost always a typo. Top-level and derived
139-
collections (`indexBy()`/`column()` maps, standalone arrays) render blank
140-
silently, so fallbacks chain cleanly:
141-
`$authorById->{$id}->or('Unknown Author')`. Method-argument checks
142-
(`where('typo')`) still warn everywhere.
143-
- `SmartArray::new($data, true)` and `SmartArrayHtml::new($data, false)` throw
144-
like the constructors do, instead of silently ignoring the boolean - code
145-
passing `true` expecting auto-encoding was getting raw, unencoded values.
146-
Redundant booleans (`false` on SmartArray, `true` on SmartArrayHtml) log a
147-
deprecation and proceed. `SmartArrayRaw::new()` matches.
148-
See [UPGRADING.md](UPGRADING.md).
149-
- All writes to a `SmartNull` throw "Cannot set values on SmartNull": property
150-
writes (previously created a silent dynamic property that shadowed
151-
chaining) and two-argument `->set($key, $value)` (previously discarded the
152-
value) now match the existing `['key'] =` guard. One-argument
153-
`->set($value)` is SmartString's set: not a write, it produces that value
154-
and ends the chain, like `or()`.
155-
- Raw-mode arrays no longer answer SmartString methods on missing keys:
156-
`$row->missing->or('n/a')` on a raw array throws the standard
157-
undefined-method Error instead of returning an HTML-encoding SmartString.
158-
Raw fallbacks use `??`. HTML mode is unchanged.
159-
- `set()`, `->key = $value`, and array assignment unwrap Smart values
160-
(SmartString, SmartArray, SmartNull) instead of throwing, so values copy
161-
between arrays in any mode without calling `->value()` first. SmartNull
162-
stores as null; nested SmartArrays convert to the target array's mode.
163-
- `get()` and `at()` unwrap Smart arguments, so keys read from another array
164-
work directly: `$users->get($article->author_id)`. Unwrapped keys follow
165-
PHP array-key rules (null reads key `''`, bool/float truncate to int).
166-
- `print_r()` and `var_dump()` show just the array data, like dumping a plain
167-
array - the injected pseudo-properties (the README help pointer and the
168-
`useSmartStrings` flag) are gone. Use `->debug()` for exact types and
169-
metadata. `SmartNull` dumps as `[value] =>`. Matches SmartString 3.0.0.
170-
- Deprecated method names are real declared methods marked `@deprecated`
171-
instead of `__call()` shims, so IDEs show strikethroughs with the
172-
replacement and `method_exists()` reports them. Same behavior and
173-
deprecation notices as before.
174-
- `orDie()` and `or404()` exit with status 1 instead of 0, so shell scripts
175-
and cron jobs see the failure. Output is unchanged. Matches SmartString.
80+
- **Value matching is more precise** in `where()`, `whereNot()`,
81+
`whereInList()`, and `contains()` - numbers still match numeric strings,
82+
but strings compare exactly, null only matches null (like SQL IS NULL),
83+
and true/false mean 1/0. See [UPGRADING.md](UPGRADING.md)
84+
- **`isset()`, `empty()`, and `??` treat a stored NULL as missing** -
85+
matching plain PHP arrays, so `??` fallbacks now fire on NULL columns.
86+
See [UPGRADING.md](UPGRADING.md)
87+
- **Row-only methods throw on mixed arrays** - a scalar next to rows
88+
(usually a wrapped API response) throws instead of being silently
89+
skipped; database results are unaffected. See
90+
[UPGRADING.md](UPGRADING.md)
91+
- **`SmartArray::new($data, true)` throws** - a boolean that contradicts
92+
the class was silently ignored, returning raw values where HTML-safe
93+
ones were asked for. See [UPGRADING.md](UPGRADING.md)
94+
- **Missing fields chain cleanly** - a missing key stays a SmartNull
95+
through the whole chain, so `$row->missing->trim()->implode(', ')` works
96+
where it previously threw; output is unchanged (echoes `""`, `or()`
97+
still fires)
98+
- **Missing-key warnings fire only on rows inside a result set** - where
99+
keys are column names and a miss is almost always a typo; derived
100+
collections and standalone arrays render blank silently, so fallbacks
101+
like `$authorById->{$id}->or('Unknown Author')` chain cleanly
102+
- **Smart values copy between arrays** - `set()`, `->key = $value`, and
103+
array assignment unwrap SmartString/SmartArray/SmartNull values instead
104+
of throwing, converting to the target array's mode; `get()` and `at()`
105+
unwrap Smart keys the same way
176106

177107
### Fixed
178108

179-
- Deprecation notices and error messages name the developer's file when a call
180-
routes through the companion SmartString library (the caller lookup treated
181-
only its own src directory as internal, so a frame in the other library
182-
could be reported as the caller)
183-
- `column(null)` and `column(null, null)` match PHP's `array_column()`: whole
184-
rows renumbered from 0, instead of throwing "unexpected arguments"
185-
- `sortBy()` no longer throws a bare `ValueError: Array sizes are
186-
inconsistent` when a row is missing the sort field. Missing fields sort
187-
first (treated as null for ordering, like MySQL ORDER BY); rows are
188-
returned unchanged.
189-
- `indexBy()` no longer gives rows missing the index field a leftover numeric
190-
key that looks like a real field value. Null and missing values both index
191-
under `''`, duplicates last-wins.
192-
- `indexBy()`, `groupBy()`, and `column($col, $indexKey)` throw
193-
`InvalidArgumentException` ("'price' has float values, convert them to
194-
strings first") when the key field holds floats, instead of PHP's
195-
float-to-int key truncation, which keyed `19.99` and `19.50` both as `19`
196-
(last row wins, one lost) and printed a PHP deprecation naming library
197-
internals. No float-to-key conversion is safe, so the caller picks the
198-
string format. Integer and boolean keys are unchanged (ints key as ints,
199-
bools as 1/0).
200-
- `column($col, $indexKey)` keys rows missing the index field under `''`,
201-
same as `column(null, $indexKey)` and `indexBy()`, instead of
202-
`array_column()`'s auto-numbered keys that look like real field values.
203-
- `asRaw()` and `asHtml()` on a row keep its position metadata, so
204-
`position()`, `isFirst()`, and `isLast()` answer the same before and after
205-
converting (conversion used to reset them to position 0 with both flags
206-
false).
207-
- Method calls on a `SmartNull` keep the source's mysqli metadata, root
208-
pointer, and load handler, matching its `asRaw()`/`asHtml()`. Previously
209-
chains through a missing key answered `mysqli()` with `[]` and `root()`
210-
with a throwaway empty array.
211-
- `$arr[null]` (deprecated `[]` syntax) reads key `''` like a plain PHP array,
212-
instead of printing a misleading `Replace []` suggestion and then throwing a
213-
TypeError that named library internals. `unset()` and `isset()` already
214-
worked this way.
215-
- Array-syntax deprecation notices suggest one replacement style across reads,
216-
writes, and `unset()`: `->key` and `->key = $value` for property-safe
217-
names, `->{0}` for integer keys, `->{'users.id'}` for other keys.
218-
Null and `''` keys suggest `->get('')` / `->set('', $value)` - the brace
219-
form is a fatal error for an empty property name.
220-
- Unknown methods on `SmartNull` throw the same `Error` as the rest of the
221-
library - method name, "did you mean" hint, caller's file and line -
222-
instead of `InvalidArgumentException("Method 'x' not found")`.
223-
- Clearer messages for two exceptions: `load()` with no handler explains
224-
handlers come from the database layer (was "No loadHandler property is
225-
defined"), and writing to a `SmartNull` says the value came from a missing
226-
key or empty result. The unsupported-type message from `set()` no longer
227-
prefixes the library's internal method name.
228-
- The string-conversion warning names the actual class (`Can't convert
229-
SmartArrayHtml to string` on HTML arrays) and the missing-brace hint ends
230-
with a newline so it no longer runs into the "Occurred in" trace.
231-
- `load()` accepts a column literally named `"0"` (the blank-name check used
232-
`empty()`), and a handler returning the wrong shape throws `Load handler
233-
must return [rows, mysqliProperties] or false` before destructuring, so no
234-
stray "Undefined array key 1" PHP warning leaks from the library.
235-
- `load()` with an invalid-character field name throws
236-
`InvalidArgumentException` (was `RuntimeException`), matching the
237-
empty-field-name check - both are argument problems. The other `load()`
238-
setup errors still throw `RuntimeException`.
239-
- `debug(1)` no longer throws `Unsupported type: Closure` on arrays with a
240-
load handler - exactly the database results it exists to inspect. Callables
241-
and other objects in the properties block print as their type (`Closure,`)
242-
instead.
243-
- `get($key, $default)` defaults act like stored values: Smart defaults
244-
(SmartString, SmartArray, SmartNull) unwrap and re-wrap for the array's
245-
mode. Previously a SmartNull default threw `InvalidArgumentException`, and
246-
cross-mode Smart defaults threw `TypeError` from the return declarations.
247-
- `SmartNull->help()` wraps its output in `<xmp>` when no Content-Type header
248-
is set, same rule as every other `help()` and `debug()` - PHP's default
249-
response type is text/html. Previously it only wrapped when text/html was
250-
set explicitly via `header()`, so on typical pages it printed as collapsed,
251-
unformatted text.
252-
- `help()` and `debug()` print plain text on the command line instead of
253-
wrapping output in literal `<xmp>` tags. Terminal detection checks
254-
`PHP_SAPI` plus two fallbacks (Windows console `SESSIONNAME`, missing
255-
`SCRIPT_NAME`) because some hosts' CGI builds misreport SAPI. Web responses
256-
are unchanged. Matches SmartString.
257-
- `or404()` outputs `<html>` instead of `<html lang>` - an empty `lang` reads
258-
as an invalid value to accessibility checkers, and the message language is
259-
caller-supplied so it can't be declared. Matches SmartString.
109+
- **Float key values throw in `indexBy()`, `groupBy()`, and `column()`** -
110+
PHP's float-to-int key truncation keyed `19.99` and `19.50` both as
111+
`19`, silently losing a row; the error asks for strings instead
112+
- **Rows missing the sort or index field are handled** - `sortBy()` sorts
113+
them first (like MySQL ORDER BY) instead of throwing, and `indexBy()`
114+
and `column()` key them under `""` instead of a leftover numeric key
115+
that looked like real data
116+
- **Errors name your file, not the library's** - deprecation notices and
117+
error messages report the right caller when a call routes through
118+
SmartString, and unknown methods on `SmartNull` throw the same helpful
119+
Error as the rest of the library
120+
121+
### Minor
122+
123+
Also: writes to a `SmartNull` throw instead of silently discarding the
124+
value, raw-mode arrays throw on SmartString-style fallbacks like `->or()`
125+
on missing keys (use `??` instead), `print_r()` and `var_dump()` show
126+
clean array data (use `debug()` for exact types), `orDie()` and `or404()`
127+
exit with status 1 so shell scripts see the failure, deprecated names are
128+
real declared methods so IDEs and `method_exists()` see them, `help()`
129+
and `debug()` print plain text on the command line, and a couple dozen
130+
small fixes to error messages and `load()`, `column()`, `get()`, and
131+
`debug()` edge cases.
260132

261133
## [2.7.0] - 2026-07-07
262134

UPGRADING.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22

33
Most old code keeps working after an upgrade:
44

5-
- **Renamed methods are removed slowly, never silently.** Old names keep
6-
working while they step through deprecation stages over multiple releases
7-
(IDE strikethrough, logged notice, visible notice, then a clear Error),
8-
always naming their replacement.
9-
- **Breaking changes produce clear errors.** Removed features and wrong named
10-
arguments throw an Error with your file and line, and once a deprecated call
11-
reaches the notice stage it names its exact replacement, e.g. "Replace
12-
->toRaw() with ->asRaw() in listings.php:14" - error handlers like
13-
CMS Builder's developer log catch these.
14-
- **Only the silent changes need checking.** This file lists them per
15-
version, each with a search.
5+
- **If it breaks, it tells you.** Old names phase out over multiple
6+
releases - IDE strikethrough, then a quietly logged notice with your file
7+
and line (CMS Builder shows these in the Developer Log), then a clear
8+
error - always naming the replacement.
9+
- **Everything worth checking is listed here.** Silent behavior changes,
10+
deprecations, and optional renames, per version, each with a search that
11+
finds affected code.
1612

1713
Upgrading SmartArray also upgrades SmartString, and SmartArrayHtml returns its
1814
values as SmartString objects, so check its

0 commit comments

Comments
 (0)