Tina4 PHP -- Quick Reference
TINA4 — The Intelligent Native Application 4ramework Simple. Fast. Human. | Built for AI. Built for you.
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 7146
Installation
tina4 init php my-project
cd my-project
composer install
tina4 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
<?php
use Tina4\Router;
Router::get("/", function ($request, $response) {
return $response->json(["message" => "Hello Tina4 PHP"]);
});
// POST requires a formToken or Bearer auth
Router::post("/api/items", function ($request, $response) {
return $response->json(["data" => $request->body], 201);
});
// Dynamic path parameters
Router::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.
<?php
use 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.
<?php
use Tina4\Middleware;
use Tina4\Router;
// Global -- runs on every request
Middleware::use(AuthMiddleware::class);
// Per-route -- third argument
Router::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><?php
use 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.0
TINA4_DEBUG=true
TINA4_DEBUG_LEVEL=[TINA4_LOG_ALL]
TINA4_CACHE_ON=false
DATABASE_URL=sqlite:///data/app.db
[Open API]
SWAGGER_TITLE=Tina4 Project
SWAGGER_DESCRIPTION=Edit your .env file to change this description
SWAGGER_VERSION=1.0.0Access environment variables through the $_ENV superglobal.
$title = $_ENV["SWAGGER_TITLE"];Access env vars programmatically:
use Tina4\DotEnv;
DotEnv::loadEnv('.env'); // Load .env file (auto on server start)
DotEnv::getEnv('DATABASE_URL'); // Get value or null
DotEnv::getEnv('PORT', '7146'); // Get value with default
DotEnv::hasEnv('TINA4_DEBUG'); // true if set
DotEnv::requireEnv('DATABASE_URL'); // Throws if missing
DotEnv::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.
<?php
use 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() method
Router::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.
<?php
use 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 DATABASE_URL in .env. The framework reads it at startup and opens the connection.
DATABASE_URL=sqlite:///data/app.db
DATABASE_URL=postgres://localhost:5432/myapp
DATABASE_URL=mysql://localhost:3306/myapp
DATABASE_URL=firebird://localhost:3050/path/to/database.fdbAccess the connection through the Database class.
<?php
use 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.
<?php
use 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.
<?php
use 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 = <<<GQL
type 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 functions
extract(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 listener
Events::on("user.registered", function (array $payload): void {
echo "New user: " . $payload["email"];
});
// Fire once -- listener is removed after first call
Events::once("app.boot", function (): void {
echo "Booted!";
});
// Emit an event
Events::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);# .env
TINA4_LOG_LEVEL=TINA4_LOG_ALL # DEBUG | INFO | WARNING | ERROR | NONELog output goes to src/logs/ by default. Set TINA4_LOG_PATH 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 key
Router::get("/api/report/{year}", function ($request, $response) {
return $response->json(buildReport($request->params["year"]));
})->cache(3600, "report-" . date("Y"));Set TINA4_CACHE_ON=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 factory
Container::register("mailer", fn() => new Mailer($_ENV["SMTP_HOST"]));
// Bind a singleton
Container::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.
# .env
TINA4_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 project
tina4 init php my-project
# Start the dev server (default port 7146)
tina4 serve
# Start on a custom port
tina4 serve --port 8080
# Run inline tests
tina4 test
# Create a migration
tina4 migrate:create add-users-table
# Run all pending migrations
tina4 migrate
# Rollback the last migration
tina4 migrate:rollback
# Generate CRUD for a model
tina4 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.
# .env
TINA4_DEBUG=true
TINA4_MCP_PORT=7147 # optional override, defaults to main port + 1# Verify the server is running
curl 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 text
echo $fake->integer(1, 100); // random int between 1 and 100
echo $fake->uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// Seed for reproducible output
$fake = new FakeData(seed: 42);Use FakeData inside migrations, seeders, or test fixtures.