1. Go to this page and download the library: Download zenstruck/collection library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
zenstruck / collection example snippets
use function Zenstruck\collect;
$page = collect($users) // array, iterable, closure or doctrine collection
->filter(fn(User $user) => $user->isActive())
->map(fn(User $user) => $user->email())
->paginate(page: 2, limit: 10)
;
\count($page); // 10 (the number of items on this page)
$page->totalCount(); // 79 (the total number of items)
$page->lastPage(); // 8
foreach ($page as $email) {
// ...
}
use Zenstruck\Collection\Doctrine\ORM\EntityResult;
$qb = $em->createQueryBuilder()
->select('p')
->from(Post::class, 'p')
->where('p.status = :status')
->setParameter('status', 'published')
;
$published = new EntityResult($qb); // nothing has been executed yet
\count($published); // SELECT COUNT(...)
$published->first(); // ...LIMIT 1
$published->paginate(page: 2, limit: 10); // a single, paginated query
$published->asArray('id', 'title')->first(); // ['id' => 1, 'title' => '...'] - no entity hydrated
foreach ($published as $post) {
// ...
}
use function Zenstruck\collect;
collect(['a', 'b']); // ArrayCollection
collect(new \ArrayIterator(['a'])); // LazyCollection
collect(fn() => fetch_rows()); // LazyCollection (callback not executed yet)
collect($doctrineCollection); // DoctrineBridgeCollection
collect(); // empty LazyCollection
/** @var Zenstruck\Collection<Post> $posts */
// transformations - each returns a new Collection, the original is untouched
$posts->filter(fn(Post $post) => $post->isPublished());
$posts->map(fn(Post $post) => $post->title());
$posts->keyBy(fn(Post $post) => $post->id());
$posts->take(10); // first 10
$posts->take(10, 5); // 10, starting at offset 5
// reductions
$posts->first(); // first item or null
$posts->first($default); // first item or $default
$posts->find(fn(Post $post) => $post->isPublished()); // first match or null
$posts->reduce(fn(int $count, Post $post) => $count + 1, 0);
$posts->isEmpty();
\count($posts);
foreach ($posts as $post) {
// ...
}
$posts->eager(); // load everything into an ArrayCollection
$posts->paginate(); // see "Pagination" below
$posts->dump(); // dump the items and return $this
$posts->dd(); // dump the items and die
$posts->filter(fn(Post $post, int $key) => $post->isPublished());
$titles = collect(fn() => fetch_rows()) // nothing has run yet
->filter(fn(array $row) => $row['published'])
->map(fn(array $row) => $row['title'])
->take(10)
; // still nothing has run
foreach ($titles as $title) {
// NOW the source is iterated - and stops after 10 matches
}
> new LazyCollection($generator); // throws \InvalidArgumentException
> new LazyCollection(fn() => $items()); // ok - re-invoked on each iteration
>
use Zenstruck\Collection\ArrayCollection;
$collection = new ArrayCollection(['a' => 1, 'b' => 2]);
$collection->set('c', 3); // new instance, $collection is unchanged
use Zenstruck\Collection\LazyCollection;
$users = new LazyCollection(function() {
$page = 1;
while ($response = $api->get('/users', ['page' => $page++])) {
yield from $response->toArray();
}
});
$users->take(50); // only fetches as many pages as needed
use Zenstruck\Collection\CallbackCollection;
use Zenstruck\Collection\ChainCollection;
use Zenstruck\Collection\FactoryCollection;
use function Zenstruck\collect;
new ChainCollection([$collection1, $collection2]); // keys are discarded
new ChainCollection([$collection1, $collection2], true); // keys are preserved
// count without iterating
new CallbackCollection(fn() => $api->results(), fn() => $api->totalCount());
// $post is only created for items you actually iterate over
new FactoryCollection(collect($rows), fn(array $row) => Post::fromArray($row));
/** @var Zenstruck\Collection<Post> $posts */
$page = $posts->paginate(); // page 1, 20 per page
$page = $posts->paginate(page: 3, limit: 50);
foreach ($page as $post) {
// only the 50 posts on page 3
}
$page = $posts->paginate(page: 999, limit: 20)->strict();
$page->currentPage(); // 7 - the last page, not 999
\count($page); // 18 - and these are the last page's items
foreach ($posts->pages(100) as $page) {
foreach ($page as $post) {
// ...
}
}
$pages = $posts->pages(100);
$pages->get(3); // the Page for page 3
\count($pages); // the number of pages (0 when the collection is empty)
use Pagerfanta\Pagerfanta;
use Zenstruck\Collection\Pagerfanta\PagerfantaAdapter;
$pagerfanta = new Pagerfanta(new PagerfantaAdapter($posts));
use Zenstruck\Collection\Doctrine\ORM\EntityResult;
use Zenstruck\Collection\Doctrine\ORM\EntityResultQueryBuilder;
// wrap any query builder
$result = new EntityResult($qb);
// ...or use the one that can create the result itself
$result = EntityResultQueryBuilder::forEntity($em, Post::class, 'p')
->where('p.status = :status')
->setParameter('status', 'published')
->result()
;
\count($result); // SELECT COUNT(...)
$result->first(); // ...LIMIT 1 (or null when there are no rows)
$result->first($default); // ...or your default
$result->paginate(); // one paginated query - see "Pagination"
$result->take(10, 20); // 10 rows, starting at offset 20
$result->eager(); // everything, as an ArrayCollection
$result->isEmpty();
foreach ($result as $post) {
// ...
}
// entities in, DTOs out
$dtos = $result->as(fn(Post $post) => PostDto::from($post));
// only select what the DTO needs, and never hydrate an entity at all
$dtos = EntityResultQueryBuilder::forEntity($em, Post::class, 'p')
->select('p.id, p.title')
->result()
->as(fn(array $row) => new PostDto($row['id'], $row['title']))
;
// or let asArray() pick the fields
$dtos = $result
->asArray('id', 'title')
->as(fn(array $row) => new PostDto(...$row))
;
foreach ($result->readonly() as $post) {
// $post is not managed
}
$result = EntityResultQueryBuilder::forEntity($em, Post::class, 'p')
->leftJoin('p.comments', 'c')
->addSelect('COUNT(c.id) AS commentCount')
->groupBy('p.id')
->result()
->withAggregates()
;
foreach ($result as $post) {
$post->title(); // proxied to the Post
$post->commentCount; // the aggregate column
$post->entity(); // the Post itself
$post->aggregates(); // ['commentCount' => 12]
}
$result = $result->disableFetchJoins(); // faster when the query has no fetch-joined collections
$result = $result->disableOutputWalkers();
$result = $result->enableOutputWalkers(); //
use Zenstruck\Collection\Spec;
/** @var Zenstruck\Collection\Doctrine\ObjectRepository<Post> $posts */
$posts->find(1); // a single Post, or null
$posts->find(['slug' => 'my-post']);
$published = $posts->filter(Spec::eq('status', 'published')); // no query yet
$published->paginate(page: 2);
$published->asArray('id', 'title');
\count($posts);
foreach ($posts as $post) {
// ...
}
use Zenstruck\Collection\Doctrine\ORM\EntityRepository;
use Zenstruck\Collection\Spec;
$posts = new EntityRepository($em, Post::class);
$posts->find(1);
$posts->filter(Spec::eq('status', 'published'));
use Doctrine\ORM\EntityManagerInterface;
use Zenstruck\Collection\Doctrine\ORM\EntityRepository;
use Zenstruck\Collection\Doctrine\ORM\EntityResult;
/**
* @extends EntityRepository<Post>
*/
final class PostRepository extends EntityRepository
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, Post::class);
}
/**
* @return EntityResult<Post>
*/
public function published(): EntityResult
{
return $this->qb('p')
->where('p.status = :status')
->setParameter('status', 'published')
->result()
;
}
}
use Zenstruck\Collection\Doctrine\ORM\Bridge\ORMEntityRepository;
/**
* @extends ORMEntityRepository<Post>
*/
final class PostRepository extends ORMEntityRepository
{
}
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
// ...
}
use Doctrine\Persistence\ManagerRegistry;
use Zenstruck\Collection\Doctrine\ORM\Bridge\ORMServiceEntityRepository;
/**
* @extends ORMServiceEntityRepository<Post>
*/
final class PostRepository extends ORMServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
}
use Doctrine\ORM\QueryBuilder;
use Zenstruck\Collection\Spec;
$posts->find(1); // by id
$posts->find(['slug' => 'my-post']); // by criteria
$posts->find(Spec::eq('slug', 'my-post')); // by specification
$posts->find(fn(QueryBuilder $qb, string $alias) => $qb->andWhere("{$alias}.views > 100"));
$published->filter(Spec::contains('title', 'symfony')); // ok
$published->filter(DoctrineSpec::delete()); // throws InvalidSpecification
use Zenstruck\Collection\Doctrine\DoctrineSpec;
$deleted = $posts
->query(DoctrineSpec::andX(
DoctrineSpec::lt('publishedAt', $cutoff),
DoctrineSpec::delete(),
))
->first() // executes the DELETE and returns the number of affected rows
;
use Doctrine\ORM\QueryBuilder;
final class Trending
{
public function __invoke(QueryBuilder $qb, string $alias): void
{
$qb
->andWhere("{$alias}.views > 1000")
->addOrderBy("{$alias}.views", 'DESC')
;
}
}
$posts->filter(new Trending());
$posts->find(new Trending());
\count($posts);
foreach ($posts as $post) {
// ...
}
use Zenstruck\Collection\Doctrine\Batch;
/** @var iterable<Post> $posts */
// read: nothing is flushed, the entity manager is cleared after each chunk
foreach (Batch::iterate($posts, $em) as $post) {
$csvExporter->addRow([$post->id(), $post->title()]);
}
// write: flushed and cleared after each chunk
foreach (Batch::process($posts, $em) as $post) {
$post->recalculateScore(); // no persist() or flush() needed
}
Batch::process($posts, $em, chunkSize: 500);
foreach (Batch::process($csvRows, $em) as $row) {
$em->persist(Post::fromRow($row)); // flushed every 100 rows, in one transaction
}
foreach (Batch::iteratorFor($qb) as $post) {
// ...
}
foreach (Batch::processorFor($query, chunkSize: 50) as $post) {
// ...
}
> use Symfony\Component\Console\Style\SymfonyStyle;
>
> /** @var SymfonyStyle $io */
>
> // if $csvRows is countable, this is a progress bar with a limit - otherwise it's open ended
> foreach ($io->progressIterate(Batch::process($csvRows, $em)) as $row) {
> $em->persist(Post::fromRow($row));
> }
>
use function Zenstruck\collect;
$comments = collect($post->getComments()); // a DoctrineBridgeCollection
// the Doctrine API
$comments->add($comment);
$comments->removeElement($comment);
$comments->containsKey(3);
// ...and this package's
$comments->paginate(page: 2, limit: 10);
$comments->filter(Spec::eq('approved', true));
$comments->map(fn(Comment $comment) => $comment->author());
// a single query with a WHERE, not "load all comments, then filter"
$approved = $comments->filter(Spec::eq('approved', true));
// paginated queries, not one big fetch
foreach ($comments as $comment) {
// ...
}
use Zenstruck\Collection\Spec;
$posts->filter(Spec::andX(
Spec::eq('status', 'published'),
Spec::contains('title', 'symfony'),
Spec::sortDesc('publishedAt'),
));
> use Zenstruck\Collection\Specification\Filter\Between;
>
> Spec::between('publishedAt', $start, $end); // both een::INCLUSIVE_BEGIN); // begin
Spec::contains('title', 'my*post'); // LIKE '%my%post%'
Spec::startsWith('title', 'my*post'); // LIKE 'my%post%'
Spec::endsWith('title', 'my*post'); // LIKE '%my%post'
Spec::contains('title', '*symfony*'); // identical to Spec::contains('title', 'symfony')
Spec::contains('title', '50%'); // titles containing "50%"
Spec::startsWith('code', 'a_b'); // codes starting with "a_b" - the underscore isn't a wildcard
use Zenstruck\Collection\Doctrine\DoctrineSpec;
$posts->filter(DoctrineSpec::andX(
DoctrineSpec::eq('status', 'published'),
DoctrineSpec::innerJoin('category')
->eager() // also SELECT the category
->scope(DoctrineSpec::eq('name', 'php')), // category.name = 'php'
DoctrineSpec::antiJoin('comments'), // ...that nobody has commented on
));
use Zenstruck\Collection\Spec;
use Zenstruck\Collection\Specification\Nested;
final class Published implements Nested
{
public function specification(): mixed
{
return Spec::andX(
Spec::eq('status', 'published'),
Spec::isNull('deletedAt'),
);
}
}
$posts->filter(new Published());
$posts->filter(Spec::not(new Published()));
use Zenstruck\Collection\Doctrine\ObjectRepositoryFactory;
final class PostController
{
public function __construct(private ObjectRepositoryFactory $repositories)
{
}
public function index(): Response
{
$posts = $this->repositories->create(Post::class); // an ObjectRepository<Post>
// ...
}
}
use Zenstruck\Collection\Doctrine\ObjectRepository;
use Zenstruck\Collection\Symfony\Attributes\ForObject;
final class PostController
{
public function __construct(
#[ForObject(Post::class)]
private ObjectRepository $posts,
) {
}
}
use Zenstruck\Collection\Doctrine\ORM\EntityRepository;
use Zenstruck\Collection\Doctrine\ORM\EntityResult;
use Zenstruck\Collection\Symfony\Attributes\ForObject;
/**
* @extends EntityRepository<Post>
*/
#[ForObject(Post::class)]
final class PostRepository extends EntityRepository
{
/**
* @return EntityResult<Post>
*/
public function published(): EntityResult
{
return $this->qb('p')
->where('p.status = :status')
->setParameter('status', 'published')
->result()
;
}
}
\count($issues); // 22202 - after requesting all 223 pages
$issues->take(20, 980); // 11 requests - it skipped 980 items to get there
use Zenstruck\Collection\CallbackCollection;
$query = ['q' => 'repo:symfony/symfony is:issue'];
$issues = new CallbackCollection(
function() use ($client, $query) {
$page = 1;
while ($items = $client->request('GET', 'https://api.github.com/search/issues', [
'query' => $query + ['per_page' => 100, 'page' => $page++],
])->toArray()['items']) {
yield from $items;
}
},
fn() => $client->request('GET', 'https://api.github.com/search/issues', [
'query' => $query + ['per_page' => 1],
])->toArray()['total_count'],
);
\count($issues); // 22202 - one request
$issues->isEmpty(); // ...the same request
namespace App\GitHub;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Zenstruck\Collection;
use Zenstruck\Collection\IterableCollection;
use Zenstruck\Collection\LazyCollection;
/**
* @implements Collection<array<string,mixed>,int>
*/
final class IssueSearch implements Collection
{
/** @use IterableCollection<array<string,mixed>,int> */
use IterableCollection;
private const PER_PAGE = 100; // the API's maximum
public function __construct(private HttpClientInterface $client, private string $query)
{
}
public function getIterator(): \Traversable
{
foreach ($this->apiPages() as $items) {
yield from $items;
}
}
public function count(): int
{
return $this->request(['per_page' => 1])['total_count'];
}
public function take(int $limit, int $offset = 0): Collection
{
return new LazyCollection(function() use ($limit, $offset) {
$skip = $offset % self::PER_PAGE;
// start at the page the window begins in, instead of skipping from the front
foreach ($this->apiPages(\intdiv($offset, self::PER_PAGE) + 1) as $items) {
foreach (\array_slice($items, $skip) as $item) {
yield $item;
if (0 === --$limit) {
return;
}
}
$skip = 0;
}
});
}
/**
* @return \Traversable<int,list<array<string,mixed>>>
*/
private function apiPages(int $from = 1): \Traversable
{
while ($items = $this->request(['per_page' => self::PER_PAGE, 'page' => $from++])['items']) {
yield $items;
}
}
/**
* @param array<string,mixed> $query
*
* @return array<string,mixed>
*/
private function request(array $query): array
{
return $this->client->request('GET', 'https://api.github.com/search/issues', [
'query' => $query + ['q' => $this->query],
])->toArray();
}
}
$issues = new IssueSearch($client, 'repo:symfony/symfony is:issue');
\count($issues); // 22202 - one request
$issues->take(20); // one request
$issues->take(20, 980); // still one - it asks for the page the window is in
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.