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

Acorn Laravel Package

roots/acorn

Acorn brings Laravel to WordPress: use Blade templates, routing, caching, events, validation, and Laravel packages in your WordPress projects, with Laravel's patterns and utilities for cleaner code.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites and install

  • WordPress (Bedrock is the happy path), Composer, and PHP 8.3+ — Acorn v6 tracks Laravel v13 and enforces this at the Composer level.
  • Acorn is the framework underneath Sage 10, so if you're on Sage it's already installed. For a non-Sage theme or plugin: composer require roots/acorn and boot it from the theme's functions.php.
  • Confirm the app is booted before you write a line of code:
wp acorn about          # version, Laravel version, environment
wp acorn list           # every command Acorn adds to WP-CLI

If wp acorn doesn't exist, Acorn isn't booting — that's your first bug, not your Blade template.

Where to look first, in this order

  1. functions.php — the bootloader entry point. Everything else depends on it running.
  2. app/Providers/ThemeServiceProvider.php — where you'll register view composers, bindings, hooks, and Blade directives. This is your daily driver file.
  3. config/*.php — Laravel-style config (app.php, view.php, assets.php, database.php). Publish defaults with wp acorn vendor:publish if a config file is missing.
  4. routes/web.php — Acorn's route file. Even if you never add a route, knowing it exists explains a lot of "why is WordPress not handling this URL" behaviour.
  5. resources/views/ — Blade templates that map onto the WordPress template hierarchy (index, single, archive, page-{slug}, 404), plus layouts/app.blade.php.

First use case: turn one PHP template into Blade

Rename single.php → resources/views/single.blade.php and keep the WordPress loop intact:

{{-- resources/views/single.blade.php --}}
@extends('layouts.app')

@section('content')
  @while(have_posts()) @php(the_post())
    <article @php(post_class())>
      <h1>{{ get_the_title() }}</h1>
      {!! get_the_content() !!}
    </article>
  @endwhile
@endsection

That single change is the assessment: you now have layout inheritance, escaping by default, and components — while every get_the_* function still works. Do not migrate the whole theme at once; migrate templates you're actively touching.

Second use case: a page WordPress shouldn't own

// routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/quote-calculator', fn () => view('pages.calculator'))->name('quote');

Verify with wp acorn route:list, then hit the URL. Acorn short-circuits parse_request so WordPress doesn't 404 on it.


Implementation Patterns

Blade for everything WordPress renders

  • Keep the template hierarchy. Acorn's view finder resolves single-{post_type} and friends, and (since v6.3) resolves views living outside the theme directory — which is what makes package/plugin-provided views work.
  • Use Roots\view() inside render_callback for blocks and shortcodes instead of include:
register_block_type('theme/hero', [
    'render_callback' => fn ($attributes) => view('blocks.hero', [
        'heading' => $attributes['heading'] ?? '',
    ])->render(),
]);
  • Reach for Blade components (<x-button variant="primary">) for design-system primitives. Use @props + $attributes so components stay WP-agnostic.

View composers as your data layer (not functions.php)

Put query logic in a composer, not in the template. Since v4, public methods and properties on a composer are automatically exposed to the view:

namespace App\View\Composers;

use Roots\Acorn\View\Composer;

class Archive extends Composer
{
    protected static $views = ['archive', 'index'];

    public function posts(): array
    {
        return get_posts(['numberposts' => 6, 'post_status' => 'publish']);
    }
}
// ThemeServiceProvider::boot()
View::composer(['archive', 'index'], \App\View\Composers\Archive::class);

Register composers in a provider, not by calling View::composer() at file scope — the provider is loaded once and is cache-friendly.

Routing + controllers for actual application endpoints

Use Laravel routes for form posts, JSON endpoints, and authenticated flows; leave content URLs to WordPress.

Route::middleware('web')->group(function () {
    Route::post('/contact', ContactController::class)->name('contact');
});

Route::get('/api/products', [ProductController::class, 'index']);
  • Use invokable controllers with constructor injection — this is the main reason to adopt Acorn at all.
  • Validate with $request->validate([...]) and redirect back with ->withInput()->withErrors(). Errors land in the session, so the web middleware group matters.
  • Prefer custom routes over admin-ajax.php. Acorn deliberately bails out of bootstrapping on wp-admin, wp-login.php, and other /wp- requests, so admin-ajax is not a friendly place for framework code. Use the REST API or a named route.

Assets: use the Bundle API, not raw wp_enqueue_script

bundle('app')->enqueue();                       // CSS + JS from the manifest
bundle('app')->enqueueJs(async: true);          // v4+ supports async/defer

In Blade, use the Vite directive and CSP nonce helpers:

@vite(['resources/js/app.js', 'resources/css/app.css'])

If you call Vite::useCspNonce() / useIntegrityKey() / prefetch(), upgrade to ≥ 6.1 — earlier versions dropped that state because the Vite singleton wasn't shared across resolution keys.

Bridging WordPress hooks into Laravel idioms

  • Dispatch events from hooks and listen in providers, so business logic lives in classes:
add_action('save_post', fn ($id) => Event::dispatch(new PostSaved($id)));
  • Resolve services out of the container inside hook callbacks rather than new-ing them: app(Indexer::class)->index($id).
  • Keep provider boot() cheap. It runs on every front-end request. Wrap expensive registration in is_admin() or wp_doing_ajax() guards.

Console, database, and deploy

  • Custom tables: use illuminate/database with a config/database.php connection and wp acorn migrate. This is excellent for plugin-owned tables — and a trap for WordPress core tables (see Gotchas).
  • Scheduled/queued work: the queue component exists but there's no long-running worker in a WP request lifecycle. Either run wp acorn queue:work --stop-when-empty from cron/systemd, or use the sync driver and accept inline execution.
  • Deploy script should include wp acorn optimize:clear (or at minimum view:clear) and, if you're routing heavily, wp acorn route:cache. storage:link / storage:unlink are registered as wp acorn commands as of v6.2 — use them for uploads outside wp-content/uploads if you need public disk URLs.

Livewire for interactive UI

Acorn v4+ ships Livewire support. For a searchable archive, filtered listing, or multi-step form, a Livewire component beats hand-rolled fetch + admin-ajax. Scaffold, register the component, and render it in Blade:

<livewire:product-search />

Keep Livewire components out of the WP loop; pass IDs in, query inside the component.

Extending the framework

  • Custom Blade directives: Blade::directive('svg', fn ($expr) => "<?php echo ... ?>") in a provider.
  • Container bindings for testability: bind an interface to a WP-backed implementation, then inject it in controllers and composers.
  • Publish and override config rather than filtering at runtime — config is the supported extension point; Composer::with()/override() behaviour was buggy in 4.0.2 and earlier (double-triggering), so pin ≥ 4.0.3 if you subclass those.

Gotchas and Tips

The unsupported-components list is a hard boundary

illuminate/mail, notifications, broadcasting, redis, and translation are not supported. Concretely:

  • No Mail::send() and no Notifiable trait. Use wp_mail(), or an SMTP plugin, or a transactional-email API through the HTTP client (guzzlehttp/guzzle was added specifically to make Http:: work).
  • No __()/trans() from Laravel. Use WordPress's __(), _e(), esc_html__() and text domains. Mixing the two silently returns the input string, which is a nasty "translation just doesn't work" bug.
  • No Redis facade. If you need Redis, use the WP Redis object-cache drop-in and a community Acorn cache package; Cache::store('redis') will not work.
  • No broadcasting means no Echo/Pusher wiring out of the box — poll or use a route-based approach.

Don't point illuminate/database at WordPress core tables

It will appear to work, and then bite you: $wpdb's table prefix, the object cache, and wp_posts semantics (revisions, autosaves, post_status quirks) aren't modelled by Eloquent. Use Eloquent for your own tables only. If you must read core data, use $wpdb or WP_Query and shape the result in a composer or repository.

Request handling is deliberately conservative

  • Acorn bails early on wp-login.php, wp-admin, and other /wp- paths. Don't put Blade or container-dependent logic in code that only runs there.
  • The Laravel-style HTTP response wrapper for WordPress requests is experimental and off by default since v4.0.2. It's behind ACORN_ENABLE_EXPERIMENTAL_WORDPRESS_REQUEST_HANDLER=true. Do not build middleware that expects $response on normal WordPress page loads; middleware is for your custom routes only.
  • Custom routes short-circuit WordPress — which means a route that should fall through to a canonical redirect or the 404 template may not. If a route 404s weirdly, check for a matching pattern before assuming a bug. Trailing-slash and subdirectory installs have historically produced exceptions here; v4.1+ fixed the subfolder/malformed-path cases, so stay current.

Route caching

route:cache is a genuine deploy-time win, but stale caches after renaming or removing a route produce baffling 404s. Always pair it with route:clear in local dev, and never commit the cache.

Blade compile cache and permissions

Compiled views live under storage/framework/views (or your configured view.compiled). Symptom of a permissions problem: templates silently render stale or throw on write, only on the production server. Fix by setting ownership correctly, then wp acorn view:clear. Also clear views when you rename a template — the hierarchy resolver caches aggressively.

functions.php scope creep

Every add_action in functions.php bypasses the container and can't be unit tested. Treat it as a bootstrap file: require the autoloader, boot Acorn, and move on. Everything else belongs in app/.

Debugging toolkit

  • wp acorn about first, always — environment, Laravel version, config source.
  • APP_DEBUG=true gets you the Ignition error page with solutions; Acorn registers Manifest and SkipProvider solutions, which is far better than a blank white screen when a Vite manifest is missing.
  • dump() / dd() are available (symfony/var-dumper is a dependency, and v6.2 widened the constraint to ^8.0).
  • Log::info() writes to storage/logs/laravel.log; error_log() / WP_DEBUG_LOG still write to the WP debug log. Know which one you're reading.
  • wp acorn route:list before you debug a 404; wp acorn config:show <key> when a config value "isn't taking".

Container vs. globals — the mental model

Acorn does not wrap $post, $wp_query, or $current_user in the container. Inside Blade and composers, use get_post(), wp_get_current_user(), get_queried_object(). Reserve injection for your own services. Mixing the two produces "works in the controller, undefined in the view" bugs.

Version-pin deliberately

  • PHP 8.3+ and Laravel 13 mean many older WP plugins/packages (and some community Acorn packages) are incompatible. Audit before upgrading.
  • The v6.0 upgrade guide is required reading if you're coming from v5 — it's a PHP floor bump plus a Laravel major.
  • v6.3's Blade fix for views outside the theme directory matters if you consume packages that ship views; earlier behaviour failed to resolve them. If you're writing an Acorn package, target ≥ 6.3.
  • Composer::with()/override() double-triggering was fixed in 4.0.3; anything older will run your composer setup twice per render.

Escape by default, and know when not to

{{ }} escapes; {!! !!} doesn't. get_the_content() needs the latter (or apply_filters('the_content', ...)) because it's already-sanitised HTML. For ACF/WYSIWYG fields, run wp_kses_post() before echoing. The most common Acorn security bug is a developer treating {!! !!} as "how you output content."

Where to extend rather than patch

Service providers (app/Providers), published config, view composers, Blade directives, custom wp acorn commands, and the Bundle API for assets. If you find yourself editing something inside vendor/roots/acorn, you almost certainly want a filter, a config override, or a child provider — this package moves fast (six minor releases in the v6 line), and forks rot.

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.
thuraaung2493/laravel-rulecraft
cyberwizard/schedule-catchup
livetennisapi/livetennisapi-laravel
roundly-consulting/notifications-for-laravel
flightsadmin/bootstrap-starter-kit
lasserafn/laravel-intempus
likyly/sdk
tdt/input
coreation/php-shapefile
coreation/gisconverter
primix/widgets
primix/notifications
primix/actions
primix/tables
primix/details
primix/forms
primix/support
marcelgwerder/laravel-api-handler
firebrikdotcom/conveyor-laravel-broadcaster
kanata-php/conveyor-laravel-broadcaster