Skip to content

Commit 0ff8387

Browse files
authored
Merge pull request #62 from dotkernel/issue-61
Find user by identity tutorial
2 parents 1e1dc1a + 0eb372c commit 0ff8387

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# A practical example: Find user by identity
2+
3+
## Our goal
4+
5+
Create a new endpoint that fetches a user record by its identity column.
6+
7+
We already have an endpoint that retrieves a user based on their UUID, so we can review it and create something similar.
8+
9+
## What we have
10+
11+
Let's print out all available endpoints using :
12+
13+
```shell
14+
php ./bin/cli.php route:list
15+
```
16+
17+
This command will list all available endpoints, which looks like this:
18+
19+
```text
20+
+--------+---------------------------------+--------------------------------+
21+
| Method | Name | Path |
22+
+--------+---------------------------------+--------------------------------+
23+
| POST | account.activate.request | /account/activate |
24+
| PATCH | account.activate | /account/activate/{hash} |
25+
| PATCH | account.modify-password | /account/reset-password/{hash} |
26+
.............................................................................
27+
.............................................................................
28+
.............................................................................
29+
| GET | user.my-avatar.view | /user/my-avatar |
30+
| GET | user.role.list | /user/role |
31+
| GET | user.role.view | /user/role/{uuid} |
32+
| PATCH | user.update | /user/{uuid} |
33+
| GET | user.view | /user/{uuid} |
34+
+--------+---------------------------------+--------------------------------+
35+
```
36+
37+
### Note
38+
39+
> **The above output is just an example.**
40+
>
41+
> More info about listing available endpoints can be found in `../commands/display-available-endpoints.md`.
42+
43+
The endpoint we're focusing on is the last one, `user.view`, so let's take a closer look at its functionality.
44+
45+
If we search for the route name `user.view` we will find its definition in the `src/User/src/RoutesDelegator.php` class, where all user related endpoints are found.
46+
47+
```php
48+
$app->get('/user/' . $uuid, UserHandler::class, 'user.view');
49+
```
50+
51+
Our route points to `get` method from `UserHandler` so let's navigate to that method.
52+
53+
```php
54+
public function get(ServerRequestInterface $request): ResponseInterface
55+
{
56+
$user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]);
57+
58+
return $this->createResponse($request, $user);
59+
}
60+
```
61+
62+
As we can see, the method will query the database for the user based on its uuid taken from the endpoint.
63+
64+
We now have an understanding of how things work and we can start to implement our own endpoint.
65+
66+
### Implementation
67+
68+
We need to create a new handler that will process our request, we can call it `IdentityHandler`.
69+
70+
Create a new PHP class called `IdentityHandler.php` in `src/User/src/Handler` folder.
71+
72+
```php
73+
<?php
74+
75+
declare(strict_types=1);
76+
77+
namespace Api\User\Handler;
78+
79+
use Api\App\Exception\BadRequestException;
80+
use Api\App\Exception\NotFoundException;
81+
use Api\App\Handler\HandlerTrait;
82+
use Api\App\Message;
83+
use Api\User\Entity\User;
84+
use Api\User\Service\UserServiceInterface;
85+
use Dot\DependencyInjection\Attribute\Inject;
86+
use Mezzio\Hal\HalResponseFactory;
87+
use Mezzio\Hal\ResourceGenerator;
88+
use Psr\Http\Message\ResponseInterface;
89+
use Psr\Http\Message\ServerRequestInterface;
90+
use Psr\Http\Server\RequestHandlerInterface;
91+
92+
use function sprintf;
93+
94+
class IdentityHandler implements RequestHandlerInterface
95+
{
96+
use HandlerTrait;
97+
98+
#[Inject(
99+
HalResponseFactory::class,
100+
ResourceGenerator::class,
101+
UserServiceInterface::class,
102+
)]
103+
public function __construct(
104+
protected HalResponseFactory $responseFactory,
105+
protected ResourceGenerator $resourceGenerator,
106+
protected UserServiceInterface $userService,
107+
) {
108+
}
109+
110+
/**
111+
* @throws NotFoundException
112+
* @throws BadRequestException
113+
*/
114+
public function get(ServerRequestInterface $request): ResponseInterface
115+
{
116+
$identity = $request->getAttribute('identity');
117+
if (empty($identity)) {
118+
throw (new BadRequestException())->setMessages([sprintf(Message::INVALID_VALUE, 'identity')]);
119+
}
120+
121+
$user = $this->userService->findByIdentity($identity);
122+
if (! $user instanceof User) {
123+
throw new NotFoundException(Message::USER_NOT_FOUND);
124+
}
125+
126+
return $this->createResponse($request, $user);
127+
}
128+
}
129+
```
130+
131+
Our handler is very similar to the existing one, with some extra steps:
132+
133+
* We store the identity from the request in the `$identity` variable for later use.
134+
* If the identity is empty we throw a `BadRequestException` with an appropriate message.
135+
* If we can't find the user in the database we throw an `NotFoundException`.
136+
* If the record is found, we generate and return the response.
137+
138+
The next step is to register the new handler.
139+
To do this go to `src/User/src/ConfigProvider.php`.
140+
In the `getDependencies()` method under the `factories` key add `IdentityHandler::class => AttributedServiceFactory::class,`
141+
142+
Next, create the route in `src/User/src/RoutesDelegator.php`:
143+
144+
```php
145+
$app->get(
146+
'/user/{identity}',
147+
IdentityHandler::class,
148+
'user.view.identity'
149+
);
150+
```
151+
152+
### Note
153+
154+
> Make sure to register the endpoint as the last one to not shadow existing endpoints.
155+
156+
The last step is to set permissions on the newly created route.
157+
158+
Go to `config/autoload/authorization.global.php` and add our route name (`user.view.identity`) under the `UserRole::ROLE_GUEST` key
159+
This will give access to every user, including guests to view other accounts. (for the sake of simplicity)
160+
161+
### Writing tests
162+
163+
Because every new piece of code should be tested we will write some tests for this endpoint also.
164+
165+
In the `test/Functional` folder create a new php class `IdentityTest.php`:
166+
167+
```php
168+
<?php
169+
170+
namespace ApiTest\Functional;
171+
172+
use Api\App\Message;
173+
174+
class IdentityTest extends AbstractFunctionalTest
175+
{
176+
public function testEmptyIdentityReturnsNotFound(): void
177+
{
178+
$response = $this->get('/user/');
179+
180+
$this->assertResponseNotFound($response);
181+
}
182+
183+
public function testInvalidIdentityReturnsNotFound(): void
184+
{
185+
$response = $this->get('/user/invalid_identity');
186+
$messages = json_decode($response->getBody()->getContents(), true);
187+
188+
$this->assertResponseNotFound($response);
189+
$this->assertNotEmpty($messages);
190+
$this->assertIsArray($messages);
191+
$this->assertNotEmpty($messages['error']['messages'][0]);
192+
$this->assertIsString($messages['error']['messages'][0]);
193+
$this->assertSame(Message::USER_NOT_FOUND, $messages['error']['messages'][0]);
194+
}
195+
196+
public function testValidIdentityReturnsUser(): void
197+
{
198+
$this->createUser([
199+
'identity' => 'valid_user',
200+
]);
201+
202+
$response = $this->get('/user/valid_user');
203+
204+
$this->assertResponseOk($response);
205+
$user = json_decode($response->getBody()->getContents(), true);
206+
207+
$this->assertSame('valid_user', $user['identity']);
208+
}
209+
}
210+
```
211+
212+
Planning and coding a new feature can be challenging at times, but reviewing our existing code or tutorials can serve as a source of inspiration.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ nav:
4343
- "Creating a book module": v5/tutorials/create-book-module.md
4444
- "Token authentication": v5/tutorials/token-authentication.md
4545
- "API Evolution": v5/tutorials/api-evolution.md
46+
- "Find user by identity": v5/tutorials/find-user-by-identity.md
4647
- Transition from API Tools:
4748
- "Laminas API Tools vs DotKernel API": v5/transition-from-api-tools/api-tools-vs-dotkernel-api.md
4849
- "Transition Approach": v5/transition-from-api-tools/transition-approach.md

0 commit comments

Comments
 (0)