Skip to content

Commit 331da1a

Browse files
author
Mohamed Khaled
committed
Feat: Add simple and flexible approach code examples
1 parent 45cfa27 commit 331da1a

File tree

2 files changed

+607
-0
lines changed

2 files changed

+607
-0
lines changed

examples/flexible-approach.php

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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

Comments
 (0)