Skip to content

Commit 21d2bd1

Browse files
committed
Start writing best practices
1 parent e27d31f commit 21d2bd1

File tree

3 files changed

+235
-2
lines changed

3 files changed

+235
-2
lines changed

docs/04. Using HTTP exceptions for reporting errors.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ return [
7575

7676
### Navigation
7777

78-
* Back to [the Introduction](/docs/03. View layer.md)
78+
* Continue to [**Best practices**](/docs/05. Best practices.md)
79+
* Back to [the View Layer](/docs/03. View layer.md)
7980
* Back to [the Index](/docs/README.md)

docs/05. Best practices.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Best Practices
2+
3+
This section will show you various best practices that I've learn while writing several REST API, using ZfrRest.
4+
5+
## Be pragmatic
6+
7+
I know a lot of people will disagree with that, but you should be pragmatic when using your API. HATEOAS is nice,
8+
but it's actually not really needed (whenever I use an API, I never ever used the URL attributes of such APIs,
9+
because the workflow of which method to call is usually known in advance).
10+
11+
If being 100% REST compliant takes too much time or is not useful for your use case, don't lose time on that and
12+
go on.
13+
14+
[This link](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api) contains a lot of best practices
15+
when designing an API.
16+
17+
## Use nested URIs
18+
19+
ZfrRest allows you to nest URLs, for accessing the same resource based on different contexts. For instance, if you
20+
have tweets, you can either access all tweets (`/tweets`) but also access all tweets from a given user (`/users/1/tweets`).
21+
22+
With the template feature of ZfrRest, you do not have to write rendering code multiple times. Instead, you will use
23+
the exact same template for the two cases, except with different set of data.
24+
25+
## List and resource controllers
26+
27+
You have to understand that you will typically have two controllers per resource: the list controller and the
28+
item controller. You should have a coherent naming that you use throughout your application (for instance you
29+
could add the `List` word for list controllers: `TweetController` and `TweetListController`).
30+
31+
This table will recap you the type of controller based on URI and verb:
32+
33+
URI | Verb | Type
34+
------------ | ------------- | ------------
35+
/tweets | POST | List controller
36+
/tweets | GET | List controller
37+
/tweets | PUT | N/A
38+
/tweets | DELETE | N/A
39+
/tweets/1 | POST | N/A
40+
/tweets/1 | GET | Item controller
41+
/tweets/1 | PUT | Item controller
42+
/tweets/1 | DELETE | Item controller
43+
44+
## Simplify your controllers
45+
46+
Controllers are known to be hard to test (you have to mock requests...). Instead, all your controllers must be kept
47+
very simple. To do that, you should use a service layer. In ZfrRest, here is how your controller typically look like.
48+
49+
* List controller
50+
51+
```php
52+
class TweetListController extends AbstractRestfulController
53+
{
54+
private $tweetService;
55+
56+
public function __construct(TweetService $tweetService)
57+
{
58+
$this->tweetService = $tweetService;
59+
}
60+
61+
/**
62+
* Create a new tweet
63+
*/
64+
public function post()
65+
{
66+
$data = $this->validateIncomingData(TweetInputFilter::class);
67+
$tweet = $this->hydrateObject(TweetHydrator::class, new Tweet(), $data);
68+
69+
$tweet = $this->tweetService->create($tweet);
70+
71+
return new ResourceViewModel(['tweet' => $tweet], ['template' => 'tweets/tweet']);
72+
}
73+
74+
/**
75+
* Get all the tweets
76+
*/
77+
public function get()
78+
{
79+
// Assuming getAll return a paginator
80+
$tweets = $this->tweetService->getAll();
81+
$tweets->setCurrentPageNumber($this->params()->fromQuery('page', 1);
82+
83+
return new ResourceViewModel(['tweets' => $tweets], ['template' => 'tweets']);
84+
}
85+
}
86+
```
87+
88+
* Item controller
89+
90+
```php
91+
class TweetController extends AbstractRestfulController
92+
{
93+
private $tweetService;
94+
95+
public function __construct(TweetService $tweetService)
96+
{
97+
$this->tweetService = $tweetService;
98+
}
99+
100+
/**
101+
* Get an existing tweet
102+
*/
103+
public function get(array $params)
104+
{
105+
if (!$tweet = $this->tweetService->getById($params['tweet_id'])) {
106+
throw new NotFoundException();
107+
}
108+
109+
return new ResourceViewModel(['tweet' => $tweet], ['template' => 'tweets/tweet']);
110+
}
111+
112+
/**
113+
* Update an existing tweet
114+
*/
115+
public function put(array $params)
116+
{
117+
if (!$tweet = $this->tweetService->getById($params['tweet_id'])) {
118+
throw new NotFoundException();
119+
}
120+
121+
$data = $this->validateIncomingData(TweetInputFilter::class);
122+
$tweet = $this->hydrateObject(TweetHydrator::class, $tweet, $data);
123+
124+
$tweet = $this->tweetService->update($tweet);
125+
126+
return new ResourceViewModel(['tweet' => $tweet], ['template' => 'tweets/tweet']);
127+
}
128+
129+
/**
130+
* Delete an existing tweet
131+
*/
132+
public function delete(array $params)
133+
{
134+
if (!$tweet = $this->tweetService->getById($params['tweet_id'])) {
135+
throw new NotFoundException();
136+
}
137+
138+
$this->tweetService->delete($tweet);
139+
140+
return new JsonModel();
141+
}
142+
}
143+
```
144+
145+
## Paginate data
146+
147+
To paginate data, you need to create an instance of `Zend\Paginator\Paginator`. DoctrineModule and DoctrineORMModule
148+
offers two adapters for Doctrine, that you can use depending on the use case: `DoctrineModule\Paginator\Adapter\Selectable`
149+
and `DoctrineORMModule\Paginator\Adapter\DoctrinePaginator`.
150+
151+
We recommend you to create the paginator in your repositories (or your services). Two cases can happen:
152+
153+
1. If you do not need a custom query (for instance if you only want to fetch the tweets, without any join or whatever),
154+
you can use the `Selectable` adapter, because repositories implement the `Selectable` interface. For instance, in your
155+
service:
156+
157+
```php
158+
use DoctrineModule\Paginator\Adapter\Selectable as SelectableAdapter;
159+
use Zend\Paginator\Paginator;
160+
161+
class TweetService
162+
{
163+
private $tweetRepository;
164+
165+
public function getAll()
166+
{
167+
return new Paginator(new SelectableAdapter($this->tweetRepository));
168+
}
169+
}
170+
```
171+
172+
> You can optionally pass a criteria object for filtering, as we'll see later.
173+
174+
2. If you need custom query, this approach is not flexible enough. We recommend you in this case to create a
175+
custom repository, that will create the query, and wrap it under a paginator:
176+
177+
First, your service now delegate to the repository:
178+
179+
```php
180+
use DoctrineModule\Paginator\Adapter\Selectable as SelectableAdapter;
181+
use Zend\Paginator\Paginator;
182+
183+
class TweetService
184+
{
185+
private $tweetRepository;
186+
187+
public function getAll()
188+
{
189+
return new $this->tweetRepository->findAll();
190+
}
191+
}
192+
```
193+
194+
While your TweetRepository create the custom query, and wrap it around a Paginator:
195+
196+
```php
197+
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
198+
use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrinePaginatorAdapter;
199+
use Zend\Paginator\Paginator;
200+
201+
class TweetRepository extends EntityRepository
202+
{
203+
public function findAll()
204+
{
205+
$queryBuilder = $this->createQueryBuilder('tweet');
206+
$queryBuilder->select('author')
207+
->join('tweet.author', 'author');
208+
209+
$doctrinePaginator = new DoctrinePaginator($queryBuilder);
210+
return new Paginator(new DoctrinePaginatorAdapter($doctrinePaginator));
211+
}
212+
}
213+
```
214+
215+
## Filtering data
216+
217+
When retrieving data, you will often need to filter the result set through query parameters. One approach to do
218+
that is to extract the query params right into your controllers, and passing each filters to your service, which
219+
in turn will do the right query.
220+
221+
However, we definitely do not want to tie your controller to query params parsing. Once again, two cases can happen:
222+
223+
* You have simple filtering needs, when you only need to filter the fields of the entity you are fetching (for instance,
224+
filtering by first name, last name or age when retrieving a list of users).
225+
* You have more complex filtering needs, when you can possibly also filter by properties of associations (for instance,
226+
you want to filter by author first name when retrieving a list of tweets).
227+
228+
229+
* Back to [**Using HTTP exceptions for reporting errors**](/docs/04. Using HTTP exceptions for reporting errors.md)
230+
* Back to [the Index](/docs/README.md)

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ If you are looking for some information that is not listed in the documentation,
2020

2121
4. [Using HTTP exceptions for reporting errors](/docs/04. Using HTTP exceptions for reporting errors.md)
2222
1. [Built-in exceptions](/docs/04. Using HTTP exceptions for reporting errors.md#built-in-exceptions)
23-
2. [Mapping custom exceptions](/docs/04. Using HTTP exceptions for reporting errors.md#mapping-custom-exceptions)
23+
2. [Mapping custom exceptions](/docs/04. Using HTTP exceptions for reporting errors.md#mapping-custom-exceptions)
24+
25+
5. [Best Practices](/docs/05. Best practices.md)

0 commit comments

Comments
 (0)