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

tiny-pixel/acorn-db

Acorn DB brings Laravel-style database tooling to WordPress. Use Eloquent models, migrations, and a fluent query builder to manage custom tables and query data with a familiar, expressive API inside Sage themes and Acorn-powered plugins.

View on GitHub
Deep Wiki
Context7

Getting Started

tiny-pixel/acorn-db gives an Acorn (Roots/Sage) site an Eloquent query layer over the standard WordPress tables. It does not replace WordPress — it reads (and to a degree writes) wp_posts, wp_users, wp_terms, wp_comments, etc. through illuminate/database.

Install and boot

composer require tiny-pixel/acorn-db

Acorn's package discovery registers the service provider automatically; if you are on an older Acorn, add TinyPixel\AcornDB\Providers\AcornDBServiceProvider::class to config/app.php → providers. The package builds an Eloquent capsule from WordPress' existing database credentials (DB_* constants / $wpdb), so there is usually no separate config/database.php to fill in. If a publishable config is present:

wp acorn vendor:publish --tag=acorn-db

Where to look first

  1. src/Model/ — the shipped models (Post, Page, Attachment, User, Term, Comment). This is the real documentation; read the booted()/scope methods before trusting any README.
  2. The service provider — to see how the connection is wired (Capsule, setAsGlobal, bootEloquent) and whether it reuses $wpdb->dbh.
  3. config/ — for table prefix handling and any connection overrides.

First use case

Swap a WP_Query loop in a controller or Blade view for a model query:

use TinyPixel\AcornDB\Model\Post;

$posts = Post::with('thumbnail')
    ->where('post_date', '>', now()->subMonth())
    ->orderByDesc('post_date')
    ->take(6)
    ->get();

Then in Blade: @foreach ($posts as $post) <h2>{{ $post->post_title }}</h2> @endforeach. That single refactor — typed collections, eager loading, no setup_postdata()/wp_reset_postdata() bookkeeping — is the day-one payoff.

Implementation Patterns

Treat models as read models first. The most reliable use of this package is querying content for templates, REST endpoints, and block render callbacks. Use it where you would otherwise write get_posts() with meta_query/tax_query arrays that are painful to read.

Scopes over raw where clauses. The shipped Post model constrains by post_type and (typically) post_status. Lean on that:

// Published posts of the default type
Post::where('post_title', 'like', "%{$q}%")->get();

// Escape the built-in status/type constraints deliberately
Post::withoutGlobalScopes()
    ->where('post_type', 'event')
    ->where('post_status', 'publish')
    ->get();

Custom post types and taxonomies. Subclass the base model rather than re-configuring one:

namespace App\Models;

use TinyPixel\AcornDB\Model\Post;

class Event extends Post
{
    protected $postType = 'event';   // confirm the property name in the parent
    protected $with = ['meta'];
}

Register these in a small service provider or a Models directory so controllers never reference the vendor namespace directly — it keeps the package swappable.

Eager load relationships aggressively. author, meta, terms, thumbnail all resolve to extra queries. Post::with(['author', 'terms'])->get() turns an N+1 into three queries. For a 50-post archive this is the difference between 5ms and 300ms.

Meta via relations. Rather than get_post_meta() scattered through templates, expose accessors on your subclass:

public function getSubtitleAttribute(): ?string
{
    return $this->meta->firstWhere('meta_key', 'subtitle')?->meta_value;
}

Terms and filtering. belongsToMany through term_relationships/term_taxonomy is where Eloquent pays for itself:

Post::whereHas('terms', fn ($q) => $q->where('slug', 'featured'))->get();

Note this is not the same caching behaviour as WP_Query's taxonomy cache — see gotchas.

Read/write split. Use Eloquent for reads; keep wp_insert_post(), wp_update_post(), wp_set_object_terms() for writes. Where you must write through the model, wrap it in a repository method and follow up with clean_post_cache($id) so WordPress' object cache and WP_Query see the change.

Raw SQL escape hatch. The capsule registers a global connection, so Illuminate\Support\Facades\DB works for tables the package has no model for (wp_options, wp_usermeta, custom plugin tables):

DB::table('options')->where('option_name', 'blogname')->value('option_value');

Blade/controller integration. Return collections directly from Sage controllers; they are Arrayable and loop cleanly. For anything needing the_title()/the_content() filters, hydrate the WP post object (get_post($model->ID)) or call apply_filters('the_content', $model->post_content) explicitly — do not assume the model applies content filters for you.

Gotchas and Tips

  • Primary keys and timestamps are non-standard. ID, not id; post_date, not created_at; $timestamps = false. $model->id will be null and $model->created_at will throw or return nothing. Route-model binding by ID needs getRouteKeyName() overridden.
  • Global scopes hide data. Drafts, private posts, and attachments routinely "disappear" from queries because of the default type/status scopes. When a count looks wrong, ->withoutGlobalScopes() is the first thing to try, and ->toSql() with bindings the second.
  • No migrations, no schema ownership. WordPress owns the tables. Never php acorn migrate against them, and don't assume column types — wp_posts has been MyISAM historically, so transactions may silently not roll back. Verify the storage engine before writing transaction-dependent code.
  • Writes bypass WordPress hooks and caches. A direct $model->save() skips save_post, revisions, clean_post_cache, term counting, and search index plugins. This is the single biggest source of "the post exists but nothing renders" bugs. Prefer the WP API for mutation.
  • Multisite is the sharp edge. Table prefixes are resolved at connection time; after switch_to_blog() the Eloquent connection still points at the old prefix unless you reconnect. Explicitly purge/reconfigure the capsule on blog switch, or scope queries manually. If multisite is central to the project, budget time to prove this out before committing to the package.
  • Connection reuse. If the provider reuses $wpdb->dbh, a DB::disconnect() or a long-running worker (queue, WP-CLI loop) can leave you with a dead handle. Reconnect in long-lived processes.
  • Serialized and array meta. meta_value comes back as a raw string. Run maybe_unserialize() yourself; Eloquent casts will not do it for you.
  • Term counts and taxonomy caches are stale by design. Eloquent reads the tables directly, so counts updated by wp_update_term_count() lag until WP runs. Fine for display, dangerous for anything transactional.
  • Debugging. Enable the query log (DB::connection()->enableQueryLog()) or DB::listen() in a service provider during development; that is how you catch both N+1s and scope surprises. ->dd()/->dump() on the builder is available through the Illuminate macros.
  • Version pinning matters. With ~36 stars and no dependents, this is a thin wrapper around illuminate/database. Pin the version and read the changelog on every update; the blast radius of an Acorn or Illuminate major bump is entirely yours. If you only need meta queries, weigh WP_Query + a small repository over adding a dependency that few others exercise.
  • Extension points are the models themselves: subclass, add scopes, add accessors, and add global scopes for cross-cutting concerns (e.g. a SiteScope for multisite). Keep vendor-class references confined to that one layer so a future package swap is a refactor of five files, not fifty.
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.
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
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
cgv/localized-resource