How to represent a Query String in PHP

PHP URI Query
As all web developers know, the Query String represents a set of key-values in the URI which is generally used to include optional parameters for a HTTP request.

To be precise, the query string is simply a string, in the following format:

?key=value&other_key=other_value

In fact, keys are required, but values are optional.

?key=value&some_key&some_other_key

These value-less keys could be interpreted by the server as flag keys, or true keys, but it may also represent that the value is an empty string.

Officially, you may include duplicate keys in the query string, e.g. ?key=hello&key=goodbye
However, I strongly recommend that you do not build your web applications to use duplicate query string keys!

A better way to think about the query string is as a dictionary of string keys and string values.

In this way, from the server's perspective, the query string, if it were an object, would implement IReadOnlyStringDictionary (see my other post about this interface!).

The Query String in PHP



In PHP, the query string may be accessed through $_GET, $_SERVER['QUERY_STRING'] or $_SERVER['REQUEST_URI'].

$_GET['key'] - the superglobal $_GET array provides easy access to the query string, but there are a few quirky things out of the box which give unexpected results.

The $_GET paradox



$_GET is a real pain in the butt. Out of the box, PHP's $_GET superglobal array actually functions as a multidimensional array with basically unlimited dimensions (array depths) which causes big problems if a user passes in a malicious query string with a series of [] (square brackets) or [0], [1], [2], etc. or even [index], [other_index] indexes after the keys. This creates a real can of worms!

For example:
?key[1]=some&key[2]=other&key[][stuff]=pain&key[][]=things&key[4][what]=happened&key[3][][multidimensional][][]=test
...produces the multidimensional array as follows:

array(1) {
["key"]=>
array(4) {
[1]=>
string(4) "some"
[2]=>
string(5) "other"
[3]=>
array(2) {
["stuff"]=>
string(4) "pain"
[0]=>
array(1) {
["multidimensional"]=>
array(1) {
[0]=>
array(1) {
[0]=>
string(4) "test"
}
}
}
}
[4]=>
array(2) {
[0]=>
string(6) "things"
["what"]=>
string(8) "happened"
}
}
}


On your marks... get set... terrible. Don't use $_GET, period. Not if you want to keep your sanity.

Alternatives to $_GET



$_SERVER['QUERY_STRING'] - provides the actual query string, minus the preceding "?" (question mark).

$_SERVER['REQUEST_URI'] - provides the full request URI after the host, which may or may not include the query string.

A word of warning in PHP



Data in $_GET is url decoded by default. But both $_SERVER['QUERY_STRING'] and $_SERVER['REQUEST_URI'] are both url encoded - that is to say, they display the original query string as sent by the browser to the server.

Other languages



JavaScript - the query string is accessed in the variable window.location.search;
See: developer.mozilla.org

.NET Core - the namespace Microsoft.AspNetCore.Http includes the HttpContext class, which provides a .Request property (HttpRequest class) that has both .Query and .QueryString properties for easy access to the query string.

Source code for a QueryString in PHP



PHP does provide a http_build_query() method, but honestly speaking I don't use it. It allows for multi-dimensional arrays, which in my opinion, do not belong in the query string.

Instead, I've developed my own QueryString class as follows:


<?php

namespace ACA\Text\URI
{
use ACA\Collections\IReadOnlyStringDictionary;

/**
* Query string in a URI
* @author Antony Charles Allen
* @since 27th July 2020
*/
class QueryString implements IReadOnlyStringDictionary
{
/**
* Query string key values
* @var string[]
*/
private array $array = array();


public function IsEmpty() : bool
{
return count($this->array) === 0;
}

public function KeySort() : bool
{
return ksort($this->array);
}

function offsetGet($offset) : string
{
$offset = (string)$offset;

$value = '';

if ($this->TryGetValue($offset, $value))
{
return $value;
}
else throw new \InvalidArgumentException("Offset [$offset] not found in ".self::class);
}


public function __construct(string $query)
{
if (preg_match('/^\?([^#]*)$/', $query, $matches))
{
$query = preg_replace('/^\?[&\?]+/', '?', $query);

do {
$query = str_replace('&&', '&', $query, $count);
}
while ($count > 0);

$query = substr($query, 1);

$query = urldecode($query);

$pairs = explode('&', $query);

foreach ($pairs as $pair)
{
if (strlen($pair) === 0) continue;

if ($pair === '=') continue;

if (preg_match('/^([^=]+?)(=(.*))?$/', $pair, $matches))
{
$key = $matches[1];
$value = '';

if (array_key_exists(3, $matches) && strlen($matches[3]) > 0)
{
$value = $matches[3];
}

$this->Add($key, $value);
}
else throw new \InvalidArgumentException("Unable to process pair [$pair] in query string!");
}
}
else throw new \InvalidArgumentException("String [$query] is not a valid query string!");
}

function ContainsKey(string $key) : bool
{
return array_key_exists($key, $this->array);
}

function offsetExists($offset) : bool
{
return $this->ContainsKey($offset);
}

public function ValueOf(string $key) : string
{
if (!$this->ContainsKey($key)) throw new \InvalidArgumentException("The key [$key] was not found in the query string!");

return (string)$this->array[$key];
}

public function RemoveKey(string $key) : bool
{
if (!$this->ContainsKey($key)) return false;

unset($this->array[$key]);

return true;
}


public function Add(string $key, string $value = '') : void
{
if ($key !== urldecode($key)) throw new \Exception("The key [$key] appears to be URL encoded!");

if (!is_null($value) && $value !== urldecode($value)) throw new \Exception("The value [$value] appears to be URL encoded!");

$this->array[$key] = $value;
}


public function __toString() : string
{
$str = '?';

foreach ($this->array as $k => $v)
{
if (strlen($str) > 1) $str .= '&';

$str .= urlencode($k);

if (!is_null($v) && strlen($v) > 0)
{
$str .= '='.urlencode($v);
}
}

return $str;
}


function Keys() : array
{
return array_keys($this->array);
}

function Values() : array
{
return array_values($this->array);
}

function TryGetValue(string $key, string & $value) : bool
{
if (!$this->ContainsKey($key)) return false;

$value = $this->ValueOf($key);

return true;
}
}
}


As always, please feel free to use and abuse this code, have fun with it and spread the word, and please, don't forget to support me! 😊
Hey you! I need your help!

Thanks for reading! All the content on this site is free, but I need your help to spread the word. Please support me by:

  1. Sharing my page on Facebook
  2. Tweeting my page on Twitter
  3. Posting my page on LinkedIn
  4. Bookmarking this site and returning in the near future
Thank you!