Skip to content

Commit af7a98f

Browse files
author
lucia
committed
Merge branch 'main' into doc-review
2 parents d190104 + 3db0052 commit af7a98f

14 files changed

Lines changed: 271 additions & 123 deletions

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ the docs - IDEs show a strikethrough with the replacement.
8787
- **`isset()`, `empty()`, and `??` treat a stored NULL as missing** -
8888
matching plain PHP arrays, so `??` fallbacks now fire on NULL columns.
8989
See [UPGRADING.md](UPGRADING.md)
90-
- **Row-only methods throw on mixed arrays** - a scalar next to rows
91-
(usually a wrapped API response) throws instead of being silently
92-
skipped; database results are unaffected. See
90+
- **Row-only methods ignore non-row elements** - `where()`, `sortBy()`,
91+
`indexBy()`, and the other row-only methods keep the rows and skip
92+
scalars next to them (each method handled that mix differently before);
93+
a non-empty array with no rows still throws. See
9394
[UPGRADING.md](UPGRADING.md)
9495
- **`SmartArray::new($data, true)` throws** - a boolean that contradicts
9596
the class was silently ignored, returning raw values where HTML-safe

UPGRADING.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,25 +150,33 @@ automatically unless your composer.json pins `itools/smartstring` lower.*
150150
> Convert the field to a string first: `CAST(price AS CHAR)` in SQL, or
151151
> format it in PHP before keying.
152152
153-
### Row-only methods throw on mixed arrays
153+
### Row-only methods ignore non-row elements
154154
155155
> `where()`, `whereNot()`, `whereInList()`, `sortBy()`, `indexBy()`,
156-
> `groupBy()`, `column()`, and `columnAt()` now require every element to be
157-
> a row. An array mixing rows and scalar values throws
158-
> `InvalidArgumentException` naming the element, instead of silently
159-
> skipping the scalars:
156+
> `groupBy()`, `column()`, and `columnAt()` now follow one rule: they work
157+
> on the rows (elements that are arrays) and ignore other elements. A
158+
> non-empty array with no rows throws `InvalidArgumentException`, same
159+
> as before; an empty array returns an empty result.
160+
>
161+
> In v2.x each method handled a scalar next to rows differently: the
162+
> `where()` family already skipped scalars (unchanged), `sortBy()` sorted
163+
> them in with the rows, and `indexBy()`, `groupBy()`, and `column()` kept
164+
> them under renumbered integer keys:
160165
>
161166
> ```php
162-
> $data = SmartArrayHtml::new(['count' => 5, 'items' => [['id' => 1]]]);
163-
> $data->where('id', 1); // before: returned 0-1 rows, 'count' silently ignored
164-
> // after: throws "where(): Expected a nested array of
165-
> // rows, but element 'count' is not a row (int)"
167+
> $schema = SmartArray::new([
168+
> 'menuName' => 'Products', // scalar setting
169+
> 'name' => ['type' => 'textfield', 'order' => 2],
170+
> 'photo' => ['type' => 'upload', 'order' => 1],
171+
> ]);
172+
> $schema->where('type', 'upload'); // rows whose type matches; the scalar is ignored
173+
> $schema->indexBy('type'); // v2.x: scalars kept under renumbered keys; v3.0: rows only
166174
> ```
167175
>
168-
> Database results and empty arrays are unaffected - this only fires on
169-
> hand-built arrays that mix shapes. The error usually means the array was
170-
> wrapped one level too high (`->items` was the intended collection) or a
171-
> scalar was assigned onto a result set.
176+
> Database results are unaffected - every element is a row. Review only
177+
> hand-built arrays that mix scalars and rows: a scalar that used to reach
178+
> the result through `sortBy()`, `indexBy()`, `groupBy()`, or `column()`
179+
> is left out now.
172180
173181
### Silent changes
174182

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
},
2727
"autoload-dev": {
2828
"psr-4": {
29-
"Itools\\SmartArray\\Tests\\": "tests/"
29+
"Itools\\SmartArray\\Tests\\": "tests/",
30+
"Itools\\Standards\\": "tests/Integration/"
3031
}
3132
},
3233
"config": {

docs/ai-reference.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,29 +236,32 @@ the new collection.
236236

237237
## Filtering and Sorting
238238

239-
All return a new collection; nested-only methods require every element to be
240-
a row and throw `InvalidArgumentException` on flat arrays or on mixed arrays
241-
where an element is not a row (empty arrays pass); flat-only methods likewise
242-
throw on nested input.
239+
All return a new collection. Methods marked "Rows only" work on the rows
240+
(elements that are arrays) and ignore other elements (scalars/null); a
241+
non-empty array with no rows throws `InvalidArgumentException`
242+
(empty arrays pass). Methods marked "Flat only" throw on nested input.
243243

244244
| Method | Behavior |
245245
|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
246-
| `where(string $field, mixed $value = null): static` | Nested 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 |
247-
| `whereNot(string $field, mixed $value = null): static` | Nested 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)`) |
248-
| `whereInList(string $field, mixed $value): static` | Nested 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 |
246+
| `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 |
247+
| `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)`) |
248+
| `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 |
249249
| `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 |
250250
| `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) |
251-
| `sortBy(string $field, int $flags = SORT_REGULAR): static` | Nested 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 |
251+
| `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 |
252252
| `unique(): static` | Flat only. Removes duplicates keeping the first, keys preserved; compares as strings (`array_unique()`), so 1 and `'1'` are duplicates |
253253

254254
## Transforming and Grouping
255255

256+
"Rows only" and "Flat only" carry the same contract as in Filtering and
257+
Sorting above.
258+
256259
| Method | Behavior |
257260
|------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
258-
| `column(int\|string\|null $columnKey, int\|string\|null $indexKey = null): static` | 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()` |
259-
| `columnAt(int $index): static` | The column at a position from each row, ignoring key names (0 first, -1 last) |
260-
| `indexBy(string $field): static` | 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 |
261-
| `groupBy(string $field): static` | Rows grouped by `$field`: one child collection per distinct value; same keying rules as `indexBy()` |
261+
| `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()` |
262+
| `columnAt(int $index): static` | Rows only. The column at a position from each row, ignoring key names (0 first, -1 last) |
263+
| `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 |
264+
| `groupBy(string $field): static` | Rows only. Rows grouped by `$field`: one child collection per distinct value; same keying rules as `indexBy()` |
262265
| `keys(): static` | The keys as a new collection (encode on output in HTML mode) |
263266
| `values(): static` | The values, keys renumbered from 0 |
264267
| `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 |

src/Deprecations.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public function help(): void
110110
#[Deprecated(reason: 'renamed to column()', replacement: '%class%->column()')]
111111
public function pluck(string|int $valueField, ?string $keyField = null): static
112112
{
113-
$this->assertNestedArray(); // assert here so the error names pluck(), not column()
113+
$this->rows(); // run the row check here so a flat-array error names pluck(), not column()
114114
return $this->column($valueField, $keyField);
115115
}
116116

@@ -133,7 +133,7 @@ public function nth(int $index): static|SmartNull|SmartString|string|int|float|b
133133
#[Deprecated(reason: 'renamed to columnAt()', replacement: '%class%->columnAt()')]
134134
public function pluckNth(int $index): static
135135
{
136-
$this->assertNestedArray(); // assert here so the error names pluckNth(), not columnAt()
136+
$this->rows(); // run the row check here so a flat-array error names pluckNth(), not columnAt()
137137
return $this->columnAt($index);
138138
}
139139

0 commit comments

Comments
 (0)