Add a New REST Method
In this tutorial, we'll create a new REST method to update the status of the example_crud table.
We'll cover the following topics:
- OpenAPI Attributes
- Protecting the endpoint
- Validating input
- Saving to the database
- Returning results
- Unit testing
OpenAPI Attributes
First, we'll add OpenAPI attributes to our REST method using the zircote/swagger-php library.
The OpenAPI specification offers many attributes. At minimum, define these three essential sets:
1. Method Attribute
This defines the HTTP method:
OA\Get- For retrieving dataOA\Post- For creating dataOA\Put- For updating dataOA\Delete- For deleting/canceling data
Example:
#[OA\Put(
path: "/example/crud/status",
security: [
["jwt-token" => []]
],
tags: ["Example"],
description: "Update the status of the ExampleCrud"
)]
The security attribute defines the security schema. Without it, the endpoint remains public.
2. Request Attribute
This defines the input to the method using OA\RequestBody or OA\Parameter.
Example:
#[OA\RequestBody(
description: "The status to be updated",
required: true,
content: new OA\JsonContent(
required: ["status"],
properties: [
new OA\Property(property: "id", type: "integer", format: "int32"),
new OA\Property(property: "status", type: "string")
]
)
)]
3. Response Attribute
This defines the expected output using OA\Response.
#[OA\Response(
response: 200,
description: "The operation result",
content: new OA\JsonContent(
required: ["result"],
properties: [
new OA\Property(property: "result", type: "string")
]
)
)]
Place these attributes at the beginning of your method. Following our pattern, we'll add this method at the end of the ExampleCrudRest class:
#[OA\Put()] // complete with the attributes above
#[OA\RequestBody()] // complete with the attributes above
#[OA\Response()] // complete with the attributes above
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Code to be added
}
Protecting the Endpoint
If you've set the security property in your OpenAPI attributes, protect the endpoint using attributes:
<?php
use RestReferenceArchitecture\Attributes\RequireAuthenticated;
use RestReferenceArchitecture\Attributes\RequireRole;
use RestReferenceArchitecture\Attributes\ValidateRequest;
use RestReferenceArchitecture\Model\User;
// Option a: Require admin role
#[RequireRole(User::ROLE_ADMIN)]
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Your code here
}
// Option b: Require any authenticated user
#[RequireAuthenticated]
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Your code here
}
// Option c: Public endpoint (no attribute)
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Your code here
}
Using attributes is cleaner and more declarative than manual JWT validation. The attributes automatically:
- Validate JWT tokens
- Check user roles
- Throw appropriate exceptions (401 for invalid token, 403 for insufficient permissions)
Access JWT Data
If you need to access the current user's data:
<?php
use RestReferenceArchitecture\Util\JwtContext;
// JwtContext helpers expose the middleware-populated JWT information
$userId = JwtContext::getUserId();
$userRole = JwtContext::getRole(); // "admin" or "user"
Validating Input
Use the #[ValidateRequest] attribute to automatically validate the incoming request against your OpenAPI specification:
<?php
#[RequireAuthenticated]
#[ValidateRequest]
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Get the validated payload
$payload = ValidateRequest::getPayload();
// The payload is already validated against your OpenAPI spec
// If validation fails, an Error400Exception is thrown automatically
}
Updating Status Using the Service Layer
After validating the payload, use the service layer to update the record:
<?php
use ByJG\Config\Config;
use RestReferenceArchitecture\Service\ExampleCrudService;
use RestReferenceArchitecture\Attributes\RequireAuthenticated;
use RestReferenceArchitecture\Attributes\ValidateRequest;
/**
* Update the status of an Example CRUD record
*/
#[RequireAuthenticated]
#[ValidateRequest]
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
$payload = ValidateRequest::getPayload();
// Use the service layer for business logic
$service = Config::get(ExampleCrudService::class);
$model = $service->getOrFail($payload['id']);
$model->setStatus($payload['status']);
$service->save($model);
$response->write(['result' => 'ok']);
}
Always use the Service Layer instead of directly accessing repositories. Services handle business logic and make your code more maintainable.
Returning the Response
After updating the record, we need to return a standardized response as specified in our OpenAPI schema:
public function putExampleCrudStatus(HttpResponse $response, HttpRequest $request)
{
// Previous code for update logic...
// Return standardized response
$response->write([
"result" => "ok"
]);
}
Functional Testing
Create a functional test to ensure your endpoint works correctly and continues to function as expected.
Create or update the test file tests/Rest/ExampleCrudTest.php:
<?php
namespace Test\Rest;
use ByJG\Config\Config;
use RestReferenceArchitecture\Service\ExampleCrudService;
use RestReferenceArchitecture\Util\FakeApiRequester;
use Test\Rest\BaseApiTestCase;
use Test\Rest\Credentials;
class ExampleCrudTest extends BaseApiTestCase
{
public function testUpdateStatus()
{
// Authenticate to get a valid token (if the endpoint requires auth)
$authResult = json_decode(
$this->assertRequest(Credentials::requestLogin(Credentials::getAdminUser()))
->getBody()
->getContents(),
true
);
// Prepare test data
$recordId = 1;
$newStatus = 'active';
// Create mock API request
$request = new FakeApiRequester();
$request
->withPsr7Request($this->getPsr7Request())
->withMethod('PUT')
->withPath("/example/crud/status")
->withBody([
'id' => $recordId,
'status' => $newStatus
])
->withRequestHeader([
"Authorization" => "Bearer " . $authResult['token']
])
->assertResponseCode(200);
// Execute the request
// assertRequest automatically validates response against OpenAPI spec
$this->assertRequest($request);
// Verify the database was updated correctly
$service = Config::get(ExampleCrudService::class);
$updatedRecord = $service->get($recordId);
$this->assertEquals($newStatus, $updatedRecord->getStatus());
}
}
The assertRequest() method automatically validates:
- Response status code matches OpenAPI specification
- Response body structure matches OpenAPI schema
- No need for manual assertions on response format!
Run the Tests
Update the OpenAPI specification and run the tests:
composer run openapi
APP_ENV=test composer run test
All tests should pass successfully!