Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Een POST request maken zonder CURL

Soms moet je vanuit een PHP script communiceren met een ander site. Denk aan een SMS gateway of een payment gateway. Bij dergelijke webservices is het meestal de bedoeling dat je via HTTP POST een stukje XML die kant op stuurt.

Wanneer je vanuit je PHP script een HTTP POST request moet maken wordt er al snel geroepen dat je de CURL library moet gebruiken. Nu is er niets mis met die library, maar sommige hosters hebben die nu eenmaal niet meegenomen in hun configuratie. In dit artikeltje zie je hoe je zonder CURL of andere externe libraries een HTTP POST request kunt maken.

Let op: Zoals altijd op Scriptorama laten we de benodigde fout afhandeling achterwege zodat we het concept dat we proberen uit te leggen met zo min mogelijk code kunnen demonstreren.

Hoe ziet een HTTP POST request eruit?

Het loont om eerst eens even te kijken hoe zo'n POST request nu opgebouwd moet worden. Het request ziet er als volgt uit:

CODE:
  1. POST /doel.php HTTP/1.1
  2. Host: www.example.org
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 7
  5.  
  6. a=b&c=d

In de eerste regel vertellen we de webserver dat we graag een POST request willen uitvoeren op /doel.php. In de 3e regel vertellen we in wat voor formaat we gegevens gaan mee sturen, de 4e regel zegt hoelang die gegevens zijn en de laatste regel zijn de daadwerkelijke gegevens, in hetzelfde formaat zoals je bij GET requests zou hebben.

Let's get that stream rolling

Sinds PHP 4.3.0 heeft PHP een uitgebreid streams systeem. Streams biedt verschillende features. Zo kun filters definieren, stukjes code die boven op een transport werken maar je kunt ook de specifieke transports, dat zijn de modules die toegang tot HTTP, HTTPS en bijv. FTP mogelijk maken, configureren zodat je er meer mee kunt dan alleen een simpel GET requestje versturen. Wij zullen ons in dit artikeltje richten op het laatste.

Om een transport te configureren creëer je een zogenaamde context en dat kun je doen met de functie stream_context_create. Deze functie verwacht een array van opties per transport. De mogelijke opties voor een bepaalde transport kun je vinden in de handleiding.

Wij zullen 3 opties gaan gebruiken voor het HTTP transport: method, content en header. Zo kunnen we alle benodigde gegevens meegeven:

  • method: geeft aan wat voor soort HTTP request we willen uitvoeren
  • header: geeft ons de mogelijkheid om de headers Content-Type en Content-Length mee te geven
  • content: gebruiken we om de POST variabelen in te plaatsen

Laten we stellen dat we de volgende variabelen willen mee sturen, die we voor de handigheid alvast even in een array hebben gehangen:

PHP:
  1. $variables = array (
  2.   'site'  => 'http://www.scriptorama.nl',
  3.   'desc' => 'Webdevelopment weblog'
  4. );

Het content type stelt bepaalde eisen aan ons, we zullen deze data moeten encoderen in een bepaald formaat. PHP biedt hier gelukkig een handige functie voor: rawurlencode() welke vrijwel alle niet alfanumerieke waarden omzet naar een hex-waarde voorgegaan door een %-teken.

We kunnen meteen even gebruik maken van de kennis die in het artikeltje Callbacks in PHP wordt beschreven om deze variabelen om te zetten in de juiste encoding en in een bruikbaar formaat:

PHP:
  1. function rawurlencode_callback($value, $key)
  2. {
  3.     return "$key=" . rawurlencode($value);
  4. }
  5.  
  6. $variables = array (
  7.     'site' => 'http://www.scriptorama.nl/',
  8.     'desc' => 'Webdevelopment weblog'
  9. );
  10.  
  11.     array_map(
  12.         'rawurlencode_callback',
  13.         $variables,
  14.         array_keys($variables)
  15.     )
  16. );

Als we deze code uitvoeren zien we dat we van array_map() een geindexeerde array terug krijgen met daarin de geencodeerde waarden:

CODE:
  1. array(2) {
  2.   [0]=>
  3.   string(39) "site=http%3A%2F%2Fwww.scriptorama.nl%2F"
  4.   [1]=>
  5.   string(28) "desc=Webdevelopment%20weblog"
  6. }

Het mooie van deze array is dat we nu met een simpele aanroep naar Join() met '&' als delimiter deze array kunnen omzetten naar de benodigde string. Hierna is het bepalen van de benodigde waarde voor de Content-Length variabele ook erg simpel geworden. We halen de var_dump() aanroep weg en voegen wat definities toe:

PHP:
  1. $encodedVariables = array_map ( 'rawurlencode_callback', $variables, array_keys($variables) );
  2.  
  3. $postContent = join('&', $encodedVariables);
  4. $postContentLen = strlen($postContent);

In principe hebben we nu alle benodigde waarden en kunnen we dus een Stream Context gaan maken:

PHP:
  1. // HTTP Headers moeten gescheiden worden door \r\n, we gebruiken een constante voor betere leesbaarheid.
  2. define ('CRLF', "\r\n");
  3.  
  4. $streamCtx = stream_context_create (
  5.   array (
  6.     'http' => array (
  7.       'method' => 'POST', // dit MOET in hoofdletters
  8.       'content' => $postContent,
  9.       'header'  => "Content-Type: application/x-www-form-urlencoded" . CRLF . "Content-Length: $postContentLen" . CRLF
  10.     )
  11.   )
  12. );

Deze context kunnen we vervolgens aan de verschillende file-functies die PHP rijk is geven om zo een aangepaste aanroep naar een website te doen. In PHP5 kun je file_get_contents() gebruiken om dat pas in die versie context-support werd toegevoegd, maar aangezien veel mensen nog met PHP4 werken zullen we het nog even met fopen() moeten doen. Aan fopen() kun je als 4e argument de context meegeven. Het derde argument specificeert of fopen() ook in het include_path moet kijken. Dat is hier niet van toepassing en dus geven we FALSE mee:

PHP:
  1. $fp    = fopen('http://localhost/~mathieukooiman/Tests/doel.php', 'r', FALSE, $streamCtx);
  2. $data = '';
  3. while (!feof($fp))
  4. {
  5.     $data .= fread($fp, 1024);
  6. }
  7.  
  8. fclose($fp);
  9.  
  10. echo $data;

Het bestand doel.php kan bijvoorbeeld voor test doeleinden het volgende bevatten:

PHP:
  1. var_dump($_POST);

Dus, als we deze code uitvoeren tegen doel.php:

CODE:
  1. $ php bron.php
  2. array(2) {
  3.   ["site"]=>
  4.   string(25) "http://www.scriptorama.nl"
  5.   ["desc"]=>
  6.   string(21) "Webdevelopment weblog"
  7. }

En zo hebben we zonder curl en andere externe libraries een HTTP POST request kunnen uitvoeren!

De gehele code

Hier is nog de gehele code uit dit artikel in 1 blok:

PHP:
  1. function rawurlencode_callback($value, $key)
  2. {
  3.     return "$key=" . rawurlencode($value);
  4. }
  5.  
  6. $variables = array (
  7.     'site' => 'http://www.scriptorama.nl',
  8.     'desc' => 'Webdevelopment weblog'
  9. );
  10.  
  11. $encodedVariables = array_map ( 'rawurlencode_callback', $variables, array_keys($variables) );
  12.  
  13. $postContent = join('&', $encodedVariables);
  14. $postContentLen = strlen($postContent);
  15.  
  16. define ('CRLF', "\r\n");
  17.  
  18. $streamCtx = stream_context_create (
  19.   array (
  20.     'http' => array (
  21.       'method' => 'POST',
  22.       'content' => $postContent,
  23.       'header'  => "Content-Type: application/x-www-form-urlencoded" . CRLF . "Content-Length: $postContentLen" . CRLF
  24.     )
  25.   )
  26. );
  27.  
  28. $fp = fopen("http://localhost/~mathieukooiman/Tests/doel.php", 'r', FALSE, $streamCtx);
  29. $data = '';
  30.  
  31. while (!feof($fp))
  32. {
  33.     $data .= fread($fp, 1024);
  34. }
  35.  
  36. fclose($fp);
  37.  
  38. echo $data;

Conclusie

PHP biedt met streams een enorm flexibele API om gebruik te maken van filters en transports, bijvoorbeeld om HTTP POST requests mee uit te voeren naar een SMS of payment gateway!

Reageer ook!

Dit is een mooi voorbeeld voor het posten van een form naar een andere server met callback, maar wat als de server deze instelling bevatt:

allow_url_fopen = Off

Deze instelling wordt vaak gebruikt op shared webhostings, het gebruik van cURL is in mijn ogen een eenvoudigere manier...

[...] In "POST request maken zonder CURL" beschreef ik hoe je vanuit een PHP script een formulier kon posten zonder dat je daarvoor de CURL extensie voor nodig had. Daarbij had ik een ding nog niet besproken en dat is het feit dat zo'n website ook down of slecht bereikbaar kan zijn. [...]

waarom eigenlijk bij array_map een eigen functie gebruiken?
waarom niet gelijk
$encodedVariables = array_map('rawurlencode', $variables);
?

Omdat ik in die eigen functie de key en value samenvoeg naar key=value, dat is niet mogelijk met array_map('rawurlencode', $variables).

sorry, te snel eroverheen gelezen :D

Zeer duidelijke 'tutorial', ik wou dat alle voorbeelden zo duidelijk waren. Ik kijk ernaar uit nog meer voorbeeldscripts van je te lezen...

Klasse!!

Richard

Een iets geoptimaliseerdere versie, bevat enkele kleine wijzigingen.

'http://www.scriptorama.nl',
'desc' => 'Webdevelopment weblog'
);

foreach ($variables as $key => $value){
$new_variables[] = $key.'='.rawurlencode($value);
}

$postContent = join('&', $new_variables);
$postContentLen = strlen($postContent);

$streamCtx = stream_context_create(
array (
'http' => array (
'method' => 'POST', // dit MOET in hoofdletters
'content' => $postContent,
'header' => "Content-Type: application/x-www-form-urlencoded" . PHP_EOL . "Content-Length: $postContentLen" . PHP_EOL
)
)
);

$fp = fopen('http://localhost/doel.php', 'r', FALSE, $streamCtx);
$data = '';
while (!feof($fp)){
$data .= fread($fp, 1024);
}
fclose($fp);

echo $data;
?>

In plaats van mijn array_map(), dat meer een referentie was naar een eerder artikel dan echt nuttig, maar ook in plaats van jouw foreach kun je ook de functie http_build_query() gebruiken.

Ja leuk,
nu nog even met plaatje uploaden, cookie authorization en een secure (ssl) verbinding.

Want welk formulier wil je hier nu eigenlijk vullen?
Met een beetje gateway als paypal of blog als wordpress kom je er met dit scriptje niet.

Hoi Joeri,

Het doel van dit artikel is om jou, de lezer, uit te leggen dat naast de CURL extensie, PHP zélf alle benodigdheden bevat om een HTTP POST request naar een server te sturen. Uiteraard laat ik daarbij ook zien hoe je dat dan kunt bereiken.

Wanneer mensen vragen hoe ze een POST request moeten maken verwijzen mensen direct naar CURL. CURL is oke (maar check de HTTP module in PECL ook eens!), áls je het hebt. Echter, niet alle mensen hebben de luxe om een module als CURL toe te laten voegen aan hun PHP configuratie en hosters zijn -enigzins terecht- niet erg happig op upgrades van hun virtual hosts waar 500 klanten op draaien.

Uit dit artikel blijkt nog niet helemaal de echte kracht van de stream wrappers. In dit geval gebruik ik een voorgedefinieërde stream wrapper, maar je kunt ook je eigen stream wrappers definieren. In dat geval wordt het gebruik van PHP's eigen functie tov het gebruik van CURL toch interessant(er), want het is flexibeler.

Om even verder in te gaan op jouw comment, cookie authorization en SSL gebruiken is echt geen probleem met die 'scriptje'. Een cookie toevoegen is een kwestie van een regel toevoegen aan de headers en voor SSL gebruik je simpelweg de https wrapper ipv de http wrapper. Of het uploaden van een bestand via stream wrappers ook mogelijk is durf ik niet met 100% zekerheid te zeggen nog, maar ik verwacht dat het wel te doen is. Een kwestie van de juiste headers en content payload samenstellen. Toegegeven, niet heel erg gebruikersvriendelijk ;).

Met het juiste gebruik van SSL en met het versturen van de juiste gegevens zal ook PayPal het request gewoon accepteren. Uiteindelijk doet het PHP script gewoon een HTTP request, eventueel over SSL, en verstuurt de headers (bijv. cookies) die je wilt hebben.

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>