|  | 
|  | 1 | +<?php | 
|  | 2 | +/** | 
|  | 3 | + * WP Restaurant Manager - Flexible Approach | 
|  | 4 | + * | 
|  | 5 | + * This file demonstrates the "Flexible Approach" using an abstract base class for abilities. | 
|  | 6 | + * This object-oriented pattern allows for shared logic (permissions, rate-limiting, logging) | 
|  | 7 | + * to be defined once and inherited by all abilities, adhering to DRY principles. | 
|  | 8 | + * | 
|  | 9 | + * @package WP_Restaurant_Manager | 
|  | 10 | + */ | 
|  | 11 | + | 
|  | 12 | +namespace WP_Restaurant_Manager\Flexible; | 
|  | 13 | + | 
|  | 14 | +/** | 
|  | 15 | + * Abstract base class for all restaurant abilities. | 
|  | 16 | + * | 
|  | 17 | + * This class contains shared logic that ALL restaurant abilities need. | 
|  | 18 | + * By defining shared permission checks, pre-execution hooks (logging, rate-limiting), | 
|  | 19 | + * and post-execution hooks here, we avoid duplicating code in every ability. | 
|  | 20 | + */ | 
|  | 21 | +abstract class Restaurant_Ability extends \WP_Ability { | 
|  | 22 | + | 
|  | 23 | +	/** | 
|  | 24 | +	 * SHARED PERMISSION LOGIC | 
|  | 25 | +	 * | 
|  | 26 | +	 * Defines a default permission callback for most restaurant abilities. | 
|  | 27 | +	 * This single method includes checks for user roles, restaurant status, and rate limiting. | 
|  | 28 | +	 * It is written ONCE and inherited by all child abilities unless they need to override it. | 
|  | 29 | +	 */ | 
|  | 30 | +	protected function get_permission_callback(): callable { | 
|  | 31 | +		return function ( $input ) { | 
|  | 32 | +			// Check 1: User has the basic role required. | 
|  | 33 | +			if ( ! current_user_can( 'edit_posts' ) ) { | 
|  | 34 | +				return new \WP_Error( 'permission_denied', 'You do not have permission to manage the restaurant.' ); | 
|  | 35 | +			} | 
|  | 36 | + | 
|  | 37 | +			// Check 2: Shared business logic (e.g., restaurant is active). | 
|  | 38 | +			if ( ! $this->is_restaurant_active() ) { | 
|  | 39 | +				return new \WP_Error( 'restaurant_closed', 'Restaurant management is currently disabled.' ); | 
|  | 40 | +			} | 
|  | 41 | + | 
|  | 42 | +			// Check 3: Shared technical logic (e.g., rate limiting to prevent API abuse). | 
|  | 43 | +			if ( ! $this->check_rate_limit() ) { | 
|  | 44 | +				return new \WP_Error( 'rate_limit', 'Too many requests. Please wait.' ); | 
|  | 45 | +			} | 
|  | 46 | + | 
|  | 47 | +			return true; | 
|  | 48 | +		}; | 
|  | 49 | +	} | 
|  | 50 | + | 
|  | 51 | +	/** | 
|  | 52 | +	 * SHARED EXECUTION WRAPPER | 
|  | 53 | +	 * | 
|  | 54 | +	 * This method acts as a wrapper around the specific logic of each ability. | 
|  | 55 | +	 * It ensures that shared tasks like logging and subscription checks are ALWAYS run. | 
|  | 56 | +	 * This is a powerful pattern for enforcing business rules across all abilities. | 
|  | 57 | +	 */ | 
|  | 58 | +	protected function execute( $input ) { | 
|  | 59 | +		// Shared Pre-Execution Hook 1: Log every ability usage for analytics. | 
|  | 60 | +		$this->log_ability_usage( $input ); | 
|  | 61 | + | 
|  | 62 | +		// Shared Pre-Execution Hook 2: Check subscription status before running expensive AI tasks. | 
|  | 63 | +		if ( ! $this->has_active_subscription() ) { | 
|  | 64 | +			return new \WP_Error( 'subscription_required', 'An active subscription is required to use this feature.' ); | 
|  | 65 | +		} | 
|  | 66 | + | 
|  | 67 | +		// Call the specific ability's implementation. | 
|  | 68 | +		$result = $this->execute_ability( $input ); | 
|  | 69 | + | 
|  | 70 | +		// Shared Post-Execution Hook: Track the result for analytics. | 
|  | 71 | +		$this->track_result( $result ); | 
|  | 72 | + | 
|  | 73 | +		return $result; | 
|  | 74 | +	} | 
|  | 75 | + | 
|  | 76 | +	/** | 
|  | 77 | +	 * Each child ability MUST implement its own specific logic in this method. | 
|  | 78 | +	 */ | 
|  | 79 | +	abstract protected function execute_ability( $input ); | 
|  | 80 | + | 
|  | 81 | +	// --- Shared Helper Methods (Written Once) --- | 
|  | 82 | + | 
|  | 83 | +	protected function is_restaurant_active(): bool { | 
|  | 84 | +		return (bool) get_option( 'wp_restaurant_active', true ); | 
|  | 85 | +	} | 
|  | 86 | + | 
|  | 87 | +	protected function check_rate_limit(): bool { | 
|  | 88 | +		$user_id = get_current_user_id(); | 
|  | 89 | +		$key     = "restaurant_ability_limit_{$user_id}"; | 
|  | 90 | +		$count   = get_transient( $key ) ?: 0; | 
|  | 91 | + | 
|  | 92 | +		if ( $count >= 100 ) { // 100 requests per hour. | 
|  | 93 | +			return false; | 
|  | 94 | +		} | 
|  | 95 | + | 
|  | 96 | +		set_transient( $key, $count + 1, HOUR_IN_SECONDS ); | 
|  | 97 | +		return true; | 
|  | 98 | +	} | 
|  | 99 | + | 
|  | 100 | +	protected function has_active_subscription(): bool { | 
|  | 101 | +		// In a real plugin, this would check with a payment provider like Stripe or Freemius. | 
|  | 102 | +		return true; | 
|  | 103 | +	} | 
|  | 104 | + | 
|  | 105 | +	protected function log_ability_usage( $input ): void { | 
|  | 106 | +		// In a real plugin, this would log to a database or analytics service. | 
|  | 107 | +		do_action( 'wp_restaurant_ability_used', $this->get_name(), $input ); | 
|  | 108 | +	} | 
|  | 109 | + | 
|  | 110 | +	protected function track_result( $result ): void { | 
|  | 111 | +		if ( is_wp_error( $result ) ) { | 
|  | 112 | +			do_action( 'wp_restaurant_ability_error', $this->get_name(), $result ); | 
|  | 113 | +		} else { | 
|  | 114 | +			do_action( 'wp_restaurant_ability_success', $this->get_name(), $result ); | 
|  | 115 | +		} | 
|  | 116 | +	} | 
|  | 117 | +} | 
|  | 118 | + | 
|  | 119 | +/** | 
|  | 120 | + * Ability 1: Generate Menu Description | 
|  | 121 | + * | 
|  | 122 | + * NOTE: This class is very lean. It only defines its unique properties and logic. | 
|  | 123 | + * All the shared logic (permissions, logging, etc.) is inherited from `Restaurant_Ability`. | 
|  | 124 | + */ | 
|  | 125 | +class Generate_Menu_Description_Ability extends Restaurant_Ability { | 
|  | 126 | + | 
|  | 127 | +	protected function get_label(): string { | 
|  | 128 | +		return __( 'Generate Menu Description', 'wp-restaurant' ); | 
|  | 129 | +	} | 
|  | 130 | + | 
|  | 131 | +	protected function get_description(): string { | 
|  | 132 | +		return __( 'Generate an appetizing description for a menu item using AI', 'wp-restaurant' ); | 
|  | 133 | +	} | 
|  | 134 | + | 
|  | 135 | +	protected function get_input_schema(): array { | 
|  | 136 | +		return array( | 
|  | 137 | +			'type'       => 'object', | 
|  | 138 | +			'properties' => array( | 
|  | 139 | +				'dish_name'   => array( 'type' => 'string' ), | 
|  | 140 | +				'ingredients' => array( 'type' => 'array' ), | 
|  | 141 | +				'cuisine'     => array( 'type' => 'string' ), | 
|  | 142 | +			), | 
|  | 143 | +			'required'   => array( 'dish_name', 'ingredients' ), | 
|  | 144 | +		); | 
|  | 145 | +	} | 
|  | 146 | + | 
|  | 147 | +	protected function get_output_schema(): array { | 
|  | 148 | +		return array( | 
|  | 149 | +			'type'       => 'object', | 
|  | 150 | +			'properties' => array( | 
|  | 151 | +				'description' => array( 'type' => 'string' ), | 
|  | 152 | +				'length'      => array( 'type' => 'integer' ), | 
|  | 153 | +			), | 
|  | 154 | +		); | 
|  | 155 | +	} | 
|  | 156 | + | 
|  | 157 | +	protected function execute_ability( $input ) { | 
|  | 158 | +		if ( empty( $input['dish_name'] ) || empty( $input['ingredients'] ) ) { | 
|  | 159 | +			return new \WP_Error( 'missing_data', 'Dish name and ingredients are required' ); | 
|  | 160 | +		} | 
|  | 161 | + | 
|  | 162 | +		$prompt = sprintf( | 
|  | 163 | +			'Write an appetizing menu description for "%s". Ingredients: %s. %s', | 
|  | 164 | +			$input['dish_name'], | 
|  | 165 | +			implode( ', ', $input['ingredients'] ), | 
|  | 166 | +			! empty( $input['cuisine'] ) ? 'Cuisine style: ' . $input['cuisine'] : '' | 
|  | 167 | +		); | 
|  | 168 | + | 
|  | 169 | +		$description = "A delicious {$input['dish_name']} featuring " . implode( ', ', $input['ingredients'] ); | 
|  | 170 | + | 
|  | 171 | +		return array( | 
|  | 172 | +			'description' => $description, | 
|  | 173 | +			'length'      => strlen( $description ), | 
|  | 174 | +		); | 
|  | 175 | +	} | 
|  | 176 | +} | 
|  | 177 | + | 
|  | 178 | +/** | 
|  | 179 | + * Ability 2: Suggest Wine Pairings | 
|  | 180 | + * NOTE: Another lean class that inherits all shared logic. | 
|  | 181 | + */ | 
|  | 182 | +class Suggest_Wine_Pairing_Ability extends Restaurant_Ability { | 
|  | 183 | + | 
|  | 184 | +	protected function get_label(): string { | 
|  | 185 | +		return __( 'Suggest Wine Pairing', 'wp-restaurant' ); | 
|  | 186 | +	} | 
|  | 187 | + | 
|  | 188 | +	protected function get_description(): string { | 
|  | 189 | +		return __( 'Suggest wine pairings for a specific dish', 'wp-restaurant' ); | 
|  | 190 | +	} | 
|  | 191 | + | 
|  | 192 | +	protected function get_input_schema(): array { | 
|  | 193 | +		return array( | 
|  | 194 | +			'type'       => 'object', | 
|  | 195 | +			'properties' => array( | 
|  | 196 | +				'dish_name'    => array( 'type' => 'string' ), | 
|  | 197 | +				'main_protein' => array( 'type' => 'string' ), | 
|  | 198 | +				'sauce_type'   => array( 'type' => 'string' ), | 
|  | 199 | +			), | 
|  | 200 | +			'required'   => array( 'dish_name' ), | 
|  | 201 | +		); | 
|  | 202 | +	} | 
|  | 203 | + | 
|  | 204 | +	protected function execute_ability( $input ) { | 
|  | 205 | +		if ( empty( $input['dish_name'] ) ) { | 
|  | 206 | +			return new \WP_Error( 'missing_data', 'Dish name is required' ); | 
|  | 207 | +		} | 
|  | 208 | + | 
|  | 209 | +		return array( | 
|  | 210 | +			'suggestions' => array( | 
|  | 211 | +				array( | 
|  | 212 | +					'wine_name' => 'Pinot Noir', | 
|  | 213 | +					'reason'    => 'Complements the rich flavors', | 
|  | 214 | +				), | 
|  | 215 | +			), | 
|  | 216 | +		); | 
|  | 217 | +	} | 
|  | 218 | +} | 
|  | 219 | + | 
|  | 220 | +/** | 
|  | 221 | + * Ability 4: Generate Social Media Post | 
|  | 222 | + * | 
|  | 223 | + * NOTE: This class demonstrates the flexibility of the pattern. | 
|  | 224 | + * It overrides the default permission callback with its own specific logic, | 
|  | 225 | + * while still inheriting all the other shared logic (rate-limiting, logging, etc). | 
|  | 226 | + */ | 
|  | 227 | +class Generate_Social_Post_Ability extends Restaurant_Ability { | 
|  | 228 | + | 
|  | 229 | +	// OVERRIDE: This ability requires a different permission level. | 
|  | 230 | +	protected function get_permission_callback(): callable { | 
|  | 231 | +		return function ( $input ) { | 
|  | 232 | +			// 1. Use a different capability check for marketing. | 
|  | 233 | +			if ( ! current_user_can( 'publish_posts' ) ) { | 
|  | 234 | +				return new \WP_Error( 'permission_denied', 'You do not have marketing permissions.' ); | 
|  | 235 | +			} | 
|  | 236 | + | 
|  | 237 | +			// 2. Still use the SHARED helper methods from the base class. | 
|  | 238 | +			if ( ! $this->is_restaurant_active() ) { | 
|  | 239 | +				return new \WP_Error( 'restaurant_closed', 'Restaurant management is currently disabled.' ); | 
|  | 240 | +			} | 
|  | 241 | +			if ( ! $this->check_rate_limit() ) { | 
|  | 242 | +				return new \WP_Error( 'rate_limit', 'Too many requests. Please wait.' ); | 
|  | 243 | +			} | 
|  | 244 | + | 
|  | 245 | +			return true; | 
|  | 246 | +		}; | 
|  | 247 | +	} | 
|  | 248 | + | 
|  | 249 | +	protected function get_label(): string { | 
|  | 250 | +		return __( 'Generate Social Media Post', 'wp-restaurant' ); | 
|  | 251 | +	} | 
|  | 252 | + | 
|  | 253 | +	protected function get_description(): string { | 
|  | 254 | +		return __( 'Create promotional social media content for menu items', 'wp-restaurant' ); | 
|  | 255 | +	} | 
|  | 256 | + | 
|  | 257 | +	protected function get_input_schema(): array { | 
|  | 258 | +		return array( | 
|  | 259 | +			'type'       => 'object', | 
|  | 260 | +			'properties' => array( | 
|  | 261 | +				'dish_name'   => array( 'type' => 'string' ), | 
|  | 262 | +				'description' => array( 'type' => 'string' ), | 
|  | 263 | +				'platform'    => array( | 
|  | 264 | +					'type' => 'string', | 
|  | 265 | +					'enum' => array( 'instagram', 'facebook', 'twitter' ), | 
|  | 266 | +				), | 
|  | 267 | +			), | 
|  | 268 | +			'required'   => array( 'dish_name', 'platform' ), | 
|  | 269 | +		); | 
|  | 270 | +	} | 
|  | 271 | + | 
|  | 272 | +	protected function execute_ability( $input ) { | 
|  | 273 | +		if ( empty( $input['dish_name'] ) || empty( $input['platform'] ) ) { | 
|  | 274 | +			return new \WP_Error( 'missing_data', 'Dish name and platform are required' ); | 
|  | 275 | +		} | 
|  | 276 | + | 
|  | 277 | +		return array( | 
|  | 278 | +			'post' => "Check out our amazing {$input['dish_name']}! 🍽️", | 
|  | 279 | +		); | 
|  | 280 | +	} | 
|  | 281 | +} | 
|  | 282 | + | 
|  | 283 | +// ... Other ability classes like Dietary_Alternatives_Ability and Analyze_Reviews_Ability would follow the same lean pattern ... | 
|  | 284 | + | 
|  | 285 | +/** | 
|  | 286 | + * Registers all abilities using the flexible, class-based approach. | 
|  | 287 | + * | 
|  | 288 | + * NOTE: With this pattern, registration is clean and declarative. | 
|  | 289 | + * We only pass the `ability_class`. All other details are encapsulated in the class itself. | 
|  | 290 | + * This is the code that would trigger a `PHPStan` error if the PR is merged, | 
|  | 291 | + * as `description`, `execute_callback`, etc., are not provided here. | 
|  | 292 | + * This demonstrates being "punished for following DRY principles." | 
|  | 293 | + */ | 
|  | 294 | +function register_abilities() { | 
|  | 295 | +	wp_register_ability( | 
|  | 296 | +		'wp-restaurant/generate-menu-description', | 
|  | 297 | +		array( | 
|  | 298 | +			'ability_class' => Generate_Menu_Description_Ability::class, | 
|  | 299 | +		) | 
|  | 300 | +	); | 
|  | 301 | + | 
|  | 302 | +	wp_register_ability( | 
|  | 303 | +		'wp-restaurant/suggest-wine-pairing', | 
|  | 304 | +		array( | 
|  | 305 | +			'ability_class' => Suggest_Wine_Pairing_Ability::class, | 
|  | 306 | +		) | 
|  | 307 | +	); | 
|  | 308 | + | 
|  | 309 | +	wp_register_ability( | 
|  | 310 | +		'wp-restaurant/generate-social-post', | 
|  | 311 | +		array( | 
|  | 312 | +			'ability_class' => Generate_Social_Post_Ability::class, | 
|  | 313 | +		) | 
|  | 314 | +	); | 
|  | 315 | + | 
|  | 316 | +	// wp_register_ability( 'wp-restaurant/dietary-alternatives', ... ); | 
|  | 317 | +	// wp_register_ability( 'wp-restaurant/analyze-reviews', ... ); | 
|  | 318 | +} | 
|  | 319 | +add_action( 'init', __NAMESPACE__ . '\register_abilities' ); | 
0 commit comments