woocommerce-paypal-payments/docs/plugin-architecture.md
Miguel Pérez Pellicer 5b585bb06b
Payment definition docs
2025-12-15 20:52:38 +04:00

16 KiB

Plugin Architecture Documentation

This document provides a comprehensive overview of the WooCommerce PayPal Payments plugin architecture, explaining its modular design and how the various components work together.

Overview

The WooCommerce PayPal Payments plugin is built using a modular architecture powered by the Syde Modularity framework. This design provides:

  • Modular Structure: Each feature is contained within its own module with clear boundaries
  • Dependency Injection: PSR-11 container for service management and dependency resolution
  • Feature Flags: Dynamic module loading based on environment variables and filters
  • Extensibility: Well-defined extension points for customization and enhancement
  • Maintainability: Clear separation of concerns and consistent patterns

Core Components

Main Plugin File

The plugin initialization begins in woocommerce-paypal-payments.php, which:

  • Loads the Composer autoloader if needed (e.g. may be already loaded in some tests)
  • Contains plugin metadata and constants definitions
  • It starts the bootstrap process, in plugins_loaded hook.

Bootstrap System

The bootstrap process is handled by bootstrap.php, which:

return function (
    string $root_dir,
    array $additional_containers = array(),
    array $additional_modules = array()
): ContainerInterface {
    // Load modules from modules.php
    $modules = ( require "$root_dir/modules.php" )( $root_dir );
    
    // Apply filters for customization
    $modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
    
    // Initialize plugin with Syde Modularity
    $properties = PluginProperties::new( "$root_dir/woocommerce-paypal-payments.php" );
    $bootstrap = Package::new( $properties );
    
    foreach ( $modules as $module ) {
        $bootstrap->addModule( $module );
    }
    
    $bootstrap->boot();
    return $bootstrap->container();
};

PPCP Container

The global PPCP class (src/PPCP.php) provides access to the dependency injection container:

class PPCP {
    private static $container = null;
    
    public static function container(): ContainerInterface {
        if ( ! self::$container ) {
            throw new LogicException( 'No PPCP container, probably called too early when the plugin is not initialized yet.' );
        }
        return self::$container;
    }
}

This allows third-party access services easily, such as in api/order-functions.php.

Module System

Module Definition

Modules are defined in modules.php with both core and conditional modules:

$modules = array(
    new PluginModule(),
    ( require "$modules_dir/woocommerce-logging/module.php" )(),
    ( require "$modules_dir/ppcp-admin-notices/module.php" )(),
    ( require "$modules_dir/ppcp-api-client/module.php" )(),
    // ... more core modules
);

Feature-Flag Controlled Modules

Conditional modules are loaded based on environment variables and filters (modules.php):

if ( apply_filters(
    'woocommerce.feature-flags.woocommerce_paypal_payments.applepay_enabled',
    getenv( 'PCP_APPLEPAY_ENABLED' ) !== '0'
) ) {
    $modules[] = ( require "$modules_dir/ppcp-applepay/module.php" )();
}

This pattern allows for:

  • Environment-based control: Use PCP_*_ENABLED environment variables
  • Runtime filtering: Apply WordPress filters to override defaults
  • Graceful degradation: Missing features don't break core functionality

Module Structure

Each module follows a consistent directory structure:

modules/ppcp-example/
├── module.php           # Module factory function
├── composer.json        # PHP dependencies
├── package.json         # JavaScript dependencies
├── webpack.config.js    # Asset building configuration
├── services.php         # Service definitions
├── extensions.php       # Service extensions/modifications
├── src/                 # PHP source code
│   └── ExampleModule.php
├── resources/          # Source assets
│   ├── js/
│   └── css/
└── assets/            # Built assets
    ├── js/
    └── css/

Module Interface Implementation

Most modules implement the Syde Modularity interfaces. For example in modules/ppcp-api-client/src/ApiModule.php:

class ApiModule implements ServiceModule, FactoryModule, ExtendingModule, ExecutableModule {
    use ModuleClassNameIdTrait;
    
    public function services(): array {
        return require __DIR__ . '/../services.php';
    }
    
    public function factories(): array {
        return require __DIR__ . '/../factories.php';
    }
    
    public function extensions(): array {
        return require __DIR__ . '/../extensions.php';
    }
    
    public function run( ContainerInterface $c ): bool {
        // Module initialization logic
        return true;
    }
}

Key Modules

Core Infrastructure Modules

  • PluginModule (src/PluginModule.php): Root module providing core services
  • woocommerce-logging: Logging infrastructure integration
  • ppcp-api-client: PayPal API integration, entities, and authentication
  • ppcp-session: Session management for payment flows
  • ppcp-webhooks: PayPal webhook handling

Payment & Checkout Modules

  • ppcp-button: PayPal Smart Payment Buttons and Advanced Credit and Debit Cards functionality
  • ppcp-blocks: WooCommerce Blocks integration
  • ppcp-wc-gateway: WooCommerce gateway integration
  • ppcp-axo: PayPal Fastlane (Accelerated Checkout) implementation

Feature Modules

  • ppcp-settings: New React-based admin settings interface
  • ppcp-vaulting: Saved payment methods functionality
  • ppcp-onboarding: Merchant onboarding flow

Alternative Payment Methods

  • ppcp-applepay/ppcp-googlepay: Digital wallet integrations
  • ppcp-local-alternative-payment-methods: Regional payment options

Dependency Injection & Services

Service Definition

Services are defined in each module's services.php file using factory functions:

return array(
    'example.service' => static function ( ContainerInterface $container ): ExampleService {
        return new ExampleService(
            $container->get( 'dependency.service' )
        );
    },
    
    'example.config' => static function (): array {
        return array(
            'setting' => 'value',
        );
    },
);

Service Extensions

The extensions.php files allow modules to modify or extend existing services:

return array(
    'existing.service' => static function ( ContainerInterface $container, ExistingService $service ): ExistingService {
        // Modify or wrap the existing service
        return new EnhancedService( $service );
    },
);

Container Access Patterns

Services can be accessed in multiple ways:

// In our modules/services/extensions (also often passed to hook handlers via `use`)
$service = $container->get( 'service.id' );

// In third-party plugins etc. (if not adding a custom module via the `woocommerce_paypal_payments_modules` filter) 
$service = PPCP::container()->get( 'service.id' );

// Check for service availability
if ( $container->has( 'optional.service' ) ) {
    $service = $container->get( 'optional.service' );
}

Plugin Feature Definition

The plugin has different features that can be enabled or disabled depending on assertions such as:

// WooCommerce country location. 
$container->get( 'api.shop.country' );

// PayPal merchant country location (notice this will fallback to WooCommerce country location if the user is not onboarded. 
$container->get( 'api.merchant.country' );

// Currency
$currency = $container->get( 'api.shop.currency.getter' );
$currency->get(); // USD

// PayPal API Feature flags 
$product_status = $container->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );
$apple_pay_enabled = $product_status->is_active();

// Any other feature dependency. For instance, checking if own_brand_only is no enabled.
$is_enabled => $feature_is_enabled && ! $general_settings->own_brand_only(),

The FeaturesDefinition.php file is used to define these features so they can be used in other services.

For instance, they are used in places as:

  • Endpoint serving the UI to know which features to show under the Features section under the Overview tab
  • Define the TODO list in the UI Overview tab

The features should be defined as public constants for easy access an prefixed as FEATURE_

// Defining Pay with Crypto feature
public const FEATURE_PAY_WITH_CRYPTO = 'pwc';

The features have different fields and a status field (enabled/disabled)

self::FEATURE_PAY_WITH_CRYPTO                 => array(
				'title'       => __( 'Pay with Crypto', 'woocommerce-paypal-payments' ),
				'description' => __( 'Enable customers to pay with cryptocurrency, and receive payments in USD in your PayPal balance.', 'woocommerce-paypal-payments' ),
				'enabled'     => $this->merchant_capabilities[ self::FEATURE_PAY_WITH_CRYPTO ],
				'buttons'     => array(
					array(
						'type'     => 'secondary',
						'text'     => __( 'Configure', 'woocommerce-paypal-payments' ),
						'action'   => array(
							'type'    => 'tab',
							'tab'     => 'payment_methods',
							'section' => 'ppcp-pay-with-crypto',
						),
						'showWhen' => 'enabled',
						'class'    => 'small-button',
					),
					array(
						'type'     => 'secondary',
						'text'     => __( 'Sign up', 'woocommerce-paypal-payments' ),
						'urls'     => array(
							'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/add-product?product=CRYPTO_PYMTS',
							'live'    => 'https://www.paypal.com/bizsignup/add-product?product=CRYPTO_PYMTS',
						),
						'showWhen' => 'disabled',
						'class'    => 'small-button',
					),
					array(
						'type'  => 'tertiary',
						'text'  => __( 'Learn more', 'woocommerce-paypal-payments' ),
						'url'   => 'https://www.paypal.com/us/digital-wallet/manage-money/crypto',
						'class' => 'small-button',
					),
				),
			),

There is a hook named woocommerce_paypal_payments_rest_common_merchant_features allowing to define which features are enabled or disabled:

// Enable Google Pay Feature
add_filter(
			'woocommerce_paypal_payments_rest_common_merchant_features',
			function ( array $features ) use ( $container ): array {
				$product_status = $container->get( 'googlepay.helpers.apm-product-status' );
				assert( $product_status instanceof ApmProductStatus );
				$google_pay_enabled = $product_status->is_active();

				$features[ FeaturesDefinition::FEATURE_GOOGLE_PAY ] = array(
					'enabled' => $google_pay_enabled,
				);

				return $features;
			}
		);

The FeaturesDefinition class is initialized in settings.data.definition.features

Another important part of the Features is the FeaturesEligibilityService.php

This service defines different callbacks to check if a feature is eligible for the merchant in runtime.

When using FeaturesDefinition::get() the plugin will run the registered eligibility callbacks checks for each Feature. The plugin will unset those Features without check or returning false in the eligibility check.

Payment Method Definition

Besides PayPal itself, we have different Payment methods such as Apple Pay, Pay with Crypto...etc.

These methods are defined in PaymentMethodsDefinition.php and they are divided in several groups:

PayPal methods

  • PayPal
  • Venmo
  • PayPal PayLater.
  • CardButtonGateway. Only in Own Brand Mode. It allows the user to pay with card even if the customer doesn't have a PayPal account.

Filterable via woocommerce_paypal_payments_gateway_group_paypal hook.

Card Methods

  • Advanced Credit and Debit Card Payments
  • Fastlane by PayPal
  • Apple Pay
  • Google Pay

Filterable via woocommerce_paypal_payments_gateway_group_cards hook.

Alternative Payment Methods

  • Pay with Crypto
  • Bancontact
  • Blik
  • EPS
  • iDeal
  • MyBank
  • Przelewy24
  • Trustly
  • Multibanco
  • Pay upon Invoice
  • OXXO

Filterable via woocommerce_paypal_payments_gateway_group_apm hook.

As in FeaturesDefinition, Payment Methods has also an eligibility service. This service is defined in PaymentMethodsEligibilityService.php

It creates different callbacks that unset the Payment methods based on the eligibility checks:

public function get_eligibility_checks(): array {
		return array(
			BancontactGateway::ID     => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			BlikGateway::ID           => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			EPSGateway::ID            => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			IDealGateway::ID          => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			MyBankGateway::ID         => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			P24Gateway::ID            => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			TrustlyGateway::ID        => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			MultibancoGateway::ID     => fn() => ! $this->is_mexico_merchant() && $this->is_apm_eligible,
			OXXO::ID                  => fn() => $this->is_mexico_merchant() && $this->is_apm_eligible,
			PWCGateway::ID            => fn() => $this->has_pwc_capability() && $this->is_apm_eligible,
			PayUponInvoiceGateway::ID => fn() => $this->merchant_country === 'DE',
			CreditCardGateway::ID     => fn() => $this->is_mexico_merchant() || $this->is_card_fields_supported(),
			CardButtonGateway::ID     => fn() => $this->is_mexico_merchant() || ! $this->is_card_fields_supported(),
			GooglePayGateway::ID      => fn() => $this->google_pay_available,
			ApplePayGateway::ID       => fn() => $this->apple_pay_available,
			AxoGateway::ID            => fn() => $this->dcc_product_status->is_active() && call_user_func( $this->axo_eligible ),
			'venmo'                   => fn() => $this->merchant_country === 'US',
		);
	}

Asset Management

Webpack Configuration

Each module with JavaScript assets includes a webpack.config.js:

const path = require('path');
const defaultConfig = require('@wordpress/scripts/config/webpack.config');

module.exports = {
    ...defaultConfig,
    entry: {
        'boot': path.resolve(process.cwd(), 'resources/js', 'boot.js'),
    },
    output: {
        path: path.resolve(process.cwd(), 'assets/js'),
        filename: '[name].js',
    },
};

Build Process

Assets are built using the shared configuration:

  • Individual builds: npm run build:modules:ppcp-{module-name}
  • Watch mode: npm run watch:modules:ppcp-{module-name} or npm run watch:modules (all modules)
  • All modules: npm run build:modules (parallel builds)

Asset Registration

Built assets are registered through module services and enqueued conditionally:

'asset.example-script' => static function( ContainerInterface $container ): Asset {
    return new Asset(
        'example-script',
        plugin_dir_url( __DIR__ ) . 'assets/js/example.js',
        array( 'wp-element' ), // dependencies
        '1.0.0'
    );
},

Extension Points

WordPress Hooks

The plugin provides numerous action and filter hooks:

// Allow modification of order request data
apply_filters( 'ppcp_create_order_request_body_data', $data );

// PayPal order creation notification
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );

// API cache clearing
do_action( 'woocommerce_paypal_payments_flush_api_cache' );

Module Filters

Modules can be modified via filters:

// Add or remove modules
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );

// Feature flag overrides
apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.applepay_enabled', $default );