Essentials

Requests & Responses

Read request data and return consistent JSON responses

Glueful controllers work with Symfony Request objects and Glueful Response helpers. For anything beyond a quick endpoint, prefer typed request and response DTOs: they validate input, shape output, and drive the generated OpenAPI spec.

Reading Request Data

In controllers that extend BaseController, use the helper methods that already exist:

use Symfony\Component\HttpFoundation\Request;

// Route: $router->put('/posts/{id}', [PostController::class, 'update'])
public function update(Request $request, int $id)  // route params bind by name
{
    $data = $this->getRequestData();               // JSON or form body, as an array

    $page = (int) $request->query->get('page', 1); // query string
    $token = $request->headers->get('Authorization');
    $params = $request->attributes->get('_route_params'); // all route params
}

JSON Requests

Symfony does not decode JSON bodies into $request->request — that bag only holds form-encoded / multipart fields. BaseController::getRequestData() detects Content-Type: application/json and returns the parsed array; outside a controller use Glueful\Helpers\RequestHelper::getRequestData($request):

public function store(Request $request)
{
    $data = $this->getRequestData();

    return $this->success([
        'received' => $data,
    ]);
}

Files

public function upload(Request $request)
{
    $file = $request->files->get('avatar');

    if ($file === null) {
        return Response::validation([
            'avatar' => ['Avatar is required.'],
        ]);
    }

    return Response::success([
        'filename' => $file->getClientOriginalName(),
    ]);
}

Typed Request DTOs

A handler parameter whose type implements Glueful\Validation\Contracts\RequestData is hydrated from the request, validated with its #[Rule] attributes, and injected — or the request fails with a 422 before your code runs. Fields come from the body by default; #[FromRoute] and #[FromQuery] pull them from the path or query string:

use Glueful\Validation\Attributes\{FromQuery, FromRoute, Rule};
use Glueful\Validation\Contracts\RequestData;

final class UpdatePostData implements RequestData
{
    public function __construct(
        #[FromRoute] public readonly string $uuid,                 // /posts/{uuid}
        #[Rule('required|string|max:200')] public readonly string $title,
        #[Rule('string|in:draft,published')] public readonly string $status = 'draft',
        #[FromQuery] #[Rule('in:true,false')] public readonly string $notify = 'false',
    ) {}
}
public function update(UpdatePostData $input): Response
{
    // $input->uuid, $input->title, ... are already validated and typed
}

Field names bind by exact name (no snake/camel conversion). Validation errors come back as one 422 envelope keyed by field. Database rules (unique:, exists:) run against the app's connection, and nullable / sometimes behave as their names say. Generate a DTO with php glueful scaffold:dto UpdatePostData. See Validation for the available rules and Typed Request & Response DTOs for binding sources, nesting and error shapes.

Response Helpers

Glueful’s Response class standardizes JSON output as {success, message, data}. Inside a BaseController, the same helpers exist as $this->success(), $this->created(), $this->notFound(), etc.

Success

use Glueful\Http\Response;

return Response::success($data);
return Response::success($data, 'Users retrieved');
return Response::created($user);
return Response::noContent();

Errors

return Response::notFound('User not found');
return Response::unauthorized('Invalid token');
return Response::forbidden('Access denied');
return Response::validation([
    'email' => ['Email is required.'],
]);
return Response::error('Something went wrong', 500);

Pagination

The query builder's paginate() returns the page plus its metadata, so you don't need a second count query:

public function index(Request $request): Response
{
    $page = (int) $request->query->get('page', 1);

    $result = db($this->getContext())->table('users')
        ->orderBy('created_at', 'DESC')
        ->paginate($page, 20);
    // ['data' => [...], 'current_page' => 1, 'per_page' => 20, 'total' => 135, 'last_page' => 7, ...]

    return Response::paginated($result['data'], $result['total'], $page, 20);
}

Response::paginated() renders a flat envelope: data, current_page, per_page, total, total_pages, has_next_page, has_previous_page.

Typed Response DTOs

Return an object implementing Glueful\Http\Contracts\ResponseData and the router serializes its public typed properties (enums, dates, and nested DTOs included) and wraps them in the success envelope. #[ResponseStatus] sets a 2xx status:

use Glueful\Http\Contracts\ResponseData;
use Glueful\Routing\Attributes\ResponseStatus;

final class PostData implements ResponseData
{
    public function __construct(
        public readonly string $uuid,
        public readonly string $title,
        public readonly \DateTimeImmutable $createdAt,
    ) {}
}

#[ResponseStatus(201)]
public function store(CreatePostData $input): PostData
{
    return new PostData(/* ... */); // → 201 {success, message, data: {...}}
}

For lists, return Glueful\Http\Responses\CollectionResponse or PaginatedResponse (new PaginatedResponse($items, $page, $perPage, $total)). The OpenAPI generator reads these types to document request and response schemas. See Typed Request & Response DTOs for serialization rules and collection responses.

Small endpoints can validate inline:

public function store(Request $request): Response
{
    $data = $this->getRequestData();

    if ($error = $this->validateRequest($data, [
        'title' => 'required|max:255',
    ])) {
        return $error;
    }

    return $this->created([
        'title' => $data['title'],
    ]);
}

validateRequest() only understands required, email, and max:N. For anything more, use a typed request DTO.