Core Concepts

Controllers

Controllers in Kipchak handle incoming HTTP requests and return responses. They contain your application's business logic and interact with models, services, and other components.

Basic Controller Structure

All controllers extend Kipchak\Core\Components\Controllers\Base, which provides access to the dependency injection container and logger:

<?php

namespace Api\Controllers;

use Kipchak\Core\Components\Controllers as CoreControllers;
use Kipchak\Core\Components\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class Status extends CoreControllers\Base
{
    public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Checking Status...');

        return Http\Response::json($response,
            [
                'status' => 'alive'
            ],
            200,
            true,
            86400,
        );
    }
}

Controller Methods

Controller methods must follow this signature:

public function methodName(
    ServerRequestInterface $request,
    ResponseInterface $response,
    array $args
): ResponseInterface

Parameters

  • $request - PSR-7 ServerRequestInterface containing the HTTP request data
  • $response - PSR-7 ResponseInterface for building the HTTP response
  • $args - Array of route parameters extracted from the URL

Return Value

All controller methods must return a ResponseInterface instance.

Available Properties in the Base Container

Controllers extending Controllers\Base have access to:

Container ($this->container)

The dependency injection container provides access to registered services:

public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
    // Access container services
    $config = $this->container->get('config');
    $customService = $this->container->get('myService');

    // Your logic here
}

Logger ($this->logger)

A Monolog logger instance for logging:

public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
    $this->logger->debug('Debug message');
    $this->logger->info('Info message');
    $this->logger->warning('Warning message');
    $this->logger->error('Error message');

    // Your logic here
}

Working with Requests

Query Parameters

$queryParams = $request->getQueryParams();
$page = $queryParams['page'] ?? 1;

Request Body

$body = $request->getBody()->getContents();
$data = json_decode($body, true);

Headers

$headers = $request->getHeaders();
$authHeader = $request->getHeaderLine('Authorization');

Attributes

Request attributes are set by middleware and can be accessed in controllers:

// Set by authentication middleware
$token = $this->container->get('token');
$userId = $request->getAttribute('userId');

Returning Responses

JSON Responses

Use Http\Response::json() for JSON responses:

return Http\Response::json(
    $response,
    ['status' => 'success', 'data' => $data],
    200,           // HTTP status code
    true,          // Enable caching (optional, default: false)
    86400          // Cache TTL in seconds (optional)
);

Custom Responses

For non-JSON responses, use PSR-7 response methods:

$response->getBody()->write('Plain text response');
return $response
    ->withHeader('Content-Type', 'text/plain')
    ->withStatus(200);

Using Drivers

Controllers can use Kipchak's drivers for various data stores and services:

Cache Driver

use Kipchak\Driver\Filecache\Filecache as FilecacheDriver;
use Kipchak\Driver\Memcached\Memcached as MemcachedDriver;
use Symfony\Contracts\Cache\ItemInterface;

public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
    // Memcached
    $memcached = MemcachedDriver::get('cache');
    $value = $memcached->get('key', function (ItemInterface $item) {
        $item->expiresAfter(3600);
        return 'cached value';
    });

    // File cache
    $file = FilecacheDriver::get();
    $value = $file->get('key', function (ItemInterface $item) {
        $item->expiresAfter(3600);
        return 'cached value';
    });

    return Http\Response::json($response, ['value' => $value], 200);
}

CouchDB Driver

use Kipchak\Driver\CouchDB\CouchDB;

public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
    $db = CouchDB::get('default');

    // Create database (if needed)
    $db->createDatabase();

    // Create document
    $document = json_encode(['key' => 'value']);
    $db->create('doc-id', $document);

    // Read document
    $result = $db->read('doc-id');

    // Update document
    $db->update('doc-id', $document);

    // Delete document
    // $db->delete('doc-id');

    return Http\Response::json($response, ['document' => $result], 200);
}

Using Data Transfer Objects (DTOs)

DTOs provide type-safe data structures using Valinor:

use Api\DataTransferObjects\v1\MamlukSultan;
use CuyZ\Valinor as Data;

public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
    $sultanData = [
        'yearFrom' => 1251,
        'yearTo' => 1257,
        'name' => 'Qutuz Al Din'
    ];

    $sultan = (new Data\MapperBuilder())
        ->mapper()
        ->map(MamlukSultan::class, Data\Mapper\Source\Source::array($sultanData));

    return Http\Response::json($response, ['sultan' => $sultan], 200);
}

Controller Organization

Organize controllers by API version and resource:

api/
└── Controllers/
    ├── Info.php          # Root-level controllers
    ├── Status.php
    └── v1/               # Version-specific controllers
        ├── Cache.php
        ├── Couch.php
        ├── Sultans.php
        └── Authenticated.php

Namespacing

Controllers should follow PSR-4 namespacing:

  • Root controllers: Api\Controllers\ClassName
  • Versioned controllers: Api\Controllers\v1\ClassName
  • Future versions: Api\Controllers\v2\ClassName

3. Swagger (OpenAPI) Integration

Kipchak generates API documentation directly from your Controllers using PHP Attributes. This ensures your documentation never goes out of sync with your code.

Key Attributes:

  • #[OA\Get], #[OA\Post], etc.: Defines the HTTP method and path.
  • #[OA\Response]: Defines what the client receives.
  • #[OA\Parameter]: Defines header, query, or path parameters.

Best Practices

  1. Keep controllers thin - Move complex logic to service classes or models
  2. Use DTOs for validation - Type-safe data structures prevent errors
  3. Log appropriately - Use debug for development, info/warning/error for production events
  4. Return consistent responses - Use Http\Response::json() for API endpoints
  5. Handle errors gracefully - Catch exceptions and return appropriate HTTP status codes
  6. Use dependency injection - Access services through $this->container
  7. Document your methods - Add PHPDoc comments explaining parameters and return values
  8. Follow PSR-7 - Always return ResponseInterface instances
  9. Version Everything - Always keep Routes, Controllers, and DTOs in versioned directories

Example: Complete Controller with OpenAPI Documentation

Here's a complete CRUD controller with OpenAPI attributes for automatic Swagger documentation:

<?php

namespace Api\Controllers\v1;

use Kipchak\Core\Components\Controllers;
use Kipchak\Core\Components\Http;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

#[OA\Tag(
    name: 'Users',
    description: 'User management endpoints'
)]
class Users extends Controllers\Base
{
    #[OA\Get(
        path: '/v1/users',
        summary: 'List all users',
        description: 'Returns a paginated list of users',
        tags: ['Users']
    )]
    #[OA\Parameter(
        name: 'page',
        description: 'Page number',
        in: 'query',
        required: false,
        schema: new OA\Schema(type: 'integer', default: 1, minimum: 1)
    )]
    #[OA\Parameter(
        name: 'limit',
        description: 'Number of items per page',
        in: 'query',
        required: false,
        schema: new OA\Schema(type: 'integer', default: 10, minimum: 1, maximum: 100)
    )]
    #[OA\Response(
        response: 200,
        description: 'Successful operation',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(
                    property: 'users',
                    type: 'array',
                    items: new OA\Items(
                        properties: [
                            new OA\Property(property: 'id', type: 'integer', example: 1),
                            new OA\Property(property: 'name', type: 'string', example: 'Baybars')
                        ],
                        type: 'object'
                    )
                ),
                new OA\Property(property: 'page', type: 'integer', example: 1),
                new OA\Property(property: 'limit', type: 'integer', example: 10)
            ]
        )
    )]
    public function list(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Fetching user list');

        // Get query parameters
        $queryParams = $request->getQueryParams();
        $page = (int)($queryParams['page'] ?? 1);
        $limit = (int)($queryParams['limit'] ?? 10);

        // Fetch data (example)
        $users = [
            ['id' => 1, 'name' => 'Baybars'],
            ['id' => 2, 'name' => 'Qutuz']
        ];

        return Http\Response::json($response, [
            'users' => $users,
            'page' => $page,
            'limit' => $limit
        ], 200);
    }

    #[OA\Get(
        path: '/v1/users/{id}',
        summary: 'Get user by ID',
        description: 'Returns a single user by their ID',
        tags: ['Users']
    )]
    #[OA\Parameter(
        name: 'id',
        description: 'User ID',
        in: 'path',
        required: true,
        schema: new OA\Schema(type: 'integer')
    )]
    #[OA\Response(
        response: 200,
        description: 'Successful operation',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'id', type: 'integer', example: 1),
                new OA\Property(property: 'name', type: 'string', example: 'Baybars')
            ]
        )
    )]
    #[OA\Response(
        response: 404,
        description: 'User not found',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'error', type: 'string', example: 'User not found')
            ]
        )
    )]
    public function get(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Fetching user', ['id' => $args['id']]);

        $userId = $args['id'];

        // Fetch user (example)
        $user = ['id' => $userId, 'name' => 'Baybars'];

        if (!$user) {
            return Http\Response::json($response, [
                'error' => 'User not found'
            ], 404);
        }

        return Http\Response::json($response, $user, 200);
    }

    #[OA\Post(
        path: '/v1/users',
        summary: 'Create a new user',
        description: 'Creates a new user with the provided data',
        requestBody: new OA\RequestBody(
            description: 'User object that needs to be created',
            required: true,
            content: new OA\JsonContent(
                required: ['name', 'email'],
                properties: [
                    new OA\Property(property: 'name', type: 'string', example: 'Qalawun'),
                    new OA\Property(property: 'email', type: 'string', format: 'email', example: 'qalawun@mamluk.com'),
                    new OA\Property(property: 'status', type: 'string', enum: ['active', 'inactive'], example: 'active')
                ]
            )
        ),
        tags: ['Users']
    )]
    #[OA\Response(
        response: 201,
        description: 'User created successfully',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'id', type: 'integer', example: 3),
                new OA\Property(property: 'name', type: 'string', example: 'Qalawun'),
                new OA\Property(property: 'email', type: 'string', example: 'qalawun@mamluk.com'),
                new OA\Property(property: 'status', type: 'string', example: 'active')
            ]
        )
    )]
    #[OA\Response(
        response: 400,
        description: 'Invalid input',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'error', type: 'string', example: 'Invalid input data')
            ]
        )
    )]
    public function create(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Creating new user');

        $body = $request->getBody()->getContents();
        $data = json_decode($body, true);

        // Validate input (example)
        if (empty($data['name']) || empty($data['email'])) {
            return Http\Response::json($response, [
                'error' => 'Invalid input data'
            ], 400);
        }

        // Create user (example)
        $newUser = [
            'id' => 3,
            'name' => $data['name'],
            'email' => $data['email'],
            'status' => $data['status'] ?? 'active'
        ];

        return Http\Response::json($response, $newUser, 201);
    }

    #[OA\Put(
        path: '/v1/users/{id}',
        summary: 'Update an existing user',
        description: 'Updates an existing user with the provided data',
        requestBody: new OA\RequestBody(
            description: 'Updated user object',
            required: true,
            content: new OA\JsonContent(
                properties: [
                    new OA\Property(property: 'name', type: 'string', example: 'Baybars Al-Bunduqdari'),
                    new OA\Property(property: 'email', type: 'string', format: 'email', example: 'baybars@mamluk.com'),
                    new OA\Property(property: 'status', type: 'string', enum: ['active', 'inactive'], example: 'active')
                ]
            )
        ),
        tags: ['Users']
    )]
    #[OA\Parameter(
        name: 'id',
        description: 'User ID',
        in: 'path',
        required: true,
        schema: new OA\Schema(type: 'integer')
    )]
    #[OA\Response(
        response: 200,
        description: 'User updated successfully',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'id', type: 'integer', example: 1),
                new OA\Property(property: 'name', type: 'string', example: 'Baybars Al-Bunduqdari'),
                new OA\Property(property: 'email', type: 'string', example: 'baybars@mamluk.com'),
                new OA\Property(property: 'status', type: 'string', example: 'active')
            ]
        )
    )]
    #[OA\Response(
        response: 404,
        description: 'User not found',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'error', type: 'string', example: 'User not found')
            ]
        )
    )]
    public function update(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Updating user', ['id' => $args['id']]);

        $userId = $args['id'];
        $body = $request->getBody()->getContents();
        $data = json_decode($body, true);

        // Check if user exists (example)
        $user = ['id' => $userId, 'name' => 'Baybars'];

        if (!$user) {
            return Http\Response::json($response, [
                'error' => 'User not found'
            ], 404);
        }

        // Update user (example)
        $updatedUser = [
            'id' => $userId,
            'name' => $data['name'] ?? $user['name'],
            'email' => $data['email'] ?? 'baybars@mamluk.com',
            'status' => $data['status'] ?? 'active'
        ];

        return Http\Response::json($response, $updatedUser, 200);
    }

    #[OA\Delete(
        path: '/v1/users/{id}',
        summary: 'Delete a user',
        description: 'Deletes a user by their ID',
        tags: ['Users']
    )]
    #[OA\Parameter(
        name: 'id',
        description: 'User ID',
        in: 'path',
        required: true,
        schema: new OA\Schema(type: 'integer')
    )]
    #[OA\Response(
        response: 204,
        description: 'User deleted successfully'
    )]
    #[OA\Response(
        response: 404,
        description: 'User not found',
        content: new OA\JsonContent(
            properties: [
                new OA\Property(property: 'error', type: 'string', example: 'User not found')
            ]
        )
    )]
    public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $this->logger->debug('Deleting user', ['id' => $args['id']]);

        $userId = $args['id'];

        // Check if user exists (example)
        $user = ['id' => $userId, 'name' => 'Baybars'];

        if (!$user) {
            return Http\Response::json($response, [
                'error' => 'User not found'
            ], 404);
        }

        // Delete user (example)
        // $this->userRepository->delete($userId);

        return $response->withStatus(204);
    }
}

OpenAPI Features

The OpenAPI attributes provide:

  • Automatic Swagger documentation - Generates interactive API docs
  • Request/response schemas - Documents expected data structures
  • Parameter validation - Defines query params, path params, and request bodies
  • Response codes - Documents success and error responses
  • Type safety - Specifies data types, formats, and constraints
  • Examples - Provides sample values for testing

To generate the OpenAPI specification, use swagger-php's scanner to scan your controllers and generate a openapi.json or openapi.yaml file for use with Swagger UI.

Previous
Routes