Nevymýšlejte kolo

Posted 14. 06. 2015 / By Petr Soukup / Vývoj

Když jste chtěli před lety něco naprogramovat v PHP, tak to znamenalo nejdřív si napsat knihovnu pro práci s databází, se soubory a tak dále. Neexistovaly ještě (pořádné) frameworky ani jiné nástroje, takže většinu vývoje spolklo zdržování se s řešením problémů, které vlastně s výslednou aplikací nesouvisely.

Doba pokročila - máme frameworky, cloud, vychytané knihovny, balíčkovací systémy a vývoj díky tomu jde šíleně rychle. Pořád tu je ale překvapivě velká skupina vývojářů, která tohle ignoruje a stejný zpátečnický postup pak vnucuje i začátečníkům. A to je škoda.

rewheel

Jak se volá API v čistém PHP

Proč je důležité využívat správně nástroje vám ukážu na jednoduchém příkladu. Cílem bude zavolat API, odeslat data a získat odpověď v JSONu. Jak se to řeší čistě v PHP bez dalších nástrojů?

Levně a chutně

Nejrychlejší způsob je použít file_get_contents, u kterého byste podle názvu čekali úplně jinou funkci, ale zadání splní nejjednodušeji.

<?php
$responseData = json_decode(file_get_contents('https://www.simplia.cz/api/'));

Paráda! Jenže to řeší jenom GET a my potřebujeme data odeslat. Žádný problém, přepíšeme to tedy do cURL.

cURL poprvé

<?php
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://www.simplia.cz/api/');
curl_setopt($ch,CURLOPT_POST, true);
curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
$responseData = json_decode(curl_exec($ch));
curl_close($ch);

Trochu se to natáhlo, ale pořád bez problémů. Jenže co když potřebujeme poslat více rozměrné pole? V tom případě se nemůžeme spolehnout na cURL a musíme si ho zakódovat sami.

EDIT: Ručně encodovat pole nakonec není potřeba - ve všech rozumných verzích PHP už si vystačíte s http_build_query.

cURL vícerozměrné

<?php
function array2url($arrays, &$new = array(), $prefix = null) {
    foreach ($arrays as $key => $value) {
        $k = isset($prefix) ? $prefix . '[' . urlencode($key) . ']' : $key;
        if(is_array($value)) {
            array2url($value, $new, $k);
        } else {
            $new[$k] = urlencode($value);
        }
    }
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://www.simplia.cz/api/');
curl_setopt($ch,CURLOPT_POST, true);
array2url($data);
curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
$responseData = json_decode(curl_exec($ch));
curl_close($ch);

Odesílání bychom měli. Vůbec ale neřešíme stavový kód, který nám API vrátí. To musíme napravit.

cURL stavové

<?php
function array2url($arrays, &$new = array(), $prefix = null) {
    foreach ($arrays as $key => $value) {
        $k = isset($prefix) ? $prefix . '[' . urlencode($key) . ']' : $key;
        if(is_array($value)) {
            array2url($value, $new, $k);
        } else {
            $new[$k] = urlencode($value);
        }
    }
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://www.simplia.cz/api/');
curl_setopt($ch,CURLOPT_POST, true);
array2url($data);
curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
if($response === FALSE) {
    throw new IOException("API call failed: " . curl_error($ch));
}
$http_status = curl_getinfo($http, CURLINFO_HTTP_CODE);
if($http_status !== 200){
    throw new IOException("API call failed: HTTP " . $http_status);
}
$responseData = json_decode($response);
curl_close($ch);

Paráda! HTTP chyby bychom měli podchycené. Ale co když API vrátí nevalidní JSON? To budeme muset kontrolovat.

cURL validující

<?php
function array2url($arrays, &$new = array(), $prefix = null) {
    foreach ($arrays as $key => $value) {
        $k = isset($prefix) ? $prefix . '[' . urlencode($key) . ']' : $key;
        if(is_array($value)) {
            array2url($value, $new, $k);
        } else {
            $new[$k] = urlencode($value);
        }
    }
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://www.simplia.cz/api/');
curl_setopt($ch,CURLOPT_POST, true);
array2url($data);
curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
if($response === FALSE) {
    throw new IOException("API call failed: " . curl_error($ch));
}
$http_status = curl_getinfo($http, CURLINFO_HTTP_CODE);
if($http_status !== 200){
    throw new IOException("API call failed: HTTP " . $http_status);
}
$responseData = json_decode($response);
if($responseData === NULL && $response !== '' && strcasecmp($response, 'null')){
    throw new IOException('API call failed: invalid json (' . json_last_error() . ')');
}
curl_close($ch);

Chyby bychom snad měli vyřešené. Jenže co se stane, když bude API přesměrovávat? Nyní by to skončilo bezdůvodně chybou.

cURL přesměrovávací

cURL pro tyto případy má atribut CURLOPT_FOLLOWLOCATION , který přesně tento problém řeší. Bohužel to ale (zcela nepochopitelně) nelze použít, pokud máte aktivní open_basedir, což máte vždycky. Musíme si tudíž přesměrování naprogramovat sami. Musíme to výrazně přepsat, což začíná být trochu otravné, ale žádný problém!

<?php
function array2url($arrays, &$new = array(), $prefix = null) {
    foreach ($arrays as $key => $value) {
        $k = isset($prefix) ? $prefix . '[' . urlencode($key) . ']' : $key;
        if(is_array($value)) {
            array2url($value, $new, $k);
        } else {
            $new[$k] = urlencode($value);
        }
    }
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL, 'https://www.simplia.cz/api/');
curl_setopt($ch,CURLOPT_POST, true);
array2url($data);
curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
if($response === FALSE) {
    throw new IOException("API call failed: " . curl_error($ch));
}
$redirects = 0;
while ($redirects < 8) {
    if($redirects === 7) {
        throw new IOException("Too many redirects");
    }
    $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if($http_status == 301 || $http_status == 302) {
        list($header) = explode("\r\n\r\n", $response, 2);
        $matches = [];
        preg_match('/(Location:|URI:)(.*)/i', $header, $matches);
        $url = trim(array_pop($matches));
        $url_parsed = parse_url($url);
        if(isset($url_parsed)) {
            curl_setopt($ch, CURLOPT_URL, $url);
            $response = curl_exec($ch);
        }
        $redirects++;
    } else {
        break;
    }
}
if($response === FALSE) {
    throw new IOException("API call failed: " . curl_error($ch));
}
if($http_status !== 200){
    throw new IOException("API call failed: HTTP " . $http_status);
}
$responseData = json_decode($response);
if($responseData === NULL && $response !== '' && strcasecmp($response, 'null')){
    throw new IOException('API call failed: invalid json (' . json_last_error() . ')');
}
curl_close($ch);

Začíná se to komplikovat

Jednoduché zavolání API už má 56 řádků a to ještě tiše ignorujeme, že některé chyby v JSON neodchytíme nebo třeba, že jsme závislí na přítomnosti cURL. A to jsme ani nenakousli, že bychom chtěli třeba volat asynchronně...

Radši toho necháme a podíváme se, jak to lze vyřešit při použití správných nástrojů.

Voláme API s Guzzle

Guzzle je knihovna určená pro komunikaci s API. Už je takovým neoficiálním standardem a jsou na ní často postaveny knihovny konkrétních API - například AWS SDK. Jak by se stejný problém řešil pomocí Guzzle?

<?php
$client = new GuzzleHttp\Client();
$responseData = $client->post('https://www.simplia.cz/api/', ['body' => $data])->json();

To je celé. Chci zavolat API, tak to prostě udělám. Žádné drbání se s prkotinami. Nemusím vůbec řešit encodování dat, přesměrování nebo různé chyby - všechno už řeší Guzzle, tak se s tím přece nebudu zdržovat. Navíc stačí přidat dva řádky a můžu posílat na API více požadavků paralelně.

Univerzální nástroje

Samozřejmě tím ale nechci říct, že na všechno máte použít knihovnu. Jsou ale určité úlohy, které jsou používané pořád dokola a tak na ně už dávno existují poměrně standardní nástroje. Není proto potřeba pořád dokola řešit ten samý problém, který už milionkrát vyřešil někdo jiný.

Samozřejmě to neplatí jen u API, ale i jinde. Dnes už není žádný důvod psát třídu pro komunikaci s databází, když máme třeba dibi nebo ještě lépe rovnou Doctrine. Není důvod psát další šablonovací systém, když už máme třeba Twig nebo Latte. Není potřeba psát miliontou třídu pro logování, když máme Monolog. A takových případů by se našla spousta.

Máte tip na univerzální nástroj? Podělte se v komentářích!

Javascript - opačný extrém

Zdůrazňuji, že mluvím o univerzálních knihovnách pro základní věci. V javascriptu se totiž postupem času došlo k přesně opačnému problému. Typická otázka na StackOverflow vypadá nějak takto:

  • Otázka: Jak v javavascriptu získám prvních pět znaků řetězce?
  • Odpověď: Použij tenhle jQuery plugin! Má jenom 16kB!

Výhody nástrojů

  1. věnujete se skutečnému vývoji a ne řešení prkotin
  2. nemusíte řešit budoucí změny jazyky - vyřeší je aktualizace knihovny (například mezi php 5.5 a 5.6 se změnil upload souborů - s Guzzle vám to může být jedno)
  3. víc hlav víc ví - ve vlastním kódu budete vždycky ignorovat nějaký speciální případ nebo nebudete vědět o bezpečnostním riziku - knihovny to řeší

Jasné?

Osobně jsem tohle považoval za naprostou samozřejmost, ale některé vývojáře asi zkrátka baví ztrácet čas. Například v tomto případě není rozumný důvod patlat se s cURL a nepoužít (třeba) Guzzle.

Jediným důvodem by mohla být rychlost, protože knihovna vždycky bude pomalejší. Ten rozdíl je ale zcela zanedbatelný a hlavně není bezdůvodný. Když porovnáte první příklad v tomto článku s Guzzle, tak bude čisté PHP určitě rychlejší. Jenže také neřeší spoustu problémů, které by řešit mělo. Pokud tedy budete potřebovat posílat na API 10 000 požadavků za sekundu, napíšete si optimalizovaný čistý kód bez knihovny. Ve všech ostatních případech ale není důvod knihovnu nepoužít.

Tags: curl, guzzle, php


O blogu
Blog o provozování eshopů a technologickém zázemí.
Aktuálně řeším hlavně cloud, bezpečnost a optimalizaci rychlosti.

Rozjíždím službu pro propojení eshopů s dodavateli.