Tina4 PHP - Quick Reference#
๐ฅ Hot Tips
- Routes go in
src/routes/, templates insrc/templates/, static files insrc/public/ - GET routes are public by default; POST/PUT/PATCH/DELETE require a token
$response->json()returns JSON with the correct headers, no manual config- Run
tina4 serveto start the dev server on port 7145
Installation โข Static Websites โข Routing โข Middleware โข Templates โข Sessions โข SCSS โข Environments โข Authentication โข Forms & Tokens โข AJAX โข OpenAPI โข Databases โข Database Results โข Migrations โข ORM โข CRUD โข REST Client โข Testing โข Services โข Websockets โข Queues โข WSDL โข GraphQL โข Localization โข HTML Builder โข Events โข Logging โข Response Cache โข Health โข DI Container โข Error Overlay โข Dev Admin โข CLI โข MCP Server โข FakeData
Installation#
# Install the tina4 CLI once. Windows: irm https://tina4.com/install.ps1 | iexcurl -fsSL https://tina4.com/install.sh | shโtina4 init php my-appcd my-apptina4 serveMore details on project setup and configuration options.
Static Websites#
Drop .html or .twig files in ./src/templates. Put assets in ./src/public.
<!-- src/templates/index.twig --><h1>Hello Static World</h1>More details on static website routing.
Basic Routing#
<?phpuse Tina4\Router;โRouter::get("/", function ($request, $response) { return $response->json(["message" => "Hello Tina4 PHP"]);});โ// POST requires a formToken or Bearer authRouter::post("/api/items", function ($request, $response) { return $response->json(["data" => $request->body], 201);});โ// Dynamic path parametersRouter::get("/users/{id:int}", function ($request, $response) { $id = $request->params["id"]; return $response->json(["user_id" => $id]);});Follow the links for basic routing, dynamic routing with variables, and different response types.
Middleware#
The v3 middleware system uses class-based middleware with before*/after* static methods.
<?phpuse Tina4\Request;use Tina4\Response;โclass AuthMiddleware{ public static function beforeAuth(Request $request, Response $response): array { if (!$request->bearerToken()) { return [$request, $response->json(["error" => "Unauthorized"], 401)]; } return [$request, $response]; }}Register middleware globally or attach it to a single route.
<?phpuse Tina4\Middleware;use Tina4\Router;โ// Global -- runs on every requestMiddleware::use(AuthMiddleware::class);โ// Per-route -- third argumentRouter::get("/api/secret", function ($request, $response) { return $response->json(["secret" => "The answer is 42"]);}, "requireApiKey");Follow the links for more on middleware declaration, linking to routes, middleware chaining, and middleware with dynamic routes.
Template Rendering#
Templates live in ./src/templates. The framework uses Frond, a Twig-compatible engine built from scratch. Call $response->render() and pass your data.
<!-- src/templates/hello.html --><h1>Hello {{ name }}</h1><?phpuse Tina4\Router;โRouter::get("/", function ($request, $response) { return $response->render("hello.html", ["name" => "World"]);});Sessions#
Sessions start by default in the Auth constructor.
SCSS Stylesheets#
Drop files in ./src/scss. The framework compiles them to ./src/public/css.
// src/scss/main.scss$primary: #2c3e50;body { background: $primary; color: white;}More details on CSS and SCSS.
Environments#
The default development environment lives in .env.
[Project Settings]VERSION=1.0.0TINA4_DEBUG=trueTINA4_LOG_LEVEL=[TINA4_LOG_ALL]TINA4_CACHE_BACKEND=falseTINA4_DATABASE_URL=sqlite:///data/app.db[Open API]TINA4_SWAGGER_TITLE=Tina4 ProjectTINA4_SWAGGER_DESCRIPTION=Edit your .env file to change this descriptionTINA4_SWAGGER_VERSION=1.0.0Access environment variables through the $_ENV superglobal.
$title = $_ENV["TINA4_SWAGGER_TITLE"];Access env vars programmatically:
use Tina4\DotEnv;โDotEnv::loadEnv('.env'); // Load .env file (auto on server start)DotEnv::getEnv('TINA4_DATABASE_URL'); // Get value or nullDotEnv::getEnv('PORT', '7146'); // Get value with defaultDotEnv::hasEnv('TINA4_DEBUG'); // true if setDotEnv::requireEnv('TINA4_DATABASE_URL'); // Throws if missingDotEnv::isTruthy(DotEnv::getEnv('TINA4_DEBUG')); // true for "true", "1", "yes"Authentication#
POST, PUT, PATCH, and DELETE routes are secured by default. GET routes stay public unless you mark them otherwise.
<?phpuse Tina4\Router;โ// @secured marks a GET route as protected/** * @secured */Router::get("/api/profile", function ($request, $response) { return $response->json(["user" => $request->user]);});โ// Or use the chainable ->secure() methodRouter::get("/api/account", function ($request, $response) { return $response->json(["account" => $request->user]);})->secure();Generate and validate tokens with the Auth class.
use Tina4\Auth;โ$token = Auth::getToken(["userId" => 1, "role" => "admin"]);$payload = Auth::validToken($token);HTML Forms and Tokens#
Form tokens protect POST routes from cross-site forgery. Add one with a Twig filter.
<form method="POST" action="/process-form"> {{ "emailForm" | formToken }} <input name="email"> <button>Save</button></form>The filter renders a hidden input with a signed JWT.
<form method="POST" action="/process-form"> <input type="hidden" name="formToken" value="ey..."> <input name="email"> <button>Save</button></form>More details on posting form data, securing routes, Tina4 tokens, uploading files, handling errors, and a full login example.
AJAX and frond.js#
Tina4 ships with frond.js, a small zero-dependency JavaScript library for AJAX calls, form submissions, and real-time WebSocket connections.
More details on available features.
OpenAPI and Swagger UI#
Swagger is built in and lives at /swagger. The @description annotation registers a route in the documentation.
<?phpuse Tina4\Router;โ/** * @description Returns all users */Router::get("/users", function ($request, $response) { return $response->json((new User())->select("*"));});Follow the links for more on configuration, annotations, and usage.
Databases#
Set TINA4_DATABASE_URL in .env. The framework reads it at startup and opens the connection.
TINA4_DATABASE_URL=sqlite:///data/app.dbTINA4_DATABASE_URL=postgres://localhost:5432/myappTINA4_DATABASE_URL=mysql://localhost:3306/myappTINA4_DATABASE_URL=firebird://localhost:3050/path/to/database.fdbAccess the connection through the Database class.
<?phpuse Tina4\Database;โ$db = Database::getConnection();$result = $db->fetch("SELECT * FROM products WHERE price > ?", [50]);Follow the links for more on available connections, core methods, usage, examples, and transaction control.
Database Results#
Fetch a single row or a paginated set. The database returns a DatabaseResult object that converts to arrays or objects.
$db = Database::getConnection();โ$row = $db->fetchOne("SELECT * FROM products WHERE id = 1");โ// fetch($sql, $params, $noOfRecords, $offset)$result = $db->fetch("SELECT * FROM products ORDER BY name", [], 10, 0);Dig into the usage guide and examples for deeper coverage.
Migrations#
The CLI manages migrations. Create one, add your SQL, then run it.
tina4 migrate:create my-first-migrationRun all pending migrations in one command.
tina4 migrateMore details on migrations, their creation, running them, and integration with ORM.
ORM#
Once your migrations have created the tables, ORM models map PHP classes to database rows.
<?phpuse Tina4\ORM;โclass User extends ORM{ public string $tableName = "users"; public string $primaryKey = "id";โ public int $id; public string $email;}โ$user = new User(["email" => "alice@example.com"]);$user->save();$user = (new User())->load("id = ?", [1]);ORM covers a lot of ground. Study the full reference to get the most from it.
CRUD#
One line of code generates a working CRUD system: screens, routes, and all.
(new User())->generateCrud("/my-crud-templates");More details on how CRUD works and where it puts the generated files.
Consuming REST APIs#
Pull data from an external API in a single call.
$api = (new \Tina4\Api("https://api.example.com"))->sendRequest("/my-route", "GET");More details on sending POST bodies, authorization headers, and other controls.
Inline Testing#
Tina4 lets you add tests to functions without setting up a test suite.
/** * @tests * assert(2, 5) == 7, "2+5 not equal 7" */public function addTwoNumbers($number1, $number2){ return $number1 + $number2;}Run the tests from the CLI.
tina4 testLimitations and further reading on the testing system.
Services#
Create a process class. The service runner picks it up and executes it on schedule.
class MyProcess extends \Tina4\Process{ public function canRun(): bool { return true; }โ public function run(): void { // Your work goes here }}Register the process with the service.
$service = new \Tina4\Service();$service->addProcess(new MyProcess("Unique Process Name"));Further reading on services. Study them alongside threads for the full picture.
Websockets#
Router::websocket("/ws/chat/{room}", function ($connection, $event, $data) { match ($event) { "open" => $connection->send("Welcome to room " . $connection->params["room"]), "message" => $connection->broadcast($data), "close" => null, };});Queues#
Tina4 includes a queue system with multiple backends: LiteQueue (SQLite), MongoDB, RabbitMQ, and Kafka. Install tina4stack/tina4php-queue and use a single API for producing and consuming messages.
use Tina4\Queue;โ$queue = new Queue(topic: 'my-events');$queue->produce('Hello World', userId: 'user123');โforeach ($queue->consume() as $message) { echo $message->data;}More details on queues, their backends, and configuration.
WSDL#
Define your WSDL service as a class. Tina4 handles the XML, the WSDL generation, and the SOAP envelope.
class Calculator extends \Tina4\WSDL { protected array $returnSchemas = [ "Add" => ["Result" => "int"], ];โ public function Add(int $a, int $b): array { return ["Result" => $a + $b]; }}Wire it to a route.
<?phpuse Tina4\Router;โRouter::any("/calculator", function ($request, $response) { $calculator = new Calculator($request); $handle = $calculator->handle(); return $response->xml($handle);});More details on WSDL services.
GraphQL#
use Tina4\GraphQL;โ\$schema = <<<GQLtype Query { hello(name: String!): String users: [User]}โtype User { id: Int name: String email: String}GQL;โ\$resolvers = [ "hello" => fn(\$args) => "Hello, " . \$args["name"] . "!", "users" => fn() => \$db->fetch("SELECT * FROM users")->toArray(),];โ\$graphql = new GraphQL(\$schema, \$resolvers);Register the endpoint:
Router::post("/graphql", function (\$request, \$response) use (\$graphql) { \$result = \$graphql->execute(\$request->body["query"] ?? ""); return \$response->json(\$result);})->noAuth();GraphiQL UI available at /__dev/graphql in debug mode.
Localization (i18n)#
Set TINA4_LOCALE=en in .env. Place JSON files in src/locales/:
// src/locales/en.json{ "greeting": "Hello, {name}!" }$i18n = new I18n();$i18n->t("greeting", ["name" => "Alice"]); // "Hello, Alice!"โ// Switch locale$i18n->setLocale("fr");HTML Builder#
$el = new HtmlElement("div", ["class" => "card"], ["Hello"]);echo $el; // <div class="card">Hello</div>โ// Nesting$card = new HtmlElement("div", ["class" => "card"], [ new HtmlElement("h2", [], ["Title"]), new HtmlElement("p", [], ["Content"]),]);โ// Helper functionsextract(HtmlElement::helpers());echo $_div(["class" => "card"], $_h1("Title"), $_p("Description"));Events#
Register listeners and fire them anywhere in the application.
use Tina4\Events;โ// Register a listenerEvents::on("user.registered", function (array $payload): void { echo "New user: " . $payload["email"];});โ// Fire once -- listener is removed after first callEvents::once("app.boot", function (): void { echo "Booted!";});โ// Emit an eventEvents::emit("user.registered", ["email" => "alice@example.com"]);Multiple listeners can bind to the same event. They run in registration order.
Logging#
Write log messages through the Debug class. The level is controlled by TINA4_LOG_LEVEL in .env.
use Tina4\Debug;โDebug::message("Starting import", TINA4_LOG_INFO);Debug::message("Query took 2.3s", TINA4_LOG_WARNING);Debug::message("Connection failed", TINA4_LOG_ERROR);# .envTINA4_LOG_LEVEL=TINA4_LOG_ALL # DEBUG | INFO | WARNING | ERROR | NONELog output goes to src/logs/ by default. Set TINA4_LOG_DIR to change the destination.
Response Cache#
Chain ->cache() onto any route handler to store the response. Subsequent requests within the TTL skip the handler entirely.
use Tina4\Router;โRouter::get("/api/products", function ($request, $response) { $db = \Tina4\Database::getConnection(); return $response->json($db->fetch("SELECT * FROM products")->toArray());})->cache(300); // cache for 300 secondsโ// Cache with a custom keyRouter::get("/api/report/{year}", function ($request, $response) { return $response->json(buildReport($request->params["year"]));})->cache(3600, "report-" . date("Y"));Set TINA4_CACHE_BACKEND=true in .env to activate caching globally.
Health Endpoint#
Tina4 exposes a built-in health check at /health. No setup required.
curl http://localhost:7146/health{ "status": "ok", "uptime": 142, "memory_mb": 12.4, "php": "8.3.0", "tina4": "3.10.20"}Add custom checks by registering a health.check event listener.
use Tina4\Events;โEvents::on("health.check", function (array &$report): void { $report["db"] = \Tina4\Database::getConnection()->ping() ? "ok" : "fail";});DI Container#
Register services once and resolve them anywhere. Singletons share one instance across the request lifecycle.
use Tina4\Container;โ// Bind a factoryContainer::register("mailer", fn() => new Mailer($_ENV["TINA4_MAIL_HOST"]));โ// Bind a singletonContainer::singleton("db", fn() => \Tina4\Database::getConnection());โ// Resolve$mailer = Container::get("mailer");$db = Container::get("db");Route handlers receive resolved dependencies automatically when type-hinted parameter names match registered keys.
Error Overlay#
When TINA4_DEBUG=true, unhandled exceptions render an interactive overlay in the browser instead of a blank 500 page. The overlay shows the stack trace, request details, and the source file at the point of failure. No configuration needed, it activates automatically.
# .envTINA4_DEBUG=trueIn production set TINA4_DEBUG=false. Errors log to src/logs/ and return a plain 500 response.
Dev Admin#
Point a browser at /__dev while the dev server is running to open the admin dashboard.
tina4 serve# open http://localhost:7146/__devThe dashboard surfaces routes, registered middleware, environment variables, recent log entries, active queue workers, and the GraphiQL interface. It is disabled automatically when TINA4_DEBUG=false.
CLI Commands#
# Scaffold a new projecttina4 init php my-projectโ# Start the dev server (default port 7146)tina4 serveโ# Start on a custom porttina4 serve --port 8080โ# Run inline teststina4 testโ# Create a migrationtina4 migrate:create add-users-tableโ# Run all pending migrationstina4 migrateโ# Rollback the last migrationtina4 migrate --rollbackโ# Generate CRUD for a modeltina4 crud UserMCP Server#
Tina4 ships an MCP (Model Context Protocol) server that starts automatically when TINA4_DEBUG=true. AI assistants connect to it to inspect routes, models, and data without leaving the editor.
# .envTINA4_DEBUG=trueTINA4_MCP_PORT=7147 # optional override, defaults to main port + 1# Verify the server is runningcurl http://localhost:7147/mcp/manifestThe manifest lists every route, ORM model, and migration. Disable it in production by setting TINA4_DEBUG=false.
FakeData#
Generate realistic test data without an internet connection. FakeData ships with Tina4, no extra packages required.
use Tina4\FakeData;โ$fake = new FakeData();โecho $fake->name(); // "Alice Johnson"echo $fake->email(); // "alice.johnson@example.com"echo $fake->phone(); // "+1-555-0123"echo $fake->address(); // "42 Maple Street, Springfield"echo $fake->company(); // "Acme Corp"echo $fake->lorem(10); // 10 words of placeholder textecho $fake->integer(1, 100); // random int between 1 and 100echo $fake->uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"โ// Seed for reproducible output$fake = new FakeData(seed: 42);Use FakeData inside migrations, seeders, or test fixtures.
๐ Download the book#
Tina4 for PHP Developers (PDF), full reference, printable, with clickable table of contents and PDF outline. Regenerated with every release.