Skip to content
System nominalUTC --:--:--Articles 085
MODULE 04 Lesson 10 / 18 30 MIN

Building a plugin by prompt

Scaffolding a Plugin the WordPress Way

WP-CLI's plugin scaffolder is a usable modern baseline, but its bundled linter and CI config still target PHP 5.6. Audit the output before an agent touches it.

The theme scaffolder hands you a classic theme and calls it done. The plugin scaffolder is a different story: wp scaffold plugin produces a structure WordPress developers actually use, and unlike wp scaffold _s it does not push you toward an architecture from 2015. It is a reasonable starting point for an agent, and it is the command I would put in a prompt today.

Reasonable is not the same as current, though. The scaffold ships a linter configured for PHP 5.6, a readme that asks for 4.5, and a CI config that builds on PHP 5.6 through 7.4 and MySQL 5.6. None of that will stop your plugin from working. All of it will end up in the repository, and the version floors especially will quietly pass code you should not ship.

What the command actually creates

Run against a WordPress 7.0.7 install with a real database, since like the theme scaffolder it refuses to run without one:

$ wp scaffold plugin acme-library --plugin_name="Acme Library" --plugin_author="Acme"
Success: Created plugin files.
Success: Created test files.

Two successes, and the file list is longer than most people expect:

acme-library/
├── acme-library.php          the plugin header, and nothing else
├── readme.txt                WordPress.org format, full of placeholders
├── composer.json             wp-cli/i18n-command and a makepot script
├── .distignore               what to leave out of a release zip
├── .editorconfig
├── .gitignore
├── .phpcs.xml.dist           WPCS ruleset with project placeholders
├── phpunit.xml.dist
├── tests/
│   ├── bootstrap.php
│   └── test-sample.php
├── bin/
│   └── install-wp-tests.sh
└── .circleci/
    └── config.yml

The plugin file is a header and a comment:

<?php
/**
 * Plugin Name:     Acme Library
 * Plugin URI:      PLUGIN SITE HERE
 * Description:     PLUGIN DESCRIPTION HERE
 * Author:          Acme
 * Author URI:      YOUR SITE HERE
 * Text Domain:     acme-library
 * Domain Path:     /languages
 * Version:         0.1.0
 *
 * @package         Acme_Library
 */

// Your code starts here.

That is the whole file. The scaffold gives you a place to work rather than an implementation, which is the correct division of labour: it means the agent is not inheriting somebody else's architecture along with the boilerplate. It also means the first thing a reader should do is fill in the header honestly, including a Requires PHP and a licence line the template does not have.

The linter is configured for a WordPress that no longer exists

This is the part worth reading closely, because a linter with the wrong floor is worse than no linter: it produces a green result that means less than the reader thinks. The generated .phpcs.xml.dist contains:

<config name="testVersion" value="5.6-"/>
<rule ref="PHPCompatibilityWP"/>
...
<config name="minimum_supported_wp_version" value="4.6"/>
<rule ref="WordPress"/>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
	<properties>
		<property name="prefixes" type="array" value="my-plugin"/>
	</properties>
</rule>
<rule ref="WordPress.WP.I18n">
	<properties>
		<property name="text_domain" type="array" value="my-plugin"/>
	</properties>
</rule>

Three things to fix before the first commit. testVersion is 5.6-, which tells PHPCompatibilityWP that PHP 5.6 is your floor, so deprecated-in-8.x patterns are not flagged. minimum_supported_wp_version is 4.6, so APIs removed in the 5.x and 6.x releases are treated as available. And the prefix and text domain are my-plugin while the plugin is acme-library, which means the PrefixAllGlobals and I18n sniffs will complain about your correct code and stay quiet about a genuinely unprefixed function.

There is a fourth problem, and it is the one that matters most for this course. The linter cannot run at all until you install PHPCS and WPCS, and the scaffold's own composer.json does not install them:

{
  "require-dev": {
    "wp-cli/i18n-command": "^2"
  },
  "scripts": {
    "makepot": "wp i18n make-pot ."
  }
}

That is the whole file. It is there to generate a .pot file, not to lint anything. On this machine composer and phpcs are both absent, so I cannot show you a passing or failing run; the research for this course hit the same wall. What I can tell you is what the file says, and what the fix is when you do have Composer: add wp-coding-standards/wpcs, phpcompatibility/phpcompatibility-wp, dealerdirect/phpcodesniffer-composer-installer and squizlabs/php_codesniffer to require-dev, then run vendor/bin/phpcs. An agent asked to set up linting will usually do exactly that, and in the tested run behind this course it did so unprompted and correctly.

A green linter is not a green plugin

Read the phpcs.xml.dist before you read its output. A ruleset whose floor is PHP 5.6 and WordPress 4.6 will report success on code that uses functions removed three releases ago, and the report is honest: it is answering the question you configured it to ask.

readme.txt, the CI config and the plugin header disagree

Three files in the scaffold each declare a version floor, and no two of them match. The readme says:

Requires at least: 4.5
Tested up to: 7.0.7
Requires PHP: 5.6
Stable tag: 0.1.0

The linter says WordPress 4.6. The plugin header says nothing at all about compatibility, because the scaffold leaves Requires PHP out of it. And the CircleCI config builds against a set of PHP versions that starts two majors below the current one:

jobs:
  - php56-build
  - php70-build
  - php71-build
  - php72-build
  - php73-build
  - php74-build

job-references:
  mysql_image: &mysql_image
    circleci/mysql:5.6

Nothing in the toolchain flags any of this. The build passes, the linter passes, and the plugin works. What you have is a repository that documents a support matrix from 2020 while the machine that produced it is running PHP 8.5, and the reason that is dangerous rather than merely untidy is that Requires PHP in a plugin header is read by WordPress and by the plugin directory. It is a claim about what you tested, and it is false.

Fixing it is a one-prompt job, and it is worth doing as a prompt because it tells you whether the agent reads files or guesses. Something like: "Set Requires at least: 7.0 and Requires PHP: 8.3 in readme.txt and in the plugin header, set the PHPCS testVersion to 8.3- and minimum_supported_wp_version to 7.0, and replace my-plugin with acme-library in both sniffs. Then grep the repository for 5.6 and 4.6 and show me every remaining match." The grep at the end is the read-back: it either finds leftovers or it does not, and the answer is visible without trusting the agent's summary.

The test scaffolding is real, and it does not run by default

Two files in that tree deserve a mention because they are the difference between the plugin scaffold and the theme scaffold. phpunit.xml.dist points at tests/bootstrap.php, and the bootstrap deliberately fails with a useful message when the WordPress test library is missing:

Could not find /tmp/wordpress-tests-lib/includes/functions.php,
have you run bin/install-wp-tests.sh ?

So the scaffold assumes a test suite that it does not install, and bin/install-wp-tests.sh is the script that would: it takes a database name, user, password and WordPress version, downloads the develop repository and the test library, and sets WP_TESTS_DIR. That script needs network access, Subversion and a database it is allowed to create, and it is not something to run casually inside an agent session. I did not run it here.

What matters for this course is the shape of the answer. WordPress plugins have a real unit-test path, an agent can write tests that use it, and the scaffold already wired the entry point. An agent that claims it "wrote and ran PHPUnit tests" against a scaffold with no WP_TESTS_DIR set has either installed the suite or is describing something that did not happen. Asking which one is a fast way to find out.

One last read of the tree is worth doing for the release workflow. The .distignore file is what excludes tests/, bin/, node_modules/, composer.json, phpunit.xml.dist and the CI directory from a wp dist-archive build. If an agent adds a directory that should not ship and does not update that file, the directory ships. It is the kind of thing that never shows up in a code review and always shows up in a plugin zip.

Lab 25 minutes

Scaffold a plugin and bring its version claims into the same century before you add a single feature.

wp scaffold plugin acme-lab --plugin_name="Acme Lab" --plugin_author="You"
cd wp-content/plugins/acme-lab
grep -rn "5\.6\|4\.6\|4\.5\|my-plugin\|php56\|mysql:5\.6" . --include="*" | grep -v node_modules

That grep finds every stale claim in the scaffold, and your job is to make it return nothing. Fix the plugin header, readme.txt, .phpcs.xml.dist and .circleci/config.yml so they agree on PHP 8.3 and WordPress 7.0.

Then write the two sentences you would put in an AGENTS.md for this plugin so that a future agent does not reintroduce the old floors. Keep them short enough that an agent reads them: one about the supported versions, one about which command proves the linter actually ran.

Show the solution and the reasoning

The fixes are mechanical; the reasoning is about which files your grep should be allowed to leave alone.

The header and readme carry the claims a human and the plugin directory read, so they get Requires at least: 7.0 and Requires PHP: 8.3. The PHPCS config carries the claims your tooling reads, so it gets testVersion 8.3- and minimum_supported_wp_version 7.0, plus the real prefix and text domain:

<config name="testVersion" value="8.3-"/>
<config name="minimum_supported_wp_version" value="7.0"/>
<property name="prefixes" type="array" value="acme-lab"/>
<property name="text_domain" type="array" value="acme-lab"/>

The CI config is the one file where deleting is a better answer than editing. Six jobs for PHP 5.6 through 7.4 are not worth maintaining by hand, and an agent asked to update it will usually add 8.3 to the list and leave the rest, which preserves the impression that the plugin supports 5.6. Either replace the job matrix with the two versions you actually test, or remove the file and let your real CI configuration own that job.

What your grep should still match at the end is readme.txt's Tested up to: 7.0.7 line, because that one is correct, and any occurrence of these strings inside prose that is describing them. A grep that returns zero matches is suspicious here: the scaffold's .distignore contains a version-less list and no hits, and if you find yourself deleting bin/install-wp-tests.sh to make the count smaller you have optimised the wrong number.

The two AGENTS.md sentences matter more than they look, and they are the transferable part of this lab. The first states the constraint: "This plugin targets PHP 8.3+ and WordPress 7.0+; do not add compatibility shims for older versions." The second states the verification, and it has to name a command rather than a goal: "Vendor PHPCS and WPCS with Composer and run vendor/bin/phpcs before reporting a change as complete; a green php -l is not a lint." An instruction file that names the command is worth more than one that asks for correctness, because the agent can run the command and cannot run the adjective.

Progress

Mark this lesson done and it stays marked in this browser.