WooCommerce is a WordPress e-commerce plugin organized as a monorepo with:
- Backend: PHP in
plugins/woocommerce/(PSR-4, WordPress standards) - Admin Client: React/TypeScript in
plugins/woocommerce/client/admin/ - Payment Settings: Specialized React/TypeScript module in
plugins/woocommerce/client/admin/client/settings-payments/
Use this doc when:
- Getting oriented in the project
- Understanding overall architecture
- Finding the right documentation or skill
The .ai/skills/ directory contains procedural HOW-TO instructions:
woocommerce-backend-dev- Backend PHP conventions and unit tests. Invoke before writing any PHP test files.woocommerce-dev-cycle- Testing and linting workflows (PHP, JS, markdown)woocommerce-local-env- Local environment setup, wp-env commands, and WooCommerce build watcherswoocommerce-copy-guidelines- UI text standards (sentence case rules)woocommerce-code-review- Code review standards and critical violations to flagwoocommerce-markdown- Markdown writing and editing guidelineswoocommerce-git-commit- Commit changes with conventional messages and smart groupingwoocommerce-git-draft-pr- Create draft PRs with proper template, changelog, and milestone handlingwoocommerce-email-editor- Email editor development setup and Mailpit configurationwoocommerce-performance- Performance guardrails. Invoke when writing or reviewing PHP code.
plugins/woocommerce/
├── src/ # Modern PHP code (PSR-4, DI container)
│ ├── Internal/ # Internal classes (default location)
│ └── [Public classes] # Public API classes
├── includes/ # Legacy WordPress code
│ └── class-woocommerce.php # Main plugin class
├── tests/php/ # PHPUnit tests
│ ├── includes/ # Tests for legacy code
│ └── src/ # Tests for modern code
└── client/ # Frontend applications
└── admin/ # Admin React app
Modern vs Legacy Code:
plugins/woocommerce/src/- Modern PHP with dependency injection, PSR-4 autoloadingplugins/woocommerce/includes/- Legacy WordPress patterns, modify only when necessary
Namespace:
- Root namespace:
Automattic\WooCommerce - Internal classes:
Automattic\WooCommerce\Internal\*
Dependency Injection:
- Classes in
plugins/woocommerce/src/use DI container ($container->get()) - Dependencies injected via
init()method
Version Management:
- Current version in
plugins/woocommerce/includes/class-woocommerce.php→$versionproperty - Used for
@sinceannotations (remove-devsuffix) - When changing template files (PHP files used to display UI on the front-end) the version in their header should be updated to the current version, without the
-devsuffix.
JavaScript:
- Prefer vanilla JavaScript/TypeScript over jQuery for new or modified code. Keep existing jQuery when a rewrite is out of scope.
- Make code changes
- Run relevant tests (see
woocommerce-dev-cycleskill) - Run linting and fix all errors and warnings before committing (see Pre-commit Checks)
- Run PHPStan for PHP changes (see below)
- Commit only after tests pass and all checks are clean
- Create changelog entries for each affected package
- Create PR only after changelog entries exist
Before committing PHP changes, run both lint commands and fix what they report:
# Lint the changed PHP files
pnpm --filter=@woocommerce/plugin-woocommerce lint:php:changes
# Lint the full branch diff (phpcs-changed — catches warnings the per-file pass can miss across commits)
pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branchFix every phpcs error and warning before committing — the CI Lint job treats warnings as failures, so an unaddressed warning turns the check red. If a finding is intentionally suppressed, add a brief inline justification. Re-run lint:changes:branch after any commit rewrite, since it compares the whole branch against trunk.
Also run PHPStan on modified PHP files (from the plugins/woocommerce directory):
composer exec -- phpstan analyse path/to/modified/File.php --memory-limit=2GPHPStan Baseline Policy: The baseline file (phpstan-baseline.neon) must never be added to. It should only shrink over time as existing errors are naturally resolved by code changes. If PHPStan reports a new error, fix it in the code rather than adding it to the baseline. If your fix resolves a previously baselined error, remove the corresponding entry from the baseline.
NEVER create a PR without changelog entries. Each package modified in the monorepo requires its own changelog entry. Run for each affected package:
pnpm --filter=<project> changelog addExample for WooCommerce Core:
pnpm --filter=@woocommerce/plugin-woocommerce changelog addThis command prompts for the change type and description. Run it once per affected package before creating any PR.
When creating PRs, always use the template from .github/PULL_REQUEST_TEMPLATE.md. Key sections:
- Submission Review Guidelines: Checkboxes confirming adherence to contributing guidelines
- Changes proposed in this Pull Request: Description of changes and link to bug-introducing PR if applicable
- Screenshots or screen recordings: UI changes screenshots (can be removed if not applicable)
- How to test the changes in this Pull Request: Step-by-step testing instructions
- Testing that has already taken place: What testing you've done
- Milestone: Check the box to auto-assign milestone, or manually set to the first available milestone that is not in the past (unless otherwise specified)
- Changelog entry: Note if changelog was created manually or check box to auto-create
For bug fixes, always reference the PR that introduced the bug using: Bug introduced in PR #XXXXX.
- PHP tests run in Docker via
wp-env - WordPress and WooCommerce auto-installed
- Uses PHPUnit 9.6.24 with PHP 8.1
For detailed test commands, see woocommerce-dev-cycle skill.
includes/directory changes should be minimal (legacy code)- All new backend code goes in
plugins/woocommerce/src/Internal/by default - Never create standalone functions (always use class methods)
- Tests require Docker environment
Any change to a public or externally exposed class, interface, function, or method signature is high-risk and must state its backward-compatibility impact in the PR description — regardless of whether the symbol lives in the Internal namespace. The Internal namespace is not a guarantee that a symbol is safe to change: third-party code implements and consumes some of these contracts in practice (for example, the WooCommerce Stripe Gateway implements Internal\ProductFeed\Feed\FeedInterface).
Treat a symbol as externally exposed when it is implemented or consumed outside plugins/woocommerce/ — by extensions, other plugins, or themes — even if it lives under Internal. When in doubt, assume it is exposed and state the BC impact.
Adding a method to an interface that external code can implement must be flagged explicitly. It is a backward-incompatible change: existing implementers fatal on load because they no longer satisfy the contract. Likewise, removing a required method from an interface is breaking for existing implementers (they carry a now-dead method, which static analysis such as PHPStan will flag). Prefer a non-breaking alternative — add the method to the concrete class rather than the interface, introduce a separate new interface, or supply a default implementation via an abstract base class.
Deprecate, don't rename. For existing public symbols (classes, interfaces, methods, constants, hooks), never rename or remove them in place. Mark the old symbol @deprecated, introduce the replacement alongside it, and keep both working through a deprecation window so external consumers have time to migrate.
This rule exists because WooCommerce 10.9.0 was reverted on WP Cloud: PR #64394 added a required
get_entry_count(): intmethod toFeedInterface, fataling older WooCommerce Stripe Gateway versions that implement it. Fixed in PR #65965.
WordPress exposes more contracts than class and function signatures. The following are equally binding: a change to any of them is high-risk and requires the same backward-compatibility impact statement in the PR description.
Hooks and filters are public contracts. Every do_action and apply_filters call is an interface that third-party callbacks depend on. Removing a hook, renaming it, or removing/reordering its arguments breaks every attached callback. Changing when or whether a hook fires can break consumers that depend on its timing. Additive is the safe path: append new arguments at the end, never remove or reorder existing ones. To retire a hook, fire it through do_action_deprecated() / apply_filters_deprecated() for a deprecation window instead of deleting it.
Do not assume global state. Code can run in admin, REST, CLI, cron, webhook, and front-end contexts, and not all of them set the globals a front-end request does ($post, $wp_query, an initialized session or cart). A newly introduced read of a global, or of WC()->… state, in a path reachable outside a standard request is a fatal or a silent misbehavior in the contexts that do not set it. Guard the exact dependency explicitly: use function_exists/class_exists for symbols, isset for variables, did_action for lifecycle state, and verify that WC() and the required component are initialized before dereferencing WC()->….
Do not assume single-site. Multisite changes where data lives: site-scoped vs network-scoped options (get_option vs get_site_option), per-site tables, user roles and capabilities, and upload paths all differ. A change that reads or writes site state must state in its PR whether it behaves correctly under multisite — and if it was not tested there, say so explicitly.
Do not assume install layout. WordPress could be configured to run in a subdirectory, with relocated wp-content, and behind reverse proxies. Never build paths or URLs by concatenation from the domain root; derive them (plugins_url(), plugin_dir_path(), wp_upload_dir(), and mind the home_url() vs site_url() distinction). A path that works on a root install and breaks elsewhere is a compatibility bug, not an edge case.
- Identify the contract you are touching: signature, hook, global/scope expectation, site topology, or install layout.
- Assume unseen consumers. You cannot enumerate third-party code; if the surface is reachable from outside this plugin, someone consumes it.
- Prefer the additive path (new optional method, appended hook argument, new symbol + deprecation) over changing what exists.
- State the impact in the PR description: what changed, who could consume it, and why it is safe or what the deprecation path is.
- If you cannot establish the impact, stop and flag it to the user as needing review.
Never include styling options such as fontSize, borderColor, textColor... as block attributes. They should only be listed under supports.
Do not add default values to block attributes in block.json.
- Default attribute values can be indistinguishable from missing attributes when parsed, especially when the default value is not serialized into saved block markup.
- Defaults can create subtle conflicts with
theme.json, block supports, editor controls, deprecations, and migrations. - During implementation or review, flag any newly inserted
defaultinblock.json.
Most WooCommerce Interactivity API stores are private by design. Exception: the woocommerce/product-filters store is public for Product Filters inner-block extensibility.
For private stores:
- Not intended for third-party extension
- Removing or changing store state/selectors is not a breaking change
- No backwards compatibility is required for store internals
- If another store needs to be extensible in the future, it will be split into private (internal) and public (API) stores
- General stores (namespace
woocommerce) may become public eventually, but currently remain private
Reference: WordPress Interactivity API - Private Stores
# Run specific test class
pnpm test:php:env -- --filter TestClassName
# Lint changed files
pnpm lint:php:changes
# Fix linting issues
pnpm lint:php:fix -- path/to/file.phpFor complete command reference and workflows, see woocommerce-dev-cycle skill.
This is part of the WooCommerce monorepo:
- Multiple packages managed with pnpm workspaces
- Root-level scripts coordinate across packages
- Some dependencies shared across packages
Why two code styles? The plugins/woocommerce/includes/ directory predates modern PHP practices. New code uses PSR-4 and dependency injection in plugins/woocommerce/src/.
Why DI container? Improves testability and maintainability compared to legacy global state patterns.
For code review standards and critical violations to flag, use the woocommerce-code-review skill.
- This doc provides context; skills provide procedures
- When in doubt about HOW to do something, check the skills
- When in doubt about WHAT something is or WHERE it fits, check this doc
- Skills are invoked automatically when relevant to the task