You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document is the canonical engineering guide for modernizing PPOM from its current legacy singleton and procedural structure into a modular WordPress-native plugin architecture.
The goal is to modernize internals without breaking store behavior, extension hooks, database schema, REST v1 contracts, bootstrap-era integrations, or existing extensions during the first refactoring program.
This is a maintainers-only document. It defines the target architecture, migration rules, compatibility constraints, and rollout order that should guide all future refactoring work.
Current State Overview
PPOM works today, but its runtime is organized around a heavy bootstrap and a central coordinator that owns too many responsibilities.
Current characteristics:
woocommerce-product-addon.php is a heavy bootstrap that defines constants, loads Composer, manually includes most runtime files, wires Themeisle SDK metadata, boots freemium behavior, registers plugin action links, loads Elementor integration, loads REST, declares HPOS compatibility, and starts the main runtime.
classes/plugin.class.php acts as the main hook coordinator for rendering, validation, pricing, cart/session behavior, order persistence, public AJAX, admin AJAX, shortcode registration, and misc local hooks.
Core runtime behavior is still concentrated in large procedural files:
Data access is duplicated across multiple layers. SQL currently appears in the metadata resolver, admin CRUD, REST handlers, and legacy runtime classes.
The runtime is only partially modernized. Most code is still non-namespaced and not PSR-4 autoloaded, with a small exception in the attach-popup classes.
The plugin already has multiple compatibility-sensitive runtime modes:
legacy vs modern rendering
legacy vs modern pricing
public shopper-facing AJAX vs privileged admin/AJAX flows
Product configuration resolution is more than a direct _product_meta_id lookup. It also depends on category-linked groups, stored tag-aware rows, and filter-driven merge and override rules in classes/ppom.class.php.
The REST API in inc/rest.class.php currently exposes public routes and performs write authentication inside callbacks with a shared secret_key.
Primary pain points:
The bootstrap is too coupled to implementation details.
Hook registration, business logic, storage, validation, and rendering are mixed together.
Procedural runtime files are large and hard to test in isolation.
SQL and data-shaping logic are repeated instead of being mediated by repositories or data stores.
Admin, AJAX, REST, and shopper-facing request handling do not yet follow a consistent controller-and-service structure.
Compatibility behavior is implicit rather than isolated behind formal shims or strategies.
SQL Distribution
flowchart TD
subgraph "Current: SQL scattered across layers"
A["classes/ppom.class.php\n7 SELECT queries"]
B["inc/admin.php\nINSERT · UPDATE · DELETE"]
C["inc/rest.class.php\nINSERT · UPDATE · DELETE"]
D["classes/admin.class.php\nSELECT queries"]
end
subgraph "Target: SQL centralized in Data layer"
E["Data\\FieldGroupRepository\nAll CRUD operations"]
F["Data\\ProductConfigResolver\nAll read queries"]
end
A -.->|moves to| F
B -.->|moves to| E
C -.->|moves to| E
D -.->|moves to| F
Loading
Current Architecture
flowchart TD
A["WordPress loads PPOM"] --> B["woocommerce-product-addon.php<br/>heavy bootstrap"]
B --> C["Manual require_once of inc/, classes/, backend/"]
B --> D["SDK / freemium / Elementor / action links / HPOS wiring"]
C --> E["NM_PersonalizedProduct singleton"]
C --> F["NM_PersonalizedProduct_Admin"]
C --> G["PPOM_Rest"]
E --> H["inc/woocommerce.php<br/>product, cart, order flow"]
E --> I["inc/prices.php<br/>pricing and fees"]
E --> J["inc/files.php<br/>uploads and cleanup"]
E --> K["Public AJAX<br/>upload/delete/validate"]
F --> L["inc/admin.php<br/>admin CRUD and product attachment"]
G --> M["inc/rest.class.php<br/>public routes + callback auth"]
E --> N["PPOM_Meta resolver"]
F --> N
G --> N
N --> O["{prefix}_nm_personalized"]
N --> P["_product_meta_id"]
N --> Q["category/tag-linked groups + filters"]
Loading
Chosen Target Architecture
PPOM should move to a WordPress-like modular architecture.
This does not mean introducing a framework rewrite, nor cloning WooCommerce internals. It means using a modern plugin shape that is native to WordPress and WooCommerce: thin bootstrap, namespaced autoloaded runtime, feature modules, controllers, services, repositories/data stores, and explicit compatibility shims.
Design rules:
WordPress remains the integration shell and hook system.
Business logic moves into services and use-case classes.
Storage access moves behind repositories and data stores.
Admin, AJAX, REST, and public shopper request handling move into explicit controllers or request handlers.
Legacy globals, classes, hooks, and functions remain available as compatibility wrappers during migration.
New runtime code goes into a namespaced, autoloaded src/ tree.
Refactoring is compatibility-first. Internal cleanup is allowed; silent public contract drift is not.
Target Architecture
flowchart TD
A["WordPress + WooCommerce hooks"] --> B["woocommerce-product-addon.php<br/>thin bootstrap"]
B --> C["PPOM\\Plugin"]
C --> CORE["src/Core<br/>registry · contracts · lifecycle"]
CORE --> ADMIN["src/Admin<br/>pages · settings · AJAX"]
CORE --> REST_M["src/Rest<br/>routes · controllers"]
CORE --> FRONT["src/Frontend<br/>rendering · templates · inputs"]
CORE --> CART["src/Cart<br/>validation · payload · lifecycle"]
CORE --> PRICING["src/Pricing<br/>strategies · fees · matrix"]
CORE --> FILES["src/Files<br/>uploads · cleanup · AJAX"]
CORE --> COMPAT["src/Compatibility<br/>legacy wrappers · shims"]
DATA["src/Data<br/>repositories · resolvers"]
ADMIN --> DATA
REST_M --> DATA
FRONT --> DATA
CART --> DATA
CART --> PRICING
PRICING --> DATA
FILES --> DATA
COMPAT -.->|delegates to| ADMIN
COMPAT -.->|delegates to| FRONT
COMPAT -.->|delegates to| DATA
Loading
Target Runtime Layout
PPOM should keep the legacy tree during the transition, but new runtime work should be organized under src/ and loaded via Composer PSR-4.
Directory strategy:
Keep inc/, classes/, and backend/ during migration.
Add src/ for all new runtime code.
Add Composer PSR-4 autoloading for the PPOM\\ namespace.
Treat legacy files as compatibility surfaces unless a targeted legacy bug fix is required.
The bootstrap must stop owning detailed runtime wiring, but bootstrap-era integrations are still part of the product surface and must be preserved or relocated explicitly:
Themeisle SDK registration and compatibility metadata
freemium bootstrapping
Elementor integration bootstrapping
REST bootstrapping
plugin action links
HPOS declaration
other product metadata filters currently registered there
Thin bootstrap means orchestration moves elsewhere. It does not mean these integrations disappear.
2. Application Entry
Introduce PPOM\Plugin as the main application object.
Responsibilities of PPOM\Plugin:
compose modules
create or receive the service registry
bootstrap the runtime in a predictable order
keep lifecycle composition centralized
relocate bootstrap-era integrations into explicit modules or boot services
Non-responsibilities of PPOM\Plugin:
business logic
direct SQL
template rendering
request validation rules
price calculations
3. Hook Registration
Each feature module should own its own hooks through a shared contract such as RegisterHooks.
Rules:
every module exposes register()
the module registers only the hooks that belong to its feature area
hook callbacks should delegate to services or controllers, not perform large amounts of work inline
avoid introducing a new mega-class that simply replaces NM_PersonalizedProduct
Hook timing compatibility rules for the first program:
preserve existing hook names, priorities, accepted argument counts, relative callback ordering, and conditional registration semantics unless a change is explicitly scoped and protected by characterization tests
preserve bootstrap-time hook registration order across before_woocommerce_init, init, woocommerce_init, rest_api_init, admin_menu, admin_init, and AJAX registration
keep the following runtime hook behavior unchanged until a versioned migration exists:
woocommerce_before_add_to_cart_button at priority 15, with legacy vs modern rendering selected by ppom_is_legacy_mode()
woocommerce_add_to_cart_validation at priority 10 only when client validation is disabled
woocommerce_get_cart_item_from_session in modern price mode with ppom_price_check_price_matrix at priority 8 before ppom_price_controller at priority 10
woocommerce_get_cart_item_from_session in legacy price mode with ppom_woocommerce_update_cart_fees at priority 10
woocommerce_cart_calculate_fees registering only the fee callback for the active pricing mode
woocommerce_checkout_create_order_line_item at priority 99
woocommerce_checkout_order_processed at priority 10
if internal modules take over a legacy hook, compatibility wrappers must still reproduce the original registration timing and callable availability
4. Data Access
PPOM should introduce a formal data layer, but the primary read model must be resolved product configuration, not just raw field-group storage.
Required pieces:
a field-group repository or data store over {prefix}_nm_personalized
a resolved product configuration service or resolver
direct product attachment lookup for _product_meta_id
category-linked and tag-aware row lookup
support for existing merge and override filters
settings derivation and merged field resolution
Rules:
SQL should not live in controllers
SQL should not live in view-adjacent classes
SQL should not remain scattered across admin, REST, resolver, and runtime orchestration classes
PPOM_Meta should become a compatibility wrapper over the new resolver once the data layer exists
Compatibility rule:
the resolver must preserve current product configuration semantics, including category matching, tag-aware stored data, filter-driven override rules, field merge order, and derived settings behavior
Current Resolution Flow
flowchart TD
A["Product page loads"] --> B["PPOM_Meta constructed\nwith product_id"]
B --> C{"_product_meta_id\nexists?"}
C -->|Yes| D["Load direct\nfield-group rows"]
C -->|No| E["Check category-linked\ngroups"]
E --> F["Check tag-aware\nrows"]
D --> G["Merge rows"]
F --> G
G --> H["Apply ppom filters\nmerge & override"]
H --> I["Resolved product\nconfiguration"]
I --> J["Computed properties:\nfields, settings,\npricing config"]
Loading
5. Controllers And Request Models
Admin, AJAX, REST, and public shopper requests should move to explicit controllers or request handlers backed by services or use-cases.
This must distinguish between privileged and public flows.
Privileged admin request rules:
validate capability before privileged operations
verify nonce for state-changing admin and admin AJAX requests
sanitize request input before use
validate schema and business rules before mutation
delegate storage and domain logic into services or repositories
Public shopper-facing request rules:
do not apply admin capability requirements to guest-facing flows
use nonce, cart-scoped state, product context, and ownership validation where appropriate
preserve guest upload, delete, and validation behavior that currently works without admin privileges
REST rules:
maintain current REST v1 route names and namespace in the first program
preserve the current write authentication model unless there is an explicit versioned migration
if current writes depend on secret_key inside callbacks, that behavior, request semantics, and error shapes are part of the compatibility contract for v1
current v1 REST write routes are a documented legacy compatibility exception to stricter WordPress-native permission_callback enforcement during the first program
stricter permission_callback logic can be introduced only if it preserves existing client behavior or is released as a new versioned contract
do not ship a hybrid auth change that moves failures from callback validation to route permission checks while also changing status codes, error messages, or response bodies for existing v1 clients
Current vs Target Request Flow
flowchart TD
subgraph "Current: mixed request handling"
R1["Admin request"] --> NMP["NM_PersonalizedProduct\n+ inc/admin.php"]
R2["Public AJAX"] --> NMP2["NM_PersonalizedProduct\n+ inc/files.php"]
R3["REST write"] --> REST["PPOM_Rest\n+ secret_key check\n+ direct SQL"]
NMP --> DB["Direct SQL\nin multiple files"]
NMP2 --> DB
REST --> DB
end
Loading
flowchart TD
subgraph "Target: separated request handling"
R1["Admin request"] --> AC["Admin\\Controller"]
R2["Public AJAX"] --> FC["Files\\UploadHandler"]
R3["REST write"] --> RC["Rest\\Controller"]
AC --> VAL["Validation\nsanitize · business rules"]
FC --> VAL
RC --> VAL
VAL --> SVC["Services\nFieldGroupService · PriceCalculator\nUploadService"]
SVC --> REPO["Data\\FieldGroupRepository\nData\\ProductConfigResolver"]
REPO --> DB["Database"]
end
Loading
6. Modes And Strategies
PPOM currently supports multiple runtime modes. Those branches should be isolated instead of spread through the codebase.
Required strategy boundaries:
legacy vs modern rendering
legacy vs modern pricing
Goal:
make mode selection explicit
keep branching localized
reduce condition leakage across unrelated subsystems
7. Compatibility
Compatibility is a first-class requirement.
Must preserve in the first program:
plugin constants
PPOM()
PPOM_Meta
PPOM_Inputs()
PPOM_Inputs (base class for all input types, extended by 27+ Pro addon classes)
NM_PersonalizedProduct
NM_PersonalizedProduct_Admin
NM_PersonalizedProduct_Admin inheritance from NM_PersonalizedProduct
static helpers on legacy classes that are used externally, including NM_PersonalizedProduct_Admin::save_categories_and_tags()
existing ppom_* hooks and functions (40+ filters and actions consumed by Pro)
existing ppom_* global functions (80+ helpers called by Pro and themes)
current DB schema
current template override behavior
current input registration behavior via ppom_all_inputs filter
current REST v1 route surface
current REST secret_key write auth model and response semantics
current public shopper-facing AJAX availability for uploads, delete, and validation
PPOM_COMPATIBILITY_FEATURES and PPOM_PRO_COMPATIBILITY_FEATURES flag contracts
Legacy class compatibility rules:
PPOM_Meta compatibility includes its computed public properties and construction-time side effects, not only its class name
PPOM_Inputs compatibility includes the PPOM_Inputs() global accessor, constructor shape, and methods and properties relied on by free and Pro input subclasses
NM_PersonalizedProduct_Admin compatibility includes its inheritance relationship to NM_PersonalizedProduct, static helpers such as save_categories_and_tags(), menu registration, settings-migration behavior, DB-table setup hooks, and other initialization side effects currently triggered in its constructor
compatibility is defined by callable shape and behavior, not only by symbol names; non-ppom_* legacy accessors that are callable today must remain callable until an explicit versioned migration exists
legacy entrypoints should become delegators into src/, not be deleted outright
Compatibility Wrapper Strategy
flowchart LR
subgraph "Legacy surface — preserved"
A["PPOM()"]
B["PPOM_Meta"]
C["NM_PersonalizedProduct"]
D["NM_PersonalizedProduct_Admin"]
E["PPOM_Inputs\n(base for 49+ input classes)"]
F["ppom_* functions\n(51+ called by Pro)"]
end
subgraph "src/ — new internals"
G["Core\\Plugin"]
H["Data\\ProductConfigResolver"]
I["Frontend + Cart modules"]
J["Admin module"]
K["Frontend\\InputBase"]
L["Services"]
end
A -->|delegates to| G
B -->|delegates to| H
C -->|delegates to| I
D -->|delegates to| J
E -->|extends| K
F -->|delegates to| L
Loading
8. Security Rules
Refactoring must not weaken the plugin's trust boundaries.
Rules:
all privileged admin and admin AJAX mutations require the right authorization model for that surface
REST mutations in v1 must preserve the current compatibility auth contract; stricter permission_callback enforcement is allowed only as a compatibility-preserving change or as a versioned migration
all state-changing admin and admin AJAX flows require nonce verification
public guest-facing endpoints require scope-appropriate request validation and ownership checks
pricing remains server-authoritative and idempotent
uploaded file paths, names, MIME types, and ownership are always verified server-side
only normalized values needed by PPOM should be stored in cart item data, session state, and order item meta
sanitize on input, validate against business rules, and escape on output
Preserve current v1 secret_key-based write auth unless an explicit versioned replacement is introduced
Phase 4. Frontend And Assets
Move rendering orchestration into frontend modules
Move asset registration and localization into dedicated modules
Preserve existing templates, template filters, and input extension points
Preserve public shopper-facing AJAX entrypoints while relocating their logic
Phase 5. Cart, Pricing, Orders, And Files
Move validation orchestration into services
Move posted payload normalization into explicit classes
Move price calculation into pricing services and strategies
Move order item persistence into dedicated lifecycle services
Move upload and file finalization into file services
Preserve WooCommerce lifecycle behavior and stored data contracts
Phase 6. Compatibility Cleanup
Reduce legacy files to thin wrappers where replacements are complete
Add deprecation markers only after compatible replacements are stable
Remove dead internal paths only when test coverage proves parity
Public Interfaces To Preserve Or Introduce
Preserve
plugin constants such as PPOM_PATH, PPOM_URL, PPOM_VERSION, PPOM_PRODUCT_META_KEY, and PPOM_TABLE_META
PPOM()
PPOM_Inputs()
current action and filter names
current hook priorities, relative callback order, and conditional registration behavior for compatibility-sensitive WordPress and WooCommerce hooks
current DB schema
current REST v1 route surface
current REST secret_key write auth semantics
current REST status and error message shapes where clients may depend on them
current public AJAX availability for guest uploads, delete, and validation
current template override filters
current input registration behavior
currently callable legacy class helpers and non-ppom_* accessors that extensions, tests, or tooling may invoke directly
Introduce Internally
RegisterHooks
FieldGroupRepository
ProductConfigurationResolver
PriceCalculator
UploadService
These contracts are internal architecture boundaries, not public extension APIs in the first phase.
Migration Guardrails
Non-negotiable rules for all refactoring work:
do not change store-facing behavior unless a change is explicitly scoped as a bug fix
do not redesign the field-group storage model in the first program
do not break add-on or extension hooks without an explicit compatibility plan
do not move new runtime code into legacy directories unless the work is a shim or legacy-only fix
do not replace WordPress and WooCommerce conventions with framework-driven abstractions that add complexity without clear value
do not perform broad cleanup without characterization tests around affected lifecycle paths
do not narrow public request surfaces unless an explicit migration plan exists
Non-goals for the first program:
full table redesign
full public API redesign
breaking REST version changes
a framework-style rewrite
replacing every global helper in one pass
Test And Acceptance Criteria
Every refactoring phase must preserve the following behavior:
simple product flows
variable product flows
guest checkout
logged-in checkout
session restore
order-again flows
tax mode handling
coupon and sale interactions
quantity changes
double-charge prevention
upload validation, ownership, cleanup, and order finalization
admin success, capability failure, and nonce failure paths
REST success, permission or auth failure, validation failure, and write failure paths
guest-facing AJAX success and failure paths for upload, delete, and validation
category-linked and filtered product-configuration resolution
Testing rules:
add characterization tests before major internal moves in validation, pricing, cart, order, file, and product-configuration resolution flows
keep PHPUnit coverage focused on services, repositories, and lifecycle orchestration as they are introduced
preserve end-to-end coverage for product, cart, checkout, admin field-group workflows, and public upload/validation flows
Acceptance bar for a completed phase:
no intentional public contract breakage
all targeted tests pass
legacy entrypoints still function
new code is placed in src/
replaced logic is thinner in legacy wrappers than before
compatibility-sensitive request surfaces still behave as before
PPOM Pro Compatibility Surface
The Pro addon (ppom-pro) is the highest-priority external consumer of the free plugin's API surface. It gates its entire runtime on class_exists('NM_PersonalizedProduct') and function_exists('PPOM'). Every refactoring phase must be validated with ppom-pro active.
This is the most structurally critical dependency. All 27+ Pro input types extend PPOM_Inputs from the free plugin. Changing the base class signature or constructor breaks every Pro addon field.
flowchart TD
BASE["PPOM_Inputs\nclasses/input.class.php\nBase input class"]
subgraph "Free plugin inputs — classes/inputs/"
F1["NM_Text_wooproduct"]
F2["NM_Select_wooproduct"]
F3["NM_File_wooproduct"]
F4["NM_Checkbox_wooproduct"]
FN["... 18 more free types"]
end
subgraph "Pro addon inputs — ppom-pro/inc/Addons/*/classes/"
P1["NM_SVG_wooproduct"]
P2["NM_Imageselect_wooproduct"]
P3["NM_BulkQuantity_wooproduct"]
P4["NM_Fancycropper_wooproduct"]
P5["NM_Superlist_wooproduct"]
P6["NM_Domain_wooproduct"]
PN["... 21 more pro types"]
end
BASE --> F1
BASE --> F2
BASE --> F3
BASE --> F4
BASE --> FN
BASE --> P1
BASE --> P2
BASE --> P3
BASE --> P4
BASE --> P5
BASE --> P6
BASE --> PN
Loading
Critical Pro Dependencies
Classes that must remain available with current signatures:
Class
Why Pro depends on it
NM_PersonalizedProduct
Boot gate: class_exists() check before any Pro code runs
PPOM_Meta
Instantiated directly in 6+ Pro files for field resolution
PPOM_Inputs
Base class for all 27+ Pro input types via extends
Functions Pro calls directly (51 unique functions across all addons and core):
High-frequency (10+ call sites):
Function
Call sites
Purpose
ppom_get_option()
40+
Settings retrieval
ppom_get_product_id()
20+
Product ID extraction
ppom_get_plugin_meta()
22+
Plugin metadata in every input class
ppom_esc_html()
25+
HTML escaping for field output
ppom_has_field_by_type()
20+
Field type detection on product
ppom_get_input_cols()
17+
Input column options for field layout
ppom_field_visibility_options()
17+
Visibility options in every input class
ppom_convert_options_to_key_val()
12+
Option format conversion
ppom_get_field_meta_by_dataname()
11+
Field lookup by data name
ppom_is_field_has_price()
12+
Price attachment check
Medium-frequency (3–9 call sites):
Function
Call sites
Purpose
ppom_make_meta_data()
8
Cart/order meta structure creation
ppom_get_dir_path()
6
PPOM files directory path
ppom_get_dir_url()
5
PPOM files directory URL
ppom_get_thumbs_size()
4
Thumbnail dimensions
ppom_file_get_name()
5
File name extraction from upload data
ppom_load_input_templates()
3
Input template loading
ppom_load_file()
3
File loading with variables
ppom_get_option_id()
3
DOM option element ID
ppom_field_has_stock()
3
Stock level checks
ppom_create_thumb_for_meta()
3
Thumbnail creation for uploaded files
ppom_save_data_url_to_image()
2
Data URL to image conversion
PPOM()
6
Main plugin instance accessor
Low-frequency but critical (1–2 call sites):
Function
Call sites
Purpose
ppom_admin_update_ppom_meta_only()
1
Field group persistence (Meta_Utility trait)
ppom_load_template()
3
Template loading
ppom_generate_field_price()
1
Single field price generation
ppom_price()
3
Price formatting
ppom_hooks_convert_price()
1
WPML price conversion callback
ppom_hooks_load_input_scripts()
1
Input scripts loading callback
ppom_is_cart_quantity_updatable()
1
Cart quantity update check
ppom_is_field_visible()
2
Field visibility check
ppom_is_legacy_mode()
2
Legacy mode detection
ppom_files_trim_name()
1
File name trimming
ppom_create_image_thumb()
1
Image thumbnail creation
ppom_hide_variation_price_html()
1
Variation price hiding
ppom_pro_is_installed()
1
Self-detection
ppom_get_price_table_location()
2
Price table placement
ppom_woocommerce_show_fields_on_product()
1
Field rendering on product page
ppom_woocommerce_template_base_inputs_rendering()
2
Input template rendering
ppom_pa()
100+
Debug output (dev-only, but still called)
Constants Pro requires:
Constant
Usage in Pro
PPOM_VERSION
Script versioning in 10+ enqueue calls
PPOM_PATH
File path resolution
PPOM_URL
Asset URL construction
PPOM_PRODUCT_META_KEY
Product meta queries
PPOM_TABLE_META
Direct SQL in CSV import/export
Hooks Pro registers callbacks on (must continue to fire):
Hook
Priority
Purpose
ppom_all_inputs
default
Registers all 27+ Pro input types
ppom_hooks_inputs
default
Input hook wiring
ppom_after_scripts_loaded
default
Pro script loading
ppom_select_meta_in_product
99
Custom product field assignment UI
ppom_before_fields_validation
default
Stock validation
ppom_settings_data
default
Pro settings injection
ppom_option_price
default
WPML price conversion
ppom_fields_prices
default
Cart price adjustment
ppom_meta_overrides
default
Category/product override mode
ppom_rendering_inputs_{$type}
default
Per-type rendering for Pro fields
Data structures Pro reads and writes:
Structure
Location
Contract
$cart_item['ppom']['fields']
Cart session
Field submissions keyed by data_name
_ppom_fields order item meta
Order items
Snapshot of field submissions
_product_meta_id post meta
Products
Array of attached field group IDs
nm_personalized table
Database
Direct SQL for CSV import/export
Compatibility Feature Flags
Both plugins declare feature flags for cross-version compatibility:
PPOM_Inputs is the single most dangerous class to modify. Any change to its constructor, public methods, or protected methods used by subclasses can break 27+ Pro input types. If this class moves to src/, the original must remain as a thin extends-and-delegates wrapper.
Pro performs direct SQL against PPOM_TABLE_META for CSV import/export. The table schema and the constant must remain stable.
Pro checks PPOM()->is_license_of_type() for license-gated features. The PPOM() function must continue returning an object with this method.
Every refactoring phase should be smoke-tested with ppom-pro active and at least one Pro input type (e.g., bulk quantity or image dropdown) configured on a product.
Relationship To Existing Documentation
architecture.md remains the factual description of the current architecture.
This document builds on that baseline and defines the intended future shape plus the migration path to get there.
PPOM Refactoring Plan
Intent
This document is the canonical engineering guide for modernizing PPOM from its current legacy singleton and procedural structure into a modular WordPress-native plugin architecture.
The goal is to modernize internals without breaking store behavior, extension hooks, database schema, REST v1 contracts, bootstrap-era integrations, or existing extensions during the first refactoring program.
This is a maintainers-only document. It defines the target architecture, migration rules, compatibility constraints, and rollout order that should guide all future refactoring work.
Current State Overview
PPOM works today, but its runtime is organized around a heavy bootstrap and a central coordinator that owns too many responsibilities.
Current characteristics:
woocommerce-product-addon.phpis a heavy bootstrap that defines constants, loads Composer, manually includes most runtime files, wires Themeisle SDK metadata, boots freemium behavior, registers plugin action links, loads Elementor integration, loads REST, declares HPOS compatibility, and starts the main runtime.classes/plugin.class.phpacts as the main hook coordinator for rendering, validation, pricing, cart/session behavior, order persistence, public AJAX, admin AJAX, shortcode registration, and misc local hooks.inc/woocommerce.phpinc/prices.phpinc/files.phpinc/admin.php_product_meta_idlookup. It also depends on category-linked groups, stored tag-aware rows, and filter-driven merge and override rules inclasses/ppom.class.php.inc/rest.class.phpcurrently exposes public routes and performs write authentication inside callbacks with a sharedsecret_key.Primary pain points:
SQL Distribution
flowchart TD subgraph "Current: SQL scattered across layers" A["classes/ppom.class.php\n7 SELECT queries"] B["inc/admin.php\nINSERT · UPDATE · DELETE"] C["inc/rest.class.php\nINSERT · UPDATE · DELETE"] D["classes/admin.class.php\nSELECT queries"] end subgraph "Target: SQL centralized in Data layer" E["Data\\FieldGroupRepository\nAll CRUD operations"] F["Data\\ProductConfigResolver\nAll read queries"] end A -.->|moves to| F B -.->|moves to| E C -.->|moves to| E D -.->|moves to| FCurrent Architecture
flowchart TD A["WordPress loads PPOM"] --> B["woocommerce-product-addon.php<br/>heavy bootstrap"] B --> C["Manual require_once of inc/, classes/, backend/"] B --> D["SDK / freemium / Elementor / action links / HPOS wiring"] C --> E["NM_PersonalizedProduct singleton"] C --> F["NM_PersonalizedProduct_Admin"] C --> G["PPOM_Rest"] E --> H["inc/woocommerce.php<br/>product, cart, order flow"] E --> I["inc/prices.php<br/>pricing and fees"] E --> J["inc/files.php<br/>uploads and cleanup"] E --> K["Public AJAX<br/>upload/delete/validate"] F --> L["inc/admin.php<br/>admin CRUD and product attachment"] G --> M["inc/rest.class.php<br/>public routes + callback auth"] E --> N["PPOM_Meta resolver"] F --> N G --> N N --> O["{prefix}_nm_personalized"] N --> P["_product_meta_id"] N --> Q["category/tag-linked groups + filters"]Chosen Target Architecture
PPOM should move to a WordPress-like modular architecture.
This does not mean introducing a framework rewrite, nor cloning WooCommerce internals. It means using a modern plugin shape that is native to WordPress and WooCommerce: thin bootstrap, namespaced autoloaded runtime, feature modules, controllers, services, repositories/data stores, and explicit compatibility shims.
Design rules:
src/tree.Target Architecture
flowchart TD A["WordPress + WooCommerce hooks"] --> B["woocommerce-product-addon.php<br/>thin bootstrap"] B --> C["PPOM\\Plugin"] C --> CORE["src/Core<br/>registry · contracts · lifecycle"] CORE --> ADMIN["src/Admin<br/>pages · settings · AJAX"] CORE --> REST_M["src/Rest<br/>routes · controllers"] CORE --> FRONT["src/Frontend<br/>rendering · templates · inputs"] CORE --> CART["src/Cart<br/>validation · payload · lifecycle"] CORE --> PRICING["src/Pricing<br/>strategies · fees · matrix"] CORE --> FILES["src/Files<br/>uploads · cleanup · AJAX"] CORE --> COMPAT["src/Compatibility<br/>legacy wrappers · shims"] DATA["src/Data<br/>repositories · resolvers"] ADMIN --> DATA REST_M --> DATA FRONT --> DATA CART --> DATA CART --> PRICING PRICING --> DATA FILES --> DATA COMPAT -.->|delegates to| ADMIN COMPAT -.->|delegates to| FRONT COMPAT -.->|delegates to| DATATarget Runtime Layout
PPOM should keep the legacy tree during the transition, but new runtime work should be organized under
src/and loaded via Composer PSR-4.Directory strategy:
inc/,classes/, andbackend/during migration.src/for all new runtime code.PPOM\\namespace.Intended layout:
Layer responsibilities:
src/CoreRegisterHookssrc/Adminsrc/Frontendsrc/Cartsrc/Pricingsrc/Filessrc/Data{prefix}_nm_personalized_product_meta_idsrc/Restsrc/CompatibilityPPOM()PPOM_Meta,NM_PersonalizedProduct, andNM_PersonalizedProduct_Adminppom_*legacy entrypointsModule Dependency Map
flowchart TD CORE["Core\nbootstrap · registry · contracts"] CORE --> ADMIN["Admin\npages · settings · AJAX"] CORE --> FRONT["Frontend\nrendering · templates · inputs"] CORE --> REST_M["Rest\nroutes · controllers"] CORE --> CART["Cart\nvalidation · payload · lifecycle"] CORE --> FILES["Files\nuploads · cleanup · AJAX"] CORE --> COMPAT["Compatibility\nlegacy wrappers · shims"] DATA["Data\nrepositories · resolvers"] ADMIN --> DATA FRONT --> DATA REST_M --> DATA REST_M --> CART CART --> PRICING["Pricing\nstrategies · fees · matrix"] CART --> DATA FILES --> DATA PRICING --> DATA COMPAT -.->|delegates to| ADMIN COMPAT -.->|delegates to| FRONT COMPAT -.->|delegates to| DATA COMPAT -.->|delegates to| CARTRule for new work:
src/unless the change is explicitly a compatibility shim or a targeted legacy fix.Key Architectural Decisions
1. Bootstrap
woocommerce-product-addon.phpshould eventually be reduced to:PPOM()accessor compatibilityThe bootstrap must stop owning detailed runtime wiring, but bootstrap-era integrations are still part of the product surface and must be preserved or relocated explicitly:
Thin bootstrap means orchestration moves elsewhere. It does not mean these integrations disappear.
2. Application Entry
Introduce
PPOM\Pluginas the main application object.Responsibilities of
PPOM\Plugin:Non-responsibilities of
PPOM\Plugin:3. Hook Registration
Each feature module should own its own hooks through a shared contract such as
RegisterHooks.Rules:
register()NM_PersonalizedProductHook timing compatibility rules for the first program:
before_woocommerce_init,init,woocommerce_init,rest_api_init,admin_menu,admin_init, and AJAX registrationwoocommerce_before_add_to_cart_buttonat priority15, with legacy vs modern rendering selected byppom_is_legacy_mode()woocommerce_add_to_cart_validationat priority10only when client validation is disabledwoocommerce_get_cart_item_from_sessionin modern price mode withppom_price_check_price_matrixat priority8beforeppom_price_controllerat priority10woocommerce_get_cart_item_from_sessionin legacy price mode withppom_woocommerce_update_cart_feesat priority10woocommerce_cart_calculate_feesregistering only the fee callback for the active pricing modewoocommerce_checkout_create_order_line_itemat priority99woocommerce_checkout_order_processedat priority104. Data Access
PPOM should introduce a formal data layer, but the primary read model must be resolved product configuration, not just raw field-group storage.
Required pieces:
{prefix}_nm_personalized_product_meta_idRules:
PPOM_Metashould become a compatibility wrapper over the new resolver once the data layer existsCompatibility rule:
Current Resolution Flow
flowchart TD A["Product page loads"] --> B["PPOM_Meta constructed\nwith product_id"] B --> C{"_product_meta_id\nexists?"} C -->|Yes| D["Load direct\nfield-group rows"] C -->|No| E["Check category-linked\ngroups"] E --> F["Check tag-aware\nrows"] D --> G["Merge rows"] F --> G G --> H["Apply ppom filters\nmerge & override"] H --> I["Resolved product\nconfiguration"] I --> J["Computed properties:\nfields, settings,\npricing config"]5. Controllers And Request Models
Admin, AJAX, REST, and public shopper requests should move to explicit controllers or request handlers backed by services or use-cases.
This must distinguish between privileged and public flows.
Privileged admin request rules:
Public shopper-facing request rules:
REST rules:
secret_keyinside callbacks, that behavior, request semantics, and error shapes are part of the compatibility contract for v1permission_callbackenforcement during the first programpermission_callbacklogic can be introduced only if it preserves existing client behavior or is released as a new versioned contractCurrent vs Target Request Flow
flowchart TD subgraph "Current: mixed request handling" R1["Admin request"] --> NMP["NM_PersonalizedProduct\n+ inc/admin.php"] R2["Public AJAX"] --> NMP2["NM_PersonalizedProduct\n+ inc/files.php"] R3["REST write"] --> REST["PPOM_Rest\n+ secret_key check\n+ direct SQL"] NMP --> DB["Direct SQL\nin multiple files"] NMP2 --> DB REST --> DB endflowchart TD subgraph "Target: separated request handling" R1["Admin request"] --> AC["Admin\\Controller"] R2["Public AJAX"] --> FC["Files\\UploadHandler"] R3["REST write"] --> RC["Rest\\Controller"] AC --> VAL["Validation\nsanitize · business rules"] FC --> VAL RC --> VAL VAL --> SVC["Services\nFieldGroupService · PriceCalculator\nUploadService"] SVC --> REPO["Data\\FieldGroupRepository\nData\\ProductConfigResolver"] REPO --> DB["Database"] end6. Modes And Strategies
PPOM currently supports multiple runtime modes. Those branches should be isolated instead of spread through the codebase.
Required strategy boundaries:
Goal:
7. Compatibility
Compatibility is a first-class requirement.
Must preserve in the first program:
PPOM()PPOM_MetaPPOM_Inputs()PPOM_Inputs(base class for all input types, extended by 27+ Pro addon classes)NM_PersonalizedProductNM_PersonalizedProduct_AdminNM_PersonalizedProduct_Admininheritance fromNM_PersonalizedProductNM_PersonalizedProduct_Admin::save_categories_and_tags()ppom_*hooks and functions (40+ filters and actions consumed by Pro)ppom_*global functions (80+ helpers called by Pro and themes)ppom_all_inputsfiltersecret_keywrite auth model and response semanticsPPOM_COMPATIBILITY_FEATURESandPPOM_PRO_COMPATIBILITY_FEATURESflag contractsLegacy class compatibility rules:
PPOM_Metacompatibility includes its computed public properties and construction-time side effects, not only its class namePPOM_Inputscompatibility includes thePPOM_Inputs()global accessor, constructor shape, and methods and properties relied on by free and Pro input subclassesNM_PersonalizedProduct_Admincompatibility includes its inheritance relationship toNM_PersonalizedProduct, static helpers such assave_categories_and_tags(), menu registration, settings-migration behavior, DB-table setup hooks, and other initialization side effects currently triggered in its constructorppom_*legacy accessors that are callable today must remain callable until an explicit versioned migration existssrc/, not be deleted outrightCompatibility Wrapper Strategy
flowchart LR subgraph "Legacy surface — preserved" A["PPOM()"] B["PPOM_Meta"] C["NM_PersonalizedProduct"] D["NM_PersonalizedProduct_Admin"] E["PPOM_Inputs\n(base for 49+ input classes)"] F["ppom_* functions\n(51+ called by Pro)"] end subgraph "src/ — new internals" G["Core\\Plugin"] H["Data\\ProductConfigResolver"] I["Frontend + Cart modules"] J["Admin module"] K["Frontend\\InputBase"] L["Services"] end A -->|delegates to| G B -->|delegates to| H C -->|delegates to| I D -->|delegates to| J E -->|extends| K F -->|delegates to| L8. Security Rules
Refactoring must not weaken the plugin's trust boundaries.
Rules:
permission_callbackenforcement is allowed only as a compatibility-preserving change or as a versioned migrationMigration Phases
flowchart LR P1["Phase 1\nFoundation"] --> P2["Phase 2\nData Layer"] P2 --> P3["Phase 3\nAdmin & REST"] P2 --> P4["Phase 4\nFrontend & Assets"] P3 --> P5["Phase 5\nCart, Pricing,\nOrders & Files"] P4 --> P5 P5 --> P6["Phase 6\nCompatibility\nCleanup"]Phase 1. Foundation
PPOM\\PPOM\PluginPhase 2. Data Layer
PPOM_Metainto a compatibility wrapper over the new resolverPhase 3. Admin And REST
secret_key-based write auth unless an explicit versioned replacement is introducedPhase 4. Frontend And Assets
Phase 5. Cart, Pricing, Orders, And Files
Phase 6. Compatibility Cleanup
Public Interfaces To Preserve Or Introduce
Preserve
PPOM_PATH,PPOM_URL,PPOM_VERSION,PPOM_PRODUCT_META_KEY, andPPOM_TABLE_METAPPOM()PPOM_Inputs()secret_keywrite auth semanticsppom_*accessors that extensions, tests, or tooling may invoke directlyIntroduce Internally
RegisterHooksFieldGroupRepositoryProductConfigurationResolverPriceCalculatorUploadServiceThese contracts are internal architecture boundaries, not public extension APIs in the first phase.
Migration Guardrails
Non-negotiable rules for all refactoring work:
Non-goals for the first program:
Test And Acceptance Criteria
Every refactoring phase must preserve the following behavior:
Testing rules:
Acceptance bar for a completed phase:
src/PPOM Pro Compatibility Surface
The Pro addon (
ppom-pro) is the highest-priority external consumer of the free plugin's API surface. It gates its entire runtime onclass_exists('NM_PersonalizedProduct')andfunction_exists('PPOM'). Every refactoring phase must be validated with ppom-pro active.Pro Integration Architecture
flowchart TD subgraph "Free plugin — woocommerce-product-addon" BOOT["Bootstrap\nconstants · PPOM()"] CLASSES["PPOM_Meta\nNM_PersonalizedProduct\nPPOM_Inputs"] FUNCS["ppom_* functions\n80+ global helpers"] HOOKS["ppom_* hooks\n40+ filters and actions"] DATA["DB table\nnm_personalized"] META["Post/Order meta\n_product_meta_id\n_ppom_fields"] end subgraph "Pro addon — ppom-pro" PRO_BOOT["Boot gate\nclass_exists · function_exists"] PRO_INPUTS["27 input classes\nextends PPOM_Inputs"] PRO_HOOKS["Hook callbacks\n40+ ppom_* filters"] PRO_CORE["Core features\nstock · export · vendor"] PRO_SQL["Direct SQL\nCSV import/export"] end BOOT --> PRO_BOOT CLASSES --> PRO_INPUTS CLASSES --> PRO_CORE FUNCS --> PRO_CORE HOOKS --> PRO_HOOKS DATA --> PRO_SQL META --> PRO_COREInput Type Inheritance Chain
This is the most structurally critical dependency. All 27+ Pro input types extend
PPOM_Inputsfrom the free plugin. Changing the base class signature or constructor breaks every Pro addon field.flowchart TD BASE["PPOM_Inputs\nclasses/input.class.php\nBase input class"] subgraph "Free plugin inputs — classes/inputs/" F1["NM_Text_wooproduct"] F2["NM_Select_wooproduct"] F3["NM_File_wooproduct"] F4["NM_Checkbox_wooproduct"] FN["... 18 more free types"] end subgraph "Pro addon inputs — ppom-pro/inc/Addons/*/classes/" P1["NM_SVG_wooproduct"] P2["NM_Imageselect_wooproduct"] P3["NM_BulkQuantity_wooproduct"] P4["NM_Fancycropper_wooproduct"] P5["NM_Superlist_wooproduct"] P6["NM_Domain_wooproduct"] PN["... 21 more pro types"] end BASE --> F1 BASE --> F2 BASE --> F3 BASE --> F4 BASE --> FN BASE --> P1 BASE --> P2 BASE --> P3 BASE --> P4 BASE --> P5 BASE --> P6 BASE --> PNCritical Pro Dependencies
Classes that must remain available with current signatures:
NM_PersonalizedProductclass_exists()check before any Pro code runsPPOM_MetaPPOM_InputsextendsFunctions Pro calls directly (51 unique functions across all addons and core):
High-frequency (10+ call sites):
ppom_get_option()ppom_get_product_id()ppom_get_plugin_meta()ppom_esc_html()ppom_has_field_by_type()ppom_get_input_cols()ppom_field_visibility_options()ppom_convert_options_to_key_val()ppom_get_field_meta_by_dataname()ppom_is_field_has_price()Medium-frequency (3–9 call sites):
ppom_make_meta_data()ppom_get_dir_path()ppom_get_dir_url()ppom_get_thumbs_size()ppom_file_get_name()ppom_load_input_templates()ppom_load_file()ppom_get_option_id()ppom_field_has_stock()ppom_create_thumb_for_meta()ppom_save_data_url_to_image()PPOM()Low-frequency but critical (1–2 call sites):
ppom_admin_update_ppom_meta_only()ppom_load_template()ppom_generate_field_price()ppom_price()ppom_hooks_convert_price()ppom_hooks_load_input_scripts()ppom_is_cart_quantity_updatable()ppom_is_field_visible()ppom_is_legacy_mode()ppom_files_trim_name()ppom_create_image_thumb()ppom_hide_variation_price_html()ppom_pro_is_installed()ppom_get_price_table_location()ppom_woocommerce_show_fields_on_product()ppom_woocommerce_template_base_inputs_rendering()ppom_pa()Constants Pro requires:
PPOM_VERSIONPPOM_PATHPPOM_URLPPOM_PRODUCT_META_KEYPPOM_TABLE_METAHooks Pro registers callbacks on (must continue to fire):
ppom_all_inputsppom_hooks_inputsppom_after_scripts_loadedppom_select_meta_in_productppom_before_fields_validationppom_settings_datappom_option_priceppom_fields_pricesppom_meta_overridesppom_rendering_inputs_{$type}Data structures Pro reads and writes:
$cart_item['ppom']['fields']data_name_ppom_fieldsorder item meta_product_meta_idpost metanm_personalizedtableCompatibility Feature Flags
Both plugins declare feature flags for cross-version compatibility:
Pro Compatibility Rules For Refactoring
PPOM_Inputsis the single most dangerous class to modify. Any change to its constructor, public methods, or protected methods used by subclasses can break 27+ Pro input types. If this class moves tosrc/, the original must remain as a thin extends-and-delegates wrapper.PPOM_TABLE_METAfor CSV import/export. The table schema and the constant must remain stable.PPOM()->is_license_of_type()for license-gated features. ThePPOM()function must continue returning an object with this method.Relationship To Existing Documentation
architecture.mdremains the factual description of the current architecture.This document builds on that baseline and defines the intended future shape plus the migration path to get there.
Use both documents together:
architecture.mdto understand what exists today