Controller - viames/pair GitHub Wiki

Pair framework: Controller

Pair\Core\Controller is the legacy MVC controller base in Pair v4.

For new Pair v4 modules, prefer Pair\Web\Controller with explicit PageResponse, JsonResponse, or another ResponseInterface implementation. The old controller remains available as a migration bridge for classic Pair MVC modules and emits a deprecation notice outside production.

When to use each controller base

Pair\Web\Controller (preferred)

Use it for new Pair v4 modules and for migrated modules that already return explicit responses.

Typical flow:

  • read request input explicitly
  • load or map data explicitly
  • return PageResponse for HTML or JsonResponse for JSON
  • keep layout state inside a typed object passed to the response

Example:

use Pair\Web\Controller;
use Pair\Web\PageResponse;

require_once __DIR__ . '/classes/UsersDefaultPageState.php';

final class UsersController extends Controller {

	/**
	 * Render the default users page with explicit typed state.
	 */
	public function defaultAction(): PageResponse {

		return $this->page('default', new UsersDefaultPageState('Users'), 'Users');

	}

}

Minimal page-state class:

use Pair\Data\ArraySerializableData;
use Pair\Data\ReadModel;

/**
 * Typed layout state for the users default page.
 */
final readonly class UsersDefaultPageState implements ReadModel {

	use ArraySerializableData;

	/**
	 * Build the page state.
	 */
	public function __construct(public string $title) {}

	/**
	 * Export the page state for optional JSON reuse.
	 *
	 * @return	array<string, mixed>
	 */
	public function toArray(): array {

		return ['title' => $this->title];

	}

}

Pair\Core\Controller (legacy bridge)

Keep using it only while the module still depends on the legacy MVC lifecycle, for example when it still uses:

  • _init() as the setup hook
  • setView()
  • implicit model/view loading
  • $this->model as ambient state
  • classic View::assign() variables

The Pair v3 to v4 upgrader keeps these controllers on Pair\Core\Controller until the module is ready for the explicit v4 path.

Legacy constructor lifecycle

final public function __construct()

Legacy execution flow:

  1. loads Application, Router, and Translator singletons
  2. emits a non-production deprecation notice for the legacy controller class
  3. derives the controller name from the class name
  4. resolves the module path via reflection
  5. tells Translator which module is active
  6. loads the default model from model.php if $model was not already set
  7. runs _init()
  8. if the app is not headless and no view was selected yet, loads the action view or default

Because the constructor is final, controller setup in the legacy MVC path still belongs in _init(). In the explicit v4 path, setup belongs in boot() on Pair\Web\Controller.

Legacy naming conventions

Given OrdersController:

  • module folder: modules/orders/
  • default model file: modules/orders/model.php
  • default model class: OrdersModel
  • view file for action edit: modules/orders/viewEdit.php
  • view class for action edit: OrdersViewEdit

That convention is what powers the old auto-loaded MVC stack. Pair v4 keeps it only for migration compatibility.

Legacy MVC methods

_init(): void

Optional setup hook executed after the default model is ready and before the default view is autoloaded.

Use it only in classic legacy modules. Migrated v4 web controllers should use boot() instead.

setView(string $viewName): void

Loads and stores a legacy Pair\Core\View instance for the controller.

This is part of the old MVC path and should not be used in new Pair v4 modules. Use an explicit PageResponse return instead.

renderView(): void

Validates that the current view is a real Pair\Core\View subclass and then calls display().

This is the classic HTML rendering path used by legacy modules. It is not the preferred Pair v4 flow.

loadModel(string $modelName): void

Loads a non-default model from the current module folder.

This helper remains relevant only to the legacy controller lifecycle. In the explicit v4 path, prefer constructing or resolving the exact object needed by the action.

loadModelForActions(string $modelName, array $actions): void

Conditional shortcut around loadModel() for the legacy controller flow.

getObjectRequestedById(string $class): ?ActiveRecord

Loads an ActiveRecord from the first route parameter (Router::get(0)).

This helper is still useful during migration, but it also signals that the controller still depends on the legacy base class. Explicit v4 controllers usually read ids through request input or route state and then query explicitly.

Recommended Pair v4 migration shape

  1. replace Pair\Core\Controller with Pair\Web\Controller
  2. replace _init() with boot()
  3. stop calling setView()
  4. move layout preparation into a typed page-state object
  5. return PageResponse or JsonResponse explicitly

Pair v4 helper examples

Pair\Web\Controller exposes a deliberately small helper surface.

use Pair\Http\JsonResponse;
use Pair\Http\ResponseInterface;
use Pair\Web\Controller;
use Pair\Web\PageResponse;

require_once __DIR__ . '/classes/OrdersDefaultPageState.php';

/**
 * Orders controller using explicit Pair v4 responses.
 */
final class OrdersController extends Controller {

	/**
	 * Prepare module dependencies without legacy model/view bootstrapping.
	 */
	protected function boot(): void {

		// Put explicit module setup here, for example service construction.

	}

	/**
	 * Render the orders list with typed query-string input.
	 */
	public function defaultAction(): PageResponse {

		$input = $this->input();

		$state = new OrdersDefaultPageState(
			status: $input->string('status', 'open'),
			page: max(1, $input->int('page', 1))
		);

		return $this->page('default', $state, 'Orders');

	}

	/**
	 * Return JSON without entering the legacy view path.
	 */
	public function statusAction(): JsonResponse {

		return $this->json([
			'ok' => true,
			'modulePath' => $this->modulePath(),
		]);

	}

	/**
	 * Return any explicit response implementation when the action can vary.
	 */
	public function pingAction(): ResponseInterface {

		return $this->json(['pong' => true]);

	}

}

Use modulePath('layouts/default.php') when a module needs an explicit file inside its own folder. Use page('default', $state, 'Title') for the standard layouts/default.php path.

See also: Input, PageResponse, JsonResponse, ResponseInterface, ReadModel, View, Upgrade-to-v4, ApiExposable, CrudController