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

Wordpress Interop Laravel Package

williarin/wordpress-interop

williarin/wordpress-interop bridges WordPress and modern PHP frameworks, letting you boot WordPress inside Laravel or Symfony apps to use its functions, hooks, and database from your own code—no separate install required.

View on GitHub
Deep Wiki
Context7

Getting Started

williarin/wordpress-interop lets a non-WordPress PHP app (Laravel, Symfony, plain PHP) read and write the WordPress tables (wp_posts, wp_postmeta, wp_users, wp_usermeta, wp_terms, wp_term_taxonomy, wp_term_relationships, wp_options, wp_comments) through Doctrine DBAL plus a thin entity/repository layer, instead of bootstrapping WordPress or shelling out to WP-CLI for every read.

1. Install and point it at the database

composer require williarin/wordpress-interop

You need the same credentials WordPress uses — ideally a dedicated MySQL user with SELECT only against the WP schema (writes are discussed below). Collect:

  • host / port / dbname / user / password
  • the table prefix (almost always wp_, but never assume — check $table_prefix in wp-config.php)
  • charset (utf8mb4 on anything modern)

If you're on Laravel or Symfony, look for the framework bridge/service provider first; otherwise construct the connection and entity/repository layer in a small bootstrap file or container binding. Read the README's configuration block and copy it verbatim the first time — the option names are the thing people get wrong.

2. Look here first

  • README.md — configuration keys and the entity list.
  • src/Bridge/Entity/ — one class per WP table (Post, User, Term, Comment, Option, …). This tells you exactly which columns are mapped and how types are cast.
  • src/Bridge/Repository/ — the query surface (find, findBy, findOneBy, pagination). This is your daily driver.
  • tests/ — the fixture schema is the fastest way to see real column shapes, serialized meta, and term relationships in action.

3. First use case: "published posts of a custom post type, with their meta"

This is 80% of real-world usage: you have a WordPress site managed by editors, and a separate app (marketing site, API, reporting job) that consumes the content.

$posts = $postRepository->findBy(
    ['postType' => 'product', 'postStatus' => 'publish'],
    ['postDate' => 'DESC'],
    20,
    0
);

foreach ($posts as $post) {
    // $post is a Post entity with typed getters
    $price = $postRepository->getMeta($post, 'price'); // postmeta lookup
}

Verify your first query against wp_posts directly in MySQL before trusting the entity mapping — mismatched prefix or a stale post_status filter is the #1 reason "the library returns nothing".

Implementation Patterns

Treat it as a read model, not a second admin. The highest-value pattern is: WordPress owns the write path (editors, plugins, WP-CLI), your app owns the read path. Keep the library 90% read-only and you avoid an entire class of cache-invalidation bugs. When you genuinely need to write, fence those writes behind one service class so you can audit them.

Wrap the library behind your own repository. Don't let Williarin\...\Post entities leak into controllers and Blade/Twig templates. Map them into your own DTOs at the boundary:

final class ArticleRepository
{
    public function __construct(private PostRepository $posts) {}

    public function published(int $limit): array
    {
        return array_map(
            fn ($post) => new ArticleDto(
                id: $post->getId(),
                title: $post->getPostTitle(),
                slug: $post->getPostName(),
                publishedAt: $post->getPostDate(),
                excerpt: $post->getPostExcerpt(),
            ),
            $this->posts->findBy(
                ['postType' => 'post', 'postStatus' => 'publish'],
                ['postDate' => 'DESC'],
                $limit
            )
        );
    }
}

This buys you three things: a stable app-side API, a place to swap in a cache, and immunity to the library's (occasionally breaking) minor releases.

Eager-load meta and terms; don't N+1. wp_postmeta is an EAV table — one row per key. Calling getMeta() inside a loop over 500 posts is 500 extra queries against a table that is frequently the largest in the database. Either fetch meta for a batch of post IDs in one query and pivot in PHP, or add a purpose-built method on your repository that joins wp_posts → wp_postmeta for the two or three keys you actually need:

SELECT p.ID, p.post_title, pm.meta_value AS price
FROM wp_posts p
LEFT JOIN wp_postmeta pm
  ON pm.post_id = p.ID AND pm.meta_key = 'price'
WHERE p.post_type = 'product' AND p.post_status = 'publish'

The library exposes the underlying DBAL connection, so you can drop to this for hot paths and hydrate entities yourself.

ACF and other meta conventions. Advanced Custom Fields stores the field value under the field name (price) and a field definition under _price (prefixed with underscore, points at field_xxx keys). Repeaters and flexible content are PHP-serialized arrays. The library unserializes meta on read — but you still need domain knowledge to interpret the structure. Write a small ACF-aware decorator rather than scattering unserialize() logic around.

Sync into local tables for anything join-heavy or search-heavy. If your app needs to filter WordPress content by three meta fields plus a taxonomy, or serve it at low latency, do a periodic ETL into your own normalized tables (or a search index) and let the library be the extraction layer. Reads stay fast, and you stop worrying about the WP DB going away mid-request.

Handle taxonomies through wp_term_relationships. Terms live in wp_terms (name/slug) but are attached via wp_term_taxonomy (which carries taxonomy and a denormalized count) and wp_term_relationships (object_id → term_taxonomy_id). To get "all categories for a post" you join all three. Custom taxonomies are just different taxonomy values in the same tables — no schema change.

Reading options. wp_options holds site config (siteurl, home, blogname), plugin settings, and serialized theme mods. Autoloaded options are loaded on every WP pageview; treat them as read-only config. Transients (_transient_*, _transient_timeout_*) live here too — ignore them in your app, or you'll be debugging why a "setting" changes every hour.

Multi-site. Each blog has its own prefix (wp_2_posts, wp_3_options, …). The library is prefix-per-connection, so you need one configured connection per blog ID. Enumerate blogs from wp_blogs and build a connection factory keyed by blog_id. Note that user roles are per-site: wp_usermeta.meta_key = 'wp_2_capabilities'.

Gotchas and Tips

The object cache will bite you on writes. WordPress caches posts, meta, terms, and options in Redis/Memcached (or in-process). Writing directly to MySQL bypasses wp_cache_set/wp_cache_delete entirely — the WordPress front end will keep serving stale data until the cache expires or is flushed. If you write, you must either (a) run wp cache flush immediately after, (b) write through WP's REST API / WP-CLI instead, or (c) keep writes to columns WordPress never caches. Option (b) is almost always the right answer.

Serialized data is a trap in both directions. Reading: the library unserializes, which is fine — but if a plugin stored JSON in meta_value, you'll get a string, not an array. Write a tolerant decoder (is_serialized() check, then json_decode fallback). Writing: PHP serialization is length-prefixed; hand-editing a serialized string in SQL will corrupt it. Never UPDATE ... REPLACE(...) on serialized values.

guid is not a permalink. It's a permanent identifier set at creation time and never updated, even if the domain changes. Build URLs from post_name + your permalink structure, or read the permalink_structure option — don't use guid.

Timezone confusion. post_date and post_modified are in the site's local timezone (timezone_string / gmt_offset options); post_date_gmt and post_modified_gmt are UTC. Pick one convention (UTC, ideally) and be explicit about it in your repository. Scheduled posts (post_status = 'future') sit in post_date_gmt until they publish.

Filter post_status and post_type deliberately. Revisions (post_type = 'revision', post_status = 'inherit'), auto-drafts, and trashed posts (post_status = 'trash') inflate wp_posts massively and will silently pollute counts and listings. Attachments are post_type = 'attachment'. A "count all posts" query that omits these filters is wrong roughly always.

Duplicate meta rows are legal. MySQL doesn't enforce uniqueness on (post_id, meta_key); add_post_meta() can create several rows for the same key. get_post_meta() in WP returns the first (or all) — the library typically returns one value. If you care, query wp_postmeta directly with an explicit ORDER BY meta_id and decide which row wins. Same story for wp_usermeta.

Denormalized columns go stale after direct writes. wp_term_taxonomy.count and the comment_count on posts are incremented by WordPress, not by MySQL. If you insert rows into wp_term_relationships or wp_comments directly, those counters drift. Prefer going through WP for anything that touches them.

WooCommerce HPOS. If the site runs WooCommerce with High-Performance Order Storage enabled, orders live in wc_orders / wc_orders_meta / wc_order_addresses, not wp_posts. The library's post-based mapping will simply not see them. Check woocommerce_custom_orders_table_enabled before promising an orders feature; you'll either need HPOS compatibility mode on the WP side or bespoke queries.

Transactions may be a lie. Old WordPress installs still ship MyISAM tables, where BEGIN/ROLLBACK are no-ops. Check SHOW TABLE STATUS and confirm InnoDB before relying on atomicity.

Charset and collation. A utf8 (3-byte) connection against a utf8mb4 schema will silently mangle emoji and 4-byte characters. Set the charset explicitly on the connection, matching DB_CHARSET in wp-config.php.

Keep a read-only credential. Run with SELECT-only grants in development and staging, and only elevate in production where you have explicit write requirements. It converts "oops, I corrupted the CMS" into "oops, permission denied".

Test against a real dump, not a guess. mysqldump --no-data of a production WP install (plus a handful of rows) as a fixture will catch prefix mismatches, plugin-added columns, and serialization edge cases that hand-written fixtures miss. The library's own tests/ fixtures are a good starting point to copy.

Pin the version. The package is a single-maintainer project with a 2024-era last release; composer.json constraints like ^2.0 (or whatever the current major is) rather than * will save you a Tuesday. Read the changelog before bumping — entity property names and repository method signatures have changed across majors.

Extension points. Most customization happens by (a) subclassing an entity/repository for custom post types and extra columns, (b) composing the provided repositories inside your own service, and (c) dropping to the raw DBAL connection for reporting queries. There's no plugin system to speak of — the library is intentionally thin, which is a feature. Don't fight it; wrap it.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect