CrudControllerInterface is a simple contract that enforces consistent method signatures and HTTP status codes across API resource controllers. It's optional — nothing in the framework requires it — but using it keeps CRUD endpoints predictable and consistent across the codebase.
interface CrudControllerInterface
{
// POST /resource
public function create(): JsonResponse;
// GET /resource/{id}
public function read( int $id ): JsonResponse;
// GET /resource
public function readAll(): JsonResponse;
// PATCH /resource/{id}
public function update( int $id ): JsonResponse;
// PUT /resource/{id}
public function replace( int $id ): JsonResponse;
// DELETE /resource/{id}
public function delete( int $id ): JsonResponse;
}
| Method | Success | Not Found | Validation Error |
|---|---|---|---|
create() |
201 Created | — | 422 Unprocessable Entity |
read() |
200 OK | 404 Not Found | — |
readAll() |
200 OK | — | — |
update() |
200 OK | 404 Not Found | 422 Unprocessable Entity |
replace() |
200 OK | 404 Not Found | 422 Unprocessable Entity |
delete() |
204 No Content | 404 Not Found | — |
// App/Http/Controller/Api/PlayerController.php
namespace App\Http\Controller\Api;
use App\Abstraction\Interfaces\CrudControllerInterface;
use App\Entity\User;
use PHP_SF\Framework\Http\Middleware\auth;
use PHP_SF\System\Attributes\Route;
use PHP_SF\System\Classes\Abstracts\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
final class PlayerController extends AbstractController implements CrudControllerInterface
{
#[Route( url: 'api/players', httpMethod: 'POST', middleware: auth::class )]
public function create(): JsonResponse
{
$email = $this->request->request->get( 'email' );
$password = $this->request->request->get( 'password' );
if ( empty( $email ) || empty( $password ) )
return $this->unprocessableEntity( [
'errors' => [ 'Email and password are required.' ]
] );
$player = new User();
$player->setEmail( $email );
$player->setPassword( $password );
if ( $player->validate() !== true )
return $this->unprocessableEntity( [
'errors' => array_values( $player->getValidationErrors() )
] );
User::rep()->persist( $player );
return $this->created( $player->jsonSerialize() );
}
#[Route( url: 'api/players/{id}', httpMethod: 'GET', middleware: auth::class )]
public function read( int $id ): JsonResponse
{
$player = User::find( $id );
if ( $player === null )
return $this->notFound( [ 'error' => 'Player not found.' ] );
return $this->ok( $player->jsonSerialize() );
}
#[Route( url: 'api/players', httpMethod: 'GET', middleware: auth::class )]
public function readAll(): JsonResponse
{
return $this->ok(
array_map(
fn( User $player ) => $player->jsonSerialize(),
User::findAll()
)
);
}
#[Route( url: 'api/players/{id}', httpMethod: 'PATCH', middleware: auth::class )]
public function update( int $id ): JsonResponse
{
$player = User::find( $id );
if ( $player === null )
return $this->notFound( [ 'error' => 'Player not found.' ] );
if ( $email = $this->request->request->get( 'email' ) )
$player->setEmail( $email );
if ( $player->validate() !== true )
return $this->unprocessableEntity( [
'errors' => array_values( $player->getValidationErrors() )
] );
User::rep()->persist( $player );
return $this->ok( $player->jsonSerialize() );
}
#[Route( url: 'api/players/{id}', httpMethod: 'PUT', middleware: auth::class )]
public function replace( int $id ): JsonResponse
{
$player = User::find( $id );
if ( $player === null )
return $this->notFound( [ 'error' => 'Player not found.' ] );
$email = $this->request->request->get( 'email' );
$password = $this->request->request->get( 'password' );
if ( empty( $email ) || empty( $password ) )
return $this->unprocessableEntity( [
'errors' => [ 'Email and password are required for full replacement.' ]
] );
$player->setEmail( $email );
$player->setPassword( $password );
if ( $player->validate() !== true )
return $this->unprocessableEntity( [
'errors' => array_values( $player->getValidationErrors() )
] );
User::rep()->persist( $player );
return $this->ok( $player->jsonSerialize() );
}
#[Route( url: 'api/players/{id}', httpMethod: 'DELETE', middleware: auth::class )]
public function delete( int $id ): JsonResponse
{
$player = User::find( $id );
if ( $player === null )
return $this->notFound( [ 'error' => 'Player not found.' ] );
User::rep()->remove( $player );
return $this->noContent();
}
}
The interface distinguishes between update() (PATCH) and replace() (PUT) following REST semantics:
In practice many APIs implement only one or the other. If you don't need both, implement the unused method to return 405 Method Not Allowed:
public function replace( int $id ): JsonResponse
{
return $this->json(
[ 'error' => 'Method not allowed. Use PATCH for partial updates.' ],
JsonResponse::HTTP_METHOD_NOT_ALLOWED
);
}
Use CrudControllerInterface when:
Skip it when: