Jump to content

Implemented various HTTP methods


NotionCommotion

Recommended Posts

Hi,

Created the below script to deal with PUT and DELETE requests as well as different content types (i.e. json and xml).  I realize there are existing libraries out there that are certainly better than what I created, but I did so just as much for the learning opportunity.  Couple of questions.  Thanks

For GET and POST requests, is it best to use the PHP super globals $_GET and $_POST, or take a similar approach as I did for PUT and DELETE?

When converting a stream to a string, is one of the following approaches better than the other?

parse_str(file_get_contents($input), $contents);

or
 

if (!($stream = @fopen($input, 'r'))) {
    throw new ParserException('Unable to read request body', 500);
}
if (!is_resource($stream)) {
    throw new ParserException('Invalid stream', 500);
}
$str = '';
while (!feof($stream)) {
    $str .= fread($stream, 8192);
}
parse_str($str, $contents);

I've never dealt with xml documents before.  Any glaring issues with my implementation?

While JavaScript, it is also related to PHP.  My JavaScript obj2url() function converts an object to a url encoded string which can be parsed by PHP.  I couldn't find existing code and had create it from scratch which makes me think I shouldn't be doing this.  Any issues with it?

Any other comments?

<?php

class ParserException extends \Exception{}
class Parser
{
    private $request;

    public function getData(bool $force=false) {
        if($this->request && !$force) return $this->request;
        switch(strtoupper($_SERVER['REQUEST_METHOD'])){
            //Question.  Is using PHP super globals $_GET and $_POST better/faster/etc for GET and POST requests than returning stream?
            case 'GET': $request=$_GET;break;
            case 'POST': $request=$_POST;break;
            case 'PUT': $request=$this->getStream();break;
            case 'DELETE': $request=$this->getStream();break;
            default: throw new ParserException("Unsupported HTTP method $_SERVER[REQUEST_METHOD]'", 500);
        }
        $this->request=$request;
        return $request;
    }

    public function returnData($data, int $code=null) {
        if($code) http_response_code($code);
        $type=$this->getBestSupportedMimeType(["application/json", "application/xml"]);
        header('Content-Type: '.$type);
        switch($type){
            case "application/xml":
                $xml_data = new \SimpleXMLElement('<?xml version="1.0"?><data></data>');
                $this->array_to_xml($data,$xml_data);
                echo($xml_data->asXML());
                break;
            default:    //case 'application/json':;
                echo json_encode($data);
        }
        exit;
    }

    private function getStream() {
        $input = substr(PHP_SAPI, 0, 3) === 'cgi'?'php://stdin':'php://input';

        //Question.  Why not just use: parse_str(file_get_contents($input), $contents);

        if (!($stream = @fopen($input, 'r'))) {
            throw new ParserException('Unable to read request body', 500);
        }
        if (!is_resource($stream)) {
            throw new ParserException('Invalid stream', 500);
        }
        $str = '';
        while (!feof($stream)) {
            $str .= fread($stream, 8192);
        }
        parse_str($str, $contents);

        return $contents;
    }

    private function array_to_xml( $data, &$xml_data ) {
        foreach( $data as $key => $value ) {
            if( is_numeric($key) ){
                $key = 'item'.$key; //dealing with <0/>..<n/> issues
            }
            if( is_array($value) ) {
                $subnode = $xml_data->addChild($key);
                $this->array_to_xml($value, $subnode);
            } else {
                $xml_data->addChild("$key",htmlspecialchars("$value"));
            }
        }
    }

    private function getBestSupportedMimeType($mimeTypes = null) {
        // Values will be stored in this array
        $AcceptTypes = [];
        $accept = strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT']));
        $accept = explode(',', $accept);
        foreach ($accept as $a) {
             $q = 1;  // the default quality is 1.
            // check if there is a different quality
            if (strpos($a, ';q=')) {
                // divide "mime/type;q=X" into two parts: "mime/type" i "X"
                list($a, $q) = explode(';q=', $a);
            }
            // mime-type $a is accepted with the quality $q
            // WARNING: $q == 0 means, that mime-type isn’t supported!
            $AcceptTypes[$a] = $q;
        }
        arsort($AcceptTypes);

        // if no parameter was passed, just return parsed data
        if (!$mimeTypes) return $AcceptTypes;

        //If supported mime-type exists, return it, else return null
        $mimeTypes = array_map('strtolower', (array)$mimeTypes);
        foreach ($AcceptTypes as $mime => $q) {
            if ($q && in_array($mime, $mimeTypes)) return $mime;
        }
        return null;
    }
}

error_reporting(E_ALL);
ini_set('display_startup_errors', 1);
ini_set("log_errors", 1);
ini_set('display_errors', 1);

$parser=new Parser();

try{
    if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest') {
        $data=$parser->getData();
        //application manipulates data as appropriate
        $parser->returnData($data);
    }
    else {
        echo getHtml();
    }
}
catch(ParserException $e) {
    $parser->returnData(['error'=>$e->getMessage()], $e->getCode());
}

function getHtml(){
    return <<<EOT
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>API Tester</title>
    </head>
    <body>
        <div>
            <div id='output'></div>
        </div>
        <script>
SHOWN BELOW TO IMPROVE READABILITY
        </script>
    </body>
</html>
EOT;
}

Below JS was moved out of the PHP file to improve readabity.

function obj2url(params, key) {
    //Standalone replacement for jQuery's $.param()
    if(!key && Array.isArray(params)) {
        throw "obj2url cannot accept an array";
    }
    var parts=[];
    if(!key) key='';
    if(typeof params === 'object' && params != null) {
        if(Array.isArray(params)) {
            for (var i = 0; i < params.length; i++) {
                if(typeof params[i] === 'object' && params[i] != null) {
                    var subnode=obj2url(params[i], key===''?[]:key+'['+i+']');
                    parts.push(subnode);
                }
                else {
                    parts.push(encodeURIComponent((key===''?[]:key+'[]'))+'='+encodeURIComponent(params[i]));
                }
            }
        }
        else {
            for (var i in params) {
                if(typeof params[i] === 'object' && params[i] != null) {
                    var subnode=obj2url(params[i], key===''?i:key+'['+i+']');
                    parts.push(subnode);
                }
                else {
                    parts.push(encodeURIComponent((key===''?i:key+'['+i+']'))+'='+encodeURIComponent(params[i]));
                }
            }
        }
    }
    else {
        throw "obj2url cannot accept a string";
    }
    return parts.join('&');
}

function ajax(stack) {
    if(stack.length===0) return;
    var request=stack.shift();

    var data=obj2url(request.data);

    var url=request.method==='GET'
    ?request.url+'?'+data
    :request.url;

    var xhr = new XMLHttpRequest();
    xhr.open(request.method, url, true);

    if(request.method!=='GET') {
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    }
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

    if(request.responseType==='xml') {
        xhr.responseType = 'document';
        xhr.overrideMimeType('text/xml');
        xhr.setRequestHeader('Accept', 'application/xml');
    }
    else {
        xhr.responseType = request.responseType;
        xhr.setRequestHeader('Accept', 'application/json');
    }

    xhr.onreadystatechange = function() {
        if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
            var responseString = request.responseType==='xml'
            ?(new XMLSerializer()).serializeToString(xhr.responseXML): //or use xhr.response?
            JSON.stringify(xhr.response);
            var msg='<p><code>'+responseString+'</code> ('+[request.method, request.responseType].join(' ')+')</p>';
            document.getElementById("output").insertAdjacentHTML('beforeend', msg);
            ajax(stack)
        }
    }
    xhr.send(request.method==='GET'?null:data);
}

var data={a:123,b:456,c:[1,2,3],d:{x:1,y:'88',z:[{a:'a',b:'b',c:'c'},'two',{a:123,b:'456','five':[{a:123}],'8':{x:[1,2,3]},c:[1,2,3],d:{x:1,y:'99',z:[3,2,1]}}]}};
var url=window.location.href;

ajax([
    {method:'GET', responseType:'json', url:url, data:data},
    {method:'POST', responseType:'json', url:url, data:data},
    {method:'PUT', responseType:'json', url:url, data:data},
    {method:'DELETE', responseType:'json', url:url, data:data},
    {method:'GET', responseType:'xml', url:url, data:data},
    {method:'POST', responseType:'xml', url:url, data:data},
    {method:'PUT', responseType:'xml', url:url, data:data},
    {method:'DELETE', responseType:'xml', url:url, data:data},
]);

 

Link to comment
Share on other sites

GET requests don't have bodies so there's nothing to read there.

POST requests do, of course, have bodies, but IIRC PHP will read the body for you when it recognizes the content-type (application/x-www-form-urlencoded and multipart/form-data) - obviously it will be more efficient to rely on PHP's data if it's there. Depending on your PHP version and SAPI, you might be able to re-read the body, but having that dependency is annoying. More detailed explanation available upon request.
So I would say you should detect the content-type: "application/x-www-form-urlencoded" means all the data is already in $_POST, "multipart/form-data" means all the data is in $_POST but there may be $_FILES too, and anything else (eg, application/json) is up to you to read yourself. Note that the content-type has the structure "mime type[; parameters...]" so the content-type you need is everything before a semicolon and trimmed in case of whitespace.

Last I knew PHP does not do anything at all with PUT bodies.

Link to comment
Share on other sites

Thanks requinix,

While GET requests don't have a body, I suspect that my get getData() method is not complete as it does not include any URL data for non-GET requests.  Agree?  Assuming so, if both the URL as well as the body defines a parameter, what should take precedent?

I expected it would be more efficient to rely on PHP's low-level implementation for POST, and I guess I don't need more detailed explanation unless you feel you should give.

I don't understand you point regarding "and anything else (eg, application/json) is up to you to read yourself."  Please elaborate.

Yes, PHP doesn't do anything with PUT requests or as far as I know DELETE requests, but there is no reason it can't be implemented as I am attempting to do, no?
 

Link to comment
Share on other sites

26 minutes ago, NotionCommotion said:

While GET requests don't have a body, I suspect that my get getData() method is not complete as it does not include any URL data for non-GET requests.  Agree?  Assuming so, if both the URL as well as the body defines a parameter, what should take precedent?

Keep them separate. Not just to avoid the problem of what to do with duplicates, but because stuff in the query string has a different meaning than what's in a request body.

26 minutes ago, NotionCommotion said:

I don't understand you point regarding "and anything else (eg, application/json) is up to you to read yourself."  Please elaborate.

If the client submits URL-encoded or multipart data, PHP will read it for you in to $_POST and/or $_FILES. But with any other type of data it won't do anything. It's entirely up to you to read the request body and deal with it accordingly.

For example, if the client sends JSON (Content-Type: application/json) then you could file_get_contents(php://input) to get the request body then json_decode it. The data would be whatever the result of that is, be it an object or array or string or whatever.

26 minutes ago, NotionCommotion said:

Yes, PHP doesn't do anything with PUT requests or as far as I know DELETE requests, but there is no reason it can't be implemented as I am attempting to do, no?

Right. In fact since PHP doesn't it means you have to.

Link to comment
Share on other sites

 

9 hours ago, requinix said:

Keep them separate. Not just to avoid the problem of what to do with duplicates, but because stuff in the query string has a different meaning than what's in a request body.

Yes, makes sense.  However, this means my whole approach with getData() and the switch(REQUEST_METHOD){case 'GET': case 'POST': case 'PUT':case 'DELETE':} logic is wrong, right?  Instead I should have a getBody() method which doesn't have a switch statement and maybe a getMostlyUsedData() method.

9 hours ago, requinix said:

If the client submits URL-encoded or multipart data, PHP will read it for you in to $_POST and/or $_FILES. But with any other type of data it won't do anything. It's entirely up to you to read the request body and deal with it accordingly.

For example, if the client sends JSON (Content-Type: application/json) then you could file_get_contents(php://input) to get the request body then json_decode it. The data would be whatever the result of that is, be it an object or array or string or whatever.

Ah, an eye opener!  Just hit home that my ajax requests are passing application/x-www-form-urlencoded data and not application/json data.  Should I not be doing so and instead pass a json string and content type application/json?  This would make my obj2url JavaScript method obsolete and PHP wouldn't magically convert it to an array and assign it to the GET/POST supers, and instead I would need to json_decode() it server side. While this kind of makes sense for requests with a body, any data passed in the url would still need to be handled the original way, so I don't know...

PUT /api/tickets.json/777577 HTTP/1.1
...
Accept: application/json, text/javascript, */*; q=0.01
...
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
...
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
...

 

Link to comment
Share on other sites

4 hours ago, NotionCommotion said:

Yes, makes sense.  However, this means my whole approach with getData() and the switch(REQUEST_METHOD){case 'GET': case 'POST': case 'PUT':case 'DELETE':} logic is wrong, right?  Instead I should have a getBody() method which doesn't have a switch statement and maybe a getMostlyUsedData() method.

Yup. Remember that any request can have a query string, not just GET. So there needs to be a way to get it from POST, PUT, and DELETE requests in addition to their request bodies.

4 hours ago, NotionCommotion said:

Ah, an eye opener!  Just hit home that my ajax requests are passing application/x-www-form-urlencoded data and not application/json data.  Should I not be doing so and instead pass a json string and content type application/json?

Your choice. A regular request is perfectly fine - JSON is more for an API.

4 hours ago, NotionCommotion said:

This would make my obj2url JavaScript method obsolete

Not if you want to send query string parameters.

4 hours ago, NotionCommotion said:

and PHP wouldn't magically convert it to an array and assign it to the GET/POST supers, and instead I would need to json_decode() it server side.

$_POST is strictly for the request body of POST requests when submitted with one of those two MIME types PHP recognizes.

4 hours ago, NotionCommotion said:

While this kind of makes sense for requests with a body, any data passed in the url would still need to be handled the original way,

Right.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.