WordPress-Fehlermeldung “widget_setting_too_many_options” – Problem und Lösung

Momentan beschäftige ich mich wieder verstärkt mit WordPress, was dazu geführt hat, ein Widget im Rahmen eines kleinen WordPress-Plugins zu entwickeln. Es handelt sich um ein Plugin, das ein neues Widget bereit stellt. Nach Fertigstellung wird es selbstverständlich veröffentlicht, letztlich habe ich es jedoch zunächst für den Eigenbedarf entwickelt. Und nicht zu unterschätzen ist der damit einher gehende Lerneffekt, wobei dieser mitunter dazu führt, sich einfach nur zu wundern, weil man auf ein Problem gestoßen ist, was entweder überhaupt nicht oder nur sehr spärlich dokumentiert ist. 

So ist es mir mit folgendem Fall passiert: Innerhalb der Widget-Admin-Oberfläche (“Design -> Widgets“) konnten ein oder mehreren Exemplare der neuen Widgets angelegt und den dafür im Theme vorgesehenen Plätzen zugeordnet werden. Auch das Ändern, Löschen etc. des neuen Widget-Typs funktionierte prima. Ich wähnte mich nahezu fertig, doch da gab es noch den Customizer mit der bekannten Preview-Funktionalität. Eigentlich hätte das kein Problem sein sollen, ich wollte nur kurz testen, denn in der Widget-UI konnten sämtliche Aktionen erfolgreich durchgeführt werden. Doch weit gefehlt!

Problem mit dem Widget in der Live-Preview im Customizer

Bereits das Ändern einer zuvor angelegten Widget-Instanz führte zu Fehlern. Das Problem stellte sich wie folgt dar: Beim Versuch, eine Widget-Option zu ändern, hörte der “Spinner” am unteren Rand des Widget-Editierformulars nicht mehr auf, sich zu “drehen”:

WordPress-Fehlermeldung "widget_setting_too_many_options" - Problem und Lösung 1

Widget-“Spinner” ist dauerhaft animiert

Darüber hinaus wurde oben innerhalb desselben Fensters eine Fehlermeldung angezeigt. Leider war diese nicht sehr aussagekräftig:

WordPress-Fehlermeldung "widget_setting_too_many_options" - Problem und Lösung 2

Fehlermeldung beim Editieren einer Widget-Einstellung

 

Dass ein Reload wie von der Meldung vorgeschlagen, das Problem nicht gelöst hat, muss ich wohl kaum erwähnen. Ein wenig weiter brachte mich schließlich der Blick in die Entwickler-Tools des Browsers. Während des Editierens im Customizer setzt WordPress quasi ständig Requests im Hintergrund ab, die die geänderten Daten umgehend speichern sollen. Nun meldete jedoch der Aufruf von “admin-ajax.php” nur einen Fehler zurück:

WordPress-Fehlermeldung "widget_setting_too_many_options" - Problem und Lösung 3

Fehlermeldung “widget_setting_too_many_options” im Customizer

 

Von der Fehlermeldung bis hin zum Code

Die Fehlermeldung “widget_setting_too_many_options” war immerhin ein Anhaltspunkt. Wenngleich sie auch nicht besonders eindeutig war. Das neue Widget speichert insgesamt 11 Einstellungen, davon sind nicht alle auf den ersten Blick sichtbar, sondern tauchen je nach Unterscheidung in der Darstellung auf. Waren es zu viele Optionen, die gespeichert würden? Doch in der “normalen” Widget-Admin-UI hatte dies problemlos funktioniert. 

Tatsächlich führte der übliche Weg, also Googlen, nochmal Googlen, diverse StackOverflow-, WordPress-Foren oder sonstige Quellen durchforsten nicht sofort zum Ziel. Insgesamt fanden sich nur wenige Fundstellen mit der Meldung “widget_setting_too_many_options” – und die meisten davon zeigten auf das entsprechende Code-Fragment im WordPress-Quellcode. Tatsächlich wird die Meldung ausgegeben innerhalb der Methode “call_widget_update()” der Datei wp-includes/class-wp-customize-widgets.php. Soweit nachviellziehbar,  das Fragment sieht wie folgt aus:

// Make sure the expected option was updated.
    if ( 0 !== $this->count_captured_options() ) {
        if ( $this->count_captured_options() > 1 ) {
            $this->stop_capturing_option_updates();
            return new WP_Error( 'widget_setting_too_many_options' );
        }
 
        $updated_option_name = key( $this->get_captured_options() );
        if ( $updated_option_name !== $option_name ) {
            $this->stop_capturing_option_updates();
            return new WP_Error( 'widget_setting_unexpected_option' );
        }
    }

So weit, so gut, doch das brachte mich zunächst ebenfalls nicht weiter. Bemerkenswert war jedoch der Hinweis, dass das Löschen dieser Zeilen auch dazu führt, dass die Fehlermeldung verschwindet. Wer hätte das auch nur vermutet..?

Ein wenig detaillierter zeigte sich ein weiterer Eintrag, doch die Frage wurde ebenfalls nur vage beantwortet. Was sollte “der Customizer kann/wird Änderungen von mehreren Optionen nicht nachverfolgen” genau bedeuten? Welche Optionen waren damit gemeint, und wie passte das zur eingangs erwähnten Meldung “widget_setting_too_many_options“? Mein Widget hatte 11 Einstellungen, waren das einfach zu viele? 

Ich untersuchte auch die Art des Speicherns bzw. welche Werte wie übermittelt wurden. In der Widget-Admin-UI wird beim Klick auf den “Speichern”-Button ein Ajax-Request ausgelöst, d.h. die Formulardaten werden übertragen. Dies sind zum einen die Einstellungen des Widgets selbst, zum anderen Daten über das Widget, etwa in welchem Bereich es platziert wurde, die interne Widget-ID usw.. Als Action wurde dabei “save-widget” übertragen – klingt soweit logisch. Doch genau dies ist im Widget-Bereich des Customizers völlig anders! Das Anlegen eines Widgets, aber auch das Ändern der Einstellungen führt ebenfalls zu einem Ajax-Request, es wird jedoch als Actionupdate-widget” übermittelt. Warum auch immer hier für eigentlich dieselbe Aktion zwei unterschiedliche Aktionen und damit auch Methoden im Backend aufgerufen werden, wissen wohl nur langjährige WordPress-Entwickler – aus meiner Sicht ist dies nicht völlig logisch, um dies mal vorsichtig zu formulieren.

Ein erster Verdacht…

Irgendwann – nach einigen Tests und noch mehr Ausprobieren hatte ich dann einen ersten Verdacht. Ein WordPress-Widget, das von der Klasse “WP_Widget” abgeleitet ist, besitzt eine Methode update(), die für die Bereinigung bzw. Prüfung der zu speichernden Werte zuständig ist. Im Beispiel der Entwicklerdokumentation von WordPress findet sich das Schema dieser Methode:

 /**
     * Sanitize widget form values as they are saved.
     *
     * @see WP_Widget::update()
     *
     * @param array $new_instance Values just sent to be saved.
     * @param array $old_instance Previously saved values from database.
     *
     * @return array Updated safe values to be saved.
     */
    public function update( $new_instance, $old_instance ) {
        $instance = array();
        $instance['title'] = ( !empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
 
        return $instance;
    }

Damit ist die update-Methode die Stelle, an der ein Eingreifen möglich ist, bevor die Widget-Einstellungen gespeichert werden. Und genau dort lag auch die Ursache des Problems.

…und dessen Bestätigung

Da das Widget Daten aus anderen Quellen darstellen soll, wollte ich diese Werte bereits während des Speicherns der Widget-Daten sowohl holen als auch cachen. D.h. ich hatte die Routine, die für den Request auf die externe URL, die Verarbeitung und anschließende Speicherung zuständig war, innerhalb der update-Methode aufgerufen.

Vom Schema her sah meine update()-Methode dem folgenden Beispiel ähnlich:

public function update( $new_instance, $old_instance ) 
{
    // This is just an example, not real-life code!

    $widgetId = $this->get_field_id('widget_id');

    $cacheId = $widgetId . 'some unique value';

    $workload = $this->getSomethingFromExternal( $new_instance['url']);
    
    update_option($cacheId, $workload); // <- problem!!!
    
    $new_instance['title'] = strip_tags( $new_instance['title'] );
    $new_instance['url'] = strip_tags( $new_instance['url'] );

// more options... 

    return $new_instance;

}

Das ist natürlich kein realer Code, sondern nur ein Beispiel, denn einzig relevant ist der Aufruf von “update_option()“.

Zum Thema Caching könnte man viel schreiben, und sicherlich werde ich auch noch weitere Cache-Verfahren unterstützen, aber zunächst lag die Möglichkeit nahe, die entsprechenden Daten analog zu anderen Optionen in der Tabelle “wp_options” zu speichern.

WordPress sorgt beim Request einer Seite letztlich dafür, dass die mit “autoload” markierten Zeilen innerhalb eines mitunter recht großen Arrays zur Verfügung stehen, d.h. die Datenbank wird nur einmal bemüht, auch wenn die entsprechende get_option-Funktion mehrfach genutzt wird.

WordPress stellt dafür die Funktionen update_option() bzw. add_option() zur Verfügung, mit der letztlich Schlüssel-Wert-Paare gespeichert werden können. In der Tabelle finden sich so etwa auch Einstellungen der Konfiguration des jeweils verwendeten Themes, Optionen anderer Plugins, aber auch so gut wie die gesamte WordPress-Konfiguration.

Die Fehlermeldung “widget_setting_too_many_options” ließ sich nun dahingehend interpretieren, dass während des Speicherns der Widget-Einstellungen (in die Tabelle wp_options) nicht weitere, davon unabhängige Werte (in die Tabelle wp_options) gesichert werden können. Warum WordPress dieser Beschränkung unterliegt, ist mir zwar nicht klar, schließlich könnte MySQL selbstverständlich mehrere Update-Requests verarbeiten, ohne damit durcheinander zu kommen. Eher vermute ich den Grund in der mitunter seltsam anmutenden Verarbeitung innerhalb von WordPress und des Cachings der Optionen. Denn kaum hatte ich das Caching bzw. Speichern deaktiviert, war auch die Bearbeitung des Widgets im Customizer erfolgreich. 

Zusammenfassung

Also in Kurzform: WordPress speichert die Widget-Einstellungen, verarbeitet die Daten in der update()-Methode. Währenddessen darf keine weitere Speicherung von “Options” erfolgen, also kein Aufruf von add_option() bzw. update_option(). Das gilt aber nur für im Customizer gespeicherten Widget-Einstellungen und somit bei der Update-Action, nicht jedoch für die Save-Action eines Widgets. Klar, oder?! 🙂

Ich gebe ja zu, dass mich diese kleine Odyssee WordPress wieder ein wenig näher gebracht hat, auch wenn ich in diesem speziellen Fall vielleicht lieber einen halben Meter Sicherheitsabstand gehalten hätte…

 

 

 

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Tags: