wp-cli/entity-command
WP-CLI commands for managing WordPress comments, menus, options, posts, sites, terms, and users. Create, update, delete, list, and moderate entities from the command line, with support for metadata, custom fields, and block notes.
Version check first — v3.0.3 is a patch release on the 3.x line, with no breaking changes. On a phar-based install, entity-command ships bundled with WP-CLI, so you upgrade the tool rather than the package:
wp cli update # phar installs
composer update wp-cli/entity-command # only if you depend on it directly
wp cli info # confirm WP-CLI + PHP versions
Note this release was validated against WordPress 7.1 stubs and re-enabled multisite wp site create tests on SQLite, so mixed/older WordPress installs still work but some commands now guard themselves by version.
First use case — list comments for a post as JSON, then pipe into Laravel:
wp comment list --post_id=123 --number=50 --fields=comment_ID,comment_author,comment_content --format=json
Where to look first:
README.md — regenerated three times in this release (#640, #641, #649, #658); it is the canonical, up-to-date command reference. Check it before the source, especially for the newly documented wp post list / wp site list arguments.wp help <command> — e.g. wp help post list now enumerates every accepted WP_Query argument.features/ — Behat tests, now with fixed PHPCS violations (#652); good for expected CLI output shapes.tests/ — PHPUnit coverage of the entity CRUD layer.CRUD on any entity — the same shape works for comments, posts, terms, users, sites:
wp comment create --comment_post_ID=123 --comment_content="Test" [email protected]
wp comment update 456 --comment_content="Revised"
wp comment delete 456 --force
user create and post update now apply parameters they previously discarded silently (#637) — see Gotchas.
Querying with real WP_Query arguments — wp post list now documents every WP_Query var it accepts (#643, #645), and wp site list runs through WP_Site_Query (#639) with its arguments documented (#642). That means you can express the same filters in CLI that you would in PHP:
wp post list --post_type=post --post_status=publish \
--posts_per_page=20 --orderby=date --order=DESC \
--date_query='[{"after":"2024-01-01"}]' --format=json
wp site list --network_id=1 --site__in=2,3,4 --fields=blog_id,url,last_updated
Do not reach for --fields=filter (removed from docs in #644) — filtering is expressed via the query args themselves.
Meta management, including nested JSON:
wp comment meta add 789 review_data '{"status":"pending","notes":["ok"]}' --format=json
wp comment meta patch update 789 review_data "notes.0" "Rejected by moderation"
wp comment meta pluck 789 review_data "status"
patch/pluck take exact key paths — notes.0, not notes[0].
Terms and hierarchy — Term_Command now enforces a non-negative parent ID (#662), so guard your input before it hits the CLI:
wp term update category 12 --parent=0 # valid
# --parent=-1 is no longer silently accepted
User preferences and application passwords — user update editor flags are normalized to core string literals (#657), and application-password list filters now declare dash-separated aliases (#653):
wp user update 5 --rich_editing=false --syntax_highlighting=true
wp user application-password list 5 --user-id=5 --format=json
wp user application-password list 5 --user_id=5 --format=json # same filter, alias form
Block editor icons (new in 3.0.3) — wp icon and wp icon collection (#648) let you inspect the registered icon registry from the command line. Useful when a theme/plugin registers icons and you need to confirm what's actually available:
wp help icon
wp icon collection list --format=json
These live in the same command surface as wp block, which is now version-guarded (see Gotchas).
Notes (WordPress 6.9+) — block notes are comments with comment_type=note:
wp comment create --comment_post_ID=123 --comment_type=note \
--comment_content="Needs review" --comment_parent=456
Use Symfony Process, not Artisan::call. Artisan::call() dispatches Laravel commands; it cannot run wp:
use Symfony\Component\Process\Process;
$process = new Process(['wp', 'comment', 'list', '--post_id=123', '--format=json', '--number=50']);
$process->mustRun();
$comments = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR);
Passing argv as an array avoids shell-quoting bugs for JSON meta values entirely.
Bind a thin service into the container so the wp binary path and --path/--url/--user flags live in config:
$this->app->singleton(WpCli::class, fn () => new WpCli(
binary: config('wordpress.wp_binary', 'wp'),
path: config('wordpress.abspath'),
));
Scheduling — run against the WordPress path explicitly, since the scheduler has no CWD context:
$schedule->exec('wp comment delete --status=spam --force --path=/var/www/wp')
->dailyAt('03:00')->withoutOverlapping();
Sync into Eloquent — chunk with --number/--offset rather than pulling everything:
foreach ($wp->listInChunks('comment', 200) as $batch) {
Comment::upsert($batch, ['id'], ['content', 'status']);
}
Cast comment_ID explicitly; WP-CLI emits it as a string in JSON.
Silent parameter drops are fixed — audit existing scripts. #637 makes user create and post update apply arguments they previously discarded. Scripts that "worked" while ignoring, say, a role or status argument may now behave differently.
--filter is no longer a documented field (#644). If anything derived a field list from the README, regenerate it. Use query args for filtering and --fields= only for real columns.
Block subcommands are version-guarded (#647). On WordPress versions without the block APIs, they no longer half-run — expect an explicit "not supported" path plus quieter wp-compat false positives. Probe with wp help block at bootstrap rather than assuming availability.
wp icon requires a supporting WordPress version. Treat it like wp block: feature-detect, don't hard-depend.
Term parents must be ≥ 0 (#662). --parent=-1 used to slip through; sanitize to 0 for "no parent".
Permissions and --user — destructive operations need capability context:
wp comment delete 123 --force --user=admin
Meta writes can fail without unfiltered_html.
Multisite — site-level commands need --url or --network:
wp comment list --network --status=spam
wp site list --network_id=2 --fields=blog_id,url
Performance — never run wp comment list unbounded on a large site; always pass --number (and --offset when chunking). Batch meta writes through xargs or, better, a single batched process call.
Trash and replies are easy to miss — trashed comments are excluded unless --status=trash; replies need --comment_parent=<id>.
wp comment list --format=ids, not --porcelain (that's a create-time flag):
wp comment list --status=spam --format=ids | xargs -r -n 50 wp comment delete --force
WP_CLI_DEBUG=1 wp comment create --comment_post_ID=123 --comment_content="Test"
wp --debug comment list --post_id=123
wp comment get 456 --field=comment_ID 2>/dev/null || echo "not found"
wp help icon >/dev/null 2>&1 || echo "wp icon unavailable"
--path, --url, --user, and JSON decoding turns every call site into a typed array.Process::fake() is what you want; $this->artisan('wp ...') does not exist:
Process::fake([['wp', 'comment', 'list', '*'] => Process::result('[{"comment_ID":"1"}]')]);
before_*/after_* hooks inside the WordPress process; if you need Laravel-side reactions, have the WP hook fire a queued job or webhook rather than trying to reach back into the app.wp-cli.yml for per-project defaults so Laravel and shell invocations agree:
path: /var/www/wp
url: https://example.test
@staging, @prod are the cleanest way to target environments from Laravel config.--user-id / --user_id), and #653 added the dash-separated aliases to user application-password list specifically. Prefer one form project-wide; mixing makes grep-based audits unreliable.--rich_editing=false is a string "false", not a boolean; don't json_encode() a PHP false into the flag.How can I help you explore Laravel packages today?