Skip to content

Commit fecaf05

Browse files
committed
init
1 parent 85243e8 commit fecaf05

26 files changed

+1240
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor
2+
cache
3+
logs
4+
composer.lock

.travis.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
language: php
2+
3+
php:
4+
- 7.1
5+
- 7.2
6+
7+
before_script:
8+
- composer install --no-interaction
9+
10+
script:
11+
- ./vendor/bin/phpunit

README.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
Symfony Request Objects
2+
===========================
3+
4+
[![Build Status](https://travis-ci.org/fesor/request-objects.svg?branch=master)](https://travis-ci.org/fesor/request-objects)
5+
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fesor/request-objects/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/fesor/request-objects/?branch=master)
6+
[![Latest Stable Version](https://poser.pugx.org/fesor/request-objects/v/stable)](https://packagist.org/packages/fesor/request-objects)
7+
[![Total Downloads](https://poser.pugx.org/fesor/request-objects/downloads)](https://packagist.org/packages/fesor/request-objects)
8+
[![License](https://poser.pugx.org/fesor/request-objects/license)](https://packagist.org/packages/fesor/request-objects)
9+
10+
**Note**: This library should not be considered as production ready until 1.0 release.
11+
Please provide your feedback to make it happen!
12+
13+
## Why?
14+
15+
Symfony Forms component is a very powerful tool for handling forms. But nowadays things have changed.
16+
Complex forms are handled mostly on the client side. As for simple forms `symfony/forms` has very large overhead.
17+
18+
And in some cases you just don't have forms. For example, if you are developing an HTTP API, you probably just
19+
need to interact with request payload. So why not just wrap request payload in some user defined object and
20+
validate just it? This also encourages separation of concerns and will help you in case of API versioning.
21+
22+
## Usage
23+
24+
First of all, we need to install this package via composer:
25+
26+
```
27+
composer require fesor/request-objects
28+
```
29+
30+
And register the bundle:
31+
32+
```
33+
public function registerBundles()
34+
{
35+
$bundles = [
36+
// ...
37+
new \Fesor\RequestObject\Bundle\RequestObjectBundle(),
38+
];
39+
}
40+
```
41+
42+
Bundle doesn't require any additional configuration, but you could also specify an error response
43+
provider service in bundle config. We will come back to this in "Handle validation errors" section.
44+
45+
### Define your request objects
46+
47+
All user defined requests should extend `Fesor\RequestObject\RequestObject`. Let's create a simple
48+
request object for user registration action:
49+
50+
```php
51+
use Fesor\RequestObject\RequestObject;
52+
use Symfony\Component\Validator\Constraints as Assert;
53+
54+
class RegisterUserRequest extends RequestObject
55+
{
56+
public function rules()
57+
{
58+
return new Assert\Collection([
59+
'email' => new Assert\Email(['message' => 'Please fill in valid email']),
60+
'password' => new Assert\Length(['min' => 4, 'minMessage' => 'Password is to short']),
61+
'first_name' => new Assert\NotNull(['message' => 'Please provide your first name']),
62+
'last_name' => new Assert\NotNull(['message' => 'Please provide your last name'])
63+
]);
64+
}
65+
}
66+
```
67+
68+
After that we can just use it in our action:
69+
70+
```php
71+
public function registerUserAction(RegisterUserRequest $request)
72+
{
73+
// Do Stuff! Data is already validated!
74+
}
75+
```
76+
77+
This bundle will bind validated request object to the `$request` argument. Request object has very simple interface
78+
for data interaction. It is very similar to Symfony's request object but is considered immutable by default (although you
79+
can add some setters if you wish so)
80+
81+
```php
82+
// returns value from payload by specific key or default value if provided
83+
$request->get('key', 'default value');
84+
85+
// returns whole payload
86+
$request->all();
87+
```
88+
89+
### Where payload comes from?
90+
91+
This library has default implementation of `PayloadResolver` interface, which acts this way:
92+
93+
1) If a request can have a body (i.e. it is POST, PUT, PATCH or whatever request with body)
94+
it uses union of `$request->request->all()` and `$request->files->all()` arrays as payload.
95+
96+
2) If request can't have a body (i.e. GET, HEAD verbs), then it uses `$request->query->all()`.
97+
98+
If you wish to apply custom logic for payload extraction, you could implement `PayloadResolver` interface within
99+
your request object:
100+
101+
```php
102+
class CustomizedPayloadRequest extends RequestObject implements PayloadResolver
103+
{
104+
public function resolvePayload(Request $request)
105+
{
106+
$query = $request->query->all();
107+
// turn string to array of relations
108+
if (isset($query['includes'])) {
109+
$query['includes'] = explode(',', $query['includes']);
110+
}
111+
112+
return $query;
113+
}
114+
}
115+
```
116+
117+
This will allow you to do some crazy stuff with your requests and DRY a lot of stuff.
118+
119+
120+
### Validating payload
121+
122+
As you can see from previous example, the `rules` method should return validation rules for [symfony validator](http://symfony.com/doc/current/book/validation.html).
123+
Your request payload will be validated against it and you will get valid data in your action.
124+
125+
If you have some validation rules which depend on payload data, then you can handle it via validation groups.
126+
127+
**Please note**: due to limitations in `Collection` constraint validator it is not so handy to use groups.
128+
So instead it is recommended to use `Callback` validator in tricky cases with dependencies on payload data.
129+
See [example](examples/Request/ContextDependingRequest.php) for details about problem.
130+
131+
You may provide validation group by implementing `validationGroup` method:
132+
133+
```php
134+
public function validationGroup(array $payload)
135+
{
136+
return isset($payload['context']) ?
137+
['Default', $payload['context']] : null;
138+
}
139+
```
140+
141+
### Handling validation errors
142+
143+
If validated data is invalid, library will throw exception which will contain validation errors and request object.
144+
145+
But if you don't want to handle it via `kernel.exception` listener, you have several options.
146+
147+
First is to use your controller action to handle errors:
148+
149+
```php
150+
151+
public function registerUserAction(RegisterUserRequest $request, ConstraintViolationList $errors)
152+
{
153+
if (0 !== count($errors)) {
154+
// handle errors
155+
}
156+
}
157+
158+
```
159+
160+
But this not so handy and will break DRY if you just need to return common error response. Thats why
161+
library provides you `ErrorResponseProvider` interface. You can implement it in your request object and move this
162+
code to `getErrorResponse` method:
163+
164+
```php
165+
public function getErrorResponse(ConstraintViolationListInterface $errors)
166+
{
167+
return new JsonResponse([
168+
'message' => 'Please check your data',
169+
'errors' => array_map(function (ConstraintViolation $violation) {
170+
171+
return [
172+
'path' => $violation->getPropertyPath(),
173+
'message' => $violation->getMessage()
174+
];
175+
}, iterator_to_array($errors))
176+
], 400);
177+
}
178+
```
179+
180+
## More examples
181+
182+
If you're still not sure is it useful for you, please see the `examples` directory for more use cases.
183+
Didn't find your case? Then share your use case in issues!
184+
185+
## Contribution
186+
187+
Feel free to give feedback and feature requests or post issues. PR's are welcomed!

composer.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "fesor/request-objects",
3+
"description": "Custom request objects for Symfony made to make life less painful",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Sergey Protko",
9+
"email": "fesors@gmail.com"
10+
}
11+
],
12+
"autoload": {
13+
"psr-4": {
14+
"Fesor\\RequestObject\\": "src/"
15+
}
16+
},
17+
"autoload-dev": {
18+
"psr-4": {
19+
"Fesor\\RequestObject\\Examples\\": "examples/"
20+
}
21+
},
22+
"minimum-stability": "stable",
23+
"require": {
24+
"php": ">=7.1.0",
25+
"symfony/http-foundation": "^4.4|^5.0",
26+
"symfony/validator": "5.1.*",
27+
},
28+
"require-dev": {
29+
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
30+
"symfony/framework-bundle": "5.1.*",
31+
"symfony/var-dumper": "^4.4|^5.0"
32+
}
33+
}

examples/App/AppController.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Fesor\RequestObject\Examples\App;
4+
5+
use Fesor\RequestObject\Examples\Request\ContextDependingRequest;
6+
use Fesor\RequestObject\Examples\Request\ExtendedRegisterUserRequest;
7+
use Fesor\RequestObject\Examples\Request\RegisterUserRequest;
8+
use Fesor\RequestObject\Examples\Request\ResponseProvidingRequest;
9+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
10+
use Symfony\Component\HttpFoundation\JsonResponse;
11+
use Symfony\Component\HttpFoundation\Response;
12+
use Symfony\Component\Validator\ConstraintViolationList;
13+
14+
class AppController extends Controller
15+
{
16+
public function registerUserAction(RegisterUserRequest $request)
17+
{
18+
return new JsonResponse($request->all(), 201);
19+
}
20+
21+
public function registerUserCustomAction(ExtendedRegisterUserRequest $request)
22+
{
23+
return new JsonResponse($request->all(), 201);
24+
}
25+
26+
public function withErrorResponseAction(ResponseProvidingRequest $request)
27+
{
28+
return new JsonResponse($request->all(), 201);
29+
}
30+
31+
public function contextDependingRequestAction(ContextDependingRequest $request)
32+
{
33+
return new JsonResponse($request->all(), 201);
34+
}
35+
36+
public function noCustomRequestAction()
37+
{
38+
return new Response(null, 204);
39+
}
40+
41+
public function validationResultsAction(RegisterUserRequest $request, ConstraintViolationList $errors)
42+
{
43+
return new Response(count($errors), 200, ['Content-Type' => 'text/plain']);
44+
}
45+
}

examples/App/AppKernel.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Fesor\RequestObject\Examples\App;
4+
5+
use Fesor\RequestObject\Bundle\RequestObjectBundle;
6+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
7+
use Symfony\Component\Config\Loader\LoaderInterface;
8+
use Symfony\Component\HttpKernel\Kernel;
9+
10+
class AppKernel extends Kernel
11+
{
12+
public function registerBundles()
13+
{
14+
return [
15+
new FrameworkBundle(),
16+
new RequestObjectBundle(),
17+
];
18+
}
19+
20+
public function registerContainerConfiguration(LoaderInterface $loader)
21+
{
22+
$loader->load($this->getRootDir().'/config.yml');
23+
}
24+
}

examples/App/config.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
parameters:
2+
locale: en
3+
kernel.secret: "example"
4+
5+
framework:
6+
test: ~
7+
secret: "%kernel.secret%"
8+
session:
9+
storage_id: session.storage.mock_file
10+
profiler:
11+
collect: false
12+
router:
13+
resource: "%kernel.root_dir%/routing.yml"
14+
strict_requirements: ~
15+
validation:
16+
enabled: true
17+
18+
services:
19+
app_controller:
20+
class: 'Fesor\RequestObject\Examples\App\AppController'
21+
public: true

examples/App/routing.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
register_user:
2+
path: /users
3+
defaults: {_controller: 'app_controller:registerUserAction'}
4+
5+
register_user_extended:
6+
path: /users_extended
7+
defaults: {_controller: 'app_controller:registerUserCustomAction'}
8+
9+
error_response:
10+
path: /error_response
11+
defaults: {_controller: 'app_controller:withErrorResponseAction'}
12+
13+
context_depending:
14+
path: /context_depending
15+
defaults: {_controller: 'app_controller:contextDependingRequestAction'}
16+
17+
no_custom_request:
18+
path: /no_request
19+
defaults: {_controller: 'app_controller:noCustomRequestAction'}
20+
21+
validation_results:
22+
path: /validation_results
23+
defaults: {_controller: 'app_controller:validationResultsAction'}

0 commit comments

Comments
 (0)