Chapter 36: Upgrading from v2 to v3#
1. Overview#
Tina4 v3 is a ground-up rewrite. Zero Composer dependencies. The built-in HTTP server, template engine, ORM, and database drivers are all native PHP. No Guzzle, no Twig, no Doctrine -- nothing external.
The patterns you know still work. Routes, ORM models, templates, migrations -- same concepts, cleaner implementation. The framework is faster, more consistent, and easier to deploy.
Three things to know before you start:
- Methods are camelCase.
$db->startTransaction(), not$db->start_transaction(). - Classes are PascalCase.
Router,Database,ORM,Response. - Zero dependencies.
composer installpulls exactly one package:tina4stack/tina4-php.
This chapter walks through every breaking change and gives you a step-by-step migration checklist at the end.
Step 0: Run the Automated Upgrade Command#
Before doing anything manually, run the automated upgrade tool:
cd your-v2-projecttina4 i-want-to-stop-using-v2-and-switch-to-v3This command automatically:
- Moves
routes/,orm/,templates/,scss/,public/,app/,locales/,seeds/intosrc/ - Updates your
composer.jsondependency from v2 to v3 - Removes split packages (
tina4php-core,tina4php-database,tina4php-orm)
After running the command, continue with the steps below for the changes that require manual attention.
2. Package and Installation#
v2#
composer require tina4stack/tina4phpThis pulled in a tree of Composer dependencies -- HTTP clients, template engines, database abstractions.
v3#
composer require tina4stack/tina4-phpNote the hyphen: tina4-php, not tina4php. This is the only package. It has zero Composer dependencies. Everything is built in.
Update your composer.json:
{ "require": { "tina4stack/tina4-php": "^3.0" }}Then run:
composer updateRemove any v2-era Tina4 packages from composer.json (tina4stack/tina4php, tina4stack/tina4-database, etc.). They are all consolidated into the single tina4-php package.
3. Project Structure Changes#
v2 Structure#
project/ routes/ orm/ templates/ migrations/ .env index.phpv3 Structure#
project/ src/ routes/ orm/ templates/ app/ migrations/ .env index.phpThe key change: source files live under src/. Routes go in src/routes/, ORM models in src/orm/, templates in src/templates/, and application logic in src/app/.
Auto-discovery still works the same way. Every .php file in src/ and its subdirectories is auto-included at startup. No manual registration. Drop a file in, it loads.
Migration path: Move your existing routes/, orm/, and templates/ directories into src/. Create src/app/ for any utility classes or service files.
4. Routing Changes#
Method-Based Registration#
The core pattern is the same. The class name changed from Route to Router:
v2:
<?php\Tina4\Get::add("/hello", function ($response, $request) { return $response("Hello, World!");});v3:
<?phpuse Tina4\Router;โRouter::get("/hello", function ($request, $response) { return $response->json(["message" => "Hello, World!"]);});Three changes:
\Tina4\Get::add()becomesRouter::get(). Same forPost,Put,Patch,Delete.- The callback signature flips:
$requestcomes first, then$response. - Response methods are explicit:
$response->json(),$response->render(),$response->text().
Class-Based Routes with Attributes#
v3 supports PHP 8 attributes for class-based routes:
<?phpuse Tina4\Router;โclass ProductController{ #[Router("/products", "GET")] public function list($request, $response) { return $response->json(["products" => []]); }โ #[Router("/products", "POST")] public function create($request, $response) { return $response->json(["created" => true], 201); }}Auth Defaults#
This is a breaking change. v3 has opinionated auth defaults:
| Method | v2 Default | v3 Default |
|---|---|---|
| GET | Public | Public |
| POST | Public | Requires auth |
| PUT | Public | Requires auth |
| PATCH | Public | Requires auth |
| DELETE | Public | Requires auth |
Write operations require authentication by default. Two attributes control this:
#[NoAuth]-- Makes a write route public (no auth required).#[Secured]-- Requires auth on a GET route.
<?phpuse Tina4\Router;use Tina4\NoAuth;use Tina4\Secured;โ// This POST route is public -- no auth needed#[NoAuth]Router::post("/register", function ($request, $response) { // Public registration endpoint return $response->json(["registered" => true], 201);});โ// This GET route requires auth#[Secured]Router::get("/admin/dashboard", function ($request, $response) { return $response->render("admin/dashboard.html");});If your v2 app had unprotected POST/PUT/PATCH/DELETE routes, add #[NoAuth] to each one or they will return 401. Review every write route during migration.
5. Database Changes#
Connection Strings#
The format is the same URL-based approach, but the syntax is cleaner:
v2:
$db = new \Tina4\DataSQLite3("data/app.db");v3:
TINA4_DATABASE_URL=sqlite:///data/app.dbOr in code:
<?phpuse Tina4\Database;โ$db = new Database("sqlite:///data/app.db");All drivers use the same Database class. The URL scheme selects the driver:
# SQLiteTINA4_DATABASE_URL=sqlite:///data/app.dbโ# PostgreSQLTINA4_DATABASE_URL=postgres://localhost:5432/myappโ# MySQLTINA4_DATABASE_URL=mysql://localhost:3306/myappโ# FirebirdTINA4_DATABASE_URL=firebird://localhost:3050/path/to/database.fdbโ# Microsoft SQL ServerTINA4_DATABASE_URL=mssql://localhost:1433/myappFirebird Notes#
v3 supports both the legacy interbase and modern firebird-driver PHP extensions. Column names are returned in lowercase by default. If your v2 code relied on uppercase column names from Firebird, update your references.
Transactions#
Transaction methods are renamed to camelCase:
v2:
$db->beginTransaction();// ... queries ...$db->commit();$db->rollBack();v3:
$db->startTransaction();// ... queries ...$db->commit();$db->rollback();Note: beginTransaction() becomes startTransaction(), and rollBack() (capital B) becomes rollback() (lowercase b).
6. ORM Changes#
Auto-Mapping#
The biggest ORM improvement in v3 is $autoMap. Set it to true and Tina4 automatically generates $fieldMapping entries from your camelCase PHP properties to snake_case database columns.
v2:
<?phpclass Product extends \Tina4\ORM{ public $tableName = "products"; public $primaryKey = "id";โ public $id; public $productName; public $unitPrice; public $inStock;โ public $fieldMapping = [ "productName" => "product_name", "unitPrice" => "unit_price", "inStock" => "in_stock" ];}v3:
<?phpuse Tina4\ORM;โclass Product extends ORM{ public string $tableName = "products"; public string $primaryKey = "id"; public bool $autoMap = true;โ public int $id; public string $productName; public float $unitPrice; public bool $inStock = true;}With $autoMap = true, Tina4 sees $productName and auto-generates the mapping to product_name. Same for $unitPrice to unit_price and $inStock to in_stock. No manual $fieldMapping needed.
Explicit Mappings Take Precedence#
If you have a column that does not follow the convention, add it to $fieldMapping manually. Explicit entries override auto-generated ones:
<?phpuse Tina4\ORM;โclass Legacy extends ORM{ public string $tableName = "legacy_table"; public bool $autoMap = true;โ public int $id; public string $firstName; // auto-maps to first_name public string $legacyField; // override belowโ public array $fieldMapping = [ "legacyField" => "LEGACY_FLD" // takes precedence over auto-map ];}Utility Methods#
Two helper methods are available for manual conversions:
$snake = ORM::camelToSnake("firstName"); // "first_name"$camel = ORM::snakeToCamel("first_name"); // "firstName"Typed Properties#
v3 models use PHP 8 typed properties. Add types to all your ORM properties:
// v2public $price;โ// v3public float $price = 0.00;7. Template Engine Changes#
Tina4 v3 uses Frond as its template engine. If you were using Twig in v2, most syntax carries over -- Frond is Twig-compatible.
Frond Singleton#
Access the Frond instance through the Response class:
<?phpuse Tina4\Response;โ// Get the Frond instance$frond = Response::getFrond();โ// Add a custom filter$frond->addFilter("shout", function ($value) { return strtoupper($value) . "!!!";});โ// Add a global variable$frond->addGlobal("appName", "My App");โ// Set the instance back (if needed after modification)Response::setFrond($frond);Custom Filters and Globals#
Custom filters and globals persist across requests within the same process. Register them once at startup (in src/app/ or early in index.php) and they are available in every template render.
Method Calls in Templates#
v3 adds the ability to call methods on array or object values inside templates:
<!-- Call a method on an object --><p>{{ user.getName() }}</p> <!-- Call with arguments --><p>{{ translator.t("welcome_message") }}</p> <!-- Chain with filters --><p>{{ user.getName()|upper }}</p>This is new in v3. v2 only supported property access ({{ user.name }}), not method calls.
8. Migration Tracking Table#
v3 uses an enhanced migration tracking table with additional columns for better tracking.
When you run migrations on a database that was previously managed by v2, Tina4 v3 auto-detects the old tracking table format and upgrades it. The upgrade adds three columns:
migration_name-- The migration filename, used to match a file to its record.batch-- Groups migrations that ran together.executed_at-- Timestamp of when the migration was executed.
The v2 migration_id and passed columns stay in place. If the v2 table holds passed = 0 rows (migrations that failed under v2), the upgrade logs a warning naming them. Those migrations re-apply on the next migration run: the runner deletes the stale passed = 0 row and writes a fresh passed = 1 row, so a previously-failed migration runs again cleanly instead of colliding on the unique migration_name. Existing migration records are preserved. The upgrade happens automatically on the first migration run. No manual intervention needed.
9. New Features in v3#
Features that did not exist in v2. Each is covered in its own chapter:
- Built-in HTTP server -- No Apache or Nginx needed for development (Chapter 1).
- Connection pooling --
poolparameter on database connections (Chapter 5). - Query builder -- Fluent SQL without raw strings (Chapter 7).
- Job queues -- Background task processing (Chapter 12).
- WebSocket support -- Real-time communication built in (Chapter 23).
- Email sending -- Native SMTP, no SwiftMailer (Chapter 16).
- Caching layer -- File, Redis, Valkey, Mongo backends (Chapter 11).
- GraphQL -- Built-in GraphQL endpoint (Chapter 22).
- CLI tooling --
tina4command for scaffolding, migrations, serving (Chapter 30). - Auto-mapping in ORM --
$autoMap = trueeliminates manual field mappings (Chapter 6). - Rate limiting -- Per-IP rate limiting via env vars (Chapter 10).
- CSRF protection -- Built-in, enabled by default (Chapter 10).
Common Pitfalls#
1. POST/PUT/DELETE routes now require authentication#
This is the most common upgrade issue. In v2, all routes were public by default. In v3, only GET routes are public -- POST, PUT, PATCH, and DELETE require a Bearer JWT token.
Symptom: Working v2 endpoints return 401 Unauthorized after upgrading.
Fix: Add #[NoAuth] or ->noAuth() to any write route that should remain public.
Find affected routes:
grep -rn "Router::post\|Router::put\|Router::patch\|Router::delete" src/routes/Review each match -- if the endpoint should be public (webhooks, public forms, etc.), add the #[NoAuth] attribute.
2. Database connection strings changed#
v2 used driver-specific classes. v3 uses URL format:
// v2$db = new \Tina4\DataSQLite3("data/app.db");// v3$db = Database::create("sqlite:///data/app.db");3. Template engine renamed#
v2: Template.render() -- v3: Frond.render() (or $response->render())
The Twig syntax is the same -- your .twig files work unchanged. Only the PHP API call changes.
10. Step-by-Step Migration Checklist#
Follow these steps in order. Check each one off as you go.
- Back up your v2 project. Copy the entire project directory. You want a rollback point.
- Update
composer.json. Replacetina4stack/tina4phpwithtina4stack/tina4-php. Remove any othertina4stack/*packages. Runcomposer update. - Restructure directories. Move
routes/tosrc/routes/,orm/tosrc/orm/,templates/tosrc/templates/. Createsrc/app/for utility classes. - Update namespace imports. Replace
\Tina4\Get::add()withRouter::get(),\Tina4\Post::add()withRouter::post(), and so on. Adduse Tina4\Router;at the top of each route file. - Fix callback signatures. Swap the parameter order from
($response, $request)to($request, $response)in every route callback. - Update response calls. Replace
$response("data")with$response->json(),$response->render(), or$response->text()as appropriate. - Review auth on write routes. Every POST, PUT, PATCH, and DELETE route now requires auth by default. Add
#[NoAuth]to any write route that should be public (registration, public form submissions, webhooks). - Add
#[Secured]to protected GET routes. Any GET route that should require authentication needs the#[Secured]attribute. - Update database connection code. Replace driver-specific classes (
DataSQLite3,DataMySQL, etc.) withnew Database("url"). Move connection strings toTINA4_DATABASE_URLin.env. - Fix transaction calls.
beginTransaction()becomesstartTransaction().rollBack()becomesrollback(). - Add typed properties to ORM models. Replace untyped
public $name;with typedpublic string $name;. Add$autoMap = trueand remove manual$fieldMappingentries where auto-mapping covers the conversion. - Update template code. If you were using Twig directly, switch to Frond. The syntax is compatible. Replace any Twig-specific PHP calls with
Response::getFrond(). - Run migrations. Start the server and trigger a migration run. Tina4 auto-upgrades the tracking table. Verify your existing migrations still apply cleanly.
- Test every route. Hit every endpoint. Check auth behaviour. Verify response formats. The test chapter (Chapter 18) covers how to write automated tests.
- Remove leftover v2 files. Delete the old
routes/,orm/, andtemplates/directories at the project root (now that everything is undersrc/). Clean up any v2-specific config files.
The migration is mechanical. Most of it is find-and-replace. The auth defaults are the one change that can silently break things -- step 7 is the most important step on this list.