Understanding the Package System

The package system is the architectural foundation of MilliRules, providing modularity, extensibility, and environment-specific functionality. This guide explains how packages work, their lifecycle, and how to leverage them effectively.

What is a Package?#Copied!

A package is a self-contained module that provides:

  • Conditions - Specific to the package's domain
  • Actions - Operations relevant to the package
  • Context - Data available during rule execution
  • Placeholder Resolvers - Custom placeholder categories
  • Dependencies - Other packages required for operation

Packages enable MilliRules to work across different environments (vanilla PHP, WordPress, custom frameworks) while maintaining a consistent API.

Package Architecture#Copied!

flowchart TB
    PM["PackageManager<br/>(Central Coordination)"]

    PM --> PHP["PHP Package"]
    PM --> WP["WP Package"]

    WP -->|"requires"| PHP

PackageManager#Copied!

The PackageManager is a static class that coordinates all packages:

  • Registers packages
  • Resolves dependencies
  • Loads packages in correct order
  • Aggregates context from all loaded packages
  • Routes rules to appropriate packages

Package Interface#Copied!

All packages implement PackageInterface:

 1namespace MilliRulesPackages;
 2
 3use MilliRulesContext;
 4
 5interface PackageInterface {
 6    public function get_name(): string;
 7    public function get_namespaces(): array;
 8    public function is_available(): bool;
 9    public function get_required_packages(): array;
10    public function register_providers(Context $context): void;
11    public function get_placeholder_resolver(Context $context);
12    public function register_rule(array $rule, array $metadata);
13    public function execute_rules(array $rules, Context $context): array;
14}

Built-in Packages#Copied!

PHP Package#Copied!

The PHP Package provides framework-agnostic HTTP and request handling.

Characteristics:

  • Always available in any PHP 7.4+ environment
  • No dependencies
  • Provides HTTP request conditions
  • Builds request-based context

Namespace: MilliRulesPackagesPHP

Conditions Provided:

  • request_url - URL/URI matching
  • request_method - HTTP method checking
  • request_header - Request header validation
  • request_param - Query/form parameter checking
  • cookie - Cookie existence/value checking
  • constant - PHP constant checking

Context Providers Registered:

 1// PHP package registers these context providers (loaded on-demand):
 2
 3'request' => [
 4    'method' => 'GET',
 5    'uri' => '/path',
 6    'scheme' => 'https',
 7    'host' => 'example.com',
 8    'path' => '/path',
 9    'query' => 'key=value',
10    'referer' => 'https://example.com/ref',
11    'user_agent' => 'Mozilla/5.0...',
12    'headers' => [...],
13    'ip' => '192.168.1.1',
14],
15
16'cookie' => [
17    // $_COOKIE data (loaded separately from request)
18],
19
20'param' => [
21    // array_merge($_GET, $_POST) (loaded separately from request)
22],

Availability Check:

 1public function is_available(): bool {
 2    return true; // Always available
 3}

WordPress Package#Copied!

The WordPress Package provides WordPress-specific functionality.

Characteristics:

  • Available only in WordPress environments
  • Depends on PHP package
  • Provides WordPress conditions
  • Integrates with WordPress hooks
  • Builds WordPress-specific context

Namespace: MilliRulesPackagesWP

Conditions Provided:

  • is_user_logged_in - User authentication status
  • is_singular - Singular post/page check
  • is_home - Home page check
  • is_archive - Archive page check
  • post_type - Post type validation

Context Providers Registered:

 1// WordPress package registers these context providers (loaded on-demand):
 2// Note: Uses flat structure, no 'wp' namespace
 3
 4'post' => [
 5    'id' => 123,
 6    'title' => 'My Post',
 7    'type' => 'post',
 8    'status' => 'publish',
 9    'author' => 1,
10    'parent' => 0,
11    'name' => 'my-post',
12    // ... normalized post fields
13],
14
15'user' => [
16    'id' => 1,
17    'login' => 'admin',
18    'email' => '',
19    'display_name' => 'Administrator',
20    'roles' => ['administrator'],
21    'logged_in' => true,
22],
23
24'query' => [
25    'post_type' => 'post',
26    'paged' => 1,
27    's' => '',
28    'm' => '',
29    // ... WordPress query variables from $wp_query->query_vars
30],
31
32'term' => [
33    // Taxonomy term data (when applicable)
34],

Availability Check:

 1public function is_available(): bool {
 2    return function_exists('add_action'); // Detects WordPress
 3}

Dependencies:

 1public function get_required_packages(): array {
 2    return ['PHP']; // Requires PHP package
 3}

Package Lifecycle#Copied!

1. Registration#Copied!

Packages are registered with PackageManager:

 1use MilliRulesPackageManager;
 2use MilliRulesPackagesPHPPHPPackage;
 3use MilliRulesPackagesWPWordPressPackage;
 4
 5// Manual registration
 6$php_package = new PHPPackage();
 7$wp_package = new WordPressPackage();
 8
 9PackageManager::register_package($php_package);
10PackageManager::register_package($wp_package);

2. Loading#Copied!

Packages are loaded during initialization:

 1use MilliRulesMilliRules;
 2
 3// Auto-loads available packages
 4MilliRules::init();
 5
 6// Or specify packages explicitly
 7MilliRules::init(['PHP', 'WP']);

Loading Process:

  1. Check if package is available (is_available())
  2. Resolve dependencies
  3. Load required packages first
  4. Load the requested package
  5. Detect circular dependencies

3. Context Provider Registration#Copied!

Packages register context providers that load data on-demand:

 1use MilliRulesContext;
 2
 3// During initialization, packages register their providers
 4class PHPPackage extends BasePackage {
 5    public function register_providers(Context $context): void {
 6        // Register request provider (loads only when needed)
 7        $context->register_provider('request', function() {
 8            return [/* request data */];
 9        });
10
11        // Register cookie provider (loads only when needed)
12        $context->register_provider('cookie', function() {
13            return $_COOKIE;
14        });
15
16        // Register param provider (loads only when needed)
17        $context->register_provider('param', function() {
18            return array_merge($_GET, $_POST);
19        });
20    }
21}
22
23// Context sections load lazily when accessed:
24// - 'request' loads when $context->get('request.uri') is called
25// - 'cookie' loads when $context->get('cookie.session_id') is called
26// - 'param' loads when $context->get('param.action') is called

4. Rule Execution#Copied!

Rules execute using context from loaded packages:

 1$result = MilliRules::execute_rules();
 2
 3/*
 4[
 5    'rules_processed' => 10,
 6    'rules_skipped' => 2,     // Skipped if package unavailable
 7    'rules_matched' => 5,
 8    'actions_executed' => 12,
 9    'context' => [...],
10]
11*/

Package Dependencies#Copied!

Packages can depend on other packages using get_required_packages().

Declaring Dependencies#Copied!

 1namespace MyPluginPackages;
 2
 3use MilliRulesPackagesBasePackage;
 4
 5class CustomPackage extends BasePackage {
 6    public function get_name(): string {
 7        return 'Custom';
 8    }
 9
10    public function get_required_packages(): array {
11        return ['PHP', 'WP']; // Requires both PHP and WordPress
12    }
13
14    // ... other methods
15}

Dependency Resolution#Copied!

PackageManager automatically resolves dependencies:

 1// Request to load Custom package
 2MilliRules::init(['Custom']);
 3
 4// Automatic resolution:
 5// 1. Custom requires ['PHP', 'WP']
 6// 2. WP requires ['PHP']
 7// 3. Load order: PHP → WP → Custom

Circular Dependency Detection#Copied!

 1// Package A requires Package B
 2// Package B requires Package A
 3//
 4// Error: Circular dependency detected
 5
 6try {
 7    MilliRules::init(['A']);
 8} catch (Exception $e) {
 9    error_log('Circular dependency: ' . $e->getMessage());
10}
Warning: Design your package dependencies carefully to avoid circular dependencies. Each package should have a clear, unidirectional dependency relationship.

Package Namespaces#Copied!

Packages provide namespaces for conditions and actions.

Namespace Registration#Copied!

 1public function get_namespaces(): array {
 2    return [
 3        'MilliRulesPackagesPHPConditions',
 4        'MilliRulesPackagesPHPActions',
 5    ];
 6}

Namespace Resolution#Copied!

When a condition is used, MilliRules finds the appropriate class:

 1// User writes:
 2->request_url('/api/*')
 3
 4// MilliRules resolves:
 5// 1. Converts 'request_url' to 'RequestUrl'
 6// 2. Searches registered namespaces
 7// 3. Finds: MilliRulesPackagesPHPConditionsRequestUrlCondition
 8// 4. Instantiates class with config and context

Longest Match Algorithm:

Multiple packages can provide overlapping namespaces. MilliRules uses the longest matching namespace:

 1// Registered namespaces:
 2// - 'MyPluginConditions'
 3// - 'MyPluginConditionsAdvanced'
 4
 5// Looking for: MyPluginConditionsAdvancedCustomCondition
 6//
 7// Uses: 'MyPluginConditionsAdvanced' (longest match)

Package Filtering#Copied!

You can filter which packages are used during execution.

Filter by Package Name#Copied!

 1// Execute only with PHP package (skip WordPress)
 2$result = MilliRules::execute_rules(['PHP']);
 3
 4// Execute only with WordPress package
 5$result = MilliRules::execute_rules(['WP']);
 6
 7// Execute with specific packages
 8$result = MilliRules::execute_rules(['PHP', 'Custom']);

Use Cases#Copied!

Early execution (before WordPress loads):

 1// In mu-plugins or early hook
 2MilliRules::init(['PHP']);
 3
 4// Execute only PHP rules
 5$result = MilliRules::execute_rules(['PHP']);

Testing specific packages:

 1// Test only PHP-related rules
 2$php_result = MilliRules::execute_rules(['PHP']);
 3
 4// Test only WordPress-related rules
 5$wp_result = MilliRules::execute_rules(['WP']);

Package Context#Copied!

Accessing Package Context#Copied!

 1use MilliRulesContext;
 2
 3Rules::register_action('context_aware', function($args, Context $context) {
 4    // Check which providers are available
 5    // Note: Providers are loaded on-demand, not preloaded
 6
 7    // Access request data (triggers lazy loading if not already loaded)
 8    if ($context->has('request.uri')) {
 9        $url = $context->get('request.uri', '');
10        error_log("Request URL: {$url}");
11    }
12
13    // Access WordPress user data (triggers lazy loading if not already loaded)
14    $context->load('user');
15    if ($context->has('user.id')) {
16        $user_id = $context->get('user.id', 0);
17        error_log("WordPress user: {$user_id}");
18    }
19});

Conditional Package Features#Copied!

 1Rules::create('flexible_rule')
 2    ->when_any()  // Use OR logic
 3        // PHP condition (always works)
 4        ->request_url('/api/*')
 5
 6        // WordPress condition (works if WP package loaded)
 7        ->is_user_logged_in()
 8    ->then()
 9        ->custom('context_aware')  // Action adapts to available packages
10    ->register();

Best Practices#Copied!

1. Check Package Availability#Copied!

 1use MilliRulesContext;
 2
 3// ✅ Good - check before using package-specific features
 4Rules::register_action('safe_wp_action', function($args, Context $context) {
 5    $context->load('user');
 6
 7    if (!$context->has('user.id')) {
 8        error_log('WordPress user context not available');
 9        return;
10    }
11
12    $user_id = $context->get('user.id', 0);
13    // Use WordPress features
14});
15
16// ❌ Bad - assumes WordPress context is always available
17Rules::register_action('unsafe_action', function($args, Context $context) {
18    $context->load('user');
19    $user_id = $context->get('user.id'); // May return null if not available!
20});

2. Declare Dependencies Explicitly#Copied!

 1// ✅ Good - explicit dependencies
 2class MyPackage extends BasePackage {
 3    public function get_required_packages(): array {
 4        return ['PHP', 'WP'];
 5    }
 6}
 7
 8// ❌ Bad - undeclared dependencies
 9class MyPackage extends BasePackage {
10    public function register_providers(Context $context): void {
11        // Uses WordPress functions without declaring dependency!
12        $context->register_provider('data', function() {
13            return ['option' => get_option('my_option')];
14        });
15    }
16}

3. Use Appropriate Package for Rules#Copied!

 1// ✅ Good - PHP rule for PHP conditions
 2Rules::create('cache_check', 'php')
 3    ->when()->request_url('/api/*')
 4    ->then()->custom('check_cache')
 5    ->register();
 6
 7// ✅ Good - WordPress rule for WordPress conditions
 8Rules::create('admin_notice', 'wp')
 9    ->when()->is_user_logged_in()
10    ->then()->custom('show_notice')
11    ->register();
12
13// ❌ Unclear - mixed without explicit type
14Rules::create('mixed_rule')  // Type will be auto-detected
15    ->when()
16        ->request_url('/api/*')
17        ->is_user_logged_in()
18    ->then()->custom('action')
19    ->register();

4. Handle Package Unavailability Gracefully#Copied!

 1use MilliRulesPackageManager;
 2
 3// Check if package is loaded
 4if (PackageManager::is_package_loaded('WP')) {
 5    // Create WordPress-specific rules
 6    Rules::create('wp_rule')
 7        ->when()->is_user_logged_in()
 8        ->then()->custom('wp_action')
 9        ->register();
10}

Package Information#Copied!

Get Loaded Packages#Copied!

 1use MilliRulesMilliRules;
 2
 3// Get package names
 4$package_names = MilliRules::get_loaded_packages();
 5// ['PHP', 'WP']
 6
 7error_log('Loaded packages: ' . implode(', ', $package_names));

Check Specific Package#Copied!

 1use MilliRulesPackageManager;
 2
 3if (PackageManager::is_package_loaded('WP')) {
 4    error_log('WordPress package is loaded');
 5}
 6
 7if (PackageManager::has_packages()) {
 8    error_log('At least one package is loaded');
 9}

Get Package Instance#Copied!

 1use MilliRulesPackageManager;
 2
 3$php_package = PackageManager::get_package('PHP');
 4
 5if ($php_package) {
 6    $namespaces = $php_package->get_namespaces();
 7    error_log('PHP package namespaces: ' . print_r($namespaces, true));
 8}

Common Patterns#Copied!

1. Environment-Specific Loading#Copied!

 1// Load different packages based on environment
 2if (defined('WP_CLI') && WP_CLI) {
 3    // CLI environment - PHP only
 4    MilliRules::init(['PHP']);
 5} elseif (defined('DOING_CRON') && DOING_CRON) {
 6    // Cron environment - PHP + WordPress
 7    MilliRules::init(['PHP', 'WP']);
 8} else {
 9    // Normal request - all packages
10    MilliRules::init();
11}

2. Progressive Enhancement#Copied!

 1// Base rules with PHP package
 2MilliRules::init(['PHP']);
 3
 4Rules::create('base_security')
 5    ->when()->request_url('*')
 6    ->then()->custom('basic_security')
 7    ->register();
 8
 9// Enhance with WordPress if available
10if (PackageManager::is_package_loaded('WP')) {
11    Rules::create('wp_security')
12        ->when()->is_user_logged_in()
13        ->then()->custom('enhanced_security')
14        ->register();
15}

3. Package-Specific Rules#Copied!

 1// Group rules by package
 2$php_rules = [
 3    'api_cache', 'request_logging', 'header_security'
 4];
 5
 6$wp_rules = [
 7    'admin_notices', 'user_redirects', 'content_filtering'
 8];
 9
10// Register PHP rules
11foreach ($php_rules as $rule_id) {
12    Rules::create($rule_id, 'php')
13        ->when()->request_url('*')
14        ->then()->custom($rule_id . '_action')
15        ->register();
16}
17
18// Register WordPress rules (if available)
19if (PackageManager::is_package_loaded('WP')) {
20    foreach ($wp_rules as $rule_id) {
21        Rules::create($rule_id, 'wp')
22            ->when()->is_user_logged_in()
23            ->then()->custom($rule_id . '_action')
24            ->register();
25    }
26}

Troubleshooting#Copied!

Package Not Loading#Copied!

Check availability:

 1$package = PackageManager::get_package('WP');
 2if (!$package) {
 3    error_log('Package not registered');
 4} elseif (!$package->is_available()) {
 5    error_log('Package not available in this environment');
 6}

Dependency Issues#Copied!

Check dependencies:

 1$package = PackageManager::get_package('Custom');
 2$required = $package->get_required_packages();
 3
 4foreach ($required as $dep) {
 5    if (!PackageManager::is_package_loaded($dep)) {
 6        error_log("Missing dependency: {$dep}");
 7    }
 8}

Context Missing Data#Copied!

Verify providers are registered:

 1use MilliRulesContext;
 2
 3$context = new Context();
 4MilliRules::init(); // Registers all providers
 5
 6// Check if a specific provider is available
 7$context->load('user');
 8if (!$context->has('user.id')) {
 9    error_log('WordPress user context not available');
10}
11
12// Export context to see all loaded sections
13$array = $context->to_array();
14error_log('Available context keys: ' . implode(', ', array_keys($array)));

Next Steps#Copied!


Ready to create your own package? Continue to Creating Custom Packages to learn how to extend MilliRules with your own functionality.