Git Product home page Git Product logo

collection's Introduction

Try! Latest Stable Version GitHub stars Total Downloads GitHub Workflow Status Scrutinizer code quality Type Coverage Code Coverage Mutation testing badge License Donate!

PHP Collection

Description

Collection is a functional utility library for PHP greater than 7.4, including PHP 8.0.

It's similar to other collection libraries based on regular PHP arrays, but with a lazy mechanism under the hood that strives to do as little work as possible while being as flexible as possible.

Functions like array_map(), array_filter() and array_reduce() are great, but they create new arrays and everything is eagerly done before going to the next step. Lazy collection leverages PHP's generators, iterators, and yield statements to allow you to work with very large data sets while keeping memory usage as low as possible.

For example, imagine your application needs to process a multi-gigabyte log file while taking advantage of this library's methods to parse the file. Instead of reading and storing the entire file into memory at once, this library may be used to keep only a small part of the file in memory at a given time.

On top of this, this library:

Except for a few methods, most methods are pure and return a new Collection object.

Also, unlike regular PHP arrays where keys must be either of type int or string, this collection library lets you use any kind of type for keys: integer, string, object, array, ... anything! This library could be a valid replacement for \SplObjectStorage but with much more features. This way of working opens up new perspectives and another way of handling data, in a more functional way.

And last but not least, collection keys are preserved throughout most operations; while it might lead to some confusion at first, please carefully read this example for the full explanation and benefits.

This library has been inspired by:

Features

  • Decoupled: Each Collection method is a shortcut to one isolated standard class, each operation has its own responsibility. Usually, the arguments needed are standard PHP variables like int, string, callable or iterator. It allows users to use those operations individually, at their own will, to build up something custom. Currently, more than 100 operations are available in this library. This library is an example of what you can do with all those small bricks, but nothing prevents users from using an operation on its own as well.

  • It takes function first, data-last: In the following example, multiple operations are created. The data to be operated on is generally supplied at last.

    <?php
    
    $input = ['foo', 'bar', 'baz'];
    
    // Using the Collection library
    $collection = Collection::fromIterable($input)
        ->filter(static fn(string $userId): bool => 'foo' !== $userId)
        ->reverse();
    
    foreach ($collection as $item); // ['baz','bar']
    
    // Using single operations.
    $pipe = Pipe::of()(
      Reverse::of(),
      Filter::of()($filterCallback)
    );
    
    foreach ($pipe($input) as $item); // ['baz','bar']

    More information about this in the Brian Lonsdorf's conference, even if this is for JavaScript, those concepts are common in other programming languages.

    In a nutshell, the combination of currying and function-first enables the developer to compose functions with very little code (often in a β€œpoint-free” fashion), before finally passing in the relevant user data.

  • Operations are stateless and curried by default: This currying makes it easy to compose functions to create new functions. Because the API is function-first, data-last, you can continue composing and composing until you build up the function you need before dropping in the data. See this Hugh Jackson article describing the advantages of this style.

    In the following example, the well-known flatMap could be composed of other operations as such:

    <?php
    
    $input = ['foo,bar', 'baz,john'];
    
    $flatMap = static fn (callable $callback) =>
      Pipe::of()(
          Map::of()(static fn(string $name): array => explode(',', $name)),
          Flatten::of()(1)
      );
    
    foreach ($flatMap($input) as $item); // ['foo', 'bar', 'baz', 'john']

Installation

composer require loophp/collection

Usage

Check out the usage page for both trivial and more advanced use cases.

Dependencies

Documentation

On top of well-documented code, the package includes a complete documentation that gets automatically compiled and published upon each commit at https://loophp-collection.rtfd.io.

The Collection Principles will get you started with understanding the elements that are at the core of this package, so you can get the most out of its usage.

The API will give you a pretty good idea of the existing methods and what you can do with them.

We are doing our best to keep the documentation up to date; if you found something odd, please let us know in the issue queue.

Code quality, tests, benchmarks

Every time changes are introduced into the library, Github runs the tests.

The library has tests written with PHPUnit. Feel free to check them out in the tests/unit/ directory. Run composer phpunit to trigger the tests.

Before each commit, some inspections are executed with GrumPHP; run composer grumphp to check manually.

The quality of the tests is tested with Infection a PHP Mutation testing framework - run composer infection to try it.

Static analyzers are also controlling the code. PHPStan and PSalm are enabled to their maximum level.

Contributing

Feel free to contribute by sending pull requests. We are a usually very responsive team and we will help you going through your pull request from the beginning to the end, read more about it in the documentation.

For some reasons, if you can't contribute to the code and willing to help, sponsoring is a good, sound and safe way to show us some gratitude for the hours we invested in this package.

Sponsor me on Github and/or any of the contributors.

On the internet

Changelog

See CHANGELOG.md for a changelog based on git commits.

For more detailed changelogs, please check the release changelogs.

collection's People

Contributors

alexandrugg avatar aszenz avatar curryed avatar ddebowczyk avatar dependabot[bot] avatar drupol avatar jdreesen avatar lctrs avatar matth-- avatar mxr576 avatar radiergummi avatar rela589n avatar renovate[bot] avatar seblours avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

collection's Issues

Key and Current operations cannot be used at the same time.

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

Append functionality.

I might not be using this accurately, but here's the issue I encountered.

Steps required to reproduce the problem

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;
    }

Expected Result

  • getCollection should return ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

Actual Result

  • ['F', 'G', 'H', 'D', 'E']

Attached repository for reproduction.

https://github.com/benelori/collections

Reduction operations should return a single value

Context

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).

Steps required to reproduce the problem

$result = Collection::fromIterable([1, 4, 3, 0, 2])->min();

Expected Result

// 0

Actual Result

// Collection([4 => 4])

Operations

  • compare, min, max
  • foldLeft, foldLeft1
  • foldRight, foldRight1
  • reduce

Consider adding a comparison with this library and similar collection libraries

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

[Question] Rename Collection interface to CollectionInterface

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.

Parse JSON in a lazy way

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) {}

Palm cannot infer types when using some operations

Steps required to reproduce the problem

/** @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

Steps for normal type inference

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>

Actual fix

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));
}

Question

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

apply() functionality

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?

There is wrong example of Not multiple of seven in documentation

## 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);

Expected Result

False

Actual Result

True

why does map expect return type to be a subtype

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

Let users customize how to access a value in the Distinct operation.

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.

How to fix?

The idea is to let users customize how values should be accessed and/or compared.

Proposal 1

Let users customize how to access to the value to compare.

$collection = Collection::fromIterable($users)
    ->distinct(static fn(User $user): string => $user->name());

Proposal 2

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());

Proposal 3

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.

API oddities

Hello :),

I'm a little embarrassed with some API methods :

1. The weirdest to me : ->first() and ->last behaviour

Why 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 !

2. ->head() and ->tail behaviour

In 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 ?).

3. Missing autocompletion

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

4. 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 ?

Add find/search/where/single/firstWhere method

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.

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;
}

Unexpected behavior of pair operation over empty collection

Steps required to reproduce the problem

See following code example:

Collection::fromIterable([])->pair()->all(false)

Expected Result

It was expected that code would return empty array []

Actual Result

But it returns following garbage information:

array:1 [
  "" => null
]

Typed properties

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!

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

composer
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-actions
.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

  • Check this box to trigger a request for Renovate to run again on this repository

PHPStan 1.0 upgrade

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/

[Feature request] Implement stable sorting

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?

FlatMap + Eager Usage

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:

  1. 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!

  2. 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!

Issue with distinct

Steps required to reproduce the problem

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);

Expected Result

  • [1]

Actual Result

  • [1,2]

Version

  • "php": ">=8.3",
  • "loophp/collection": "^7.0",

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 πŸ˜‰?

Weird interplay between Collection and PDO result set

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.

Collection interface doesn't extend Countable

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.

Issue with cache and fromCallable

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:

image

Using the latest loophp/collection (6.0.3) and PHP 8.1. Is it a bug, or am I doing something wrong πŸ˜‰?

Bug in Has operation

        Collection::fromIterable(['b', 1, 'foo', 'bar'])
            ->has(
                static function ($key, $value) {
                    return 'foo';
                }
            );

This should return [True] but returns [False].

Typed collection support

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 ...
    }
}

group function accurate?

Steps required to reproduce the problem

  1. Sample array: [['id' => 1, 'name' => 'A'], ['id' => 2, 'name' => 'B'], ['id' => 1, 'name' => 'C']]
  2. Doing Collection::with(arr)->groupBy(function($k, $v) { return $v['id']; })->all()

Expected Result

[ 1 => [ ['id' => 1, 'name' => 'A'], ['id' => 1, 'name' => 'C'] ], 2 => [ ['id' => 2, 'name' => 'B'] ] ]

Actual Result

  • [ 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.

Distinct is slow

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);

Version

"php": ">=8.3",
"loophp/collection": "^7.0",

Is there any way to speed up these operations?

`Partition` Operation - Awkward to use?

Background

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.

Doctrine Collection

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');

Loophp Collection

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();

Possible improvements

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

Modify `all` operation to prevent data loss

Description

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.

Current behaviour

$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']

Reasoning

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.

Proposal

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.

Options:

  1. Keep backwards compatibility -> in this version we'd introduce a parameter which could be passed to 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
  1. 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.

  2. 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 :).

Plus operation RFC

PHP plus operator

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.

How it'd look like with 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);
}

Comparison with 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.

PHPStan reporting an error for missing optional parameters

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.

Memory size exhausted for large collections

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 πŸ™.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.