Load before writing or changing ANY plugin that runs on Woodev Framework v2 (shipping, payment or plain WordPress/WooCommerce) — the entry file and loader definition, the plugin class, lifecycle and upgrades, settings, licensing, i18n, testing and the traps that already cost us time. For plugin AUTHORS who consume the framework, not for developing the framework itself (that is `woodev-framework-backend-dev`).
Resources
7Install
npx skillscat add kalbac/woodev-plugin-framework/woodev-plugin-core Install via the SkillsCat registry.
Woodev Plugin Core — building a plugin on Framework v2
This skill is for the author of a plugin that BUNDLES the framework (woodev/ inside the plugin).
It covers what every plugin needs. A shipping plugin loads
../woodev-shipping-plugin/SKILL.md on top of this one.
Where paths point. woodev/… is the framework copy bundled inside your plugin (the same files in
the framework repository). docs-internal/…, tests/…, .ai/… and AGENTS.md are in the framework
REPOSITORY (kalbac/woodev-plugin-framework) — read them there; they are not shipped in your plugin.
Ground rules
- Read the code, not
docs/. The publicdocs/are stale and will be rewritten after the first
real plugin. The source of truth iswoodev/(symbol names are cited below; line numbers rot). - Never invent an API. Every class, method, filter and option named in this skill exists in
woodev/today. If you need something not named here,rgthe framework first; if it is not
there, it does not exist — do not write the call, ask the operator. - Never
requireframework internals by path. The framework resolves its own classes
(Woodev_Framework_Autoloader+woodev/class-map.php); a plugin declares its type by what its
classextends. - Internal APIs may change; installed-site data never does (ADR-005,
docs-internal/AGENT-RULES.md→ Rule 0). Option keys, hook names, shipping/gateway method ids,
instance setting keys, cron hooks, REST namespaces, admin slugs, meta keys, license option names —
preserve byte-for-byte. See lifecycle.md. - Style: WordPress Coding Standards,
snake_casemethods/hooks, short array syntax[]only,
type declarations on every parameter and return, docblocks (@since,@param,@return) on
every public/protected method, pure methodsstatic. New classes are namespaced; do not copy
the legacy globalWoodev_*shape.
The map
| You need to… | Where | Detail |
|---|---|---|
| Register the plugin, bundle the framework, survive multi-version | entry file, Woodev_Loader::register() |
loading.md |
| Write the main plugin class, override subsystems, declare HPOS/blocks | Woodev_Plugin → Woocommerce_Plugin |
plugin-class.md |
| Install/upgrade routines, migrate a v1 plugin's data | Woodev_Lifecycle |
lifecycle.md |
| Add settings (typed API, sections, connection test, wizard) | Settings_Provider, Woodev_Abstract_Settings |
settings.md |
| Licensing, error reporting, REST, logging, notices, i18n | subsystems on the base class | services.md |
| Lay out the plugin, write unit/integration tests | tests/_fixtures/* as worked examples |
layout-and-testing.md |
| Avoid the known traps | docs-internal/gotchas/* |
traps.md |
Which base class
The type is the extends — never a flag, never a capabilities array.
| Plugin | Extend | File |
|---|---|---|
| Pure WordPress | Woodev_Plugin |
woodev/class-plugin.php |
| WooCommerce (no shipping/payment) | \Woodev\Framework\Woocommerce_Plugin |
woodev/class-woocommerce-plugin.php |
| Payment gateway | Woodev_Payment_Gateway_Plugin (already a Woocommerce_Plugin) |
woodev/payment-gateway/class-payment-gateway-plugin.php |
| Shipping | \Woodev\Framework\Shipping\Shipping_Plugin (already a Woocommerce_Plugin) |
woodev/shipping-method/class-shipping-plugin.php |
Woodev_Plugin is platform-neutral by design (no WooCommerce or HPOS methods). Anything that touches
WooCommerce — logging through the WC logger, HPOS declaration, Blocks handler — comes fromWoocommerce_Plugin.
The five-minute path to a loading plugin
- Bundle the framework: copy the framework's
woodev/directory into the plugin root aswoodev/
(the framework's release ZIPwoodev-plugin-framework-<version>.zipis the artifact; see
loading.md). - Entry file
my-plugin.php: define constants,require_once __DIR__ . '/woodev/loader.php', callWoodev_Loader::register( __FILE__, [ …literals only… ] ). Exact shape and every definition
field: loading.md. - A main class
extendsthe right base (table above) with a staticinstance()accessor (recommended: the resolver falls back tonewwithout one); its constructor
callsparent::__construct( $id, $version, [ 'text_domain' => … ] ); it implements the abstractget_file()andget_plugin_name(). See plugin-class.md. - A
Woodev_Lifecyclesubclass for install/upgrade, wired by overridinginit_lifecycle_handler().
See lifecycle.md. - Settings through
get_settings_providers()— onWoodev → Настройки, never on WooCommerce →
Integrations by default. See settings.md. - Tests: unit with Brain Monkey/Mockery, one integration test that boots the REAL entry path.
See layout-and-testing.md.
Settings controls
Textarea height is an optional register_control() argument: use$this->register_control( 'address', \Woodev_Control::TYPE_TEXTAREA, [ 'rows' => 3 ] );
for a three-line address field. rows is a positive integer, applies only to textarea controls,
and travels through Field_Schema to the React settings page and wizard. Omit it to keep the
existing renderer default; nonpositive values also use that default. The separate classic helper
accepts the row count as its third argument: textarea( $id, $classes, 3 ), with 10 rows when omitted.
For the rest of the settings API, read settings.md.
Hard rules (each one cost a session)
- A loader definition holds literals only. No framework class constant (not even
Framework_Plugin_Loader_Definition::PLATFORM_WOOCOMMERCE) — PHP builds the array beforeregister()loads the autoloader, so it is a fatal on the plugin's first line. Write'woocommerce'.
Gotchadocs-internal/gotchas/a-loader-definition-cannot-use-a-framework-class-constant.md. - Always set
framework_versionANDbackwards_compatible. Without the second, a too-old plugin
is never quarantined.framework_versionis the version you bundle;backwards_compatibleis the
oldest framework version your plugin works with. download_idis the license identity. It is a positive integer (the EDD product id on
woodev.ru) and two plugins sharing one is an author error: the second is quarantined and, in admin,
deactivated. Do not overrideWoodev_Plugin::get_download_id()— it reads the loader definition.- HPOS and blocks compatibility are declared in the LOADER DEFINITION (
supported_features), not in
the constructor. The loader definition is authoritative; a conflicting constructor value only raises_doing_it_wrong(). Never touch order data withget_post_meta()— useWoodev_Order_Compatibilityor theWC_Orderobject. - Settings live on
woodev-settings(Woodev → Настройки), reached throughWoodev_Plugin::get_settings_providers(). The WooCommerce → Integrations tab is an exception with its
own seam (Shipping_Plugin::get_integration_handler()), used only where an installed site's data
contract requires it. Never build your own top-level admin settings page. - Three subsystems are enforced: the admin-notice handler, the license and the lifecycle handler. A
subclass that overrides theirinit_*method without building the object gets_doing_it_wrong()
underWP_DEBUGand a default instead — do not returnnull. - A hook registered as
[ $this, … ]from an object every plugin builds fires once per plugin.
Shared (per-site) hooks belong in a static callback. Gotchadocs-internal/gotchas/a-hook-registered-from-a-per-plugin-object-fires-once-per-plugin.md. - Merchant-facing copy: label = short essence (2–3 words); help goes to
tooltip(anddescription
only when it must be unmissable or copyable); never the words «чекаут» or «фреймворк».docs-internal/AGENT-RULES.md→ Rules 10a–10c. - i18n msgid language is decided by who reads the string. Storefront → English msgid, Russian from
the catalogue; admin → Russian msgid is fine; logs/exceptions → plain strings, no__().AGENTS.md→ Conventions → «Translatable strings». Details in services.md.
Before you hand work back
npm run probe:signature(a PHP class repointed at a stricter framework base fatals at DECLARATION,
and a mocked unit suite cannot see it —docs-internal/migration/signature-probe.md).- A real-entry-path test (a plugin entry that goes through
Woodev_Loader::register()), not onlyregister_loader_definition()called by a test — seetests/_fixtures/woodev-entry-path-fixture/andtests/unit/LoaderEntryPathTest.php. - The data-preservation checklist for a migrated plugin is verified (see lifecycle.md).
- Read traps.md once per plugin — it is one line per trap and each links its gotcha.