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 matchingrequest_method- HTTP method checkingrequest_header- Request header validationrequest_param- Query/form parameter checkingcookie- Cookie existence/value checkingconstant- 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 statusis_singular- Singular post/page checkis_home- Home page checkis_archive- Archive page checkpost_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:
- Check if package is available (
is_available()) - Resolve dependencies
- Load required packages first
- Load the requested package
- 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}
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!
- Creating Custom Conditions - Extend package conditions
- Creating Custom Packages - Build your own packages
- Advanced Patterns - Advanced package techniques
- WordPress Integration - WordPress package details
Ready to create your own package? Continue to Creating Custom Packages to learn how to extend MilliRules with your own functionality.