Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capsule 3.0 #3

Merged
merged 12 commits into from
Sep 21, 2024
Merged
6 changes: 5 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ end_of_line=lf
indent_style=tab
indent_size=4
charset=utf-8
trim_trailing_whitespace=true
trim_trailing_whitespace=true

[*.yml]
indent_style=space
indent_size=2
8 changes: 5 additions & 3 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ jobs:
strategy:
fail-fast: false
matrix:
php-version: [8.0, 8.1, 8.2]
php-version: [8.2, 8.3]

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -38,7 +38,9 @@ jobs:
- name: Run test suite
run: make coverage

- uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./build/logs/clover.xml
#flags: unittests # optional
Expand Down
113 changes: 108 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,123 @@ $response = new Response(200, \json_encode(["foo" => "bar"]), ["Content-Type" =>

### Response Status

Capsule provides a `ResponseStatus` helper class with HTTP response codes as constants and reason phrases.
Capsule provides a `ResponseStatus` enum with HTTP response codes and reason phrases.

```php
$response = new Response(ResponseStatus::NOT_FOUND);
$response = new Response(ResponseStatus::NOT_FOUND));
```

```php
$phrase = ResponseStatus::getPhrase(ResonseStatus::NOT_FOUND);
$phrase = ResponseStatus::NOT_FOUND->getPhrase();

echo $phrase; // Outputs "Not Found"
```

## HTTP Factory (PSR-17)

Capsule includes a set of PSR-17 factory classes to be used to create `Request`, `ServerRequest`, `Response`, `Stream`, `UploadedFile`, and `Uri` instances.
Capsule includes a set of PSR-17 factory classes to be used to create `Request`, `ServerRequest`, `Response`, `Stream`, `UploadedFile`, and `Uri` instances, found in the `Nimbly\Capsule\Factory` namespace. These factories are typically used with other libraries that are PSR-7 agnostic. They're also useful for creating mocked instances in unit testing.

`RequestFactory`, `ServerRequestFactory`, `ResponseFactory`, `StreamFactory`, `UploadedFileFactory`, and `UriFactory`.
### RequestFactory
```php
$requestFactory = new RequestFactory;
$request = $requestFactory->createRequest("get", "https://api.example.com");
```

### ServerRequestFactory
```php
$serverRequestFactory = new ServerRequestFactory;
$serverRequest = $serverRequestFactory->createServerRequest("post", "https://api.example.com/books");
```

In addition, the `ServerRequestFactory` provides several static methods for creating server requests.

#### Creating ServerRequest from PHP globals
You can create a `ServerRequest` instance from the PHP globals space ($_POST, $_GET, $_FILES, $_SERVER, and $_COOKIES).

```php
$serverRequest = ServerRequestFactory::createFromGlobals();
```

#### Creating ServerRequest from another PSR-7 ServerRequest
You can create a Capsule `ServerRequest` instance from another PSR-7 ServerRequest instance:

```php
$serverRequest = ServerRequestFactory::createServerRequestFromPsr7($otherServerRequest);
```

### ResponseFactory

```php
$responseFactory = new ResponseFactory;
$response = $responseFactory->createResponse(404);
```

### StreamFactory

#### Create a stream from string content

```php
$streamFactory = new StreamFactory;
$stream = $streamFactory->createStream(\json_encode($body));
```

#### Create a stream from a file

```php
$streamFactory = new StreamFactory;
$stream = $streamFactory->createStreamFromFile("/reports/q1.pdf");
```

#### Create a stream from any resource

```php
$resource = \fopen("https://example.com/reports/q1.pdf", "r");

$streamFactory = new StreamFactory;
$stream = $streamFactory->createStreamFromResource($resource);
```

Alternatively, these methods are also available statically:

```php
// Create a stream from a string.
$stream = StreamFactory::createFromString(\json_encode($body));

// Create a stream from a local file.
$stream = StreamFactory::createFromFile("/reports/q1.pdf");

// Create a stream from a PHP resource.
$resource = \fopen("https://example.com/reports/q1.pdf", "r");
$stream = StreamFactory::createFromResource($resource);
```

### UploadedFileFactory

#### Create an UploadedFile instance
```php
$uploadedFileFactory = new UploadedFileFactory;

$stream = StreamFactory::createFromFile("/tmp/upload");

$uploadedFile = $uploadedFileFactory->createUploadedFile(
$stream,
$stream->getSize(),
UPLOAD_ERR_OK,
"q1_report.pdf",
"application/pdf"
);
```

### UriFactory

The `UriFactory` allows you to create and parse URIs.

```php
$uriFactory = new UriFactory;
$uri = $uriFactory->createUri("https://api.example.com/v1/books?a=Kurt+Vonnegut");
```

This method is also available statically:

```php
$uri = UriFactory::createFromString("https://api.example.com/v1/books?a=Kurt+Vonnegut");
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
],
"require": {
"php": "^8.0",
"php": "^8.2",
"psr/http-message": "^1.0|^2.0",
"psr/http-factory": "^1.0"
},
Expand All @@ -24,7 +24,7 @@
}
},
"require-dev": {
"vimeo/psalm": "^4.0",
"vimeo/psalm": "^5.0",
"phpunit/phpunit": "^9.0",
"symfony/var-dumper": "^4.3"
},
Expand Down
4 changes: 3 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<?xml version="1.0"?>
<psalm
errorLevel="2"
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="false"
>
<projectFiles>
<directory name="src" />
Expand Down
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
9 changes: 8 additions & 1 deletion src/Factory/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
namespace Nimbly\Capsule\Factory;

use Nimbly\Capsule\Response;
use Nimbly\Capsule\ResponseStatus;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;

/**
* With this factory you can generate Response instances.
*/
class ResponseFactory implements ResponseFactoryInterface
{
/**
* @inheritDoc
*/
public function createResponse(int $code = 200, string $reasonPhrase = ""): ResponseInterface
{
return new Response($code, null, [], $reasonPhrase);
return new Response(
statusCode: ResponseStatus::from($code),
reasonPhrase: $reasonPhrase ?: null
);
}
}
18 changes: 6 additions & 12 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 Expand Up @@ -65,16 +68,7 @@ public static function createFromGlobals(): ServerRequest
// Get the request body first by getting raw input from php://input.
$body = \file_get_contents("php://input");

// Process the uploaded files into an array<UploadedFile>.
$files = [];

/**
* @var array<string,array{error:int,name:string,size:int,tmp_name:string,type:string}> $_FILES
*/
foreach( $_FILES as $name => $file ){
$files[$name] = UploadedFileFactory::createFromGlobal($file);
}

// Get all request headers
if( \function_exists("getallheaders") ){
$headers = \getallheaders();
}
Expand All @@ -89,7 +83,7 @@ public static function createFromGlobals(): ServerRequest
}

/**
* @psalm-suppress InvalidScalarArgument
* @psalm-suppress InvalidArgument
*/
$serverRequest = new ServerRequest(
$_SERVER["REQUEST_METHOD"] ?? "GET",
Expand All @@ -98,7 +92,7 @@ public static function createFromGlobals(): ServerRequest
$_GET,
\array_change_key_case($headers),
$_COOKIE,
$files,
UploadedFileFactory::createFromGlobals($_FILES),
$_SERVER,
$versionMatch[2] ?? "1.1"
);
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
54 changes: 47 additions & 7 deletions src/Factory/UploadedFileFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;

/**
* With this factory you can generate an UploadedFile instance.
*/
class UploadedFileFactory implements UploadedFileFactoryInterface
{
/**
* @inheritDoc
*/
public function createUploadedFile(
StreamInterface $stream,
int $size = null,
int $error = UPLOAD_ERR_OK,
string $clientFilename = null,
string $clientMediaType = null
): UploadedFileInterface {
StreamInterface $stream,
int $size = null,
int $error = UPLOAD_ERR_OK,
string $clientFilename = null,
string $clientMediaType = null
): UploadedFileInterface {

return new UploadedFile(
$stream,
Expand All @@ -40,11 +43,48 @@ public function createUploadedFile(
public static function createFromGlobal(array $file): UploadedFile
{
return new UploadedFile(
$file["tmp_name"],
StreamFactory::createFromFile($file["tmp_name"], "r"),
$file["name"] ?? null,
$file["type"] ?? null,
$file["size"] ?? 0,
$file["error"] ?? UPLOAD_ERR_OK
);
}

/**
* Create a tree of UploadedFile instances.
*
* @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
{
$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]
]);
}
}
else {
/**
* @psalm-suppress ArgumentTypeCoercion
*/
$uploaded_files[$name] = self::createFromGlobal($file);
}
}

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
Loading