Closed Guyverix closed 3 years ago
Sure let's do a simple signup endpoint for example:
Let's add a signup route
routes.php
$app->post('/sign-up', SignUpUserAction::class);
Create src/Application/Validation
folder
Let's make an abstract validator so we can extend it for specific use cases like below. You'll need to install webmozart/assert
via composer for this code to work:
src/Application/Validation/Validator.php
<?php
declare(strict_types=1);
namespace App\Application\Validation;
use InvalidArgumentException;
abstract class Validator
{
/**
* @throws ValidationException
*/
public function validate(array $data): void
{
try {
$this->__validate($data);
} catch (InvalidArgumentException $exception) {
throw new ValidationException($exception->getMessage());
}
}
/**
* @throws InvalidArgumentException
*/
abstract public function __validate(array $data): void;
}
Named Validator Exception src/Application/Validation/ValidationException.php
<?php
declare(strict_types=1);
namespace App\Application\Validation;
use App\Application\Exception\ApplicationException;
class ValidationException extends ApplicationException
{
}
SignUp Validator src/Application/Validation/User/SignUpValidator.php
<?php
declare(strict_types=1);
namespace App\Application\Validation\User;
use App\Application\Validation\Validator;
use Webmozart\Assert\Assert;
class SignUpValidator extends Validator
{
public function __validate(array $data): void
{
Assert::stringNotEmpty($data['firstName'], 'Field `firstName` cannot be empty.');
Assert::maxLength($data['firstName'], 255, 'Field `firstName` be longer than 255 characters.');
Assert::stringNotEmpty($data['lastName'], 'Field `lastName` cannot be empty.');
Assert::maxLength($data['lastName'], 255, 'Field `lastName` cannot be longer than 255 characters.');
Assert::stringNotEmpty($data['username'], 'Field `username` cannot be empty.');
Assert::maxLength($data['username'], 255, 'Field `username` cannot be longer than 255 characters.');
}
}
Now let's modify the base action class to capture ValidationException
so it can rethrow it as an HttpBadRequestException
:
Action.php
src/Application/Actions/Action.php
<?php
declare(strict_types=1);
namespace App\Application\Actions;
use App\Domain\DomainException\DomainRecordNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
abstract class Action
{
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
/**
* @var array
*/
protected $args;
/**
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @param Request $request
* @param Response $response
* @param array $args
* @return Response
* @throws HttpNotFoundException
* @throws HttpBadRequestException
*/
public function __invoke(Request $request, Response $response, array $args): Response
{
$this->request = $request;
$this->response = $response;
$this->args = $args;
try {
return $this->action();
} catch (DomainRecordNotFoundException $e) {
throw new HttpNotFoundException($this->request, $e->getMessage());
} catch (ValidationException $e) {
throw new HttpBadRequestException($this->request, $e->getMessage());
}
}
// Replace the original getFormData() method with this one
protected function getFormData(): array
{
return $this->request->getParsedBody() ?? [];
}
}
SignUpAction: src/Application/Actions/User/SignUpAction.php
<?php
declare(strict_types=1);
namespace App\Application\Actions\User;
use App\Application\Validation\User\SignUpValidator;
use App\Domain\User\User;
use App\Domain\User\UserRepository;
use Psr\Http\Message\ResponseInterface as Response;
class SignUpAction extends UserAction
{
protected function action(): Response
{
$data = $this->getFormData();
$validator = new SignUpValidator();
$validator->validate($data);
// I'm not going to go into the persistence of the user here because
// I don't know what you're using to store stuff in your database
// If you're using an ORM this should be fairly easy though.
$user = new User(1, $data['firstName'], $data['lastName'], $data['username']);
$this->userRepository->store($user);
return $this->respondWithData($user);
}
}
A sample request would be:
POST /sign-up
Content-Type: application/json
{"firstName": "John", "lastName": "Doe", "username": "johndoe"}
ROFL! Thats amazing how you were able to knock that out so fast!
Much of what I have been doing is monkey-see, monkey-do. This will help me out a lot with the second part of my project. Using google I have been able to find help with the framework and GET but POSTs did not have as much documentation, and what I could find was not clear enough for a beginner.
I have about 10 API routes written with GETs that I have working the way I like, but need to get more complex with POSTs and just was not able to wrap my head around how the framework expected it to be done.
Thank you very much!
ROFL! Thats amazing how you were able to knock that out so fast!
It was easy to pull from some of my existing projects. Also, there's bits around storing entities not included for the sake of this example as it would get way too complicated but hopefully this is a good platform to start from.
Perhaps we should include this in the skeleton.
Thank you very much!
You're welcome 😄
I have added your examples, but running into errs about the class not existing. Assumed that SignUpUserAction is supposed to call SignUpAction class that was declared, but still not working. Any suggestions on how I can further debug this?
This is what I get no matter which class name I define in route.php: curl -X POST -i http://larvel01:8002/sign-up --data '{"firstName": "John", "lastName": "Doe", "username": "johndoe"}' HTTP/1.1 500 Internal Server Error Host: larvel01:8002 Date: Thu, 22 Apr 2021 21:11:41 GMT Connection: close X-Powered-By: PHP/7.4.16 Content-Type: application/json Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: * Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept, Origin, Authorization Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Cache-Control: no-store, no-cache, must-revalidate, max-age=0 Cache-Control: post-check=0, pre-check=0 Pragma: no-cache
{ "statusCode": 500, "error": { "type": "SERVER_ERROR", "description": "Callable SignUpAction does not exist" }
Found the start of my problem with the example. Needs a use
at the top of routes.php for SignUpAction.. If I figure out how I messed this up on the next part after this, I will update.
route.php: (added)
use App\Application\Actions\User\SignUpAction;
Guessing a collision between the $user defined in SignUpAction and the in memory users defined in the skeleton for the InMemoryUserRepository.php..
{ "statusCode": 500, "error": { "type": "SERVER_ERROR", "description": "Call to undefined method App\Infrastructure\Persistence\User\InMemoryUserRepository::store()" } }
Final update (promise), since I got it mostly working. At least enough to find out how to POST. But wanted to make sure you knew one other area that would need an update in the skeleton itself which is that InMemoryUserRepository.php gets called from the SignUpAction.php, and freaks out since there is no store() defined.
I defined it and just return what was passed ($user), however it messes up the data returned, but at least does a JSON return with data. It is just messed up on the order of data. the firstName defined in the array is returning the last name, etc. Guessing it is mixing with the existing in-memory store that was already part of the users example when defined like this:
public function store(): array { return array_values($this->users); }
But at least it is returning data for me :) I figured I should let you know about this behavior before you add POST examples into the main framework in the future and have to dig to find what is goofy.
Thank you again for giving me such a good example to use and learn from, I really appreciate your time on this.
InMemoryUserRepository.php gets called from the SignUpAction.php, and freaks out since there is no store() defined.
As I commented in the code, you would need to implement your storage solution. The InMemoryUserRepository
is just a placeholder for an ORM like Doctrine or whatever you want to use.
public function store(): array { return array_values($this->users); }
That function should store the new user entity in the storage. It'd be something like:
public function store(User $user): User
{
$this->users[] = $user;
return $user;
}
Thank you again for giving me such a good example to use and learn from, I really appreciate your time on this.
No problem. I am closing this issue as resolved.
I have been learning a lot by using your skeleton, however I am having some trouble with POST.
The GET makes sense, but since I am not a real developer, I am having a tough time making a POST without the application barfing on my ignorance. Any chance that a POST in the routes can be added with a simple/complete example?