Generator - viames/pair GitHub Wiki

Pair framework: generator

Pair v4 ships a small CLI entrypoint at bin/pair.

The generator is intentionally simple: it writes explicit files, avoids hidden discovery, and does not overwrite user-edited files unless --force is provided.

Commands

pair make:module <name>

Creates a Pair v4 web module:

vendor/bin/pair make:module orders

Generated files:

  • modules/orders/controller.php
  • modules/orders/classes/OrdersDefaultPageState.php
  • modules/orders/layouts/default.php

Optional flags:

  • --with-js adds modules/orders/assets/orders.js
  • --with-test adds a PHPUnit skeleton under tests/Unit/Modules/
  • --path=/absolute/app/path targets another application directory
  • --force replaces existing generated files

Generated layouts stay mostly HTML and read only from the typed $state object.

Generated controller shape:

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

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

/**
 * Generated Pair v4 controller for the orders module.
 */
final class OrdersController extends Controller {

	/**
	 * Return the default module page.
	 */
	public function defaultAction(): PageResponse {

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

	}

}

Generated page-state shape:

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

/**
 * Generated typed state for the Orders default layout.
 */
final readonly class OrdersDefaultPageState implements ReadModel {

	use ArraySerializableData;

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

	/**
	 * Export the page state as an array.
	 *
	 * @return	array<string, mixed>
	 */
	public function toArray(): array {

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

	}

}

pair make:api <name>

Creates a minimal API module controller:

vendor/bin/pair make:api api

Generated file:

  • modules/api/controller.php

The controller extends Pair\Api\CrudController and includes a healthAction() returning an explicit JSON response.

pair make:crud <name> --table=<table>

Creates a CRUD model and read model skeleton:

vendor/bin/pair make:crud order --table=orders --fields=id,customer_id,total_amount

Generated files:

  • models/Order.php
  • classes/OrderReadModel.php

--fields is optional. When omitted, the generator starts with id only so the skeleton is safe and minimal.

The generated model:

  • extends Pair\Orm\ActiveRecord
  • uses Pair\Api\ApiExposable
  • declares readModel
  • declares explicit getBinds()

The generated read model:

  • implements Pair\Data\ReadModel
  • implements Pair\Data\MapsFromRecord
  • exports an explicit array shape

Generated read-model shape:

use Pair\Data\ArraySerializableData;
use Pair\Data\MapsFromRecord;
use Pair\Data\ReadModel;
use Pair\Orm\ActiveRecord;

/**
 * Generated read model for Order API responses.
 */
final readonly class OrderReadModel implements ReadModel, MapsFromRecord {

	use ArraySerializableData;

	/**
	 * Build the read model.
	 */
	public function __construct(
		public mixed $id,
		public mixed $customerId,
		public mixed $totalAmount
	) {}

	/**
	 * Build the read model from a persistence record.
	 */
	public static function fromRecord(ActiveRecord $record): static {

		return new self(
			$record->id,
			$record->customerId,
			$record->totalAmount
		);

	}

	/**
	 * Export the read model as an array.
	 *
	 * @return	array<string, mixed>
	 */
	public function toArray(): array {

		return [
			'id' => $this->id,
			'customerId' => $this->customerId,
			'totalAmount' => $this->totalAmount,
		];

	}

}

Naming rules

Module and API names are restricted to lowercase letters and numbers because Pair's current module controller resolution expects simple module names.

Examples:

  • valid: orders, api, crm2
  • invalid: order-items, order_items

Use folder or route aliases outside the generator if the application needs more complex URLs.

Overwrite behavior

If a target file already exists:

  • identical content is reported as unchanged
  • different content blocks the command
  • --force intentionally replaces it

This protects manual edits and keeps the generator safe to re-run.

See also: Controller, Input, PageResponse, ReadModel, MapsFromRecord, API, CrudController, RequestData.

⚠️ **GitHub.com Fallback** ⚠️