Cykl życia wtyczki: aktywacja, dezaktywacja, odinstalowanie

Management Summary
Wtyczka WordPressa potrzebuje jasnych zasad uruchamiania, czasowego wyłączania i usuwania. Te etapy określają, które dane pozostają oraz kiedy zatrzymują się zadania wykonywane w tle. Rozdzielenie etapów podczas szukania błędów lub zmiany oprogramowania ogranicza ryzyko przypadkowej utraty historii i pozostawienia zbędnych zadań. Przykład zachowuje trwałe dane przy wyłączeniu, a usunięcie danych wymaga osobnej decyzji. Aktualizacje potrzebują własnego przygotowania.
Spis treści
- Trzy fazy, trzy punkty wejścia
- Aktywacja to nie hook aktualizacji
- Własna tabela z dbDelta i wersjonowanym schematem
- Dezaktywacja: zatrzymać, nie kasować
- Odinstalowanie: uninstall.php, register_uninstall_hook i zgoda
- Multisite: raz na sieć, dane na każdej witrynie
- Ograniczenia i otwarte kwestie
- Pytania i odpowiedzi
- Źródła
Wyłączona wtyczka może pozostawić tabele, zaplanowane zadania i uprawnienia dostępu. WordPress rozdziela trzy kroki: aktywacja przygotowuje wtyczkę, dezaktywacja zatrzymuje działanie, a odinstalowanie usuwa pozostałości. Hook to określony moment, w którym WordPress wywołuje zarejestrowaną funkcję. Aktualizacja wymaga osobnej obsługi: hook aktywacji nie uruchamia się wtedy ponownie.
LW Search Log zapisuje wyszukiwane frazy we własnej tabeli bazy danych. Numer wersji opisuje strukturę tabeli. Migracja dostosowuje zapisane dane do nowszego kodu. Zaplanowane zadanie usuwa stare wpisy, a osobne uprawnienie pozwala otworzyć raport. Plik uninstall.php usuwa dane dziennika tylko po wyraźnej zgodzie. Objaśnienia odnoszą się do WordPressa 7.1.2; ograniczenia poprawionego przykładu opisano na końcu.
Trzy fazy, trzy punkty wejścia
Aktywacja przygotowuje między innymi tabele i domyślne opcje. Dezaktywacja usuwa dane tymczasowe, w tym pamięć podręczną, i zatrzymuje zaplanowane zadania. Przykład zachowuje trwałą zawartość. Dopiero odinstalowanie umożliwia końcowe porządki. To zalecany podział zadań wtyczki, a nie mechanizm WordPressa blokujący wcześniejsze usuwanie.
register_activation_hook() i register_deactivation_hook() rejestrują funkcje dla tych dwóch kroków. Pierwszy argument wskazuje główny plik z nagłówkiem wtyczki; w tym pliku pasuje __FILE__. Przy odinstalowaniu WordPress ładuje uninstall.php albo wywołuje funkcję zarejestrowaną przez register_uninstall_hook(). Proces rozpoczyna zwykłe usunięcie wtyczki w panelu. WP-CLI pozwala też odinstalować wtyczkę z zachowaniem plików. Sama dezaktywacja nie uruchamia odinstalowania.
| Faza | Punkt wejścia | Przykładowa wtyczka: zakłada lub usuwa |
|---|---|---|
| Aktywacja | register_activation_hook() | zakłada tabelę, wersję schematu, capability i zdarzenie cron |
| Aktualizacja | bez hooka aktywacji; kontrola wersji na plugins_loaded | ta sama procedura konfiguracji plus migracja danych |
| Dezaktywacja | register_deactivation_hook() | usuwa zdarzenie cron i transient, resztę zostawia |
| Odinstalowanie | uninstall.php | zawsze usuwa capability, cron i cache; tabelę, opcje i meta użytkowników tylko po zgodzie |
Aktywacja to nie hook aktualizacji
Od WordPressa 3.1 hook aktywacji nie uruchamia się przy aktualizacjach. Andrew Nacin uzasadnił tę zmianę w październiku 2010 roku na blogu Make WordPress Core: wcześniej hook działał przy pojedynczych aktualizacjach, przy zbiorczych już nie, a ręczne aktualizacje przez FTP lub SVN nigdy go nie wywoływały. Zalecenie z tego samego wpisu pozostaje aktualne: wersję należy zapisać w bazie danych, porównać z wersją w kodzie i przy rozbieżności uruchomić procedurę aktualizacji.
Obecny kod źródłowy to potwierdza. Przed aktualizacją Plugin_Upgrader::deactivate_plugin_before_upgrade() dezaktywuje aktywną wtyczkę wywołaniem deactivate_plugins( $plugin, true ); drugi argument oznacza tryb cichy, więc żaden hook dezaktywacji się nie uruchamia. Przy aktualizacjach w tle przez cron wtyczka w ogóle nie jest dezaktywowana. Następnie wp-admin/update.php ponownie aktywuje wtyczkę wywołaniem activate_plugin( $plugin, '', ! empty( $_GET['networkwide'] ), true ), również po cichu. Docblock funkcji activate_plugin() stwierdza, że cicha aktywacja pomija hooki aktywacji.
upgrader_process_complete działa jeszcze w żądaniu wykonującym aktualizację. Jeżeli poprzednia wersja wtyczki została już załadowana, nadal może działać stary kod. Sam ten hook nie zapewnia więc uruchomienia nowej migracji. Kontrola na plugins_loaded porównuje zapisaną wersję z załadowanym kodem. Starsza wersja bazy uruchamia ponownie konfigurację.
Własna tabela z dbDelta i wersjonowanym schematem
To, czy własna tabela w ogóle się opłaca, omawia artykuł o granicach wydajności wp_postmeta; tutaj tabela jest założeniem. dbDelta() znajduje się w pliku wp-admin/includes/upgrade.php, który trzeba dołączyć jawnie. Funkcja porównuje instrukcję CREATE TABLE z istniejącą tabelą i uzupełnia braki. Ponieważ analizuje SQL jako tekst, jest bardzo wymagająca co do formatu. Podręcznik wymaga:
- każdego pola w osobnym wierszu;
- dwóch spacji między
PRIMARY KEYa definicją klucza; - słowa
KEYzamiast synonimuINDEXi co najmniej jednegoKEY; - braku apostrofów i grawisów wokół nazw pól;
- typów pól zapisanych małymi literami, a słów kluczowych SQL, takich jak
CREATE TABLE, wielkimi; - długości przy każdym typie pola, który ją przyjmuje, na przykład
int(11).
dbDelta() nie zastępuje pełnej migracji. Funkcja nie usuwa starych kolumn; nowa nazwa kolumny oznacza początkowo dodatkową kolumnę. Komunikat o planowanej zmianie nie dowodzi też powodzenia operacji w bazie. Dlatego przykład sprawdza później nazwy wymaganych kolumn. Nie jest to pełna kontrola typów ani indeksów. Zmiana nazw, usuwanie i przekształcanie danych wymagają osobnych kroków. $wpdb->get_charset_collate() dostarcza ustawienia zestawu znaków i sortowania.
Poniższy plik główny zapisuje wyszukiwania na witrynie. Raport w menu „Narzędzia” jest dostępny dla kont z uprawnieniem lwsl_view_log. Zadanie WP-Cron zaplanowane raz dziennie usuwa wpisy starsze niż 90 dni w chwili rzeczywistego wykonania. WP-Cron potrzebuje wywołania uruchamiającego zaległe zadania. Bez takiego wywołania wpisy pozostają dłużej. Kod nie gwarantuje więc usunięcia dokładnie po 90 dniach.
/**
* Plugin Name: LW Search Log
* Description: Stores front-end search terms in a custom table and prunes old rows once a day.
* Version: 1.1.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: Lukas Wojcik
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: lw-search-log
*
* @package LW_Search_Log
*/
defined( 'ABSPATH' ) || exit;
const LWSL_DB_VERSION = 2;
const LWSL_CRON_HOOK = 'lwsl_prune_log';
const LWSL_CAP = 'lwsl_view_log';
/**
* Returns the log table name of the current site.
*
* @return string
*/
function lwsl_table() {
global $wpdb;
return $wpdb->prefix . 'lwsl_searches';
}
/**
* Creates the log table or brings it to the current structure.
*/
function lwsl_create_table() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$table = lwsl_table();
$collate = $wpdb->get_charset_collate();
// dbDelta() parses this text: one column per line, two spaces after
// PRIMARY KEY, KEY instead of INDEX, no backticks around names.
$sql = "CREATE TABLE {$table} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
term varchar(190) NOT NULL DEFAULT '',
result_count int(11) unsigned NOT NULL DEFAULT 0,
searched_at datetime NOT NULL,
PRIMARY KEY (id),
KEY searched_at (searched_at)
) {$collate};";
dbDelta( $sql );
// Verify the structure, not dbDelta()'s descriptive return messages.
$columns = $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $table ) );
if ( $wpdb->last_error || array_diff( array( 'id', 'term', 'result_count', 'searched_at' ), (array) $columns ) ) {
return false;
}
return true;
}
/**
* Data migration for schema version 2: terms are stored in lower case.
* The new column result_count is added by dbDelta() itself.
*/
function lwsl_migrate_to_2() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
return false !== $wpdb->query( $wpdb->prepare( 'UPDATE %i SET term = LOWER(term)', lwsl_table() ) );
}
/**
* Brings the current site to the state this code expects.
* Idempotent: used on activation, after updates and for new sites.
*/
function lwsl_setup_site() {
global $wpdb;
// Advisory lock on this database connection; no waiting in a web request.
$lock = 'lwsl:' . md5( DB_NAME . ':' . $wpdb->prefix );
if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lock ) ) ) {
return false;
}
try {
$installed = (int) get_option( 'lwsl_db_version', 0 );
if ( $installed > LWSL_DB_VERSION || ! lwsl_create_table() ) {
return false;
}
if ( $installed > 0 && $installed < 2 && ! lwsl_migrate_to_2() ) {
return false;
}
$role = get_role( 'administrator' );
if ( ! $role ) {
return false;
}
if ( ! $role->has_cap( LWSL_CAP ) ) {
$role->add_cap( LWSL_CAP );
if ( $wpdb->last_error ) {
return false;
}
}
if ( ! wp_next_scheduled( LWSL_CRON_HOOK ) ) {
$result = wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', LWSL_CRON_HOOK, array(), true );
if ( is_wp_error( $result ) || ! $result ) {
return false;
}
}
update_option( 'lwsl_db_version', LWSL_DB_VERSION, true );
return LWSL_DB_VERSION === (int) get_option( 'lwsl_db_version', 0 );
} finally {
$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) );
}
}
/**
* Runs a callback once for every site of the network, 100 sites per query.
*
* @param callable $callback Function without parameters.
*/
function lwsl_for_each_site( $callback ) {
$offset = 0;
do {
$site_ids = get_sites(
array(
'network_id' => get_current_network_id(),
'fields' => 'ids',
'number' => 100,
'offset' => $offset,
)
);
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
try {
call_user_func( $callback );
} finally {
restore_current_blog();
}
}
$offset += 100;
} while ( 100 === count( $site_ids ) );
}
/**
* Activation: set up the current site, or every site on network activation.
*
* @param bool $network_wide True when the plugin is network-activated.
*/
function lwsl_activate( $network_wide = false ) {
if ( is_multisite() && $network_wide ) {
// On large networks the sites are set up lazily by lwsl_maybe_upgrade().
if ( ! wp_is_large_network() ) {
lwsl_for_each_site( 'lwsl_setup_site' );
}
return;
}
lwsl_setup_site();
}
register_activation_hook( __FILE__, 'lwsl_activate' );
/**
* Update path: activation hooks do not run on updates, so the stored
* schema version is compared with the code on every request.
*/
function lwsl_maybe_upgrade() {
if ( (int) get_option( 'lwsl_db_version', 0 ) < LWSL_DB_VERSION ) {
lwsl_setup_site();
}
}
add_action( 'plugins_loaded', 'lwsl_maybe_upgrade' );
/**
* Stops scheduled work and drops caches on the current site. Keeps all data.
*/
function lwsl_teardown_site() {
wp_clear_scheduled_hook( LWSL_CRON_HOOK );
delete_transient( 'lwsl_top_terms' );
}
/**
* Deactivation: current site, or every site on network deactivation.
*
* @param bool $network_deactivating True when the plugin is network-deactivated.
*/
function lwsl_deactivate( $network_deactivating = false ) {
if ( is_multisite() && $network_deactivating ) {
lwsl_for_each_site( 'lwsl_teardown_site' );
return;
}
lwsl_teardown_site();
}
register_deactivation_hook( __FILE__, 'lwsl_deactivate' );
/**
* Cron callback: deletes log rows older than 90 days.
*/
function lwsl_prune_log() {
global $wpdb;
if ( LWSL_DB_VERSION !== (int) get_option( 'lwsl_db_version', 0 ) ) {
return;
}
$cutoff = gmdate( 'Y-m-d H:i:s', time() - 90 * DAY_IN_SECONDS );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE searched_at < %s', lwsl_table(), $cutoff ) );
delete_transient( 'lwsl_top_terms' );
}
add_action( LWSL_CRON_HOOK, 'lwsl_prune_log' );
/**
* Logs the search term of the main front-end query.
*/
function lwsl_log_search() {
global $wpdb, $wp_query;
if ( LWSL_DB_VERSION !== (int) get_option( 'lwsl_db_version', 0 ) || ! is_search() || is_paged() ) {
return;
}
$term = mb_substr( sanitize_text_field( get_search_query( false ) ), 0, 190 );
if ( '' === $term ) {
return;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare(
'INSERT INTO %i (term, result_count, searched_at) VALUES (LOWER(%s), %d, %s)',
lwsl_table(),
$term,
(int) $wp_query->found_posts,
current_time( 'mysql', true )
)
);
}
add_action( 'wp', 'lwsl_log_search' );
/**
* Adds the log page under Tools, visible with the plugin capability only.
*/
function lwsl_admin_menu() {
add_management_page(
__( 'Search log', 'lw-search-log' ),
__( 'Search log', 'lw-search-log' ),
LWSL_CAP,
'lwsl-search-log',
'lwsl_render_page'
);
}
add_action( 'admin_menu', 'lwsl_admin_menu' );
/**
* Renders the top terms (cached in a transient) and the searches since the
* current user's last visit (stored in user meta).
*/
function lwsl_render_page() {
global $wpdb;
if ( ! current_user_can( LWSL_CAP ) ) {
wp_die( esc_html__( 'Access denied.', 'lw-search-log' ) );
}
if ( LWSL_DB_VERSION !== (int) get_option( 'lwsl_db_version', 0 ) ) {
wp_die( esc_html__( 'Search log setup is incomplete.', 'lw-search-log' ) );
}
$user_id = get_current_user_id();
$last_seen = (int) get_user_meta( $user_id, $wpdb->prefix . 'lwsl_last_viewed', true );
update_user_meta( $user_id, $wpdb->prefix . 'lwsl_last_viewed', time() );
$top = get_transient( 'lwsl_top_terms' );
if ( false === $top ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- cached in a transient.
$top = $wpdb->get_results(
$wpdb->prepare( 'SELECT term, COUNT(*) AS hits FROM %i GROUP BY term ORDER BY hits DESC LIMIT 20', lwsl_table() )
);
set_transient( 'lwsl_top_terms', $top, HOUR_IN_SECONDS );
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$new = (int) $wpdb->get_var(
$wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE searched_at > %s', lwsl_table(), gmdate( 'Y-m-d H:i:s', $last_seen ) )
);
echo '<div class="wrap"><h1>' . esc_html__( 'Search log', 'lw-search-log' ) . '</h1><p>';
echo esc_html(
sprintf(
/* translators: %d: number of searches since the last visit */
__( 'New searches since the last visit: %d', 'lw-search-log' ),
$new
)
);
echo '</p><ol>';
foreach ( $top as $row ) {
printf( '<li>%s (%d)</li>', esc_html( $row->term ), (int) $row->hits );
}
echo '</ol></div>';
}
/**
* Multisite: drop the log table together with a deleted site.
*
* @param string[] $tables Tables WordPress is about to drop.
* @param int $site_id ID of the deleted site.
* @return string[]
*/
function lwsl_drop_site_table( $tables, $site_id ) {
global $wpdb;
$tables[] = $wpdb->get_blog_prefix( $site_id ) . 'lwsl_searches';
return $tables;
}
add_filter( 'wpmu_drop_tables', 'lwsl_drop_site_table', 10, 2 );lwsl_setup_site() może działać kolejno wiele razy bez celowego tworzenia dodatkowych tabel ani zdarzeń Cron. To znaczenie słowa „idempotentna”; równoległe wywołania wymagają osobnego zabezpieczenia. Nazwana blokada bazy ogranicza konfigurację do jednego wywołania na witrynę w danym połączeniu z bazą. Brak kolumn, błąd migracji lub błąd planowania Cron uniemożliwia zapis wersji 2. Późniejsze żądanie ponawia konfigurację. Zapis wyszukiwań i raport pozostają wyłączone do uzyskania oczekiwanej wersji. Wersja 2 dodaje result_count i przy przejściu z wersji 1 zmienia istniejące frazy na małe litery.
lwsl_db_version jest często odczytywana. Autoload pobiera tę małą opcję razem z innymi często potrzebnymi opcjami. Bez autoload możliwe jest dodatkowe zapytanie do bazy, ale pamięć podręczna obiektów może zapobiec zapytaniu przy każdym odsłonięciu strony. add_cap() przekazuje opcję ról do update_option(); niezmieniona wartość nie jest ponownie zapisywana. Symbol %i w $wpdb->prepare() obsługuje nazwy tabel jako identyfikatory bazy i jest dostępny od WordPressa 6.2.
Dezaktywacja: zatrzymać, nie kasować
Dezaktywacja podczas szukania błędu jest często tymczasowa. Dlatego przykład usuwa zaplanowane zdarzenie Cron oraz zapisany ranking. WordPress nazywa taki tymczasowy wpis pamięci podręcznej transientem. Tabela, wersja schematu, uprawnienie oraz czas ostatniego otwarcia raportu pozostają dostępne.
wp_clear_scheduled_hook() usuwa zdarzenia hooka z dokładnie podanymi argumentami. wp_unschedule_hook() usuwa wszystkie zdarzenia tego hooka, również z innymi argumentami. Przykład planuje zadanie bez argumentów, więc podczas dezaktywacji wystarcza pierwsza funkcja. Odinstalowanie korzysta z drugiej, aby usunąć także inaczej skonfigurowane zdarzenia tego hooka wtyczki.
Przy dezaktywacji sieciowej funkcja otrzymuje true. Pętla przetwarza wtedy tylko witryny bieżącej sieci. Uprawnienie pozostaje na potrzeby późniejszej aktywacji. Sam raport nie jest dostępny, gdy wtyczka jest wyłączona.
Odinstalowanie: uninstall.php, register_uninstall_hook i zgoda
Podczas zwykłego usuwania delete_plugins() najpierw wywołuje uninstall_plugin(). Istniejący plik uninstall.php ma pierwszeństwo przed zarejestrowanym callbackiem. WordPress ustawia WP_UNINSTALL_PLUGIN i ładuje ten plik bez dodatkowego ładowania pliku głównego w tym celu. Dlatego plik odinstalowania zawiera wszystkie potrzebne nazwy. Bez uninstall.php WordPress ładuje plik główny i wywołuje zapisany callback; działa wtedy również kod poza funkcjami.
Kontrola WP_UNINSTALL_PLUGIN blokuje przewidziany przebieg porządków przy bezpośrednim wywołaniu pliku. Callback zarejestrowany przez register_uninstall_hook() trafia do opcji. Domknięcie, czyli funkcja anonimowa, nie daje się serializować do takiego zapisu i powoduje błąd. Metoda konkretnego obiektu również jest odrzucana. Poprawne są nazwa funkcji lub statyczna metoda klasy. Rejestracja należy do aktywacji. Poniższe wywołania pokazują alternatywy i błędne przypadki; cały blok nie jest przeznaczony do wspólnego uruchomienia.
// Counter-example: what register_uninstall_hook() accepts and what it rejects.
// Accepted: a function name or a static method. The callback is stored
// in the option uninstall_plugins, so register it once, on activation.
register_uninstall_hook( __FILE__, 'lwsl_uninstall' );
register_uninstall_hook( __FILE__, array( 'LWSL_Plugin', 'uninstall' ) );
// Rejected: a closure cannot be serialized into that option (fatal error),
// an object method triggers _doing_it_wrong() and is not registered.
register_uninstall_hook( __FILE__, function () {} );
register_uninstall_hook( __FILE__, array( new LWSL_Plugin(), 'uninstall' ) );Usunięcie plików wtyczki nie musi oznaczać rezygnacji z danych: ponowna instalacja może wykorzystać ten sam dziennik. Procedura usuwa więc zdarzenia Cron, pamięć podręczną i uprawnienia ról niezależnie od przełącznika. Tabele dziennika i związane ustawienia usuwa tylko przy wartości 1. W multisite decyzja należy do każdej sieci osobno. Błąd usuwania tabeli zatrzymuje procedurę przed usunięciem wersji schematu tej witryny.
/**
* Uninstall routine of LW Search Log.
*
* WordPress includes this file when the plugin is deleted. The main plugin
* file is not loaded, so every name is spelled out here.
*
* @package LW_Search_Log
*/
// Only run when WordPress itself starts the uninstall.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
/**
* Cleans one site: technical leftovers always, data only after opt-in.
*
* @param bool $delete_data Whether the site owner opted in to data removal.
*/
function lwsl_uninstall_site( $delete_data ) {
global $wpdb;
// Always: scheduled events, caches and the capability.
wp_unschedule_hook( 'lwsl_prune_log' );
delete_transient( 'lwsl_top_terms' );
foreach ( wp_roles()->role_objects as $role ) {
if ( $role->has_cap( 'lwsl_view_log' ) ) {
$role->remove_cap( 'lwsl_view_log' );
}
}
if ( ! $delete_data ) {
return;
}
// Opt-in only: the log table and the schema version.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
$result = $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'lwsl_searches' ) );
if ( false === $result ) {
throw new RuntimeException( 'Search log table could not be removed.' );
}
delete_option( 'lwsl_db_version' );
delete_metadata( 'user', 0, $wpdb->prefix . 'lwsl_last_viewed', '', true );
}
// Files are shared across networks, but each network owns its opt-in.
// Accept the documented value 1 only; a string such as "false" is not consent.
if ( is_multisite() ) {
$offset = 0;
do {
$sites = get_sites( array( 'number' => 100, 'offset' => $offset ) );
foreach ( $sites as $site ) {
$delete_data = '1' === (string) get_network_option( (int) $site->network_id, 'lwsl_delete_data_on_uninstall', false );
switch_to_blog( (int) $site->blog_id );
try {
lwsl_uninstall_site( $delete_data );
} finally {
restore_current_blog();
}
}
$offset += 100;
} while ( 100 === count( $sites ) );
// The consent options stay until all sites have been processed successfully.
foreach ( get_networks( array( 'number' => 0 ) ) as $network ) {
if ( '1' === (string) get_network_option( (int) $network->id, 'lwsl_delete_data_on_uninstall', false ) ) {
delete_network_option( (int) $network->id, 'lwsl_delete_data_on_uninstall' );
}
}
} else {
$delete_data = '1' === (string) get_option( 'lwsl_delete_data_on_uninstall', false );
lwsl_uninstall_site( $delete_data );
if ( $delete_data ) {
delete_option( 'lwsl_delete_data_on_uninstall' );
}
}
// Legacy unprefixed lwsl_last_viewed metadata is deliberately not deleted:
// its ownership across sites is ambiguous and requires a separate cleanup plan.Czas ostatniego otwarcia raportu otrzymuje teraz klucz z prefiksem witryny, na przykład wp_2_lwsl_last_viewed. Otwarcie raportu jednej witryny nie zmienia więc licznika drugiej. delete_metadata() z $delete_all = true usuwa dokładnie ten klucz dla wszystkich kont. Wartości pod dawnym wspólnym kluczem pozostają, ponieważ brak pewnego przypisania do witryny. delete_transient() obsługuje również zewnętrzną pamięć podręczną obiektów; samo usunięcie wpisów z wp_options nie opróżni takiej pamięci.
Poniższe polecenia usuwają dane i są przeznaczone wyłącznie do tymczasowej instalacji testowej. Sposób ustawiania przełącznika to zadanie strony ustawień; Settings API omawia osobny artykuł z tej serii. Do testu wystarczy WP-CLI. wp plugin uninstall z opcją --skip-delete wykonuje tylko procedurę odinstalowania i zostawia pliki, a wp plugin delete usuwa pliki bez dezaktywacji i bez odinstalowania. Tę różnicę dokładniej opisuje artykuł o poleceniach usuwania w WP-CLI i tabelach, których dotykają.
# Single site: opt in, then run only the uninstall routine (files stay).
wp option update lwsl_delete_data_on_uninstall 1
wp plugin deactivate lw-search-log
wp plugin uninstall lw-search-log --skip-delete
# Multisite: the switch is a network option.
wp site option update lwsl_delete_data_on_uninstall 1
wp plugin deactivate lw-search-log --network
wp plugin uninstall lw-search-log --skip-delete
# Deletes the files only: neither deactivation nor uninstall code runs.
wp plugin delete lw-search-logMultisite: raz na sieć, dane na każdej witrynie
Przykład przechowuje tabele, opcje, zdarzenia Cron i role osobno dla każdej witryny. Metadane kont znajdują się we wspólnej tabeli. Prefiks witryny w kluczu metadanych rozdziela czasy otwarcia raportu. get_network_option() odczytuje zgodę na usunięcie danych właściwej sieci; pojedyncza instalacja używa get_option(). Zgoda jednej sieci nie decyduje więc przypadkowo o danych innej.
Aktywacja sieciowa wywołuje hook aktywacji raz, a nie raz na witrynę. Własna pętla pobiera więc najwyżej 100 identyfikatorów witryn bieżącej sieci na zapytanie. switch_to_blog() zmienia kontekst witryny, ale nie ładuje innej wersji wtyczki. finally przywraca poprzedni kontekst również po wyjątku. Odinstalowanie obejmuje natomiast wszystkie sieci, ponieważ pliki wtyczki są wspólne.
Bardzo duża sieć może przekroczyć czas dostępny dla jednego żądania aktywacji. Bez dodatkowego argumentu wp_is_large_network() sprawdza liczbę witryn: domyślny próg to ponad 10 000. Sprawdzenie liczby kont wymaga odpowiedniego argumentu; filtry mogą zmienić wynik. W dużej sieci przykład odkłada konfigurację do pierwszego żądania każdej witryny z aktywną wtyczką. Dotyczy to także później utworzonych witryn. Natychmiastową konfigurację nowych witryn można dodać przez wp_initialize_site, po inicjalizacji wykonywanej przez rdzeń.
Filtr wpmu_drop_tables dodaje tabelę wtyczki do usuwania wraz z witryną. Działa to tylko przy załadowanym kodzie wtyczki. Gdy wtyczka jest wyłączona, tabela może pozostać. Późniejsze odinstalowanie nie znajdzie już usuniętej witryny przez get_sites(). Takie pozostałości wymagają osobnych, sprawdzonych porządków. Odinstalowanie obejmujące wiele istniejących witryn także może przekroczyć limit czasu; przykład nie ma kolejki zadań z możliwością wznowienia.
Ograniczenia i otwarte kwestie
Historyczny raport dokumentuje testy pierwotnej wersji w WordPressie 7.1.2, PHP 8.4 i MariaDB 11.8, na pojedynczej instalacji oraz w sieci opartej na podkatalogach. Testy obejmowały aktywację, zmianę schematu, nowe i usunięte witryny oraz odinstalowanie ze zgodą i bez zgody. Plugin Check zgłaszał wtedy tylko brak readme.txt. Poprawiona wersja przedstawiona tutaj przeszła kontrolę składni PHP i dwanaście izolowanych testów, w tym przypadków błędów z zastępczymi funkcjami WordPressa. Nie stanowi to nowego pełnego testu tej wersji w WordPressie, w wielu sieciach ani przy rzeczywistych równoległych żądaniach.
GET_LOCK()działa na serwerze bazy obsługującym żądanie i jest związana z połączeniem. Utrata połączenia lub inny serwer za proxy może znieść to zabezpieczenie. Blokada nie jest transakcją i nie cofa wykonanych zmian. Przed użyciem produkcyjnym wymaga sprawdzenia w rzeczywistym środowisku.- Podręcznik wymaga podawania długości także przy typach całkowitych, takich jak
int(11). W MariaDB 11.8 powtarzane wywołaniadbDelta()zint(11)nie zgłaszały zmian; bez podanej długości każde wywołanie zgłaszało „Changed type”, bo MariaDB pokazujeint(10). W MySQL od 8.0.17 rdzeń od WordPressa 6.1 pomija same różnice szerokości (według kodu źródłowego, bez pomiaru). - Capabilities przypisane bezpośrednio pojedynczym użytkownikom są zapisane w meta użytkowników i pętla po rolach ich nie usuwa.
- Poprawiona procedura odczytuje zgodę osobno dla każdej sieci i usuwa czasy wizyt tylko pod właściwym prefiksem witryny. Dawne wspólne klucze metadanych i uprawnienia nadane bezpośrednio kontom wymagają osobnych porządków. Późniejszy błąd nie cofa wcześniej wykonanych kroków.
- Samo usunięcie plików przez FTP, Git lub proces wdrożeniowy nie uruchamia
uninstall.php. Usuwanie danych z bazy wymaga w takim procesie dodatkowego, wcześniej sprawdzonego kroku.
Pytania i odpowiedzi
Czy uninstall.php działa także wtedy, gdy wtyczkę usunięto przez FTP lub poleceniem wp plugin delete?
Nie. FTP oraz wp plugin delete usuwają pliki bez wykonania procedury odinstalowania. wp plugin uninstall uruchamia tę procedurę; opcja --skip-delete zachowuje pliki. Proces oparty na Git lub skrypcie wdrożeniowym usuwa dane z bazy tylko wtedy, gdy osobny krok porządkowania został odpowiednio skonfigurowany.
Co się dzieje, gdy wtyczka korzysta jednocześnie z uninstall.php i register_uninstall_hook()?
uninstall.php ma pierwszeństwo. WordPress usuwa w tym przebiegu zarejestrowany callback z opcji uninstall_plugins i ładuje plik. Drugi callback nie jest dodatkowo wykonywany.
Dlaczego kontrola wersji działa na plugins_loaded, a nie na admin_init?
admin_init działa dla stron administracyjnych, admin-ajax.php i admin-post.php, lecz nie dla zwykłego żądania strony publicznej. Po wymianie plików nowy kod mógłby więc trafić na starą strukturę tabeli. plugins_loaded sprawdza wersję wcześniej, w żądaniach ładujących wtyczkę. Przykład dodatkowo blokuje użycie tabeli do uzyskania oczekiwanej wersji.
Źródła
Pierwotny przegląd źródeł: 29.09.2026. Poprawki zweryfikowano 25.10.2026 na podstawie dokumentacji WordPressa i MariaDB.
- Plugin Handbook: Activation / Deactivation Hooks
- Plugin Handbook: Uninstall Methods
- Plugin Handbook: Creating Tables with Plugins
- Make WordPress Core: Plugin activation hooks no longer fire for updates (27.10.2010)
- Dokumentacja funkcji: register_deactivation_hook()
- Dokumentacja funkcji: register_uninstall_hook()
- Dokumentacja funkcji: dbDelta()
- Dokumentacja hooka: upgrader_process_complete
- Dokumentacja funkcji: wp_clear_scheduled_hook()
- Dokumentacja funkcji: wp_unschedule_hook()
- Dokumentacja funkcji: get_sites()
- Dokumentacja funkcji: switch_to_blog()
- Kod źródłowy WordPress 7.1.2: wp-admin/includes/plugin.php
- Kod źródłowy WordPress 7.1.2: wp-admin/update.php
- Kod źródłowy WordPress 7.1.2: class-plugin-upgrader.php
- Kod źródłowy WordPress 7.1.2: wp-includes/plugin.php
- Kod źródłowy WordPress 7.1.2: wp-includes/option.php
- Kod źródłowy WordPress 7.1.2: wp-includes/meta.php
- Kod źródłowy WordPress 7.1.2: class-wp-roles.php
- Kod źródłowy WordPress 7.1.2: wp-includes/cron.php
- Kod źródłowy WordPress 7.1.2: wp-includes/ms-site.php
- Kod źródłowy WordPress 7.1.2: ms-default-filters.php
- Kod źródłowy WordPress 7.1.2: ms-functions.php
- Kod źródłowy WordPress 7.1.2: capabilities.php
- Kod źródłowy WordPress 7.1.2: class-wpdb.php
- WP-CLI: wp plugin uninstall
- WP-CLI: wp plugin delete
- WP-CLI: wp site option update
- admin_init
- wp_is_large_network()
- WP_Site
- MariaDB: GET_LOCK