Skip to content

Commit

Permalink
[Feature] Provided the Query class to ease parsing of query strings
Browse files Browse the repository at this point in the history
  • Loading branch information
mnavarrocarter committed Jul 8, 2021
1 parent 1838b32 commit 1a5ae01
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 8 deletions.
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 113 additions & 1 deletion docs/00-intro.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,116 @@
# Introduction

Castor Uri provides the `Castor\Net\Uri` value object. This is an immutable object
with a simple api that represents an RFC 3986 compliant Uniform Resource Identifier.
with a simple api that represents an RFC 3986 compliant Uniform Resource Identifier.

It also provides a helper `Castor\Net\Uri\Query` object, useful for parsing a query
string and read/mutate its values. It is a perfect replacement for php `parse_str`
function.

## Parsing a URI

You can easily parse a URI calling the `Castor\Net\Uri::parse()` method and then
access the parts of the URI very easily.

```php
<?php

use Castor\Net\Uri;

$uri = Uri::parse('https://example.com');
echo $uri->getScheme(); // Prints "https"
echo $uri->getHost(); // Prints "example.com";
echo $uri->getPort(); // Prints "";
echo $uri->getDefaultPort(); // Prints "80";
echo $uri->getPath(); // Prints "";
echo $uri->getQuery(); // Prints "";
echo $uri->getFragment(); // Prints "";
echo $uri; // Prints "https://example.com";
```

By design, parts that are not present in the URI are returned as an empty string.
This is to ensure type consistency and better practices, as often more than
a `NULL` check is necessary to ensure correctness of a program.

### A note on invalid URIs

An invalid URI is any URI containing invalid characters and/or lacking a scheme. If you
pass an invalid URI to the `Castor\Net\Uri::parse` method, a `Castor\Net\InvalidUri` error
will be thrown explaining why the URI is invalid. You are advised to properly handle the
exception.

If you want to avoid the exception handling, or if you simply need to check whether a
string is a valid URI, you can use the `Castor\Net\Uri::isValid` static method.

## Mutating the Uri

The `Castor\Net\Uri` class provides a `with` api that can mutate every component
of the URI. This mutation returns a new reference, so the original reference is
preserved. Keep this in mind when working with this class. This, for example, changes
the path of the URI. Note how the original reference is preserved.

```php
<?php

use Castor\Net\Uri;

$uri = Uri::parse('https://example.com');
echo $uri->withPath('/')->toStr(); // Prints "https://example.com/login"
echo $uri->toStr(); // Prints "https://example.com"
```

## Parsing Query Parameters

The `Castor\Net\Uri::getQuery()` method returns a string with the query parameters,
without the preceding question mark (`?`) character. This string can be fed directly
to the `Castor\Net\Uri\Query::parse()` method to get a `Castor\Net\Uri\Query` object
that allows you to easily read and mutate a query string.

```php
<?php

use Castor\Net\Uri;

$uri = Uri::parse('https://example.com?foo=bar&bar=foo');
$query = Uri\Query::parse($uri->getQuery());
$query->get('foo');
$query->add('bar2', '');
$query->del('foo');
$query->put('bar', 'foo2');
```

You must note that, unlike the `Castor\Net\Uri` object, the `Castor\Net\Uri\Query` object
is mutable, because it's sole purpose of existence is to help build query strings. It
provides a good api to change it's state.

### A note on multiple parameters with the same name

One very important detail about this query parser is that it supports multiple parameters
with the same name. If you have worked with the PHP `$_GET` global, you'll know that it
contains an associative array. This means that if you get a query string `bar=foo&bar=bar`,
you'll lose the first `bar` value (`foo`) and your `$_GET` global will contain
only `['bar' => 'bar']` as an entry.

The `Castor\Net\Uri\Query::get()` method returns an array, so you can handle those cases
and don't lose information. So, for the `bar=foo&bar=bar` example above you'll get:

```php

use Castor\Net\Uri;

$query = Uri\Query::parse('bar=foo&bar=bar');
echo $query->get('bar')[0]; // Prints "foo"
echo $query->get('bar')[1]; // Prints "bar"
```

If you know in advance that you'll be processing only one query parameter, then you can
go straight into the first index of the parameter array and use the null coalesce operator
to handle defaults.

```php

use Castor\Net\Uri;

$query = Uri\Query::parse('bar=foo&bar=bar');
echo $query->get('page')[0] ?? null; // Prints "" because is null
```
18 changes: 17 additions & 1 deletion src/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function parse(string $uri): Uri
$parts['user'] ?? '',
$parts['pass'] ?? '',
$parts['host'] ?? '',
(string) ($parts['port'] ?? self::$defaultSchemePort[$scheme] ?? ''),
(string) ($parts['port'] ?? ''),
$parts['path'] ?? '',
$parts['query'] ?? '',
rawurlencode(rawurldecode($parts['fragment'] ?? '')),
Expand All @@ -117,6 +117,22 @@ public function withScheme(string $scheme): Uri
return $clone;
}

public function withUser(string $user): Uri
{
$clone = clone $this;
$clone->user = $user;

return $clone;
}

public function withPass(string $pass): Uri
{
$clone = clone $this;
$clone->pass = $pass;

return $clone;
}

public function withHost(string $host): Uri
{
$clone = clone $this;
Expand Down
167 changes: 167 additions & 0 deletions src/Uri/Query.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

declare(strict_types=1);

/**
* @project Castor Uri
* @link https://github.com/castor-labs/uri
* @package castor/uri
* @author Matias Navarro-Carter [email protected]
* @license MIT
* @copyright 2021 CastorLabs Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Castor\Net\Uri;

use Castor\Str;
use Stringable;

/**
* Class Query parses a query string into a convenient mutable bag of data.
*
* Parameters and values are assumed to be already URL-decoded. Handling url
* encoded values should be done by client code. This is mainly done because
* the PHP CGI SAPI already decodes uris.
*
* This class also returns arrays when a parameter is requested. This is because
* it supports repeated keys in the uri string.
*/
class Query implements Stringable
{
/**
* @psalm-param array<string,list<string>>
*/
private array $params;

/**
* Query constructor.
*/
protected function __construct()
{
$this->params = [];
}

public function __toString(): string
{
return $this->toStr();
}

/**
* Creates an empty Query object.
*/
public static function create(): Query
{
return new self();
}

/**
* Makes a query string from an associative array.
*
* @param array<string,string> $array
*/
public static function make(array $array): Query
{
$query = new self();
foreach ($array as $key => $value) {
$query->add($key, $value);
}

return $query;
}

/**
* Parses a query string.
*/
public static function parse(string $query): Query
{
$parsed = new self();
if ('' === $query) {
return $parsed;
}
foreach (Str\split($query, '&') as $pair) {
$pair = Str\split($pair, '=', 2);
$parsed->add($pair[0], $pair[1] ?? '');
}

return $parsed;
}

public function toStr(): string
{
$parts = [];
foreach ($this->params as $key => $value) {
if ('' === $key) {
continue;
}
foreach ($value as $item) {
if ('' === $item) {
$parts[] = $key;

continue;
}
$parts[] = $key.'='.$item;
}
}

return Str\join('&', ...$parts);
}

public function all(): array
{
return $this->params;
}

/**
* Gets the values of a query parameter.
*/
public function get(string $param): array
{
return $this->params[$param] ?? [];
}

/**
* Adds a query parameter, preserving the previous values for that parameter.
*/
public function add(string $param, string $value): Query
{
$this->params[$param][] = $value;

return $this;
}

/**
* Puts values inside a parameter, overriding the previous contents of that
* parameter.
*
* @param array $values
*/
public function put(string $param, string ...$values): Query
{
$this->params[$param] = $values;

return $this;
}

/**
* Removes a parameter component.
*
* @return $this
*/
public function del(string $param): Query
{
$this->params[$param] = [];

return $this;
}

/**
* Returns true if there are any values for a parameter.
*/
public function has(string $param): bool
{
return ($this->params[$param] ?? []) !== [];
}
}
Loading

0 comments on commit 1a5ae01

Please sign in to comment.