@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,1041 @@
|
|
|
1
|
+
# Plugin Architecture
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Plugin Structure
|
|
6
|
+
|
|
7
|
+
### Minimal Plugin Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
plugin-name/
|
|
11
|
+
├── plugin-name.php # Main plugin file with header
|
|
12
|
+
├── uninstall.php # Cleanup on uninstall
|
|
13
|
+
├── includes/
|
|
14
|
+
│ ├── class-plugin-name.php
|
|
15
|
+
│ ├── class-activator.php
|
|
16
|
+
│ ├── class-deactivator.php
|
|
17
|
+
│ └── class-loader.php
|
|
18
|
+
├── admin/
|
|
19
|
+
│ ├── class-admin.php
|
|
20
|
+
│ ├── css/
|
|
21
|
+
│ └── js/
|
|
22
|
+
├── public/
|
|
23
|
+
│ ├── class-public.php
|
|
24
|
+
│ ├── css/
|
|
25
|
+
│ └── js/
|
|
26
|
+
├── languages/
|
|
27
|
+
│ └── plugin-name.pot
|
|
28
|
+
└── README.txt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Full Plugin Structure (Enterprise)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
plugin-name/
|
|
35
|
+
├── plugin-name.php
|
|
36
|
+
├── uninstall.php
|
|
37
|
+
├── composer.json
|
|
38
|
+
├── phpcs.xml.dist
|
|
39
|
+
├── phpunit.xml.dist
|
|
40
|
+
├── includes/
|
|
41
|
+
│ ├── class-plugin.php # Main plugin class
|
|
42
|
+
│ ├── class-activator.php # Activation logic
|
|
43
|
+
│ ├── class-deactivator.php # Deactivation logic
|
|
44
|
+
│ ├── class-loader.php # Hook loader
|
|
45
|
+
│ ├── class-i18n.php # Internationalization
|
|
46
|
+
│ ├── Traits/
|
|
47
|
+
│ │ └── Singleton.php
|
|
48
|
+
│ ├── Interfaces/
|
|
49
|
+
│ │ ├── Registrable.php
|
|
50
|
+
│ │ └── Hookable.php
|
|
51
|
+
│ ├── Services/
|
|
52
|
+
│ │ └── class-api-service.php
|
|
53
|
+
│ └── Repositories/
|
|
54
|
+
│ └── class-data-repository.php
|
|
55
|
+
├── admin/
|
|
56
|
+
│ ├── class-admin.php
|
|
57
|
+
│ ├── class-settings.php
|
|
58
|
+
│ ├── partials/
|
|
59
|
+
│ │ └── settings-page.php
|
|
60
|
+
│ ├── css/
|
|
61
|
+
│ └── js/
|
|
62
|
+
├── public/
|
|
63
|
+
│ ├── class-frontend.php
|
|
64
|
+
│ ├── partials/
|
|
65
|
+
│ ├── css/
|
|
66
|
+
│ └── js/
|
|
67
|
+
├── blocks/
|
|
68
|
+
│ └── custom-block/
|
|
69
|
+
├── templates/
|
|
70
|
+
│ └── single-custom-type.php
|
|
71
|
+
├── languages/
|
|
72
|
+
├── tests/
|
|
73
|
+
│ ├── bootstrap.php
|
|
74
|
+
│ └── unit/
|
|
75
|
+
└── vendor/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Main Plugin File
|
|
81
|
+
|
|
82
|
+
### Plugin Header
|
|
83
|
+
|
|
84
|
+
```php
|
|
85
|
+
<?php
|
|
86
|
+
/**
|
|
87
|
+
* Plugin Name: Plugin Name
|
|
88
|
+
* Plugin URI: https://example.com/plugin-name
|
|
89
|
+
* Description: A brief description of what this plugin does.
|
|
90
|
+
* Version: 1.0.0
|
|
91
|
+
* Requires at least: 6.4
|
|
92
|
+
* Requires PHP: 8.1
|
|
93
|
+
* Author: Author Name
|
|
94
|
+
* Author URI: https://example.com
|
|
95
|
+
* License: GPL v2 or later
|
|
96
|
+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
97
|
+
* Text Domain: plugin-name
|
|
98
|
+
* Domain Path: /languages
|
|
99
|
+
* Update URI: https://example.com/plugin-name
|
|
100
|
+
*
|
|
101
|
+
* @package PluginName
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
declare(strict_types=1);
|
|
105
|
+
|
|
106
|
+
namespace PluginName;
|
|
107
|
+
|
|
108
|
+
// Prevent direct access
|
|
109
|
+
defined('ABSPATH') || exit;
|
|
110
|
+
|
|
111
|
+
// Plugin constants
|
|
112
|
+
define('PLUGIN_NAME_VERSION', '1.0.0');
|
|
113
|
+
define('PLUGIN_NAME_FILE', __FILE__);
|
|
114
|
+
define('PLUGIN_NAME_PATH', plugin_dir_path(__FILE__));
|
|
115
|
+
define('PLUGIN_NAME_URL', plugin_dir_url(__FILE__));
|
|
116
|
+
define('PLUGIN_NAME_BASENAME', plugin_basename(__FILE__));
|
|
117
|
+
|
|
118
|
+
// Autoloader
|
|
119
|
+
if (file_exists(PLUGIN_NAME_PATH . 'vendor/autoload.php')) {
|
|
120
|
+
require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Manual includes if no autoloader
|
|
124
|
+
require_once PLUGIN_NAME_PATH . 'includes/class-plugin.php';
|
|
125
|
+
require_once PLUGIN_NAME_PATH . 'includes/class-activator.php';
|
|
126
|
+
require_once PLUGIN_NAME_PATH . 'includes/class-deactivator.php';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Plugin activation hook
|
|
130
|
+
*/
|
|
131
|
+
function activate(): void {
|
|
132
|
+
Activator::activate();
|
|
133
|
+
}
|
|
134
|
+
register_activation_hook(__FILE__, __NAMESPACE__ . '\\activate');
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Plugin deactivation hook
|
|
138
|
+
*/
|
|
139
|
+
function deactivate(): void {
|
|
140
|
+
Deactivator::deactivate();
|
|
141
|
+
}
|
|
142
|
+
register_deactivation_hook(__FILE__, __NAMESPACE__ . '\\deactivate');
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Initialize the plugin
|
|
146
|
+
*/
|
|
147
|
+
function init(): void {
|
|
148
|
+
$plugin = new Plugin();
|
|
149
|
+
$plugin->run();
|
|
150
|
+
}
|
|
151
|
+
add_action('plugins_loaded', __NAMESPACE__ . '\\init');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Activation & Deactivation
|
|
157
|
+
|
|
158
|
+
### Activator Class
|
|
159
|
+
|
|
160
|
+
```php
|
|
161
|
+
<?php
|
|
162
|
+
declare(strict_types=1);
|
|
163
|
+
|
|
164
|
+
namespace PluginName;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Fired during plugin activation
|
|
168
|
+
*/
|
|
169
|
+
class Activator {
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Activation tasks
|
|
173
|
+
*/
|
|
174
|
+
public static function activate(): void {
|
|
175
|
+
// Check requirements
|
|
176
|
+
self::check_requirements();
|
|
177
|
+
|
|
178
|
+
// Create database tables
|
|
179
|
+
self::create_tables();
|
|
180
|
+
|
|
181
|
+
// Set default options
|
|
182
|
+
self::set_default_options();
|
|
183
|
+
|
|
184
|
+
// Schedule cron events
|
|
185
|
+
self::schedule_events();
|
|
186
|
+
|
|
187
|
+
// Add capabilities
|
|
188
|
+
self::add_capabilities();
|
|
189
|
+
|
|
190
|
+
// Flush rewrite rules (if registering CPT/taxonomy)
|
|
191
|
+
flush_rewrite_rules();
|
|
192
|
+
|
|
193
|
+
// Set activation flag for welcome notice
|
|
194
|
+
set_transient('plugin_name_activated', true, 30);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check system requirements
|
|
199
|
+
*/
|
|
200
|
+
private static function check_requirements(): void {
|
|
201
|
+
if (version_compare(PHP_VERSION, '8.1', '<')) {
|
|
202
|
+
deactivate_plugins(PLUGIN_NAME_BASENAME);
|
|
203
|
+
wp_die(
|
|
204
|
+
esc_html__('This plugin requires PHP 8.1 or higher.', 'plugin-name'),
|
|
205
|
+
'Plugin Activation Error',
|
|
206
|
+
['back_link' => true]
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
global $wp_version;
|
|
211
|
+
if (version_compare($wp_version, '6.4', '<')) {
|
|
212
|
+
deactivate_plugins(PLUGIN_NAME_BASENAME);
|
|
213
|
+
wp_die(
|
|
214
|
+
esc_html__('This plugin requires WordPress 6.4 or higher.', 'plugin-name'),
|
|
215
|
+
'Plugin Activation Error',
|
|
216
|
+
['back_link' => true]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create custom database tables
|
|
223
|
+
*/
|
|
224
|
+
private static function create_tables(): void {
|
|
225
|
+
global $wpdb;
|
|
226
|
+
|
|
227
|
+
$charset_collate = $wpdb->get_charset_collate();
|
|
228
|
+
$table_name = $wpdb->prefix . 'plugin_name_data';
|
|
229
|
+
|
|
230
|
+
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
|
231
|
+
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
232
|
+
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
233
|
+
data_key varchar(191) NOT NULL,
|
|
234
|
+
data_value longtext,
|
|
235
|
+
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
|
236
|
+
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
237
|
+
PRIMARY KEY (id),
|
|
238
|
+
KEY user_id (user_id),
|
|
239
|
+
KEY data_key (data_key)
|
|
240
|
+
) {$charset_collate};";
|
|
241
|
+
|
|
242
|
+
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
243
|
+
dbDelta($sql);
|
|
244
|
+
|
|
245
|
+
// Store DB version
|
|
246
|
+
update_option('plugin_name_db_version', PLUGIN_NAME_VERSION);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set default options
|
|
251
|
+
*/
|
|
252
|
+
private static function set_default_options(): void {
|
|
253
|
+
$defaults = [
|
|
254
|
+
'enabled' => true,
|
|
255
|
+
'api_key' => '',
|
|
256
|
+
'cache_duration' => 3600,
|
|
257
|
+
'items_per_page' => 10,
|
|
258
|
+
'allowed_roles' => ['administrator', 'editor'],
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
if (get_option('plugin_name_settings') === false) {
|
|
262
|
+
add_option('plugin_name_settings', $defaults);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Schedule cron events
|
|
268
|
+
*/
|
|
269
|
+
private static function schedule_events(): void {
|
|
270
|
+
if (!wp_next_scheduled('plugin_name_daily_cleanup')) {
|
|
271
|
+
wp_schedule_event(time(), 'daily', 'plugin_name_daily_cleanup');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Add custom capabilities
|
|
277
|
+
*/
|
|
278
|
+
private static function add_capabilities(): void {
|
|
279
|
+
$admin = get_role('administrator');
|
|
280
|
+
if ($admin) {
|
|
281
|
+
$admin->add_cap('manage_plugin_name');
|
|
282
|
+
$admin->add_cap('edit_plugin_name_data');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Deactivator Class
|
|
289
|
+
|
|
290
|
+
```php
|
|
291
|
+
<?php
|
|
292
|
+
declare(strict_types=1);
|
|
293
|
+
|
|
294
|
+
namespace PluginName;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Fired during plugin deactivation
|
|
298
|
+
*/
|
|
299
|
+
class Deactivator {
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Deactivation tasks
|
|
303
|
+
*/
|
|
304
|
+
public static function deactivate(): void {
|
|
305
|
+
// Clear scheduled events
|
|
306
|
+
self::clear_scheduled_events();
|
|
307
|
+
|
|
308
|
+
// Clear transients
|
|
309
|
+
self::clear_transients();
|
|
310
|
+
|
|
311
|
+
// Flush rewrite rules
|
|
312
|
+
flush_rewrite_rules();
|
|
313
|
+
|
|
314
|
+
// Note: Do NOT delete options or tables here
|
|
315
|
+
// That should only happen in uninstall.php
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Clear all scheduled cron events
|
|
320
|
+
*/
|
|
321
|
+
private static function clear_scheduled_events(): void {
|
|
322
|
+
$timestamp = wp_next_scheduled('plugin_name_daily_cleanup');
|
|
323
|
+
if ($timestamp) {
|
|
324
|
+
wp_unschedule_event($timestamp, 'plugin_name_daily_cleanup');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clear all instances of our events
|
|
328
|
+
wp_clear_scheduled_hook('plugin_name_daily_cleanup');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Clear transients
|
|
333
|
+
*/
|
|
334
|
+
private static function clear_transients(): void {
|
|
335
|
+
global $wpdb;
|
|
336
|
+
|
|
337
|
+
// Clear specific transients
|
|
338
|
+
delete_transient('plugin_name_cache');
|
|
339
|
+
delete_transient('plugin_name_activated');
|
|
340
|
+
|
|
341
|
+
// Clear all plugin transients (use with caution)
|
|
342
|
+
$wpdb->query(
|
|
343
|
+
$wpdb->prepare(
|
|
344
|
+
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
|
|
345
|
+
'_transient_plugin_name_%',
|
|
346
|
+
'_transient_timeout_plugin_name_%'
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### uninstall.php
|
|
354
|
+
|
|
355
|
+
```php
|
|
356
|
+
<?php
|
|
357
|
+
/**
|
|
358
|
+
* Uninstall script - runs when plugin is deleted
|
|
359
|
+
*
|
|
360
|
+
* @package PluginName
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
// If uninstall not called from WordPress, exit
|
|
364
|
+
if (!defined('WP_UNINSTALL_PLUGIN')) {
|
|
365
|
+
exit;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check if we should preserve data
|
|
369
|
+
$settings = get_option('plugin_name_settings', []);
|
|
370
|
+
$preserve_data = $settings['preserve_data_on_uninstall'] ?? false;
|
|
371
|
+
|
|
372
|
+
if (!$preserve_data) {
|
|
373
|
+
global $wpdb;
|
|
374
|
+
|
|
375
|
+
// Delete options
|
|
376
|
+
delete_option('plugin_name_settings');
|
|
377
|
+
delete_option('plugin_name_db_version');
|
|
378
|
+
|
|
379
|
+
// Delete user meta
|
|
380
|
+
$wpdb->query("DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'plugin_name_%'");
|
|
381
|
+
|
|
382
|
+
// Delete post meta
|
|
383
|
+
$wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_plugin_name_%'");
|
|
384
|
+
|
|
385
|
+
// Delete custom tables
|
|
386
|
+
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}plugin_name_data");
|
|
387
|
+
|
|
388
|
+
// Delete transients
|
|
389
|
+
$wpdb->query(
|
|
390
|
+
$wpdb->prepare(
|
|
391
|
+
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
|
|
392
|
+
'_transient_plugin_name_%',
|
|
393
|
+
'_transient_timeout_plugin_name_%'
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Remove capabilities
|
|
398
|
+
$roles = ['administrator', 'editor'];
|
|
399
|
+
foreach ($roles as $role_name) {
|
|
400
|
+
$role = get_role($role_name);
|
|
401
|
+
if ($role) {
|
|
402
|
+
$role->remove_cap('manage_plugin_name');
|
|
403
|
+
$role->remove_cap('edit_plugin_name_data');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Clear any cached data
|
|
408
|
+
wp_cache_flush();
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Settings API
|
|
415
|
+
|
|
416
|
+
### Settings Class
|
|
417
|
+
|
|
418
|
+
```php
|
|
419
|
+
<?php
|
|
420
|
+
declare(strict_types=1);
|
|
421
|
+
|
|
422
|
+
namespace PluginName\Admin;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Plugin settings management
|
|
426
|
+
*/
|
|
427
|
+
class Settings {
|
|
428
|
+
|
|
429
|
+
private const OPTION_GROUP = 'plugin_name_settings';
|
|
430
|
+
private const OPTION_NAME = 'plugin_name_settings';
|
|
431
|
+
private const PAGE_SLUG = 'plugin-name-settings';
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Initialize settings
|
|
435
|
+
*/
|
|
436
|
+
public function init(): void {
|
|
437
|
+
add_action('admin_menu', [$this, 'add_menu_page']);
|
|
438
|
+
add_action('admin_init', [$this, 'register_settings']);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Add admin menu page
|
|
443
|
+
*/
|
|
444
|
+
public function add_menu_page(): void {
|
|
445
|
+
add_options_page(
|
|
446
|
+
__('Plugin Name Settings', 'plugin-name'),
|
|
447
|
+
__('Plugin Name', 'plugin-name'),
|
|
448
|
+
'manage_options',
|
|
449
|
+
self::PAGE_SLUG,
|
|
450
|
+
[$this, 'render_settings_page']
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Register all settings
|
|
456
|
+
*/
|
|
457
|
+
public function register_settings(): void {
|
|
458
|
+
register_setting(
|
|
459
|
+
self::OPTION_GROUP,
|
|
460
|
+
self::OPTION_NAME,
|
|
461
|
+
[
|
|
462
|
+
'type' => 'array',
|
|
463
|
+
'sanitize_callback' => [$this, 'sanitize_settings'],
|
|
464
|
+
'default' => $this->get_defaults(),
|
|
465
|
+
]
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// General section
|
|
469
|
+
add_settings_section(
|
|
470
|
+
'plugin_name_general',
|
|
471
|
+
__('General Settings', 'plugin-name'),
|
|
472
|
+
[$this, 'render_general_section'],
|
|
473
|
+
self::PAGE_SLUG
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// API section
|
|
477
|
+
add_settings_section(
|
|
478
|
+
'plugin_name_api',
|
|
479
|
+
__('API Settings', 'plugin-name'),
|
|
480
|
+
[$this, 'render_api_section'],
|
|
481
|
+
self::PAGE_SLUG
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Fields
|
|
485
|
+
$this->add_fields();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Add settings fields
|
|
490
|
+
*/
|
|
491
|
+
private function add_fields(): void {
|
|
492
|
+
// Enable field
|
|
493
|
+
add_settings_field(
|
|
494
|
+
'enabled',
|
|
495
|
+
__('Enable Plugin', 'plugin-name'),
|
|
496
|
+
[$this, 'render_checkbox_field'],
|
|
497
|
+
self::PAGE_SLUG,
|
|
498
|
+
'plugin_name_general',
|
|
499
|
+
[
|
|
500
|
+
'label_for' => 'enabled',
|
|
501
|
+
'description' => __('Enable or disable the plugin functionality.', 'plugin-name'),
|
|
502
|
+
]
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// Items per page
|
|
506
|
+
add_settings_field(
|
|
507
|
+
'items_per_page',
|
|
508
|
+
__('Items Per Page', 'plugin-name'),
|
|
509
|
+
[$this, 'render_number_field'],
|
|
510
|
+
self::PAGE_SLUG,
|
|
511
|
+
'plugin_name_general',
|
|
512
|
+
[
|
|
513
|
+
'label_for' => 'items_per_page',
|
|
514
|
+
'min' => 1,
|
|
515
|
+
'max' => 100,
|
|
516
|
+
'description' => __('Number of items to display per page.', 'plugin-name'),
|
|
517
|
+
]
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// API Key
|
|
521
|
+
add_settings_field(
|
|
522
|
+
'api_key',
|
|
523
|
+
__('API Key', 'plugin-name'),
|
|
524
|
+
[$this, 'render_text_field'],
|
|
525
|
+
self::PAGE_SLUG,
|
|
526
|
+
'plugin_name_api',
|
|
527
|
+
[
|
|
528
|
+
'label_for' => 'api_key',
|
|
529
|
+
'type' => 'password',
|
|
530
|
+
'description' => __('Enter your API key for external service.', 'plugin-name'),
|
|
531
|
+
]
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Cache duration
|
|
535
|
+
add_settings_field(
|
|
536
|
+
'cache_duration',
|
|
537
|
+
__('Cache Duration', 'plugin-name'),
|
|
538
|
+
[$this, 'render_select_field'],
|
|
539
|
+
self::PAGE_SLUG,
|
|
540
|
+
'plugin_name_api',
|
|
541
|
+
[
|
|
542
|
+
'label_for' => 'cache_duration',
|
|
543
|
+
'options' => [
|
|
544
|
+
'900' => __('15 minutes', 'plugin-name'),
|
|
545
|
+
'1800' => __('30 minutes', 'plugin-name'),
|
|
546
|
+
'3600' => __('1 hour', 'plugin-name'),
|
|
547
|
+
'86400' => __('1 day', 'plugin-name'),
|
|
548
|
+
],
|
|
549
|
+
'description' => __('How long to cache API responses.', 'plugin-name'),
|
|
550
|
+
]
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Get default settings
|
|
556
|
+
*/
|
|
557
|
+
private function get_defaults(): array {
|
|
558
|
+
return [
|
|
559
|
+
'enabled' => true,
|
|
560
|
+
'api_key' => '',
|
|
561
|
+
'cache_duration' => 3600,
|
|
562
|
+
'items_per_page' => 10,
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Sanitize settings
|
|
568
|
+
*/
|
|
569
|
+
public function sanitize_settings(array $input): array {
|
|
570
|
+
$sanitized = [];
|
|
571
|
+
|
|
572
|
+
$sanitized['enabled'] = !empty($input['enabled']);
|
|
573
|
+
|
|
574
|
+
$sanitized['api_key'] = sanitize_text_field($input['api_key'] ?? '');
|
|
575
|
+
|
|
576
|
+
$sanitized['cache_duration'] = absint($input['cache_duration'] ?? 3600);
|
|
577
|
+
if (!in_array($sanitized['cache_duration'], [900, 1800, 3600, 86400], true)) {
|
|
578
|
+
$sanitized['cache_duration'] = 3600;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
$sanitized['items_per_page'] = absint($input['items_per_page'] ?? 10);
|
|
582
|
+
$sanitized['items_per_page'] = max(1, min(100, $sanitized['items_per_page']));
|
|
583
|
+
|
|
584
|
+
return $sanitized;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Render settings page
|
|
589
|
+
*/
|
|
590
|
+
public function render_settings_page(): void {
|
|
591
|
+
if (!current_user_can('manage_options')) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Show success message
|
|
596
|
+
if (isset($_GET['settings-updated'])) {
|
|
597
|
+
add_settings_error(
|
|
598
|
+
self::OPTION_GROUP,
|
|
599
|
+
'settings_updated',
|
|
600
|
+
__('Settings saved.', 'plugin-name'),
|
|
601
|
+
'updated'
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
?>
|
|
605
|
+
<div class="wrap">
|
|
606
|
+
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
|
607
|
+
|
|
608
|
+
<?php settings_errors(self::OPTION_GROUP); ?>
|
|
609
|
+
|
|
610
|
+
<form action="options.php" method="post">
|
|
611
|
+
<?php
|
|
612
|
+
settings_fields(self::OPTION_GROUP);
|
|
613
|
+
do_settings_sections(self::PAGE_SLUG);
|
|
614
|
+
submit_button(__('Save Settings', 'plugin-name'));
|
|
615
|
+
?>
|
|
616
|
+
</form>
|
|
617
|
+
</div>
|
|
618
|
+
<?php
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Render general section description
|
|
623
|
+
*/
|
|
624
|
+
public function render_general_section(): void {
|
|
625
|
+
echo '<p>' . esc_html__('Configure general plugin settings.', 'plugin-name') . '</p>';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Render API section description
|
|
630
|
+
*/
|
|
631
|
+
public function render_api_section(): void {
|
|
632
|
+
echo '<p>' . esc_html__('Configure API connection settings.', 'plugin-name') . '</p>';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Render checkbox field
|
|
637
|
+
*/
|
|
638
|
+
public function render_checkbox_field(array $args): void {
|
|
639
|
+
$options = get_option(self::OPTION_NAME, $this->get_defaults());
|
|
640
|
+
$value = $options[$args['label_for']] ?? false;
|
|
641
|
+
?>
|
|
642
|
+
<input type="checkbox"
|
|
643
|
+
id="<?php echo esc_attr($args['label_for']); ?>"
|
|
644
|
+
name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
|
|
645
|
+
value="1"
|
|
646
|
+
<?php checked($value, true); ?>
|
|
647
|
+
/>
|
|
648
|
+
<?php if (!empty($args['description'])): ?>
|
|
649
|
+
<p class="description"><?php echo esc_html($args['description']); ?></p>
|
|
650
|
+
<?php endif;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Render text field
|
|
655
|
+
*/
|
|
656
|
+
public function render_text_field(array $args): void {
|
|
657
|
+
$options = get_option(self::OPTION_NAME, $this->get_defaults());
|
|
658
|
+
$value = $options[$args['label_for']] ?? '';
|
|
659
|
+
$type = $args['type'] ?? 'text';
|
|
660
|
+
?>
|
|
661
|
+
<input type="<?php echo esc_attr($type); ?>"
|
|
662
|
+
id="<?php echo esc_attr($args['label_for']); ?>"
|
|
663
|
+
name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
|
|
664
|
+
value="<?php echo esc_attr($value); ?>"
|
|
665
|
+
class="regular-text"
|
|
666
|
+
/>
|
|
667
|
+
<?php if (!empty($args['description'])): ?>
|
|
668
|
+
<p class="description"><?php echo esc_html($args['description']); ?></p>
|
|
669
|
+
<?php endif;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Render number field
|
|
674
|
+
*/
|
|
675
|
+
public function render_number_field(array $args): void {
|
|
676
|
+
$options = get_option(self::OPTION_NAME, $this->get_defaults());
|
|
677
|
+
$value = $options[$args['label_for']] ?? 0;
|
|
678
|
+
?>
|
|
679
|
+
<input type="number"
|
|
680
|
+
id="<?php echo esc_attr($args['label_for']); ?>"
|
|
681
|
+
name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>"
|
|
682
|
+
value="<?php echo esc_attr($value); ?>"
|
|
683
|
+
min="<?php echo esc_attr($args['min'] ?? 0); ?>"
|
|
684
|
+
max="<?php echo esc_attr($args['max'] ?? 100); ?>"
|
|
685
|
+
class="small-text"
|
|
686
|
+
/>
|
|
687
|
+
<?php if (!empty($args['description'])): ?>
|
|
688
|
+
<p class="description"><?php echo esc_html($args['description']); ?></p>
|
|
689
|
+
<?php endif;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Render select field
|
|
694
|
+
*/
|
|
695
|
+
public function render_select_field(array $args): void {
|
|
696
|
+
$options = get_option(self::OPTION_NAME, $this->get_defaults());
|
|
697
|
+
$value = $options[$args['label_for']] ?? '';
|
|
698
|
+
?>
|
|
699
|
+
<select id="<?php echo esc_attr($args['label_for']); ?>"
|
|
700
|
+
name="<?php echo esc_attr(self::OPTION_NAME . '[' . $args['label_for'] . ']'); ?>">
|
|
701
|
+
<?php foreach ($args['options'] as $key => $label): ?>
|
|
702
|
+
<option value="<?php echo esc_attr($key); ?>" <?php selected($value, $key); ?>>
|
|
703
|
+
<?php echo esc_html($label); ?>
|
|
704
|
+
</option>
|
|
705
|
+
<?php endforeach; ?>
|
|
706
|
+
</select>
|
|
707
|
+
<?php if (!empty($args['description'])): ?>
|
|
708
|
+
<p class="description"><?php echo esc_html($args['description']); ?></p>
|
|
709
|
+
<?php endif;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Custom Post Types & Taxonomies
|
|
717
|
+
|
|
718
|
+
### Registering Custom Post Types
|
|
719
|
+
|
|
720
|
+
```php
|
|
721
|
+
<?php
|
|
722
|
+
declare(strict_types=1);
|
|
723
|
+
|
|
724
|
+
namespace PluginName;
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Register custom post types and taxonomies
|
|
728
|
+
*/
|
|
729
|
+
class CustomPostTypes {
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Initialize
|
|
733
|
+
*/
|
|
734
|
+
public function init(): void {
|
|
735
|
+
add_action('init', [$this, 'register_post_types']);
|
|
736
|
+
add_action('init', [$this, 'register_taxonomies']);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Register custom post type
|
|
741
|
+
*/
|
|
742
|
+
public function register_post_types(): void {
|
|
743
|
+
$labels = [
|
|
744
|
+
'name' => _x('Products', 'Post Type General Name', 'plugin-name'),
|
|
745
|
+
'singular_name' => _x('Product', 'Post Type Singular Name', 'plugin-name'),
|
|
746
|
+
'menu_name' => __('Products', 'plugin-name'),
|
|
747
|
+
'name_admin_bar' => __('Product', 'plugin-name'),
|
|
748
|
+
'archives' => __('Product Archives', 'plugin-name'),
|
|
749
|
+
'attributes' => __('Product Attributes', 'plugin-name'),
|
|
750
|
+
'parent_item_colon' => __('Parent Product:', 'plugin-name'),
|
|
751
|
+
'all_items' => __('All Products', 'plugin-name'),
|
|
752
|
+
'add_new_item' => __('Add New Product', 'plugin-name'),
|
|
753
|
+
'add_new' => __('Add New', 'plugin-name'),
|
|
754
|
+
'new_item' => __('New Product', 'plugin-name'),
|
|
755
|
+
'edit_item' => __('Edit Product', 'plugin-name'),
|
|
756
|
+
'update_item' => __('Update Product', 'plugin-name'),
|
|
757
|
+
'view_item' => __('View Product', 'plugin-name'),
|
|
758
|
+
'view_items' => __('View Products', 'plugin-name'),
|
|
759
|
+
'search_items' => __('Search Product', 'plugin-name'),
|
|
760
|
+
'not_found' => __('Not found', 'plugin-name'),
|
|
761
|
+
'not_found_in_trash' => __('Not found in Trash', 'plugin-name'),
|
|
762
|
+
'featured_image' => __('Featured Image', 'plugin-name'),
|
|
763
|
+
'set_featured_image' => __('Set featured image', 'plugin-name'),
|
|
764
|
+
'remove_featured_image' => __('Remove featured image', 'plugin-name'),
|
|
765
|
+
'use_featured_image' => __('Use as featured image', 'plugin-name'),
|
|
766
|
+
'insert_into_item' => __('Insert into product', 'plugin-name'),
|
|
767
|
+
'uploaded_to_this_item' => __('Uploaded to this product', 'plugin-name'),
|
|
768
|
+
'items_list' => __('Products list', 'plugin-name'),
|
|
769
|
+
'items_list_navigation' => __('Products list navigation', 'plugin-name'),
|
|
770
|
+
'filter_items_list' => __('Filter products list', 'plugin-name'),
|
|
771
|
+
];
|
|
772
|
+
|
|
773
|
+
$args = [
|
|
774
|
+
'label' => __('Product', 'plugin-name'),
|
|
775
|
+
'description' => __('Product custom post type', 'plugin-name'),
|
|
776
|
+
'labels' => $labels,
|
|
777
|
+
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'revisions'],
|
|
778
|
+
'taxonomies' => ['product_category', 'product_tag'],
|
|
779
|
+
'hierarchical' => false,
|
|
780
|
+
'public' => true,
|
|
781
|
+
'show_ui' => true,
|
|
782
|
+
'show_in_menu' => true,
|
|
783
|
+
'menu_position' => 20,
|
|
784
|
+
'menu_icon' => 'dashicons-products',
|
|
785
|
+
'show_in_admin_bar' => true,
|
|
786
|
+
'show_in_nav_menus' => true,
|
|
787
|
+
'can_export' => true,
|
|
788
|
+
'has_archive' => 'products',
|
|
789
|
+
'exclude_from_search' => false,
|
|
790
|
+
'publicly_queryable' => true,
|
|
791
|
+
'capability_type' => 'post',
|
|
792
|
+
'show_in_rest' => true, // Enable Gutenberg
|
|
793
|
+
'rest_base' => 'products',
|
|
794
|
+
'rewrite' => [
|
|
795
|
+
'slug' => 'product',
|
|
796
|
+
'with_front' => false,
|
|
797
|
+
],
|
|
798
|
+
'template' => [
|
|
799
|
+
['core/image', ['align' => 'wide']],
|
|
800
|
+
['core/paragraph', ['placeholder' => 'Product description...']],
|
|
801
|
+
],
|
|
802
|
+
'template_lock' => false, // 'all', 'insert', false
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
register_post_type('product', $args);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Register custom taxonomies
|
|
810
|
+
*/
|
|
811
|
+
public function register_taxonomies(): void {
|
|
812
|
+
// Product Category (hierarchical like categories)
|
|
813
|
+
$category_labels = [
|
|
814
|
+
'name' => _x('Product Categories', 'taxonomy general name', 'plugin-name'),
|
|
815
|
+
'singular_name' => _x('Product Category', 'taxonomy singular name', 'plugin-name'),
|
|
816
|
+
'search_items' => __('Search Product Categories', 'plugin-name'),
|
|
817
|
+
'all_items' => __('All Product Categories', 'plugin-name'),
|
|
818
|
+
'parent_item' => __('Parent Product Category', 'plugin-name'),
|
|
819
|
+
'parent_item_colon' => __('Parent Product Category:', 'plugin-name'),
|
|
820
|
+
'edit_item' => __('Edit Product Category', 'plugin-name'),
|
|
821
|
+
'update_item' => __('Update Product Category', 'plugin-name'),
|
|
822
|
+
'add_new_item' => __('Add New Product Category', 'plugin-name'),
|
|
823
|
+
'new_item_name' => __('New Product Category Name', 'plugin-name'),
|
|
824
|
+
'menu_name' => __('Categories', 'plugin-name'),
|
|
825
|
+
];
|
|
826
|
+
|
|
827
|
+
register_taxonomy('product_category', ['product'], [
|
|
828
|
+
'hierarchical' => true,
|
|
829
|
+
'labels' => $category_labels,
|
|
830
|
+
'show_ui' => true,
|
|
831
|
+
'show_admin_column' => true,
|
|
832
|
+
'query_var' => true,
|
|
833
|
+
'show_in_rest' => true,
|
|
834
|
+
'rewrite' => ['slug' => 'product-category'],
|
|
835
|
+
]);
|
|
836
|
+
|
|
837
|
+
// Product Tags (non-hierarchical like tags)
|
|
838
|
+
$tag_labels = [
|
|
839
|
+
'name' => _x('Product Tags', 'taxonomy general name', 'plugin-name'),
|
|
840
|
+
'singular_name' => _x('Product Tag', 'taxonomy singular name', 'plugin-name'),
|
|
841
|
+
'search_items' => __('Search Product Tags', 'plugin-name'),
|
|
842
|
+
'popular_items' => __('Popular Product Tags', 'plugin-name'),
|
|
843
|
+
'all_items' => __('All Product Tags', 'plugin-name'),
|
|
844
|
+
'edit_item' => __('Edit Product Tag', 'plugin-name'),
|
|
845
|
+
'update_item' => __('Update Product Tag', 'plugin-name'),
|
|
846
|
+
'add_new_item' => __('Add New Product Tag', 'plugin-name'),
|
|
847
|
+
'new_item_name' => __('New Product Tag Name', 'plugin-name'),
|
|
848
|
+
'separate_items_with_commas' => __('Separate tags with commas', 'plugin-name'),
|
|
849
|
+
'add_or_remove_items' => __('Add or remove tags', 'plugin-name'),
|
|
850
|
+
'choose_from_most_used' => __('Choose from the most used tags', 'plugin-name'),
|
|
851
|
+
'not_found' => __('No tags found.', 'plugin-name'),
|
|
852
|
+
'menu_name' => __('Tags', 'plugin-name'),
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
register_taxonomy('product_tag', ['product'], [
|
|
856
|
+
'hierarchical' => false,
|
|
857
|
+
'labels' => $tag_labels,
|
|
858
|
+
'show_ui' => true,
|
|
859
|
+
'show_admin_column' => true,
|
|
860
|
+
'query_var' => true,
|
|
861
|
+
'show_in_rest' => true,
|
|
862
|
+
'rewrite' => ['slug' => 'product-tag'],
|
|
863
|
+
]);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## Plugin Updates
|
|
871
|
+
|
|
872
|
+
### Self-Hosted Update Checker
|
|
873
|
+
|
|
874
|
+
```php
|
|
875
|
+
<?php
|
|
876
|
+
declare(strict_types=1);
|
|
877
|
+
|
|
878
|
+
namespace PluginName;
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Handle plugin updates from custom server
|
|
882
|
+
*/
|
|
883
|
+
class UpdateChecker {
|
|
884
|
+
|
|
885
|
+
private string $plugin_slug;
|
|
886
|
+
private string $update_url;
|
|
887
|
+
private string $plugin_file;
|
|
888
|
+
|
|
889
|
+
public function __construct() {
|
|
890
|
+
$this->plugin_slug = 'plugin-name';
|
|
891
|
+
$this->plugin_file = PLUGIN_NAME_BASENAME;
|
|
892
|
+
$this->update_url = 'https://example.com/api/plugin-updates/';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Initialize update checker
|
|
897
|
+
*/
|
|
898
|
+
public function init(): void {
|
|
899
|
+
add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_update']);
|
|
900
|
+
add_filter('plugins_api', [$this, 'plugin_info'], 20, 3);
|
|
901
|
+
add_action('in_plugin_update_message-' . $this->plugin_file, [$this, 'update_message'], 10, 2);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Check for plugin updates
|
|
906
|
+
*/
|
|
907
|
+
public function check_for_update(object $transient): object {
|
|
908
|
+
if (empty($transient->checked)) {
|
|
909
|
+
return $transient;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
$remote = $this->get_remote_info();
|
|
913
|
+
|
|
914
|
+
if (
|
|
915
|
+
$remote &&
|
|
916
|
+
version_compare(PLUGIN_NAME_VERSION, $remote->version, '<') &&
|
|
917
|
+
version_compare($remote->requires, get_bloginfo('version'), '<=') &&
|
|
918
|
+
version_compare($remote->requires_php, PHP_VERSION, '<=')
|
|
919
|
+
) {
|
|
920
|
+
$transient->response[$this->plugin_file] = (object) [
|
|
921
|
+
'slug' => $this->plugin_slug,
|
|
922
|
+
'plugin' => $this->plugin_file,
|
|
923
|
+
'new_version' => $remote->version,
|
|
924
|
+
'url' => $remote->url,
|
|
925
|
+
'package' => $remote->package,
|
|
926
|
+
'icons' => (array) ($remote->icons ?? []),
|
|
927
|
+
'banners' => (array) ($remote->banners ?? []),
|
|
928
|
+
'tested' => $remote->tested ?? '',
|
|
929
|
+
'requires' => $remote->requires ?? '',
|
|
930
|
+
];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return $transient;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Plugin information for update screen
|
|
938
|
+
*/
|
|
939
|
+
public function plugin_info(mixed $result, string $action, object $args): mixed {
|
|
940
|
+
if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
|
|
941
|
+
return $result;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
$remote = $this->get_remote_info();
|
|
945
|
+
|
|
946
|
+
if (!$remote) {
|
|
947
|
+
return $result;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return (object) [
|
|
951
|
+
'name' => $remote->name,
|
|
952
|
+
'slug' => $this->plugin_slug,
|
|
953
|
+
'version' => $remote->version,
|
|
954
|
+
'author' => $remote->author,
|
|
955
|
+
'author_profile' => $remote->author_profile ?? '',
|
|
956
|
+
'requires' => $remote->requires,
|
|
957
|
+
'tested' => $remote->tested,
|
|
958
|
+
'requires_php' => $remote->requires_php,
|
|
959
|
+
'sections' => (array) $remote->sections,
|
|
960
|
+
'download_link' => $remote->package,
|
|
961
|
+
'banners' => (array) ($remote->banners ?? []),
|
|
962
|
+
'icons' => (array) ($remote->icons ?? []),
|
|
963
|
+
'last_updated' => $remote->last_updated ?? '',
|
|
964
|
+
'homepage' => $remote->url ?? '',
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Custom update message
|
|
970
|
+
*/
|
|
971
|
+
public function update_message(array $plugin_data, object $response): void {
|
|
972
|
+
if (!empty($response->upgrade_notice)) {
|
|
973
|
+
printf(
|
|
974
|
+
'<br /><strong>%s</strong>: %s',
|
|
975
|
+
esc_html__('Upgrade Notice', 'plugin-name'),
|
|
976
|
+
esc_html($response->upgrade_notice)
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Get remote plugin information
|
|
983
|
+
*/
|
|
984
|
+
private function get_remote_info(): ?object {
|
|
985
|
+
$transient_key = 'plugin_name_update_info';
|
|
986
|
+
$remote = get_transient($transient_key);
|
|
987
|
+
|
|
988
|
+
if ($remote !== false) {
|
|
989
|
+
return $remote === 'error' ? null : $remote;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
$response = wp_remote_get($this->update_url . 'info.json', [
|
|
993
|
+
'timeout' => 10,
|
|
994
|
+
'headers' => [
|
|
995
|
+
'Accept' => 'application/json',
|
|
996
|
+
],
|
|
997
|
+
]);
|
|
998
|
+
|
|
999
|
+
if (
|
|
1000
|
+
is_wp_error($response) ||
|
|
1001
|
+
wp_remote_retrieve_response_code($response) !== 200 ||
|
|
1002
|
+
empty(wp_remote_retrieve_body($response))
|
|
1003
|
+
) {
|
|
1004
|
+
set_transient($transient_key, 'error', HOUR_IN_SECONDS);
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
$remote = json_decode(wp_remote_retrieve_body($response));
|
|
1009
|
+
set_transient($transient_key, $remote, 12 * HOUR_IN_SECONDS);
|
|
1010
|
+
|
|
1011
|
+
return $remote;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## Best Practices
|
|
1019
|
+
|
|
1020
|
+
### Do
|
|
1021
|
+
|
|
1022
|
+
- Use namespaces to avoid function/class name collisions
|
|
1023
|
+
- Follow WordPress Coding Standards (WPCS)
|
|
1024
|
+
- Include proper plugin headers with all required fields
|
|
1025
|
+
- Implement proper activation/deactivation/uninstall hooks
|
|
1026
|
+
- Use the Settings API for options pages
|
|
1027
|
+
- Register CPTs with `show_in_rest` for Gutenberg support
|
|
1028
|
+
- Cache remote API responses with transients
|
|
1029
|
+
- Provide translation support with text domains
|
|
1030
|
+
- Include a proper uninstall.php for cleanup
|
|
1031
|
+
|
|
1032
|
+
### Do Not
|
|
1033
|
+
|
|
1034
|
+
- Access the database directly without proper preparation
|
|
1035
|
+
- Store options without sanitization
|
|
1036
|
+
- Skip capability checks in admin functions
|
|
1037
|
+
- Leave orphaned data after uninstall
|
|
1038
|
+
- Hardcode plugin paths (use constants)
|
|
1039
|
+
- Register hooks in constructors (use init methods)
|
|
1040
|
+
- Modify core WordPress tables
|
|
1041
|
+
- Include heavy operations in activation hooks
|