odan / slim4-skeleton

A Slim 4 Skeleton
https://odan.github.io/slim4-skeleton/
MIT License
439 stars 80 forks source link

QueryParams as optional in #71

Closed esallum-iluminare closed 3 years ago

esallum-iluminare commented 3 years ago

Hi Odan! How are you? I want to implement in all GETs of my API the parameters Limit and Offset as optional. I have some questions regarding QueryParams: 1) Using good programming practices, I can implement Query Params in URI $app->get('/users[/{array}]', \App\Action\User\UserFindAction::class), for example, or the more correct way is to create another URI with another action (UserSearchAction, for example): $app->get('/users/search[/{array}]', \App\Action\User\UserFindAction::class) . 2) The validation of this array of parameters is done only in the Service?

Do you have any complete examples on your blog or in the book that I can follow of QueryParams and also a route that uses the PATCH method? I really appreciate your help, I'm a beginner programmer, but I want to do everything as correctly as possible.

PS: I bought your book and found it sensational, it surprised the expectations a lot. I looked for copper, but I found gold. Are there any communication channels (Telegram, Discord) for book buyers?

odan commented 3 years ago

Hi @esallum-iluminare I'm happy to help :-) Thank you for your great feedback about the eBook.

1) When you are creating an endpoint for a concrete resource (like a user id) I would create an route patter like GET /users/{id}.

Example URL: /users/123

$app->get('/users/{id}', \App\Action\User\UserReaderAction::class)

But when you have some kind of dynamic search inputs, a classic HTTP QueryString would be a better option because of the flexibility and because of some special chars that should not be in a URI path (for technical reasons).

Example URL: /users?firstname=Sally&lastname=Doe

$app->get('/users', \App\Action\User\UserFindAction::class)`

Then fetch the query parameters from the request object:

$params = $request->getQueryParams();

2) Yes, the validation belongs into the Service. Example

Do you have any complete examples on your blog or in the book that I can follow of QueryParams and also a route that uses the PATCH method?

Best practice here would be to follow the RESTful principles.

Are there any communication channels (Telegram, Discord) for book buyers?

Yes, for public questions you can use this GitHub issue page. I also have Discord, Skype, Teams. For private consultation, code reviews, training, etc... you can contact me here.

esallum-iluminare commented 3 years ago

Thanks again Odan! My code looked like this:

//routes
 $app->get('/users', \App\Action\User\UserFindAction::class);
//UserFindAction
 public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $params = $request->getQueryParams();
        // Optional: Pass parameters from the request to the findUsers method
        $users = $this->userFinder->findUsers($params);

        return $this->transform($response, $users);
    }
//UserFinder
    public function findUsers($params): array
    {
        $queryParams[] = [];
        if (!empty($params)){
            $queryParams = [
                'search_string' => $params['search_string'], 
                'limit' => intval($params['limit']), 
                'offset' => intval($params['offset']), 
            ];
        }

        return $this->repository->findUsers($queryParams);
    }
//UserFinderRepository
  public function findUsers($queryParams): array
    {
        $query = $this->queryFactory->newSelect('users');

        $query->select(
            [
                'id',
                'username',
                'first_name',
                'last_name',
                'email',
                'user_role_id',
                'locale',
                'enabled',
            ]
        );

        // Add more "use case specific" conditions to the query
        if (!empty($queryParams)) {
            $query->where([
                'OR' => [
                    'username LIKE' => '%' . $queryParams['search_string'] . '%',
                    'first_name LIKE' => '%' . $queryParams['search_string'] . '%',
                    'last_name LIKE' => '%' . $queryParams['search_string'] . '%'
                ]
            ])->limit($queryParams['limit'])->offset($queryParams['offset']);
        }

        $rows = $query->execute()->fetchAll('assoc') ?: [];

        // Convert to list of objects
        return $this->hydrator->hydrate($rows, UserData::class);
    }

Is there anything I can improve it?

odan commented 3 years ago

Looks good. Just a tip. When you have some clearly defined parameters, it would be better to declare them directly. Example:

public function findUsers(string $search, int $offset, int $limit): array
{
    $query = $this->queryFactory->newSelect('users');

    $query->select(
        [
            'id',
            'username',
            'first_name',
            'last_name',
            'email',
            'user_role_id',
            'locale',
            'enabled',
        ]
    );

    if ($search) {
        $query->andWhere(
            [
                'OR' => [
                    'username LIKE' => '%' . $search . '%',
                    'first_name LIKE' => '%' . $search . '%',
                    'last_name LIKE' => '%' . $search . '%',
                ],
            ]
        );
    }

    $query->limit($limit);
    $query->offset($offset);

    $rows = $query->execute()->fetchAll('assoc') ?: [];

    // Convert to list of objects
    return $this->hydrator->hydrate($rows, UserData::class);
}

Usage:

$search = $params['search_string'] ?? '';
$offset = $params['offset '] ?? 0;
$limit = $params['limit '] ?? 10;

return $this->repository->findUsers($search, $offset, $limit);
esallum-iluminare commented 3 years ago

Thanks @odan !