From ca0f1ecb39bea2252630bd36f1b5440a68427136 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 14 May 2020 21:39:38 +0000 Subject: [PATCH] Made the router code less janky. --- composer.json | 4 +- composer.lock | 105 +------------- misuzu.php | 4 +- public/index.php | 50 +++---- src/Http/Filters/EnforceLogInFilter.php | 5 +- src/Http/Filters/EnforceLogOutFilter.php | 5 +- src/Http/Filters/Filter.php | 8 -- src/Http/Filters/FilterInterface.php | 6 +- src/Http/Filters/ValidateCsrfFilter.php | 11 +- src/Http/Handlers/AssetsHandler.php | 6 +- src/Http/Handlers/AuthHandler.php | 8 +- src/Http/Handlers/ForumHandler.php | 12 +- src/Http/Handlers/Handler.php | 13 +- src/Http/Handlers/HomeHandler.php | 4 +- src/Http/Handlers/InfoHandler.php | 9 +- src/Http/Handlers/SockChatHandler.php | 28 ++-- src/Http/HttpMessage.php | 9 +- src/Http/HttpRequestMessage.php | 160 +++++++++++++++++++-- src/Http/HttpResponseMessage.php | 58 +++++++- src/Http/HttpServerRequestMessage.php | 154 -------------------- src/Http/Routing/Route.php | 140 ++++++++---------- src/Http/Routing/Router.php | 125 +++++++++++----- src/Http/Routing/RouterResponseMessage.php | 59 -------- src/Http/UploadedFile.php | 6 +- src/{Http => }/Stream.php | 35 +++-- src/{Http => }/Uri.php | 38 +---- 26 files changed, 453 insertions(+), 609 deletions(-) delete mode 100644 src/Http/Filters/Filter.php delete mode 100644 src/Http/HttpServerRequestMessage.php delete mode 100644 src/Http/Routing/RouterResponseMessage.php rename src/{Http => }/Stream.php (87%) rename src/{Http => }/Uri.php (75%) diff --git a/composer.json b/composer.json index 7e951a5f..2083597f 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,7 @@ "geoip2/geoip2": "~2.0", "twig/extensions": "^1.5", "jublonet/codebird-php": "^3.1", - "chillerlan/php-qrcode": "^3.0", - "psr/http-message": "^1.0", - "psr/http-server-handler": "^1.0" + "chillerlan/php-qrcode": "^3.0" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index f1a9eeb2..16777fc3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "bfa28642db2d2fcc44e6d1e872948d44", + "content-hash": "4e5f02da04a08b7717736b58d033f8d3", "packages": [ { "name": "chillerlan/php-qrcode", @@ -684,109 +684,6 @@ "homepage": "https://github.com/maxmind/web-service-common-php", "time": "2020-05-06T14:07:26+00:00" }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "psr/http-server-handler", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side request handler", - "keywords": [ - "handler", - "http", - "http-interop", - "psr", - "psr-15", - "psr-7", - "request", - "response", - "server" - ], - "time": "2018-10-30T16:46:14+00:00" - }, { "name": "swiftmailer/swiftmailer", "version": "v6.2.3", diff --git a/misuzu.php b/misuzu.php index af3d8dd0..166dc3bd 100644 --- a/misuzu.php +++ b/misuzu.php @@ -57,8 +57,8 @@ spl_autoload_register(function(string $className) { require_once $classPath; }); -class_alias(\Misuzu\Http\HttpServerRequestMessage::class, '\Misuzu\Http\Handlers\Request'); -class_alias(\Misuzu\Http\Routing\RouterResponseMessage::class, '\Misuzu\Http\Handlers\Response'); +class_alias(\Misuzu\Http\HttpResponseMessage::class, '\HttpResponse'); +class_alias(\Misuzu\Http\HttpRequestMessage::class, '\HttpRequest'); require_once 'utility.php'; require_once 'src/audit_log.php'; diff --git a/public/index.php b/public/index.php index bc307503..2203fdd7 100644 --- a/public/index.php +++ b/public/index.php @@ -1,49 +1,51 @@ setInstance(); -$router->addRoutes( +Router::setHandlerFormat('\Misuzu\Http\Handlers\%sHandler'); +Router::setFilterFormat('\Misuzu\Http\Filters\%sFilter'); +Router::addRoutes( // Home - Route::get('/', Handler::call('index@Home')), + Route::get('/', 'index', 'Home'), // Assets - Route::get('/assets/([a-zA-Z0-9\-]+)\.(css|js)', true, Handler::call('view@Assets')), + Route::get('/assets/([a-zA-Z0-9\-]+)\.(css|js)', 'view', 'Assets'), // Info - Route::get('/info', Handler::call('index@Info')), - Route::get('/info/([A-Za-z0-9_/]+)', true, Handler::call('page@Info')), + Route::get('/info', 'index', 'Info'), + Route::get('/info/([A-Za-z0-9_/]+)', 'page', 'Info'), // Forum - Route::get('/forum/mark-as-read', Handler::call('markAsReadGET@Forum'))->addFilters(Filter::call('EnforceLogIn')), - Route::post('/forum/mark-as-read', Handler::call('markAsReadPOST@Forum'))->addFilters(Filter::call('EnforceLogIn'), Filter::call('ValidateCsrf')), + Route::group('/forum', 'Forum')->addChildren( + Route::get('/mark-as-read', 'markAsReadGET')->addFilters('EnforceLogIn'), + Route::post('/mark-as-read', 'markAsReadPOST')->addFilters('EnforceLogIn', 'ValidateCsrf'), + ), // Sock Chat - Route::create(['GET', 'POST'], '/_sockchat.php', Handler::call('phpFile@SockChat')), - Route::get('/_sockchat/emotes', Handler::call('emotes@SockChat')), - Route::get('/_sockchat/bans', Handler::call('bans@SockChat')), - Route::get('/_sockchat/login', Handler::call('login@SockChat')), - Route::post('/_sockchat/bump', Handler::call('bump@SockChat')), - Route::post('/_sockchat/verify', Handler::call('verify@SockChat')), + Route::create(['GET', 'POST'], '/_sockchat.php', 'phpFile', 'SockChat'), + Route::group('/_sockchat', 'SockChat')->addChildren( + Route::get('/emotes', 'emotes'), + Route::get('/bans', 'bans'), + Route::get('/login', 'login'), + Route::post('/bump', 'bump'), + Route::post('/verify', 'verify'), + ), // Redirects - Route::get('/index.php', Handler::redirect(url('index'), true)), - Route::get('/info.php', Handler::redirect(url('info'), true)), - Route::get('/info.php/([A-Za-z0-9_/]+)', true, Handler::call('redir@Info')), - Route::get('/auth.php', Handler::call('legacy@Auth')) + Route::get('/index.php', url('index')), + Route::get('/info.php', url('info')), + Route::get('/info.php/([A-Za-z0-9_/]+)', 'redir', 'Info'), + Route::get('/auth.php', 'legacy', 'Auth'), ); -$response = $router->handle($request); +$response = Router::handle($request); $response->setHeader('X-Powered-By', 'Misuzu'); $responseStatus = $response->getStatusCode(); diff --git a/src/Http/Filters/EnforceLogInFilter.php b/src/Http/Filters/EnforceLogInFilter.php index ba5e735c..e665b5ec 100644 --- a/src/Http/Filters/EnforceLogInFilter.php +++ b/src/Http/Filters/EnforceLogInFilter.php @@ -2,11 +2,10 @@ namespace Misuzu\Http\Filters; use Misuzu\Http\HttpResponseMessage; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Misuzu\Http\HttpRequestMessage; class EnforceLogInFilter implements FilterInterface { - public function process(ServerRequestInterface $request): ?ResponseInterface { + public function process(HttpRequestMessage $request): ?HttpResponseMessage { if(!user_session_active()) return new HttpResponseMessage(403); diff --git a/src/Http/Filters/EnforceLogOutFilter.php b/src/Http/Filters/EnforceLogOutFilter.php index 6fec4373..1a6bd182 100644 --- a/src/Http/Filters/EnforceLogOutFilter.php +++ b/src/Http/Filters/EnforceLogOutFilter.php @@ -2,11 +2,10 @@ namespace Misuzu\Http\Filters; use Misuzu\Http\HttpResponseMessage; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Misuzu\Http\HttpRequestMessage; class EnforceLogOutFilter implements FilterInterface { - public function process(ServerRequestInterface $request): ?ResponseInterface { + public function process(HttpRequestMessage $request): ?HttpResponseMessage { if(user_session_active()) return new HttpResponseMessage(404); diff --git a/src/Http/Filters/Filter.php b/src/Http/Filters/Filter.php deleted file mode 100644 index bd489d00..00000000 --- a/src/Http/Filters/Filter.php +++ /dev/null @@ -1,8 +0,0 @@ -getMethod() !== 'GET' && $request->getMethod() !== 'DELETE') { $token = $request->getHeaderLine('X-Misuzu-CSRF'); - if(empty($token)) { - $body = $request->getParsedBody(); - $token = isset($body['_csrf']) && is_string($body['_csrf']) ? $body['_csrf'] : null; - } + if(empty($token)) + $token = $request->getBodyParam('_csrf', FILTER_SANITIZE_STRING); if(empty($token) || !CSRF::validate($token)) return new HttpResponseMessage(400); diff --git a/src/Http/Handlers/AssetsHandler.php b/src/Http/Handlers/AssetsHandler.php index e36d1184..5a2d9f45 100644 --- a/src/Http/Handlers/AssetsHandler.php +++ b/src/Http/Handlers/AssetsHandler.php @@ -1,6 +1,8 @@ getHeaderLine('If-None-Match') === $entityTag) return 304; if(array_key_exists($type, self::TYPES)) { diff --git a/src/Http/Handlers/AuthHandler.php b/src/Http/Handlers/AuthHandler.php index 2a36a249..4de2fe9a 100644 --- a/src/Http/Handlers/AuthHandler.php +++ b/src/Http/Handlers/AuthHandler.php @@ -1,10 +1,12 @@ getQueryParams(); - $mode = isset($query['m']) && is_string($query['m']) ? $query['m'] : ''; + public static function legacy(HttpResponse $response, HttpRequest $request): void { + $mode = $request->getQueryParam('m', FILTER_SANITIZE_STRING); $destination = [ 'logout' => 'auth-logout', 'reset' => 'auth-reset', diff --git a/src/Http/Handlers/ForumHandler.php b/src/Http/Handlers/ForumHandler.php index a09147b5..35f46763 100644 --- a/src/Http/Handlers/ForumHandler.php +++ b/src/Http/Handlers/ForumHandler.php @@ -1,12 +1,13 @@ getQueryParams(); - $forumId = isset($query['forum']) && is_string($query['forum']) ? (int)$query['forum'] : null; + public function markAsReadGET(HttpResponse $response, HttpRequest $request): void { + $forumId = (int)$request->getQueryParam('forum', FILTER_SANITIZE_NUMBER_INT); $response->setTemplate('confirm', [ 'title' => 'Mark forum as read', 'message' => 'Are you sure you want to mark ' . ($forumId === null ? 'the entire' : 'this') . ' forum as read?', @@ -17,9 +18,8 @@ final class ForumHandler extends Handler { ]); } - public function markAsReadPOST(Response $response, Request $request) { - $body = $request->getParsedBody(); - $forumId = isset($body['forum']) && is_string($body['forum']) ? (int)$body['forum'] : null; + public function markAsReadPOST(HttpResponse $response, HttpRequest $request) { + $forumId = (int)$request->getBodyParam('forum', FILTER_SANITIZE_NUMBER_INT); forum_mark_read($forumId, user_session_current('user_id')); $response->redirect( diff --git a/src/Http/Handlers/Handler.php b/src/Http/Handlers/Handler.php index 5fd1ce9f..96686fb3 100644 --- a/src/Http/Handlers/Handler.php +++ b/src/Http/Handlers/Handler.php @@ -1,15 +1,4 @@ redirect($location, $permanent); - }; - } -} +abstract class Handler {} diff --git a/src/Http/Handlers/HomeHandler.php b/src/Http/Handlers/HomeHandler.php index 986c060f..9184843c 100644 --- a/src/Http/Handlers/HomeHandler.php +++ b/src/Http/Handlers/HomeHandler.php @@ -1,11 +1,13 @@ Config::get('site.name', Config::TYPE_STR, 'Misuzu'), diff --git a/src/Http/Handlers/InfoHandler.php b/src/Http/Handlers/InfoHandler.php index 229ccc1f..33cbe204 100644 --- a/src/Http/Handlers/InfoHandler.php +++ b/src/Http/Handlers/InfoHandler.php @@ -1,16 +1,19 @@ setTemplate('info.index'); } - public function redir(Response $response, Request $request, string $name = ''): void { + public function redir(HttpResponse $response, HttpRequest $request, string $name = ''): void { $response->redirect(url('info', ['title' => $name]), true); } - public function page(Response $response, Request $request, string $name) { + public function page(HttpResponse $response, HttpRequest $request, string $name) { $document = [ 'content' => '', 'title' => '', diff --git a/src/Http/Handlers/SockChatHandler.php b/src/Http/Handlers/SockChatHandler.php index d7676247..24884456 100644 --- a/src/Http/Handlers/SockChatHandler.php +++ b/src/Http/Handlers/SockChatHandler.php @@ -2,11 +2,13 @@ namespace Misuzu\Http\Handlers; use Exception; +use HttpResponse; +use HttpRequest; use Misuzu\Base64; use Misuzu\Config; use Misuzu\DB; use Misuzu\Emoticon; -use Misuzu\Http\Stream; +use Misuzu\Stream; use Misuzu\Users\ChatToken; use Misuzu\Users\User; @@ -47,22 +49,22 @@ final class SockChatHandler extends Handler { $this->hashKey = file_get_contents($hashKeyPath); } - public function phpFile(Response $response, Request $request) { + public function phpFile(HttpResponse $response, HttpRequest $request) { $query = $request->getQueryParams(); if(isset($query['emotes'])) return $this->emotes($response, $request); if(isset($query['bans']) && is_string($query['bans'])) - return $this->bans($response, $request->withHeader('X-SharpChat-Signature', $query['bans'])); + return $this->bans($response, $request->setHeader('X-SharpChat-Signature', $query['bans'])); $body = $request->getParsedBody(); if(isset($body['bump'], $body['hash']) && is_string($body['bump']) && is_string($body['hash'])) return $this->bump( $response, - $request->withHeader('X-SharpChat-Signature', $body['hash']) - ->withBody(Stream::create($body['bump'])) + $request->setHeader('X-SharpChat-Signature', $body['hash']) + ->setBody(Stream::create($body['bump'])) ); $source = isset($body['user_id']) ? $body : $query; @@ -72,8 +74,8 @@ final class SockChatHandler extends Handler { && is_string($source['ip']) && is_string($source['hash'])) return $this->verify( $response, - $request->withHeader('X-SharpChat-Signature', $source['hash']) - ->withBody(Stream::create(json_encode([ + $request->setHeader('X-SharpChat-Signature', $source['hash']) + ->setBody(Stream::create(json_encode([ 'user_id' => $source['user_id'], 'token' => $source['token'], 'ip' => $source['ip'], @@ -83,7 +85,7 @@ final class SockChatHandler extends Handler { return $this->login($response, $request); } - public function emotes(Response $response, Request $request): array { + public function emotes(HttpResponse $response, HttpRequest $request): array { $response->setHeader('Access-Control-Allow-Origin', '*') ->setHeader('Access-Control-Allow-Methods', 'GET'); @@ -107,7 +109,7 @@ final class SockChatHandler extends Handler { return $out; } - public function bans(Response $response, Request $request): array { + public function bans(HttpResponse $response, HttpRequest $request): array { $userHash = $request->getHeaderLine('X-SharpChat-Signature'); $realHash = hash_hmac('sha256', 'givemethebeans', $this->hashKey); @@ -124,7 +126,7 @@ final class SockChatHandler extends Handler { ')->fetchAll(); } - public function login(Response $response, Request $request) { + public function login(HttpResponse $response, HttpRequest $request) { if(!user_session_active()) { $response->redirect(url('auth-login')); return; @@ -140,7 +142,7 @@ final class SockChatHandler extends Handler { } if(MSZ_DEBUG && isset($params['dump'])) { - $ipAddr = $request->getServerParams()['REMOTE_ADDR']; + $ipAddr = $request->getRemoteAddress(); $hash = hash_hmac('sha256', implode('#', [$token->getUserId(), $token->getToken(), $ipAddr]), $this->hashKey); $response->setText(sprintf( @@ -168,7 +170,7 @@ final class SockChatHandler extends Handler { } } - public function bump(Response $response, Request $request): void { + public function bump(HttpResponse $response, HttpRequest $request): void { $userHash = $request->getHeaderLine('X-SharpChat-Signature'); $bumpString = (string)$request->getBody(); $realHash = hash_hmac('sha256', $bumpString, $this->hashKey); @@ -185,7 +187,7 @@ final class SockChatHandler extends Handler { user_bump_last_active($bumpUser->id, $bumpUser->ip); } - public function verify(Response $response, Request $request): array { + public function verify(HttpResponse $response, HttpRequest $request): array { $userHash = $request->getHeaderLine('X-SharpChat-Signature'); if(strlen($userHash) !== 64) diff --git a/src/Http/HttpMessage.php b/src/Http/HttpMessage.php index 2f22e3ce..61fca942 100644 --- a/src/Http/HttpMessage.php +++ b/src/Http/HttpMessage.php @@ -2,10 +2,9 @@ namespace Misuzu\Http; use InvalidArgumentException; -use Psr\Http\Message\MessageInterface; -use Psr\Http\Message\StreamInterface; +use Misuzu\Stream; -abstract class HttpMessage implements MessageInterface { +abstract class HttpMessage { protected $version = ''; protected $headers = []; protected $body = null; @@ -28,11 +27,11 @@ abstract class HttpMessage implements MessageInterface { public function getBody() { return $this->body; } - public function setBody(StreamInterface $body): self { + public function setBody(Stream $body): self { $this->body = $body; return $this; } - public function withBody(StreamInterface $stream) { + public function withBody(Stream $stream) { return (clone $this)->setBody($stream); } diff --git a/src/Http/HttpRequestMessage.php b/src/Http/HttpRequestMessage.php index 227ae925..763eef46 100644 --- a/src/Http/HttpRequestMessage.php +++ b/src/Http/HttpRequestMessage.php @@ -2,37 +2,54 @@ namespace Misuzu\Http; use InvalidArgumentException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; +use Misuzu\Stream; +use Misuzu\Uri; -class HttpRequestMessage extends HttpMessage implements RequestInterface { +class HttpRequestMessage extends HttpMessage { private $method = null; private $uri = null; private $requestTarget = null; + private $server = []; + private $cookies = []; + private $query = []; + private $files = []; + private $parsedBody = null; + private $bodyStream = null; - public function __construct(string $method, UriInterface $uri, array $headers = []) { + public function __construct( + string $method, + Uri $uri, + array $serverParams = [], + array $headers = [], + array $queryParams = [], + array $uploadedFiles = [], + $parsedBody = null, + Stream $rawBody = null + ) { parent::__construct($headers); $this->setMethod($method); $this->setUri($uri); + $this->setServerParams($serverParams); + $this->setQueryParams($queryParams); + $this->setUploadedFiles($uploadedFiles); + $this->setParsedBody($parsedBody); + $this->setBody($rawBody); } public function getMethod() { return $this->method; } - public function setMethod(string $method): self { + private function setMethod(string $method): self { if(empty($method)) throw new InvalidArgumentException('Invalid method name.'); $this->method = $method; return $this; } - public function withMethod($method) { - return (clone $this)->setMethod($method); - } public function getUri() { return $this->uri; } - public function setUri(UriInterface $uri, bool $preserveHost = false): self { + private function setUri(Uri $uri, bool $preserveHost = false): self { $this->uri = $uri; if(!$preserveHost || !$this->hasHeader('Host')) @@ -40,9 +57,6 @@ class HttpRequestMessage extends HttpMessage implements RequestInterface { return $this; } - public function withUri($uri, $preserveHost = false) { - return (clone $this)->setUri($uri, $preserveHost); - } private function applyHostHeader(): void { $uri = $this->getUri(); @@ -73,14 +87,130 @@ class HttpRequestMessage extends HttpMessage implements RequestInterface { return $target; } - public function setRequestTarget(?string $requestTarget): self { + private function setRequestTarget(?string $requestTarget): self { if(preg_match('#\s#', $requestTarget)) throw new InvalidArgumentException('Request target may not contain spaces.'); $this->requestTarget = $requestTarget; return $this; } - public function withRequestTarget($requestTarget) { - return (clone $this)->setRequestTarget($requestTarget); + + public function getServerParams() { + return $this->server; + } + private function setServerParams(array $serverParams): self { + $this->server = $serverParams; + return $this; + } + public function getServerParam(string $name, int $filter = FILTER_DEFAULT, $options = null) { + if(!isset($this->server[$name])) + return null; + return filter_var($this->server[$name], $filter, $options); + } + + public function getRemoteAddress(): string { + return $this->server['REMOTE_ADDR'] ?? '::1'; + } + + public function getCookieParams() { + return $this->cookies; + } + private function setCookieParams(array $cookies): self { + $this->cookies = $cookies; + return $this; + } + public function getCookieParam(string $name, int $filter = FILTER_DEFAULT, $options = null) { + if(!isset($this->cookies[$name])) + return null; + return filter_var($this->cookies[$name], $filter, $options); + } + + public function getQueryParams() { + return $this->query; + } + private function setQueryParams(array $query): self { + $this->query = $query; + return $this; + } + public function getQueryParam(string $name, int $filter = FILTER_DEFAULT, $options = null) { + if(!isset($this->query[$name])) + return null; + return filter_var($this->query[$name], $filter, $options); + } + + public function getUploadedFiles() { + return $this->files; + } + private function setUploadedFiles(array $uploadedFiles): self { + $this->files = $uploadedFiles; + return $this; + } + + public function getParsedBody() { + return $this->parsedBody; + } + private function setParsedBody($data): self { + if(!is_array($data) && !is_object($data) && $data !== null) + throw new InvalidArgumentException('Parsed body must by of type array, object or null.'); + $this->parsedBody = $data; + return $this; + } + public function getBodyParam(string $name, int $filter = FILTER_DEFAULT, $options = null) { + if($this->parsedBody === null) + return null; + + $value = null; + + if(is_object($this->parsedBody) && isset($this->parsedBody->{$name})) { + $value = $this->parsedBody->{$name}; + } elseif(is_array($this->parsedBody) && isset($this->parsedBody[$name])) { + $value = $this->parsedBody[$name]; + } + + return filter_var($value, $filter, $options); + } + + private static function getRequestHeaders(): array { + if(function_exists('getallheaders')) + return getallheaders(); + + $headers = []; + + foreach($_SERVER as $key => $value) { + if(substr($key, 0, 5) === 'HTTP_') { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + $reqHeaders[$key] = $value; + } elseif($key === 'CONTENT_TYPE') { + $reqHeaders['Content-Type'] = $value; + } elseif($key === 'CONTENT_LENGTH') { + $reqHeaders['Content-Length'] = $value; + } + } + + if(!isset($headers['Authorization'])) { + if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } elseif(isset($_SERVER['PHP_AUTH_USER'])) { + $password = $_SERVER['PHP_AUTH_PW'] ?? ''; + $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $password); + } elseif(isset($_SERVER['PHP_AUTH_DIGEST'])) { + $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; + } + } + + return $headers; + } + + public static function fromGlobals(): HttpRequestMessage { + return new static( + $_SERVER['REQUEST_METHOD'], + new Uri('/' . trim($_SERVER['REQUEST_URI'] ?? '', '/')), + $_SERVER, + self::getRequestHeaders(), + $_GET, + UploadedFile::createFromFILES($_FILES), + $_POST, + Stream::createFromFile('php://input') + ); } } diff --git a/src/Http/HttpResponseMessage.php b/src/Http/HttpResponseMessage.php index 9750f8dd..e6eefddb 100644 --- a/src/Http/HttpResponseMessage.php +++ b/src/Http/HttpResponseMessage.php @@ -2,10 +2,10 @@ namespace Misuzu\Http; use InvalidArgumentException; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; +use Misuzu\Template; +use Misuzu\Stream; -class HttpResponseMessage extends HttpMessage implements ResponseInterface { +class HttpResponseMessage extends HttpMessage { private $statusCode = 0; private $reasonPhrase = ''; @@ -32,8 +32,56 @@ class HttpResponseMessage extends HttpMessage implements ResponseInterface { $this->reasonPhrase = $reasonPhrase; return $this; } - public function withStatus($code, $reasonPhrase = '') { - return (clone $this)->setStatusCode($code)->setReasonPhrase($reasonPhrase); + + public function getContentType(): string { + return $this->getHeaderLine('Content-Type'); + } + public function setContentType(string $type): self { + $this->setHeader('Content-Type', $type); + return $this; + } + + public function setText(string $text): self { + $body = $this->getBody(); + + if($body !== null) + $body->close(); + + $this->setBody(Stream::create($text)); + return $this; + } + public function appendText(string $text): self { + $body = $this->getBody(); + + if($body === null) + $this->setBody(Stream::create($text)); + else + $body->write($text); + return $this; + } + public function setHtml(string $content, bool $html = true): self { + if(empty($this->getContentType())) + $this->setContentType('text/html; charset=utf-8'); + $this->setText($content); + return $this; + } + public function setTemplate(string $file, array $vars = []): self { + return $this->setHtml(Template::renderRaw($file, $vars)); + } + public function setJson($content, int $options = 0): self { + $this->setContentType('application/json; charset=utf-8'); + $this->setText(json_encode($content, $options)); + return $this; + } + + public function redirect(string $path, bool $permanent = false, bool $xhr = false): self { + $this->setStatusCode($permanent ? 301 : 302); + $this->setHeader($xhr ? 'X-Misuzu-Location' : 'Location', $path); + return $this; + } + + public function getContents(): string { + return (string)($this->getBody() ?? ''); } // https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml diff --git a/src/Http/HttpServerRequestMessage.php b/src/Http/HttpServerRequestMessage.php deleted file mode 100644 index 503573a5..00000000 --- a/src/Http/HttpServerRequestMessage.php +++ /dev/null @@ -1,154 +0,0 @@ -setServerParams($serverParams); - $this->setQueryParams($queryParams); - $this->setUploadedFiles($uploadedFiles); - $this->setParsedBody($parsedBody); - $this->setBody($rawBody); - } - - public function getServerParams() { - return $this->server; - } - public function setServerParams(array $serverParams): self { - $this->server = $serverParams; - return $this; - } - - public function getCookieParams() { - return $this->cookies; - } - public function setCookieParams(array $cookies): self { - $this->cookies = $cookies; - return $this; - } - public function withCookieParams(array $cookies) { - return (clone $this)->setCookieParams($cookies); - } - - public function getQueryParams() { - return $this->query; - } - public function setQueryParams(array $query): self { - $this->query = $query; - return $this; - } - public function withQueryParams(array $query) { - return (clone $this)->setQueryParams($query); - } - - public function getUploadedFiles() { - return $this->files; - } - public function setUploadedFiles(array $uploadedFiles): self { - $this->files = $uploadedFiles; - return $this; - } - public function withUploadedFiles(array $uploadedFiles): self { - return (clone $this)->setUploadedFiles($uploadedFiles); - } - - public function getParsedBody() { - return $this->parsedBody; - } - public function setParsedBody($data): self { - if(!is_array($data) && !is_object($data) && $data !== null) - throw new InvalidArgumentException('Parsed body must by of type array, object or null.'); - $this->parsedBody = $data; - return $this; - } - public function withParsedBody($data) { - return (clone $this)->setParsedBody($data); - } - - public function getAttributes() { - return $this->attributes; - } - public function getAttribute($name, $default = null) { - return $this->attributes[$name] ?? $default; - } - public function setAttribute(string $name, $value): self { - $this->attributes[$name] = $value; - return $this; - } - public function withAttribute($name, $value) { - return (clone $this)->setAttribute($name, $value); - } - public function removeAttribute(string $name): self { - unset($this->attributes[$name]); - return $this; - } - public function withoutAttribute($name) { - return (clone $this)->removeAttribute($name); - } - - private static function getRequestHeaders(): array { - if(function_exists('getallheaders')) - return getallheaders(); - - $headers = []; - - foreach($_SERVER as $key => $value) { - if(substr($key, 0, 5) === 'HTTP_') { - $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); - $reqHeaders[$key] = $value; - } elseif($key === 'CONTENT_TYPE') { - $reqHeaders['Content-Type'] = $value; - } elseif($key === 'CONTENT_LENGTH') { - $reqHeaders['Content-Length'] = $value; - } - } - - if(!isset($headers['Authorization'])) { - if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { - $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; - } elseif(isset($_SERVER['PHP_AUTH_USER'])) { - $password = $_SERVER['PHP_AUTH_PW'] ?? ''; - $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $password); - } elseif(isset($_SERVER['PHP_AUTH_DIGEST'])) { - $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; - } - } - - return $headers; - } - - public static function fromGlobals(): HttpServerRequestMessage { - return new static( - $_SERVER['REQUEST_METHOD'], - new Uri('/' . trim($_SERVER['REQUEST_URI'] ?? '', '/')), - $_SERVER, - self::getRequestHeaders(), - $_GET, - UploadedFile::createFromFILES($_FILES), - $_POST, - Stream::createFromFile('php://input') - ); - } -} diff --git a/src/Http/Routing/Route.php b/src/Http/Routing/Route.php index 9c537efb..056701b0 100644 --- a/src/Http/Routing/Route.php +++ b/src/Http/Routing/Route.php @@ -2,47 +2,65 @@ namespace Misuzu\Http\Routing; use InvalidArgumentException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Serializable; +use Misuzu\Http\HttpRequestMessage; +use Misuzu\Http\HttpResponseMessage; -class Route { +class Route implements Serializable { private $methods = []; private $path = ''; - private $regex = false; - private $handler; private $children = []; private $filters = []; + private $parentRoute = null; - public function __construct(array $methods, string $path, bool $regex, $handler) { + private $handlerClass = null; + private $handlerMethod = null; + + public function __construct(array $methods, string $path, ?string $method = null, ?string $class = null) { $this->methods = array_map('strtoupper', $methods); - $this->regex = $regex; - $this->handler = $handler; $this->path = $path; + $this->handlerClass = $class; + $this->handlerMethod = $method; } - public static function create(array $methods, string $path, $regex = null, $callable = null): self { - return new static($methods, $path, is_bool($regex) ? $regex : false, is_bool($regex) ? $callable : $regex); + public static function create(array $methods, string $path, ?string $method = null, ?string $class = null): self { + return new static($methods, $path, $method, $class); } - public static function get(string $path, $regex = null, $callable = null): self { - return self::create(['GET'], $path, $regex, $callable); + public static function get(string $path, ?string $method = null, ?string $class = null): self { + return self::create(['GET'], $path, $method, $class); } - public static function post(string $path, $regex = null, $callable = null): self { - return self::create(['POST'], $path, $regex, $callable); + public static function post(string $path, ?string $method = null, ?string $class = null): self { + return self::create(['POST'], $path, $method, $class); + } + public static function group(string $path, ?string $class = null): self { + return self::create([''], $path, null, $class); } - public function isRegex(): bool { - return $this->regex; + public function getHandlerClass(): string { + return $this->handlerClass ?? ($this->parentRoute === null ? '' : $this->parentRoute->getHandlerClass()); } - public function setRegex(): self { - $this->regex = true; - foreach($this->children as $child) - $child->setRegex(); + public function setHandlerClass(string $class): self { + $this->handlerClass = $class; + return $this; + } + + public function getHandlerMethod() { + return $this->handlerMethod; + } + + public function getParent(): ?self { + return $this->parentRoute; + } + public function setParent(self $route): self { + $this->parentRoute = $route; return $this; } public function getPath(): string { - return $this->path; + $path = $this->path; + if($this->parentRoute !== null) + $path = $this->parentRoute->getPath() . '/' . trim($path, '/'); + return $path; } public function setPath(string $path): self { $this->path = $path; @@ -51,80 +69,42 @@ class Route { public function addFilters(string ...$filters): self { $this->filters = array_merge($this->filters, $filters); - foreach($this->children as $child) - $child->addFilters(...$filters); return $this; } public function getFilters(): array { - return $this->filters; + $filters = $this->filters; + if($this->parentRoute !== null) + $filters += $this->parentRoute->getFilters(); + return $filters; } public function getChildren(): array { return $this->children; } public function addChildren(Route ...$routes): self { - foreach($routes as $route) { - $route->setPrefix($this->getPath())->addFilters(...$this->getFilters()); - - if($this->isRegex()) - $route->setRegex(); - - $this->children[] = $route; - } + foreach($routes as $route) + $this->children[] = $route->setParent($this); return $this; } - public function setPrefix(string $prefix): self { - foreach($this->children as $child) - $child->setPrefix($prefix); - return $this->setPath($prefix . '/' . trim($this->getPath(), '/')); - } - - public function match(RequestInterface $request, array &$matches): bool { - $matches = [$this->getPath()]; - + public function match(HttpRequestMessage $request, array &$matches): bool { + $matches = []; if(!in_array($request->getMethod(), $this->methods)) return false; - - $requestPath = $request->getUri()->getPath(); - - return $this->isRegex() - ? preg_match('#^' . $this->getPath() . '$#', $requestPath, $matches) - : $this->getPath() === $requestPath; + return preg_match('#^' . $this->getPath() . '$#', $request->getUri()->getPath(), $matches) === 1; } - public function dispatch(ServerRequestInterface $request, ...$args): ResponseInterface { - $response = new RouterResponseMessage(200); - $result = null; + public function serialize() { + return serialize([ + $this->methods, + $this->getPath(), + $this->getFilters(), + $this->getHandlerClass(), + $this->getHandlerMethod(), + ]); + } - array_unshift($args, $response, $request); - - if(is_array($this->handler)) { - if(method_exists($this->handler[0] ?? '', $this->handler[1] ?? '')) { - $handlerClass = new $this->handler[0]($response, $request); - $result = $handlerClass->{$this->handler[1]}(...$args); - } - } elseif(is_callable($this->handler)) { - $result = call_user_func_array($this->handler, $args); - } - - if($result !== null) { - $resultType = gettype($result); - - switch($resultType) { - case 'array': - case 'object': - $response->setJson($result); - break; - case 'integer': - $response->setStatusCode($result); - break; - default: - $response->setHtml($result); - break; - } - } - - return $response; + public function unserialize($data) { + [$this->methods, $this->path, $this->filters, $this->handlerClass, $this->handlerMethod] = unserialize($data); } } diff --git a/src/Http/Routing/Router.php b/src/Http/Routing/Router.php index fe1bc67f..5bfec620 100644 --- a/src/Http/Routing/Router.php +++ b/src/Http/Routing/Router.php @@ -1,75 +1,126 @@ {$name}(...$args); + public function __call(string $name, array $args) { + if($name[0] === '_') + return null; + return $this->{'_' . $name}(...$args); } - public function __construct() {} + public static function __callStatic(string $name, array $args) { + if($name[0] === '_') + return null; + if(self::$instance === null) + (new static)->setInstance(); + return self::$instance->{'_' . $name}(...$args); + } public function setInstance(): self { return self::$instance = $this; } - public function addRoutes(Route ...$routes): self { - foreach($routes as $route) { - if($route->isRegex()) { - $this->routesRegex[] = $route; - } else { - $this->routesExact[] = $route; - } - - $this->addRoutes(...$route->getChildren()); - } - + public function _getHandlerFormat(): string { + return $this->handlerFormat; + } + public function _setHandlerFormat(string $format): self { + $this->handlerFormat = $format; return $this; } - private function matchExact(RequestInterface $request, array &$matches): ?Route { - foreach($this->routesExact as $route) + public function _getFilterFormat(): string { + return $this->filterFormat; + } + public function _setFilterFormat(string $format): self { + $this->filterFormat = $format; + return $this; + } + + public function _addRoutes(Route ...$routes): self { + foreach($routes as $route) { + $this->routes[] = $route; + $this->addRoutes(...$route->getChildren()); + } + return $this; + } + + // Unused, might be useful for the future to immediately smash the routes list together + // without having to propagate children. + // Should obviously not be in debug mode and should also be nuked after a pull on stable. + public function _getData(): string { + return serialize([$this->routes]); + } + public function _setData(string $data): void { + [$this->routes] = unserialize($data); + } + + private function match(HttpRequestMessage $request, array &$matches): ?Route { + foreach($this->routes as $route) if($route->match($request, $matches)) return $route; return null; } - private function matchRegex(RequestInterface $request, array &$matches): ?Route { - foreach($this->routesRegex as $route) - if($route->match($request, $matches)) - return $route; - return null; - } - - public function match(RequestInterface $request, array &$matches): ?Route { - return $this->matchExact($request, $matches) ?? $this->matchRegex($request, $matches); - } - - public function handle(ServerRequestInterface $request): ResponseInterface { + public function _handle(HttpRequestMessage $request): HttpResponseMessage { $matches = []; $route = $this->match($request, $matches); + array_shift($matches); if($route === null) return new HttpResponseMessage(404); foreach($route->getFilters() as $filter) { + if(!class_exists($filter)) + $filter = sprintf($this->_getFilterFormat(), $filter); $response = (new $filter)->process($request, $this); - if($response !== null) return $response; } - $response = $route->dispatch($request, ...array_slice($matches, 1)); + $response = new HttpResponseMessage(200); + $result = null; + array_unshift($matches, $response, $request); + + $handlerMethod = $route->getHandlerMethod(); + if(is_callable($handlerMethod)) { + $result = call_user_func_array($handlerMethod, $args); + } elseif($handlerMethod[0] === '/') { + $response->redirect($handlerMethod); + } else { + $handlerClass = $route->getHandlerClass(); + if(!empty($handlerClass)) { + if(!class_exists($handlerClass)) + $handlerClass = sprintf($this->_getHandlerFormat(), $handlerClass); + $handlerClass = new $handlerClass($response, $request); + $result = $handlerClass->{$handlerMethod}(...$matches); + } + } + + if($result !== null) { + $resultType = gettype($result); + + switch($resultType) { + case 'array': + case 'object': + $response->setJson($result); + break; + case 'integer': + $response->setStatusCode($result); + break; + default: + $response->setHtml($result); + break; + } + } return $response; } diff --git a/src/Http/Routing/RouterResponseMessage.php b/src/Http/Routing/RouterResponseMessage.php deleted file mode 100644 index 4c739f5d..00000000 --- a/src/Http/Routing/RouterResponseMessage.php +++ /dev/null @@ -1,59 +0,0 @@ -getHeaderLine('Content-Type'); - } - public function setContentType(string $type): self { - $this->setHeader('Content-Type', $type); - return $this; - } - - public function setText(string $text): self { - $body = $this->getBody(); - - if($body !== null) - $body->close(); - - $this->setBody(Stream::create($text)); - return $this; - } - public function appendText(string $text): self { - $body = $this->getBody(); - - if($body === null) - $this->setBody(Stream::create($text)); - else - $body->write($text); - return $this; - } - public function setHtml(string $content, bool $html = true): self { - if(empty($this->getContentType())) - $this->setContentType('text/html; charset=utf-8'); - $this->setText($content); - return $this; - } - public function setTemplate(string $file, array $vars = []): self { - return $this->setHtml(Template::renderRaw($file, $vars)); - } - public function setJson($content, int $options = 0): self { - $this->setContentType('application/json; charset=utf-8'); - $this->setText(json_encode($content, $options)); - return $this; - } - - public function redirect(string $path, bool $permanent = false, bool $xhr = false): self { - $this->setStatusCode($permanent ? 301 : 302); - $this->setHeader($xhr ? 'X-Misuzu-Location' : 'Location', $path); - return $this; - } - - public function getContents(): string { - return (string)($this->getBody() ?? ''); - } -} diff --git a/src/Http/UploadedFile.php b/src/Http/UploadedFile.php index ef5eb156..eeba719b 100644 --- a/src/Http/UploadedFile.php +++ b/src/Http/UploadedFile.php @@ -3,9 +3,9 @@ namespace Misuzu\Http; use InvalidArgumentException; use RuntimeException; -use Psr\Http\Message\UploadedFileInterface; +use Misuzu\Stream; -class UploadedFile implements UploadedFileInterface { +class UploadedFile { private $stream = null; private $fileName = null; private $size = null; @@ -34,7 +34,7 @@ class UploadedFile implements UploadedFileInterface { $this->fileName = $fileNameOrStream; elseif(is_resource($fileNameOrStream)) $this->stream = Stream::create($fileNameOrStream); - elseif($fileNameOrStream instanceof StreamInterface) + elseif($fileNameOrStream instanceof Stream) $this->stream = $fileNameOrStream; if($size === null && $this->stream !== null) diff --git a/src/Http/Stream.php b/src/Stream.php similarity index 87% rename from src/Http/Stream.php rename to src/Stream.php index e8a3b613..3034e2d5 100644 --- a/src/Http/Stream.php +++ b/src/Stream.php @@ -1,12 +1,11 @@ seekable = $metaData['seekable'] && fseek($this->stream, 0, SEEK_CUR) === 0; } - public static function create($contents = ''): StreamInterface { - if($contents instanceof StreamInterface) + public static function create($contents = ''): Stream { + if($contents instanceof Stream) return $contents; return new static($contents); } - public static function createFromFile(string $filename, string $mode = 'rb'): StreamInterface { + public static function createFromFile(string $filename, string $mode = 'rb'): Stream { if(!in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) throw new InvalidArgumentException("Provided mode ({$mode}) is invalid."); @@ -76,18 +75,18 @@ class Stream implements StreamInterface { return $metaData[$key] ?? null; } - public function isReadable() { + public function isReadable(): bool { return $this->readable; } - public function read($length) { + public function read($length): int { if(!$this->isReadable()) throw RuntimeException('Can\'t read from this stream.'); return fread($this->stream, $length); } - public function getContents() { + public function getContents(): string { if(!isset($this->stream)) throw new RuntimeException('Can\'t read contents of stream.'); @@ -97,11 +96,11 @@ class Stream implements StreamInterface { return $contents; } - public function isWritable() { + public function isWritable(): bool { return $this->writable; } - public function write($string) { + public function write($string): int { if(!$this->isWritable()) throw new RuntimeException('Can\'t write to this stream.'); @@ -113,11 +112,11 @@ class Stream implements StreamInterface { return $count; } - public function isSeekable() { + public function isSeekable(): bool { return $this->seekable; } - public function seek($offset, $whence = SEEK_SET) { + public function seek($offset, $whence = SEEK_SET): void { if(!$this->isSeekable()) throw new RuntimeException('Can\'t seek in this stream.'); @@ -125,11 +124,11 @@ class Stream implements StreamInterface { throw new RuntimeException("Failed to seek to position {$offset} ({$whence})."); } - public function rewind() { + public function rewind(): void { $this->seek(0); } - public function tell() { + public function tell(): int { if(!isset($this->stream)) throw new RuntimeException('Can\'t determine the position of a detached stream.'); if(($pos = ftell($this->stream)) === false) @@ -138,11 +137,11 @@ class Stream implements StreamInterface { return $pos; } - public function eof() { + public function eof(): bool { return !isset($this->stream) || feof($this->stream); } - public function getSize() { + public function getSize(): ?int { if($this->size !== null) return $this->size; @@ -170,7 +169,7 @@ class Stream implements StreamInterface { return $stream; } - public function close() { + public function close(): void { $stream = $this->detach(); if(is_resource($stream)) diff --git a/src/Http/Uri.php b/src/Uri.php similarity index 75% rename from src/Http/Uri.php rename to src/Uri.php index 43842d1f..bc4a64a9 100644 --- a/src/Http/Uri.php +++ b/src/Uri.php @@ -1,10 +1,9 @@ scheme = $scheme; return $this; } - public function withScheme($scheme) { - if(!is_string($scheme)) - throw new InvalidArgumentException('Scheme must be a string.'); - - return (clone $this)->setScheme($scheme); - } public function getAuthority() { $authority = ''; @@ -79,9 +72,6 @@ class Uri implements UriInterface { $this->password = $password; return $this; } - public function withUserInfo($user, $password = null) { - return (clone $this)->setUserInfo($user, $password); - } public function getHost() { return $this->host; @@ -90,12 +80,6 @@ class Uri implements UriInterface { $this->host = $host; return $this; } - public function withHost($host) { - if(!is_string($host)) - throw new InvalidArgumentException('Hostname must be a string.'); - - return (clone $this)->setHost($host); - } public function getPort() { return $this->port; @@ -107,9 +91,6 @@ class Uri implements UriInterface { $this->port = $port; return $this; } - public function withPort($port) { - return (clone $this)->setPort($port); - } public function getPath() { return $this->path; @@ -118,12 +99,6 @@ class Uri implements UriInterface { $this->path = $path; return $this; } - public function withPath($path) { - if(!is_string($path)) - throw new InvalidArgumentException('Path must be a string.'); - - return (clone $this)->setPath($path); - } public function getQuery() { return $this->query; @@ -132,12 +107,6 @@ class Uri implements UriInterface { $this->query = $query; return $this; } - public function withQuery($query) { - if(!is_string($query)) - throw new InvalidArgumentException('Query string must be a string.'); - - return (clone $this)->setQuery($query); - } public function getFragment() { return $this->fragment; @@ -146,9 +115,6 @@ class Uri implements UriInterface { $this->fragment = $fragment; return $this; } - public function withFragment($fragment) { - return (clone $this)->setFragment($fragment); - } public function __toString() { $string = '';