← Start Here

START HERE FEATURE

Beyond Controllers

If one class owns the route, request parsing, validation, auth assumptions, business orchestration, and response assembly, it stops being simple and starts being the hidden coupling point of the whole feature.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Understand why Semitexa keeps transport, use case, and rendering as separate explicit responsibilities.

Architecture Contrast

A controller is one object doing yesterday's whole HTTP stack

Semitexa is not anti-class. It is anti-collapse. The example payload below owns a real `{slug}` route parameter, its regex guard, normalization, and validation before the handler even starts business work.

The controller pattern feels compact only while the endpoint is trivial.

As soon as route parameters, input rules, auth rules, response variants, and SSR composition appear, the controller becomes a mixed-concern shell that is harder to test, harder to extend, and harder for tooling to explain.

Concern Typical controller-first class Semitexa canonical owner
Route contract Annotation or controller method metadata Payload DTO
Input boundary Request object + ad hoc reads Payload setters and validation
Use case orchestration Controller action body Typed handler
Response shape Inline arrays / Response building Resource DTO
Extensibility Middleware, helper traits, controller inheritance Explicit contracts and modules

Semitexa keeps the HTTP boundary typed so route discovery, validation, response decoration, and introspection can all reason about the same declared contract.

controllers payload handler resource rendering boundary

Verified against Semitexa Ultimate 2026.09.19.1020

Beyond Controllers

Semitexa does not treat controllers as the universal container for transport, orchestration, and rendering.

Canonical split

  • payload owns inbound HTTP structure
  • handler owns the use case
  • resource owns response data and metadata
  • template owns presentation

What this avoids

  • transport and business logic collapsing into one unstable class
  • hidden rendering state
  • ad hoc arrays passed across unclear boundaries

Why this matters

The framework stays readable because each layer owns one job. That keeps growth additive instead of turning every new feature into another oversized controller.

© Harold Abelson: "Programs must be written for people to read, and only incidentally for machines to execute."

Payload Trusted input boundary
<?phpdeclare(strict_types=1);use Semitexa\Core\Attribute\AsPublicPayload;use Semitexa\Core\Exception\ValidationException;#[AsPublicPayload(    path: '/products/{slug}',    methods: ['GET'],    responseWith: ProductShowcaseResource::class,    requirements: ['slug' => '[a-z0-9-]+'],    defaults: ['slug' => 'wireless-headphones'],)]final class ProductShowcasePayload{    protected string $slug = 'wireless-headphones';    public function getSlug(): string    {        return $this->slug;    }    public function setSlug(string $slug): void    {        $normalized = strtolower(trim($slug));        $normalized = preg_replace('/[^a-z0-9-]+/', '-', $normalized) ?? '';        $normalized = trim($normalized, '-');        if ($normalized === '') {            throw new ValidationException(['slug' => ['Product slug is required.']]);        }        if (strlen($normalized) > 120) {            throw new ValidationException(['slug' => ['Product slug must stay below 120 characters.']]);        }        if (preg_match('/^[a-z0-9-]+$/', $normalized) !== 1) {            throw new ValidationException(['slug' => ['Product slug may only contain lowercase letters, numbers, and dashes.']]);        }        $this->slug = $normalized;    }}

Why Controller-First Ages Badly

Common failure modes that look normal until the codebase grows

The problem is not the word "controller". The problem is using one class as the accidental dumping ground for every HTTP concern.

Validation logic leaks into action methods because the transport contract is not a first-class object.

Route parameters such as slugs are often trimmed, sanitized, defaulted, and rejected ad hoc in the controller body instead of at the payload boundary.

Response shape drifts between arrays, Response objects, view models, and template variables.

Route metadata becomes harder to inspect because it is attached to controller actions, middleware, and framework conventions at the same time.

Module extension gets coarse-grained because replacing a small behavior often means replacing the whole controller action or wrapping it indirectly.

LLM and static-analysis tooling see one mixed blob instead of a typed transport contract, a use case step, and a response contract.

Semitexa Canon

What to remember when building the first real feature

Once the transport boundary, use case, and response each have an owner, the rest of the framework becomes easier to inspect, extend, and automate.

Declare the route, HTTP methods, and payload boundary on the payload DTO, not in a controller method signature.

Keep the handler focused on orchestration and use-case flow, not transport parsing or response assembly details.

Let the resource own response shape, SEO metadata, and template context so rendering stays explicit and reusable.

Add modules and contracts around this typed spine instead of reopening one growing action class for every new concern.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal