Skip to content

Commit 538a6d4

Browse files
committed
server api tools added (request validator, response factory and exception transformer) (#2)
1 parent 87ca2d6 commit 538a6d4

19 files changed

+549
-3
lines changed

.github/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ consider package version as unsupported except last version.
1717

1818
## Usage
1919

20+
- [Api client](#api-client-service)
21+
- [Api server](#api-server-controller-tools)
22+
2023
### Api client service
2124

2225
Api client require guzzle client, psr client interface is not good enough because absence of async request.
@@ -84,3 +87,83 @@ catch (\SimpleAsFuck\ApiToolkit\Model\Client\ApiException $exception) {
8487
}
8588

8689
```
90+
91+
### Api server controller tools
92+
93+
For request handling is prepared Validator and Response factories.
94+
More information about validation rules you can find in
95+
[Simple as fuck / Php Validator](https://github.com/simple-as-fuck/php-validator) readme.
96+
97+
If you using symfony request and responses, you can use factories from different namespace, commented in example.
98+
99+
```php
100+
101+
// star of your action
102+
103+
$rules = \SimpleAsFuck\ApiToolkit\Factory\Server\Validator::make($request);
104+
//$rules = \SimpleAsFuck\ApiToolkit\Factory\Symfony\Validator::make($request);
105+
106+
// validate some query parameter
107+
$someQueryValidValue = $rules->query()->key('someKey')->string()->parseInt()->min(1)->notNull();
108+
109+
// validate something from request body with json format
110+
$someJsonValidValue = $rules->json()->object()->property('someProperty')->string()->notEmpty()->max(255)->notNull();
111+
112+
113+
114+
// end fo your action
115+
116+
/**
117+
* @var YourClass $yourModelForResponseBody
118+
* @var \SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer<YourClass> $transformer
119+
*/
120+
121+
// response with one object
122+
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson($yourModelForResponseBody, $transformer, \Kayex\HttpCodes::HTTP_OK);
123+
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson($yourModelForResponseBody, $transformer, \Kayex\HttpCodes::HTTP_OK);
124+
125+
// response with some array or collection (avoiding out of memory problem recommended some lazy loading iterator)
126+
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer);
127+
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJsonStream(new \ArrayIterator([$yourModelForResponseBody]), $transformer);
128+
129+
```
130+
131+
### Api server middleware tools
132+
133+
If anything go wrong you can use Exception transformers in your exception catching middleware or in some exception handler.
134+
135+
For laravel is prepared Laravel config adapter which load automatically configuration for ExceptionTransformer,
136+
you can easily get this transformer from DI, without any new configuration (standard configuration from Laravel is used).
137+
138+
```php
139+
140+
/**
141+
* @var \SimpleAsFuck\ApiToolkit\Service\Config\Repository $configRepository
142+
*/
143+
144+
try {
145+
// some breakable logic
146+
}
147+
catch(\SimpleAsFuck\ApiToolkit\Model\Server\ApiException $exception) {
148+
//catch(\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
149+
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson(
150+
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson(
151+
$exception,
152+
// transformer will convert exception in to json object with message property with original exception message
153+
new \SimpleAsFuck\ApiToolkit\Service\Server\ApiExceptionTransformer(),
154+
$exception->getStatusCode()
155+
);
156+
}
157+
catch (\Throwable $exception) {
158+
$response = \SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson(
159+
//$response = \SimpleAsFuck\ApiToolkit\Factory\Symfony\ResponseFactory::makeJson(
160+
$exception,
161+
// transformer will convert exception in to json object with message property
162+
// if application has turned off debug, message property contain only "Internal server error"
163+
// but with enabled debug message contains exception type, message, file and line where was exception thrown
164+
new \SimpleAsFuck\ApiToolkit\Service\Server\ExceptionTransformer($configRepository),
165+
\Kayex\HttpCodes::HTTP_INTERNAL_SERVER_ERROR
166+
);
167+
}
168+
169+
```

.github/workflows/main.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
- name: Run static analysis
2525
run: vendor/bin/phpstan analyse
2626

27+
- name: Run tests
28+
run: vendor/bin/phpunit test/
29+
2730
php81laraver8:
2831
name: "php 8.1, laravel 8"
2932
runs-on: ubuntu-latest
@@ -38,6 +41,9 @@ jobs:
3841
- name: Run static analysis
3942
run: vendor/bin/phpstan analyse
4043

44+
- name: Run tests
45+
run: vendor/bin/phpunit test/
46+
4147
php80:
4248
name: "php 8.0"
4349
runs-on: ubuntu-latest
@@ -52,6 +58,9 @@ jobs:
5258
- name: Run static analysis
5359
run: vendor/bin/phpstan analyse
5460

61+
- name: Run tests
62+
run: vendor/bin/phpunit test/
63+
5564
php74:
5665
name: "php 7.4"
5766
runs-on: ubuntu-latest
@@ -65,3 +74,6 @@ jobs:
6574

6675
- name: Run static analysis
6776
run: vendor/bin/phpstan analyse
77+
78+
- name: Run tests
79+
run: vendor/bin/phpunit test/

composer.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
"ext-json": "*",
66
"psr/http-message": "^1.0",
77
"psr/http-factory": "^1.0",
8-
"simple-as-fuck/php-validator": "^0.2.3",
8+
"simple-as-fuck/php-validator": "^0.2.5",
99
"guzzlehttp/guzzle": "^7.4",
1010
"illuminate/support": "^8.81|^9.0",
11-
"kayex/http-codes": "^1.1"
11+
"kayex/http-codes": "^1.1",
12+
"guzzlehttp/psr7": "^2.1",
13+
"symfony/psr-http-message-bridge": "^2.1",
14+
"symfony/http-kernel": "^5.4"
1215
},
1316
"require-dev": {
1417
"phpstan/phpstan": "^1.2",
15-
"friendsofphp/php-cs-fixer": "^3.4"
18+
"friendsofphp/php-cs-fixer": "^3.4",
19+
"phpunit/phpunit": "^9.5"
1620
},
1721
"autoload": {
1822
"psr-4": {"SimpleAsFuck\\ApiToolkit\\": "src/"}

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ parameters:
22
level: max
33
paths:
44
- src
5+
- test
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Server;
6+
7+
use SimpleAsFuck\Validator\Factory\Exception;
8+
9+
final class ApiValidationException extends Exception
10+
{
11+
public function create(string $message): \Exception
12+
{
13+
return new \SimpleAsFuck\ApiToolkit\Model\Server\ApiException($message, 400);
14+
}
15+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Server;
6+
7+
use GuzzleHttp\Psr7\HttpFactory;
8+
use GuzzleHttp\Psr7\PumpStream;
9+
use GuzzleHttp\Utils;
10+
use Kayex\HttpCodes;
11+
use Psr\Http\Message\ResponseFactoryInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
14+
15+
final class ResponseFactory
16+
{
17+
/**
18+
* @template TBody
19+
* @param TBody|null $body
20+
* @param Transformer<TBody>|null $transformer
21+
* @param int<100,505> $code
22+
* @param array<non-empty-string, string|array<string>> $headers
23+
*/
24+
public static function makeJson($body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = []): ResponseInterface
25+
{
26+
$factory = new HttpFactory();
27+
$response = self::makeResponse($factory, $code, $headers);
28+
29+
if ($body !== null) {
30+
if ($transformer) {
31+
$body = $transformer->toApi($body);
32+
}
33+
}
34+
return $response->withBody($factory->createStream(Utils::jsonEncode($body)));
35+
}
36+
37+
/**
38+
* @template TBody
39+
* @param \Iterator<TBody> $body
40+
* @param Transformer<TBody>|null $transformer
41+
* @param int<100,505> $code
42+
* @param array<non-empty-string, string|array<string>> $headers
43+
* @param float $speedLimit speed limit in KB/s for sending response slow down, zero means no slow down (this is not precise but is something)
44+
*/
45+
public static function makeJsonStream(\Iterator $body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = [], float $speedLimit = 0): ResponseInterface
46+
{
47+
$factory = new HttpFactory();
48+
$response = self::makeResponse($factory, $code, $headers);
49+
50+
$start = true;
51+
$previousItemTime = \microtime(true);
52+
return $response->withBody(new PumpStream(function (int $size) use ($body, $transformer, $speedLimit, &$start, &$previousItemTime): ?string {
53+
$item = '';
54+
if ($start) {
55+
$start = false;
56+
if (! $body->valid()) {
57+
return '[]';
58+
}
59+
$item = '[';
60+
}
61+
62+
if (! $body->valid()) {
63+
return null;
64+
}
65+
66+
$responseData = $body->current();
67+
if ($transformer) {
68+
$responseData = $transformer->toApi($responseData);
69+
}
70+
71+
$item .= Utils::jsonEncode($responseData);
72+
$body->next();
73+
if ($body->valid()) {
74+
$item .= ',';
75+
if ($speedLimit > 0) {
76+
$timeLimit = (strlen($item) / 1024) / $speedLimit;
77+
$timeOverHead = $timeLimit - (\microtime(true) - $previousItemTime);
78+
if ($timeOverHead > 0) {
79+
\usleep((int) ($timeOverHead * 1000000));
80+
}
81+
}
82+
} else {
83+
$item .= ']';
84+
}
85+
86+
$previousItemTime = \microtime(true);
87+
return $item;
88+
}));
89+
}
90+
91+
/**
92+
* @param int<100,505> $code
93+
* @param array<non-empty-string, string|array<string>> $headers
94+
*/
95+
private static function makeResponse(ResponseFactoryInterface $factory, int $code, array $headers): ResponseInterface
96+
{
97+
$response = $factory->createResponse($code);
98+
99+
foreach ($headers as $name => $header) {
100+
$response = $response->withHeader($name, $header);
101+
}
102+
103+
return $response;
104+
}
105+
}

src/Factory/Server/Validator.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Server;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use SimpleAsFuck\ApiToolkit\Model\Server\RequestRules;
9+
10+
final class Validator
11+
{
12+
public static function make(ServerRequestInterface $request): RequestRules
13+
{
14+
return new RequestRules(new ApiValidationException(), $request);
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Symfony;
6+
7+
use SimpleAsFuck\Validator\Factory\Exception;
8+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
9+
10+
final class BadRequestException extends Exception
11+
{
12+
public function create(string $message): \Exception
13+
{
14+
return new BadRequestHttpException($message);
15+
}
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Symfony;
6+
7+
use Kayex\HttpCodes;
8+
use SimpleAsFuck\ApiToolkit\Service\Transformation\Transformer;
9+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
10+
11+
final class ResponseFactory
12+
{
13+
/**
14+
* @template TBody
15+
* @param TBody|null $body
16+
* @param Transformer<TBody>|null $transformer
17+
* @param int<100,505> $code
18+
* @param array<non-empty-string, string|array<string>> $headers
19+
*/
20+
public static function makeJson($body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = []): \Symfony\Component\HttpFoundation\Response
21+
{
22+
$factory = new HttpFoundationFactory();
23+
return $factory->createResponse(\SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJson($body, $transformer, $code, $headers));
24+
}
25+
26+
/**
27+
* @template TBody
28+
* @param \Iterator<TBody> $body
29+
* @param Transformer<TBody>|null $transformer
30+
* @param int<100,505> $code
31+
* @param array<non-empty-string, string|array<string>> $headers
32+
* @param float $speedLimit speed limit in KB/s for sending response slow down, zero means no slow down (this is not precise but is something)
33+
*/
34+
public static function makeJsonStream(\Iterator $body, ?Transformer $transformer = null, int $code = HttpCodes::HTTP_OK, array $headers = [], float $speedLimit = 0): \Symfony\Component\HttpFoundation\Response
35+
{
36+
$factory = new HttpFoundationFactory();
37+
return $factory->createResponse(\SimpleAsFuck\ApiToolkit\Factory\Server\ResponseFactory::makeJsonStream($body, $transformer, $code, $headers, $speedLimit), true);
38+
}
39+
}

src/Factory/Symfony/Validator.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleAsFuck\ApiToolkit\Factory\Symfony;
6+
7+
use GuzzleHttp\Psr7\HttpFactory;
8+
use SimpleAsFuck\ApiToolkit\Model\Server\RequestRules;
9+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
10+
use Symfony\Component\HttpFoundation\Request;
11+
12+
final class Validator
13+
{
14+
public static function make(Request $request): RequestRules
15+
{
16+
$psrFactory = new HttpFactory();
17+
$factory = new PsrHttpFactory($psrFactory, $psrFactory, $psrFactory, $psrFactory);
18+
$request = $factory->createRequest($request);
19+
return new RequestRules(new BadRequestException(), $request);
20+
}
21+
}

0 commit comments

Comments
 (0)