@lebtiga/sonic-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +223 -0
- package/README.md +61 -0
- package/bin/sonic.js +304 -0
- package/lib/index.js +20 -0
- package/lib/installer.js +156 -0
- package/lib/license.js +48 -0
- package/package.json +46 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/README.md +100 -0
- package/plugin/agents/sonic.md +80 -0
- package/plugin/commands/sonic-build.md +145 -0
- package/plugin/commands/sonic-help.md +71 -0
- package/plugin/skills/accessibility-qa/SKILL.md +160 -0
- package/plugin/skills/accessibility-qa/templates/accessibility-qa-report-template.md +123 -0
- package/plugin/skills/accessibility-qa/templates/wcag-compliance-statement.md +70 -0
- package/plugin/skills/aka-wireframe-wp/SKILL.md +149 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/README.md +190 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/footer.php +49 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/functions.php +395 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/header.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/index.php +39 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-answer.php +62 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-authority-hub.php +122 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-knowledge.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/style.css +633 -0
- package/plugin/skills/aka-wireframe-wp/references/content-generator.md +371 -0
- package/plugin/skills/aka-wireframe-wp/references/internal-linker.md +430 -0
- package/plugin/skills/aka-wireframe-wp/references/orchestrator.md +269 -0
- package/plugin/skills/aka-wireframe-wp/references/prompts-library.md +880 -0
- package/plugin/skills/aka-wireframe-wp/references/seo-optimizer.md +433 -0
- package/plugin/skills/aka-wireframe-wp/references/strategy-planner.md +317 -0
- package/plugin/skills/aka-wireframe-wp/references/wordpress-deployer.md +545 -0
- package/plugin/skills/authority-site-builder/SKILL.md +138 -0
- package/plugin/skills/brand-philosophy/SKILL.md +77 -0
- package/plugin/skills/freepik-spaces/SKILL.md +122 -0
- package/plugin/skills/freepik-spaces/docs/automation-guide.md +233 -0
- package/plugin/skills/freepik-spaces/docs/research-notes.md +264 -0
- package/plugin/skills/freepik-spaces/plans/naseberry-demo-plan.md +320 -0
- package/plugin/skills/freepik-spaces/templates/naseberry-demo.json +302 -0
- package/plugin/skills/freepik-spaces/templates/saas-demo.json +212 -0
- package/plugin/skills/frontend-design/LICENSE.txt +177 -0
- package/plugin/skills/frontend-design/SKILL.md +77 -0
- package/plugin/skills/programmatic-seo/SKILL.md +236 -0
- package/plugin/skills/programmatic-seo/references/playbooks.md +293 -0
- package/plugin/skills/seo-qa/SKILL.md +132 -0
- package/plugin/skills/seo-qa/templates/schema-localbusiness.json +49 -0
- package/plugin/skills/seo-qa/templates/schema-service.json +36 -0
- package/plugin/skills/seo-qa/templates/seo-qa-report-template.md +90 -0
- package/plugin/skills/visual-identity/SKILL.md +109 -0
- package/plugin/skills/visual-identity/templates/style-guide-template.md +108 -0
- package/plugin/skills/website-image-gen/SKILL.md +82 -0
- package/plugin/skills/website-image-gen/templates/blog-featured.md +56 -0
- package/plugin/skills/website-image-gen/templates/hero-service-photo.md +56 -0
- package/plugin/skills/wordpress-pro/SKILL.md +105 -0
- package/plugin/skills/wordpress-pro/references/gutenberg-blocks.md +870 -0
- package/plugin/skills/wordpress-pro/references/hooks-filters.md +845 -0
- package/plugin/skills/wordpress-pro/references/performance-security.md +1012 -0
- package/plugin/skills/wordpress-pro/references/plugin-architecture.md +1041 -0
- package/plugin/skills/wordpress-pro/references/theme-development.md +858 -0
- package/plugin/sops/SOP-Sonic 777/authority-site-sop.html +1100 -0
- package/plugin/sops/SOP-WORDPRESS-330-PAGE-SITES.md +926 -0
- package/scripts/postinstall.js +109 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# Hooks & Filters
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## WordPress Hook System
|
|
6
|
+
|
|
7
|
+
WordPress uses an event-driven architecture with two types of hooks:
|
|
8
|
+
|
|
9
|
+
| Hook Type | Purpose | Function |
|
|
10
|
+
|-----------|---------|----------|
|
|
11
|
+
| **Actions** | Execute code at specific points | `add_action()` / `do_action()` |
|
|
12
|
+
| **Filters** | Modify data before it's used | `add_filter()` / `apply_filters()` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Actions
|
|
17
|
+
|
|
18
|
+
Actions allow you to execute custom code at specific points in WordPress execution.
|
|
19
|
+
|
|
20
|
+
### Action Basics
|
|
21
|
+
|
|
22
|
+
```php
|
|
23
|
+
<?php
|
|
24
|
+
declare(strict_types=1);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register an action hook
|
|
28
|
+
*
|
|
29
|
+
* @param string $hook_name The name of the action
|
|
30
|
+
* @param callable $callback Function to execute
|
|
31
|
+
* @param int $priority Order of execution (default: 10)
|
|
32
|
+
* @param int $accepted_args Number of arguments passed (default: 1)
|
|
33
|
+
*/
|
|
34
|
+
add_action('init', 'my_plugin_init', 10, 1);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute custom code on init
|
|
38
|
+
*/
|
|
39
|
+
function my_plugin_init(): void {
|
|
40
|
+
// Your code here
|
|
41
|
+
register_post_type('product', [...]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Using anonymous functions (PHP 8.1+)
|
|
45
|
+
add_action('wp_footer', function(): void {
|
|
46
|
+
echo '<!-- Custom footer content -->';
|
|
47
|
+
}, 99);
|
|
48
|
+
|
|
49
|
+
// Using class methods
|
|
50
|
+
class My_Plugin {
|
|
51
|
+
public function __construct() {
|
|
52
|
+
add_action('init', [$this, 'init']);
|
|
53
|
+
add_action('admin_init', [$this, 'admin_init']);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public function init(): void {
|
|
57
|
+
// Initialization code
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public function admin_init(): void {
|
|
61
|
+
// Admin initialization
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Using static methods
|
|
66
|
+
add_action('init', [My_Plugin::class, 'static_init']);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Essential Action Hooks
|
|
70
|
+
|
|
71
|
+
```php
|
|
72
|
+
<?php
|
|
73
|
+
/**
|
|
74
|
+
* Execution order and common action hooks
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
// === EARLY LOADING ===
|
|
78
|
+
|
|
79
|
+
// After WordPress loads but before headers sent
|
|
80
|
+
add_action('muplugins_loaded', function(): void {
|
|
81
|
+
// Must-use plugins loaded
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// After active plugins loaded
|
|
85
|
+
add_action('plugins_loaded', function(): void {
|
|
86
|
+
// Safe to check for other plugins
|
|
87
|
+
if (class_exists('WooCommerce')) {
|
|
88
|
+
// WooCommerce is active
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// After theme functions.php loaded
|
|
93
|
+
add_action('after_setup_theme', function(): void {
|
|
94
|
+
// Theme setup: add_theme_support, register_nav_menus, etc.
|
|
95
|
+
}, 10);
|
|
96
|
+
|
|
97
|
+
// === MAIN INITIALIZATION ===
|
|
98
|
+
|
|
99
|
+
// WordPress fully loaded, safe for most operations
|
|
100
|
+
add_action('init', function(): void {
|
|
101
|
+
// Register post types, taxonomies, shortcodes
|
|
102
|
+
// Load text domains
|
|
103
|
+
// Start session if needed
|
|
104
|
+
}, 10);
|
|
105
|
+
|
|
106
|
+
// All widgets registered
|
|
107
|
+
add_action('widgets_init', function(): void {
|
|
108
|
+
// Register widget areas
|
|
109
|
+
register_sidebar([...]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// === ADMIN HOOKS ===
|
|
113
|
+
|
|
114
|
+
// Admin area initializing
|
|
115
|
+
add_action('admin_init', function(): void {
|
|
116
|
+
// Register settings, add capabilities
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Build admin menu
|
|
120
|
+
add_action('admin_menu', function(): void {
|
|
121
|
+
// Add menu pages
|
|
122
|
+
add_menu_page(...);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Enqueue admin assets
|
|
126
|
+
add_action('admin_enqueue_scripts', function(string $hook_suffix): void {
|
|
127
|
+
// $hook_suffix: e.g., 'post.php', 'settings_page_my-settings'
|
|
128
|
+
if ($hook_suffix !== 'settings_page_my-settings') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
wp_enqueue_script('my-admin-script', ...);
|
|
132
|
+
}, 10, 1);
|
|
133
|
+
|
|
134
|
+
// === FRONTEND HOOKS ===
|
|
135
|
+
|
|
136
|
+
// Main query parsed, before template loaded
|
|
137
|
+
add_action('template_redirect', function(): void {
|
|
138
|
+
// Check conditions, redirect if needed
|
|
139
|
+
if (is_page('restricted') && !is_user_logged_in()) {
|
|
140
|
+
wp_redirect(wp_login_url());
|
|
141
|
+
exit;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Enqueue frontend assets
|
|
146
|
+
add_action('wp_enqueue_scripts', function(): void {
|
|
147
|
+
wp_enqueue_style('my-style', ...);
|
|
148
|
+
wp_enqueue_script('my-script', ...);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Inside <head> tag
|
|
152
|
+
add_action('wp_head', function(): void {
|
|
153
|
+
// Meta tags, inline styles
|
|
154
|
+
echo '<meta name="custom" content="value" />';
|
|
155
|
+
}, 1); // Priority 1 = early in head
|
|
156
|
+
|
|
157
|
+
// Before </body> tag
|
|
158
|
+
add_action('wp_footer', function(): void {
|
|
159
|
+
// Tracking scripts, modals
|
|
160
|
+
}, 99); // Priority 99 = late in footer
|
|
161
|
+
|
|
162
|
+
// === POST/PAGE HOOKS ===
|
|
163
|
+
|
|
164
|
+
// Before post is saved
|
|
165
|
+
add_action('pre_post_update', function(int $post_id, array $data): void {
|
|
166
|
+
// Validate or modify before save
|
|
167
|
+
}, 10, 2);
|
|
168
|
+
|
|
169
|
+
// After post is saved (any status)
|
|
170
|
+
add_action('save_post', function(int $post_id, WP_Post $post, bool $update): void {
|
|
171
|
+
// Skip autosaves
|
|
172
|
+
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Skip revisions
|
|
177
|
+
if (wp_is_post_revision($post_id)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Skip specific post types
|
|
182
|
+
if ($post->post_type !== 'my_post_type') {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update meta, trigger notifications, etc.
|
|
187
|
+
update_post_meta($post_id, '_custom_meta', sanitize_text_field($_POST['custom_field'] ?? ''));
|
|
188
|
+
}, 10, 3);
|
|
189
|
+
|
|
190
|
+
// Post status transitions
|
|
191
|
+
add_action('transition_post_status', function(string $new_status, string $old_status, WP_Post $post): void {
|
|
192
|
+
if ($new_status === 'publish' && $old_status !== 'publish') {
|
|
193
|
+
// Post just published
|
|
194
|
+
my_notify_subscribers($post);
|
|
195
|
+
}
|
|
196
|
+
}, 10, 3);
|
|
197
|
+
|
|
198
|
+
// Post deleted (moved to trash)
|
|
199
|
+
add_action('wp_trash_post', function(int $post_id): void {
|
|
200
|
+
// Clean up related data
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Post permanently deleted
|
|
204
|
+
add_action('before_delete_post', function(int $post_id, WP_Post $post): void {
|
|
205
|
+
// Clean up custom tables, files, etc.
|
|
206
|
+
global $wpdb;
|
|
207
|
+
$wpdb->delete(
|
|
208
|
+
$wpdb->prefix . 'my_table',
|
|
209
|
+
['post_id' => $post_id],
|
|
210
|
+
['%d']
|
|
211
|
+
);
|
|
212
|
+
}, 10, 2);
|
|
213
|
+
|
|
214
|
+
// === USER HOOKS ===
|
|
215
|
+
|
|
216
|
+
// User registered
|
|
217
|
+
add_action('user_register', function(int $user_id, array $userdata): void {
|
|
218
|
+
// Set default meta, send welcome email
|
|
219
|
+
update_user_meta($user_id, 'welcome_dismissed', false);
|
|
220
|
+
}, 10, 2);
|
|
221
|
+
|
|
222
|
+
// User logged in
|
|
223
|
+
add_action('wp_login', function(string $user_login, WP_User $user): void {
|
|
224
|
+
// Log login, update last login time
|
|
225
|
+
update_user_meta($user->ID, 'last_login', current_time('mysql'));
|
|
226
|
+
}, 10, 2);
|
|
227
|
+
|
|
228
|
+
// User logged out
|
|
229
|
+
add_action('wp_logout', function(int $user_id): void {
|
|
230
|
+
// Cleanup session data
|
|
231
|
+
}, 10, 1);
|
|
232
|
+
|
|
233
|
+
// === REST API HOOKS ===
|
|
234
|
+
|
|
235
|
+
// Register REST routes
|
|
236
|
+
add_action('rest_api_init', function(): void {
|
|
237
|
+
register_rest_route('my-plugin/v1', '/items', [
|
|
238
|
+
'methods' => 'GET',
|
|
239
|
+
'callback' => 'my_plugin_get_items',
|
|
240
|
+
'permission_callback' => '__return_true',
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// === CRON HOOKS ===
|
|
245
|
+
|
|
246
|
+
// Schedule custom cron event
|
|
247
|
+
add_action('init', function(): void {
|
|
248
|
+
if (!wp_next_scheduled('my_plugin_daily_task')) {
|
|
249
|
+
wp_schedule_event(time(), 'daily', 'my_plugin_daily_task');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Handle cron event
|
|
254
|
+
add_action('my_plugin_daily_task', function(): void {
|
|
255
|
+
// Cleanup, sync, report generation, etc.
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Removing Actions
|
|
260
|
+
|
|
261
|
+
```php
|
|
262
|
+
<?php
|
|
263
|
+
/**
|
|
264
|
+
* Remove actions added by WordPress or other plugins
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
// Remove default WordPress actions
|
|
268
|
+
remove_action('wp_head', 'wp_generator');
|
|
269
|
+
remove_action('wp_head', 'wlwmanifest_link');
|
|
270
|
+
remove_action('wp_head', 'rsd_link');
|
|
271
|
+
remove_action('wp_head', 'wp_shortlink_wp_head');
|
|
272
|
+
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
|
273
|
+
remove_action('wp_print_styles', 'print_emoji_styles');
|
|
274
|
+
|
|
275
|
+
// Remove action from a class (must match exact instance)
|
|
276
|
+
// If original: add_action('init', [$instance, 'method'], 10);
|
|
277
|
+
// Need to access same $instance to remove
|
|
278
|
+
|
|
279
|
+
// Remove using class name for static methods
|
|
280
|
+
remove_action('init', [Some_Class::class, 'static_method'], 10);
|
|
281
|
+
|
|
282
|
+
// Remove all callbacks from a hook
|
|
283
|
+
remove_all_actions('some_hook');
|
|
284
|
+
|
|
285
|
+
// Check if action is hooked
|
|
286
|
+
if (has_action('init', 'some_callback')) {
|
|
287
|
+
// Callback is registered
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Filters
|
|
294
|
+
|
|
295
|
+
Filters modify data before it's used or displayed.
|
|
296
|
+
|
|
297
|
+
### Filter Basics
|
|
298
|
+
|
|
299
|
+
```php
|
|
300
|
+
<?php
|
|
301
|
+
declare(strict_types=1);
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Register a filter hook
|
|
305
|
+
*
|
|
306
|
+
* @param string $hook_name The name of the filter
|
|
307
|
+
* @param callable $callback Function to filter data
|
|
308
|
+
* @param int $priority Order of execution (default: 10)
|
|
309
|
+
* @param int $accepted_args Number of arguments passed (default: 1)
|
|
310
|
+
*/
|
|
311
|
+
add_filter('the_content', 'my_plugin_modify_content', 10, 1);
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Modify post content
|
|
315
|
+
*
|
|
316
|
+
* @param string $content The post content
|
|
317
|
+
* @return string Modified content
|
|
318
|
+
*/
|
|
319
|
+
function my_plugin_modify_content(string $content): string {
|
|
320
|
+
// Always return the filtered value
|
|
321
|
+
if (is_single() && in_the_loop()) {
|
|
322
|
+
$content .= '<div class="post-cta">Subscribe for more!</div>';
|
|
323
|
+
}
|
|
324
|
+
return $content;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Filter with multiple arguments
|
|
328
|
+
add_filter('post_thumbnail_html', function(
|
|
329
|
+
string $html,
|
|
330
|
+
int $post_id,
|
|
331
|
+
int $thumbnail_id,
|
|
332
|
+
string $size,
|
|
333
|
+
string $attr
|
|
334
|
+
): string {
|
|
335
|
+
// Add lazy loading
|
|
336
|
+
return str_replace('<img', '<img loading="lazy"', $html);
|
|
337
|
+
}, 10, 5);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Essential Filter Hooks
|
|
341
|
+
|
|
342
|
+
```php
|
|
343
|
+
<?php
|
|
344
|
+
/**
|
|
345
|
+
* Common filter hooks
|
|
346
|
+
*/
|
|
347
|
+
|
|
348
|
+
// === CONTENT FILTERS ===
|
|
349
|
+
|
|
350
|
+
// Modify post title
|
|
351
|
+
add_filter('the_title', function(string $title, int $post_id): string {
|
|
352
|
+
if (is_admin()) {
|
|
353
|
+
return $title;
|
|
354
|
+
}
|
|
355
|
+
// Add icon for featured posts
|
|
356
|
+
if (get_post_meta($post_id, '_is_featured', true)) {
|
|
357
|
+
$title = '★ ' . $title;
|
|
358
|
+
}
|
|
359
|
+
return $title;
|
|
360
|
+
}, 10, 2);
|
|
361
|
+
|
|
362
|
+
// Modify post content
|
|
363
|
+
add_filter('the_content', function(string $content): string {
|
|
364
|
+
// Add social sharing after content
|
|
365
|
+
if (is_singular('post') && in_the_loop() && is_main_query()) {
|
|
366
|
+
$content .= my_plugin_get_share_buttons();
|
|
367
|
+
}
|
|
368
|
+
return $content;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Modify excerpt length
|
|
372
|
+
add_filter('excerpt_length', function(int $length): int {
|
|
373
|
+
return 30; // words
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Modify excerpt "more" text
|
|
377
|
+
add_filter('excerpt_more', function(string $more): string {
|
|
378
|
+
return '… <a href="' . esc_url(get_permalink()) . '">Read more</a>';
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// === QUERY FILTERS ===
|
|
382
|
+
|
|
383
|
+
// Modify main query
|
|
384
|
+
add_filter('pre_get_posts', function(WP_Query $query): void {
|
|
385
|
+
if (is_admin() || !$query->is_main_query()) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Exclude category from blog
|
|
390
|
+
if ($query->is_home()) {
|
|
391
|
+
$query->set('cat', '-5'); // Exclude category ID 5
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Custom archive ordering
|
|
395
|
+
if ($query->is_post_type_archive('product')) {
|
|
396
|
+
$query->set('orderby', 'menu_order');
|
|
397
|
+
$query->set('order', 'ASC');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Modify search results
|
|
402
|
+
add_filter('posts_search', function(string $search, WP_Query $query): string {
|
|
403
|
+
if (!$query->is_search() || !$query->is_main_query()) {
|
|
404
|
+
return $search;
|
|
405
|
+
}
|
|
406
|
+
// Customize search SQL
|
|
407
|
+
return $search;
|
|
408
|
+
}, 10, 2);
|
|
409
|
+
|
|
410
|
+
// === TEMPLATE FILTERS ===
|
|
411
|
+
|
|
412
|
+
// Override template file
|
|
413
|
+
add_filter('template_include', function(string $template): string {
|
|
414
|
+
if (is_singular('product')) {
|
|
415
|
+
$custom = locate_template('templates/single-product.php');
|
|
416
|
+
if ($custom) {
|
|
417
|
+
return $custom;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return $template;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Add body classes
|
|
424
|
+
add_filter('body_class', function(array $classes): array {
|
|
425
|
+
if (is_user_logged_in()) {
|
|
426
|
+
$classes[] = 'logged-in-user';
|
|
427
|
+
}
|
|
428
|
+
if (wp_is_mobile()) {
|
|
429
|
+
$classes[] = 'is-mobile';
|
|
430
|
+
}
|
|
431
|
+
return $classes;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Add post classes
|
|
435
|
+
add_filter('post_class', function(array $classes, array $class, int $post_id): array {
|
|
436
|
+
if (has_post_thumbnail($post_id)) {
|
|
437
|
+
$classes[] = 'has-thumbnail';
|
|
438
|
+
}
|
|
439
|
+
return $classes;
|
|
440
|
+
}, 10, 3);
|
|
441
|
+
|
|
442
|
+
// === ADMIN FILTERS ===
|
|
443
|
+
|
|
444
|
+
// Modify admin columns
|
|
445
|
+
add_filter('manage_product_posts_columns', function(array $columns): array {
|
|
446
|
+
$new_columns = [];
|
|
447
|
+
foreach ($columns as $key => $value) {
|
|
448
|
+
$new_columns[$key] = $value;
|
|
449
|
+
if ($key === 'title') {
|
|
450
|
+
$new_columns['price'] = __('Price', 'my-plugin');
|
|
451
|
+
$new_columns['sku'] = __('SKU', 'my-plugin');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return $new_columns;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Populate custom columns
|
|
458
|
+
add_action('manage_product_posts_custom_column', function(string $column, int $post_id): void {
|
|
459
|
+
switch ($column) {
|
|
460
|
+
case 'price':
|
|
461
|
+
echo esc_html(get_post_meta($post_id, '_price', true));
|
|
462
|
+
break;
|
|
463
|
+
case 'sku':
|
|
464
|
+
echo esc_html(get_post_meta($post_id, '_sku', true));
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}, 10, 2);
|
|
468
|
+
|
|
469
|
+
// Make columns sortable
|
|
470
|
+
add_filter('manage_edit-product_sortable_columns', function(array $columns): array {
|
|
471
|
+
$columns['price'] = 'price';
|
|
472
|
+
$columns['sku'] = 'sku';
|
|
473
|
+
return $columns;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// === URL/LINK FILTERS ===
|
|
477
|
+
|
|
478
|
+
// Modify permalink structure
|
|
479
|
+
add_filter('post_type_link', function(string $permalink, WP_Post $post): string {
|
|
480
|
+
if ($post->post_type !== 'product') {
|
|
481
|
+
return $permalink;
|
|
482
|
+
}
|
|
483
|
+
// Add category to permalink
|
|
484
|
+
$terms = get_the_terms($post->ID, 'product_category');
|
|
485
|
+
if ($terms && !is_wp_error($terms)) {
|
|
486
|
+
$permalink = str_replace('%product_category%', $terms[0]->slug, $permalink);
|
|
487
|
+
}
|
|
488
|
+
return $permalink;
|
|
489
|
+
}, 10, 2);
|
|
490
|
+
|
|
491
|
+
// Modify upload directory
|
|
492
|
+
add_filter('upload_dir', function(array $uploads): array {
|
|
493
|
+
// Custom upload path for specific post types
|
|
494
|
+
if (isset($_POST['post_id'])) {
|
|
495
|
+
$post_type = get_post_type((int) $_POST['post_id']);
|
|
496
|
+
if ($post_type === 'product') {
|
|
497
|
+
$uploads['subdir'] = '/products' . $uploads['subdir'];
|
|
498
|
+
$uploads['path'] = $uploads['basedir'] . $uploads['subdir'];
|
|
499
|
+
$uploads['url'] = $uploads['baseurl'] . $uploads['subdir'];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return $uploads;
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// === SECURITY FILTERS ===
|
|
506
|
+
|
|
507
|
+
// Modify allowed HTML in wp_kses
|
|
508
|
+
add_filter('wp_kses_allowed_html', function(array $allowed, string $context): array {
|
|
509
|
+
if ($context === 'post') {
|
|
510
|
+
$allowed['iframe'] = [
|
|
511
|
+
'src' => true,
|
|
512
|
+
'width' => true,
|
|
513
|
+
'height' => true,
|
|
514
|
+
'frameborder' => true,
|
|
515
|
+
'allowfullscreen' => true,
|
|
516
|
+
];
|
|
517
|
+
}
|
|
518
|
+
return $allowed;
|
|
519
|
+
}, 10, 2);
|
|
520
|
+
|
|
521
|
+
// Modify authentication
|
|
522
|
+
add_filter('authenticate', function(?WP_User $user, string $username, string $password): WP_User|WP_Error|null {
|
|
523
|
+
// Block login for specific conditions
|
|
524
|
+
if ($username === 'admin') {
|
|
525
|
+
return new WP_Error('invalid_username', __('Direct admin login is disabled.', 'my-plugin'));
|
|
526
|
+
}
|
|
527
|
+
return $user;
|
|
528
|
+
}, 30, 3);
|
|
529
|
+
|
|
530
|
+
// === REST API FILTERS ===
|
|
531
|
+
|
|
532
|
+
// Modify REST response
|
|
533
|
+
add_filter('rest_prepare_post', function(WP_REST_Response $response, WP_Post $post, WP_REST_Request $request): WP_REST_Response {
|
|
534
|
+
// Add custom field to response
|
|
535
|
+
$response->data['reading_time'] = my_plugin_calculate_reading_time($post->post_content);
|
|
536
|
+
return $response;
|
|
537
|
+
}, 10, 3);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Removing Filters
|
|
541
|
+
|
|
542
|
+
```php
|
|
543
|
+
<?php
|
|
544
|
+
/**
|
|
545
|
+
* Remove filters
|
|
546
|
+
*/
|
|
547
|
+
|
|
548
|
+
// Remove wpautop (auto paragraphs)
|
|
549
|
+
remove_filter('the_content', 'wpautop');
|
|
550
|
+
remove_filter('the_excerpt', 'wpautop');
|
|
551
|
+
|
|
552
|
+
// Remove wptexturize (smart quotes)
|
|
553
|
+
remove_filter('the_content', 'wptexturize');
|
|
554
|
+
remove_filter('the_title', 'wptexturize');
|
|
555
|
+
remove_filter('comment_text', 'wptexturize');
|
|
556
|
+
|
|
557
|
+
// Remove specific filter (must match priority)
|
|
558
|
+
remove_filter('the_content', 'some_callback', 10);
|
|
559
|
+
|
|
560
|
+
// Remove all filters from a hook
|
|
561
|
+
remove_all_filters('the_content');
|
|
562
|
+
|
|
563
|
+
// Check if filter is applied
|
|
564
|
+
if (has_filter('the_content', 'some_callback')) {
|
|
565
|
+
// Filter is registered
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Creating Custom Hooks
|
|
572
|
+
|
|
573
|
+
Allow other developers to extend your plugin/theme.
|
|
574
|
+
|
|
575
|
+
### Custom Actions
|
|
576
|
+
|
|
577
|
+
```php
|
|
578
|
+
<?php
|
|
579
|
+
declare(strict_types=1);
|
|
580
|
+
|
|
581
|
+
namespace MyPlugin;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Example: Custom hooks in a plugin
|
|
585
|
+
*/
|
|
586
|
+
class OrderProcessor {
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Process an order with custom hooks
|
|
590
|
+
*/
|
|
591
|
+
public function process_order(array $order_data): int {
|
|
592
|
+
// Allow modification of order data before processing
|
|
593
|
+
$order_data = apply_filters('my_plugin_pre_process_order', $order_data);
|
|
594
|
+
|
|
595
|
+
// Action before order creation
|
|
596
|
+
do_action('my_plugin_before_create_order', $order_data);
|
|
597
|
+
|
|
598
|
+
// Create the order
|
|
599
|
+
$order_id = $this->create_order($order_data);
|
|
600
|
+
|
|
601
|
+
if ($order_id) {
|
|
602
|
+
// Action after successful order creation
|
|
603
|
+
do_action('my_plugin_order_created', $order_id, $order_data);
|
|
604
|
+
|
|
605
|
+
// Process payment
|
|
606
|
+
$payment_result = $this->process_payment($order_id);
|
|
607
|
+
|
|
608
|
+
if ($payment_result) {
|
|
609
|
+
// Action after successful payment
|
|
610
|
+
do_action('my_plugin_payment_complete', $order_id, $payment_result);
|
|
611
|
+
} else {
|
|
612
|
+
// Action on payment failure
|
|
613
|
+
do_action('my_plugin_payment_failed', $order_id);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Action after all processing complete
|
|
618
|
+
do_action('my_plugin_after_process_order', $order_id, $order_data);
|
|
619
|
+
|
|
620
|
+
return $order_id;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Get order total with filter
|
|
625
|
+
*/
|
|
626
|
+
public function get_order_total(int $order_id): float {
|
|
627
|
+
$subtotal = $this->calculate_subtotal($order_id);
|
|
628
|
+
$shipping = $this->calculate_shipping($order_id);
|
|
629
|
+
$tax = $this->calculate_tax($order_id);
|
|
630
|
+
|
|
631
|
+
$total = $subtotal + $shipping + $tax;
|
|
632
|
+
|
|
633
|
+
// Allow modification of total (for discounts, fees, etc.)
|
|
634
|
+
return (float) apply_filters('my_plugin_order_total', $total, $order_id, [
|
|
635
|
+
'subtotal' => $subtotal,
|
|
636
|
+
'shipping' => $shipping,
|
|
637
|
+
'tax' => $tax,
|
|
638
|
+
]);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Generate email content with filter
|
|
643
|
+
*/
|
|
644
|
+
public function get_order_email_content(int $order_id): string {
|
|
645
|
+
$order = $this->get_order($order_id);
|
|
646
|
+
|
|
647
|
+
$content = sprintf(
|
|
648
|
+
__('Order #%d has been placed.', 'my-plugin'),
|
|
649
|
+
$order_id
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Allow complete override or modification
|
|
653
|
+
return apply_filters('my_plugin_order_email_content', $content, $order_id, $order);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Example usage by another developer
|
|
659
|
+
*/
|
|
660
|
+
|
|
661
|
+
// Add discount to order total
|
|
662
|
+
add_filter('my_plugin_order_total', function(float $total, int $order_id, array $components): float {
|
|
663
|
+
// Apply 10% discount for orders over $100
|
|
664
|
+
if ($total > 100) {
|
|
665
|
+
$total *= 0.9;
|
|
666
|
+
}
|
|
667
|
+
return $total;
|
|
668
|
+
}, 10, 3);
|
|
669
|
+
|
|
670
|
+
// Send notification on order creation
|
|
671
|
+
add_action('my_plugin_order_created', function(int $order_id, array $order_data): void {
|
|
672
|
+
// Send Slack notification
|
|
673
|
+
my_send_slack_notification("New order #{$order_id} created!");
|
|
674
|
+
}, 10, 2);
|
|
675
|
+
|
|
676
|
+
// Custom email content
|
|
677
|
+
add_filter('my_plugin_order_email_content', function(string $content, int $order_id, object $order): string {
|
|
678
|
+
// Add custom footer
|
|
679
|
+
$content .= "\n\nThank you for your business!";
|
|
680
|
+
return $content;
|
|
681
|
+
}, 10, 3);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Custom Filter with Default Value
|
|
685
|
+
|
|
686
|
+
```php
|
|
687
|
+
<?php
|
|
688
|
+
/**
|
|
689
|
+
* Create filter with sensible defaults
|
|
690
|
+
*/
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Get items per page with filter
|
|
694
|
+
*/
|
|
695
|
+
function my_plugin_get_items_per_page(): int {
|
|
696
|
+
$default = 10;
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Filter the number of items per page
|
|
700
|
+
*
|
|
701
|
+
* @since 1.0.0
|
|
702
|
+
*
|
|
703
|
+
* @param int $items_per_page Number of items. Default 10.
|
|
704
|
+
*/
|
|
705
|
+
return (int) apply_filters('my_plugin_items_per_page', $default);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Get allowed file types with filter
|
|
710
|
+
*/
|
|
711
|
+
function my_plugin_get_allowed_file_types(): array {
|
|
712
|
+
$defaults = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Filter allowed file types for upload
|
|
716
|
+
*
|
|
717
|
+
* @since 1.0.0
|
|
718
|
+
*
|
|
719
|
+
* @param array $types Array of allowed file extensions.
|
|
720
|
+
*/
|
|
721
|
+
return (array) apply_filters('my_plugin_allowed_file_types', $defaults);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Check if feature is enabled with filter
|
|
726
|
+
*/
|
|
727
|
+
function my_plugin_is_feature_enabled(string $feature): bool {
|
|
728
|
+
$enabled_features = [
|
|
729
|
+
'dark_mode' => true,
|
|
730
|
+
'analytics' => true,
|
|
731
|
+
'beta_feature' => false,
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
$is_enabled = $enabled_features[$feature] ?? false;
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Filter whether a feature is enabled
|
|
738
|
+
*
|
|
739
|
+
* @since 1.0.0
|
|
740
|
+
*
|
|
741
|
+
* @param bool $is_enabled Whether the feature is enabled.
|
|
742
|
+
* @param string $feature The feature slug.
|
|
743
|
+
*/
|
|
744
|
+
return (bool) apply_filters('my_plugin_feature_enabled', $is_enabled, $feature);
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Hook Priority & Order
|
|
751
|
+
|
|
752
|
+
```php
|
|
753
|
+
<?php
|
|
754
|
+
/**
|
|
755
|
+
* Priority determines execution order
|
|
756
|
+
* Lower number = runs earlier
|
|
757
|
+
* Default priority: 10
|
|
758
|
+
*/
|
|
759
|
+
|
|
760
|
+
// Runs first (priority 1)
|
|
761
|
+
add_action('init', 'my_first_function', 1);
|
|
762
|
+
|
|
763
|
+
// Runs with default priority (10)
|
|
764
|
+
add_action('init', 'my_default_function');
|
|
765
|
+
add_action('init', 'my_default_function_2'); // Runs after, same priority
|
|
766
|
+
|
|
767
|
+
// Runs last (priority 999)
|
|
768
|
+
add_action('init', 'my_last_function', 999);
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Filter priority example: Modify content
|
|
772
|
+
*/
|
|
773
|
+
|
|
774
|
+
// First: Add wrapper
|
|
775
|
+
add_filter('the_content', function(string $content): string {
|
|
776
|
+
return '<div class="content-wrapper">' . $content . '</div>';
|
|
777
|
+
}, 5);
|
|
778
|
+
|
|
779
|
+
// Default: Add sharing buttons
|
|
780
|
+
add_filter('the_content', function(string $content): string {
|
|
781
|
+
return $content . '<div class="share-buttons">...</div>';
|
|
782
|
+
}, 10);
|
|
783
|
+
|
|
784
|
+
// Late: Final output processing
|
|
785
|
+
add_filter('the_content', function(string $content): string {
|
|
786
|
+
// Do final cleanup
|
|
787
|
+
return $content;
|
|
788
|
+
}, 99);
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## Best Practices
|
|
794
|
+
|
|
795
|
+
### Do
|
|
796
|
+
|
|
797
|
+
- Document custom hooks with PHPDoc comments
|
|
798
|
+
- Use prefixed hook names (`my_plugin_*`)
|
|
799
|
+
- Provide sensible default values for filters
|
|
800
|
+
- Pass relevant context to hooks (post ID, data arrays)
|
|
801
|
+
- Check `has_filter()`/`has_action()` before calling expensive operations
|
|
802
|
+
- Use appropriate priorities (don't default to 10 when order matters)
|
|
803
|
+
- Type hint callback parameters and return values (PHP 8.1+)
|
|
804
|
+
- Use namespaced functions or class methods as callbacks
|
|
805
|
+
|
|
806
|
+
### Do Not
|
|
807
|
+
|
|
808
|
+
- Remove core WordPress hooks without understanding consequences
|
|
809
|
+
- Create hooks that pass sensitive data (passwords, tokens)
|
|
810
|
+
- Rely on global variables in callbacks
|
|
811
|
+
- Forget to return filtered values
|
|
812
|
+
- Use anonymous functions when removal might be needed
|
|
813
|
+
- Create circular hook dependencies
|
|
814
|
+
- Add too many hooks at low priorities (performance impact)
|
|
815
|
+
- Modify data passed by reference unexpectedly
|
|
816
|
+
|
|
817
|
+
### Security Considerations
|
|
818
|
+
|
|
819
|
+
```php
|
|
820
|
+
<?php
|
|
821
|
+
/**
|
|
822
|
+
* Security in hooks
|
|
823
|
+
*/
|
|
824
|
+
|
|
825
|
+
// Always validate/sanitize data from hooks
|
|
826
|
+
add_filter('my_plugin_user_input', function(mixed $input): string {
|
|
827
|
+
return sanitize_text_field((string) $input);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Check capabilities in action callbacks
|
|
831
|
+
add_action('my_plugin_admin_action', function(): void {
|
|
832
|
+
if (!current_user_can('manage_options')) {
|
|
833
|
+
wp_die(__('Unauthorized', 'my-plugin'));
|
|
834
|
+
}
|
|
835
|
+
// Proceed with admin action
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Verify nonces for form submissions
|
|
839
|
+
add_action('admin_post_my_plugin_save', function(): void {
|
|
840
|
+
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'my_plugin_save')) {
|
|
841
|
+
wp_die(__('Security check failed', 'my-plugin'));
|
|
842
|
+
}
|
|
843
|
+
// Process form
|
|
844
|
+
});
|
|
845
|
+
```
|