Last active
December 22, 2015 23:49
-
-
Save DaveRandom/6549483 to your computer and use it in GitHub Desktop.
URI/query string classes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class QueryLevel implements \ArrayAccess, \Iterator, \Countable | |
{ | |
private $elements = []; | |
private $iterationValid; | |
private function encodeElement($name, $value, $nameFormat = '%s') | |
{ | |
$result = null; | |
if ($value !== null) { | |
$name = sprintf($nameFormat, $name); | |
if ($value instanceof QueryLevel) { | |
$result = []; | |
foreach ($value as $subName => $subValue) { | |
$result[] = $this->encodeElement($subName, $subValue, $name . '[%s]'); | |
} | |
$result = implode('&', $result); | |
} else { | |
if (is_bool($value)) { | |
$value = (int) $value; | |
} | |
$result = urlencode($name) . '=' . urlencode($value); | |
} | |
} | |
return $result; | |
} | |
public function __construct($elements = []) | |
{ | |
if (!is_array($elements) && !($elements instanceof QueryLevel)) { | |
if (is_object($elements)) { | |
$elements = get_object_vars($elements); | |
} else { | |
$elements = (array) $elements; | |
} | |
} | |
foreach ($elements as $name => $value) { | |
$this->__set($name, $value); | |
} | |
} | |
public function __get($name) | |
{ | |
if (!array_key_exists($name, $this->elements)) { | |
trigger_error('Undefined property: ' . __CLASS__ . '::$' . $name, E_USER_NOTICE); | |
return null; | |
} | |
return $this->elements[$name]; | |
} | |
public function __set($name, $value) | |
{ | |
if (is_scalar($value) || $value === null) { | |
$this->elements[$name] = $value; | |
} else { | |
$this->elements[$name] = new static($value); | |
} | |
} | |
public function __toString() | |
{ | |
$result = []; | |
foreach ($this->elements as $name => $value) { | |
if (!is_numeric($name) && null !== $encoded = $this->encodeElement($name, $value)) { | |
$result[] = $encoded; | |
} | |
} | |
return implode('&', $result); | |
} | |
/* ArrayAccess */ | |
public function offsetExists($name) | |
{ | |
return isset($this->elements[$name]); | |
} | |
public function offsetGet($name) | |
{ | |
return $this->__get($name); | |
} | |
public function offsetSet($name, $value) | |
{ | |
$this->__set($name, $value); | |
} | |
public function offsetUnset($name) | |
{ | |
unset($this->elements[$name]); | |
} | |
/* Iterator */ | |
public function current() | |
{ | |
return current($this->elements); | |
} | |
public function key() | |
{ | |
return key($this->elements); | |
} | |
public function next() | |
{ | |
next($this->elements); | |
} | |
public function rewind() | |
{ | |
reset($this->elements); | |
} | |
public function valid() | |
{ | |
return key($this->elements) !== null; | |
} | |
/* Countable */ | |
public function count() | |
{ | |
return count($this->elements); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class URI implements \ArrayAccess, \Iterator, \Countable, \JsonSerializable | |
{ | |
const SCHEME = 0x01; | |
const USER = 0x02; | |
const PASS = 0x04; | |
const HOST = 0x08; | |
const PORT = 0x10; | |
const PATH = 0x20; | |
const QUERY = 0x40; | |
const FRAGMENT = 0x80; | |
// The private props and magic methods are only implemented like this to give type validation, | |
// these are effectively public properties | |
private $scheme; | |
private $user; | |
private $pass; | |
private $host; | |
private $port; | |
private $path; | |
private $query; | |
private $fragment; | |
// obviously these two properties would be handled internally in a native impl | |
private static $constPropMap = [ | |
self::SCHEME => 'scheme', | |
self::USER => 'user', | |
self::PASS => 'pass', | |
self::HOST => 'host', | |
self::PORT => 'port', | |
self::PATH => 'path', | |
self::QUERY => 'query', | |
self::FRAGMENT => 'fragment', | |
]; | |
private $iterationPointer = self::SCHEME; | |
private function validateScheme($value) | |
{ | |
// in the generic URI syntax, only the format of the scheme is rigid | |
return (bool) preg_match('/^[a-z][a-z0-9+.\-]*$/i', $value); | |
} | |
public function __construct($uri) | |
{ | |
$parts = []; | |
$this->query = new QueryLevel; | |
if (((string) $uri) !== '' && false === $parts = parse_url($uri)) { | |
throw new \InvalidArgumentException('Invalid URI'); | |
} | |
foreach ($parts as $name => $value) { | |
$this->__set($name, urldecode($value)); | |
} | |
} | |
public function __get($name) | |
{ | |
if (!in_array($name, self::$constPropMap)) { | |
trigger_error('Undefined property: ' . __CLASS__ . '::$' . $name, E_USER_NOTICE); | |
return null; | |
} | |
return $this->$name; | |
} | |
public function __set($name, $value) | |
{ | |
if ($value === null) { | |
$this->$name = null; | |
} else if ($name === 'port') { | |
$this->port = (int) $value; | |
} else if ($name === 'query') { | |
parse_str($value, $query); | |
$this->query = new QueryLevel($query); | |
} else if (in_array($name, self::$constPropMap)) { | |
if ($name === 'scheme' && !$this->validateScheme($value)) { | |
throw new \InvalidArgumentException('Invalid URI scheme'); | |
} | |
$this->$name = (string) $value; | |
} else { | |
// because PHP allows expando properties on anything afaik :-( | |
$this->$name = $value; | |
} | |
} | |
public function __toString() | |
{ | |
$result = ''; | |
if (isset($this->scheme)) { | |
$result = $this->scheme . ':'; | |
} | |
if (isset($this->host)) { | |
$result .= '//'; | |
if (isset($this->user)) { | |
$result .= urlencode($this->user); | |
if (isset($this->pass)) { | |
$result .= ':' . urlencode($this->pass); | |
} | |
$result .= '@'; | |
} | |
$result .= urlencode($this->host); | |
if (isset($this->port)) { | |
$result .= ':' . $this->port; | |
} | |
} | |
if (isset($this->path)) { | |
$result .= implode('/', array_map('urlencode', preg_split('~/+~', $this->path))); | |
} | |
if (count($this->query)) { | |
$result .= '?' . $this->query; | |
} | |
if (isset($this->fragment)) { | |
$result .= '#' . urlencode($this->fragment); | |
} | |
return $result; | |
} | |
/* ArrayAccess */ | |
public function offsetExists($name) | |
{ | |
return isset($this->$name) || isset(self::$constPropMap[$name]); | |
} | |
public function offsetGet($name) | |
{ | |
if (isset(self::$constPropMap[$name])) { | |
return $this->__get(self::$constPropMap[$name]); | |
} else { | |
return $this->__get($name); | |
} | |
} | |
public function offsetSet($name, $value) | |
{ | |
if (isset(self::$constPropMap[$name])) { | |
$this->__set(self::$constPropMap[$name], $value); | |
} else { | |
$this->__set($name, $value); | |
} | |
} | |
public function offsetUnset($name) | |
{ | |
if (isset(self::$constPropMap[$name])) { | |
$this->__set(self::$constPropMap[$name], null); | |
} else if (in_array($name, self::$constPropMap)) { | |
$this->__set($name, null); | |
} else { | |
unset($this->$name); | |
} | |
} | |
/* Iterator */ | |
public function current() | |
{ | |
return $this->{self::$constPropMap[$this->iterationPointer]}; | |
} | |
public function key() | |
{ | |
return self::$constPropMap[$this->iterationPointer]; | |
} | |
public function next() | |
{ | |
$this->iterationPointer *= 2; | |
} | |
public function rewind() | |
{ | |
$this->iterationPointer = self::SCHEME; | |
} | |
public function valid() | |
{ | |
return $this->iterationPointer <= self::FRAGMENT; | |
} | |
/* Countable */ | |
public function count() | |
{ | |
$result = 0; | |
foreach (self::$constPropMap as $const => $name) { | |
if ($this->$name !== null) { | |
$result++; | |
} | |
} | |
return $result; | |
} | |
/* JsonSerializable */ | |
public function jsonSerialize() | |
{ | |
return $this->__toString(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment