loophp / collection Goto Github PK
View Code? Open in Web Editor NEWA (memory) friendly, easy, lazy and modular collection class.
Home Page: https://loophp-collection.rtfd.io/
License: MIT License
A (memory) friendly, easy, lazy and modular collection class.
Home Page: https://loophp-collection.rtfd.io/
License: MIT License
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.
composer.json
php >= 8.1
loophp/iterators ^3.2.0
amphp/parallel-functions ^1
doctrine/collections ^2
drupol/php-conventions ^5
infection/infection ^0.27 || ^0.28 || ^0.29
loophp/phpunit-iterable-assertions ^1.0
phpstan/phpstan-strict-rules ^1.0
phpunit/php-code-coverage ^10
phpunit/phpunit ^10
psr/cache ^2.0 || 3.0
symfony/cache ^6
vimeo/psalm ^5
.github/workflows/code-style.yml
actions/checkout v4
shivammathur/setup-php v2
ramsey/composer-install v2
.github/workflows/documentation.yml
actions/checkout v4
actions/checkout v4
actions/upload-artifact v4
actions/create-release v1
actions/download-artifact v4
actions/upload-release-asset v1
.github/workflows/mutation-tests.yml
actions/checkout v4
shivammathur/setup-php v2
ramsey/composer-install v2
.github/workflows/prettier.yml
actions/checkout v4
.github/workflows/prune.yaml
actions/stale v9
.github/workflows/release.yaml
actions/checkout v4
mindsers/changelog-reader-action v2
actions/create-release v1.1.4
.github/workflows/static-analysis.yml
actions/checkout v4
shivammathur/setup-php v2
ramsey/composer-install v2
.github/workflows/tests.yml
actions/checkout v4
WyriHaximus/github-action-composer-php-versions-in-range v1
actions/checkout v4
shivammathur/setup-php v2
ramsey/composer-install v2
This morning I discovered this: https://packagist.org/packages/halaxa/json-machine
I think we should provide an example and documentation on how to use it with this collection library.
Something like this?
With Guzzle:
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $file);
$phpStream = \GuzzleHttp\Psr7\StreamWrapper::getResource($response->getBody());
$json = Collection::fromIterable(\JsonMachine\JsonMachine::fromStream($phpStream));
foreach ($json as $key => $value) {}
With Symfony client:
<?php
/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App;
use JsonMachine\JsonMachine;
use loophp\collection\Collection;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\ChunkInterface;
include __DIR__ . '/vendor/autoload.php';
$file = 'https://httpbin.org/anything';
//$file = 'https://github.com/json-iterator/test-data/blob/master/large-file.json?raw=true';
$client = HttpClient::create();
$response = $client->request('GET', $file);
$json = Collection::fromIterable(
JsonMachine::fromIterable(
Collection::fromIterable($client->stream($response))
->map(
static function(ChunkInterface $chunk): string {
return $chunk->getContent();
}
)
)
);
foreach ($json as $key => $value) {}
PHPStan 1.0 has been released today.
We have to make sure that PHPStan is still running fine on loophp/collection
by either fixing the new discovered issues and/or if not applicable by adding them to the baseline files.
We should also make sure it run with the new level 9 that has been introduced in it.
To run phpstan:
./vendor/bin/phpstan analyze --level 9 src/ tests/static-analysis/
I've come across an issue in which the get
and cache
methods don't seem to be behaving correctly when used with a PDO result set, as in the following code:
<?php
use \loophp\collection\Collection;
$fromDate = '2023-09-15 00:00:00';
$toDate = '2023-09-30 23:59:59';
$query = "SELECT *
FROM communication
WHERE `date` BETWEEN :fromDate AND :toDate";
$statement = $pdo->prepare($query);
$statement->bindValue('fromDate', $fromDate, PDO::PARAM_STR);
$statement->bindValue('toDate', $toDate, PDO::PARAM_STR);
$statement->execute();
$statement->setFetchMode(PDO::FETCH_ASSOC);
echo $statement->rowCount();
$collection = Collection::fromIterable($statement)->cache();
$collection->get(0);
$collection->get(0);
foreach ($collection as $i=> $r){
var_dump($i, $r);
}
This produces the following output (formatted, and with some fields removed for brevity):
3
int 0
array (size=8)
'id' => int 26580
'user_id' => int 45
'type' => string 'phone' (length=5)
'remarks' => string '<p>test test</p>' (length=16)
'date' => string '2023-09-26 09:04:00' (length=19)
The executed query returns three results, however, when I iterate over the collection, I get a single result. If I comment out both of the $collection->get(0);
, however, I get the expect results:
3
int 0
array (size=8)
'id' => int 26580
'user_id' => int 45
'type' => string 'phone' (length=5)
'remarks' => string '<p>test test</p>' (length=16)
'date' => string '2023-09-26 09:04:00' (length=19)
int 1
array (size=8)
'id' => int 26581
'user_id' => int 45
'type' => string 'fax' (length=3)
'remarks' => string '<p>Flagged it</p>' (length=17)
'date' => string '2023-09-26 09:07:00' (length=19)
int 2
array (size=8)
'id' => int 26582
'user_id' => int 45
'type' => string 'home_visit' (length=10)
'remarks' => string '<p>ffdfhfhd</p>' (length=15)
'date' => string '2023-09-26 09:07:00' (length=19)
At first, it made me wonder if get
was intended to remove items from the collection. However, I also get my expected results (all three rows) with either of the two following changes to my code: (using squash
or using fetchAll
)
<?php
use \loophp\collection\Collection;
$fromDate = '2023-09-15 00:00:00';
$toDate = '2023-09-30 23:59:59';
$query = "SELECT *
FROM communication
WHERE `date` BETWEEN :fromDate AND :toDate";
$statement = $pdo->prepare($query);
$statement->bindValue('fromDate', $fromDate, PDO::PARAM_STR);
$statement->bindValue('toDate', $toDate, PDO::PARAM_STR);
$statement->execute();
$statement->setFetchMode(PDO::FETCH_ASSOC);
echo $statement->rowCount();
$collection = Collection::fromIterable($statement->fetchAll())->cache();
$collection->get(0);
$collection->get(0);
foreach ($collection as $i=> $r){
var_dump($i, $r);
}
<?php
use \loophp\collection\Collection;
$fromDate = '2023-09-15 00:00:00';
$toDate = '2023-09-30 23:59:59';
$query = "SELECT *
FROM communication
WHERE `date` BETWEEN :fromDate AND :toDate";
$statement = $pdo->prepare($query);
$statement->bindValue('fromDate', $fromDate, PDO::PARAM_STR);
$statement->bindValue('toDate', $toDate, PDO::PARAM_STR);
$statement->execute();
$statement->setFetchMode(PDO::FETCH_ASSOC);
echo $statement->rowCount();
$collection = Collection::fromIterable($statement)->cache()->squash();
$collection->get(0);
$collection->get(0);
foreach ($collection as $i=> $r){
var_dump($i, $r);
}
I also tried a similar thing with a generator function, and got the results I expected:
<?php
use \loophp\collection\Collection;
function getn(){
for($i=1; $i < 5; $i++) yield $i;
}
$collection = Collection::fromIterable(getn())->cache();
$collection->get(0);
$collection->get(0);
foreach ($collection as $r){
var_dump($r);
}
int 1
int 2
int 3
int 4
So, in every case except the very first one, get
leaves the item in the collection. For some reason though, when I try to convert an executed PDO statement into a Collection, it doesn't. Even though I used cache
to make it rewindable. Still looking to see if there are any other iterable types that have the same weird behavior as PDO.
Hello :),
I'm a little embarrassed with some API methods :
->first()
and ->last
behaviourWhy first
and last
don't simply return the first / last element in the collection ? It's the behaviour of all collection implementation I know and it sounds so natural.
I could understand that it would be interesting to have methods returning a new collection of a single first/last element, but I wouldn't have used ->first
/ ->last
for that :(.
To me :
public function head(int $size = 1): CollectionInterface
public function tail(int $size = 1): CollectionInterface
would have been great candidates for that !
->head()
and ->tail
behaviourIn addition, I'm confused that head
is an alias for first
but tail
as nothing to do with last
and is then not the mirror of head
(why ?).
This indirectly leads to another issue :
Is it possible to have autocompletion on the type of the first / last element without using shortcut ? Let's take this example :
<?php
use loophp\collection\Collection;
class Random
{
public function __construct(
private int $min,
private int $max,
)
{}
public function generateRandom(): int
{
return rand($this->min, $this->max);
}
}
/**
* @var Collection<int, Random> $collection
*/
$collection = Collection::fromIterable([
new Random(0, 5),
new Random(1, 100)
]);
$firstElementUsingShortcut = $collection->current(); // -> has IDE autocompletion
$firstElement = $collection->first()->current(); // -> NO autocompletion
$lastElement = $collection->last()->current(); // -> NO autocompletion
contains
vs has
To me, contains
and has
has exactly the same meaning for a collection (to me it's an alias). The difference in the API is that contains
accepts values and has
accepts callables. Wouldn't it be preferable to allow both types like illuminate/collections ? Is the reason for this to be able to know if the colection contains a specific closure ?
I was working on a talk about this library for next year and while doing some code snippets, I noticed some issues when trying to get at the same time the current key
and current value
out of the collection.
To reproduce the issue:
<?php
declare(strict_types=1);
namespace App;
use loophp\collection\Collection;
include __DIR__ . '/../vendor/autoload.php';
$input = array_combine(range(1, 26), range('a', 'z'));
$collection = Collection::fromIterable($input)
->last();
dump($collection->key()); // Expected: 26, Actual: 0
dump($collection->current()); // Expected: z, Actual: null
$collection = Collection::fromIterable($input)
->map(static fn ($i) => $i)
->last();
dump($collection->key()); // Expected: 26, Actual: null
dump($collection->current()); // Expected: z, Actual: null
Collection::fromIterable(['b', 1, 'foo', 'bar'])
->has(
static function ($key, $value) {
return 'foo';
}
);
This should return [True]
but returns [False]
.
Currently there is no method to return the first collection item that passes a given predicate; this would be a shorthand for filter($callable)->first()
, but is useful nevertheless.
firstWhere
(for values) or search
(for keys)find
search
find_index
It would be nice to have a method to quickly search for an item. I'd propose a syntax like the following:
/**
* @template T
*/
interface Searchable
{
/**
* @return T|null
*/
public function search(callable ...$callbacks): mixed;
}
Hello there :),
I'm wondering if there would be a way to support typed collection using this library (like in ramsey/collection) ?
The main goal would be to avoid repeating code like :
class Customer
{
public function addAddresses(\loophp\collection\Collection $addresses)
{
Assert::allIsInstanceOf($addresses, Address::class);
// continue process ...
}
}
It took me a while to finally find this https://loophp-collection.readthedocs.io/en/latest/pages/usage.html
I suggest maybe putting a portion of those usage examples in the readme to increase adoption of this library.
Hello,
I have a question related to typed collections.
All classes in this library are final and we are expected to use the composition pattern.
However I would like to use typed collections eg. PersonCollection as the collection of Person entities, including typehinting in business methods.
PHP Stan / PSALM generics are not enough for my use case as I would like to add some specific methods eg. AddressCollection getter to (and only to) the PersonCollection.
The implementation of my use-case seems more complicated when using composition as opposed to some AbstractCollection extension as I will have to implement all the basic CollectionInterface methods including count()
or getIterator()
.
I understand that this is a library concept, I just want to make sure I understand it correctly.
Or is there some possibility that hasn't occurred to me? Of course I can think of e.g. creating the mentioned AbstractCollection which will no longer be final, but I don't know if that's a conceptual solution.
Thanks!
I am looking to replace usage of Doctrine Collections with this library in a project, and one of the issues I've come across is the usage of partition
. Its return type is a bit confusing at first glance and replicating the functionality of the Doctrine Collections method does not seems straightforward for a new user.
The return type of the Doctrine Collection Partition method is very straightforward. It returns an array shape with exactly two elements consisting of the same collection objects with the same type for the keys and values. Because of this, usage with the PHP list language construct is possible and very convenient:
$data = [
['id' => 'ABC', 'name' => 'Bob'],
['id' => 'BAC', 'name' => 'John'],
['id' => 'ABC', 'name' => 'Mike'],
];
[$matches, $nonMatches] = (new ArrayCollection($data))
->partition(static fn (int $key, array $entity): bool => $entity['id'] === 'ABC');
The return type of the partition operation is a collection of lists of arrays of an array shape where the key and value of the elements is included (the nesting is real!).
The first issue is that this operation will always return exactly two elements, but this is not immediately visible from the return type and I'm not sure how this can be fixed because we cannot specify collection shape in the same way as array shape.
The other main issue is the combination of operations needed to achieve the same behaviour as Doctrine Collections: I just want to be able to get the two separate collections with the values inside them, nothing more. This is what I've come up with:
$partitioned = Collection::fromIterable($data)
->partition(static fn(array $entity): bool => $entity['id'] === 'ABC');
$matches = $partitioned->first()->pluck('*.1')->unwrap();
$nonMatches = $partitioned->last()->pluck('*.1')->unwrap();
A) don't return the keys and the values separately. Rather, they should be together just like in the original collection, before the partition
B) can we reduce the nesting so the unwrap()
is not necessary?
C) could we return an array shape so that the list
construct can be used? I realise this is a significant difference, however otherwise we rely on the user "knowing" that this operation will only return two elements in the collection - something that's not currently captured in the type annotations
Hello,
Firstly, thank you for creating this library! I was also looking into writing a Collections library to replace the Doctrine Collections I typically use in Symfony projects because I couldn't find any good replacement with sufficient functionality, strict typing, proper code analysis and code quality checks etc. And just today I found your library, which looks awesome! ๐
I have two main questions:
I can't see any flatMap
operation being available (also called bind
in other languages). I know one can do map
+ flatten
but I think it's a common enough operation that having it would be nice. What do you think? I'd be happy to contribute!
If I want to apply a collection operation eagerly rather than lazily in a certain instance, but I still want to keep the collection rather than transform to an array, what's the best way to do it? Is it to call apply()
? Let's say I'm calling map()
with a function that throws an exception, and then returning the collection. I want the exception to be thrown at that level rather than at a level higher, where the collection is used. Due to the lazy nature, simply calling map
will not cause the exception to be thrown.
I hope I've explained this well enough but let me know if I should clarify or provide an example! Thanks!
## Steps required to reproduce the problem
/**
* Check if a number is a multiple of 7.
*
* @param $value
* The number.
*
* @return bool
* Whether or not the number is a multiple of 7.
*/
$notMultipleOf7 = static function ($value): bool {
$number = $value;
while (14 <= $number) {
$lastDigit = mb_substr((string) $number, -1);
if ('0' === $lastDigit) {
return true;
}
$number = (int) abs((int) mb_substr((string) $number, 0, -1) - 2 * (int) $lastDigit);
}
return !(0 === $number || 7 === $number);
};
echo notMultipleOf7 (70);
False
True
See following code example:
Collection::fromIterable([])->pair()->all(false)
It was expected that code would return empty array []
But it returns following garbage information:
array:1 [
"" => null
]
This code takes about 3 minutes to execute:
$collection1 = Collection::range(start: 1, end: 1001); // [1,2,3,...,1_000] (count: 1_000)
$collection2 = Collection::range(start: 1, end: (30 * 1000) + 1); // [1,2,3,...,,30_000] (count 30_000)
$result = $collection1->merge($collection2)->distinct()->all();
self::assertCount(30000, $result);
similar code written in pure PHP executes in less than a second:
$collection1 = range(start: 1, end: 1000); // [1,2,3,...,1_000] (count: 1_000)
$collection2 = range(start: 1, end: 30 * 1000); // [1,2,3,...,30_000] (count: 30_000)
$result = array_unique(array_merge($collection1, $collection2));
self::assertCount(30000, $result);
"php": ">=8.3",
"loophp/collection": "^7.0",
Is there any way to speed up these operations?
[['id' => 1, 'name' => 'A'], ['id' => 2, 'name' => 'B'], ['id' => 1, 'name' => 'C']]
Collection::with(arr)->groupBy(function($k, $v) { return $v['id']; })->all()
[ 1 => [ ['id' => 1, 'name' => 'A'], ['id' => 1, 'name' => 'C'] ], 2 => [ ['id' => 2, 'name' => 'B'] ] ]
[ 1 => [ ['id' => 1, 'name' => 'A', [ 0 => ['id' => 1, 'name' => 'C'] ] ], 2 => [ ['id' => 2, 'name' => 'B'] ] ]
Maybe my understanding of what the group function is supposed to do is incorrect by I was thinking it is more in line with what I expect the result to be. The problem seems to be that first time value is set, it is set as is. Second time, it is converted to array and then appended which cause the behavior above. So that means right now, the above would only work for simple values and not arrays or objects.
Hey ๐!
I have this code:
$this->commandList
->filter(static fn (CommandInterface $command) => $command instanceof $commandClass)
->count();
and I'm receiving such PHPStan error since upgrading PHPStan to 1.8.* (now using 1.9.*):
Parameter ...$callbacks of method loophp\collection\Contract\Operation\Filterable<int,CommandInterface>::filter() expects
callable(CommandInterface=, int=, Iterator<int, CommandInterface>=): bool,
Closure(CommandInterface): bool given.
How to manage with this problem? I'm using v6.0.3 of loophp/collection
. Thank you in advance for any help.
Hello ๐.
Let's assume I have an entity:
final readonly class MyEntity
{
public function __construct(
public int $id,
) {
}
}
When operating on collections with high amount of elements (1000 - 2000), the system crashes because of memory size being exhausted. The case can be easily simulated with a PHPUnit test:
public function testCollection(): void
{
$input = $this->findAllFromRepository()->shuffle();
self::assertEquals(2000, $input->count());
}
// Just a separate method which "simulates" to be a method from a repository
public function findAllFromRepository(): Collection
{
$input = [];
for ($i = 0; $i < 2000; ++$i) {
$input[] = new MyEntity(id: $i);
}
return Collection::fromIterable($input);
}
On my side the limit is set on 128 MB:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes)
The crash happens in most of the test runs (so not all of them). It's triggered by either $collection->count()
or $collection->all()
. When using PHP shuffle(...)
, the test passes without the crash:
public function testArrayShuffle(): void
{
$input = $this->findAllFromRepository()->all();
shuffle($input);
$collection = Collection::fromIterable($input);
self::assertEquals(2000, $collection->count());
}
It is also all fine when working on arrays only (not using collection at all in the code).
What I could do in such a case? Isn't there some kind of memory leak? This issue came from my real-world example. Of course, it has a bit more operations on the collection, but even the simple as above fails. I'd rather to stay with collections to have a fine API working on my data, but I'm not sure if I can.
Thanks in advance for any help ๐.
PHP 8.0 introduces new expression named match, which is BC break for classes and functions named same as keyword "match"
https://github.com/loophp/collection/blob/master/src/Operation/Match.php
The user should be able to use the distinct
operation with a collection of object.
Currently, we use ===
in the Distinct operation to compare values, however, it happens sometimes that we want to customize
how to access to the values to compare.
Code example
<?php
/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App;
use loophp\collection\Collection;
include __DIR__ . '/vendor/autoload.php';
class User {
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function name(): string
{
return $this->name;
}
};
$users = [
new User('a'),
new User('b'),
new User('a'),
new User('c'),
];
$collection = Collection::fromIterable($users)
->distinct();
print_r($collection->all());
In this particular case, all the users will be returned and the Distinct function will not work.
The idea is to let users customize how values should be accessed and/or compared.
Let users customize how to access to the value to compare.
$collection = Collection::fromIterable($users)
->distinct(static fn(User $user): string => $user->name());
Let users customize how to access to the value to compare and how to compare them.
$collection = Collection::fromIterable($users)
->distinct(static fn(User $left, User $right): bool => $left->name() === $right->name());
Mix the two previous proposals:
$collection = Collection::fromIterable($users)
->distinct(
static fn(User $user): string => $user->name(),
static fn(string $left, string $right): bool => $left === $right
),
I tested both proposals and I'm unable to decide which way to go.
Therefore, this issue is to decide on which way to go.
I might not be using this accurately, but here's the issue I encountered.
I am attempting to use the append method.
$collection = Collection::with(['A', 'B', 'C', 'D', 'E']);
$this->collection = $collection
->append('F', 'G', 'H')
->all(); // ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
public function getCollection()
{
return $this->collection;
}
Attached repository for reproduction.
Hey ๐
use loophp\collection\Contract\Collection;
function func(Collection $collection): int {
$collection->filter(...)->count();
}
PHPStan is telling me that:
Call to an undefined method loophp\collection\Contract\Collection<int, object>::count()
I think the issue is with missing Countable
interface on Collection
interface.
It's well-documented both when reading about the all operation and in the section working with keys and values that when converting a Collection
to a PHP array, potential data loss can occur.
This behaviour stems from the flexibility that PHP Generators offer in terms of using multiple times the same key for example. However, in multiple discussions I've had it seemed that for the user this is an "implementation detail", and having to account for this potential data loss can become a cognitive burden.
$generator = static function (): Generator {
yield 0 => 'a';
yield 1 => 'b';
yield 0 => 'c';
yield 1 => 'd';
};
$collection = Collection::fromIterable($generator())
->all(); // [0 => 'c', 1 => 'd']
$collection = Collection::fromIterable($generator())
->normalize()
->all(); // [0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd']
I'm curious to understand why dealing with this behaviour is left to the user, rather than it being handled by the package itself. Most of the times we can safely assume that the user would prefer to keep the data when converting to a PHP array. Many PHP array functions, such as array_merge
, automatically re-index the keys; this means people are used to this behaviour already.
I would like to propose that the all
operation automatically applies normalize
before converting to an array. This removes the need for the user to remember to do this and simplifies the API.
all
in order to apply the normalization. The existing behaviour would be kept by default. Thus, the signature becomes:public function all(bool $normalize = false): array
Break backwards compatibility, but still allow old behaviour -> similar to Option 1 but the parameter is $normalize = true
by default. This ensures that if the user wants the old behaviour they can still have it, but by default the operation will now prevent data loss.
Break backwards compatibility, remove old behaviour altogether -> in this version we keep the function signature intact but we always apply normalize
. This leads to a simpler and easier to use API.
Let me know what you think @drupol :).
I'm not sure if this is a bug or if I just don't understand how this is supposed to work.
The documentation for Collection::apply() states:
Execute a callback for each element of the collection without altering the collection item itself.
If the callback does not return true then it stops.
To me, this implies functionality similar to foreach. Returning false
should stop the callback from executing for the items that follow.
In reality the apply() method executes the callback for every item regardless of the return value. However, if you supply several callbacks, returning false
from one of them stops execution of the following callbacks, which is not the same.
After the following example, I would expect $count === 2
but in reality $count === 3
.
$count = 0;
$fn = function($value) use (&$count) {
$count++;
if ($value === 'b') {
return false;
}
return true;
}
Collection::fromIterable(['a', 'b', 'c'])
->apply($fn)
->all();
After running this code, with two callbacks (the same $fn
from above), I would expect $count === 2
, or possibly $count === 4
, but in reality $count === 5
:
$count = 0;
Collection::fromIterable(['a', 'b', 'c'])
->apply($fn, $fn)
->all();
The callbacks will be called in this order:
'a': callback 1, callback 2
'b': callback 1,
'c': callback 1, callback 2
If this is the intended behaviour, is there another way to achieve foreach-like behaviour?
Thanks for creating this library, the Readme lists similar collection libraries as inspiration, I think it would be really helpful for the project to also highlight the key points that distinguish it from these libraries.
I think a few notes about differences in api design between the libraries would be quite useful and help people decide which one to pick.
I would especially like to know how this library compares to https://github.com/DusanKasan/Knapsack and https://github.com/mtdowling/transducers.php
null or ''
*
[0 => '']
*
/** @psalm-trace $test */
$test = Collection::fromIterable(self::cases())
->associate(
fn (int $key, ApplicationGrade $grade) => $grade->value,
fn (ApplicationGrade $grade) => sprintf('%d - %s', $grade->value, self::transformKey($grade->name))
)->all();
This trace results in psalm reporting the following
psalm: MixedAssignment: Unable to determine the type that $test is being assigned to
If I do normalize first it is able to infer the type
/** @psalm-trace $test2 */
$test2 = Collection::fromIterable(self::cases())
->normalize()
->associate(
fn(int $key, ApplicationGrade $grade) => $grade->value,
fn(ApplicationGrade $grade) => sprintf('%d - %s', $grade->value, self::transformKey($grade->name))
);
The result is
psalm: Trace: $test2: loophp\collection\Contract\Collection<1|2|3|4|5, non-empty-string>
If i change the @return
annotation to one of the following examples it works
/**
* @template UKey
* @template U
*
* @param iterable<UKey, U> $iterable
*
* //THIS WORKS
* @return CollectionInterface<UKey, U>&self<UKey, U>
* @return CollectionInterface<UKey, U>
*
* //THIS DOES NOT WORK
* @return self<UKey, U>
*/
public static function fromIterable(iterable $iterable): CollectionInterface
{
return new self(static fn (): Generator => yield from new IterableIteratorAggregate($iterable));
}
I'm happy to open a pull request but wanted to know what is preferred to do here. It might be that a fix is needed in Psalm (not familiar with that codebase). PHPStan does not seem to have any issues with the examples above and the fix does not seem to break PHPStan
Hey ๐. I have this code:
$test = Collection::fromCallable(static function () {
yield 100 => 'a';
yield 200 => 'b';
yield 300 => 'c';
yield 400 => 'd';
})->cache();
$result1 = $test->get(100)->current();
$result2 = $test->get(200)->current();
$result3 = $test->get(300)->current();
$result4 = $test->get(400)->current();
With each run, the result is:
Using the latest loophp/collection (6.0.3) and PHP 8.1. Is it a bug, or am I doing something wrong ๐?
Hello ๐.
Let's assume to have this simple value object in the project:
final readonly class MyValueObject
{
public function __construct(
public int $id,
public int $weight,
) {
}
}
I need a feature of sorting such value objects by weight. The major thing about it is that the sorting itself must be stable. Meaning, two objects of the same weight should not exchange their position with each other within a single list. This can be tested with such a test:
public function testStableSorting(): void
{
$input = Collection::fromIterable([
new MyValueObject(id: 1, weight: 1),
new MyValueObject(id: 2, weight: 1),
new MyValueObject(id: 3, weight: 1),
])
->sort(callback: static fn (MyValueObject $a, MyValueObject $b): int => $a->weight <=> $b->weight)
->map(static fn (MyValueObject $item): int => $item->id)
->all();
self::assertEquals([1, 2, 3], $input);
}
Unfortunately, the test fails as the result stored in the $input
variable is [1, 3, 2]
. Meaning: sorting is not stable.
Such thing was already addressed in the past for PHP. On 2022 Nikita Popov made PHP sorting stable. Hence, using usort(...)
function in the above example will make the test to pass.
Would it be possible to implement stable sorting in loophp/collection?
As described here, user experience would be improved if reduction operations would return a value directly, rather than a collection.
While in some cases it might be useful to have these operations return a collection, it's much more common for users to expect these to return a single value; after all, that's the typical behaviour of reductions. This is the case in other languages (just a couple examples):
This change would make Collection easier to use and also make it easier to differentiate between some operations, like Reduce (will return a single value) and Reduction (will continue to return a collection).
$result = Collection::fromIterable([1, 4, 3, 0, 2])->min();
// 0
// Collection([4 => 4])
I have two arrays:
$base_list = Collection::fromIterable([1, 2]);
$excluded_list = Collection::fromIterable([2])->distinct();
And I want to efficiently remove elements from the base list using the second list:
$excluded_map = $excluded_list->associate(
callbackForKeys: static fn (int $key, $nid) => $nid,
callbackForValues: static fn ($value) => $value,
);
$result = $base_list->filter(static fn ($row) => $excluded_map->get($row) === null);
I found that something is wrong with ->distinct(). When I remove it, the code above works fine.
It seems to me that 'distinct' shouldn't have an impact on the outcome in this case. Am I mistaken ๐?
I'm wondering if there's same feature as php plus operator:
public function testPlusOperation(): void
{
$initialArray =[
'id' => ['A', 'B'],
'id2' => ['C'],
];
$resultArray = $initialArray + ['id' => [], 'id2' => [], 'id3' => []];
self::assertSame([
'id' => ['A', 'B'],
'id2' => ['C'],
'id3' => [],
], $resultArray);
}
As of me, personally I often use plus operation on arrays and it would make sense to implement it within a collection.
public function testCollectionPlusOperation(): void
{
$initialCollection = Collection::fromIterable([
['id' => 'A'],
['id' => 'B'],
['id2' => 'C'],
]);
$result = $initialCollection
->unwrap()
->groupBy(static fn ($value, $key) => $key)
->plus(['id' => [], 'id2' => [], 'id3' => []])
->all(false)
;
self::assertSame([
'id' => ['A', 'B'],
'id2' => ['C'],
'id3' => [],
], $result);
}
prepend
With current implementation seemingly the same behavior may be achieved using prepend:
$result = $initialCollection
->unwrap()
->groupBy(static fn ($value, $key) => $key)
->prepend(...['id' => [], 'id2' => [], 'id3' => []])
->all(false)
;
self::assertSame([
'id' => ['A', 'B'],
'id2' => ['C'],
'id3' => [],
], $result);
However it is not explicit solution, since the collection maintains all the keys until all()
is called.
public function testPrependedKeysAreStillPresent(): void
{
$initialCollection = Collection::fromIterable([
['id' => 'A'],
['id' => 'B'],
['id2' => 'C'],
]);
$result = $initialCollection
->unwrap()
->groupBy(static fn ($value, $key) => $key)
->prepend(...['id' => [], 'id2' => [], 'id3' => []])
->pack()
->all()
;
self::assertSame([
['id', []],
['id2', []],
['id3', []],
['id', ['A', 'B']],
['id2', ['C']],
], $result);
}
Therefore, when using prepend
, we must convert collection into array before we can safely use the result, lest same instance iteration would yield id
and id2
keys twice and id3
key before the rest.
Please, let me know what you think about this plus()
proposal.
When using both an implementation and an interface of collections, there's a need for making an alias:
<?php declare(strict_types=1);
namespace App;
use loophp\collection\Collection;
use loophp\collection\Contract\Collection as CollectionInterface;
final class MyClass
{
}
Would it be possible to change the Collection
interface onto CollectionInterface
? It would be a lot less confusing I think.
class Quantity {
public float $value;
public string $unitCode;
}
// psalm error here
$qtys = $value = Collection::fromIterable($qtys)->map(fn (Quantity $qty): float => $qty->value);
The canonical type definition of map
map : (T -> V) -> List T -> List V
,
where T and V are generic type variables, as far as i know there is no requirement of V being a sub type of T in other languages
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.