toscho.design

WordPress-Tutorial: Eigenes Widget schreiben

Widgets liegen in der WordPress-Architektur irgendwo zwischen Theme und Plugin: Die eigene Funktionalität und das Aussehen sind meistens eng an vorhandenen Code gebunden, doch können sie auch ganz eigenständig erzeugt werden.
Sie lassen sich in Sidebars einbetten; man kann sie aber auch »manuell« im Theme aufrufen mit der Funktion the_widget().

Die Widgets-API ist die einzige, die objektorientiertes Arbeiten erzwingt – deshalb eignet sie sich gut zum Lernen und gefahrlosen Ausprobieren, finde ich.
Themes und Plugins hingegen dürfen komplett prozedural geschrieben werden. Werden sie leider oft auch.

Ein Widget muß die Klasse WP_Widget erweitern. Ein Konstruktor wird für den Aufruf gebraucht und eine Funktion widget() für die Ausgabe. Um den Speicherfresser create_function() zu umgehen, packe ich noch eine Funktion register() dazu. Das sieht im einfachsten Falle so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
add_action(
    'widgets_init'
,   array ('Toscho_Mini_Widget', 'register' )
);
class Toscho_Mini_Widget extends WP_Widget
{
    function Toscho_Mini_Widget()
    {
        $this->WP_Widget(__CLASS__, __CLASS__);
    }

    function widget($args, $instance)
    {
        print 'Juhu, hat geklappt!';
    }

    function register()
    {
        register_widget(__CLASS__);
    }
}

Hier müffelt es noch schlimm nach PHP 4. Wenn der Konstruktor nicht wie die Klasse heißt, läuft der Speicher aus … das müssen wir vorerst wohl hinnehmen.

Das Beispiel oben kann im Backend aktiviert werden, wenn es im Theme eingebunden wird und wenn das Theme mindestens eine Sidebar registriert hat.

Es tut natürlich noch nicht viel.

Screenshot Eingabemaske

Im nächsten Schritt sehen wir uns mal ein Beispiel an, das wir tatsächlich einsetzen könnten: Wir wollen die letzten Posts auflisten, dabei aber bestimmte Kategorien ausschließen oder zulassen.

Da können wir uns bei der Klasse WP_Widget_Recent_Posts einigen Code abgucken. Die liegt WordPress schon bei in der default-widgets.php.

Wir brauchen zusätzlich noch eine Eingabemaske für die Kategorien, und wir müssen den Query erweitern. Die nötigen Argumente finden wir in der Dokumentation zu query_posts().

Die beiden letzten Funktionen stammen nicht aus der Elternklasse, sondern ich habe sie eingebaut, um den Rest des Codes lesbar zu halten.

Live läuft genau dieses Widget gerade auf der Website der Interessengemeinschaft Britisch Kurzhaar-Katzen links in der Sidebar; hier werden die Jungtiermeldungen ausgeklammert, weil sie schon sehr prominent auf der Startseite präsentiert werden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
<?php
add_action(
    'widgets_init'
,   array ('Restricted_Latest_Posts_Widget', 'register' )
);
/**
 * Restricted Recent Posts widget class.
 *
 * Restricts the list of recent posts to selected categories.
 * Derivate of the default widget.
 *
 * @todo Select post types
 * @todo Select taxonomy
 */
class Restricted_Latest_Posts_Widget extends WP_Widget
{
    // Adjust this to your needs.
    var
        $max_posts = 50
    ,   $wid_name  = 'widget_restricted_recent_posts';

    /**
     * Constructor.
     */
    function Restricted_Latest_Posts_Widget()
    {
        $widget_ops = array(
            'classname'   => $this->wid_name
        ,   'description' => 'Letzte Beiträge per Kategorie ausgewählt.
                                Mit Komma trennen.'
        );
        $this->WP_Widget(
            'restricted-recent-posts'
        ,   'Restricted Recent Posts'
        ,   $widget_ops
        );
        $this->alt_option_name = $this->wid_name;

        add_action( 'save_post',
            array(&$this, 'flush_widget_cache') );
        add_action( 'deleted_post',
            array(&$this, 'flush_widget_cache') );
        add_action( 'switch_theme',
            array(&$this, 'flush_widget_cache') );
    }

    /**
     * Called statically to register the widget.
     *
     * @return void.
     */
    function register()
    {
        register_widget(__CLASS__);
    }

    function widget($args, $instance)
    {
        $cache = wp_cache_get($this->wid_name, 'widget');

        if ( ! is_array($cache) )
        {
            $cache = array();
        }

        if ( isset ( $cache[ $args['widget_id'] ] ) )
        {
            echo $cache[ $args['widget_id'] ];
            return;
        }

        ob_start();
        extract($args);

        $title        = empty ( $instance['title'] )
                        ? 'Restricted Recent Posts'
                        : $instance['title'];
        $title        = apply_filters('widget_title', $title);

        $number       = $this->sanitize_number($instance['number']);

        $include_cats = trim($instance['include_cats']);
        $exclude_cats = trim($instance['exclude_cats']);

        $query_args   = array(
            'showposts'        => $number
        ,   'nopaging'         => 0
        ,   'post_status'      => 'publish'
        ,   'caller_get_posts' => 1
        );

        if ( ! empty ( $include_cats ) )
        {
            $query_args['category__in']
                = $this->catlist_to_array($include_cats);
        }
        elseif ( ! empty ( $exclude_cats ) )
        {
            $query_args['category__not_in']
                = $this->catlist_to_array($exclude_cats);
        }

        $r = new WP_Query($query_args);

        if ( $r->have_posts() )
        {
            print $before_widget
                . ( $title
                        ? $before_title . $title . $after_title : ''
                    )
                . '<ul>';

            while ( $r->have_posts() )
            {
                $r->the_post();

                print '<li><a href="' . get_permalink() . '">'
                    . get_the_title() . '</a></li>';
            }

            print "</ul>$after_widget";

            // Restore global post data stomped by the_post().
            wp_reset_query();
        }

        $cache[ $args['widget_id'] ] = ob_get_flush();

        wp_cache_add($this->wid_name, $cache, 'widget');
    }

    function update( $new_instance, $old_instance )
    {
        $instance                 = $old_instance;
        $instance['title']        = strip_tags($new_instance['title']);
        $instance['number']       =
                $this->sanitize_number($new_instance['number']);
        $instance['exclude_cats'] = $new_instance['exclude_cats'];
        $instance['include_cats'] = $new_instance['include_cats'];

        $this->flush_widget_cache();

        $alloptions = wp_cache_get( 'alloptions', 'options' );

        if ( isset ( $alloptions[$this->wid_name] ) )
        {
            delete_option($this->wid_name);
        }

        return $instance;
    }

    function flush_widget_cache()
    {
        wp_cache_delete($this->wid_name, 'widget');
    }

    function form( $instance )
    {
        $title = isset ( $instance['title'] )
                ? esc_attr($instance['title']) : '';

        if (   ! isset ( $instance['number'])
            || ! $number = (int) $instance['number']
        )
        {
            $number = 5;
        }

        $incats = isset ( $instance['include_cats'] )
            ? esc_attr($instance['include_cats']) : '';
        $excats = isset ( $instance['exclude_cats'] )
            ? esc_attr($instance['exclude_cats']) : '';
        ?>
        <p>
            <label for="<?php
                echo $this->get_field_id('title');
            ?>">
                <?php
                    _e('Title:');
                ?>
            </label>
            <input class="widefat"
                id="<?php
                    echo $this->get_field_id('title');
                ?>"
                name="<?php
                    echo $this->get_field_name('title');
                ?>"
                type="text"
                value="<?php
                    echo $title;
                ?>"
            />
        </p>

        <p>
            <label for="<?php
                echo $this->get_field_id('number');
            ?>">
                Artikelmenge
            </label>
            <input id="<?php
                echo $this->get_field_id('number');
            ?>"
                name="<?php
                    echo $this->get_field_name('number');
                ?>"
                type="text"
                value="<?php
                    echo $number;
                ?>"
                size="3" />
            <small>(maximal <?php
                print $this->max_posts;
            ?>)</small>
        </p>

        <p>
            <label for="<?php
                echo $this->get_field_id('include_cats');
            ?>">
                Auf Kategorien beschränken:
            </label>
            <input id="<?php
                echo $this->get_field_id('include_cats');
            ?>"
                name="<?php
                    echo $this->get_field_name('include_cats');
                ?>"
                type="text"
                value="<?php
                    echo $incats;
                ?>"
                size="10"
            />
            <br /><strong>oder</strong><br />
            <label for="<?php
                echo $this->get_field_id('exclude_cats');
            ?>">
                Kategorien ausschließen:
            </label>
            <input id="<?php
                echo $this->get_field_id('exclude_cats');
            ?>"
                name="<?php
                    echo $this->get_field_name('exclude_cats');
                ?>"
                type="text"
                value="<?php
                    echo $excats;
                ?>"
                size="10"
            />
        </p>
        <?php
    }

    /**
     * Guarantee a number between 1 and max_posts.
     *
     * @return int
     */
    function sanitize_number($nr)
    {
        $nr = (int) $nr;

        if ( $nr > $this->max_posts )
        {
            return $this->max_posts;
        }

        if ( 0 == $nr )
        {
            return 1;
        }

        return $nr;
    }

    /**
     * Used by widget() to fill the category parameter.
     *
     * @param string $catlist
     * @return array
     */
    function catlist_to_array($catlist)
    {
        $tmp_cats = explode(',', $catlist);
        $new_cats = array ();

        foreach ( $tmp_cats as $cat )
        {
            $new_cats[] = (int) trim($cat);
        }

        return $new_cats;
    }
}

Zwei mögliche Erweiterungen habe ich im Kopf angedeutet. Wer will es versuchen?

Eine sehr viel umfangreichere Erweiterung der WordPress-internen Widgets liefert Justin Tadlocks Plugin Widgets Reloaded. Sehr praktisch – und von dem sauberen Code kann man eine Menge lernen.

Siehe auch:

7 Kommentare

  1. Patrick am 04.06.2010 · 15:44

    Reicht es aus, die erforderlichen Kommentarzeilen am Anfang der Datei zu machen und die Datei in den Plugin-Ordner zu schieben, um ein Widget als Plugin zur Verfügung zu stellen?

  2. Thomas Scholz am 04.06.2010 · 15:52

    @Patrick: Ja, das genügt. Ich würde dann noch eine Funktion einbauen, die das Update verhindert, damit es nicht von einem gleichnamigen Plugin aus dem Repository überschrieben wird. Ein Beispiel für solch eine Funktion findest du im Plugin Backend Style Enhancements.

  3. Kai am 14.11.2010 · 15:11

    Klasse! Ich bin gerade dabei Dein Tutorial umzusetzen und es scheint zu klappen! Ich würde mir noch ein solches Tutorial wünschen für Widgets mit etwas ausgefalleneren Eigenschaften...

  4. Sascha am 16.05.2011 · 23:35

    Sagt mal liebe Kollegen, wie kann ich verhindern, dass ich bei einem Update jedesmal das ganze neu aufsetzen muss. Die Datei würde mir schon reichen, wo ich das einstellen muss - such mir grade "nen Wolf".

  5. Thomas Scholz am 16.05.2011 · 23:45

    @Sascha: Das ganze Was? Update wovon?

  6. Ronny am 25.07.2011 · 09:42

    In der Zwischenzeit gibt es gute Bücher zu dem Thema. Wer durch das Tutorial Geschmack bekommen hat, sollte mal bei Amazon stöbern. Vorallem im Englischen ist schon gute Literatur vorhanden. Es lohnt, es macht Spaß, wenn man es mal richtig verstanden hat.

  7. Robert Hartl am 08.02.2012 · 11:07

    Klasse Anleitung, vor allem der Hinweise mit den Codevorlagen bei den default Widgets hat mir geholfen!