{"id":20876,"date":"2026-10-12T15:33:00","date_gmt":"2026-10-12T13:33:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=20876"},"modified":"2026-10-09T22:46:44","modified_gmt":"2026-10-09T20:46:44","slug":"plugin-lebenszyklus-deinstallation","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/","title":{"rendered":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation"},"content":{"rendered":"<p>Viele Plugins hinterlassen Spuren, lange nachdem sie ihre Arbeit eingestellt haben: eine Tabelle, die niemand mehr liest, Cron-Ereignisse, f\u00fcr die kein Callback mehr vorhanden ist, oder eine Capability an der Administratorrolle, die keinen Zugriff mehr regelt. WordPress kennt drei getrennte Einstiegspunkte f\u00fcr Aktivierung, Deaktivierung und Deinstallation. Dabei gibt es leicht zu \u00fcbersehende Besonderheiten: Der Aktivierungs-Hook l\u00e4uft bei Updates nicht, eine Netzwerkaktivierung auf Multisite l\u00f6st ihn genau einmal statt einmal je Website aus, und eine als Closure registrierte Deinstallationsroutine endet in einem fatalen Fehler.<\/p>\n<p>Dieser Beitrag begleitet ein kleines, aber vollst\u00e4ndiges Plugin durch alle drei Phasen. Es legt mit <code>dbDelta()<\/code> eine eigene Tabelle an, speichert eine Schemaversion und migriert bei Updates, plant ein t\u00e4gliches Cron-Ereignis, gibt Administratoren eine eigene Capability und r\u00e4umt all das \u00fcber eine <code>uninstall.php<\/code> wieder ab, die Daten nur nach ausdr\u00fccklicher Zustimmung l\u00f6scht. Jede Aussage zum Verhalten des Core ist am Handbuch, an der Funktionsreferenz und am Quelltext von WordPress 7.1.2 gepr\u00fcft, der aktuellen Version am 29.09.2026.<\/p>\n<h2>Drei Phasen, drei Einstiegspunkte<\/h2>\n<p>Das Plugin-Handbuch trennt die Phasen klar: Die Aktivierung ist der Ort f\u00fcr Rewrite-Regeln, eigene Tabellen und Standardoptionen, die Deaktivierung entfernt Vorl\u00e4ufiges wie Caches, und erst die Deinstallation darf dauerhafte Daten wie Optionen und Tabellen l\u00f6schen.<\/p>\n<p>Technisch sind <code>register_activation_hook()<\/code> und <code>register_deactivation_hook()<\/code> d\u00fcnne H\u00fcllen. Beide wenden <code>plugin_basename()<\/code> auf die \u00fcbergebene Datei an und h\u00e4ngen den Callback an eine dynamische Action, <code>activate_{$plugin}<\/code> beziehungsweise <code>deactivate_{$plugin}<\/code>. Das erste Argument muss deshalb die Hauptdatei mit dem Plugin-Header sein, \u00fcblicherweise <code>__FILE__<\/code> in genau dieser Datei. Die Deinstallation funktioniert anders: Entweder bindet WordPress eine <code>uninstall.php<\/code> aus dem Plugin-Ordner ein, oder es ruft einen mit <code>register_uninstall_hook()<\/code> hinterlegten Callback auf. Beide Varianten laufen nur beim L\u00f6schen, nicht beim Deaktivieren.<\/p>\n<table>\n<thead>\n<tr>\n<th>Phase<\/th>\n<th>Einstiegspunkt<\/th>\n<th>Beispiel-Plugin: legt an oder entfernt<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Aktivierung<\/td>\n<td><code>register_activation_hook()<\/code><\/td>\n<td>legt Tabelle, Schemaversion, Capability und Cron-Ereignis an<\/td>\n<\/tr>\n<tr>\n<td>Update<\/td>\n<td>kein Hook, Versionspr\u00fcfung auf <code>plugins_loaded<\/code><\/td>\n<td>dieselbe Einrichtung, dazu Datenmigration<\/td>\n<\/tr>\n<tr>\n<td>Deaktivierung<\/td>\n<td><code>register_deactivation_hook()<\/code><\/td>\n<td>entfernt Cron-Ereignis und Transient, beh\u00e4lt alles andere<\/td>\n<\/tr>\n<tr>\n<td>Deinstallation<\/td>\n<td><code>uninstall.php<\/code><\/td>\n<td>entfernt immer Capability, Cron und Cache; Tabelle, Optionen und Nutzer-Meta nur nach Opt-in<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<figure class=\"lw-diagram\">\n<img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/wp-plugin-lebenszyklus-de.png?v=20260930\" width=\"1120\" height=\"580\" alt=\"Lebenszyklus des Beispiel-Plugins LW Search Log: Die Aktivierung legt Tabelle, Versionsoption, Capability und t\u00e4glichen Cron an; die Deaktivierung entfernt nur Cron-Ereignis und Transient; die Deinstallation entfernt immer Capability und Cron, Tabelle und Nutzer-Meta nur nach Opt-in; Updates wiederholen die Einrichtung\"><figcaption>Beispiel-Plugin LW Search Log: Aktivierung richtet ein, Deaktivierung h\u00e4lt an, Deinstallation l\u00f6scht Daten nur nach Opt-in<\/figcaption><\/figure>\n<h2>Die Aktivierung ist kein Update-Hook<\/h2>\n<p>Seit WordPress 3.1 feuert der Aktivierungs-Hook bei Updates nicht mehr. Andrew Nacin hat die \u00c4nderung im Oktober 2010 auf Make WordPress Core begr\u00fcndet: Vorher lief der Hook bei Einzel-Updates, bei Sammel-Updates dagegen nicht, und manuelle Updates per FTP oder SVN l\u00f6sten ihn nie aus. Die Empfehlung aus demselben Beitrag gilt unver\u00e4ndert: eine Version in der Datenbank speichern, sie mit der Version im Code vergleichen und bei Abweichung die Upgrade-Routine starten.<\/p>\n<p>Der aktuelle Quelltext best\u00e4tigt das. Vor einem Update deaktiviert <code>Plugin_Upgrader::deactivate_plugin_before_upgrade()<\/code> ein aktives Plugin mit <code>deactivate_plugins( $plugin, true )<\/code>; das zweite Argument bedeutet \u201estill\u201c, es l\u00e4uft also kein Deaktivierungs-Hook. Bei Hintergrund-Updates per Cron wird das Plugin gar nicht erst deaktiviert. Danach aktiviert <code>wp-admin\/update.php<\/code> das Plugin mit <code>activate_plugin( $plugin, '', ! empty( $_GET['networkwide'] ), true )<\/code> wieder, ebenfalls still. Der Docblock von <code>activate_plugin()<\/code> h\u00e4lt fest, dass eine stille Aktivierung die Aktivierungs-Hooks \u00fcberspringt.<\/p>\n<p>Der Hook <code>upgrader_process_complete<\/code> taugt f\u00fcr Migrationen kaum. Er feuert in der Anfrage, die das Update ausgef\u00fchrt hat, und ein Hinweis in der Hook-Referenz weist darauf hin, dass in diesem Moment noch der alte Code des Plugins l\u00e4uft. Eine Migration f\u00fcr Version 2 k\u00e4me so erst ein Update sp\u00e4ter zum Zug. Die Versionspr\u00fcfung auf <code>plugins_loaded<\/code>, wie sie das Handbuch im Kapitel \u00fcber eigene Tabellen zeigt, l\u00e4uft dagegen bei jeder Anfrage mit dem neuen Code, gleich auf welchem Weg das Update kam.<\/p>\n<h2>Eigene Tabelle mit dbDelta und versioniertem Schema<\/h2>\n<p>Ob sich eine eigene Tabelle \u00fcberhaupt lohnt, behandelt <a href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/web-entwicklung\/wordpress-datenbank-leistungsgrenzen-von-wp_postmeta-und-der-aufbau-eigener\/\">der Beitrag zu den Leistungsgrenzen von wp_postmeta<\/a>; hier ist sie gesetzt. <code>dbDelta()<\/code> steckt in <code>wp-admin\/includes\/upgrade.php<\/code>, die ausdr\u00fccklich eingebunden werden muss. Die Funktion vergleicht eine <code>CREATE TABLE<\/code>-Anweisung mit der vorhandenen Tabelle und erg\u00e4nzt, was fehlt. Weil sie das SQL als Text zerlegt, ist sie beim Format pingelig. Das Handbuch verlangt:<\/p>\n<ul>\n<li>jedes Feld in einer eigenen Zeile;<\/li>\n<li>zwei Leerzeichen zwischen <code>PRIMARY KEY<\/code> und der Schl\u00fcsseldefinition;<\/li>\n<li><code>KEY<\/code> statt des Synonyms <code>INDEX<\/code>, und mindestens einen <code>KEY<\/code>;<\/li>\n<li>keine Apostrophe oder Backticks um Feldnamen;<\/li>\n<li>Feldtypen klein, SQL-Schl\u00fcsselw\u00f6rter wie <code>CREATE TABLE<\/code> gro\u00df;<\/li>\n<li>eine L\u00e4nge bei jedem Feldtyp, der eine annimmt, etwa <code>int(11)<\/code>.<\/li>\n<\/ul>\n<p>Die Funktionsreferenz erg\u00e4nzt Grenzen: <code>dbDelta()<\/code> l\u00f6scht keine Spalten, ein umbenanntes Feld landet als zus\u00e4tzliche Spalte in der Tabelle, und die zur\u00fcckgegebenen Meldungen belegen nicht, dass eine \u00c4nderung tats\u00e4chlich ausgef\u00fchrt wurde. Umbenennen, Entfernen und Umwandeln von Daten geh\u00f6ren deshalb in eigene Migrationsschritte. Den Zeichensatz liefert <code>$wpdb-&gt;get_charset_collate()<\/code>.<\/p>\n<p>Es folgt die vollst\u00e4ndige Hauptdatei des Beispiel-Plugins. Es protokolliert Suchanfragen im Frontend in einer Tabelle, l\u00f6scht einmal t\u00e4glich Zeilen, die \u00e4lter als 90 Tage sind, und zeigt unter \u201eWerkzeuge\u201c die h\u00e4ufigsten Begriffe f\u00fcr alle, die die Capability <code>lwsl_view_log<\/code> besitzen.<\/p>\n<pre><code>&lt;?php\n\/**\n * Plugin Name:       LW Search Log\n * Description:       Stores front-end search terms in a custom table and prunes old rows once a day.\n * Version:           1.1.0\n * Requires at least: 6.5\n * Requires PHP:      7.4\n * Author:            Lukas Wojcik\n * License:           GPL-2.0-or-later\n * License URI:       https:\/\/www.gnu.org\/licenses\/gpl-2.0.html\n * Text Domain:       lw-search-log\n *\n * @package LW_Search_Log\n *\/\n\ndefined( 'ABSPATH' ) || exit;\n\nconst LWSL_DB_VERSION = 2;\nconst LWSL_CRON_HOOK  = 'lwsl_prune_log';\nconst LWSL_CAP        = 'lwsl_view_log';\n\n\/**\n * Returns the log table name of the current site.\n *\n * @return string\n *\/\nfunction lwsl_table() {\n\tglobal $wpdb;\n\treturn $wpdb-&gt;prefix . 'lwsl_searches';\n}\n\n\/**\n * Creates the log table or brings it to the current structure.\n *\/\nfunction lwsl_create_table() {\n\tglobal $wpdb;\n\n\trequire_once ABSPATH . 'wp-admin\/includes\/upgrade.php';\n\n\t$table   = lwsl_table();\n\t$collate = $wpdb-&gt;get_charset_collate();\n\n\t\/\/ dbDelta() parses this text: one column per line, two spaces after\n\t\/\/ PRIMARY KEY, KEY instead of INDEX, no backticks around names.\n\t$sql = \"CREATE TABLE {$table} (\n\tid bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n\tterm varchar(190) NOT NULL DEFAULT '',\n\tresult_count int(11) unsigned NOT NULL DEFAULT 0,\n\tsearched_at datetime NOT NULL,\n\tPRIMARY KEY  (id),\n\tKEY searched_at (searched_at)\n) {$collate};\";\n\n\tdbDelta( $sql );\n}\n\n\/**\n * Data migration for schema version 2: terms are stored in lower case.\n * The new column result_count is added by dbDelta() itself.\n *\/\nfunction lwsl_migrate_to_2() {\n\tglobal $wpdb;\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t$wpdb-&gt;query( $wpdb-&gt;prepare( 'UPDATE %i SET term = LOWER(term)', lwsl_table() ) );\n}\n\n\/**\n * Brings the current site to the state this code expects.\n * Idempotent: used on activation, after updates and for new sites.\n *\/\nfunction lwsl_setup_site() {\n\t$installed = (int) get_option( 'lwsl_db_version', 0 );\n\n\tlwsl_create_table();\n\n\tif ( $installed &gt; 0 &amp;&amp; $installed &lt; 2 ) {\n\t\tlwsl_migrate_to_2();\n\t}\n\n\t$role = get_role( 'administrator' );\n\tif ( $role &amp;&amp; ! $role-&gt;has_cap( LWSL_CAP ) ) {\n\t\t$role-&gt;add_cap( LWSL_CAP );\n\t}\n\n\tif ( ! wp_next_scheduled( LWSL_CRON_HOOK ) ) {\n\t\twp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', LWSL_CRON_HOOK );\n\t}\n\n\t\/\/ Read on every request by lwsl_maybe_upgrade(), so it stays autoloaded.\n\tupdate_option( 'lwsl_db_version', LWSL_DB_VERSION, true );\n}\n\n\/**\n * Runs a callback once for every site of the network, 100 sites per query.\n *\n * @param callable $callback Function without parameters.\n *\/\nfunction lwsl_for_each_site( $callback ) {\n\t$offset = 0;\n\tdo {\n\t\t$site_ids = get_sites(\n\t\t\tarray(\n\t\t\t\t'fields' =&gt; 'ids',\n\t\t\t\t'number' =&gt; 100,\n\t\t\t\t'offset' =&gt; $offset,\n\t\t\t)\n\t\t);\n\t\tforeach ( $site_ids as $site_id ) {\n\t\t\tswitch_to_blog( $site_id );\n\t\t\tcall_user_func( $callback );\n\t\t\trestore_current_blog();\n\t\t}\n\t\t$offset += 100;\n\t} while ( 100 === count( $site_ids ) );\n}\n\n\/**\n * Activation: set up the current site, or every site on network activation.\n *\n * @param bool $network_wide True when the plugin is network-activated.\n *\/\nfunction lwsl_activate( $network_wide = false ) {\n\tif ( is_multisite() &amp;&amp; $network_wide ) {\n\t\t\/\/ On large networks the sites are set up lazily by lwsl_maybe_upgrade().\n\t\tif ( ! wp_is_large_network() ) {\n\t\t\tlwsl_for_each_site( 'lwsl_setup_site' );\n\t\t}\n\t\treturn;\n\t}\n\tlwsl_setup_site();\n}\nregister_activation_hook( __FILE__, 'lwsl_activate' );\n\n\/**\n * Update path: activation hooks do not run on updates, so the stored\n * schema version is compared with the code on every request.\n *\/\nfunction lwsl_maybe_upgrade() {\n\tif ( (int) get_option( 'lwsl_db_version', 0 ) &lt; LWSL_DB_VERSION ) {\n\t\tlwsl_setup_site();\n\t}\n}\nadd_action( 'plugins_loaded', 'lwsl_maybe_upgrade' );\n\n\/**\n * Stops scheduled work and drops caches on the current site. Keeps all data.\n *\/\nfunction lwsl_teardown_site() {\n\twp_clear_scheduled_hook( LWSL_CRON_HOOK );\n\tdelete_transient( 'lwsl_top_terms' );\n}\n\n\/**\n * Deactivation: current site, or every site on network deactivation.\n *\n * @param bool $network_deactivating True when the plugin is network-deactivated.\n *\/\nfunction lwsl_deactivate( $network_deactivating = false ) {\n\tif ( is_multisite() &amp;&amp; $network_deactivating ) {\n\t\tlwsl_for_each_site( 'lwsl_teardown_site' );\n\t\treturn;\n\t}\n\tlwsl_teardown_site();\n}\nregister_deactivation_hook( __FILE__, 'lwsl_deactivate' );\n\n\/**\n * Cron callback: deletes log rows older than 90 days.\n *\/\nfunction lwsl_prune_log() {\n\tglobal $wpdb;\n\t$cutoff = gmdate( 'Y-m-d H:i:s', time() - 90 * DAY_IN_SECONDS );\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t$wpdb-&gt;query( $wpdb-&gt;prepare( 'DELETE FROM %i WHERE searched_at &lt; %s', lwsl_table(), $cutoff ) );\n\tdelete_transient( 'lwsl_top_terms' );\n}\nadd_action( LWSL_CRON_HOOK, 'lwsl_prune_log' );\n\n\/**\n * Logs the search term of the main front-end query.\n *\/\nfunction lwsl_log_search() {\n\tglobal $wpdb, $wp_query;\n\n\tif ( ! is_search() || is_paged() ) {\n\t\treturn;\n\t}\n\n\t$term = mb_substr( sanitize_text_field( get_search_query( false ) ), 0, 190 );\n\tif ( '' === $term ) {\n\t\treturn;\n\t}\n\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t$wpdb-&gt;query(\n\t\t$wpdb-&gt;prepare(\n\t\t\t'INSERT INTO %i (term, result_count, searched_at) VALUES (LOWER(%s), %d, %s)',\n\t\t\tlwsl_table(),\n\t\t\t$term,\n\t\t\t(int) $wp_query-&gt;found_posts,\n\t\t\tcurrent_time( 'mysql', true )\n\t\t)\n\t);\n}\nadd_action( 'wp', 'lwsl_log_search' );\n\n\/**\n * Adds the log page under Tools, visible with the plugin capability only.\n *\/\nfunction lwsl_admin_menu() {\n\tadd_management_page(\n\t\t__( 'Search log', 'lw-search-log' ),\n\t\t__( 'Search log', 'lw-search-log' ),\n\t\tLWSL_CAP,\n\t\t'lwsl-search-log',\n\t\t'lwsl_render_page'\n\t);\n}\nadd_action( 'admin_menu', 'lwsl_admin_menu' );\n\n\/**\n * Renders the top terms (cached in a transient) and the searches since the\n * current user's last visit (stored in user meta).\n *\/\nfunction lwsl_render_page() {\n\tglobal $wpdb;\n\n\t$user_id   = get_current_user_id();\n\t$last_seen = (int) get_user_meta( $user_id, 'lwsl_last_viewed', true );\n\tupdate_user_meta( $user_id, 'lwsl_last_viewed', time() );\n\n\t$top = get_transient( 'lwsl_top_terms' );\n\tif ( false === $top ) {\n\t\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- cached in a transient.\n\t\t$top = $wpdb-&gt;get_results(\n\t\t\t$wpdb-&gt;prepare( 'SELECT term, COUNT(*) AS hits FROM %i GROUP BY term ORDER BY hits DESC LIMIT 20', lwsl_table() )\n\t\t);\n\t\tset_transient( 'lwsl_top_terms', $top, HOUR_IN_SECONDS );\n\t}\n\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t$new = (int) $wpdb-&gt;get_var(\n\t\t$wpdb-&gt;prepare( 'SELECT COUNT(*) FROM %i WHERE searched_at &gt; %s', lwsl_table(), gmdate( 'Y-m-d H:i:s', $last_seen ) )\n\t);\n\n\techo '&lt;div class=\"wrap\"&gt;&lt;h1&gt;' . esc_html__( 'Search log', 'lw-search-log' ) . '&lt;\/h1&gt;&lt;p&gt;';\n\techo esc_html(\n\t\tsprintf(\n\t\t\t\/* translators: %d: number of searches since the last visit *\/\n\t\t\t__( 'New searches since the last visit: %d', 'lw-search-log' ),\n\t\t\t$new\n\t\t)\n\t);\n\techo '&lt;\/p&gt;&lt;ol&gt;';\n\tforeach ( $top as $row ) {\n\t\tprintf( '&lt;li&gt;%s (%d)&lt;\/li&gt;', esc_html( $row-&gt;term ), (int) $row-&gt;hits );\n\t}\n\techo '&lt;\/ol&gt;&lt;\/div&gt;';\n}\n\n\/**\n * Multisite: drop the log table together with a deleted site.\n *\n * @param string[] $tables  Tables WordPress is about to drop.\n * @param int      $site_id ID of the deleted site.\n * @return string[]\n *\/\nfunction lwsl_drop_site_table( $tables, $site_id ) {\n\tglobal $wpdb;\n\t$tables[] = $wpdb-&gt;get_blog_prefix( $site_id ) . 'lwsl_searches';\n\treturn $tables;\n}\nadd_filter( 'wpmu_drop_tables', 'lwsl_drop_site_table', 10, 2 );<\/code><\/pre>\n<p>Kern des Plugins ist <code>lwsl_setup_site()<\/code>. Die Funktion ist idempotent: <code>dbDelta()<\/code> \u00e4ndert nichts, wenn die Tabelle schon passt, die Capability wird nur geschrieben, wenn sie fehlt, und das Cron-Ereignis wird nur geplant, wenn <code>wp_next_scheduled()<\/code> keines findet. Damit l\u00e4sst sie sich gefahrlos aus dem Aktivierungs-Hook, aus der Versionspr\u00fcfung und f\u00fcr noch nicht eingerichtete Websites im Netzwerk aufrufen. Schemaversion 2 zeigt beide Arten von Migration. Die neue Spalte <code>result_count<\/code> erg\u00e4nzt <code>dbDelta()<\/code> von selbst, das Umwandeln vorhandener Begriffe in Kleinbuchstaben ist dagegen eine Datenmigration mit eigenem Schritt, der nur bei gespeicherter Version 1 l\u00e4uft.<\/p>\n<p>Zwei Details gehen leicht unter. Die Option <code>lwsl_db_version<\/code> wird bei jeder Anfrage gelesen und deshalb mit Autoload gespeichert; sonst kostet jeder Seitenaufruf eine zus\u00e4tzliche Abfrage. Bei jedem Aufruf \u00fcbergibt <code>add_cap()<\/code> die gesamte Rollenoption an <code>update_option()<\/code>. Diese Funktion schreibt nur dann in die Datenbank, wenn die Capability neu ist. Das Beispiel vergibt sie trotzdem nur bei der Einrichtung und nicht auf <code>init<\/code>. Tabellennamen werden mit dem Platzhalter <code>%i<\/code> \u00fcbergeben, den <code>$wpdb-&gt;prepare()<\/code> seit WordPress 6.2 f\u00fcr Bezeichner unterst\u00fctzt.<\/p>\n<h2>Deaktivierung: anhalten, nicht l\u00f6schen<\/h2>\n<p>Eine Deaktivierung ist oft nur vor\u00fcbergehend, etwa bei der Fehlersuche. Das Beispiel entfernt deshalb nur, was ohne Plugin weiterlaufen oder bei der Reaktivierung veraltet sein w\u00fcrde: das Cron-Ereignis und die zwischengespeicherte Rangliste im Transient. Tabelle, Schemaversion, Capability und Nutzer-Meta bleiben erhalten.<\/p>\n<p>F\u00fcr Cron gibt es zwei Funktionen. <code>wp_clear_scheduled_hook()<\/code> entfernt alle Ereignisse eines Hooks mit den angegebenen Argumenten, und die Referenz betont, dass diese exakt \u00fcbereinstimmen m\u00fcssen. Plant ein Plugin denselben Hook mit unterschiedlichen Argumenten, hilft nur <code>wp_unschedule_hook()<\/code>: Die Funktion gibt es seit 4.9, sie entfernt alle Ereignisse eines Hooks unabh\u00e4ngig von den Argumenten, und ihr Docblock nennt das Aufr\u00e4umen der Cron-Warteschlange beim Deaktivieren ausdr\u00fccklich als Einsatzzweck. Das Beispiel plant ohne Argumente; die Deinstallationsroutine greift trotzdem sicherheitshalber zur zweiten Funktion.<\/p>\n<p>Der Deaktivierungs-Callback erh\u00e4lt einen booleschen Wert, der auf Multisite bei einer Netzwerkdeaktivierung wahr ist. <code>lwsl_deactivate()<\/code> r\u00e4umt in diesem Fall alle Websites auf. Die Capability bleibt beim Deaktivieren bewusst stehen: Ohne Plugin pr\u00fcft sie kein Code, und Entfernen plus erneutes Hinzuf\u00fcgen hie\u00dfe zwei unn\u00f6tige Schreibvorg\u00e4nge in die Rollenoption je Website.<\/p>\n<h2>Deinstallation: uninstall.php, register_uninstall_hook und das Opt-in<\/h2>\n<p>Beim L\u00f6schen ruft <code>delete_plugins()<\/code> f\u00fcr jedes Plugin <code>uninstall_plugin()<\/code> auf, bevor die Dateien verschwinden. Der Quelltext kl\u00e4rt auch den Vorrang zwischen beiden Varianten: Existiert eine <code>uninstall.php<\/code>, entfernt WordPress einen eventuell registrierten Callback aus der Option <code>uninstall_plugins<\/code>, definiert die Konstante <code>WP_UNINSTALL_PLUGIN<\/code> mit dem Plugin-Basename und bindet nur diese Datei ein. Die Hauptdatei des Plugins wird dabei nicht geladen. Deshalb schreibt die Datei weiter unten jeden Namen aus, statt Konstanten wie <code>LWSL_CAP<\/code> zu verwenden. Erst wenn keine <code>uninstall.php<\/code> existiert, bindet WordPress die Hauptdatei ein und f\u00fchrt den gespeicherten Callback auf <code>uninstall_{$file}<\/code> aus; dann l\u00e4uft auch jede Codezeile auf Dateiebene mit.<\/p>\n<p>Die Pr\u00fcfung auf <code>WP_UNINSTALL_PLUGIN<\/code> am Anfang der Datei sch\u00fctzt vor direktem Aufruf, wie es das Handbuch verlangt. F\u00fcr <code>register_uninstall_hook()<\/code> gelten strengere Regeln als f\u00fcr die beiden anderen Registrierungsfunktionen, weil der Callback in einer Option landet. Eine Closure l\u00e4sst sich nicht serialisieren und endet in einem fatalen Fehler; eine Objektmethode wird mit einem <code>_doing_it_wrong()<\/code>-Hinweis abgelehnt. Erlaubt sind nur Funktionsnamen und statische Methoden. Jeder Aufruf liest die Option und schreibt sie, sobald der Eintrag fehlt oder abweicht; die Registrierung geh\u00f6rt deshalb in die Aktivierung und nicht in jede Anfrage.<\/p>\n<pre><code>\/\/ Counter-example: what register_uninstall_hook() accepts and what it rejects.\n\n\/\/ Accepted: a function name or a static method. The callback is stored\n\/\/ in the option uninstall_plugins, so register it once, on activation.\nregister_uninstall_hook( __FILE__, 'lwsl_uninstall' );\nregister_uninstall_hook( __FILE__, array( 'LWSL_Plugin', 'uninstall' ) );\n\n\/\/ Rejected: a closure cannot be serialized into that option (fatal error),\n\/\/ an object method triggers _doing_it_wrong() and is not registered.\nregister_uninstall_hook( __FILE__, function () {} );\nregister_uninstall_hook( __FILE__, array( new LWSL_Plugin(), 'uninstall' ) );<\/code><\/pre>\n<p>Was bedeutet \u201eDaten beim Deinstallieren l\u00f6schen\u201c als Opt-in? Wer ein Plugin l\u00f6scht, verabschiedet sich nicht zwingend von dessen Daten; auch eine Neuinstallation, die besch\u00e4digte Dateien ausschlie\u00dfen soll, beginnt mit dem L\u00f6schen. Das Beispiel teilt das Aufr\u00e4umen deshalb in zwei Gruppen. Technische Reste gehen immer: Cron-Ereignisse, Transients und die Capability sind ohne Plugin nutzlos und entstehen bei der Aktivierung ohnehin neu. Inhalte und Konfiguration gehen nur nach ausdr\u00fccklichem Ja: die Protokolltabelle, die Schemaversion, die Nutzer-Meta und der Schalter selbst. Ohne Opt-in findet eine Neuinstallation das alte Protokoll samt Schemaversion vor.<\/p>\n<pre><code>&lt;?php\n\/**\n * Uninstall routine of LW Search Log.\n *\n * WordPress includes this file when the plugin is deleted. The main plugin\n * file is not loaded, so every name is spelled out here.\n *\n * @package LW_Search_Log\n *\/\n\n\/\/ Only run when WordPress itself starts the uninstall.\nif ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {\n\texit;\n}\n\n\/**\n * Cleans one site: technical leftovers always, data only after opt-in.\n *\n * @param bool $delete_data Whether the site owner opted in to data removal.\n *\/\nfunction lwsl_uninstall_site( $delete_data ) {\n\tglobal $wpdb;\n\n\t\/\/ Always: scheduled events, caches and the capability.\n\twp_unschedule_hook( 'lwsl_prune_log' );\n\tdelete_transient( 'lwsl_top_terms' );\n\n\tforeach ( wp_roles()-&gt;role_objects as $role ) {\n\t\tif ( $role-&gt;has_cap( 'lwsl_view_log' ) ) {\n\t\t\t$role-&gt;remove_cap( 'lwsl_view_log' );\n\t\t}\n\t}\n\n\tif ( ! $delete_data ) {\n\t\treturn;\n\t}\n\n\t\/\/ Opt-in only: the log table and the schema version.\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange\n\t$wpdb-&gt;query( $wpdb-&gt;prepare( 'DROP TABLE IF EXISTS %i', $wpdb-&gt;prefix . 'lwsl_searches' ) );\n\tdelete_option( 'lwsl_db_version' );\n}\n\n\/\/ The opt-in switch: a network option on multisite, a normal option otherwise.\n$lwsl_delete_data = (bool) get_site_option( 'lwsl_delete_data_on_uninstall', false );\n\nif ( is_multisite() ) {\n\t$lwsl_offset = 0;\n\tdo {\n\t\t$lwsl_site_ids = get_sites(\n\t\t\tarray(\n\t\t\t\t'fields' =&gt; 'ids',\n\t\t\t\t'number' =&gt; 100,\n\t\t\t\t'offset' =&gt; $lwsl_offset,\n\t\t\t)\n\t\t);\n\t\tforeach ( $lwsl_site_ids as $lwsl_site_id ) {\n\t\t\tswitch_to_blog( $lwsl_site_id );\n\t\t\tlwsl_uninstall_site( $lwsl_delete_data );\n\t\t\trestore_current_blog();\n\t\t}\n\t\t$lwsl_offset += 100;\n\t} while ( 100 === count( $lwsl_site_ids ) );\n} else {\n\tlwsl_uninstall_site( $lwsl_delete_data );\n}\n\nif ( $lwsl_delete_data ) {\n\t\/\/ wp_usermeta is shared by all sites: one call removes the key for every user.\n\tdelete_metadata( 'user', 0, 'lwsl_last_viewed', '', true );\n\tdelete_site_option( 'lwsl_delete_data_on_uninstall' );\n}<\/code><\/pre>\n<p>Die Nutzer-Meta entfernt <code>delete_metadata()<\/code> mit dem f\u00fcnften Argument <code>$delete_all<\/code> auf true, das den Schl\u00fcssel f\u00fcr alle Nutzer auf einmal l\u00f6scht. <code>delete_user_meta()<\/code> arbeitet dagegen immer nur mit einer Nutzer-ID. <code>delete_transient()<\/code> ber\u00fccksichtigt zudem einen externen Objekt-Cache und l\u00f6scht den Eintrag in diesem Fall dort statt in der Optionstabelle. Eine <code>LIKE<\/code>-Abfrage auf <code>_transient_%<\/code> in <code>wp_options<\/code> erreicht einen solchen Cache nicht.<\/p>\n<p>Wie der Schalter gesetzt wird, ist Sache der Einstellungsseite; die Settings API behandelt <span class=\"lw-artikel-geplant\">ein eigener Beitrag dieser Serie<\/span>. F\u00fcr einen Test gen\u00fcgt WP-CLI. <code>wp plugin uninstall<\/code> mit <code>--skip-delete<\/code> f\u00fchrt nur die Deinstallationsroutine aus und l\u00e4sst die Dateien liegen, <code>wp plugin delete<\/code> dagegen entfernt die Dateien, ohne zu deaktivieren oder zu deinstallieren. Diesen Unterschied vertieft <a href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/wp-cli-loschbefehle-welche-tabellen-jeder-befehl-anfasst-und-was-er\/\">der Beitrag zu den WP-CLI-L\u00f6schbefehlen und den Tabellen, die sie anfassen<\/a>.<\/p>\n<pre><code># Single site: opt in, then run only the uninstall routine (files stay).\nwp option update lwsl_delete_data_on_uninstall 1\nwp plugin deactivate lw-search-log\nwp plugin uninstall lw-search-log --skip-delete\n\n# Multisite: the switch is a network option.\nwp site option update lwsl_delete_data_on_uninstall 1\nwp plugin deactivate lw-search-log --network\nwp plugin uninstall lw-search-log --skip-delete\n\n# Deletes the files only: neither deactivation nor uninstall code runs.\nwp plugin delete lw-search-log<\/code><\/pre>\n<h2>Multisite: einmal je Netzwerk, Daten je Website<\/h2>\n<p>Auf Multisite existieren die meisten Daten des Beispiels je Website: Die Tabelle tr\u00e4gt das Pr\u00e4fix der Website, Optionen und Transients liegen je Website, Cron-Ereignisse in der Option <code>cron<\/code> jeder Website und Rollen in einer Option aus Website-Pr\u00e4fix plus <code>user_roles<\/code>. Nutzer-Meta dagegen liegt in einer einzigen Tabelle f\u00fcr das ganze Netzwerk, und der Opt-in-Schalter wird \u00fcber <code>get_site_option()<\/code> als Netzwerkoption gelesen, die auf Einzelinstallationen auf eine normale Option zur\u00fcckf\u00e4llt.<\/p>\n<p>Eine Netzwerkaktivierung l\u00f6st den Aktivierungs-Hook genau einmal aus, im Kontext der aktuellen Website und mit <code>$network_wide<\/code> auf true. <code>activate_plugin()<\/code> setzt diesen Wert auch dann, wenn der Plugin-Header <code>Network: true<\/code> enth\u00e4lt. Um alle anderen Websites muss sich das Plugin selbst k\u00fcmmern. <code>lwsl_for_each_site()<\/code> erledigt das mit <code>get_sites()<\/code> und <code>switch_to_blog()<\/code>. Dabei lauern zwei Fallen: <code>get_sites()<\/code> liefert ohne weitere Angabe h\u00f6chstens 100 Websites, weshalb die Schleife ausdr\u00fccklich bl\u00e4ttert; und <code>switch_to_blog()<\/code> wechselt nur den Datenbankkontext, nicht den geladenen Code. Hier spielt das keine Rolle, weil die Schleife nur Funktionen dieses Plugins aufruft.<\/p>\n<p>In gro\u00dfen Netzwerken kann eine Schleife \u00fcber alle Websites w\u00e4hrend der Aktivierung an Zeitlimits sto\u00dfen. WordPress stuft ein Netzwerk ab mehr als 10.000 Websites oder Nutzern als gro\u00df ein, und <code>wp_is_large_network()<\/code> l\u00e4sst sich filtern. In diesem Fall \u00fcberspringt das Beispiel die Schleife und verl\u00e4sst sich auf die Versionspr\u00fcfung: <code>lwsl_maybe_upgrade()<\/code> l\u00e4uft bei jeder Anfrage jeder Website, auf der das Plugin aktiv ist, und richtet eine Website bei ihrer ersten Anfrage ein. Das deckt auch Websites ab, die nach der Aktivierung entstehen. Wer neue Websites sofort einrichten will, h\u00e4ngt sich an <code>wp_initialize_site<\/code>; der Core f\u00fchrt dort mit Priorit\u00e4t 10 seine eigene Initialisierung aus, ein Plugin-Callback geh\u00f6rt also auf eine sp\u00e4tere Priorit\u00e4t.<\/p>\n<p>Das L\u00f6schen einer einzelnen Website regelt der Filter <code>wpmu_drop_tables<\/code>, der die Tabellen auflistet, die WordPress mit der Website entfernt. Das Beispiel tr\u00e4gt seine Tabelle dort unabh\u00e4ngig vom Opt-in ein, denn die ganze Website verschwindet ohnehin. Das L\u00f6schen des Plugins selbst ist auf Multisite Super-Admins vorbehalten, wie die Capability-Zuordnung von <code>delete_plugins<\/code> zeigt, und <code>uninstall.php<\/code> l\u00e4uft dann einmal im Netzwerk-Admin. Die Schleife \u00fcber alle Websites in der Deinstallationsroutine ist deshalb Pflicht; das Handbuch warnt ausdr\u00fccklich, dass sie in gro\u00dfen Netzwerken viele Ressourcen kosten kann.<\/p>\n<h2>Grenzen und offene Punkte<\/h2>\n<p>Der Beispielcode lief unter WordPress 7.1.2 (PHP 8.4, MariaDB 11.8) auf einer Einzelinstallation und in einem Netzwerk mit Unterverzeichnissen: Aktivierung, Netzwerkaktivierung, nachtr\u00e4glich angelegte Site, L\u00f6schen einer Site, Update von Schemaversion 1 auf 2, Deaktivierung sowie Deinstallation mit und ohne Opt-in. Plugin Check 2.1.0 meldet f\u00fcr die beiden PHP-Dateien keinen Befund.<\/p>\n<ul>\n<li>Parallele Anfragen direkt nach einem Update k\u00f6nnen <code>lwsl_setup_site()<\/code> doppelt ausf\u00fchren. Hier ist das unsch\u00e4dlich, weil jeder Schritt idempotent ist; andere Migrationen brauchen eine Sperre.<\/li>\n<li>Das Handbuch verlangt L\u00e4ngenangaben auch bei Ganzzahltypen wie <code>int(11)<\/code>. Unter MariaDB 11.8 meldete <code>dbDelta()<\/code> mit <code>int(11)<\/code> bei wiederholten Aufrufen keine \u00c4nderung; ohne L\u00e4ngenangabe meldete es bei jedem Aufruf \u201eChanged type\u201c, weil MariaDB <code>int(10)<\/code> ausweist. Unter MySQL ab 8.0.17 \u00fcbergeht der Core reine Breitenunterschiede seit WordPress 6.1 (laut Quelltext, nicht gemessen).<\/li>\n<li>Capabilities, die einzelnen Nutzern direkt zugewiesen wurden, liegen in der Nutzer-Meta und bleiben von der Schleife \u00fcber die Rollen unber\u00fchrt.<\/li>\n<li>In Installationen mit mehreren Netzwerken liefert <code>get_sites()<\/code> ohne <code>network_id<\/code> die Websites aller Netzwerke, w\u00e4hrend der Opt-in-Schalter je Netzwerk gespeichert ist. Das richtige Verhalten f\u00fcr diesen Fall bleibt offen.<\/li>\n<li>Wer Plugins per FTP, Git oder Deployment-Skript entfernt, umgeht <code>uninstall.php<\/code> vollst\u00e4ndig. Aufr\u00e4umen kann dann kein Plugin.<\/li>\n<\/ul>\n<div class=\"lw-faq\">\n<h2>Fragen und Antworten<\/h2>\n<h3>L\u00e4uft uninstall.php auch, wenn ein Plugin per FTP oder mit wp plugin delete entfernt wird?<\/h3>\n<p>Nein. WordPress ruft die Deinstallationsroutine nur aus <code>delete_plugins()<\/code> heraus auf, also beim L\u00f6schen im Backend. WP-CLI bietet daf\u00fcr <code>wp plugin uninstall<\/code>; <code>wp plugin delete<\/code> entfernt laut Dokumentation nur die Dateien, ohne zu deaktivieren oder die Deinstallationsroutine auszuf\u00fchren. Bei FTP, Git oder Deployment-Skripten bleibt alles in der Datenbank liegen.<\/p>\n<h3>Was passiert, wenn ein Plugin sowohl uninstall.php als auch register_uninstall_hook() verwendet?<\/h3>\n<p>Die Datei gewinnt. <code>uninstall_plugin()<\/code> pr\u00fcft zuerst, ob <code>uninstall.php<\/code> existiert, entfernt dann den registrierten Callback aus der Option <code>uninstall_plugins<\/code> und bindet nur die Datei ein. Der Callback wird in diesem Fall nie aufgerufen.<\/p>\n<h3>Warum steht die Versionspr\u00fcfung auf plugins_loaded und nicht auf admin_init?<\/h3>\n<p>Updates kommen nicht nur \u00fcber das Backend, sondern auch \u00fcber WP-CLI, automatische Hintergrund-Updates oder das Hochladen neuer Dateien. Auf <code>admin_init<\/code> liefe die Migration erst, wenn jemand das Backend \u00f6ffnet; bis dahin arbeitet der neue Code im Frontend mit dem alten Schema. <code>plugins_loaded<\/code> l\u00e4uft bei jeder Anfrage, das Handbuch zeigt die Pr\u00fcfung genau dort. Die Option mit der Schemaversion sollte deshalb mit Autoload gespeichert sein.<\/p>\n<\/div>\n<div class=\"lw-quellen\">\n<h2>Quellen<\/h2>\n<p>Alle Quellen abgerufen am 29.09.2026.<\/p>\n<ul>\n<li><a href=\"https:\/\/developer.wordpress.org\/plugins\/plugin-basics\/activation-deactivation-hooks\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Handbook: Activation \/ Deactivation Hooks<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/plugins\/plugin-basics\/uninstall-methods\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Handbook: Uninstall Methods<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/plugins\/creating-tables-with-plugins\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Handbook: Creating Tables with Plugins<\/a><\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2010\/10\/27\/plugin-activation-hooks-no-longer-fire-for-updates\/\" target=\"_blank\" rel=\"noopener noreferrer\">Make WordPress Core: Plugin activation hooks no longer fire for updates (27.10.2010)<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/register_deactivation_hook\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: register_deactivation_hook()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/register_uninstall_hook\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: register_uninstall_hook()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/dbdelta\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: dbDelta()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/hooks\/upgrader_process_complete\/\" target=\"_blank\" rel=\"noopener noreferrer\">Hook-Referenz: upgrader_process_complete<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_clear_scheduled_hook\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: wp_clear_scheduled_hook()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_unschedule_hook\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: wp_unschedule_hook()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/get_sites\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: get_sites()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/switch_to_blog\/\" target=\"_blank\" rel=\"noopener noreferrer\">Funktionsreferenz: switch_to_blog()<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-admin\/includes\/plugin.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-admin\/includes\/plugin.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-admin\/update.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-admin\/update.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-admin\/includes\/class-plugin-upgrader.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: class-plugin-upgrader.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/plugin.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-includes\/plugin.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/option.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-includes\/option.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/meta.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-includes\/meta.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/class-wp-roles.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: class-wp-roles.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/cron.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-includes\/cron.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/ms-site.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: wp-includes\/ms-site.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/ms-default-filters.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: ms-default-filters.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/ms-functions.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: ms-functions.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/capabilities.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: capabilities.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1.2\/src\/wp-includes\/class-wpdb.php\" target=\"_blank\" rel=\"noopener noreferrer\">Quelltext WordPress 7.1.2: class-wpdb.php<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/cli\/commands\/plugin\/uninstall\/\" target=\"_blank\" rel=\"noopener noreferrer\">WP-CLI: wp plugin uninstall<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/cli\/commands\/plugin\/delete\/\" target=\"_blank\" rel=\"noopener noreferrer\">WP-CLI: wp plugin delete<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/cli\/commands\/site\/option\/update\/\" target=\"_blank\" rel=\"noopener noreferrer\">WP-CLI: wp site option update<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Aktivierungs-Hooks \u00fcberspringen Updates, die Netzwerkaktivierung feuert einmal, und uninstall.php l\u00e4uft ohne die Hauptdatei. Ein vollst\u00e4ndiges Beispiel-Plugin zeigt alle drei Phasen.<\/p>\n","protected":false},"author":1,"featured_media":20931,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24],"tags":[92256,92274,92277],"class_list":["post-20876","post","type-post","status-publish","format-standard","hentry","category-wordpress-plugins-tricks-de","tag-mysql-de","tag-php-de","tag-wordpress-de"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-12T13:33:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"21 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation\",\"datePublished\":\"2026-10-12T13:33:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/\"},\"wordCount\":2386,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20876-plugin-lebenszyklus-deinstallation.png\",\"keywords\":[\"MySQL\",\"PHP\",\"WordPress\"],\"articleSection\":[\"WordPress-Plugins &amp; Tricks\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/\",\"name\":\"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20876-plugin-lebenszyklus-deinstallation.png\",\"datePublished\":\"2026-10-12T13:33:00+00:00\",\"description\":\"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20876-plugin-lebenszyklus-deinstallation.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20876-plugin-lebenszyklus-deinstallation.png\",\"width\":1200,\"height\":630,\"caption\":\"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/plugin-lebenszyklus-deinstallation\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik","description":"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/","og_locale":"en_US","og_type":"article","og_title":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik","og_description":"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-10-12T13:33:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"21 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation","datePublished":"2026-10-12T13:33:00+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/"},"wordCount":2386,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png","keywords":["MySQL","PHP","WordPress"],"articleSection":["WordPress-Plugins &amp; Tricks"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/","url":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/","name":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png","datePublished":"2026-10-12T13:33:00+00:00","description":"WordPress-Plugin-Lebenszyklus: Tabelle mit dbDelta und versionierter Migration, Cron, Capabilities und eine multisite-f\u00e4hige uninstall.php mit Opt-in.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20876-plugin-lebenszyklus-deinstallation.png","width":1200,"height":630,"caption":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/plugin-lebenszyklus-deinstallation\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Plugin-Lebenszyklus: Aktivierung, Deaktivierung, Deinstallation"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20876","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=20876"}],"version-history":[{"count":2,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20876\/revisions"}],"predecessor-version":[{"id":21842,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20876\/revisions\/21842"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/20931"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=20876"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=20876"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=20876"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}