Skip to content

Collection: bug fixes, dot notation, multi-key sorting, 21 new methods, higher-order proxies - #352

Merged
techmahedy merged 2 commits into
doppar:4.xfrom
techmahedy:collection
Oct 3, 2026
Merged

techmahedy merged 2 commits into
doppar:4.xfrom
techmahedy:collection

Conversation

@techmahedy

@techmahedy techmahedy commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Pull Request Checklist

Q A
Branch? 4.x
Bug fix? yes
New feature? yes
Deprecations? no
Issues -
License MIT

Summary

An audit of Phaseolies\Support\Collection, with each suspected problem reproduced before changing anything. It found six real bugs (one of which made groupBy('date') throw), several everyday methods missing, and no way to reach nested values or sort by more than one key. This PR fixes the bugs and fills those gaps. Everything that worked before behaves the same; the exceptions are listed under "Behaviour changes".

Bug fixes

  1. A string key that is also a PHP function name was called as a function. buildKeyResolver() tested is_callable($key) on strings, so groupBy('date'), keyBy('date') and sortBy('date') threw TypeError: date(): Argument #1 ($format) must be of type string, array given (same for count, time, key, min, max, sort, ...). A string is now always a key; closures and array callables still work.
  2. $collection[] = $x and add() stored the item under the key "". offsetSet() did not handle a null offset (and raised a "null as array offset" deprecation), so a second add() overwrote the first. A null offset now appends.
  3. first($callback) silently ignored its argument and returned the first item (PHP drops extra arguments to a user method). first() and last() now accept ?callable $callback and a $default (a Closure default is called).
  4. merge() and getType() crashed with "Typed property Ramsey\Collection\Collection::$collectionType must not be accessed before initialization": they are inherited from the Ramsey collection, whose state the Doppar constructor never sets. Both are overridden. merge() accepts collections, arrays and iterators and follows array_merge rules.
  5. unique() treated 1.5 and 1.2 as the same value, and raised "Implicit conversion from float to int loses precision", because scalars were used directly as array keys. They are now keyed by their string form.
  6. filter() could not be called without a callback. Without one it now removes falsy items (null, false, 0, '', []).

New

Reading nested values. valueFrom($item, 'a.b.c') works on arrays, objects (including models) and ArrayAccess values; a key that really contains a dot is tried as it is first. It is used by pluck, sortBy, groupBy, keyBy, unique, duplicates and the new methods below, so pluck('profile.city') and sortBy('profile.city') work.

Sorting by several keys. sortBy(['age', 'name']), sortBy([['age', 'desc'], ['name', 'asc']]), callbacks in the list, a default direction from the existing $descending argument, and the SORT_* flags (SORT_STRING | SORT_FLAG_CASE, SORT_NATURAL, SORT_NUMERIC). Stable. An array callable such as [$object, 'method'] is still treated as a callback.

Methods: reject, every, some, reverse, keys, only, except, firstWhere (1, 2 or 3 arguments, operators = == === != <> !== < > <= >=), whereIn, whereNotIn, whereNull, whereNotNull, countBy, implode, flatMap, collapse, skip, slice, concat, search, median, when, unless. All return a Collection of the same model, except countBy (an array, like groupBy), every/some (bool), implode (string), search, median and firstWhere.

Higher-order access. HigherOrderCollectionProxy replaces the two anonymous classes in __get(). Besides the existing $c->map->name and $c->each->save(), it adds filter, reject, sum, avg, min, max, sortBy, sortByDesc, groupBy, keyBy, unique, every, some and flatMap:

$users->filter->isActive()->map->name;
$users->sum->points;
$users->sortBy->age;

map and each keep their precedence over a data key of the same name. The new names only act when no data key has that name, so existing $collection->filter style data access is unaffected. each->method() still skips items that do not have the method.

Behaviour changes

  • Return types are now static where they were self (33 methods). The docblocks already said @return static / @return $this, and the methods return new static or $this; the declarations now agree (this also clears the IDE's "documented type is not compatible" hints). Nothing in this repository or the sibling packages extends Collection; a subclass that overrides one of these methods with : self would need : static.
  • unique() (non-strict) compares scalars by string form. Different floats are now different, and 0 and false are no longer treated as the same value ('1', 1, true and 1.0 still are). unique(null, true) is unchanged.
  • A string passed as a key is never a callback. Code that relied on passing the name of a global function as a string to groupBy/keyBy/sortBy must pass a closure (fn($x) => strtolower($x)).
  • first() / last() / filter() gain optional parameters only; existing calls behave as before.
  • HigherOrderCollectionProxy is a new public class; Collection::valueFrom() is a new public method.

Files

  • src/Phaseolies/Support/Collection.php
  • src/Phaseolies/Support/HigherOrderCollectionProxy.php (new)
  • phpstan-baseline.neon (one entry removed)
  • tests/CollectionFeaturesTest.php (new)

Checklist

  • Tests have been added or updated
  • Documentation has been updated
  • Code follows the project coding standards
  • All tests pass locally

@techmahedy techmahedy added bug Something isn't working feat new feature labels Oct 3, 2026
@techmahedy
techmahedy merged commit ec8ebf4 into doppar:4.x Oct 3, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working feat new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant