Skip to content

Code Base Refactoring #557

Description

@Soare-Robert-Daniel

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.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.

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
  • src/Rest
    • route registration
    • request argument schema
    • permission or compatibility callbacks
    • REST controllers
  • src/Compatibility
    • wrappers for PPOM()
    • wrappers for PPOM_Meta, NM_PersonalizedProduct, and NM_PersonalizedProduct_Admin
    • delegators for ppom_* legacy entrypoints

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
Loading

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_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

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"]
Loading

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_Meta into 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, 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.

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
Loading

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
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:

// 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_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.

Use both documents together:

  • read architecture.md to understand what exists today
  • read this document to understand what should change, what must stay compatible, and in what order the refactor should happen

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Chore.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions