riverside / php-express

:horse: PHP micro-framework inspired by Express.js
https://riverside.github.io/php-express/
MIT License
26 stars 10 forks source link

Populate request data payload for RESTful HTTP methods like PUT, PATCH, DELETE #10

Closed jameswilson closed 4 months ago

jameswilson commented 4 months ago

Problem statement

Support for PUT, PATCH, DELETE verbs exist (Eg \PhpExpress\Application::put() and \PhpExpress\Application::patch()) but are of limited use because there is no payload data populated into in the Request object.

Steps to reproduce

  1. Create an index.php with the following:
$app = new \PhpExpress\Application();

$testVerbs = function($req, $res) {
  echo "method:" . var_export($req->method, TRUE) . PHP_EOL;
  echo "content-type:" . var_export($req->get("content-type"), TRUE) . PHP_EOL;
  echo "params:" . var_export($req->params, TRUE) . PHP_EOL;
  echo "body:" . var_export($req->body, TRUE) . PHP_EOL;
  $res->end();
};

$app->get('contact/:id', $testVerbs);
$app->post('contact/:id', $testVerbs);
$app->put('contact/:id', $testVerbs);
$app->patch('contact/:id', $testVerbs);
  1. Start the server and make several requests with various HTTP request methods via curl.
curl -X GET -d argument=value -d argument2=value2 http://localhost:8000/contact/1
curl -X POST -d argument=value -d argument2=value2 http://localhost:8000/contact/1
curl -X PUT -d argument=value -d argument2=value2 http://localhost:8000/contact/1
curl -X PATCH -d argument=value -d argument2=value2 http://localhost:8000/contact/1
  1. The expected result in all cases should be:
content-type:'application/x-www-form-urlencoded'
params:array (
  'id' => '1',
)
body:array (
  'argument' => 'value',
  'argument2' => 'value2',
)
  1. But the actual result varies by HTTP Request Method.

    Only the POST request type matches the expected result above with a populated $req->body

    All others show an empty body:

content-type:'application/x-www-form-urlencoded'
params:array (
  'id' => '1',
)
body:array (
)

Proposed Resolution

Populate the $req->body with the payload from PHP's Standard Input stream via PHP's parse_str() when $_POST is empty. For simplicity, since the $req->body is just a pointer to $_POST superglobal, we can parse the data into $_POST directly which is guaranteed to be empty for any HTTP request method other than POST.

parse_str(file_get_contents('php://input'), $_POST);

Forms can be submitted with different encoding types in the Content-Type header. application/x-www-form-urlencoded is the default, but multipart/form-data could also be accepted. However handling files uploaded with multipart/form-data encoding via HTTP PUT is not supported in PHP <8.4, therefore the parsing has to be done in different ways.

Full and future-proof solution, for when php 8.4 drops in November 2024:

        // Make data payloads available in $this->body for other HTTP verbs.
        if (
            $this->method !== 'POST'
            && $this->is('application/x-www-form-urlencoded')
        ) {
            parse_str(file_get_contents('php://input'), $_POST);
        }

        // Add support for multipart/form-data parsing for other HTTP verbs in PHP >=8.4.
        if (
            $this->method !== 'POST'
            && $this->is('multipart/form-data')
            && function_exists('request_parse_body')
        ) {
            [$_POST, $_FILES] = call_user_func('request_parse_body');
        }
ricardogj08 commented 4 months ago

x2 same problem