remove_action sau remove_filter cu clase externe?
Într-o situație în care un plugin și-a încapsulat metodele într-o clasă și apoi a înregistrat un filtru sau o acțiune pentru una dintre aceste metode, cum poți elimina acțiunea sau filtrul dacă nu mai ai acces la instanța acelei clase?
De exemplu, să presupunem că ai un plugin care face următoarele:
class MyClass {
function __construct() {
add_action( "plugins_loaded", array( $this, 'my_action' ) );
}
function my_action() {
// execută diverse...
}
}
new MyClass();
Observând că acum nu mai am nicio modalitate de a accesa instanța, cum pot să deînregistrez clasa? Această abordare: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );
nu pare să fie cea corectă - cel puțin, nu a funcționat în cazul meu.
Ori de câte ori un plugin creează un new MyClass();
, ar trebui să îl atribuie unei variabile cu un nume unic. În acest fel, instanța clasei este accesibilă.
Deci, dacă ar fi făcut $myclass = new MyClass();
, atunci ai putea face asta:
global $myclass;
remove_action( 'wp_footer', array( $myclass, 'my_action' ) );
Aceasta funcționează pentru că pluginurile sunt incluse în namespace-ul global, așadar declarațiile implicite de variabile în corpul principal al unui plugin sunt variabile globale.
Dacă pluginul nu salvează identificatorul noii clase undeva, atunci, tehnic vorbind, este un bug. Unul dintre principiile generale ale Programării Orientate pe Obiecte este că obiectele care nu sunt referențiate de o variabilă undeva sunt supuse curățării sau eliminării.
Acum, PHP în special nu face asta așa cum ar face Java, deoarece PHP este o implementare cam pe jumătate a OOP. Variabilele de instanță sunt doar șiruri de caractere cu nume unice de obiecte în ele, cam așa. Ele funcționează doar datorită modului în care interacțiunea numelui variabilei funcționează cu operatorul ->
. Deci, doar făcând new class()
poate funcționa perfect, dar stupid. :)
Așadar, concluzia este să nu faci niciodată new class();
. Fă $var = new class();
și asigură-te că acea variabilă $var este accesibilă într-un fel pentru alte părți să o poată referenția.
Editare: ani mai târziu
Un lucru pe care l-am văzut la multe pluginuri este utilizarea unui model similar cu "Singleton". Ele creează o metodă getInstance() pentru a obține singura instanță a clasei. Aceasta este probabil cea mai bună soluție pe care am văzut-o. Exemplu de plugin:
class ExamplePlugin
{
protected static $instance = NULL;
public static function getInstance() {
NULL === self::$instance and self::$instance = new self;
return self::$instance;
}
}
Prima dată când getInstance() este apelată, instantiază clasa și salvează pointerul acesteia. Poți folosi asta pentru a lega acțiuni.
O problemă cu aceasta este că nu poți folosi getInstance() în interiorul constructorului dacă folosești așa ceva. Acest lucru se întâmplă pentru că "new" apelează constructorul înainte de a seta $instance, așadar apelarea getInstance() din constructor duce la o buclă infinită și strică totul.
O soluție este să nu folosești constructorul (sau, cel puțin, să nu folosești getInstance() în el), ci să ai în mod explicit o funcție "init" în clasă pentru a configura acțiunile tale și altele. Ca aceasta:
public static function init() {
add_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
}
Cu ceva de genul acesta, la sfârșitul fișierului, după ce clasa a fost definită complet, instantierea pluginului devine la fel de simplă ca aceasta:
ExamplePlugin::init();
Funcția init începe să adauge acțiunile tale, iar în acest proces apelează getInstance(), care instantiază clasa și se asigură că există doar una singură. Dacă nu ai o funcție init, ai face asta pentru a instantia clasa inițial:
ExamplePlugin::getInstance();
Pentru a răspunde la întrebarea inițială, eliminarea acelui action hook din exterior (adică, din alt plugin) poate fi făcută astfel:
remove_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
Pune asta în ceva legat de action hook-ul plugins_loaded
și va anula acțiunea legată de pluginul original.

+1 Adevărat. Aceasta este clar o practică recomandată. Ar trebui să ne străduim cu toții să scriem codul pluginului în acest fel.

+1 aceste instrucțiuni m-au ajutat cu adevărat să elimin un filtru într-o clasă cu model singleton.

+1, dar cred că în general ar trebui să folosești hook-ul wp_loaded
, nu plugins_loaded
, care poate fi apelat prea devreme.

Nu, plugins_loaded
ar fi locul corect. Acțiunea wp_loaded
are loc după acțiunea init
, deci dacă plugin-ul tău efectuează orice acțiuni pe init
(și majoritatea o fac), atunci vrei să inițializezi și să configurezi plugin-ul înainte de asta. Hook-ul plugins_loaded
este locul potrivit pentru această fază de construcție.

Acest răspuns poate fi puțin întârziat. Dar îți mulțumesc pentru această observație. După ce am căutat ore întregi, am realizat că nu am respectat principiile generale ale Programării Orientate pe Obiecte, care afirmă că obiectele care nu sunt referențiate de nicio variabilă undeva sunt supuse eliminării sau curățării.

Cel mai bun lucru de făcut aici este să folosești o clasă statică. Următorul cod ar trebui să fie instructiv:
class MyClass {
function __construct() {
add_action( 'wp_footer', array( $this, 'my_action' ) );
}
function my_action() {
print '<h1>' . __class__ . ' - ' . __function__ . '</h1>';
}
}
new MyClass();
class MyStaticClass {
public static function init() {
add_action( 'wp_footer', array( __class__, 'my_action' ) );
}
public static function my_action() {
print '<h1>' . __class__ . ' - ' . __function__ . '</h1>';
}
}
MyStaticClass::init();
function my_wp_footer() {
print '<h1>my_wp_footer()</h1>';
}
add_action( 'wp_footer', 'my_wp_footer' );
function mfields_test_remove_actions() {
remove_action( 'wp_footer', 'my_wp_footer' );
remove_action( 'wp_footer', array( 'MyClass', 'my_action' ), 10 );
remove_action( 'wp_footer', array( 'MyStaticClass', 'my_action' ), 10 );
}
add_action( 'wp_head', 'mfields_test_remove_actions' );
Dacă rulezi acest cod dintr-un plugin, ar trebui să observi că metoda StaticClass, precum și funcția, vor fi eliminate din wp_footer.

Punct de vedere acceptat, dar nu toate clasele pot fi pur și simplu transformate în statice.

Am acceptat acest răspuns pentru că răspunde cel mai direct la întrebare, deși răspunsul lui Otto este cea mai bună practică. Menționez aici că nu cred că trebuie să declari în mod explicit static. Din experiența mea (deși aș putea greși), poți trata funcția ca și cum ar fi statică array( 'MyClass', 'member_function' ) și de multe ori funcționează fără cuvântul cheie 'static'.

@TomAuger nu, nu poți, DOAR dacă este adăugată ca o clasă statică poți folosi funcția remove_action
, altfel nu va funcționa... de aceea a trebuit să scriu propria mea funcție pentru a gestiona cazul în care nu este o clasă statică. Acest răspuns ar fi cel mai bun doar dacă întrebarea ta era legată de propriul cod, altfel vei încerca să elimini un alt filtru/acțiune din codul altcuiva și nu poți să-l schimbi în static

2 funcții PHP mici pentru a permite eliminarea filtrelor/acțiunilor cu clase "anonime": https://github.com/herewithme/wp-filters-extras/

După cum au menționat și alții în postarea mea de mai jos, acestea nu vor funcționa în WordPress 4.7 (decât dacă repo-ul este actualizat, dar nu s-a întâmplat în ultimii 2 ani)

Iată o funcție bine documentată pe care am creat-o pentru eliminarea filtrelor atunci când nu ai acces la obiectul clasei (funcționează cu WordPress 1.2+, inclusiv 4.7+):
https://gist.github.com/tripflex/c6518efc1753cf2392559866b4bd1a53
/**
* Elimină Filtru de Clasă Fără Acces la Obiectul Clasei
*
* Pentru a utiliza remove_filter() din nucleul WordPress pe un filtru adăugat cu callback-ul
* către o clasă, fie trebuie să ai acces la acel obiect de clasă, fie trebuie să fie un apel
* către o metodă statică. Această metodă îți permite să elimini filtrele cu un callback către o clasă
* la care nu ai acces.
*
* Funcționează cu WordPress 1.2+ (suport pentru 4.7+ adăugat pe 19-09-2016)
* Actualizat pe 27-02-2017 pentru a utiliza eliminarea internă WordPress pentru 4.7+ (pentru a preveni avertismentele PHP)
*
* @param string $tag Filtru de eliminat
* @param string $class_name Numele clasei pentru callback-ul filtrului
* @param string $method_name Numele metodei pentru callback-ul filtrului
* @param int $priority Prioritatea filtrului (implicit 10)
*
* @return bool Dacă funcția a fost eliminată.
*/
function remove_class_filter( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
global $wp_filter;
// Verifică dacă filtrul există de fapt
if ( ! isset( $wp_filter[ $tag ] ) ) return FALSE;
/**
* Dacă configurația filtrului este un obiect, înseamnă că folosim WordPress 4.7+ și configurația nu mai este
* un simplu array, ci un obiect care implementează interfața ArrayAccess.
*
* Pentru compatibilitate înapoi, setăm $callbacks egal cu array-ul corect ca referință (astfel $wp_filter este actualizat)
*
* @see https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/
*/
if ( is_object( $wp_filter[ $tag ] ) && isset( $wp_filter[ $tag ]->callbacks ) ) {
// Crează obiectul $fob din eticheta filtrului, pentru a-l utiliza mai jos
$fob = $wp_filter[ $tag ];
$callbacks = &$wp_filter[ $tag ]->callbacks;
} else {
$callbacks = &$wp_filter[ $tag ];
}
// Ieși dacă nu există callback-uri pentru prioritatea specificată
if ( ! isset( $callbacks[ $priority ] ) || empty( $callbacks[ $priority ] ) ) return FALSE;
// Parcurge fiecare filtru pentru prioritatea specificată, căutând clasa și metoda noastră
foreach( (array) $callbacks[ $priority ] as $filter_id => $filter ) {
// Filtrul ar trebui să fie întotdeauna un array - array( $this, 'method' ), dacă nu, treci mai departe
if ( ! isset( $filter[ 'function' ] ) || ! is_array( $filter[ 'function' ] ) ) continue;
// Dacă prima valoare din array nu este un obiect, nu poate fi o clasă
if ( ! is_object( $filter[ 'function' ][ 0 ] ) ) continue;
// Metoda nu se potrivește cu cea pe care o căutăm, treci mai departe
if ( $filter[ 'function' ][ 1 ] !== $method_name ) continue;
// Metoda se potrivește, acum verificăm clasa
if ( get_class( $filter[ 'function' ][ 0 ] ) === $class_name ) {
// WordPress 4.7+ utilizează remove_filter() din nucleu deoarece am găsit obiectul clasei
if( isset( $fob ) ){
// Gestionează eliminarea filtrului, resetarea cheilor de prioritate callback în timpul iterației, etc.
$fob->remove_filter( $tag, $filter['function'], $priority );
} else {
// Folosește procesul de eliminare vechi (pre 4.7)
unset( $callbacks[ $priority ][ $filter_id ] );
// și dacă era singurul filtru din acea prioritate, elimină prioritatea
if ( empty( $callbacks[ $priority ] ) ) {
unset( $callbacks[ $priority ] );
}
// și dacă singurul filtru pentru acea etichetă, setează eticheta la un array gol
if ( empty( $callbacks ) ) {
$callbacks = array();
}
// Elimină acest filtru din merged_filters, care specifică dacă filtrele au fost sortate
unset( $GLOBALS['merged_filters'][ $tag ] );
}
return TRUE;
}
}
return FALSE;
}
/**
* Elimină Acțiune de Clasă Fără Acces la Obiectul Clasei
*
* Pentru a utiliza remove_action() din nucleul WordPress pe o acțiune adăugată cu callback-ul
* către o clasă, fie trebuie să ai acces la acel obiect de clasă, fie trebuie să fie un apel
* către o metodă statică. Această metodă îți permite să elimini acțiunile cu un callback către o clasă
* la care nu ai acces.
*
* Funcționează cu WordPress 1.2+ (suport pentru 4.7+ adăugat pe 19-09-2016)
*
* @param string $tag Acțiune de eliminat
* @param string $class_name Numele clasei pentru callback-ul acțiunii
* @param string $method_name Numele metodei pentru callback-ul acțiunii
* @param int $priority Prioritatea acțiunii (implicit 10)
*
* @return bool Dacă funcția a fost eliminată.
*/
function remove_class_action( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
return remove_class_filter( $tag, $class_name, $method_name, $priority );
}

Întrebare - ai testat asta în versiunea 4.7? Au fost unele modificări în modul în care sunt înregistrate funcțiile de callback în filtre care sunt complet noi. Nu m-am uitat în detaliu la codul tău, dar e ceva ce ar trebui verificat: https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/

Aha! Nu am făcut-o dar îți mulțumesc, cu siguranță voi investiga asta și voi actualiza codul să fie compatibil (dacă este nevoie)

@TomAuger mulțumesc pentru informație! Am actualizat funcția, testată și funcțională pe WordPress 4.7+ (cu menținerea compatibilității retroactive)

Soluțiile de mai sus par învechite, a trebuit să scriu propria mea soluție...
function remove_class_action ($action,$class,$method) {
global $wp_filter ;
if (isset($wp_filter[$action])) {
$len = strlen($method) ;
foreach ($wp_filter[$action] as $pri => $actions) {
foreach ($actions as $name => $def) {
if (substr($name,-$len) == $method) {
if (is_array($def['function'])) {
if (get_class($def['function'][0]) == $class) {
if (is_object($wp_filter[$action]) && isset($wp_filter[$action]->callbacks)) {
unset($wp_filter[$action]->callbacks[$pri][$name]) ;
} else {
unset($wp_filter[$action][$pri][$name]) ;
}
}
}
}
}
}
}
}

În astfel de cazuri, WordPress adaugă un hash (ID unic) numelui funcției și îl stochează în variabila globală $wp_filter
. Așadar, dacă folosești funcția remove_filter
, nu se va întâmpla nimic. Chiar dacă adaugi numele clasei înaintea numelui funcției, ca în remove_filter('plugins_loaded', ['MyClass', 'my_action'])
.
Tot ce poți face este să elimini manual toate hook-urile my_action
din variabila globală $wp_filter
.
Iată funcția care face asta:
function my_remove_filter($tag, $function_name, $priority = 10){
global $wp_filter;
if( isset($wp_filter[$tag]->callbacks[$priority]) and !empty($wp_filter[$tag]->callbacks[$priority]) ){
$wp_filter[$tag]->callbacks[$priority] = array_filter($wp_filter[$tag]->callbacks[$priority], function($v, $k) use ($function_name){
return ( stripos($k, $function_name) === false );
}, ARRAY_FILTER_USE_BOTH );
}
}
Folosește-o astfel:
my_remove_filter('plugins_loaded', 'my_action');

Acesta nu este un răspuns generic, ci unul specific pentru tema Avada și WooCommerce, despre care cred că și alți oameni ar putea să-l găsească util:
function remove_woo_commerce_hooks() {
global $avada_woocommerce;
remove_action( 'woocommerce_single_product_summary', array( $avada_woocommerce, 'add_product_border' ), 19 );
}
add_action( 'after_setup_theme', 'remove_woo_commerce_hooks' );

Am găsit acest Gist care făcea exact ce doream:
function remove_class_hook( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
global $wp_filter;
$is_hook_removed = false;
if ( ! empty( $wp_filter[ $tag ]->callbacks[ $priority ] ) ) {
$methods = array_filter(wp_list_pluck(
$wp_filter[ $tag ]->callbacks[ $priority ],
'function'
), function ($method) {
/**
* Permite doar notația de tip array și string pentru hook-uri, deoarece
* căutăm să eliminăm o metodă exactă a unei clase. În plus,
* metoda clasei este transmisă ca string.
*/
return is_string($method) || is_array($method);
});
$found_hooks = ! empty( $methods ) ? wp_list_filter( $methods, array( 1 => $method_name ) ) : array();
foreach( $found_hooks as $hook_key => $hook ) {
if ( ! empty( $hook[0] ) && is_object( $hook[0] ) && get_class( $hook[0] ) === $class_name ) {
$wp_filter[ $tag ]->remove_filter( $tag, $hook, $priority );
$is_hook_removed = true;
}
}
}
return $is_hook_removed;
}

Sunt destul de sigur că TripFlex (autorul gistului pe care l-ai găsit) este de fapt @sMyles, al cărui răspuns (https://wordpress.stackexchange.com/a/239431/3687) este acum răspunsul acceptat.

Această funcție este bazată pe răspunsul lui @Digerkam. Am adăugat o verificare suplimentară pentru a compara dacă $def['function'][0]
este un șir de caractere și în final a funcționat pentru mine.
De asemenea, utilizarea $wp_filter[$tag]->remove_filter()
ar trebui să o facă mai stabilă.
function remove_class_action($tag, $class = '', $method, $priority = null) : bool {
global $wp_filter;
if (isset($wp_filter[$tag])) {
$len = strlen($method);
foreach($wp_filter[$tag] as $_priority => $actions) {
if ($actions) {
foreach($actions as $function_key => $data) {
if ($data) {
if (substr($function_key, -$len) == $method) {
if ($class !== '') {
$_class = '';
if (is_string($data['function'][0])) {
$_class = $data['function'][0];
}
elseif (is_object($data['function'][0])) {
$_class = get_class($data['function'][0]);
}
else {
return false;
}
if ($_class !== '' && $_class == $class) {
if (is_numeric($priority)) {
if ($_priority == $priority) {
//if (isset( $wp_filter->callbacks[$_priority][$function_key])) {}
return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
}
}
else {
return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
}
}
}
else {
if (is_numeric($priority)) {
if ($_priority == $priority) {
return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
}
}
else {
return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
}
}
}
}
}
}
}
}
return false;
}
Exemplu de utilizare:
Potrivire exactă
add_action('plugins_loaded', function() {
remove_class_action('plugins_loaded', 'MyClass', 'my_action', 0);
});
Orice prioritate
add_action('plugins_loaded', function() {
remove_class_action('plugins_loaded', 'MyClass', 'my_action');
});
Orice clasă și orice prioritate
add_action('plugins_loaded', function() {
remove_class_action('plugins_loaded', '', 'my_action');
});
