Creating Custom Packages
Custom packages are the ultimate way to extend MilliRules. They let you bundle conditions, actions, context data, and placeholder resolvers into reusable, self-contained modules. This guide shows you how to create packages that integrate seamlessly with MilliRules.
Why Create Custom Packages?#Copied!
Custom packages enable you to:
- Bundle related functionality - Group conditions and actions by domain
- Provide context data - Make data available to all rules
- Add placeholder resolvers - Enable dynamic values in rules
- Integrate third-party services - Connect external APIs and systems
- Create reusable libraries - Share packages across projects
- Maintain clean separation - Keep code organized by concern
Package Structure#Copied!
A complete custom package includes:
MyCustomPackage/
├── MyCustomPackage.php # Package class (implements PackageInterface)
├── Conditions/ # Condition classes
│ ├── CustomCondition1.php
│ └── CustomCondition2.php
├── Actions/ # Action classes
│ ├── CustomAction1.php
│ └── CustomAction2.php
└── PlaceholderResolver.php # Optional placeholder resolver
Implementing PackageInterface#Copied!
All packages must 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}
Basic Custom Package#Copied!
Minimal Package Implementation#Copied!
1namespace MyPluginPackages;
2
3use MilliRulesPackagesBasePackage;
4
5class MyCustomPackage extends BasePackage {
6 /**
7 * Package name (must be unique)
8 */
9 public function get_name(): string {
10 return 'MyCustom';
11 }
12
13 /**
14 * Namespaces for conditions and actions
15 */
16 public function get_namespaces(): array {
17 return [
18 'MyPluginPackagesMyCustomConditions',
19 'MyPluginPackagesMyCustomActions',
20 ];
21 }
22
23 /**
24 * Check if package can be used in current environment
25 */
26 public function is_available(): bool {
27 // Example: check if required functions exist
28 return function_exists('my_required_function');
29 }
30}
Registering the Package#Copied!
1use MilliRulesMilliRules;
2use MyPluginPackagesMyCustomPackage;
3
4// Register custom package
5$custom_package = new MyCustomPackage();
6MilliRules::init(null, [$custom_package]);
7
8// Or let MilliRules auto-discover (if registered globally)
9MilliRules::init();
Registering Context Providers#Copied!
Packages register context providers that load data lazily when needed. This improves performance by only loading data that rules actually use.
Simple Context Provider#Copied!
1use MilliRulesContext;
2
3public function register_providers(Context $context): void {
4 // Register a simple provider that loads on-demand
5 $context->register_provider('my_custom', function() {
6 return [
7 'my_custom' => [
8 'value1' => get_option('my_option_1'),
9 'value2' => get_option('my_option_2'),
10 'timestamp' => time(),
11 ],
12 ];
13 });
14}
Benefit: Data is only retrieved when $context->get('my_custom.value1') is called.
Dynamic Context Provider#Copied!
1use MilliRulesContext;
2
3public function register_providers(Context $context): void {
4 // Register provider that loads complex data on-demand
5 $context->register_provider('my_custom', function() {
6 $user_data = [];
7 if (is_user_logged_in()) {
8 $user_id = get_current_user_id();
9 $user_data = [
10 'id' => $user_id,
11 'meta' => get_user_meta($user_id),
12 'purchases' => $this->get_user_purchases($user_id),
13 ];
14 }
15
16 return [
17 'my_custom' => [
18 'user' => $user_data,
19 'site' => [
20 'name' => get_bloginfo('name'),
21 'url' => home_url(),
22 ],
23 'stats' => [
24 'total_posts' => wp_count_posts()->publish,
25 'total_users' => count_users()['total_users'],
26 ],
27 ],
28 ];
29 });
30}
31
32private function get_user_purchases($user_id) {
33 global $wpdb;
34 return $wpdb->get_results($wpdb->prepare(
35 "SELECT * FROM {$wpdb->prefix}purchases WHERE user_id = %d",
36 $user_id
37 ));
38}
Benefit: Expensive database queries and WordPress functions only execute when needed.
Context Provider with External API#Copied!
1use MilliRulesContext;
2
3public function register_providers(Context $context): void {
4 // Register provider that loads API data on-demand
5 $context->register_provider('my_custom', function() {
6 // Cache expensive API calls
7 $api_data = get_transient('my_custom_api_data');
8
9 if ($api_data === false) {
10 $response = wp_remote_get('https://api.example.com/data', [
11 'timeout' => 10,
12 'headers' => ['Authorization' => 'Bearer ' . $this->get_api_key()],
13 ]);
14
15 if (!is_wp_error($response)) {
16 $api_data = json_decode(wp_remote_retrieve_body($response), true);
17 set_transient('my_custom_api_data', $api_data, 300); // Cache 5 minutes
18 } else {
19 $api_data = [];
20 }
21 }
22
23 return [
24 'my_custom' => [
25 'api' => $api_data,
26 'cached_at' => get_transient('my_custom_api_data_time') ?: time(),
27 ],
28 ];
29 });
30}
Benefit: API calls only execute if a rule actually needs the API data.
Creating Package Conditions#Copied!
Package conditions extend the available condition types.
Simple Package Condition#Copied!
1namespace MyPluginPackagesMyCustomConditions;
2
3use MilliRulesConditionsBaseCondition;
4use MilliRulesContext;
5
6class UserLevelCondition extends BaseCondition {
7 protected function get_actual_value(Context $context) {
8 $context->load('user');
9 $user_id = $context->get('user.id', 0);
10
11 if (!$user_id) {
12 return 0;
13 }
14
15 // Get custom user level
16 return (int) get_user_meta($user_id, 'user_level', true);
17 }
18
19 public function get_type(): string {
20 return 'user_level';
21 }
22}
Usage:
1Rules::create('premium_users')
2 ->when()
3 ->custom('user_level', ['value' => 5, 'operator' => '>='])
4 ->then()
5 ->custom('show_premium_content')
6 ->register();
Condition Using Package Context#Copied!
1namespace MyPluginPackagesMyCustomConditions;
2
3use MilliRulesConditionsBaseCondition;
4use MilliRulesContext;
5
6class PurchaseCountCondition extends BaseCondition {
7 protected function get_actual_value(Context $context) {
8 // Load package context data
9 $context->load('my_custom');
10
11 // Use data from package context
12 $purchases = $context->get('my_custom.user.purchases', []);
13 return count($purchases);
14 }
15
16 public function get_type(): string {
17 return 'purchase_count';
18 }
19}
Creating Package Actions#Copied!
Package actions provide functionality specific to your package's domain.
Simple Package Action#Copied!
1namespace MyPluginPackagesMyCustomActions;
2
3use MilliRulesActionsBaseAction;
4use MilliRulesContext;
5
6class UpdateUserLevelAction extends BaseAction {
7 public function execute(Context $context): void {
8 $context->load('user');
9 $user_id = $context->get('user.id', 0);
10 $level = $this->config['level'] ?? 1;
11
12 if (!$user_id) {
13 error_log('UpdateUserLevelAction: No user logged in');
14 return;
15 }
16
17 update_user_meta($user_id, 'user_level', $level);
18 error_log("Updated user {$user_id} to level {$level}");
19 }
20
21 public function get_type(): string {
22 return 'update_user_level';
23 }
24}
Action with Placeholder Support#Copied!
1namespace MyPluginPackagesMyCustomActions;
2
3use MilliRulesActionsBaseAction;
4use MilliRulesContext;
5
6class SendNotificationAction extends BaseAction {
7 public function execute(Context $context): void {
8 // Resolve placeholders
9 $message = $this->resolve_value($this->config['message'] ?? '');
10 $recipient = $this->resolve_value($this->config['to'] ?? '');
11
12 // Send notification
13 $this->send_notification($recipient, $message);
14 }
15
16 private function send_notification($to, $message) {
17 // Implementation...
18 wp_mail($to, 'Notification', $message);
19 }
20
21 public function get_type(): string {
22 return 'send_notification';
23 }
24}
Adding Placeholder Resolvers#Copied!
Placeholder resolvers enable dynamic values in rules.
Basic Placeholder Resolver#Copied!
1use MilliRulesContext;
2
3public function get_placeholder_resolver(Context $context) {
4 return function($placeholder_parts) use ($context) {
5 // $placeholder_parts = ['my_custom', 'category', 'key']
6 // From placeholder: {my_custom:category:key}
7
8 if ($placeholder_parts[0] !== 'my_custom') {
9 return null; // Not for this package
10 }
11
12 // Load context data if not already loaded
13 $context->load('my_custom');
14
15 // Convert parts to dot notation path
16 $path = implode('.', $placeholder_parts);
17
18 // Get value from context
19 $value = $context->get($path, '');
20
21 return is_scalar($value) ? (string) $value : '';
22 };
23}
Usage:
1Rules::create('use_custom_placeholder')
2 ->when()->request_url('/api/*')
3 ->then()
4 ->custom('log', [
5 'message' => 'Site: {my_custom:site:name}, Users: {my_custom:stats:total_users}'
6 ])
7 ->register();
Advanced Placeholder Resolver#Copied!
1use MilliRulesContext;
2
3public function get_placeholder_resolver(Context $context) {
4 return function($placeholder_parts) use ($context) {
5 if ($placeholder_parts[0] !== 'my_custom') {
6 return null;
7 }
8
9 $category = $placeholder_parts[1] ?? '';
10 $key = $placeholder_parts[2] ?? '';
11
12 // Load context data once
13 $context->load('my_custom');
14
15 switch ($category) {
16 case 'user':
17 return $this->resolve_user_placeholder($context, $key);
18
19 case 'product':
20 return $this->resolve_product_placeholder($context, $key);
21
22 case 'setting':
23 return $this->resolve_setting_placeholder($context, $key);
24
25 default:
26 return '';
27 }
28 };
29}
30
31private function resolve_user_placeholder(Context $context, $key) {
32 switch ($key) {
33 case 'level':
34 return $context->get('my_custom.user.level', '0');
35 case 'points':
36 return $context->get('my_custom.user.points', '0');
37 default:
38 return $context->get("my_custom.user.{$key}", '');
39 }
40}
Declaring Package Dependencies#Copied!
Packages can require other packages.
Simple Dependency#Copied!
1public function get_required_packages(): array {
2 return ['PHP']; // Requires PHP package
3}
Multiple Dependencies#Copied!
1public function get_required_packages(): array {
2 return ['PHP', 'WP']; // Requires both PHP and WordPress packages
3}
Complete Custom Package Example#Copied!
Here's a complete example of a custom package for a membership system:
1namespace MyPluginPackages;
2
3use MilliRulesPackagesBasePackage;
4
5class MembershipPackage extends BasePackage {
6 public function get_name(): string {
7 return 'Membership';
8 }
9
10 public function get_namespaces(): array {
11 return [
12 'MyPluginPackagesMembershipConditions',
13 'MyPluginPackagesMembershipActions',
14 ];
15 }
16
17 public function is_available(): bool {
18 // Check if membership system is active
19 return class_exists('My_Membership_System');
20 }
21
22 public function get_required_packages(): array {
23 return ['PHP', 'WP']; // Requires both PHP and WordPress
24 }
25
26 public function register_providers(Context $context): void {
27 // Register membership provider (loads on-demand)
28 $context->register_provider('membership', function() {
29 $user_id = get_current_user_id();
30
31 $membership_data = [];
32 if ($user_id) {
33 $membership_data = [
34 'level' => get_user_meta($user_id, 'membership_level', true) ?: 'free',
35 'status' => get_user_meta($user_id, 'membership_status', true) ?: 'inactive',
36 'expiry' => get_user_meta($user_id, 'membership_expiry', true) ?: 0,
37 'features' => $this->get_user_features($user_id),
38 ];
39 }
40
41 return [
42 'membership' => [
43 'user' => $membership_data,
44 'levels' => $this->get_available_levels(),
45 'features' => $this->get_all_features(),
46 ],
47 ];
48 });
49 }
50
51 public function get_placeholder_resolver(Context $context) {
52 return function($parts) use ($context) {
53 if ($parts[0] !== 'membership') {
54 return null;
55 }
56
57 // Load membership context if not already loaded
58 $context->load('membership');
59
60 $category = $parts[1] ?? '';
61 $key = $parts[2] ?? '';
62
63 if ($category === 'user') {
64 return $context->get("membership.user.{$key}", '');
65 }
66
67 return '';
68 };
69 }
70
71 private function get_user_features($user_id) {
72 // Get features available to user
73 return ['feature1', 'feature2'];
74 }
75
76 private function get_available_levels() {
77 return ['free', 'basic', 'premium', 'enterprise'];
78 }
79
80 private function get_all_features() {
81 return ['feature1', 'feature2', 'feature3'];
82 }
83}
Membership Condition Example:
1namespace MyPluginPackagesMembershipConditions;
2
3use MilliRulesConditionsBaseCondition;
4use MilliRulesContext;
5
6class MembershipLevelCondition extends BaseCondition {
7 protected function get_actual_value(Context $context) {
8 $context->load('membership');
9 return $context->get('membership.user.level', 'free');
10 }
11
12 public function get_type(): string {
13 return 'membership_level';
14 }
15}
Membership Action Example:
1namespace MyPluginPackagesMembershipActions;
2
3use MilliRulesActionsBaseAction;
4use MilliRulesContext;
5
6class UpgradeMembershipAction extends BaseAction {
7 public function execute(Context $context): void {
8 $context->load('user');
9 $user_id = $context->get('user.id', 0);
10 $new_level = $this->config['level'] ?? 'basic';
11
12 if (!$user_id) {
13 return;
14 }
15
16 update_user_meta($user_id, 'membership_level', $new_level);
17 update_user_meta($user_id, 'membership_status', 'active');
18
19 // Resolve message with placeholders
20 $message = $this->resolve_value(
21 $this->config['message'] ?? 'Upgraded to {membership:user:level}'
22 );
23
24 error_log($message);
25 }
26
27 public function get_type(): string {
28 return 'upgrade_membership';
29 }
30}
Using the Custom Package:
1use MilliRulesMilliRules;
2use MyPluginPackagesMembershipPackage;
3
4// Initialize with custom package
5$membership_package = new MembershipPackage();
6MilliRules::init(null, [$membership_package]);
7
8// Create rule using package conditions and actions
9Rules::create('auto_upgrade_frequent_buyers')
10 ->when()
11 ->is_user_logged_in() // WP condition
12 ->custom('membership_level', ['value' => 'free']) // Membership condition
13 ->custom('purchase_count', ['value' => 10, 'operator' => '>='])
14 ->then()
15 ->custom('upgrade_membership', [
16 'level' => 'premium',
17 'message' => 'Congratulations! Upgraded to premium membership.'
18 ])
19 ->register();
Real-World Example: Acorn MilliRules#Copied!
The Acorn MilliRules package is a real-world custom package that extends MilliRules for the Roots Acorn framework. It's a good reference for how to structure a production package.
Package Class#Copied!
The Acorn package registers route-aware conditions, HTTP response actions, and a route context provider:
1namespace MilliRulesAcornPackagesAcorn;
2
3use MilliRulesAcornPackagesAcornContextsRoute;
4use MilliRulesPackagesBasePackage;
5
6class Package extends BasePackage
7{
8 public function get_name(): string
9 {
10 return 'Acorn';
11 }
12
13 public function get_namespaces(): array
14 {
15 return [
16 'MilliRules\Acorn\Packages\Acorn\Actions',
17 'MilliRules\Acorn\Packages\Acorn\Conditions',
18 'MilliRules\Acorn\Packages\Acorn\Contexts',
19 ];
20 }
21
22 public function is_available(): bool
23 {
24 return function_exists('app');
25 }
26
27 public function get_required_packages(): array
28 {
29 return ['PHP'];
30 }
31}
What It Provides#Copied!
| Component | Description |
|---|---|
| Conditions | RouteName, RouteParameter, RouteController |
| Actions | Redirect, SetHeader |
| Context | Route metadata (name, parameters, controller, URI, middleware) |
| Auto-discovery | Rule classes in app/Rules/ are registered automatically |
| Artisan commands | 8 CLI commands to list, inspect, and scaffold rules |
Usage Example#Copied!
1// app/Rules/RedirectLegacyDocs.php
2namespace AppRules;
3
4use MilliRulesRules;
5
6class RedirectLegacyDocs
7{
8 public function register(): void
9 {
10 Rules::create('redirect_legacy_docs', 'Acorn')
11 ->when()
12 ->routeName('docs.*')
13 ->routeParameter('product', ['value' => 'old-product'])
14 ->then()
15 ->redirect('/docs/new-product/', ['status' => 301])
16 ->register();
17 }
18}
For full documentation, see the Acorn MilliRules docs.
Best Practices#Copied!
1. Use Descriptive Package Names#Copied!
1// ✅ Good - clear and specific
2public function get_name(): string {
3 return 'WooCommerce';
4}
5
6// ❌ Bad - vague or generic
7public function get_name(): string {
8 return 'Custom';
9}
2. Validate Environment in is_available()#Copied!
1// ✅ Good - comprehensive checks
2public function is_available(): bool {
3 return class_exists('WooCommerce')
4 && function_exists('wc_get_product')
5 && defined('WC_VERSION');
6}
7
8// ❌ Bad - minimal checking
9public function is_available(): bool {
10 return true;
11}
3. Cache Expensive Context Data#Copied!
1use MilliRulesContext;
2
3// ✅ Good - caches API calls in lazy provider
4public function register_providers(Context $context): void {
5 $context->register_provider('my_package', function() {
6 $data = get_transient('my_package_context');
7
8 if ($data === false) {
9 $data = $this->fetch_expensive_data();
10 set_transient('my_package_context', $data, 300);
11 }
12
13 return ['my_package' => $data];
14 });
15}
Note: With lazy loading, this expensive data is only fetched when a rule actually needs it!
4. Document Your Package#Copied!
1/**
2 * Membership Package
3 *
4 * Provides membership-related conditions and actions.
5 *
6 * Conditions:
7 * - membership_level: Check user's membership level
8 * - membership_status: Check membership status
9 * - has_feature: Check if user has access to feature
10 *
11 * Actions:
12 * - upgrade_membership: Upgrade user to new level
13 * - grant_feature: Grant feature access
14 * - send_membership_email: Send membership-related email
15 *
16 * Context:
17 * - membership.user.level: User's membership level
18 * - membership.user.status: Membership status
19 * - membership.user.features: Available features
20 *
21 * Placeholders:
22 * - {membership:user:level}: User's membership level
23 * - {membership:user:status}: Membership status
24 */
25class MembershipPackage extends BasePackage {
26 // ...
27}
Common Pitfalls#Copied!
1. Circular Dependencies#Copied!
1// ❌ Wrong - circular dependency
2class PackageA extends BasePackage {
3 public function get_required_packages(): array {
4 return ['PackageB']; // A requires B
5 }
6}
7
8class PackageB extends BasePackage {
9 public function get_required_packages(): array {
10 return ['PackageA']; // B requires A - CIRCULAR!
11 }
12}
2. Accessing Unavailable Context#Copied!
1use MilliRulesContext;
2
3// ❌ Wrong - doesn't check availability
4protected function get_actual_value(Context $context) {
5 $context->load('user');
6 return $context->get('user.id'); // May return null if not available!
7}
8
9// ✅ Correct - provides default value
10protected function get_actual_value(Context $context) {
11 $context->load('user');
12 return $context->get('user.id', 0);
13}
3. Expensive Provider Registration#Copied!
1use MilliRulesContext;
2
3// ❌ Wrong - executes expensive operation during registration
4public function register_providers(Context $context): void {
5 $data = expensive_api_call(); // Runs on every request!
6 $context->register_provider('my_package', function() use ($data) {
7 return ['my_package' => $data];
8 });
9}
10
11// ✅ Correct - expensive operation runs only when provider loads
12public function register_providers(Context $context): void {
13 $context->register_provider('my_package', function() {
14 $data = wp_cache_get('my_package_data', 'my_group');
15
16 if ($data === false) {
17 $data = expensive_api_call(); // Only runs when needed!
18 wp_cache_set('my_package_data', $data, 'my_group', 300);
19 }
20
21 return ['my_package' => $data];
22 });
23}
Next Steps#Copied!
- Advanced Patterns - Advanced package techniques
- WordPress Integration - WordPress-specific patterns
- API Reference - Complete API documentation
- Real-World Examples - See complete package implementations
Ready for advanced techniques? Continue to Advanced Patterns to learn optimization strategies and advanced rule patterns.