WPify Custom Fields — WordPress plugin providing 59 field types across 15 integration points (metaboxes, options pages, taxonomy terms, users, WooCommerce products/orders/coupons, Gutenberg blocks, etc.). PHP 8.1+ / WordPress 6.2+ / React 18.
Entry point: custom-fields.php → singleton wpify_custom_fields() returns CustomFields instance.
src/ # PHP source (PSR-4: Wpify\CustomFields)
CustomFields.php # Main class — factory methods, sanitization, type mapping
Api.php # REST API endpoints (/wpifycf/v1/)
FieldFactory.php # Fluent PHP API for building field definitions
Helpers.php # URL fetching, post/term queries, file operations
Fields/ # PHP field type handlers (DirectFileField)
Integrations/ # All 15 integration classes
Exceptions/ # MissingArgumentException, CustomFieldsException
assets/ # JS/React source (@ alias in imports)
custom-fields.js # Entry point — bootstraps React apps from DOM containers
components/ # Shared: App, Field, AppContext, MultiField, GutenbergBlock, Tabs, Label, etc.
fields/ # 60 field type React components
helpers/ # hooks.js, functions.js, validators.js, field-types.js, generators.js
styles/ # SCSS with CSS custom properties and container queries
docs/ # Markdown documentation (field-types/, integrations/, features/)
tests/
php/ # PHP integration tests (wp-phpunit) — suites in Core/ and WooCommerce/
js/ # Jest unit tests — helpers, shared components, field mount-sweep
e2e/ # Playwright smoke specs (fixed 8-spec set)
fixtures/wcf-demo/ # Demo plugin: all field types + surfaces; shared E2E/dev fixture
build/ # Webpack output (generated)
- Dev server:
npm run start - Production build:
npm run build - Bundle analysis:
npm run build:analyze - PHP code standards:
composer run phpcs - PHP auto-fix:
composer run phpcbf - PHP integration tests:
composer test(core),composer test:wc(WooCommerce),composer test:all— run viaddev execlocally; seetests/php/README.md - JS unit tests:
npm run test:unit - E2E smoke tests:
npm run build && npm run wp-env start && npm run test:e2e— seetests/e2e/README.md
BaseIntegration (abstract)
│ normalize_items(), enqueue(), register_rest_options()
│
├── OptionsIntegration (abstract)
│ │ print_app(), prepare_items_for_js(), get/set_field(), set_fields_from_post_request()
│ │
│ ├── Options — Admin menu pages (get_option / update_option)
│ ├── SiteOptions — Multisite network options
│ ├── WooCommerceSettings — WC settings tabs
│ │
│ └── ItemsIntegration (abstract)
│ │ Adds get_item_id() for item-bound storage
│ │
│ ├── Metabox — Post meta boxes
│ ├── Taxonomy — Term meta fields
│ ├── User — User profile fields
│ ├── Comment — Comment meta
│ ├── MenuItem — Nav menu item meta
│ ├── ProductOptions — WC product data tabs
│ ├── ProductVariationOptions — WC product variations
│ ├── CouponOptions — WC coupon fields
│ ├── OrderMetabox — WC orders (HPOS compatible)
│ ├── SubscriptionMetabox — WC Subscriptions
│ └── WcMembershipPlanOptions — WC Memberships
│
└── GutenbergBlock — Block attributes, server-side rendering, InnerBlocks
- Normalize —
normalize_items()adds IDs, global_ids, resolves type aliases, registers async option endpoints - Prepare —
prepare_items_for_js()builds input names, fetches current values from DB - Render —
print_app()outputs a.wpifycf-app-instancecontainer with all field data indata-fieldsJSON attribute (plusdata-preresolved-options: a per-options_keyvalue→label map so async selects show their selected label without a value-bound request) - Bootstrap — JS entry reads
data-fields, creates React root withAppContextProvider - Render tree —
App→RootFields→Field(dispatcher) → specific field component - Submit — Hidden
<input>elements carry values; PHPset_fields_from_post_request()sanitizes and saves - Gutenberg — Uses controlled state (
attributes/setAttributes) instead of form submission
Field.js— Central dispatcher: resolves type to component viagetFieldComponentByType(), evaluates conditions (useConditions), runs validation (checkValidity), handles generatorsAppContext.js— Global state provider: values, fields, tabs, config. Supports both controlled (Gutenberg) and uncontrolled (form) modesMultiField.js— Generic repeater: add/remove/reorder (Sortable.js), min/max constraints. Allmulti_*types are thin wrappers around thisGutenbergBlock.js— View/Edit mode toggle, server-side block rendering, InnerBlocks via HTML comment replacementfunctions.js—evaluateConditions(),getValueByPath()(dot notation + relative#/##paths),interpolateFieldValues()validators.js—checkValidityStringType,checkValidityNumberType,checkValidityGroupType,checkValidityMultiFieldType()factoryhooks.js—useConditions,useMulti,usePosts,useTerms,useOptions,useMediaLibrary,useValidity,useSortableList
- PHP: WordPress Coding Standards (WPCS) — see
phpcs.xmlfor project customizations - JS: WordPress scripts standards via
@wordpress/scripts - CSS: SCSS, BEM-style with
wpifycf-prefix, CSS custom properties (--wpifycf-*), container queries on.wpifycf-app-instanceand.wpifycf-field__control - Naming: PHP namespace
Wpify\CustomFields(PSR-4), React components PascalCase, JS helpers camelCase - Prefix: PHP globals
wpifycf, text domainwpify-custom-fields - JS imports:
@alias =assets/directory - Docs: PHPDoc in code, markdown in
docs/. Follow WordPress Coding Standards in PHP examples (tabs, spaces in parentheses/functions). Always escape output:esc_html(),esc_attr(),esc_url(),wp_kses()
- Render:
normalize_items()→prepare_items_for_js()→print_app(context, tabs, attrs, items) - Save:
normalize_items()→set_fields_from_post_request(items) - Register meta:
normalize_items()→flatten_items()→register_{type}_meta()per field
switch → toggle, multiswitch → multi_toggle, multiselect → multi_select, colorpicker → color, gallery → multi_attachment, repeater → multi_group
wpifycf_sanitize_{type}— custom sanitizationwpifycf_wp_type_{type}— WordPress data type mapping (integer, number, boolean, object, array, string)wpifycf_default_value_{type}— default valueswpifycf_items— filter normalized items
wpifycf_field_{type}— register custom field componentwpifycf_definition— filter field definitions before renderwpifycf_generator_{name}— auto-generate field values (e.g., UUID)
- PHP: Register
wpifycf_sanitize_{type},wpifycf_wp_type_{type},wpifycf_default_value_{type}filters - JS: Create component in
assets/fields/, add staticcheckValidity(value, field)method, register withaddFilter('wpifycf_field_{type}', ...) - Multi-version: Wrap with
MultiField, usecheckValidityMultiFieldType(type)helper - Full guide:
docs/features/extending.md
Conditions array with operators (==, !=, >, >=, <, <=, between, contains, not_contains, in, not_in, empty, not_empty), and/or combinators, nested groups, relative path refs (# parent, ## grandparent). Hidden fields still submit values with data-hide-field="true". Full docs: docs/features/conditions.md
Field components export static checkValidity(value, field) → array of error strings. Form submission blocked if errors. Validators in assets/helpers/validators.js. Full docs: docs/features/validation.md
Fields fetch remote data only once visible: viewport intersection (200px preload margin, 200ms dwell) + visible browser tab; focus loads immediately; once loaded, latched forever. Central gate: sentinel in Field.js + LoadableContext (assets/helpers/visibility.js) consumed by the data hooks in helpers/hooks.js. Gutenberg render-block is continuously gated instead (no renders off-canvas). Full docs: docs/features/lazy-loading.md, rationale: docs/adr/0001-viewport-gated-data-loading.md
Two-axis strategy (see docs/adr/0002-two-axis-test-strategy.md and the Testing glossary in CONTEXT.md): the field-type axis is tested once through the canonical Options integration; the integration axis with a small representative field set per surface; cross-axis couplings (Gutenberg controlled mode, multi_*/group nesting, WC variation input names) are named exceptions with dedicated tests.
- PHP integration (
tests/php/) — wp-phpunit against real WordPress; two suites:core(multisite, no WooCommerce, includes graceful-degradation checks) andwoocommerce(single-site, WC loaded) - JS unit (
tests/js/) — Jest via wp-scripts: helper logic,Field/MultiField/AppContext, per-typecheckValidity, and a mount-sweep rendering every field type in jsdom - E2E (
tests/e2e/) — Playwright + wp-env, a fixed 8-spec smoke set; growth requires removing or justifying a spec, not appending
CI (.github/workflows/tests.yml) runs phpcs, both PHP suites, JS unit, build, and E2E on every push/PR — all blocking.
When writing or updating docs in docs/:
- Follow existing templates — consistent structure for field types, integrations, and features (see any existing file as reference)
- PHP examples: WordPress Coding Standards with tabs, spaces in parentheses/functions
- Always escape output in examples (
esc_html(),esc_attr(),esc_url(),wp_kses()) - Parameter format:
name(type) — description - File organization:
docs/field-types/,docs/integrations/,docs/features/
When your changes invalidate or create gaps in this file, update it as part of the same task. Typical triggers:
- Build commands or scripts change
- New integrations, field types, or major features are added
- Class hierarchy or data flow changes
- File/directory structure changes
Keep updates minimal, match existing style, do not add session-specific or speculative content.
Issues are tracked in GitHub Issues (wpify/custom-fields) via the gh CLI. See docs/agents/issue-tracker.md.
Default label vocabulary — needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See docs/agents/triage-labels.md.
Single-context layout — CONTEXT.md and docs/adr/ at the repo root, created lazily. See docs/agents/domain.md.