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

Eval Command Laravel Package

wp-cli/eval-command

Run arbitrary PHP code or files from the command line with WP-CLI. wp eval executes a code string, while wp eval-file loads a PHP file with positional arguments. Both support --skip-wordpress to run without WordPress and --hook to run after a specific WordPress hook fires.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations. This is not a Composer package you require into a Laravel app. It is a WP-CLI bundled command that ships with WP-CLI itself, so there is nothing to install on a normal machine — if wp --info works, wp eval already exists. The Laravel-developer value proposition is narrow but real: whenever a Laravel codebase touches WordPress (headless WP as a content API, a legacy WP install being migrated into Laravel, a corcel-backed read layer, or a WP site deployed from the same pipeline as a Laravel app), wp eval is the fastest way to inspect and mutate WordPress state from the shell — the closest thing WordPress has to php artisan tinker.

Minimal verification steps:

wp --info                              # confirm WP-CLI + PHP version
wp eval 'echo WP_CONTENT_DIR;'         # does it see the WordPress install?
wp eval 'echo get_bloginfo("version");'
wp eval-file - < /dev/null             # confirm STDIN path works

Run these from the WordPress root (or pass --path=/var/www/wp). If wp eval 'echo WP_CONTENT_DIR;' prints an empty line, you are outside a WP install; if it errors, you are likely on PHP < 7.2.24 (the floor set in v2.2.7; recent releases require WP-CLI ≥ 2.13).

Where to look first: wp help eval and wp help eval-file are generated from the same docblocks as the README and are always accurate for your installed version. Then read the two flags that determine everything about behaviour — --skip-wordpress and --hook=<hook> — because they define the three execution contexts you will actually use.

Your first use case should be the one that replaces "SSH in, edit a theme file, reload, revert": a one-off data check against the live database.

# Laravel dev's "what's in this table?" moment, WordPress edition
wp eval 'global $wpdb; var_export( $wpdb->get_results( "SELECT option_name FROM {$wpdb->options} WHERE autoload = \"yes\" LIMIT 20" ) );'

# Equivalent of tinker's User::count()
wp eval 'echo count( get_users( [ "role" => "administrator" ] ) );'

Installation note: you only ever run wp package install wp-cli/eval-command to override the bundled version (e.g. to pull in the --hook flag on an older WP-CLI, or to test a branch). Do not add this to a Laravel composer.json; keep WP-CLI in your global toolchain or a project-local vendor/bin via wp-cli/wp-cli-bundle in a separate tooling directory.

Implementation Patterns

1. Treat bin/*.php as your WordPress "Artisan commands"

The single highest-leverage habit: never grow a wp eval one-liner beyond a line or two. Promote it to a versioned file and run it with wp eval-file. This gives you reviewable, testable, re-runnable scripts that live in the repo next to your Laravel app/Console/Commands.

wp eval-file bin/wp/backfill-meta.php
<?php
// bin/wp/backfill-meta.php
// Runs with WordPress fully loaded; $args holds positional CLI arguments.

global $wpdb;

$limit = isset( $args[0] ) ? (int) $args[0] : 100;
$dry   = in_array( '--dry-run', $args, true );

$ids = get_posts( [
    'post_type'      => 'post',
    'posts_per_page' => $limit,
    'fields'         => 'ids',
    'meta_key'       => '_legacy_flag',
] );

foreach ( $ids as $id ) {
    if ( $dry ) {
        WP_CLI::log( "would migrate {$id}" );
        continue;
    }
    update_post_meta( $id, '_new_flag', get_post_meta( $id, '_legacy_flag', true ) );
    delete_post_meta( $id, '_legacy_flag' );
}

WP_CLI::success( sprintf( '%d posts processed', count( $ids ) ) );

Key details that make this work like an Artisan command:

  • $args is a positional array (wp eval-file file.php 100 --dry-run → $args[0] === '100'). Since v2.2.7 $args is registered as a global, so functions inside your file still need global $args;.
  • Use WP_CLI::log(), WP_CLI::success(), WP_CLI::warning(), WP_CLI::error() instead of echo/exit(). WP_CLI::error() sets a non-zero exit code, which is what makes the script usable in CI or in a Laravel Process::run() call.
  • return and exit behave sanely; a throw produces a stack trace and a failure exit code.

2. Pipe generated code through STDIN to dodge quoting hell

The most common friction for a Laravel dev is bash quoting colliding with PHP's $ variables, namespaces (\WP_User), and nested quotes. Three escalating escapes:

# Single-quote the PHP, double-quote inside it
wp eval 'echo get_option( "blogname" );'

# Multi-line: use a heredoc into eval-file's STDIN target
wp eval-file - <<'PHP'
<?php
foreach ( get_sites() as $site ) {
    WP_CLI::log( $site->blog_id . ' ' . get_blog_option( $site->blog_id, 'blogname' ) );
}
PHP

# Or keep it in a file and use include semantics (see pattern 4)
wp eval-file bin/wp/audit.php --use-include

The - STDIN form combined with a quoted heredoc (<<'PHP') is the pattern to standardise on for anything with $ in it — it also keeps secrets and tokens out of shell history if you read from a file descriptor instead.

3. Pick the execution context deliberately (the --hook / --skip-wordpress triad)

Context Invocation When a Laravel dev needs it
No WordPress at all --skip-wordpress Pure PHP: hashing, JSON reshaping, calling a Laravel/Composer autoloader, checking an extension. Fast — no WP bootstrap.
WordPress, default (after_wp_load) plain wp eval Data reads/writes, $wpdb, options, posts, terms, cron. Most work happens here.
WordPress, deferred to a hook --hook=wp_loaded / init / after_setup_theme Anything requiring the current user, registered post types/taxonomies from plugins, rewrite rules, or code that must run at a plugin-like moment.

The --hook flag (added in v2.2.8) is the one Laravel devs under-use. Anything touching wp_get_current_user(), current_user_can(), or a custom post type registered by a plugin will silently misbehave under the default load order:

wp eval 'echo "Current user: " . wp_get_current_user()->user_login;' --hook=wp_loaded
wp eval 'var_dump( post_type_exists( "acme_product" ) );' --hook=init

4. Use --use-include for anything with a namespace or declare

Added in v2.2.0, this switches from eval() of the file's contents to include of the file itself. Two things only work this way:

  • declare( strict_types=1 );
  • namespace Acme\Scripts;

…because eval'd code is injected into a method body, where both statements are illegal. It also makes stack traces point at your real file and line numbers, and makes __FILE__/__DIR__ resolve naturally.

wp eval-file bin/wp/sync.php --use-include

Practical rule: eval-file for scripts you require other files from or that use namespaces; plain eval-file for throwaway scripts that just run top-to-bottom.

5. Bridge into a Laravel app from the WordPress side

For hybrid stacks where WordPress and Laravel share a codebase or a database, --skip-wordpress is your escape hatch to boot only what you need — but the genuinely useful direction is the reverse: use wp eval-file as a WordPress-context entry point that then pulls in shared domain code.

<?php
// bin/wp/publish-to-laravel.php — run with: wp eval-file bin/wp/publish-to-laravel.php
// WordPress context: read posts, hand them to a Laravel service, write back an ID.

require_once __DIR__ . '/../../vendor/autoload.php';   // __DIR__ is correct in eval-file

$app = require __DIR__ . '/../../bootstrap/app.php';
$app->make( Illuminate\Contracts\Console\Kernel::class )->bootstrap();

$importer = $app->make( App\Services\ContentImporter::class );

foreach ( get_posts( [ 'post_type' => 'post', 'numberposts' => -1 ] ) as $post ) {
    try {
        $id = $importer->import( $post );
        update_post_meta( $post->ID, '_laravel_id', $id );
    } catch ( Throwable $e ) {
        WP_CLI::warning( "post {$post->ID}: " . $e->getMessage() );
    }
}

WP_CLI::success( 'sync complete' );

This is the pattern that lets you keep one Eloquent model definition and reuse it from cron-less WP runs. Be aware you now have two frameworks' error handlers and two autoloaders in one process — see the gotchas.

6. Wrap it in your existing task runner

Because it is just a CLI process, it composes cleanly with the tooling a Laravel dev already has:

// composer.json (tooling-only section)
"scripts": {
    "wp:audit": "wp eval-file bin/wp/audit.php --use-include",
    "wp:seed-dev": "wp eval-file bin/wp/seed-dev.php --url=dev.example.test",
    "wp:migrate-urls": "wp search-replace 'http://old' 'https://new' --all-tables --dry-run"
}

And in CI, use eval as a smoke test rather than a data mutation:

wp eval 'exit( is_plugin_active( "advanced-custom-fields/acf.php" ) ? 0 : 1 );'
# non-zero exit fails the build

From Laravel itself, Process::run(['wp', 'eval', '...', '--path=/srv/wp']) gives you a typed, testable integration — with the caveat that you are invoking a process, so pass arguments as an array (never shell-concatenated strings) to avoid quoting and injection problems.

7. Prefer wp eval for reads, real commands for writes

The mature position: wp eval is a diagnostic and one-off tool, not a deployment mechanism. If a snippet becomes something you run on every deploy, promote it one of two ways:

  • A bin/wp/*.php script invoked from your deploy script (fast, cheap, zero packaging), or
  • A small custom WP-CLI command package (wp scaffold package) that exposes wp acme sync with proper argument parsing, --dry-run, and Behat coverage.

The second is what the WP-CLI maintainers expect; the eval command is explicitly framed as the escape hatch, not the architecture. Extension point worth knowing: --hook defers execution to an arbitrary hook name, so you can piggyback on your own custom action (--hook=acme_ready) and treat that as an extension seam without shipping a package.

8. Multisite and environment targeting

wp eval respects --url and --path, which is how you scope to one site in a network:

wp eval 'echo get_option( "blogname" );' --url=one.example.com
wp eval-file bin/wp/each-site.php --url=one.example.com   # inside: switch_to_blog() explicitly

Always pass --url explicitly in scripts. Relying on the default site makes a script that works locally destructive in a network context.

Gotchas and Tips

The "inside a method" rule is the #1 source of confusing bugs

Both README sections carry the same warning: code is executed within a method, so globals must be explicitly globalized. Concretely:

# Wrong — $wpdb is a local, null. Fatal error, not a silent empty result.
wp eval 'var_dump( $wpdb->prefix );'

# Right
wp eval 'global $wpdb; var_dump( $wpdb->prefix );'

Same for $post, $wp_query, $current_user, and — since v2.2.7 — $args, which is registered as a global specifically so files can global $args;. Inside a function in your eval'd file, you must globalize it again.

echo only — return values are discarded

wp eval 'return 1 + 1;' prints nothing and exits 0. This differs from tinker and trips people up constantly. Use echo, var_export(), var_dump(), print_r(), or WP_CLI::log(). For structured output, WP_CLI\Utils\format_items( 'table', $rows, $fields ) gives you readable tables for free.

__FILE__ / __DIR__ are rewritten, but only in eval mode

WP-CLI replaces path magic constants before eval() so that require __DIR__ . '/x.php' works inside an eval'd file (fixed in v2.0.3/v2.0.6, consolidated onto Utils\replace_path_consts() in v2.1.0). Two consequences:

  • It works, but it is a regex substitution — pathological strings containing __FILE__ inside larger literals can behave unexpectedly.
  • --use-include sidesteps the whole mechanism. If a script does heavy path work or requires siblings, use --use-include.

Related: v2.0.2 strips a leading #! /usr/bin/env php shebang from files, so your scripts can be directly executable and wp eval-file-able. Keep the shebang; don't remove it to work around something.

$assoc_args works in wp eval (since v2.2.9) — use it for parameters

wp eval 'WP_CLI::log( $assoc_args["env"] ?? "none" );' --env=staging

This is undocumented in the README body and easy to miss. It gives you a lightweight way to parameterise one-liners without switching to a file. For eval-file, prefer positional $args — it is explicitly supported and stable.

Fatal errors, output buffering, and half-finished writes

An eval'd snippet that fatals takes the whole WP-CLI process with it; there is no try/catch if the failure is a PHP fatal (type error, calling a method on null). Mitigations:

  • Wrap the risky body in try { … } catch ( \Throwable $e ) { WP_CLI::warning( $e->getMessage() ); } — this catches \Error too.
  • Anything that echoes before dying will have partially flushed output; if a script both prints and mutates, print first and mutate second, or buffer with ob_start().
  • Do not run mutations under --skip-wordpress "for speed" — you lose $wpdb and every WP function, and the failure mode is Call to undefined function, which reads like a broken install.

Shell quoting will bite you, and it will bite you silently

Double-quoted bash strings interpolate $wpdb, $args, and $post before PHP sees them. The symptom is a snippet that works in a PHP file and produces empty output on the CLI. Rules of thumb:

  • Always single-quote the PHP argument.
  • Prefer " inside PHP over ' to avoid nested-quote gymnastics.
  • When in doubt, use the wp eval-file - heredoc form from the patterns section.
  • Never eval a string built from a shell variable or user input — see the security note below.

Security posture: this is a sudo-class tool

wp eval is unauthenticated arbitrary code execution with the privileges of the WordPress install — full DB access, ability to shell_exec, ability to write files. Keep it to local/dev/staging and deliberate, reviewed production use. Concretely:

  • Do not expose it through any web endpoint, webhook, or admin-ajax handler "for convenience". There is no nonce, no capability check, no auth layer.
  • Do not interpolate user input into the --eval string. If you must accept input, pass it via $args and treat it as untrusted (esc_sql, absint, etc.).
  • Remember shell history and CI logs capture the full snippet. Read secrets from env (getenv()) or from a file via eval-file, never inline.
  • --skip-wordpress does not make it safe; it makes it marginally less dangerous.

Framework-collision hazards when bridging to Laravel

If you require a Laravel bootstrap inside wp eval-file (pattern 5), expect:

  • Autoloader conflicts. Two vendor/autoload.php files may both claim the same class. Check composer.json autoload maps before assuming the WP one wins.
  • Error handler ownership. Laravel's HandleExceptions bootstrap installs an error handler that will convert warnings to ErrorException, including WordPress deprecations. Wrap plugin-heavy calls in error_reporting() scoping or run the Laravel bootstrap after WordPress work, not before.
  • Container state. app() will be a singleton for the whole process; a loop over 5,000 posts sharing one container is fine for DB connections but will leak memory in Eloquent's relation cache. Chunk and unset() or use DB::table() for bulk work.
  • --skip-wordpress + Laravel is usually the wrong tool. If you don't need WordPress, use artisan tinker — you get proper REPL ergonomics, PsySH, and no quoting pain.

Version and platform constraints to check before adopting

  • PHP 7.2.24+ minimum; v2.2.10 requires WP-CLI v2.13. If your CI runs an older pinned WP-CLI, --hook and $assoc_args may be absent.
  • Multisite: always pass --url; never assume the default site.
  • Windows: functional tests were hardened for Windows (#93, #92), but quoting differs enough that you should standardise on wp eval-file script.php rather than inline wp eval in any cross-platform script.
  • Long-running loops: WP-CLI has no built-in progress or memory ceiling. Add WP_CLI\Utils\make_progress_bar() and wp_cache_flush()/unset() inside loops, or you will OOM on a large migration and leave data half-written.

Debugging checklist

When a snippet "does nothing":

  1. Did you echo? (return values are dropped)
  2. Did you global $wpdb; / global $args;?
  3. Did the shell eat your $variables? (switch to heredoc)
  4. Does the feature need a later hook? (add --hook=wp_loaded or --hook=init)
  5. Is it a multisite default-site problem? (add --url)
  6. Are you outside the WP root? (add --path)
  7. Is a fatal being swallowed by a try/catch you forgot about? (temporarily var_dump( $e ))

When to reach for the neighbouring commands instead

wp eval is a general tool with specific neighbours that do the job better:

Task Better command
Search/replace across tables wp search-replace --dry-run then without
Cron inspection wp cron event list / wp cron event run <hook>
Option/transient reads wp option get, wp transient get
DB queries with clean output wp db query
Bulk post meta edits your own wp eval-file script, or wp post meta update in a loop
Interactive inspection wp shell (a REPL over the same bootstrap)

Learn the boundaries: reach for eval when no first-class command exists, when you need to compose several APIs in one transaction-ish unit, or when you are deliberately probing WP-CLI's own behaviour. Reach for a real command the moment the snippet acquires if ( $args[0] === '--force' ).

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel