MarkupQueryBuilder Class

Fluent query builder for searching Markup elements with chainable methods.

Table of Contents

Overview

The MarkupQueryBuilder class provides a fluent, chainable interface for building Markup search queries. It allows you to combine multiple search criteria and returns results as a MarkupCollection for easy manipulation.

Key Features

  • Fluent interface - Chain multiple criteria together
  • Multiple search methods - Class, tag, attribute, slug, CSS selectors
  • Custom callbacks - Add complex logic with where()
  • OR logic support - Use orWhere() for alternative criteria
  • Depth control - Search deeply or only immediate children
  • Collection results - Returns MarkupCollection for powerful transformations
  • Performance optimized - Stops early with first(), optimizes single criteria

When to Use MarkupQueryBuilder

Use MarkupQueryBuilder (via find()) when:

  • You need a fluent, readable query interface
  • You want to combine multiple search criteria
  • You need to transform results with collections
  • You want IDE autocompletion and type hints

Use MarkupFinder directly when:

  • You need a single criterion search (findByClass(), findByTag(), etc.)
  • You prefer explicit method names
  • You’re already familiar with the Finder API

Access

The MarkupQueryBuilder is accessed through the find() method on any Markup instance:

use MaxPertici\Markup\Markup;

$markup = Markup::div('content');
$query = $markup->find(); // Returns MarkupQueryBuilder

You can also instantiate it directly:

use MaxPertici\Markup\MarkupQueryBuilder;

$query = new MarkupQueryBuilder($markup);

Building Queries

class()

Adds a CSS class criterion to the query.

class(string $class): self

Parameters

Parameter Type Description
$class string The CSS class to match

Examples

// Find all elements with 'active' class
$results = $markup->find()
    ->class('active')
    ->get();

// Chain multiple classes (AND logic)
$results = $markup->find()
    ->class('card')
    ->class('featured')
    ->get();

tag()

Adds an HTML tag criterion to the query.

tag(string $tag): self

Parameters

Parameter Type Description
$tag string The HTML tag name to match (e.g., ‘div’, ‘span’)

Examples

// Find all div elements
$divs = $markup->find()
    ->tag('div')
    ->get();

// Combine with other criteria
$activeDivs = $markup->find()
    ->tag('div')
    ->class('active')
    ->get();

attribute() / hasAttribute()

Adds an HTML attribute criterion to the query.

attribute(string $name, ?string $value = null): self
hasAttribute(string $name, ?string $value = null): self // Alias

Parameters

Parameter Type Description
$name string The attribute name
$value string\|null Optional. The attribute value. If null, checks only existence

Examples

// Find elements with any 'data-id' attribute
$results = $markup->find()
    ->attribute('data-id')
    ->get();

// Find elements with specific attribute value
$main = $markup->find()
    ->attribute('role', 'main')
    ->first();

// Using the alias
$required = $markup->find()
    ->hasAttribute('required')
    ->get();

slug()

Adds a slug criterion to the query.

slug(string $slug): self

Parameters

Parameter Type Description
$slug string The slug identifier to match

Examples

// Find element with specific slug
$header = $markup->find()
    ->slug('main-header')
    ->first();

// Combine with other criteria
$activeHeader = $markup->find()
    ->slug('main-header')
    ->class('active')
    ->first();

css()

Adds a CSS selector criterion to the query.

css(string $selector): self

Supports the same CSS selectors as MarkupFinder::css(): basic selectors, combinations, descendant/child combinators, and :has() pseudo-class.

Parameters

Parameter Type Description
$selector string The CSS selector string

Examples

// CSS selector alone
$links = $markup->find()
    ->css('nav a')
    ->get();

// Combine with other criteria
$results = $markup->find()
    ->css('.card')
    ->hasAttribute('data-featured')
    ->get();

Note: When using only a CSS selector, it’s optimized to use MarkupFinder::css() directly.


where()

Adds a custom callback criterion to the query.

where(callable $callback): self

The callback receives a Markup instance and should return true if it matches.

Parameters

Parameter Type Description
$callback callable Function that receives Markup $markup and returns bool

Examples

// Custom logic
$expensive = $markup->find()
    ->where(function($m) {
        $price = $m->getAttribute('data-price');
        return $price && (float)$price > 100;
    })
    ->get();

// Complex conditions
$results = $markup->find()
    ->class('product')
    ->where(fn($m) => $m->hasAttribute('data-stock'))
    ->where(fn($m) => (int)$m->getAttribute('data-stock') > 0)
    ->get();

orWhere()

Adds an OR callback criterion to the query.

orWhere(callable $callback): self

All regular criteria are combined with AND logic. Use orWhere() to add alternative criteria.

Parameters

Parameter Type Description
$callback callable Function that receives Markup $markup and returns bool

Examples

// Match elements with EITHER 'error' OR 'warning' class
$alerts = $markup->find()
    ->where(fn($m) => $m->hasClass('error'))
    ->orWhere(fn($m) => $m->hasClass('warning'))
    ->get();

// Complex OR logic
$results = $markup->find()
    ->class('product')
    ->where(fn($m) => $m->hasClass('featured'))
    ->orWhere(fn($m) => (float)$m->getAttribute('data-discount') > 50)
    ->get();

Search Depth Control

deep()

Sets whether to search recursively (deeply) or not.

deep(bool $deep = true): self

Parameters

Parameter Type Description
$deep bool Whether to search deeply. Default: true

Examples

// Deep search (default)
$all = $markup->find()
    ->class('card')
    ->deep(true)
    ->get();

// Shallow search
$direct = $markup->find()
    ->class('card')
    ->deep(false)
    ->get();

shallow()

Sets to search only immediate children (not recursive). Alias for deep(false).

shallow(): self

Examples

// Only direct children
$directCards = $markup->find()
    ->class('card')
    ->shallow()
    ->get();

Executing Queries

get()

Executes the query and returns a MarkupCollection of results.

get(): MarkupCollection

Return

  • MarkupCollection - Collection of matching Markup instances

Examples

// Basic execution
$results = $markup->find()
    ->class('card')
    ->get();

// Transform with collection methods
$titles = $markup->find()
    ->class('card')
    ->get()
    ->map(fn($card) => $card->find()->css('.title')->first())
    ->filter(fn($title) => $title !== null);

all()

Executes the query and returns results as a plain array.

all(): array

Return

  • array - Array of matching Markup instances

Examples

// Get array of results
$cards = $markup->find()
    ->class('card')
    ->all();

// Iterate
foreach ($cards as $card) {
    $card->addClass('enhanced');
}

first()

Executes the query and returns the first result.

first(): ?Markup

Optimized to stop searching after finding the first match.

Return

  • Markup|null - The first matching instance or null

Examples

// Get first match
$firstActive = $markup->find()
    ->class('active')
    ->first();

if ($firstActive) {
    echo $firstActive->getAttribute('id');
}

// With null coalescing
$header = $markup->find()
    ->slug('main-header')
    ->first() ?? Markup::div('Default header');

exists()

Checks if any results exist.

exists(): bool

Return

  • bool - True if at least one match exists

Examples

// Check existence
if ($markup->find()->class('error')->exists()) {
    echo 'Errors found!';
}

// Guard clause
if ($markup->find()->tag('img')->doesntExist()) {
    echo 'No images in this markup';
}

doesntExist()

Checks if no results exist.

doesntExist(): bool

Return

  • bool - True if no matches exist

Examples

// Check non-existence
if ($markup->find()->class('ads')->doesntExist()) {
    echo 'No ads on this page';
}

count()

Counts the number of matching elements.

count(): int

Return

  • int - The count of matches

Examples

// Count results
$cardCount = $markup->find()
    ->class('card')
    ->count();

echo "Found {$cardCount} cards";

// Conditional logic
if ($markup->find()->class('error')->count() > 5) {
    echo 'Too many errors!';
}

Practical Examples

Basic Queries

use MaxPertici\Markup\Markup;

$page = Markup::div('', ['page']);
// ... build page structure ...

// Find by class
$buttons = $page->find()->class('btn')->get();

// Find by tag
$images = $page->find()->tag('img')->get();

// Find by attribute
$navs = $page->find()->attribute('role', 'navigation')->get();

// Find by slug
$header = $page->find()->slug('site-header')->first();

Combined Criteria

// Multiple classes (AND logic)
$primaryButtons = $page->find()
    ->class('btn')
    ->class('btn-primary')
    ->get();

// Tag + class + attribute
$externalLinks = $page->find()
    ->tag('a')
    ->class('external')
    ->hasAttribute('target', '_blank')
    ->get();

// Complex combination
$activeProducts = $page->find()
    ->class('product')
    ->class('active')
    ->hasAttribute('data-stock')
    ->where(fn($m) => (int)$m->getAttribute('data-stock') > 0)
    ->get();

CSS Selectors

// Simple selector
$navLinks = $page->find()
    ->css('nav a')
    ->get();

// Complex selector
$activeCards = $page->find()
    ->css('.container > .card.active')
    ->get();

// With :has()
$sectionsWithImages = $page->find()
    ->css('section:has(img)')
    ->get();

Collection Transformations

// Map and filter
$prices = $page->find()
    ->class('product')
    ->get()
    ->map(fn($p) => (float)$p->getAttribute('data-price'))
    ->filter(fn($price) => $price > 50);

// Sort products by price
$sortedProducts = $page->find()
    ->class('product')
    ->get()
    ->sortBy(fn($p) => (float)$p->getAttribute('data-price'));

// Group by category
$grouped = $page->find()
    ->class('product')
    ->get()
    ->groupBy('data-category');

Conditional Queries

function findProducts(Markup $page, ?string $category = null, ?bool $inStock = null): MarkupCollection
{
    $query = $page->find()->class('product');
    
    if ($category) {
        $query->hasAttribute('data-category', $category);
    }
    
    if ($inStock !== null) {
        $query->where(function($m) use ($inStock) {
            $stock = (int)$m->getAttribute('data-stock');
            return $inStock ? $stock > 0 : $stock === 0;
        });
    }
    
    return $query->get();
}

// Usage
$electronics = findProducts($page, 'electronics', true);
$all = findProducts($page);

// Deep search (default) - finds all nested divs
$allDivs = $page->find()
    ->tag('div')
    ->get();

// Shallow search - only direct div children
$directDivs = $page->find()
    ->tag('div')
    ->shallow()
    ->get();

// Useful for layouts
$sections = $page->find()
    ->tag('section')
    ->shallow() // Only top-level sections
    ->get();

Modifying Results

// Add classes to all cards
$page->find()
    ->class('card')
    ->get()
    ->each(fn($card) => $card->addClass('enhanced'));

// Set attributes on external links
$page->find()
    ->tag('a')
    ->hasAttribute('target', '_blank')
    ->get()
    ->each(fn($link) => $link->setAttribute('rel', 'noopener noreferrer'));

// Process first 5 items
$page->find()
    ->class('item')
    ->get()
    ->take(5)
    ->each(fn($item) => $item->addClass('top-5'));

Validation Pipeline

function validatePage(Markup $page): array
{
    $errors = [];
    
    // Check for images without alt
    $imagesWithoutAlt = $page->find()
        ->tag('img')
        ->where(fn($img) => !$img->hasAttribute('alt'))
        ->count();
    
    if ($imagesWithoutAlt > 0) {
        $errors[] = "{$imagesWithoutAlt} images missing alt attribute";
    }
    
    // Check for empty headings
    $emptyHeadings = $page->find()
        ->where(function($m) {
            $wrapper = $m->getWrapper();
            return preg_match('/^<h[1-6]/', $wrapper) && $m->isEmpty();
        })
        ->count();
    
    if ($emptyHeadings > 0) {
        $errors[] = "{$emptyHeadings} empty headings found";
    }
    
    return $errors;
}

Performance Considerations

Single Criterion Optimization

// ✅ Optimized - single CSS criterion uses MarkupFinder::css() directly
$results = $markup->find()->css('.card')->get();

// ✅ Also optimized - MarkupFinder methods are faster for single criteria
$finder = new \MaxPertici\Markup\MarkupFinder($markup);
$results = $finder->findByClass('card');

Use first() When Possible

// ✅ Fast - stops at first match
$active = $markup->find()->class('active')->first();

// ❌ Slower - searches everything
$active = $markup->find()->class('active')->get()->first();

Shallow Search for Performance

// ✅ Fast - only searches direct children
$sections = $markup->find()->tag('section')->shallow()->get();

// ❌ Slower - searches entire tree
$sections = $markup->find()->tag('section')->get();

Reuse Collections

// ✅ Good - query once, use multiple times
$cards = $markup->find()->class('card')->get();
$featuredCards = $cards->filter(fn($c) => $c->hasClass('featured'));
$regularCards = $cards->reject(fn($c) => $c->hasClass('featured'));

// ❌ Bad - queries twice
$featuredCards = $markup->find()->class('card')->where(fn($c) => $c->hasClass('featured'))->get();
$regularCards = $markup->find()->class('card')->where(fn($c) => !$c->hasClass('featured'))->get();

Best Practices

1. Chain Criteria for Readability

// ✅ Good - fluent and readable
$results = $markup->find()
    ->class('product')
    ->hasAttribute('data-stock')
    ->where(fn($m) => (int)$m->getAttribute('data-stock') > 0)
    ->get();

// ❌ Less readable
$results = $markup->find()->class('product')->hasAttribute('data-stock')->where(fn($m) => (int)$m->getAttribute('data-stock') > 0)->get();

2. Use exists() for Checks

// ✅ Good - semantic and clear
if ($markup->find()->class('error')->exists()) {
    // Handle errors
}

// ❌ Less clear
if ($markup->find()->class('error')->count() > 0) {
    // Handle errors
}

3. Leverage Collections

// ✅ Good - use collection power
$highPrices = $markup->find()
    ->class('product')
    ->get()
    ->map(fn($p) => (float)$p->getAttribute('data-price'))
    ->filter(fn($price) => $price > 100)
    ->values();

// ❌ Manual array operations
$products = $markup->find()->class('product')->all();
$highPrices = [];
foreach ($products as $p) {
    $price = (float)$p->getAttribute('data-price');
    if ($price > 100) {
        $highPrices[] = $price;
    }
}

See Also