diff --git a/Env.php b/Env.php index 914ea79..9e4ac0a 100755 --- a/Env.php +++ b/Env.php @@ -57,8 +57,8 @@ public function drop(string $key): void public function formatKey($key) { - return Format\Str::value($key)->clearBreaks()->trim()->replaceSpecialChar() - ->trimSpaces()->replaceSpaces("-")->toUpper()->get(); + return Format\Str::value($key)->clearBreaks()->trim()->normalizeAccents() + ->normalizeSeparators()->replaceSpaces("-")->toUpper()->get(); } public function generateOutput(array $fromArr = ["data", "fileData", "set"]) diff --git a/Environment.php b/Environment.php index 14be440..31e8337 100755 --- a/Environment.php +++ b/Environment.php @@ -77,9 +77,9 @@ public function getPath(): string { if (is_null($this->path)) { $basePath = ''; - $requestName = Format\Str::value($this->get("SCRIPT_NAME"))->extractPath()->get(); + $requestName = Format\Str::value($this->get("SCRIPT_NAME"))->getUrlPath()->get(); $requestDir = dirname($requestName); - $requestUri = Format\Str::value($this->get("REQUEST_URI"))->extractPath()->get(); + $requestUri = Format\Str::value($this->get("REQUEST_URI"))->getUrlPath()->get(); $this->path = $requestUri; if (stripos($requestUri, $requestName) === 0) { diff --git a/README.md b/README.md index 764a37e..000cedb 100755 --- a/README.md +++ b/README.md @@ -1,130 +1,258 @@ + + # MaplePHP - A Full-Featured PSR-7 Compliant HTTP Library -MaplePHP/Http is a PHP library that brings simplicity and adherence to the PSR-7 standard into handling HTTP messages, requests, and responses within your web projects. It's thoughtfully designed to make the integration of crucial elements like Stream, Client, Cookies, UploadedFile, and Headers straightforward and efficient. -By aligning closely with PSR-7, MaplePHP/Http facilitates better interoperability between web components, allowing for more effective communication within applications. Whether you're working with client-side cookies, managing headers in a request, or handling file uploads through UploadedFile, this library has got you covered, making these tasks more manageable and less time-consuming. +**MaplePHP/Http** is a powerful and easy-to-use PHP library that fully supports the PSR-7 HTTP message interfaces. It simplifies handling HTTP requests, responses, streams, URIs, and uploaded files, making it an excellent choice for developers who want to build robust and interoperable web applications. + +With MaplePHP, you can effortlessly work with HTTP messages while adhering to modern PHP standards, ensuring compatibility with other PSR-7 compliant libraries. -MaplePHP/Http aims to support your web development by offering a reliable foundation for working with HTTP messaging, streamlining the process of dealing with requests and responses. It's a practical choice for developers looking to enhance their applications with PSR-7 compliant HTTP handling in a user-friendly way. +## Why Choose MaplePHP? +- **Full PSR-7 Compliance**: Seamlessly integrates with other PSR-7 compatible libraries and frameworks. +- **User-Friendly API**: Designed with developers in mind for an intuitive and straightforward experience. +- **Comprehensive Functionality**: Handles all aspects of HTTP messaging, including requests, responses, streams, URIs, and file uploads. +- **Flexible and Extensible**: Easily adapts to projects of any size and complexity. ## Installation -``` +Install MaplePHP via Composer: + +```bash composer require maplephp/http ``` -## Initialize -The **examples** below is utilizing the "namespace" below just to more easily demonstrate the guide. -```php -use MaplePHP\Http; -``` +### Handling HTTP Requests -## Request +#### Creating a Server Request + +To create a server request, use the `ServerRequest` class: ```php -$request = new Http\ServerRequest(UriInterface $uri, EnvironmentInterface $env); -``` -#### Get request method -```php -echo $request->getMethod(); // GET, POST, PUT, DELETE +use MaplePHP\Http\ServerRequest; +use MaplePHP\Http\Uri; +use MaplePHP\Http\Environment; + +// Create an environment instance (wraps $_SERVER) +$env = new Environment(); + +// Create a URI instance from the environment +$uri = new Uri($env->getUriParts()); + +// Create the server request +$request = new ServerRequest($uri, $env); ``` -#### Get Uri instance + +#### Accessing Request Data + +You can easily access various parts of the request: + ```php -$uri = $request->getUri(); // UriInterface -echo $uri->getScheme(); // https -echo $uri->getAuthority(); // [userInfo@]host[:port] -echo $uri->getUserInfo(); // username:password -echo $uri->getHost(); // example.com, staging.example.com, 127.0.0.1, localhost -echo $uri->getPort(); // 443 -echo $uri->getPath(); // /about-us/workers -echo $uri->getQuery(); // page-id=12&filter=2 -echo $uri->getFragment(); // anchor-12 (The anchor hash without "#") -echo $uri->getUri(); // Get the full URI +// Get the HTTP method +$method = $request->getMethod(); // e.g., GET, POST + +// Get request headers +$headers = $request->getHeaders(); + +// Get a specific header +$userAgent = $request->getHeaderLine('User-Agent'); + +// Get query parameters +$queryParams = $request->getQueryParams(); + +// Get parsed body (for POST requests) +$parsedBody = $request->getParsedBody(); + +// Get uploaded files +$uploadedFiles = $request->getUploadedFiles(); + +// Get server attributes +$attributes = $request->getAttributes(); ``` -## Response -Only the **(StreamInterface) Body** attribute is required and the rest will auto propagate if you leave them be. + +#### Modifying the Request + +Requests are immutable; methods that modify the request return a new instance: + ```php -$response = new Http\Response( - StreamInterface $body, - ?HeadersInterface $headers = null, - int $status = 200, - ?string $phrase = null, - ?string $version = null -); +// Add a new header +$newRequest = $request->withHeader('X-Custom-Header', 'MyValue'); + +// Change the request method +$newRequest = $request->withMethod('POST'); + +// Add an attribute +$newRequest = $request->withAttribute('user_id', 123); ``` -#### Get Status code + +### Managing HTTP Responses + +#### Creating a Response + +Create a response using the `Response` class: + ```php -echo $response->getStatusCode(); // 200 +use MaplePHP\Http\Response; +use MaplePHP\Http\Stream; + +// Create a stream for the response body +$body = new Stream('php://temp', 'rw'); + +// Write content to the body +$body->write('Hello, world!'); +$body->rewind(); + +// Create the response with the body +$response = new Response($body); ``` -#### Get Status code + +#### Setting Status Codes and Headers + +You can set the HTTP status code and headers: + ```php -$newInst = $response->withStatus(404); -echo $newInst->getStatusCode(); // 404 -echo $newInst->getReasonPhrase(); // Not Found +// Set the status code to 200 OK +$response = $response->withStatus(200); + +// Add headers +$response = $response->withHeader('Content-Type', 'text/plain'); + +// Add multiple headers +$response = $response->withAddedHeader('X-Powered-By', 'MaplePHP'); ``` -## Message -Both Request and Response library will inherit methods under Message but with different information. + +#### Sending the Response + +To send the response to the client: + ```php -echo $response->getProtocolVersion(); // 1.1 -echo $response->getHeaders(); // Array with all headers -echo $response->hasHeader("Content-Length"); // True -echo $response->getHeader("Content-Length"); // 1299 -echo $response->getBody(); // StreamInterface +// Output headers +foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + header(sprintf('%s: %s', $name, $value), false); + } +} + +// Output status line +header(sprintf( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() +)); + +// Output body +echo $response->getBody(); ``` -## A standard example usage +### Working with Streams + +Streams are used for the message body in requests and responses. + +#### Creating a Stream +Reading and Writing with stream + ```php -$stream = new Http\Stream(Http\Stream::TEMP); -$response = new Http\Response($stream); -$env = new Http\Environment(); -$request = new Http\ServerRequest(new Http\Uri($env->getUriParts()), $env); +use MaplePHP\Http\Stream; + +// Create a stream from a file +//$fileStream = new Stream('/path/to/file.txt', 'r'); + +// Create a stream from a string +$memoryStream = new Stream(Stream::MEMORY); +//$memoryStream = new Stream('php://memory', 'r+'); // Same as above +$memoryStream->write('Stream content'); + +// Write to the stream +$memoryStream->write(' More content'); + +// Read from the stream +$memoryStream->rewind(); +echo $memoryStream->getContents(); +// Result: 'Stream content More content' ``` -## Stream -None of the construct attributes are required and will auto propagate if you leave them be. + +#### Using Streams in Requests and Responses + ```php -$stream = new Http\Stream( - (mixed) Stream - (string) permission -); +// Set stream as the body of a response +$response = $response->withBody($memoryStream); ``` -### Basic stream examples -#### Write to stream +### Manipulating URIs + +URIs are used to represent resource identifiers. + +#### Creating and Modifying URIs + ```php -$stream = new Http\Stream(Http\Stream::TEMP); -if ($stream->isSeekable()) { - $stream->write("Hello world"); - //echo $stream; // will print Hello world - // Or - $stream->rewind(); - echo $stream->getContents(); // Hello world - // Or Same as above - //echo $stream->read($stream->getSize()); -} +// Create a URI instance +$uri = new Uri('http://example.com:8000/path?query=value#fragment'); + +// Modify the URI +$uri = $uri->withScheme('https') + ->withUserInfo('guest', 'password123') + ->withHost('example.org') + ->withPort(8080) + ->withPath('/new-path') + ->withQuery('query=newvalue') + ->withFragment('section1'); + +// Convert URI to string +echo $uri; // Outputs the full URI +//Result: https://guest:password123@example.org:8080/new-path?query=newvalue#section1 ``` -#### Get file content with stream +#### Accessing URI Components + ```php -$stream = new Http\Stream("/var/www/html/YourApp/dir/dir/data.json"); -echo $stream->getContents(); +echo $uri->getScheme(); // 'http' +echo $uri->getUserInfo(); // 'guest:password123' +echo $uri->getHost(); // 'example.org' +echo $uri->getPath(); // '/new-path' +echo $uri->getQuery(); // 'key=newvalue' +echo $uri->getFragment(); // 'section1' +echo $uri->getAuthority(); // 'guest:password123@example.org:8080' ``` -#### Upload a stream to the server +### Handling Uploaded Files + +Manage file uploads with ease using the `UploadedFile` class. + +#### Accessing Uploaded Files + ```php -$upload = new Http\UploadedFile($stream); -$upload->moveTo("/var/www/html/upload/log.txt"); // Place Hello world in txt file +// Get uploaded files from the request +$uploadedFiles = $request->getUploadedFiles(); + +// Access a specific uploaded file +$uploadedFile = $uploadedFiles['file_upload']; + +// Get file details +$clientFilename = $uploadedFile->getClientFilename(); +$clientMediaType = $uploadedFile->getClientMediaType(); + +// Move the uploaded file to a new location +$uploadedFile->moveTo('/path/to/uploads/' . $clientFilename); ``` -### Create a request -The client will be using curl, so it's essential to ensure that it is enabled in case it has been disabled for any reason. +### Using the HTTP Client + +Send HTTP requests using the built-in HTTP client. + +#### Sending a Request + ```php +use MaplePHP\Http\Client; +use MaplePHP\Http\Request; + // Init request client -$client = new Http\Client([CURLOPT_HTTPAUTH => CURLAUTH_DIGEST]); // Pass on Curl options +$client = new Client([CURLOPT_HTTPAUTH => CURLAUTH_DIGEST]); // Pass on Curl options // Create request data -$request = new Http\Request( +$request = new Request( "POST", // The HTTP Method (GET, POST, PUT, DELETE, PATCH) "https://admin:mypass@example.com:443/test.php", // The Request URI ["customHeader" => "lorem"], // Add Headers, empty array is allowed @@ -133,7 +261,20 @@ $request = new Http\Request( // Pass request data to client and POST $response = $client->sendRequest($request); - -// Get Stream data -var_dump($response->getBody()->getContents()); +if ($response->getStatusCode() === 200) { + // Parse the response body + $data = json_decode($response->getBody()->getContents(), true); + // Use the data + echo 'User Name: ' . $data['name']; +} else { + echo 'Error: ' . $response->getReasonPhrase(); +} ``` + +## Conclusion + +**MaplePHP/Http** is a comprehensive library that makes working with HTTP in PHP a breeze. Its full PSR-7 compliance ensures that your applications are built on solid, modern standards, promoting interoperability and maintainability. + +Whether you're handling incoming requests, crafting responses, manipulating URIs, working with streams, or managing file uploads, MaplePHP provides a clean and intuitive API that simplifies your development process. + +Get started today and enhance your PHP applications with MaplePHP! diff --git a/ServerRequest.php b/ServerRequest.php index d622a14..ef68bc4 100755 --- a/ServerRequest.php +++ b/ServerRequest.php @@ -28,7 +28,7 @@ public function __construct(UriInterface $uri, EnvironmentInterface $env) $this->attr = [ "env" => $this->env->fetch(), "cookies" => $_COOKIE, - "files" => $_FILES + "files" => $this->normalizeFiles($_FILES) ]; } @@ -323,6 +323,57 @@ public function withoutAttribute($name): self return $inst; } + /** + * This will normalize/flatten the a file Array + * @param array $file + * @return array + */ + protected function normalizeFiles(array $files): array + { + $normalized = []; + + foreach ($files as $key => $file) { + if (is_array($file['error'])) { + $normalized[$key] = $this->normalizeFileArray($file); + } else { + $normalized[$key] = new UploadedFile($file); + } + } + return $normalized; + } + + /** + * This will normalize/flatten the a multi-level file Array + * @param array $file + * @return array + */ + protected function normalizeFileArray(array $file): array + { + $normalized = []; + + foreach ($file['error'] as $key => $error) { + if (is_array($error)) { + $normalized[$key] = $this->normalizeFileArray([ + 'name' => $file['name'][$key], + 'type' => $file['type'][$key], + 'tmp_name' => $file['tmp_name'][$key], + 'error' => $file['error'][$key], + 'size' => $file['size'][$key] + ]); + } else { + $normalized[$key] = new UploadedFile( + $file['name'][$key], + $file['type'][$key], + $file['tmp_name'][$key], + $file['error'][$key], + $file['size'][$key] + ); + } + } + + return $normalized; + } + /* public function getEnv() { diff --git a/UploadedFile.php b/UploadedFile.php index 5e1c68d..b1e813a 100755 --- a/UploadedFile.php +++ b/UploadedFile.php @@ -39,8 +39,14 @@ class UploadedFile implements UploadedFileInterface * StreamInterface * (string) FilePath/php stream */ - public function __construct($stream) + public function __construct(StreamInterface|array|string $stream, mixed ...$vars) { + + if(count($vars) > 0 && is_string($stream)) { + array_unshift($vars, $stream); + $stream = array_combine(['name', 'type', 'tmp_name', 'error', 'size'], $vars); + } + if ($stream instanceof StreamInterface) { $this->stream = $stream; } elseif (isset($stream['tmp_name'])) { @@ -52,7 +58,7 @@ public function __construct($stream) } elseif (is_string($stream)) { $this->stream = $this->withStream($stream); } else { - throw new RuntimeException("The stream argument is not a valid resource", 1); + throw new RuntimeException("Could not validate arguments for the upload stream", 1); } } diff --git a/Uri.php b/Uri.php index 13b2557..b5f3306 100755 --- a/Uri.php +++ b/Uri.php @@ -61,7 +61,7 @@ protected function polyfill() { $this->scheme = "http"; $this->host = "localhost"; - $this->port = 80; + $this->port = null; $this->user = ""; $this->pass = ""; $this->path = ""; @@ -213,7 +213,10 @@ public function getDefaultPort(): ?int public function getPath(): string { if ($val = $this->getUniquePart("path")) { - $this->encoded['path'] = Format\Str::value($val)->toggleUrlencode(['%2F'], ['/'])->get(); + $this->encoded['path'] = Format\Str::value($val) + ->normalizeUrlEncoding() + ->replace(['%2F'], ['/']) + ->get(); if($this->encoded['path']) { $this->encoded['path'] = "/".ltrim($this->encoded['path'], "/"); } @@ -229,8 +232,9 @@ public function getQuery(): string { if ($val = $this->getUniquePart("query")) { $this->encoded['query'] = Format\Str::value($val) - ->toggleUrlencode(['%3D', '%26', '%5B', '%5D'], ['=', '&', '[', ']']) - ->get(); + ->normalizeUrlEncoding() + ->replace(['%3D', '%26', '%5B', '%5D'], ['=', '&', '[', ']']) + ->get(); } return (string)$this->encoded['query']; } diff --git a/composer.json b/composer.json index b4a1dfb..22f390c 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "maplephp/http", - "version": "v1.2.0", + "version": "v1.2.3", "type": "library", - "description": "The library is fully integrated with PSR-7 Http Message and designed for use with MaplePHP framework.", + "description": "MaplePHP/Http is a powerful and easy-to-use PHP library that fully supports the PSR-7 HTTP message interfaces.", "keywords": [ "psr7", "http", @@ -29,7 +29,7 @@ ], "require": { "php": ">=8.0", - "maplephp/dto": "^2.0" + "maplephp/dto": "^3.0" }, "require-dev": { "maplephp/unitary": "^1.0" diff --git a/tests/unitary-request.php b/tests/unitary-request.php index ea6d867..1319ae2 100644 --- a/tests/unitary-request.php +++ b/tests/unitary-request.php @@ -3,24 +3,22 @@ $unit = new MaplePHP\Unitary\Unit(); // If you build your library right it will become very easy to mock, like I have below. -$request = new MaplePHP\Http\Request( - "POST", // The HTTP Method (GET, POST, PUT, DELETE, PATCH) - "https://admin:mypass@example.com:65535/test.php?id=5221&place=stockholm", // The Request URI - ["Content-Type" => "application/x-www-form-urlencoded"], // Add Headers, empty array is allowed - ["email" => "john.doe@example.com"] // Post data -); // Begin by adding a test -$unit->case("MaplePHP Request URI path test", function() use($request) { +$unit->case("MaplePHP Request URI path test", function() { + $request = new MaplePHP\Http\Request( + "POST", // The HTTP Method (GET, POST, PUT, DELETE, PATCH) + "https://admin:mypass@example.com:65535/test.php?id=5221&place=stockholm", // The Request URI + ["Content-Type" => "application/x-www-form-urlencoded"], // Add Headers, empty array is allowed + ["email" => "john.doe@example.com"] // Post data + ); - // Test 1 $this->add($request->getMethod(), function() { return $this->equal("POST"); }, "HTTP Request method Type is not POST"); - // Adding a error message is not required, but it is highly recommended + // Adding an error message is not required, but it is highly recommended - // Test 2 $this->add($request->getUri()->getPort(), [ "isInt" => [], // Has no arguments = empty array "min" => [1], // Strict way is to pass each argument to array @@ -29,7 +27,6 @@ ], "Is not a valid port number"); - // Test 3 $this->add($request->getUri()->getUserInfo(), [ "isString" => [], "User validation" => function($value) { @@ -38,6 +35,8 @@ } ], "Is not a valid port number"); -}); -$unit->execute(); \ No newline at end of file + $this->add((string)$request->withUri(new \MaplePHP\Http\Uri("https://example.se"))->getUri(), [ + "equal" => ["https://example.se"], + ], "GetUri expects https://example.se as result"); +}); \ No newline at end of file