Skip to content

Commit

Permalink
Adding class and constructor comments.
Browse files Browse the repository at this point in the history
Revamping file upload code - specifically on parsing the $_FILES superglobal.
Updating unit tests with new file upload behavior.
  • Loading branch information
brentscheffler committed Oct 28, 2023
1 parent fe54959 commit 0ae927c
Show file tree
Hide file tree
Showing 17 changed files with 149 additions and 164 deletions.
3 changes: 3 additions & 0 deletions src/Factory/RequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;

/**
* With this factory you can generate Request instances.
*/
class RequestFactory implements RequestFactoryInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/Factory/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;

/**
* With this factory you can generate Response instances.
*/
class ResponseFactory implements ResponseFactoryInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/Factory/ServerRequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* With this factory you can generate ServerRequest instances.
*/
class ServerRequestFactory implements ServerRequestFactoryInterface
{
/**
Expand Down
3 changes: 3 additions & 0 deletions src/Factory/StreamFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use Psr\Http\Message\StreamInterface;
use RuntimeException;

/**
* With this factory you can generate various StreamInterface instances.
*/
class StreamFactory implements StreamFactoryInterface
{
/**
Expand Down
134 changes: 28 additions & 106 deletions src/Factory/UploadedFileFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;

/**
* With this factory you can generate various an UploadedFile instance.
*/
class UploadedFileFactory implements UploadedFileFactoryInterface
{
/**
Expand Down Expand Up @@ -50,120 +53,39 @@ public static function createFromGlobal(array $file): UploadedFile
}

/**
* Create an array of UploadedFile instances from the PHP $_FILES global.
* Create a tree of UploadedFile instances.
*
* The $_FILES super global can take on several different shapes, especially
* if multiple files are being uploaded, and can be nested.
*
* @param array<string,mixed> $files
* @throws InvalidArgumentException For unrecognized values.
* @return array<UploadedFile>
* @param array<array-key,array{tmp_name:string,name:string,type:string,size:int,error:int}|array{tmp_name:array<string>,name:array<string>,type:array<string>,size:array<int>,error:array<int>}> $files Tree of uploaded files in the PHP $_FILES format.
* @return array<array-key,UploadedFile|array<UploadedFile>>
*/
public static function createFromGlobals(array $files): array
{
/**
* Traverse a nested tree of uploaded file specifications.
*
* @param array<string>|array $tmpNameTree
* @param array<int>|array $sizeTree
* @param array<int>|array $errorTree
* @param array<string>|array|null $nameTree
* @param array<string>|array|null $typeTree
* @return array<UploadedFile>
*/
$recursiveNormalize = static function (
array $tmpNameTree,
array $sizeTree,
array $errorTree,
?array $nameTree = null,
?array $typeTree = null
) use (&$recursiveNormalize): array {
$normalized = [];
foreach( $tmpNameTree as $key => $value) {
if( \is_array($value) ) {
$normalized[$key] = $recursiveNormalize(
$tmpNameTree[$key],
$sizeTree[$key],
$errorTree[$key],
$nameTree[$key] ?? null,
$typeTree[$key] ?? null
);
continue;
$uploaded_files = [];

foreach( $files as $name => $file ){
if( \is_array($file["tmp_name"]) ) {
for( $i = 0; $i < \count($file["tmp_name"]); $i++ ){
/**
* @psalm-suppress PossiblyInvalidArrayAccess
* @psalm-suppress UndefinedMethod
*/
$uploaded_files[$name][] = self::createFromGlobal([
"tmp_name" => $file["tmp_name"][$i],
"name" => $file["name"][$i],
"type" => $file["type"][$i],
"size" => $file["size"][$i],
"error" => $file["error"][$i]
]);
}

$normalized[$key] = self::createFromGlobal([
"tmp_name" => $tmpNameTree[$key],
"size" => $sizeTree[$key],
"error" => $errorTree[$key],
"name" => $nameTree[$key] ?? null,
"type" => $typeTree[$key] ?? null,
]);
}

return $normalized;
};

/**
* Normalize an array of file specifications.
*
* Loops through all nested files (as determined by receiving an array to the
* `tmp_name` key of a `$_FILES` specification) and returns a normalized array
* of UploadedFile instances.
*
* This function normalizes a `$_FILES` array representing a nested set of
* uploaded files as produced by the php-fpm SAPI, CGI SAPI, or mod_php
* SAPI.
*
* @param array $files
* @return array<UploadedFile>
*/
$normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array {
if ( !isset($files["tmp_name"]) || !is_array($files["tmp_name"])
|| ! isset($files["size"]) || !is_array($files["size"])
|| ! isset($files["error"]) || !is_array($files["error"]) )
{
throw new InvalidArgumentException(sprintf(
"\$files provided to %s MUST contain each of the keys \"tmp_name\","
. " \"size\", and \"error\", with each represented as an array;"
. " one or more were missing or non-array values",
__FUNCTION__
));
}

return $recursiveNormalize(
$files["tmp_name"],
$files["size"],
$files["error"],
$files["name"] ?? null,
$files["type"] ?? null
);
};

$normalized = [];
foreach( $files as $key => $value ) {
if( $value instanceof UploadedFileInterface ) {
$normalized[$key] = $value;
continue;
}

if( \is_array($value) && isset($value["tmp_name"]) && \is_array($value["tmp_name"])) {
$normalized[$key] = $normalizeUploadedFileSpecification($value);
continue;
}

if( \is_array($value) && isset($value["tmp_name"])) {
$normalized[$key] = self::createFromGlobal($value);
continue;
else {
/**
* @psalm-suppress ArgumentTypeCoercion
*/
$uploaded_files[$name] = self::createFromGlobal($file);
}

if( \is_array($value) ) {
$normalized[$key] = self::createFromGlobals($value);
continue;
}

throw new InvalidArgumentException("Malformed file upload.");
}

return $normalized;
return $uploaded_files;
}
}
3 changes: 3 additions & 0 deletions src/Factory/UriFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Psr\Http\Message\UriInterface;
use RuntimeException;

/**
* With this factory you can generate a Uri instance.
*/
class UriFactory implements UriFactoryInterface
{
/**
Expand Down
4 changes: 4 additions & 0 deletions src/MessageAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
use Psr\Http\Message\StreamInterface;
use RuntimeException;

/**
* This abstract class provides common functionality between Request, ServerRequest, and Response
* implementations.
*/
abstract class MessageAbstract implements MessageInterface
{
/**
Expand Down
14 changes: 9 additions & 5 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;

/**
* The `Request` class represents an outgoing HTTP request to another service, typically to be used by
* a PSR-18 compliant HTTP client library.
*/
class Request extends MessageAbstract implements RequestInterface
{
/**
Expand All @@ -32,11 +36,11 @@ class Request extends MessageAbstract implements RequestInterface
protected ?string $requestTarget = null;

/**
* @param string $method
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
* @param array<string,string> $headers
* @param string $httpVersion
* @param string $method The HTTP method to use for the request. For example, "POST", "GET", etc.
* @param string|UriInterface $uri The URI of the resource you are trying to call. For example: "https://api.example.com/books/12345"
* @param string|StreamInterface|null $body The body of the request. If request does not contain a body, you can use a null or empty string value.
* @param array<string,string> $headers An array of key & value pairs for headers to be included in the request. For example, ["Content-Type" => "application/json"]
* @param string $httpVersion The HTTP protocol version to use for this request. Defaults to "1.1".
*/
public function __construct(
string $method,
Expand Down
12 changes: 6 additions & 6 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ class Response extends MessageAbstract implements ResponseInterface
protected string $reasonPhrase;

/**
* @param int|ResponseStatus $statusCode
* @param string|StreamInterface $body
* @param array<string,string> $headers
* @param string|null $reasonPhrase
* @param string $http_version
* @param int|ResponseStatus $statusCode The HTTP response status code. For example: 200, 404, etc. Alternatively, you can use the ResponseStatus enum.
* @param string|StreamInterface|null $body The body of the response. If no body is expected for the response, you can use a null or empty string value.
* @param array<string,string> $headers An array of key & value pairs for headers to be included in the response. For example, ["Content-Type" => "application/json"]
* @param string|null $reasonPhrase The HTTP status code reason phrase. For example, "Not Found" for 404. By default, the reason phrases listed in the ResponseStatus enum will be used if none provided.
* @param string $http_version The HTTP protocol version of the response. Defaults to "1.1".
*/
public function __construct(
int|ResponseStatus $statusCode,
string|StreamInterface $body = null,
string|StreamInterface|null $body = null,
array $headers = [],
?string $reasonPhrase = null,
string $httpVersion = "1.1")
Expand Down
3 changes: 3 additions & 0 deletions src/ResponseStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Nimbly\Capsule;

/**
* All officially supported HTTP response codes and their corresponding reason phrase.
*/
enum ResponseStatus: int
{
case CONTINUE = 100;
Expand Down
34 changes: 21 additions & 13 deletions src/ServerRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;

/**
* The `ServerRequest` class represents an incoming HTTP request to be handled in your application, service, or
* middleware. Typically, you would want to create an instance of this class using the `ServerRequestFactory::createFromGlobals()`
* static method, which populates the ServerRequest from the PHP globals space ($_SERVER, $_POST, $_COOKIES, etc).
*
* You might want to create an instance of this class directly when mocking incoming requests for your application
* to build unit and integration tests.
*/
class ServerRequest extends Request implements ServerRequestInterface
{
/**
Expand All @@ -26,7 +34,7 @@ class ServerRequest extends Request implements ServerRequestInterface
/**
* Uploaded files sent in request.
*
* @var array<UploadedFileInterface>
* @var array<array-key,UploadedFileInterface|array<UploadedFileInterface>>
*/
protected array $uploadedFiles = [];

Expand All @@ -52,15 +60,15 @@ class ServerRequest extends Request implements ServerRequestInterface
protected array $serverParams = [];

/**
* @param string $method
* @param string|UriInterface $uri
* @param string|StreamInterface $body
* @param array<string,mixed> $query
* @param array<string,mixed> $headers
* @param array<string,mixed> $cookies
* @param array<UploadedFileInterface> $files
* @param array<string,mixed> $serverParams
* @param string $version
* @param string $method The HTTP method of the request. For example, "POST", "GET", etc.
* @param string|UriInterface $uri The URI of the resource to be called. For example: "https://api.example.com/books/12345"
* @param string|StreamInterface $body The body of the request. If request does not contain a body, you can use a null or empty string value.
* @param array<string,mixed> $query An array of key & value pairs for the query params. For example: ["q" => "red socks", "p" => 2]
* @param array<string,mixed> $headers An array of key & value pairs for headers in the request. For example: ["Content-Type" => "application/json"]
* @param array<string,mixed> $cookies An array of key & value pairs of the cookies in the request. For example: ["c_source" => "web"]
* @param array<array-key,UploadedFileInterface|array<UploadedFileInterface>> $files An array of UploadedFileInterface instance for the files to be included in the request.
* @param array<string,mixed> $serverParams An array of key & value pairs to be included into the server params space. For example: ["REAL_IP" => "4.4.4.4"]
* @param string $version The HTTP protocol version used for this request. Defaults to "1.1".
*/
public function __construct(
string $method,
Expand Down Expand Up @@ -134,7 +142,7 @@ public function withQueryParams(array $query): static

/**
* @inheritDoc
* @return array<UploadedFileInterface>
* @return array<array-key,UploadedFileInterface|array<UploadedFileInterface>>
*/
public function getUploadedFiles(): array
{
Expand Down Expand Up @@ -336,9 +344,9 @@ public function hasUploadedFile(string $name): bool
* Get an UploadedFileInterface instance by its name.
*
* @param string $name
* @return UploadedFileInterface|null
* @return UploadedFileInterface|array<UploadedFileInterface>|null
*/
public function getUploadedFile(string $name): ?UploadedFileInterface
public function getUploadedFile(string $name): UploadedFileInterface|array|null
{
return $this->getUploadedFiles()[$name] ?? null;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Stream/BufferStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
use Psr\Http\Message\StreamInterface;
use RuntimeException;

/**
* A simple in memory character buffer stream. This stream is ideal for streaming smaller string contents. There is no
* PHP stream resource backing this stream, instead, all content is buffered directly into the instance.
*/
class BufferStream implements StreamInterface
{
/**
Expand All @@ -17,7 +21,7 @@ class BufferStream implements StreamInterface
protected ?string $buffer = "";

/**
* @param string $data
* @param string $data Initial data to write to buffer.
*/
public function __construct(string $data = "")
{
Expand Down
2 changes: 1 addition & 1 deletion src/Stream/ResourceStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ResourceStream implements StreamInterface
protected $resource;

/**
* @param resource $resource Resource *must* be of type "stream."
* @param resource $resource Resource *must* be of PHP type "stream." For example: $resource = \fopen("/tmp/aa981naai1", "r");
*/
public function __construct($resource)
{
Expand Down
Loading

0 comments on commit 0ae927c

Please sign in to comment.