Chapter 30: Tina4 CLI#
1. Getting a New Developer Up to Speed#
A new developer joins your team Monday morning. You hand them the repo URL. By 10am they have a running project, a new database model, CRUD routes, a migration, and a deployment to staging. All from the command line. No hunting through documentation. No copy-pasting boilerplate from old projects. The CLI handles scaffolding.
The Tina4 CLI is a single Rust binary. It manages all four Tina4 frameworks (PHP, Python, Ruby, Node.js). Commands stay identical across languages. Learn the CLI for PHP. You know it for Python.
2. tina4 init -- Project Scaffolding#
You saw this in Chapter 1. Now the details.
tina4 init my-projectCreating Tina4 project in ./my-project ... Detected language: PHP (composer.json) Created .env Created .env.example Created .gitignore Created src/routes/ Created src/orm/ Created migrations/ Created src/seeds/ Created src/templates/ Created src/templates/errors/ Created src/public/ Created src/public/js/ Created src/public/css/ Created src/public/scss/ Created src/public/images/ Created src/public/icons/ Created src/locales/ Created data/ Created logs/ Created secrets/ Created tests/โProject created! Next steps: cd my-project composer install tina4 serveLanguage Detection#
The CLI detects the language from existing files in the directory:
| File Present | Language |
|---|---|
composer.json | PHP |
pyproject.toml or requirements.txt | Python |
Gemfile | Ruby |
package.json | Node.js |
If no language-specific file exists, the CLI asks you:
tina4 init my-projectNo language detected. Which language? 1. PHP 2. Python 3. Ruby 4. Node.js> 1โCreating Tina4 PHP project in ./my-project ...Init into an Existing Directory#
Already have a project? Add Tina4 structure:
cd existing-projecttina4 init .The CLI creates only files and directories that do not exist. It never overwrites.
3. tina4 serve -- Dev Server#
tina4 serve _____ _ _ _|_ _(_)_ __ __ _| || | | | | | '_ \ / _` | || |_ | | | | | | | (_| |__ _| |_| |_|_| |_|\__,_| |_|โ Tina4 PHP v3.0.0 Server running at http://0.0.0.0:7145 Debug mode: ON Database: sqlite:///data/app.db Press Ctrl+C to stopOptions#
| Flag | Description | Example |
|---|---|---|
--port | Custom port (default: 7145) | tina4 serve --port 8080 |
--host | Bind address (default: 0.0.0.0) | tina4 serve --host 127.0.0.1 |
--no-reload | Disable live reload | tina4 serve --no-reload |
Production Mode Detection#
tina4 serve --productionWhen the --production flag is passed, the CLI:
- Checks for
TINA4_DEBUG=falsein.env(warns if debug is on) - Uses FrankenPHP if available for better performance
- Enables response caching and template pre-compilation
- Disables the dev toolbar and error overlay
- Enables graceful shutdown handling
In practice, you rarely use tina4 serve --production directly. Instead, you use Docker or a process manager (Chapter 33). But this flag is useful for quick production testing.
4. tina4 generate model -- ORM Scaffolding#
Generate a new ORM model:
tina4 generate model OrderCreated src/orm/Order.phpCreated migrations/20260322100000_create_orders_table.sqlThe generated model:
<?phpuse Tina4\ORM;โclass Order extends ORM{ public int $id; public string $createdAt; public string $updatedAt;โ public string $tableName = "orders"; public string $primaryKey = "id";}The generated migration:
-- UPCREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP);โ-- DOWNDROP TABLE IF EXISTS orders;Adding Fields#
You can specify fields on the command line:
tina4 generate model Order --fields "userId:int,total:float,status:string,paid:bool"Created src/orm/Order.phpCreated migrations/20260322100000_create_orders_table.sqlThe generated model now includes all the fields:
<?phpuse Tina4\ORM;โclass Order extends ORM{ public int $id; public int $userId; public float $total; public string $status; public bool $paid; public string $createdAt; public string $updatedAt;โ public string $tableName = "orders"; public string $primaryKey = "id";}And the migration:
-- UPCREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, total REAL NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT '', paid INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP);โ-- DOWNDROP TABLE IF EXISTS orders;Field Types#
| CLI Type | PHP Type | SQLite Column |
|---|---|---|
string | string | TEXT |
int | int | INTEGER |
float | float | REAL |
bool | bool | INTEGER |
text | string | TEXT |
date | string | TEXT |
Options#
| Flag | Description | Example |
|---|---|---|
--fields | Comma-separated field definitions | --fields "name:string,price:float" |
--auto-crud | Enable auto-CRUD on the model | --auto-crud |
--soft-delete | Add soft delete support | --soft-delete |
--no-migration | Skip migration generation | --no-migration |
5. tina4 generate route -- CRUD Route Scaffolding#
Generate CRUD routes for a model:
tina4 generate route OrderCreated src/routes/orders.phpThe generated route file:
<?phpuse Tina4\Router;โRouter::group("/api", function () {โ // List all orders Router::get("/orders", function ($request, $response) { $order = new Order(); $page = (int) ($request->params["page"] ?? 1); $perPage = (int) ($request->params["per_page"] ?? 20); $offset = ($page - 1) * $perPage;โ $orders = $order->select("*", "", [], "created_at DESC", $perPage, $offset); $results = array_map(fn($o) => $o->toArray(), $orders);โ return $response->json([ "data" => $results, "page" => $page, "per_page" => $perPage, "count" => count($results) ]); });โ // Get a single order Router::get("/orders/{id:int}", function ($request, $response) { $order = new Order(); $order->load($request->params["id"]);โ if (empty($order->id)) { return $response->json(["error" => "Order not found"], 404); }โ return $response->json($order->toArray()); });โ // Create an order Router::post("/orders", function ($request, $response) { $body = $request->body; $order = new Order(); $order->userId = (int) ($body["user_id"] ?? 0); $order->total = (float) ($body["total"] ?? 0); $order->status = $body["status"] ?? ""; $order->paid = (bool) ($body["paid"] ?? false); $order->save();โ return $response->json($order->toArray(), 201); });โ // Update an order Router::put("/orders/{id:int}", function ($request, $response) { $order = new Order(); $order->load($request->params["id"]);โ if (empty($order->id)) { return $response->json(["error" => "Order not found"], 404); }โ $body = $request->body; if (isset($body["user_id"])) $order->userId = (int) $body["user_id"]; if (isset($body["total"])) $order->total = (float) $body["total"]; if (isset($body["status"])) $order->status = $body["status"]; if (isset($body["paid"])) $order->paid = (bool) $body["paid"]; $order->save();โ return $response->json($order->toArray()); });โ // Delete an order Router::delete("/orders/{id:int}", function ($request, $response) { $order = new Order(); $order->load($request->params["id"]);โ if (empty($order->id)) { return $response->json(["error" => "Order not found"], 404); }โ $order->delete(); return $response->json(null, 204); });});The generator reads the model's properties and creates routes with type casting and null checks. Customize the generated code immediately. It is regular PHP, not magic.
Options#
| Flag | Description | Example |
|---|---|---|
--prefix | Custom route prefix (default: /api) | --prefix /api/v2 |
--middleware | Add middleware to all routes | --middleware requireAuth |
6. tina4 generate migration -- Migration Scaffolding#
Create an empty migration file:
tina4 generate migration "add email to orders"Created migrations/20260322101500_add_email_to_orders.sqlThe generated file:
-- UP-- Add your forward migration SQL hereโโ-- DOWN-- Add your rollback migration SQL hereโYou fill in the SQL yourself:
-- UPALTER TABLE orders ADD COLUMN email TEXT DEFAULT '';โ-- DOWNALTER TABLE orders DROP COLUMN email;The timestamp prefix (20260322101500) ensures migrations run in order. Each migration runs once. The framework tracks which ones have been applied.
7. tina4 generate middleware -- Middleware Scaffolding#
tina4 generate middleware rateLimiterCreated src/middleware/rateLimiter.phpThe generated file:
<?phpโfunction rateLimiter($request, $response, $next){ // Add your middleware logic hereโ // Continue to the next middleware or route handler return $next($request, $response);โ // Or return early to block the request: // return $response->json(["error" => "Rate limit exceeded"], 429);}The middleware is a named function that you can reference in route definitions:
Router::get("/api/data", function ($request, $response) { return $response->json(["data" => "protected"]);}, "rateLimiter");8. tina4 doctor -- Environment Health Check#
tina4 doctorTina4 Doctor - Environment Health Checkโ PHP Version .............. 8.3.4 [OK] Composer ................. 2.7.2 [OK] ext-json ................. loaded [OK] ext-mbstring ............. loaded [OK] ext-openssl .............. loaded [OK] ext-sqlite3 .............. loaded [OK] ext-fileinfo ............. loaded [OK] ext-pdo_sqlite ........... loaded [OK] .env file ................ found [OK] data/ directory .......... writable [OK] logs/ directory .......... writable [OK] secrets/ directory ....... writable [OK] Database connection ...... connected [OK] Routes discovered ........ 14 routes [OK] ORM models discovered .... 5 models [OK] Templates directory ...... found [OK] tina4.css ................ found [OK] frond.js ................. found [OK]โ All checks passed. Your environment is ready.If something is wrong, the doctor tells you:
ext-mbstring ............. missing [FAIL] Fix: apt install php8.3-mbstring (Ubuntu) brew install php (macOS, includes mbstring)โ data/ directory .......... not writable [FAIL] Fix: chmod 755 data/โ Database connection ...... failed [FAIL] Error: could not open database file Fix: Check TINA4_DATABASE_URL in .env. Current value: sqlite:///data/app.db Make sure the data/ directory exists and is writable.The doctor checks everything a new developer might get wrong. Specific fix instructions for each issue.
9. tina4 test -- Running Tests#
tina4 testRunning tests...โ ProductTest [PASS] test_create_product [PASS] test_load_productโ AuthTest [PASS] test_loginโ 3 tests, 3 passed, 0 failed (0.21s)See Chapter 18 for the full testing guide. The CLI command discovers all test files in tests/ and runs them.
Options#
| Flag | Description | Example |
|---|---|---|
--file | Run a specific test file | --file tests/ProductTest.php |
--method | Run a specific test method | --method testCreateProduct |
--verbose | Show all assertions | --verbose |
10. tina4 routes -- List All Routes#
tina4 routesMethod Path Middleware Auth------ ---- ---------- ----GET /health - publicGET /api/products - publicGET /api/products/{id:int} - publicPOST /api/products - securedPUT /api/products/{id:int} - securedDELETE /api/products/{id:int} - securedGET /api/orders - publicPOST /api/orders requireAuth securedGET /admin - publicGET /admin/users - publicFiltering#
# Filter by HTTP methodtina4 routes --method POSTโ# Filter by path patterntina4 routes --filter ordersโ# Filter by middlewaretina4 routes --middleware requireAuthWhen debugging routing issues, check here first. If a route is not matching, tina4 routes shows whether it was registered and what middleware is attached.
11. tina4 migrate -- Run Database Migrations#
tina4 migrateRunning migrations... [APPLIED] 20260322100000_create_orders_table.sql [APPLIED] 20260322101500_add_email_to_orders.sql [SKIPPED] 20260322080000_create_products_table.sql (already applied)Migrations complete. 2 applied, 1 skipped.Options#
| Flag | Description | Example |
|---|---|---|
--rollback | Undo the last migration | tina4 migrate --rollback |
--rollback-all | Undo all migrations | tina4 migrate --rollback |
--status | Show migration status | tina4 migrate --status |
Migration Status#
tina4 migrate --statusMigration Status:โ [APPLIED] 20260322080000_create_products_table.sql (applied 2026-03-22 08:15:00) [APPLIED] 20260322100000_create_orders_table.sql (applied 2026-03-22 10:05:00) [APPLIED] 20260322101500_add_email_to_orders.sql (applied 2026-03-22 10:20:00) [PENDING] 20260322120000_add_status_to_products.sql (not yet applied)โ 3 applied, 1 pending12. Exercise: Scaffold a Complete Feature in 5 Commands#
You need to add a "Tasks" feature to your project. Each task has a title, description, priority (string), completed status (boolean), and belongs to a user.
Requirements#
Using only CLI commands, do the following:
- Generate the Task model with fields
- Generate CRUD routes for the Task model
- Run the migration to create the table
- Verify the routes are registered
- Run the doctor to make sure everything is healthy
Expected Commands#
Complete this in exactly 5 commands. After running them, test the API with curl to verify everything works.
Test with:#
# Create a taskcurl -X POST http://localhost:7145/api/tasks \ -H "Content-Type: application/json" \ -d '{"title": "Write chapter 19", "description": "CLI and scaffolding", "priority": "high", "user_id": 1}'โ# List taskscurl http://localhost:7145/api/tasksโ# Get one taskcurl http://localhost:7145/api/tasks/113. Solution#
The five commands:
# 1. Generate the model with fields (creates model + migration)tina4 generate model Task --fields "userId:int,title:string,description:text,priority:string,completed:bool"โ# 2. Generate CRUD routestina4 generate route Taskโ# 3. Run the migrationtina4 migrateโ# 4. Verify routes are registeredtina4 routes --filter tasksโ# 5. Health checktina4 doctorCommand 1 output:
Created src/orm/Task.phpCreated migrations/20260322140000_create_tasks_table.sqlCommand 2 output:
Created src/routes/tasks.phpCommand 3 output:
Running migrations... [APPLIED] 20260322140000_create_tasks_table.sqlMigrations complete. 1 applied.Command 4 output:
Method Path Middleware Auth------ ---- ---------- ----GET /api/tasks - publicGET /api/tasks/{id:int} - publicPOST /api/tasks - securedPUT /api/tasks/{id:int} - securedDELETE /api/tasks/{id:int} - securedCommand 5 output:
Tina4 Doctor - Environment Health Check ... All checks passed. Your environment is ready.Test - create a task:
curl -X POST http://localhost:7145/api/tasks \ -H "Content-Type: application/json" \ -d '{"title": "Write chapter 19", "description": "CLI and scaffolding", "priority": "high", "user_id": 1}'{ "id": 1, "user_id": 1, "title": "Write chapter 19", "description": "CLI and scaffolding", "priority": "high", "completed": false, "created_at": "2026-03-22 14:05:00", "updated_at": "2026-03-22 14:05:00"}From zero to a working CRUD API in 5 commands and under 2 minutes.
14. Gotchas#
1. Model Name Must Be PascalCase#
Problem: tina4 generate model order_item creates a model class named order_item which is not valid PHP.
Cause: The CLI uses the argument as-is for the class name if it cannot determine the PascalCase form.
Fix: Always use PascalCase for model names: tina4 generate model OrderItem. The CLI converts it to snake_case for the table name (order_items).
2. Migration Already Applied#
Problem: You regenerated a model and the migration has the same name as an existing one, but the CLI says it was already applied.
Cause: Tina4 tracks applied migrations by filename. If the filename matches, it considers it already applied.
Fix: Each migration should have a unique timestamp prefix. If you need to modify a table after the initial migration, create a new migration: tina4 generate migration "add status to tasks".
3. Generated Routes Conflict with Auto-CRUD#
Problem: You generated routes for a model that has $autoCrud = true, and now requests hit the wrong handler.
Cause: Both the generated routes and auto-CRUD routes register at the same paths. The first one registered wins.
Fix: Either use generated routes or auto-CRUD, not both for the same model. If you use tina4 generate route, set $autoCrud = false on the model (or do not set it at all -- it defaults to false).
4. Port Already in Use#
Problem: tina4 serve fails with "Address already in use."
Cause: Another process is using port 7145 (or whichever port you configured).
Fix: Find and stop the other process, or use a different port:
tina4 serve --port 8080To find what is using the port:
lsof -i :71455. CLI Not Found After Installation#
Problem: tina4: command not found after running the install script.
Cause: The CLI binary is not in your PATH. The install script puts it in ~/.tina4/bin/ which may not be in your shell's PATH.
Fix: Add the binary location to your PATH. For zsh (macOS default):
echo 'export PATH="$HOME/.tina4/bin:$PATH"' >> ~/.zshrcsource ~/.zshrcFor bash:
echo 'export PATH="$HOME/.tina4/bin:$PATH"' >> ~/.bashrcsource ~/.bashrc15. Documentation#
tina4 docs # Download framework-specific book chapters to .tina4-docs/tina4 books # Download the complete Tina4 book (all languages) to tina4-book/tina4 docs detects your project language and downloads only the relevant chapters. The documentation is available in Markdown format, optimised for AI tools and local reference.
16. Test Port (Dual-Port Development)#
When TINA4_DEBUG=true, Tina4 automatically starts a second HTTP server on port + 1000:
- Main port (e.g. 7145), hot-reload enabled, for AI dev tools
- Test port (e.g. 8146), stable, no hot-reload, for user testing
This prevents the browser from refreshing mid-test when AI tools edit files.
| Setting | Effect |
|---|---|
TINA4_NO_AI_PORT=true | Disables the test port entirely |
TINA4_NO_RELOAD=true | Disables hot-reload on the main port too |
--no-reload | CLI flag equivalent of TINA4NORELOAD |