Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wp Match Blocks Laravel Package

alleyinteractive/wp-match-blocks

A WordPress utility for matching Gutenberg blocks in post content. Detect whether a post contains a given block type or attributes, so themes and plugins can conditionally load assets or change rendering based on the blocks actually used.

View on GitHub
Deep Wiki
Context7

Getting Started

alleyinteractive/wp-match-blocks is a small, dependency-light WordPress library that gives you a functional way to traverse parsed Gutenberg block trees and pick out the blocks you care about. From a Laravel perspective, treat it as a read-only content-querying utility for WordPress-authored HTML — useful in headless setups, content migrations, importers, or a WP-in- Laravel bridge where post content arrives as content.raw.

Install and prerequisites

composer require alleyinteractive/wp-match-blocks

The library assumes the WordPress block API is present: it operates on the output of parse_blocks() and can also walk a WP_Block_List. In a Laravel app that means you either boot a WordPress core context (or a block-parser polyfill) inside a service provider or dedicated service class, or you run the matching in a WP-side mu-plugin and expose the result over REST. Everything else is plain PHP — no hooks, no global state, no bootstrapping side effects, which is why it is safe to call from queued jobs and Artisan commands.

Where to look first

  • The package's inc/ (or src/) directory — the public surface is a handful of namespaced functions, so a five-minute read beats any docs.
  • match_blocks() and match_block() are the traversal core.
  • The small predicate helpers (match_block_name(), attribute matchers) exist so you rarely write a callback from scratch.
  • CHANGELOG.md for the v4 line — note that v4.4.0's release notes only link to the changelog, so read the top few entries directly before bumping; nothing in the release announcement itself indicates a breaking change.

First use case

You have a posts table (mirrored from WP or pulled via the REST API) and need "give me every core/button in this article so I can render CTAs natively in Blade":

use function Alley\WP\Match_Blocks\match_blocks;
use function Alley\WP\Match_Blocks\match_block_name;

$blocks = parse_blocks( $post->content_raw );

$ctas = match_blocks(
    $blocks,
    fn ( array $block ) => match_block_name( $block, 'core/button' )
        ? $block['attrs']['url'] ?? null
        : null
);

That's the whole day-one workflow: parse once, match recursively, get back only the matched results.

Implementation Patterns

Recursive matching as filter/map. match_blocks() descends into innerBlocks for you, so callbacks never need their own recursion. The return value of the callback is the result: truthy values are collected into the returned array, falsy ones are dropped. Use it two ways:

  • Predicate mode — return true and you get back matching block arrays (or trues, depending on your callback). Good for "does this post contain X".
  • Projection mode — return the data you actually want ($block['attrs']['url'], a flattened array, a DTO) and you get a clean list with no block plumbing left in it.

Wrap it in a Laravel service. Don't sprinkle parse_blocks() through controllers. One BlockContent service with methods like buttons(string $html): Collection, firstOfType(), hasBlock(), toArray() keeps the WP-flavoured array shapes out of your domain code and gives you a single place to memoise parsing per request/job.

Attribute-driven extraction. Most real work is "find block by name, read its attrs". Keep predicates narrow and let a single pass collect everything, rather than scanning the tree once per block type. For config-heavy blocks (reusable patterns, core/group wrappers), match the wrapper then iterate its innerBlocks yourself to preserve authoring order.

WP_Block_List when rendering matters. If you need blocks rendered with registered block types and their render_callback/render behaviour, build a WP_Block_List (which requires the block types to be registered) and hand it to the matcher instead of a raw parsed array. This is the bridge to render_block() output and is the only sane path when a block's markup is produced server-side.

Tests without WordPress. Because the API is just arrays in / arrays out, you can unit-test your matching rules in Pest/PHPUnit with hand-written fixture arrays — no WP bootstrap needed. Copy two or three real parse_blocks() outputs into tests/fixtures/ and assert on your service. Reserve the full WP environment for integration tests only.

Ingestion pattern for headless. If content comes from the REST API, use context=edit (or an application-password-authenticated request) to get content.raw. The public content.rendered field is post-do_blocks() HTML and will defeat block matching entirely — at that point you're parsing HTML, not blocks.

Gotchas and Tips

parse_blocks() emits null blocks. Whitespace between blocks produces entries with blockName === null and empty innerBlocks. They flow through your callbacks, so guard every predicate (if ( empty( $block['blockName'] ) ) { return false; }) or you'll get surprising "matched everything" results when your predicate does loose comparisons.

Falsy results silently disappear. Returning 0, '', '0', false, or null from the callback is indistinguishable from "no match". If you're projecting values, wrap them (['value' => $v] or a DTO) or collect into a Collection/array by reference inside the callback. This is the single most common source of "why is one item missing" bugs.

innerBlocks vs blocks. Parsed arrays use $block['innerBlocks']; some WP structures and serialized variants use blocks. Prefer the package's own match_block() / predicate helpers over hand-rolled key access so you don't have to remember which shape you're holding — and assert your fixture shape in tests.

Attribute matching is type-strict in practice. attrs values come back from JSON-ish serialisation: absent keys, null, '', and '0' are all different, and attribute matching against an empty string usually means "author didn't set it". Normalise ($block['attrs']['level'] ?? 3) rather than relying on loose comparison, and remember attrs may be an empty array rather than missing.

Matching ≠ rendering. The library never renders anything. core/shortcode, dynamic blocks, and block bindings will look empty in the parsed tree — their content only exists after render_block()/do_blocks(). Don't conclude a block "isn't there" from a matcher miss on dynamic content.

Performance is fine, re-parsing is not. Traversal is a linear recursion over a small tree; parse_blocks() on every request is the real cost, especially on long posts. Cache the parsed array (or your extracted DTOs) keyed by post ID + post_modified, and run this work in a job during import rather than in the hot path of a page render.

v4 namespacing. The v4 line exposes namespaced functions under Alley\WP\Match_Blocks\. If you're porting snippets from older blog posts/global-function era code, add the use function imports — a missing import fails loudly (fatal on undefined function) rather than silently, which is the good news.

Upgrade hygiene for v4.4.0. The release notes are a changelog link with no summary, so treat 4.4.0 as a minor bump to be validated rather than trusted: diff composer.lock, run your block-matching unit tests against fixtures, and skim the 4.4.0 section of CHANGELOG.md in the installed vendor copy. Nothing signals a BC break, but a link-only release note is exactly the case where you want CI — not a human — to confirm your matchers still return the same shapes.

Extension points. There are no filters or config to speak of; the extension point is the callback. Build a small set of composable predicates (isBlock( 'core/heading', 2 ), hasClass( 'is-style-lead' )) in your own namespace and pass them in — that's cheaper than forking the package for one missing matcher.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console