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 Bundle Laravel Package

happyr/wordpress-bundle

Symfony bundle that integrates WordPress into your Symfony application, letting you run WordPress alongside Symfony routing, services, and templating so content can be managed in WordPress while the front end is built with Symfony.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually installing: a read-only Symfony adapter for a WordPress database. It does not boot WordPress, does not run wp-load.php, and does not give you WP_Query unless you separately wire that in. It ships Doctrine mappings for the WP tables (wp_posts, wp_postmeta, wp_terms, wp_term_taxonomy, wp_term_relationships, wp_users), a PostManager with a cache layer, a Twig extension, shortcode handling, a controller that reproduces WordPress permalinks so /my-post/ resolves in Symfony, and a content-image downloader that pulls <img> sources referenced in post bodies onto your local filesystem and rewrites them.

Minimal steps

  1. composer require happyr/wordpress-bundle (tags are sparse — 0.4.1 is a one-line fix on top of a 2021-era codebase; check it installs against your PHP/Doctrine version before committing to it).
  2. Register the bundle in config/bundles.php:
    Happyr\WordPressBundle\HappyrWordPressBundle::class => ['all' => true],
    
  3. Give Doctrine a second connection pointed at the WP database. Read-only credentials are strongly recommended:
    # config/packages/doctrine.yaml
    doctrine:
        dbal:
            connections:
                wordpress:
                    url: '%env(WORDPRESS_DATABASE_URL)%'
        orm:
            entity_managers:
                wordpress:
                    connection: wordpress
                    mappings:
                        HappyRWordPress:
                            type: annotation   # or attribute on newer Doctrine
                            dir: '%kernel.project_dir%/vendor/happyr/wordpress-bundle/Entity'
                            prefix: 'Happyr\WordPressBundle\Entity'
    
  4. Point the bundle at that EM, at the table prefix, and at an image destination:
    # config/packages/happyr_wordpress.yaml
    happyr_wordpress:
        table_prefix: wp_
        entity_manager: wordpress
        # cache: cache.app            # exact key varies by version — see DependencyInjection/Configuration.php
        # image_download_path: '%kernel.project_dir%/public/uploads/wp'   # verify the key name in Configuration.php
    
    Whatever the download option is called, set it explicitly. The default writes into the project tree, and a silent default is how you end up with a git-ignored directory full of editor uploads.
  5. First use case — a blog index you control, plus letting the bundle serve the post detail pages:
    #[Route('/blog', name: 'blog_index')]
    public function index(PostManager $posts): Response
    {
        return $this->render('blog/index.html.twig', [
            // third arg is the image-download switch — pass false for index pages
            'posts' => $posts->getPosts(10, 0, false),
        ]);
    }
    
    {% for post in posts %}
        <a href="{{ path('happyr_wordpress_post', {slug: post.postName}) }}">{{ post.postTitle }}</a>
        <div>{{ post.postExcerpt|raw }}</div>
    {% endfor %}
    

Where to look first, in this order: Resources/config/ (service ids and the controller route names), DependencyInjection/Configuration.php (what is actually configurable — it's a short list, but it now includes the image/upload settings, so read it rather than guessing), Entity/Post.php and Repository/PostRepository.php (what fields and query helpers you really get), then Service/PostManager.php (caching semantics and the image-download flag plumbing). Run bin/console debug:container happyr_wordpress and bin/console doctrine:mapping:info --em=wordpress before writing any feature code; ten minutes here saves a day of guessing.

Implementation Patterns

Pattern 1 — PostManager as the only entry point. Treat PostManager as your repository boundary and never inject the Doctrine EntityManager for WordPress into application code. That keeps the cache in play and gives you one place to swap out the package later. If getPosts()/getPost() don't cover your query (category archives, search, related posts), write a small WordPressQuery service over the wordpress EM and decorate/cache it yourself — don't scatter DQL through controllers.

Pattern 2 — Let the bundle own permalinks, you own everything else. The bundle's controller maps /{slug} (and typically category/tag variants) to a post. Keep your own routes for /blog, /archive, /search, /feed, and give them explicit paths that can't collide. If you need finer control, disable the bundle's route import and write a single catch-all controller that resolves last:

#[Route('/{slug}', name: 'wp_post', requirements: ['slug' => '[^/]+'], priority: -100)]
public function show(string $slug, PostManager $posts): Response
{
    $post = $posts->getPost($slug) ?? throw $this->createNotFoundException();
    return $this->render('blog/show.html.twig', ['post' => $post]);
}

Pattern 3 — Twig-first for marketing pages. For landing pages built by editors in WP, skip controllers entirely and pull content by slug in the template. This is the highest-leverage use of the package: a designer-editable block inside an otherwise normal Symfony page.

{# templates/home/about.html.twig #}
{{ wp_post('about-us').postContent|raw }}

Pattern 4 — Shortcodes. WordPress bodies contain [gallery ids="1,2,3"], [caption], and plugin shortcodes. Implement ShortcodeInterface for the ones you care about, register it as a service with the bundle's tag, and render before output. Anything you don't handle will leak as literal bracket text — so add an "unknown shortcode stripper" as a final pass, otherwise editors will file bugs about [contact-form-7 ...] appearing on the page.

Pattern 5 — Be deliberate about image downloading. The bundle rewrites content images to local files and fetches them on first read. That is a write performed by a supposedly read-only adapter, and it happens inside the request that renders the page. Two rules: (a) only pass the download flag on the detail/landing-page render path, never on listings, feeds, sitemaps, or search — those can touch hundreds of posts and will stampede the WordPress host; (b) pre-warm by running the download during deploy rather than letting the first visitor pay for it. 0.4.1 tightens exactly this: it skips fetching when the image isn't actually needed, so upgrading reduces redundant traffic — but the flag is still yours to pass correctly.

Pattern 6 — Cache warmup as a deploy step. The bundle exposes a warmup command (check Command/ for the exact name). Hook it into your deploy after cache clear, and treat any publish in WP as invalidating it — the bundle cannot know that WordPress changed. A tiny WP plugin that POSTs to a Symfony webhook (/internal/wp-purge, secured by a shared secret) which deletes the relevant cache keys is the cheapest way to get fresh content without a stale-hour complaint. Remember the warmup also triggers image downloads, so decide whether that belongs in the deploy step or is deferred.

Pattern 7 — Multi-site / multi-prefix. One connection + one EM per WP site, with different table_prefix values and different image destinations. Don't try to make a single EM do both; the table prefix is baked into mappings at container build time, and the downloader would otherwise overwrite one site's media with another's.

Pattern 8 — Content projection for anything that needs to be searchable or relational. If you need faceted search, related products, or a stable content model, mirror posts into your own entity on a schedule (command or WP webhook) and serve from there. WordPress tables are not indexed for your queries and wp_postmeta joins get expensive fast. Mirroring also gives you a stable place to keep downloaded image paths.

Integration tips: use a read replica for the WordPress connection; add wrapper_class/server_version if the WP MySQL flavour differs from your app's; call $em->clear() between batches in commands and workers to avoid identity-map bloat; paginate with limit/offset arguments rather than a Doctrine Paginator unless you've enabled it on the wordpress EM; and make sure your image directory is writable by the web user and excluded from git status noise.

Gotchas and Tips

  • post_status and post_type are not filtered for you in raw queries. The convenience methods usually filter to publish, but any DQL you write must add post_status = 'publish' AND post_type = 'post'. Revisions, autosaves, attachments, and nav menu items all live in wp_posts and will happily appear in your loop.
  • Slug collisions throw. post_name is not globally unique — attachments and pages can share a slug with a post. A getOneOrNullResult() will raise NonUniqueResultException. Always constrain post_type (or order and take the first) in a slug lookup.
  • Dates. post_date is site-local, post_date_gmt is UTC. Pick one (usually GMT) and convert explicitly; Symfony's default timezone will silently disagree with the WP setting.
  • Never run schema tooling on this EM. doctrine:schema:update --em=wordpress, migrations diffs, and doctrine:fixtures:load must never touch the WordPress connection. Guard it in CI and in your Makefile.
  • The mapping is owned by the vendor. Column names are mapped to PHP properties (post_title → postTitle etc.); WP plugins adding columns are invisible. For custom fields, go through wp_postmeta directly or a custom DQL join.
  • HTML is raw and unsanitized. post_content comes from editors with unfiltered_html capability — output with |raw and accept it, but never render it in an admin-only or user-generated context. Consider an HTML purifier pass for low-trust editors.
  • Block editor markup. WP 5+ content is full of <!-- wp:paragraph --> comments. They're harmless in browsers but will pollute excerpts, meta descriptions, and any text extraction. Strip comments before truncating. This applies equally to downloaded-image-re-written HTML.
  • Featured images are a three-hop lookup. _thumbnail_id postmeta → attachment post → _wp_attached_file → /wp-content/uploads/.... The guid column is not a reliable URL. Build the URL yourself from a configured uploads base, or you'll hardcode a host. Note this path is separate from the content-image downloader — the downloader handles in-body <img> tags, not the featured image.
  • Image downloads are a side effect you did not ask for. They run on read, they write to disk, and they are not transactional with the cache. If the filesystem is read-only (container, Lambda), the render will fail or silently produce broken src. Test on your real deploy target early, and decide up front whether media is served from your app or proxied/cached from WordPress.
  • Serialized PHP in wp_options and wp_postmeta. Anything menu-, widget-, or ACF-related is serialize()d. Use unserialize($value, ['allowed_classes' => false]) — never plain unserialize() on database content.
  • Route collisions and trailing slashes. A catch-all /{slug} will eat /login, /api/..., and /sitemap.xml if route order slips. Add a requirements negative lookahead or an explicit priority, and decide on trailing-slash canonicalisation up front (WP links usually have it).
  • Cache staleness is the #1 support ticket. Editors publish, nothing changes, they publish again. Set a short TTL (5–15 min) as a safety net even with warmup + purge webhooks, and log cache hits/misses on the PostManager pool so you can prove what happened.
  • Debugging toolkit: debug:router --show-controllers to see exactly which route the bundle claimed, debug:container happyr_wordpress for service ids, doctrine:mapping:info --em=wordpress for mapping errors, and SQL logging on the wordpress connection (set logging: true in dev only) to see the joins being generated. For image issues, check the download directory permissions and the outbound HTTP client config before blaming the mapping.
  • Extension points, in order of safety: custom shortcodes (tagged services) → decorating PostManager (including wrapping the download call) → decorating the Twig extension → replacing the controller via your own route → forking. Avoid editing vendor code; the release cadence here is slow and you'll be re-applying patches.
  • Upgrading to 0.4.1 is low risk and mildly beneficial. The change only avoids downloading images that aren't needed; it doesn't alter signatures or output markup in any way the docs call out. Re-run your warmup and smoke-test a post with inline images to confirm nothing started returning a remote src. If you were relying on the old eager behaviour to populate your media directory, move that to an explicit warmup/prefetch step now.
  • Project risk. Single-digit-star territory, essentially no dependents, and long gaps between tags — 0.4.1 is a one-line fix, not a sign of renewed maintenance. Expect Doctrine attribute-vs-annotation friction on modern Symfony and PHP 8.x notices. Budget for a fork, and keep the WordPress access behind your own thin interface (PostRepository, ContentBlockProvider, MediaFetcher) so a future rewrite is a swap, not a refactor. The whole useful surface is a few hundred lines — if you hit more than two real blockers, writing your own read-only repository is a legitimate call.
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