wordpress-plugin-php
PHP and WordPress plugin development—security (nonce, capability, sanitize, escape), hooks, enqueue (dynamic version), AJAX, $wpdb, activation/deactivation/uninstall, prefix, option autoload, extensibility, file paths. Use when building or modifying WordPress plugins, PHP in WP context, plugin security, activation hooks, uninstall, or admin/frontend plugin code.
# WordPress Plugin PHP – Agent Skill
Brug denne skill når du skriver eller ændrer PHP i WordPress-plugins. Følg WordPress Coding Standards og plugin-sikkerhed uden at gætte på hooks, tabeller eller filstier—verificer i kodebasen.
## 1. Plugin-struktur og indgangspunkt
- **Hovedfil**: Én root PHP-fil med plugin header (Plugin Name, Version, Text Domain, etc.). Tjek `@wordpress-plugin` i header.
- **Direkte adgang**: Altid `if (!defined('ABSPATH')) { exit; }` øverst i hver PHP-fil der inkluderes.
- **Konstanter**: Definer plugin path/URL med `plugin_dir_path(__FILE__)` og `plugin_dir_url(__FILE__)`. Brug disse i stedet for hardcodede stier.
- **Indlæsning**: Brug `require_once` til classes og filer. Undgå at inkludere i global scope uden behov.
```php
// Eksempel: konstanter i hovedfil
if (!defined('MY_PLUGIN_DIR')) {
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
}
if (!defined('MY_PLUGIN_URL')) {
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
}
```
## 2. Sikkerhed (obligatorisk)
### Input
- **Alle superglobals**: `wp_unslash()` før sanitization.
- **Tekst**: `sanitize_text_field()`, `sanitize_textarea_field()`.
- **Tal/ID**: `intval()` eller `absint()`.
- **Keys/slugs**: `sanitize_key()`.
- **E-mail**: `sanitize_email()`.
- **URL**: `esc_url_raw()` til brug i DB eller redirects.
### Output
- **HTML**: `esc_html()` for plain text, `esc_attr()` i attributter.
- **URL**: `esc_url()` i links.
- **JavaScript-streng**: `wp_json_encode()` eller `esc_js()` efter kontekst.
### AJAX
- **Nonce**: `check_ajax_referer('action_name', 'nonce');` i hver AJAX-handler. Brug samme nonce-navn som i `wp_create_nonce()` på client.
- **Capability**: Tjek `current_user_can('manage_options')` (eller passende capability) for admin-AJAX, medmindre handlingen er bevidst offentlig.
- **Svar**: `wp_send_json_success($data)` eller `wp_send_json_error('message')`. Ingen rå `echo` af JSON.
### Database
- **Alle dynamiske SQL**: `$wpdb->prepare($sql, ...$args)`. Brug placeholders `%s`, `%d`, `%f`. Ingen string concatenation af brugerinput i SQL.
## 3. Hooks og AJAX
- **Registrering**: `add_action('wp_ajax_my_action', [$this, 'my_handler']);` for indloggede. `add_action('wp_ajax_nopriv_my_action', ...)` kun hvis handlingen skal være tilgængelig uden login.
- **Prioritet**: Brug fjerde parameter ved behov, fx `add_action('init', 'fn', 10, 1);`.
- **Filter**: `apply_filters('plugin_prefix_name', $value, $arg);` og `add_filter('plugin_prefix_name', 'callback', 10, 2);`.
Søg i kodebasen efter eksisterende action/filter-navne før du opfinder nye.
## 4. Scripts og styles
- **Dynamisk versionsnummer (cache busting)**: Brug ét versionsnummer fra plugin header i alle enqueue-kald. Så slipper serveren og browseren for gammel cache af CSS/JS, og udvikleren skal kun opdatere version ét sted (plugin header). Læs version fx med `get_plugin_data(__FILE__, false, false)['Version']`, definér en konstant én gang, og brug den som version-parameter i hver `wp_enqueue_script()` og `wp_enqueue_style()`. Undgå hardcodet version eller `null`—så opdateres cache ikke ved release.
- **Enqueue**: `wp_enqueue_script()` / `wp_enqueue_style()` med dependency array, **version** (plugin-konstant som ovenfor), og `true` for scripts i footer hvis det er ønsket.
- **Localize**: `wp_localize_script()` for at sende PHP-værdier (fx `ajax_url`, `nonce`) til JS. Brug samme handle som det script der skal modtage dem.
- **Kun hvor nødvendigt**: Hook ind på `wp_enqueue_scripts` (frontend) eller `admin_enqueue_scripts` (admin) og enqueue kun på de sider hvor plugin’en bruges (fx ved shortcode eller specifik admin-side).
```php
// Eksempel: ét sted for version, brug i alle enqueue-kald
$plugin_data = get_plugin_data(__FILE__, false, false);
if (!defined('MY_PLUGIN_VERSION')) {
define('MY_PLUGIN_VERSION', $plugin_data['Version']);
}
// I enqueue-funktion:
wp_enqueue_style('my-css', MY_PLUGIN_URL . 'style.css', array(), MY_PLUGIN_VERSION, 'all');
wp_enqueue_script('my-js', MY_PLUGIN_URL . 'script.js', ['jquery'], MY_PLUGIN_VERSION, true);
```
## 5. Database
- **Tabelnavne**: Brug `$wpdb->prefix` (fx `$wpdb->prefix . 'my_plugin_table'`). Verificer tabel- og kolonnenavne i kodebasen eller migration-filer.
- **Læsning**: `$wpdb->get_row()`, `$wpdb->get_results()` med `prepare()`.
- **Skrivning**: `$wpdb->insert()`, `$wpdb->update()`, `$wpdb->replace()` med format array. Brug ikke rå SQL med brugerinput uden `prepare()`.
- **Skemaændringer**: Følg projektets migrations-/opgraderingslogik (fx `dbDelta` eller egne migration-klasser). Opdater dokumentation ved ændringer.
## 6. Konfliktskyld og prefix
- **Prefix alt**: Funktioner, klasser, konstanter, hooks (actions/filters), option/transient-keys, cron-hooks, shortcodes, REST namespaces, script/style handles. Brug plugin slug eller forkortelse (fx `my_plugin_`, `My_Plugin_`). Så kolliderer plugin’en ikke med tema, andre plugins eller WP core.
- **Unikke handles**: `wp_enqueue_script('my_plugin_survey_js', ...)` ikke `survey-js` alene—andre plugins kan bruge samme navn.
## 7. Activation, deactivation og uninstall
- **Activation** (`register_activation_hook(__FILE__, 'callback')`): Opret tabeller, default options, flush rewrite rules hvis plugin registrerer post types. Tjek om det er første gang eller opgradering (fx gem DB-version); kør kun migrationer der mangler.
- **Deactivation** (`register_deactivation_hook`): Ryd midlertidigt op (fx unschedule cron). Slet ikke brugerdata eller options—brugeren kan genaktivere.
- **Uninstall** (fil `uninstall.php` i plugin-rod, kører kun når plugin slettes via WP): Her må du slette options, transients, cron events og evt. custom tabeller. Tjek `defined('WP_UNINSTALL_PLUGIN')` og udfør kun sletning. Lad uninstall.php være minimal og deterministisk.
## 8. Performance og options
- **Option autoload**: Store options (stor data, sjældent læst på hver request) med `update_option('key', $value, 'no')` så de ikke autoloades på hver sidevisning. Spar memory og request-tid.
- **Dyrebare operationer**: Cache resultater med transients eller object cache; undgå N+1 queries og tunge beregninger på hver request. Kør tunge ting på cron eller ved brugerhandling.
## 9. Filer og paths
- **Valider paths**: Ved fil-læsning/skrivning: sikr at resulterende path ligger under plugin-dir (fx sammenlign `realpath($path)` med `plugin_dir_path(__FILE__)`). Undgå directory traversal.
- **Skriv til filsystem**: Brug WP Filesystem API (`WP_Filesystem`, `request_filesystem_credentials`) når brugeren kan konfigurere plugin, så det virker på hostinger uden direkte filskriv.
## 10. Udvidelsesmuligheder (extensibility)
- **Hooks til tredjepart**: `do_action('my_plugin_after_save', $id, $data)` og `apply_filters('my_plugin_value', $value, $context)` på kritiske steder—så andre plugins/temaer kan udvide uden at redigere koden. Dokumenter hook-navne og parametre (fx i docblock eller README).
## 11. Capability og i18n
- **Capability**: Brug den mindst nødvendige rettighed: ikke altid `manage_options`—fx `edit_posts`, `publish_posts` eller custom capability. Tjek `current_user_can()` med det passende.
- **Oversættelser**: Load text domain på `plugins_loaded`: `load_plugin_textdomain('plugin-slug', false, dirname(plugin_basename(__FILE__)) . '/languages');`. Brug samme slug i alle `__()`, `_e()`, `esc_html__()`. Konkatenér ikke oversættelige strenge—brug placeholders: `sprintf(__('Hej %s', 'plugin-slug'), $name)`.
## 12. Kodekvalitet og regler
- **Ingen gætteri**: Opfind ikke filstier, funktionsnavne, AJAX actions, DB-tabeller, option keys. Søg eller læs eksisterende kode/docs.
- **Minimale ændringer**: Brug eksisterende funktioner og hooks; udvid kun hvad der er nødvendigt. Undgå duplikat-logik.
- **Fejlhåndtering**: I dev: log med `error_log()` eller projektets logger; undgå at eksponere stack traces til slutbruger. Returnér klare fejlbeskeder i JSON ved AJAX.
- **Debug**: Log kun bag et flag eller `WP_DEBUG`; ingen ukontrolleret debug-output i produktion.
## 13. Kort workflow ved nye features
1. Verificer i repo: find eksisterende hooks, tabeller, option keys, filer.
2. Input: `wp_unslash` + passende `sanitize_*` / `intval`.
3. Auth: nonce + capability hvor det gælder.
4. DB: kun `$wpdb->prepare()` med placeholders.
5. Output: `esc_*` eller `wp_send_json_*`; ingen rå echo af brugerdata.
6. Dokumenter: kort inline ved non-obvious beslutninger; opdater relevante docs ved schema/API-ændringer.
## 14. Succeskriterier for en ændring
- Ingen nye PHP-notices i log ved typisk brug.
- AJAX returnerer fornuftig JSON med `success: true/false` og evt. `data`/fejlbesked.
- Ingen SQL uden `prepare()` med brugerinput.
- Alle bruger-input sanitized ved modtagelse og escaped ved output.
- Nye hooks, options, cron events og handles har plugin-prefix.
- Uninstall-logik kun i uninstall.php; ingen sletning af brugerdata ved deactivation.
For udvidet reference (activation/uninstall-eksempel, multisite, upgrade path, WP Filesystem) se [reference.md](reference.md).