utils icon indicating copy to clipboard operation
utils copied to clipboard

Add Nette\Utils\Collection

Open milo opened this issue 8 years ago • 10 comments

  • bug fix? no
  • new feature? yes
  • BC break? no
  • doc PR: will

I'm using this abstract collection really often. Its purpose is to emulate typed array in most cases.

May seem to be strange that methods like get() or add() are missing. It is because of PHP invariance limitation.

An example of basic usage:

# One item for collection
class Person
{
    public $email;
    public $firstName;
    public $lastname;
    public $username;
}

# Collection itself
class People extends Nette\Utils\Collection
{
    public function add(Person $person): People
    {
        $this->addItem($person, $person->username);
        return $this;
    }

    public function get(string $username): Person
    {
        return $this->getItem($username);
    }
}


# Creating collection
$people = People::fromIterator(...);

# If good friend Joe is here...
if ($people->has('joe')) {
	debug("Joe is here... again.");
}


# Example usage: create an array of emails, only if email is set
$emails = $people->convert(function (Person $person, $key, & $unset) {
	$unset = $person->email === NULL;
	return $person->email;
});

Collection can declare frequent helpers on self:

# Sorting example
public function sortDefault()
{
	$this->sortBy(function (Person $a, Person $b) {
		if ($a->username === 'joe') return -1;  # sorry joe
		return $a->username <=> $b->username;
	});
}

# Coversion to array for Form <select> input
public function forSelectInput()
{
	return $this->convert(function (Person $person) {
		return "$person->lastname $person->firstName";
	});
}

And usage as typed array:

class Mailer
{
	public function mail(People $people, Mail $message)
	{
		$people->walk(function (Person $person) use ($message) {
			$this->senfTo($person->email, $message);
		});
	}
}

The IteratorAggreagete is here for ordinary loops, like:


/** @var People|Person[] $people */
$people = ...;
foreach ($people as $person) {

)

The ArrayAccess is not implemented. I tried that, but never found it useful.

The normalizeKey() method can convert complex types to scalar, for example for multi column primary keys in database. On the other hand, I overloaded it very rarely.

If this would be accepted, I'll add tests and doc.

milo avatar Jan 09 '17 18:01 milo

From the user's point of view, why would/should one use this, compared e.g. to doctrine/collections (which are even more generic and actually behave like array thanks to ArrayAccess)?

Majkl578 avatar Jan 09 '17 22:01 Majkl578

@dg Rebased

@Majkl578 I don't use doctrine collections, but when I take a look at source...

My collections used to look very similarly. Problem arises when you want to be strict on types. You cannot change the get($key) or add($element) signature. So you have to do something like:

public function add($element)
{
    if (!$element instanceof Person) throw ...
}

ArrayAccess is a very small part of "behave like array". May seem usable, but consider:

$people[$person->username] = $person;
# vs.
$people->add($person);

$people[$person->username]
# vs.
$people->get($person->username)

unset($person[$person->username])
# vs.
# not implemented, I don't remove items one by one, usually by filter only

And sometimes, IDE has a problem with "napovídání" (mi vypadl anglický termín) with ArrayAccess.

milo avatar Jan 10 '17 06:01 milo

I don't think this belongs to nette/utils.

JanTvrdik avatar Jan 10 '17 13:01 JanTvrdik

@JanTvrdik Partially agree. The best would be native typed arrays in the PHP itself.

Collection is a typical part of a model layer, Nette does not have such. This is helper only without big ambitions.

Real world example how I use it:

return People::fromIterator(
    $this->dibi->query('...')->setRowClass(Person::class)
);

But not just database. In one project, I'm listing firewall rules from router:

$rules = new Firewall\Rules;
foreach ($this->switch->command(.....) as $line) {
    $rules->add(Firewall\Rule::fromCliFormat($line));
}

The point is, that working with typehint People is much more efficient and safe than working with array and @var Person[] annotation.

Btw. there used to be Collections in Nette, but this is different.

milo avatar Jan 10 '17 13:01 milo

Since its WIP, rebased to some old commit.

milo avatar Jan 18 '17 18:01 milo

@dg Do you think it is a good idea to have it in the Utils?

I'm using it literally everywhere and for comfort, I want to have it in a public repo. One possibility is to finish this PR (tests, doc, ...), or to create a milo/collection repo. I'm fine with both options, only want to prevent duplicit work :)

milo avatar May 18 '17 07:05 milo

milo/collection is better from my point of view.

JanTvrdik avatar May 18 '17 12:05 JanTvrdik

I like an idea a lot. If it's not suitable for nette/utils I would gladly see it in contributte\utils or also in contributte/collection, something like that was in my plan too.

Please consider that. I could help you @milo with maintenance of course.

Good job. :+1:

f3l1x avatar May 21 '17 18:05 f3l1x

It is not Nette related at all, is it? I think that milo/collection would be best.

josefsabl avatar Oct 07 '19 15:10 josefsabl

milo/collection already exists few years :) But as a private repo. Reason to propose for Nette is, that I consider it extremly useful.

milo avatar Oct 07 '19 17:10 milo