Differences from Laravel
@tolki/collection follows Laravel's Collection wherever JavaScript can hold the same data. These are the places where it cannot, or chooses not to.
Reading items
$c['a']isc.offsetGet('a'),$c->get('a', 0)isc.get('a', 0), and$c->all()['a']isc.all().aorconst { a } = c.all(). There is noc.aorc[0], and$c->athrows in Laravel too.
import { collect } from "@tolki/collection";
const c = collect({ a: 1 });
c.offsetGet("a");
// => 1
c.get("b", 0);
// => 0
c.all().a;
// => 1Keys and order
A list stays a list, since a JavaScript array cannot hold sparse keys: where PHP would leave a gap in a list's integer keys, the collection renumbers them, so
collect([1, 2, 3]).filter((v) => v !== 2).all()is[1, 3], where PHP keeps the keys0and2. A keyed write such asput(5, 'x')keeps PHP's keys, and the list becomes keyed.jsimport { collect } from "@tolki/collection"; collect([1, 2, 3]) .filter((v) => v !== 2) .all(); // => [1, 3] collect([1, 2]).put(5, "x").all(); // => { 0: 1, 1: 2, 5: "x" }duplicates()andchunk()keep PHP's positional keys, because the keys are the answer, and so dogroupBy(key, true)andrandom(n, true), which ask for them.chunkBy()andchunkWhile()do not followchunk(): the chunks of a list are lists, so each one counts from 0 again, where PHP's later chunks keep the keys their items had. A keyed collection keeps its string keys in each chunk.jsimport { collect } from "@tolki/collection"; collect([1, 2, 3, 4, 5]).chunk(2).toArray(); // => [{ 0: 1, 1: 2 }, { 2: 3, 3: 4 }, { 4: 5 }] collect([1, 1, 2, 2, 3]) .chunkBy((value) => value) .toArray(); // => [[1, 1], [2, 2], [3]] collect({ a: 1, b: 1, c: 2 }) .chunkBy((value) => value) .toArray(); // => [{ a: 1, b: 1 }, { c: 2 }]A keyed collection keeps its object backing even when its keys run
0..n-1:collect({ 0: 'a', 1: 'b' })'sall()andtoArray()are{ 0: 'a', 1: 'b' }, where PHP'sall()is the list['a', 'b'].values()gives the list, andjsonSerialize()answers it andtoJson()writes["a","b"], as PHP's do.jsimport { collect } from "@tolki/collection"; const keyed = collect({ 0: "a", 1: "b" }); keyed.all(); // => { 0: "a", 1: "b" } keyed.values().all(); // => ["a", "b"] keyed.toJson(); // => '["a","b"]'A keyed collection lists its integer keys in ascending order, as a plain object does. Where PHP keeps integer keys out of sequence (
[2 => 'c', 0 => 'a']), the methods that walk or rebuild such a collection can answer differently from Laravel. In this port, keys built from aMap, and items appended after a string key, keep their insertion order wherever the class already tracks it.The sort family (
sort(),sortDesc(),sortBy(),sortByDesc(),sortKeys(),sortKeysDesc(),sortKeysUsing()) andreverse()renumber integer keys from 0 in the new order and keep string keys, where PHP keeps every key:collect([3, 1, 2]).sort()has the keys0, 1, 2and writes[1,2,3], where PHP's has1, 2, 0and writes{"1":1,"2":2,"0":3}.jsimport { collect } from "@tolki/collection"; collect([3, 1, 2]).sort().keys().all(); // => [0, 1, 2] collect([3, 1, 2]).sort().toJson(); // => '[1,2,3]'On a collection mixing integer and string keys, the sort family and
reverse()list the integer keys first, as a plain object does, so a new order that puts a string key before an integer one is lost:collect({ 0: 1, x: 2 }).reverse()keeps the values[1, 2]andcollect({ 0: 3, x: 1 }).sort()the values[3, 1], where PHP's have the keys['x', 0]and the values[2, 1]and[1, 3].jsimport { collect } from "@tolki/collection"; collect({ 0: 1, x: 2 }).reverse().values().all(); // => [1, 2] collect({ 0: 3, x: 1 }).sort().values().all(); // => [3, 1]PHP remembers the highest integer key a collection has held, so after
forget()removes that key the next append still counts on from it:collect([5 => 'a', 6 => 'b'])->forget(6)->push('x')stores'x'under7, where this port uses6.
Values
all()hands back the collection's own items, not a copy, so writing to them writes to the collection, where PHP'sall()returns a copy of its array.toArray()andcollect()return copies.Truthiness is PHP's, not JavaScript's: see Truthiness. So
reject(false)rejectsnulland{}too but keeps'0.0', a callback answeringNaNcounts as true, and an ArrayAccess-style item whoseoffsetExists()answers'0'or[]reads as absent.unique()andduplicates()keep the first of each loosely equal run in one walk (see Loose and strict comparison). Where==is not transitive across mixed types, PHP's answer can differ:collect(['abc', '0', false, '']).unique()keeps'', which PHP drops.jsimport { collect } from "@tolki/collection"; collect(["abc", "0", false, ""]).unique().all(); // => ["abc", "0", ""]diff(),diffAssoc(),intersect()andintersectAssoc()match an array or object item by identity, where PHP compares string casts: every array casts to"Array", socollect([[1], [2]]).diff([[1]])keeps both items where PHP's keeps none, and an object casts through its__toString(), or throws without one.jsimport { collect } from "@tolki/collection"; collect([[1], [2]]) .diff([[1]]) .all(); // => [[1], [2]]toJson()writes/and non-ASCII characters as they are, where PHP'sjson_encode()escapes them (\/,\u00e9); both decode to the same value.jsimport { collect } from "@tolki/collection"; collect(["a/b", "é"]).toJson(); // => '["a/b","é"]'
JavaScript-only additions
length,[Symbol.iterator](sofor...ofand spreading work),toJSON()(soJSON.stringify(c)encodes the items) and[Symbol.toPrimitive](the count for+c, and the JSON for`${c}`, as PHP's string cast gives).toJson(replacer, space)andtoPrettyJson(replacer, space)takeJSON.stringify()'s arguments in place of PHP's flags.reduceInto()'s callback may return the new accumulator, since JavaScript cannot pass a primitive by reference.avg()andaverage()hand their callback the key as well.whereBetween()andwhereNotBetween()accept a collection of bounds, from which PHP reads none, so itswhereBetween()keeps no item and itswhereNotBetween()every one.keyBy(),groupBy()andcountBy()read an@tolki/enumcase as its value, as PHP'senum_value()does.eachSpread()andmapSpread()hand the callback a scalar row whole, where PHP throws.eachSpread()andmapSpread()spread a string-keyed row (a plain object, aMapor a collection) by its values, where PHP throwsCannot use positional argument after named argument during unpacking.ensure()also takes JavaScript'stypeofnames ('number','object', …) beside PHP'sget_debug_type()names and classes.- A
Mapis accepted wherever PHP takes an array.undefinedreads asnullthrough a path, in the loose comparisons ofcontains(),search()andwhere(), and in truthiness (whereNull('a')matches{ a: undefined }), but anundefineditem compared strictly, or by the string cast ofdiff()andintersect(), is notnull: keylesswhereNull()andcontainsStrict(null)skip it, anddiff([null])keeps it.
Not portable
- Higher-order proxies (
$c->map->name,proxy(), and the one-argumentwhen()andunless()),Macroable(macro()),lazy()andLazyCollection,dd(),getCachingIterator(), and PHP's sort flags: every sort takes only a direction. fromJson()takesdepthandflagsfor PHP's signature and ignores them. AWeakMapcannot be iterated, so it makes an empty collection. An@tolki/enumcase is a plain object, socollect()reads it as a record of its fields, where PHP wraps an enum case as one item.- Paths read only an object's own fields, so a class getter reads as absent.
TypeScript limits
- In-place writes cannot narrow the variable they are called on:
c.put('x', 1)returns the widened type whileckeeps its declared one, andcstill names the keysforget(),pull()oroffsetUnset()removed. - There is deliberately no index signature, so
c.ais a compile error andc[0]is untyped (an error undernoImplicitAny); read items as shown under Reading items. - A subclass's static factory is typed as the base class:
Sub.make([1])is aCollection<number>to TypeScript, though it returns aSub. - PHP's
intandfloatare bothnumber, soensure('int')andensure('float')narrow to the same type. new Collection(x)types only what the class's own type parameters can say, since a constructor cannot declare its own: aMap<string, V>stays keyed bystring(PHP stores a numeric string key as an integer), a record with integer keys gets the list shape (new Collection({ 1: 'a' })), a Jsonable is typed by its own members and a JsonSerializable answering a scalar by that scalar's, and a list-or-record union takes its key type's default shape.collect()andmake()type all of these exactly.- A class operand whose only named members are getters (
class G { get name() { … } }) is typed as holdingname, somerge(),union(),replace()andreplaceRecursive()claim a keyed result, while the runtime copies no own field and keeps a list. - A class instance's methods are typed as items:
collect(new User())includesgreet()in its value and key types, though only own fields are copied, since TypeScript cannot tell a method from a function-valued field. - A class instance row in
mapSpread()oreachSpread()is typed as spreading its fields, as a plain object row is, though the runtime passes the instance whole. - Spread rows of varying length (list rows of different lengths, or a row that may be
null) type every callback parameter as present, each an item or the key; a shorter row's key arrives in an earlier parameter, and the ones after it areundefined. - A union of two collection types cannot call an overloaded method:
(cond ? collect([1]) : collect({ a: 'x' })).filter(cb)does not compile. Wrap the union instead:collect(cond ? [1] : { a: 'x' })merges its members into one collection type. partition()'s[0]and[1]survive a method that returnsthis:c.partition(cb).filter(cb2)[0]compiles, though the filtered collection has no such member.select()offers a primitive's or a class item's prototype members as keys (collect(['ab']).select('length')) and types a class item's methods as selected, though only own fields are copied.- On a wide item type (
Collection<unknown>, orobject,{}orFunctionitems), a mis-typedcontains()orevery()callback such as(v: string) => …compiles; a narrower item type rejects it. - A misspelt
ensure()type name compiles, since any string may name a class:ensure('interger')narrows the items toobject, and the runtime throws for the items it rejects. Collection.wrap()of a class instance, anErroror a typed array is typed as keyed by its members, as a plain object is, though the runtime wraps it as one item.- A plain object with an
all()member is typed as Collection-like byflatten(),mapSpread()andeachSpread(), and the operand methods (merge(),mergeRecursive(),concat(),union(),replace(),replaceRecursive(),zip(),crossJoin(),combine(),diffUsing(),intersectUsing()) type none of its function-valued members, while the runtime treats every plain object as data:collect([1]).merge({ all: () => ['s'] })is typed as holding numbers alone but keeps the function under the key'all'. - A plain object with a
toArray()member is typed as an Arrayable bycollect()and bytoArray()'s item conversion (collect([{ toArray: () => [9] }]).toArray()is typednumber[][]), though at runtime it is data and comes back unchanged. The operand methods likewise type a plain object with atoArray(),toJson()orjsonSerialize()member as the items that member would hand over:collect([1]).merge({ toArray: () => ['s'] })is typedCollection<string | number, number, "list">, while the runtime keeps the function under the key'toArray'. - A
symbolkey compiles input()andprepend()on a collection typed withPropertyKeykeys, such asCollection.fromJson('{}'), and the runtime stores it under its string form,'Symbol(s)'.