Codeinwp / Codeinwp/woocommerce-product-addon
Code Base Refactoring
A pull request for this has already been merged.
- #565 by @Soare-Robert-Daniel — merged
- Dominant language
- PHP
- Stars
- 15
- Forks
- 9
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 16
Description
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.- 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_idlookup. It also depends on category-linked groups, stored tag-aware rows, and filter-driven merge and override rules inclasses/ppom.class.php. - The REST API in
inc/rest.class.phpcurrently exposes public routes and performs write authentication inside callbacks with a sharedsecret_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
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"]
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
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/, andbackend/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.
Intended layout:
src/
Core/
Admin/
Frontend/
Cart/
Pricing/
Files/
Data/
Rest/
Compatibility/
Layer responsibilities:
src/Core- application bootstrap
- app container or service registry
- module contracts such as
RegisterHooks - plugin lifecycle composition
src/Admin- admin pages
- settings controllers
- admin AJAX controllers
- field-group management use-cases
src/Frontend- rendering orchestration
- input registry orchestration
- template coordination
- public shopper request coordination where it belongs to frontend state
src/Cart- posted payload normalization
- validation orchestration
- cart item lifecycle
- order item persistence orchestration
src/Pricing- price calculation services
- strategy classes for legacy vs modern pricing
- fee and matrix pricing coordination
src/Files- upload validation
- upload ownership checks
- temporary/confirmed file movement
- cleanup services
- guest/public upload and delete request handlers
src/Data- field-group repository or data-store classes for
{prefix}_nm_personalized - resolved product configuration read model
- product attachment lookup for
_product_meta_id - category and tag assignment queries
- merge and override resolution support
- field-group repository or data-store classes for
src/Rest- route registration
- request argument schema
- permission or compatibility callbacks
- REST controllers
src/Compatibility- wrappers for
PPOM() - wrappers for
PPOM_Meta,NM_PersonalizedProduct, andNM_PersonalizedProduct_Admin - delegators for
ppom_*legacy entrypoints
- wrappers for
Module 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| CART
Rule for new work:
- New runtime classes must be added under
src/unless the change is explicitly a compatibility shim or a targeted legacy fix.
Key Architectural Decisions
1. Bootstrap
woocommerce-product-addon.php should eventually be reduced to:
- plugin constants
- Composer autoload bootstrap
- translation setup
- WooCommerce compatibility declarations
PPOM()accessor compatibility- application startup
- activation and deactivation registration
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_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 priority10
- 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_Metashould 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"]
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_keyinside 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_callbackenforcement during the first program - stricter
permission_callbacklogic 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
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
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_MetaPPOM_Inputs()PPOM_Inputs(base class for all input types, extended by 27+ Pro addon classes)NM_PersonalizedProductNM_PersonalizedProduct_AdminNM_PersonalizedProduct_Admininheritance fromNM_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_inputsfilter - current REST v1 route surface
- current REST
secret_keywrite auth model and response semantics - current public shopper-facing AJAX availability for uploads, delete, and validation
PPOM_COMPATIBILITY_FEATURESandPPOM_PRO_COMPATIBILITY_FEATURESflag contracts
Legacy 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 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
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_callbackenforcement 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
Migration 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
- Add Composer PSR-4 autoloading for
PPOM\\ - Introduce
PPOM\Plugin - Introduce the module contract and service registry
- Keep runtime behavior unchanged while establishing the new application shell
- Relocate bootstrap-era integrations into explicit modules or boot services without changing behavior
Phase 2. Data Layer
- Extract field-group queries into repositories or data stores
- Extract resolved product configuration into a dedicated resolver or read model
- Move duplicated data logic out of admin, REST, and legacy runtime classes
- Convert
PPOM_Metainto a compatibility wrapper over the new resolver - Preserve category-linked, tag-aware, and filter-driven configuration semantics
Phase 3. Admin And REST
- Move admin CRUD flows into controllers and use-case services
- Move admin AJAX into controller classes
- Move REST route handling into controller classes
- Preserve existing route names, payload shapes, response semantics, and extension hooks
- 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, andPPOM_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_keywrite 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
RegisterHooksFieldGroupRepositoryProductConfigurationResolverPriceCalculatorUploadService
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.
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_CORE
Input Type Inheritance Chain
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
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:
// Free plugin (woocommerce-product-addon.php)
'PPOM_COMPATIBILITY_FEATURES' => [
'pro_cond_field_repeat' => true,
'pgfbdfm_wp_filter_param_fix' => true,
]
// Pro plugin (ppom.php)
'PPOM_PRO_COMPATIBILITY_FEATURES' => [
'cond_field_repeat' => true,
'pgfbdfm_wp_filter_param_fix' => true,
]
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.- Pro performs direct SQL against
PPOM_TABLE_METAfor CSV import/export. The table schema and the constant must remain stable. - Pro checks
PPOM()->is_license_of_type()for license-gated features. ThePPOM()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.
Use both documents together:
- read
architecture.mdto understand what exists today - read this document to understand what should change, what must stay compatible, and in what order the refactor should happen
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the PPOM Refactoring Plan in this issue, then inspect woocommerce-product-addon.php, classes/plugin.class.php, inc/woocommerce.php, inc/prices.php, inc/files.php, inc/admin.php, and classes/ppom.class.php. The plan describes a broad migration to a namespaced src/ architecture with Composer PSR-4 loading, repositories, modules, and compatibility shims; completion requires preserving existing behavior and contracts across the migration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100