MarkupCollection Class
Powerful collection class for working with arrays of Markup elements.
Table of Contents
Overview
The MarkupCollection class provides a fluent, chainable interface for working with arrays of Markup instances. It’s inspired by Laravel Collections and provides dozens of methods for transforming, filtering, and manipulating Markup elements.
Key Features
- Fluent interface - Chain methods together
- Laravel-inspired API - Familiar methods for Laravel developers
- Immutable transformations - Most methods return new collections
- Iterator support - Use in foreach loops
- Array access - Access items with array syntax
- Countable - Use with count() function
When to Use MarkupCollection
Use MarkupCollection when:
- You have multiple Markup elements to process
- You need to transform, filter, or sort elements
- You want to apply operations to groups of elements
- You’re using
MarkupQueryBuilder(returns collections automatically)
Creation
Constructor
public function __construct(array $items = [])
Create a new collection from an array of items.
use MaxPertici\Markup\MarkupCollection;
use MaxPertici\Markup\Markup;
$collection = new MarkupCollection([
Markup::div('Item 1'),
Markup::div('Item 2'),
Markup::div('Item 3')
]);
make()
Creates a new collection instance (static factory method).
public static function make(array $items = []): self
$collection = MarkupCollection::make([
Markup::div('Item 1'),
Markup::div('Item 2')
]);
Transformation Methods
map()
Applies a callback to each item and returns a new collection.
map(callable $callback): self
// Add a class to all elements
$enhanced = $collection->map(function($markup) {
$markup->addClass('enhanced');
return $markup;
});
// Extract data
$ids = $collection->map(fn($m) => $m->getAttribute('id'));
filter()
Filters the collection using a callback.
filter(callable $callback): self
// Keep only active elements
$active = $collection->filter(fn($m) => $m->hasClass('active'));
// Filter by attribute value
$expensive = $collection->filter(function($m) {
$price = (float)$m->getAttribute('data-price');
return $price > 100;
});
reject()
Filters by rejecting items that pass the callback (opposite of filter).
reject(callable $callback): self
// Remove hidden elements
$visible = $collection->reject(fn($m) => $m->hasClass('hidden'));
// Remove elements without images
$withImages = $collection->reject(function($m) {
$images = $m->find()->tag('img')->count();
return $images === 0;
});
unique()
Returns unique items based on a callback or attribute.
unique(callable|string|null $key = null): self
// Unique by object identity (default)
$unique = $collection->unique();
// Unique by attribute value
$uniqueCategories = $collection->unique('data-category');
// Unique by callback
$unique = $collection->unique(fn($m) => $m->getAttribute('id'));
Access Methods
first()
Gets the first item that passes an optional callback.
first(?callable $callback = null, $default = null): mixed
// Get first item
$first = $collection->first();
// Get first matching item
$firstActive = $collection->first(fn($m) => $m->hasClass('active'));
// With default value
$first = $collection->first(null, Markup::div('Default'));
last()
Gets the last item that passes an optional callback.
last(?callable $callback = null, $default = null): mixed
// Get last item
$last = $collection->last();
// Get last matching item
$lastActive = $collection->last(fn($m) => $m->hasClass('active'));
nth()
Gets an item at a specific index (via array access).
// Access by index
$second = $collection[1];
$third = $collection[2];
Querying Methods
where()
Gets all items where a method returns a truthy value.
where(string $method, ...$args): self
// Find elements with 'active' class
$active = $collection->where('hasClass', 'active');
// Find elements with specific attribute
$withRole = $collection->where('hasAttribute', 'role');
// With value check
$main = $collection->where('getAttribute', 'role')->first();
pluck()
Extracts values by calling a method or accessing an attribute.
pluck(string $key): array
// Pluck IDs
$ids = $collection->pluck('id'); // Calls getAttribute('id')
// Pluck custom data
$prices = $collection->pluck('data-price');
// With methods
$slugs = $collection->pluck('slug'); // Calls slug() method
groupBy()
Groups the collection by a callback or attribute.
groupBy(callable|string $key): array
// Group by category attribute
$grouped = $collection->groupBy('data-category');
// Result: ['electronics' => Collection, 'books' => Collection, ...]
// Group by callback
$grouped = $collection->groupBy(function($m) {
$price = (float)$m->getAttribute('data-price');
return $price > 100 ? 'expensive' : 'affordable';
});
contains()
Checks if the collection contains an item.
contains(callable|mixed $value): bool
// Contains specific instance
$contains = $collection->contains($someMarkup);
// Contains by callback
$hasActive = $collection->contains(fn($m) => $m->hasClass('active'));
Sorting Methods
sortBy()
Sorts the collection using a callback.
sortBy(callable $callback, int $options = SORT_REGULAR, bool $descending = false): self
// Sort by price (ascending)
$sorted = $collection->sortBy(fn($m) => (float)$m->getAttribute('data-price'));
// Sort by price (descending)
$sorted = $collection->sortBy(
fn($m) => (float)$m->getAttribute('data-price'),
SORT_NUMERIC,
true
);
// Sort by title
$sorted = $collection->sortBy(function($m) {
$title = $m->find()->css('.title')->first();
return $title ? $title->text() : '';
});
reverse()
Reverses the order of items.
reverse(): self
$reversed = $collection->reverse();
Slicing Methods
take()
Takes the first n items.
take(int $limit): self
// Take first 5
$first5 = $collection->take(5);
// Take last 3 (negative)
$last3 = $collection->take(-3);
skip()
Skips the first n items.
skip(int $offset): self
// Skip first 3 items
$remaining = $collection->skip(3);
slice()
Slices the collection.
slice(int $offset, ?int $length = null): self
// Get items 5-10
$slice = $collection->slice(5, 5);
// Get items from position 10 onwards
$tail = $collection->slice(10);
chunk()
Chunks the collection into smaller collections.
chunk(int $size): array
// Split into groups of 3
$chunks = $collection->chunk(3);
// Process each chunk
foreach ($chunks as $chunk) {
// Each $chunk is a MarkupCollection
$chunk->each(fn($m) => $m->addClass('processed'));
}
Iteration Methods
each()
Executes a callback on each item.
each(callable $callback): self
Returns the collection for chaining. Callback receives ($item, $index).
// Add class to all
$collection->each(fn($m) => $m->addClass('processed'));
// With index
$collection->each(function($m, $index) {
$m->setAttribute('data-index', (string)$index);
});
// Break early
$collection->each(function($m, $index) {
if ($index >= 5) {
return false; // Stop iteration
}
$m->addClass('top-5');
});
tap()
Passes the collection to a callback and returns the collection.
tap(callable $callback): self
Useful for side effects while chaining.
$results = $collection
->filter(fn($m) => $m->hasClass('active'))
->tap(fn($c) => error_log("Found {$c->count()} active items"))
->take(10);
pipe()
Applies a callback to the collection and returns the result.
pipe(callable $callback): mixed
Useful for transforming to non-collection values.
// Calculate total price
$total = $collection
->map(fn($m) => (float)$m->getAttribute('data-price'))
->pipe(fn($prices) => array_sum($prices->all()));
// Render all as HTML
$html = $collection
->pipe(fn($c) => $c->map(fn($m) => $m->render())->all())
->pipe(fn($html) => implode("\n", $html));
State Methods
isEmpty()
Checks if the collection is empty.
isEmpty(): bool
if ($collection->isEmpty()) {
echo 'No items found';
}
isNotEmpty()
Checks if the collection is not empty.
isNotEmpty(): bool
if ($collection->isNotEmpty()) {
// Process items
}
count()
Counts the items in the collection.
count(): int
$count = $collection->count();
echo "Found {$count} items";
// Also works with PHP count()
echo count($collection);
random()
Gets a random item from the collection.
random(): mixed
$random = $collection->random();
if ($random) {
echo $random->render();
}
Export Methods
all() / toArray()
Gets all items as an array.
all(): array
toArray(): array // Alias
$array = $collection->all();
foreach ($array as $markup) {
echo $markup->render();
}
values()
Gets all values (re-indexed from 0).
values(): array
// After filtering, keys might not be sequential
$filtered = $collection->filter(fn($m) => $m->hasClass('active'));
// Re-index
$reindexed = $filtered->values();
Debugging Methods
dump()
Dumps the collection for debugging and returns it.
dump(): self
$results = $collection
->filter(fn($m) => $m->hasClass('active'))
->dump() // Shows filtered results
->take(5);
dd()
Dumps the collection and dies (dump and die).
dd(): void
$collection
->filter(fn($m) => $m->hasClass('active'))
->dd(); // Shows results and stops execution
Practical Examples
Filter and Transform
use MaxPertici\Markup\Markup;
$page = Markup::div('');
// ... build page ...
// Find all products, filter, and enhance
$premiumProducts = $page->find()
->class('product')
->get()
->filter(function($product) {
$price = (float)$product->getAttribute('data-price');
return $price > 100;
})
->each(fn($p) => $p->addClass('premium'))
->sortBy(fn($p) => (float)$p->getAttribute('data-price'), SORT_NUMERIC, true);
Group and Process
// Group products by category
$grouped = $page->find()
->class('product')
->get()
->groupBy('data-category');
// Process each group
foreach ($grouped as $category => $products) {
echo "Category: {$category}\n";
echo "Count: {$products->count()}\n";
// Get top 3 by price
$top3 = $products
->sortBy(fn($p) => (float)$p->getAttribute('data-price'), SORT_NUMERIC, true)
->take(3);
}
Statistical Analysis
$products = $page->find()->class('product')->get();
// Calculate average price
$average = $products
->map(fn($p) => (float)$p->getAttribute('data-price'))
->pipe(fn($prices) => array_sum($prices->all()) / $prices->count());
// Find price range
$prices = $products->map(fn($p) => (float)$p->getAttribute('data-price'))->all();
$min = min($prices);
$max = max($prices);
echo "Price range: \${$min} - \${$max}";
echo "Average: \${$average}";
Pagination
function paginateProducts(MarkupCollection $products, int $page, int $perPage): MarkupCollection
{
return $products
->skip(($page - 1) * $perPage)
->take($perPage);
}
// Usage
$allProducts = $page->find()->class('product')->get();
$page1 = paginateProducts($allProducts, 1, 10); // First 10
$page2 = paginateProducts($allProducts, 2, 10); // Next 10
Building a Grid
// Split products into rows of 3
$products = $page->find()->class('product')->get();
$rows = $products->chunk(3);
$grid = Markup::div('', ['product-grid']);
foreach ($rows as $row) {
$rowMarkup = Markup::div('', ['row']);
$row->each(function($product) use ($rowMarkup) {
$col = Markup::div('', ['col'])
->children($product);
$rowMarkup->children($col);
});
$grid->children($rowMarkup);
}
echo $grid->render();
Data Extraction
// Extract all image URLs from a page
$imageUrls = $page->find()
->tag('img')
->get()
->pluck('src')
->filter(fn($url) => !empty($url));
// Extract all link URLs
$links = $page->find()
->tag('a')
->get()
->map(fn($link) => [
'url' => $link->getAttribute('href'),
'text' => $link->text()
])
->filter(fn($link) => !empty($link['url']));
Unique Elements
// Find unique elements by ID
$uniqueById = $collection->unique('id');
// Find unique by slug
$uniqueBySl
ug = $collection->unique(fn($m) => $m->slug());
// Remove duplicate products
$uniqueProducts = $products->unique(function($product) {
return $product->getAttribute('data-sku');
});
Performance Tips
Chain Efficiently
// ✅ Good - single iteration
$results = $collection
->filter(fn($m) => $m->hasClass('product'))
->map(fn($m) => $m->addClass('enhanced'))
->take(10);
// ❌ Bad - multiple iterations
$filtered = $collection->filter(fn($m) => $m->hasClass('product'));
$mapped = $filtered->map(fn($m) => $m->addClass('enhanced'));
$results = $mapped->take(10);
Use Lazy Evaluation
// ✅ Good - stops early
$first = $collection->first(fn($m) => $m->hasClass('error'));
// ❌ Bad - processes everything
$first = $collection->filter(fn($m) => $m->hasClass('error'))->first();
Best Practices
1. Use Method Chaining
// ✅ Fluent and readable
$result = $collection
->filter(fn($m) => $m->hasClass('active'))
->sortBy(fn($m) => $m->getAttribute('priority'))
->take(5);
2. Leverage tap() for Debugging
$results = $collection
->filter(fn($m) => $m->hasClass('product'))
->tap(fn($c) => error_log("After filter: {$c->count()}"))
->sortBy(fn($m) => (float)$m->getAttribute('data-price'))
->tap(fn($c) => error_log("After sort: {$c->count()}"))
->take(10);
3. Use pipe() for Aggregations
// ✅ Clear intent
$total = $collection->pipe(function($c) {
return array_sum($c->map(fn($m) => (float)$m->getAttribute('data-price'))->all());
});
See Also
- MarkupQueryBuilder - Query builder that returns collections
- Markup Class - Main markup building class
- MarkupFinder - Direct search methods
- Getting Started - Introduction and first steps