Install
$ agentstack add skill-mralaminahamed-wp-dev-skills-wp-phpstan-stubs ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
PHPStan Stubs Scaffold
> Model note: File scaffolding from a fixed template — haiku handles end-to-end. Only reach for sonnet if the source plugin has complex namespace hierarchies needing stub organization decisions.
Scaffold a complete PHPStan stubs package from scratch, following the my-org/phpstan-freemius-stubs standard structure.
When to use
- "Create stubs for X", "new stubs package", "scaffold phpstan stubs", "add stubs for plugin/composer package".
Not for: Configuring phpstan.neon or generating baselines — use the official wp-phpstan skill. Adding PHPStan to a plugin that already has stubs installed.
References
references/wp-org-api.md— WP.org plugin API, version listing, download URLs, cleanup patternsreferences/packagist-api.md— Packagist API, version filtering, source/composer.json update patternreferences/common-errors.md— Known errors and fixes (jq\d, unzip prompt, find+set-e, git reset, missing source/composer.json)references/github-setup.md— Repo creation, branch rename trunk→main, topics, secrets, all 11 current repos
Gather Required Info
Before writing any files, collect (ask user if missing):
- Plugin/package name — human-readable (e.g. "SureCart", "Action Scheduler")
- Source type — one of:
wp-plugin— downloadable from WordPress.org (slug known)composer— Composer package on Packagist (e.g.woocommerce/action-scheduler)paid— paid plugin, user places source manually
- WP.org slug (if
wp-plugin) — e.g.surecart,forminator - Packagist package (if
composer) — e.g.woocommerce/action-scheduler - Source dir path inside package — where the plugin/package files land:
wp-plugin:source//composer:source/vendor///paid:source//
- Packagist name —
my-org/phpstan--stubs - GitHub repo name —
phpstan--stubs - Versions to release — for
wp-plugin/composer: minor version series (e.g.3.4 3.5 3.6); forpaid: manual - GitHub assignee — default
my-org
Standard Directory Layout
phpstan--stubs/
├── bin/
│ ├── generate.sh
│ └── release-latest-versions.sh # (not for paid)
├── configs/
│ ├── bootstrap.php
│ └── finder.php
├── source/
│ ├── composer.json # only if wp-plugin or paid (no composer deps)
│ └── .gitignore
├── .github/
│ └── workflows/
│ └── release.yml # (not for paid)
├── .editorconfig
├── .gitattributes
├── .gitignore
├── composer.json
├── phpstan.neon
└── -stubs.php # empty placeholder (generated)
-constants-stubs.php # empty placeholder (generated)
File Contents
composer.json
{
"name": "my-org/phpstan--stubs",
"description": " function and class declaration stubs for static analysis.",
"type": "library",
"keywords": [
"",
"wordpress",
"static analysis",
"phpstan",
"stubs"
],
"homepage": "https://github.com/my-org/phpstan--stubs",
"license": "MIT",
"authors": [
{
"name": "Al Amin Ahamed",
"homepage": "https://github.com/my-org"
}
],
"require": {
"php": ">=7.4",
"php-stubs/wordpress-stubs": "^5.3 || ^6.0"
},
"require-dev": {
"php-stubs/generator": "^0.8.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"squizlabs/php_codesniffer": "^3.7"
},
"minimum-stability": "stable",
"prefer-stable": true,
"config": {
"allow-plugins": {
"php-stubs/generator": true
},
"sort-packages": true,
"optimize-autoloader": true,
"preferred-install": "dist",
"platform": {
"php": "7.4.0"
}
},
"scripts": {
"post-install-cmd": [
"@composer --working-dir=source/ update --no-interaction"
],
"post-update-cmd": [
"@composer --working-dir=source/ update --no-interaction"
],
"generate": "bash bin/generate.sh",
"release": "bash bin/release-latest-versions.sh"
},
"support": {
"issues": "https://github.com/my-org/phpstan--stubs/issues",
"source": "https://github.com/my-org/phpstan--stubs"
}
}
For composer source type — source/ has its own composer.json, so omit post-install-cmd/post-update-cmd from root composer.json and add them pointing to the source subdir instead. The source/composer.json requires the actual package:
{
"require": {
"php": ">=5.6",
"/": ""
},
"minimum-stability": "stable"
}
For wp-plugin and paid — create source/composer.json:
{"minimum-stability": "stable"}
And source/.gitignore:
/vendor/
/composer.lock
configs/bootstrap.php
Standard WordPress constants file — copy verbatim from freemius, then append plugin-specific constants at bottom if needed:
in(array(
'',
))
->sortByName(true)
;
Replace `` with the actual path (relative to project root):
wp-plugin:source/composer:source/vendor//paid:source/
Add ->notPath(...) calls to exclude test dirs, docs, or large asset dirs that bloat the stubs.
bin/generate.sh
#!/usr/bin/env bash
#
# Generate stubs from the source directory.
#
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
HEADER=$'/**\n * Generated stub declarations for .\n * @see \n * @see https://github.com/my-org/phpstan--stubs\n */'
FILE="$ROOT_DIR/-stubs.php"
FILE_CONSTANTS="$ROOT_DIR/-constants-stubs.php"
GENERATOR_BIN="$ROOT_DIR/vendor/bin/generate-stubs"
FINDER_FILE="$ROOT_DIR/configs/finder.php"
set -e
test -f "$FILE" || touch "$FILE"
test -f "$FILE_CONSTANTS" || touch "$FILE_CONSTANTS"
test -d "$ROOT_DIR/"
"$GENERATOR_BIN" \
--include-inaccessible-class-nodes \
--force \
--finder="$FINDER_FILE" \
--header="$HEADER" \
--functions \
--classes \
--interfaces \
--traits \
--out="$FILE"
"$GENERATOR_BIN" \
--include-inaccessible-class-nodes \
--force \
--finder="$FINDER_FILE" \
--header="$HEADER" \
--constants \
--out="$FILE_CONSTANTS"
bin/release-latest-versions.sh — WP.org plugin
Iterates minor version series (e.g. 2.0 2.1 ... 2.20), finds latest patch, skips if tag exists, downloads, unzips, generates, commits, tags:
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
set -e
git -C "$ROOT_DIR" fetch --all
git -C "$ROOT_DIR" reset --hard origin/main
WP_JSON="$(wget -q -O- "https://api.wordpress.org/plugins/info/1.0/.json")"
for V in ; do
printf -v JQ_FILTER '."versions" | keys[] | select(test("^%s\\.%s\\.[0-9]+$"))' "${V%.*}" "${V#*.}"
LATEST="$(jq -r "$JQ_FILTER" /dev/null 2>&1; then
echo "Tag exists for v${LATEST}, skipping..."
continue
fi
rm -rf "$ROOT_DIR/source/" 2>/dev/null || true
rm -f "$ROOT_DIR/source/."*.zip 2>/dev/null || true
wget -q -P "$ROOT_DIR/source/" "https://downloads.wordpress.org/plugin/.${LATEST}.zip"
unzip -q -o -d "$ROOT_DIR/source/" "$ROOT_DIR/source/.${LATEST}.zip"
rm -f "$ROOT_DIR/source/.${LATEST}.zip"
echo "Generating stubs for ${LATEST}..."
"$SCRIPT_DIR/generate.sh"
if git -C "$ROOT_DIR" diff-index --quiet HEAD --; then
echo "No changes for ${LATEST}, skipping commit..."
else
git -C "$ROOT_DIR" commit --all -m "Generate stubs for ${LATEST}"
git -C "$ROOT_DIR" tag "v${LATEST}"
fi
done
git -C "$ROOT_DIR" push origin main --follow-tags
echo "Done."
Minor version list format: space-separated MAJOR.MINOR values, e.g. 1.0 1.1 1.2 ... 1.20 2.0 2.1. The jq filter selects all patch versions matching ^MAJOR.MINOR.[0-9]+$ then takes the latest.
CRITICAL jq regex note: In jq string literals, use [0-9] not \d. The printf -v JQ_FILTER approach above uses \\.[0-9]+ which renders correctly.
bin/release-latest-versions.sh — Composer package
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
set -e
git -C "$ROOT_DIR" fetch --all
git -C "$ROOT_DIR" reset --hard origin/main
PACKAGIST_JSON="$(wget -q -O- "https://packagist.org/packages//.json")"
VERSIONS=()
for V in "${VERSIONS[@]}"; do
printf -v JQ_FILTER '."package"."versions" | keys[] | select(test("^%s\\.%s\\.[0-9]+$"))' "${V%.*}" "${V#*.}"
LATEST="$(jq -r "$JQ_FILTER" /dev/null 2>&1; then
echo "Tag exists for v${LATEST}, skipping..."
continue
fi
printf -v SED_EXP 's#\("/"\): "[^"]*"#\1: "%s"#' "${LATEST}"
sed -i -e "$SED_EXP" "$ROOT_DIR/source/composer.json"
composer --working-dir="$ROOT_DIR/source" update --no-interaction
"$SCRIPT_DIR/generate.sh"
if git -C "$ROOT_DIR" diff-index --quiet HEAD --; then
echo "No changes for ${LATEST}, skipping commit..."
else
git -C "$ROOT_DIR" commit --all -m "Generate stubs for ${LATEST}"
git -C "$ROOT_DIR" tag "v${LATEST}"
fi
done
git -C "$ROOT_DIR" push origin main --follow-tags
echo "Done."
bin/release-latest-versions.sh — Paid plugin
Accept VERSION as first argument, source must be pre-placed at source//:
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
set -e
VERSION="${1:?Usage: $0 }"
if git -C "$ROOT_DIR" rev-parse "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "Tag v${VERSION} already exists. Skipping."
exit 0
fi
if [ ! -d "$ROOT_DIR/source/" ]; then
echo "ERROR: Place ${VERSION} source at source// first."
exit 1
fi
"$SCRIPT_DIR/generate.sh"
git -C "$ROOT_DIR" commit --all -m "Generate stubs for ${VERSION}"
git -C "$ROOT_DIR" tag "v${VERSION}"
git -C "$ROOT_DIR" push origin main --follow-tags
echo "Done."
.github/workflows/release.yml
name: Release new version
on:
push:
paths:
- ".github/workflows/release.yml"
schedule:
- cron: '0 * * * *'
jobs:
release-new-stubs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
extensions: json zip xdebug
coverage: none
tools: composer
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "${{ runner.os }}-${{ hashFiles('**/composer.lock') }}"
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-suggest --no-progress --no-interaction --ignore-platform-reqs
- name: Set up Git user
run: |
git config user.name 'github-actions'
git config user.email 'github-actions@github.com'
- name: Release new stubs from latest version
run: bash bin/release-latest-versions.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PERSONAL_TOKEN }}
commit-message: Generate stubs from latest version
committer: github-actions[bot]
author: ${{ github.actor }}
signoff: false
delete-branch: true
title: 'Generate stubs from latest version'
body: |
This PR updates stubs from the latest available version.
labels: |
update
automated pr
assignees: my-org
draft: false
Omit this workflow entirely for paid source type.
phpstan.neon
parameters:
paths:
- -stubs.php
scanFiles:
- -constants-stubs.php
bootstrapFiles:
- configs/bootstrap.php
level: 5
ignoreErrors:
- '#but return statement is missing\.$#'
- '#has an unused parameter#'
- '#^(Property|Static property|Method|Static method) \S+ is unused\.$#'
- '#is never read, only written\.$#'
- '#has invalid (return )?type (WP_Error|WP_Customize_Manager|WP_Theme|WP_User|WP_Site|WP_Upgrader)#'
.gitignore
/vendor/
/composer.lock
/report.txt
.gitattributes
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/source export-ignore
.editorconfig
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.yml]
indent_size = 2
Execution Steps
After writing all files:
- Init git and make first commit:
``bash cd /path/to/phpstan--stubs git init git branch -m trunk main git add . git commit -m "chore: initial scaffold for stubs" ``
- Create GitHub repo (public):
``bash gh repo create my-org/phpstan--stubs \ --public \ --description " function and class declaration stubs for static analysis." \ --source=. \ --remote=origin \ --push ` Then ensure default branch is main: `bash gh repo edit my-org/phpstan--stubs --default-branch main ``
- Add GitHub topics:
``bash gh repo edit my-org/phpstan--stubs \ --add-topic phpstan \ --add-topic php \ --add-topic stubs \ --add-topic wordpress \ --add-topic ``
- Install local dependencies:
``bash composer install --ignore-platform-reqs ``
- Run first release (if not
paid):
``bash bash bin/release-latest-versions.sh ` For paid: instruct user to place source at source// then run bash bin/release-latest-versions.sh `.
Common Mistakes to Avoid
| Mistake | Correct | |---------|---------| | \d in jq string literal | Use [0-9] | | git reset --hard origin/main before committing fix | Always push fixes before re-running release | | unzip without -o flag | Always use unzip -q -o to avoid interactive prompts | | find -exec rm -rf {} + with set -e | Use rm -rf source/ 2>/dev/null || true | | gh repo create defaults to trunk branch | Always rename: git branch -m trunk main then gh repo edit --default-branch main | | Packagist API: repo.packagist.org/p2/ | Correct URL: packagist.org/packages//.json | | Packagist JSON path: packages.x[] | Correct path: ."package"."versions" | | Missing source/composer.json for wp-plugin | Creates post-install-cmd failure; always create source/composer.json |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mralaminahamed
- Source: mralaminahamed/wp-dev-skills
- License: MIT
- Homepage: https://github.com/mralaminahamed/wp-dev-skills/blob/trunk/README.md
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.