WordPress Plugin Entwicklung: Anbindung an APIs

Einführung

Bei manchen WordPress Plugins ergibt es mehr Sinn, die Kernfunktionalität nicht selbst zu implementieren, sondern einen externen Service dafür zu benutzen. Beispiele dafür sind daten- oder rechenintensive Aufgaben.

Für unser WordPress Suche Plugin CurrySearch haben wir uns deshalb entschieden die Kernfunktionalität, die Suche, als externen Service in Rust zu entwickeln.

Dieser Post zeigt unsere Ergebnisse auf diesem Weg auf und erklärt übliche Probleme und deren Lösungen.

Grundsätzliche Empfehlungen

Wrapper entwickeln

Um eine API zu benutzen braucht es eine URL die sich im Regelfall aus folgenden Komponenten zusammensetzt:

Aber anstatt diese on-the-fly zu kombinieren sollten besser Konstanten für die einzelnen Komponenten benutzt werden. Außerdem ist es empfehlenswert Funktionen zu definieren, die die Parameter des API-Calls akzeptieren und die Antwort der API decoded zurückgeben.

Hier ein abgespecktes Beispiel:

<?php
class API {
    const BASE_URL = "https://api.example.com/";
    const GET_DATA_METHOD = "get/data/";
    const POST_DATA_METHOD = "post/data/";

    static function get_data($id) {
        $url = BASE_URL.GET_DATA_METHOD.urlencode($id);

        // Do the actual and return the decoded result
    }

    static function post_data($data) {
        $url = BASE_URL.POST_DATA_METHOD;

        // Do the actual call and return the decoded result
    }
}
?>

Nutzerinput überprüfen

Im oberen Beispiel wurde die ID erst durch urlencode geschickt, bevor sie an die URL gehängt wurde, um sicherzustellen, dass keine illegalen Zeichen in die URL und damit zur API gelangen.

Grundsätzlich ist WordPress ein raue Gegend und es ist sinnvoll niemandem und nichts zu vertrauen.

Außerdem ist es wichtig Eingaben auf Validität zu prüfen (wenn es eine Zahl sein sollte, stell sicher, dass es eine ist).

API überprüfen

Einfach blind irgendeine API aus dem Internet zu nutzen kann in einen fehlerhaften Plugin enden oder sogar die Bedingungen dieser verletzen. Es ist also wichtig die API und ihre Betreiber vorher zu überprüfen.

Wichtige Fragen dabei sind:

Oft sind APIs auch limitiert in den Anzahl der Anfragen pro Tag oder erfordern Authentifizierung durch ApiKeys. Es ist wichtig solche Begrenzungen zu kennen und zu verstehen.

Die WordPress HTTP API

Aus dem Beispiel von oben sollen jetzt die Details mithilfe der WordPress HTTP Api implementiert werden. Ihre Benutzung wird im offiziellen WordPress Developer Handbuch empfohlen und es gibt gute Gründe dafür.

Die wichtigsten Gründe sind, dass diese API auf jedenfall in jeder WordPress Installation unterstütz wird und dass viel Arbeit abgenommen wird.

Auf gehts:

<?php
class API {
    const BASE_URL = "https://api.example.com/";
    const GET_DATA_METHOD = "get/data/";
    const POST_DATA_METHOD = "post/data/";

    static function get_data($id) {
        $url = BASE_URL.GET_DATA_METHOD.urlencode($id);
        $response = wp_remote_get($url)

        // Decode the response into an associative array and return it
        return json_decode(wp_remote_retrieve_body($response), true);
    }

    static function post_data($data) {
        $url = BASE_URL.POST_DATA_METHOD;

        // Set the content type to application/json and add a body
        $response = wp_remote_post($url, array(
            'headers' => array(
                'Content-Type' => 'application/json'
            ),
            'body' => json_encode($data)));

        return json_decode(wp_remote_retrieve_body($response), true);
    }
}
?>

Diese Herangehensweise sollte für die meisten Fälle ausreichen.

PHP und HTTPS Verbindungen

Auch wenn die WordPress HTTP API ein bisschen langsamer als curl ist, liegt der Hauptvorteil von curl darin, dass HTTP Verbindungen gehalten werden können.

Eine kleine Ausführung: Moderne APIs nutzen normalerweise das HTTPS Protokoll. HTTPS hat im Regelfall keinen Einfluss auf die Performance von Browsern oder Webservern. Aber im Falle von WordPress Plugins kann es größere Performanceeinbußen geben. Warum?

Der Aufbau von HTTPS Verbindungen ist relativ teuer. Um eine HTTPS Verbindung zu öffnen braucht es im Regelfall 3-4mal der normalen Latenz. Fast alle Browser und Webserver können eine solche Verbindung aber offenhalten.

Dieser Preis muss also nur einmal bezahlt werden und amortisiert sich über nachfolgende Zugriffe, welche nichtmehr unter der hohen Latenz leiden.

Die Architektur von PHP auf der anderen Hand, erlaubt solche langlebigen HTTPS Verbindungen nicht: Sie leben maximal einen Abfrage auf den WordPress-Server lang.

Das verwandte Problem der Datenbankverbindungen wurde intern gelöst. Leider gibt es für HTTPS-Verbindungen keine einfache Lösung.

Jede HTTPS Verbindung zu einer API kostet also wertvolle Zeit.

Performanceverbesserungen mit curl

Wie im letzten Abschnitt aufgezeigt ist es unmöglich Verbindungen über mehrere Clientanfragen hinweg offenzuhalten. Es ist allerdings möglich nur eine einzelne Verbindung über eine Clientanfrage hinweg zu nutzen.

Leider unterstützt die WordPress HTTP API das nicht und es müssen gröbere Werkzeuge ausgepackt werden: curl.

Vor der Entscheidung curl zu benutzen sollten allerdings die Nachteile verstanden sein:

Wenn es kein Performanceproblem gibt, sollte curl nicht benutzt werden!


Trotz all diesen Warnungen ist hier der Code, der curl benutzt um eine Verbindung mehrfach zu verwenden:

<?php
class API {
    static $curl_handle = NULL;


    const BASE_URL = "https://api.example.com/";
    const GET_DATA_METHOD = "get/data/";
    const POST_DATA_METHOD = "post/data/";

    static function get_data($id) {
        $url = BASE_URL.GET_DATA_METHOD.urlencode($id);

        if (curl_installed() {
            //curl is installed and we can use it

            //Initialize the curl handle if it is not initialized yet
            if (!isset($curl_handle)) {
			    $curl_handle = curl_init();
			}

            // Copy it and fill it with your parameters
            $ch = curl_copy_handle($curl_handle);
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");


            // Enable keep alive
            $header = array(
				'Connection: keep-alive',
				'Keep-Alive: 300'
			);

            // Set the user agent which tells you the wordpress and php version and that curl is used
            // This will help you when debugging problems
            curl_setopt($ch, CURLOPT_USERAGENT, 'Curl/WordPress/'.$wp_version
                       .'/PHP'.phpversion().'; ' . home_url());

            // Do not echo result
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            // Set header
			curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            // Set HTTP version to 1.1 to allow keepalive connections
			curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);

            $response = curl_exec($ch);
			$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
			if ($http_status != 200) {
                //TODO: Handle error
			}
            curl_clone($ch);
            return json_decode($response, true);
        } else {
            //Curl is not installed. fallback to WP HTTP API

            $response = wp_remote_get($url)

            // Decode the response into an associative array and return it
            return json_decode(wp_remote_retrieve_body($response), true);
        }
    }

    static function post_data($data) {
        $url = BASE_URL.POST_DATA_METHOD;

        if (curl_installed() {
            //curl is installed and we can use it

            //Initialize the curl handle if it is not initialized yet
            if (!isset($curl_handle)) {
			    $curl_handle = curl_init();
			}

            // Copy it and fill it with your parameters
            $ch = curl_copy_handle($curl_handle);
            curl_setopt($ch, CURLOPT_URL, $url);

            // Encode payload and set post body
            $data_string = json_encode($data);
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
		   	curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
	   		array_push($header, 'Content-Type: application/json');
   			array_push($header, 'Content-Length: ' . strlen($data_string));

            // Enable keep alive
            $header = array(
				'Connection: keep-alive',
				'Keep-Alive: 300'
			);

            // Set the user agent which tells you the wordpress and php version and that curl is used
            // This will help you when debugging problems
            curl_setopt($ch, CURLOPT_USERAGENT, 'Curl/WordPress/'.$wp_version.'/PHP'
                       .phpversion().'; ' . home_url());

            // Do not echo result
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            // Set header
			curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            // Set HTTP version to 1.1 to allow keepalive connections
			curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);

            $response = curl_exec($ch);
			$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
			if ($http_status != 200) {
                //TODO: Handle error
			}
            curl_clone($ch);
            return json_decode($response, true);
        } else {
            //Curl is not installed. fallback to WP HTTP API


            // Set the content type to application/json and add a body
            $response = wp_remote_post($url, array(
                'headers' => array(
                    'Content-Type' => 'application/json'
                 ),
                 'body' => json_encode($data)));

            return json_decode(wp_remote_retrieve_body($response), true);
        }
    }

    private static function curl_installed(){
		return function_exists('curl_version');
	}
}
?>

Das ist viel mehr und komplizierterer Code als in unserem ersten Beispiel. Nichtsdestotrotz ist es jetzt möglich die API während einer Abfrage mehrere Male über eine einzige Verbindung zu benutzen.

Der Unterschied wird nicht nur messbar sondern für Nutzer des Plugins auch spürbar sein.

Weiter Verbesserungen mit Umweg über den Browser

Obwohl die Performance schon mittels curl verbessert werden konnte, ist das in machen Fällen noch nicht ausreichend. Eine Autocomplete-API zum Beispiel, die die Eingaben der Nutzer sofort verarbeiten und beantworten soll.

Mit WordPress wird das nicht gelingen: Die WordPress Ajax abfragen sind langsam und selbst wenn man diese beschleunigt indem man die Zeit der Initialisierung verkürzt, wird das höchstwahrscheinlich nicht ausreichen um ein befriedigendes Ergebniss für Nutzer zu erzielen.

Es ist jedoch möglich den Browser zu benutzen um mit der API zu kommunizieren. Allerdings bedeutet das auch, dass die IP-Addressen der Nutzer für die externe API sichtbar sind und eine Authentifizierung mittels ApiKey nicht mehr möglich ist.

Fazit

Jetzt ist es Ihnen möglich die WordPress HTTP API zu benutzen um Wrapper um APIs zu bauen und Sie kennen die Probleme im Zusammenhang mit PHP und HTTPS Verbindungen. In Außnahmefällen können Sie auf die curl-Lösung zurückgreifen.

Wenn Sie unzufrieden mit der Standard WordPress Suche sind, probieren Sie doch CurrySearch!