Releases: tina4stack/tina4-php
3.12.1
ci(publish): make contents:write explicit on the release job
PHP's release step works today because tina4-php's repo-level Workflow
permissions setting is "Read and write". That's a runtime click in
Settings → Actions, not version-controlled. If someone flipped that
setting tomorrow, every release would silently start 403'ing.
Declaring permissions: contents: write on the job is the bulletproof
form — the GITHUB_TOKEN gets the right scope regardless of repo
defaults. Aligned with the same fix landing in tina4-ruby and
tina4-nodejs (where this WAS biting us).
Also bumping softprops/action-gh-release v1 → v2 (v1 unmaintained).
3.12.0
release: v3.12.0 — env var TINA4_ prefix, bug fixes #36-39
BREAKING CHANGE. Every framework env var now requires TINA4_ prefix.
Hard rename, no fallback chain. Setting any of the 22 legacy un-prefixed
names (DATABASE_URL, SECRET, SMTP_HOST, HOST_NAME, etc.) makes the
framework refuse to boot — App::checkLegacyEnvVars() prints the rename
map and exit(2)s. Bypass via TINA4_ALLOW_LEGACY_ENV=true for migration.
PHP guard reads three sources (getenv(), $_ENV, $_SERVER) since CLI/FPM/
Apache populate them inconsistently. Stored as App::LEGACY_ENV_VARS
class constant, public for test introspection.
What's in this release:
-
Hard env var rename (22 vars) + boot guard
- Tina4/App.php: LEGACY_ENV_VARS constant + checkLegacyEnvVars(),
hooked into start() as the first action - All 17 framework PHP files updated — getenv()/getEnv()/$ENV reads
of legacy names removed; only TINA4 prefixed names are read now - PORT/HOST/NODE_ENV/RACK_ENV/RUBY_ENV/ENVIRONMENT stay un-prefixed
- Tina4/App.php: LEGACY_ENV_VARS constant + checkLegacyEnvVars(),
-
New regression test
- tests/LegacyEnvGuardTest.php — 36 tests, 86 assertions:
22-entry mapping shape, every legacy var trips guard, getenv/
$_ENV/$_SERVER detection, TINA4_ALLOW_LEGACY_ENV bypass with
truthy/falsy values, full error-message contents
- tests/LegacyEnvGuardTest.php — 36 tests, 86 assertions:
-
Bug fixes (originally tracked for 3.11.33, folded into 3.12.0)
- #39 Landing + template auto-routing:
• Tina4/Router.php: pages/-scoped resolveTemplate() + buildTemplateCache()
• TINA4_TEMPLATE_ROUTING=off kills auto-routing
• Tina4/StaticFiles.php: index.html resolution at / and /foo/
• Tina4/App.php: landing page route only when TINA4_DEBUG=true
• Tina4/Server.php: RFC 7231/9110 reason phrase table — no more
"HTTP/1.1 404 OK" - #38 PostgreSQL UUID-PK transaction abort:
• Tina4/Database/PostgresAdapter.php: SAVEPOINT _t4_lastval_probe
wrap; UUID INSERTs no longer poison outer txn - #37 frond.form.submit redirect — verified shipped (frond v2.1.3)
- #36 Session file handler — safeguards re-verified
- #39 Landing + template auto-routing:
-
composer.json: 'version' field NOT included.
Packagist reads from the git tag — having both breaks the crawler
(see commit 171f37b for the full story). -
Frond v2.1.3 bundle (version-stamped footer)
Tests: 2502 tests, 5658 assertions, 0 failures, 11 skipped (pre-existing).
New test files:
- tests/LandingPageTest.php (44 tests, 460 assertions)
- tests/PostgresUuidPkTest.php (6 tests, 10 assertions, live PG)
- tests/LegacyEnvGuardTest.php (36 tests, 86 assertions)
Migration:
$ tina4 env-migrate # rewrites .env automatically (Python CLI;
# use sed for now in PHP-only projects)
Or rename manually using the map in book chapter 33.
Coordinated release across all 4 frameworks at 3.12.0.
3.11.36
fix(packagist): remove hardcoded version field from composer.json
Packagist's crawler skipped every tag from 3.11.33 onwards with:
"Skipped tag X.Y.Z, tag (X.Y.Z.0) does not match version (3.11.32.0)"
The version field was re-added in 8b4ddc3 (v3.11.32 release) — it had
been removed in 52baf4e for this exact reason. Hardcoded versions in
composer.json on git-tagged packages always break Packagist sync; the
crawler reads the tag name as the canonical version.
Removing the field permanently. Future tag pushes will sync correctly.
Affected releases now sitting in GitHub but invisible on Packagist:
3.11.33 — security patch ($SERVER HTTP* leak)
3.11.34 — CI fix (Auth SECRET resolution)
3.11.35 — Firebird auto-reconnect on dead sockets
This release (3.11.36) carries the same content as 3.11.35 plus this
single composer.json one-liner — push exists primarily so Packagist
has a tag without the version field to ingest.
3.11.35
fix(firebird): one-shot reconnect on dead-socket errors
Idle Firebird connections die silently behind NAT timeouts, server-side
ConnectionIdleTimeout, or Docker network rotation. The next ibase_prepare
crashes with "Error writing data to the connection." (or "connection
shutdown", "Connection lost", etc.) and the request blows up.
FirebirdAdapter::executeInternal now wraps the underlying ibase calls in a
one-shot reconnect-and-retry: on a dead-connection error marker, close
the stale handle (silently), reopen using cached connection params, and
retry the statement once on a fresh handle. Skipped inside an explicit
transaction — atomicity beats resilience there; caller handles rollback.
Public API: FirebirdAdapter::isDeadConnection(?string) for spec/test reuse.
15 regression tests in FirebirdReconnectTest.php cover the matcher
(real-world wording, logical-error rejection, case insensitivity, null
input).
Parity: identical fix shipping to tina4-python and tina4-ruby in 3.11.35.
3.11.34
fix(ci): Auth SECRET resolution prefers getenv() over $_ENV
Auth::getToken and Auth::validToken resolved SECRET as
$_ENV['SECRET'] ?? getenv('SECRET'). Router::dispatch then read
getenv('SECRET') and passed it explicitly to validToken. When CI
loads SECRET into $_ENV (via .env or shell env) and a test calls
putenv("SECRET=test-value") to override, the two sources disagree:
- getToken signs with $_ENV['SECRET'] (stale)
- Router resolves getenv('SECRET') (the test's value)
- validToken receives the explicit secret (the test's value)
- Signature mismatch → 401
Manifested as RouterV3Test::testDispatchSecureRouteWithToken failing
on every push to v3 since 3.11.29 (401 vs 200).
Fix: flip the priority — getenv() ?: $_ENV[...]. putenv() at runtime
now consistently overrides, matching standard PHP idiom. DotEnv::load
already writes to BOTH sources via setVariable(), so .env-loaded values
are unaffected.
Also: Router::dispatch no longer pre-resolves the secret; it calls
Auth::validToken($token) with no secret arg, letting Auth resolve
once. One source of truth.
Tests: 4 new regression tests in AuthV3Test (getenv-overrides-$_ENV,
explicit-secret pattern, direct priority probe, JWT_ALGORITHM parity).
RouterV3Test setUp/tearDown now snapshot/clear SECRET in BOTH $_ENV
and getenv to prevent cross-test contamination on shared processes.
Total: 2434 tests passing.
3.11.33
fix: drop unset PK from save() payload + ORMV3 tests + entrypoint
ORM.php: when save() runs an INSERT and the primary key is null/0/empty
on the model, drop it from the column list so the database's auto-
increment / sequence assigns a value. Without this, the declared
public int $id = 0 default leaked into the INSERT as id = 0, SQLite
stored 0 literally, lastInsertId() returned 0, and the property never
synced back. (Issue #102 follow-up.)
tests/ORMV3Test.php: 170 lines of new regression coverage for save(),
PK auto-assignment, and the public-property → row round-trip.
index.php: $app->handle() → $app->run() (handle is for single-request
SAPI dispatch, run is the right entry under the built-in server).
3.11.32
release: v3.11.32 — pool/txn atomicity fix + parity alignment
Critical fix. Database with poolSize>0 silently broke transactions.
The pool's round-robin getNextAdapter() rotated to a different adapter
on every call — startTransaction() pinned its flag on adapter A, the
executes autocommitted on adapters B and C, and the final
commit()/rollback() landed on adapter D, which had nothing to commit.
Result: rollback() was a no-op, writes leaked through, no error or
log surfaced the problem.
The fix: per-instance pinned adapter in Database. While inside a
transaction, getNextAdapter() returns the pinned adapter so the
whole transaction runs on one connection. startTransaction() sets
the pin, commit() and rollback() clear it. PHP-FPM is one process
per request, so a plain instance property is the right primitive
(Python uses threading.local for its multi-threaded model).
- fix (Tina4/Database/Database.php): adapter pinning across
transaction scope. Every backend affected. - tests (tests/PoolTransactionAtomicityTest.php): new regression
suite — six tests covering rollback under pool=4, commit under
pool=4, pin release after commit, pin release after rollback,
pool=0 no-regression, pin honoured across executes. All pass;
full suite 2416 tests green. - composer.json: version field added (3.11.32). App::resolveVersion()
reads from composer metadata at runtime. - parity: all 4 frameworks aligned at 3.11.32. Coordinated release
across PyPI, Packagist, RubyGems, npm.
3.11.31
release: v3.11.31 — Live Docs (live API RAG) + dev-admin UX overhaul
The framework is now a live RAG. AI tools (Claude Code, Cursor,
dev-admin chat) hit /__dev/api/docs/* on the running server and get
ground-truth signatures for both framework public API AND the user's
own src/ code via Tina4\Docs reflection. No staleness, no cross-version
drift, no privacy concerns — the running server IS the source of
truth. Auto-discovery via .tina4/mcp.json so MCP-aware tools just find
it. See plan/v3/22-LIVE-API-RAG.md for the full design.
Major surface:
- New \Tina4\Docs class — token-parser reflection over Tina4/* and
the user's src/{orm,routes,app,services,services} dirs. Search,
classSpec, methodSpec, index, drift detection, sync. 16 tests. - HTTP /__dev/api/docs/{search,class,method,index,.well-known.json}
endpoints — thin wrappers around Docs. - MCP tools api_search / api_class / api_method registered alongside
existing docs_* (markdown search) — same names across all 4
frameworks for cross-framework AI-tool compatibility. - .tina4/mcp.json auto-discovery written on first server boot in
debug mode. .gitignore amended automatically.
Dev-admin UX overhaul (cross-framework parity in the same release wave):
- Dev toolbar now injects on 404 / 403 / 500 error pages — error
pages are no longer dead-ends. - Error overlay HTML embeds the toolbar inline, with a one-click
link to the dev-admin SPA. - Dev-admin overlay state persisted to localStorage so saving a file
(which kicks the file watcher → page reload) doesn't dismiss the
user's chat / plan / file tree. Auto-restored on next page paint. - opcache_reset() on server boot in debug mode + opcache_invalidate()
per-file on /__dev/api/reload POST. Fixes the symptom where
patched method calls report "Call to undefined method ::template()"
long after the source has been corrected.
Documentation cleanup (CLAUDE.md):
- Phantom methods removed: AI::detectAi/AI::detectAiNames/
AI::installContext/AI::statusReport (replaced with the real
isInstalled/showMenu/installSelected/installAll/generateContext
surface), Api::addCustomHeaders → addHeaders,
Api::setUsernamePassword → setBasicAuth,
Migration::doMigration → migrate. - \Tina4\Debug::message rewritten to Log::debug/info/warning/error
(the real class name).
PHPUnit: 2410 / 2410 still green. New: DocsTest (16 examples).
4 PHP-deprecation warnings remaining (down from 7) — all in test
files (setAccessible), out of scope for framework releases.
3.11.30
release: v3.11.30 — /image proxy + fuzzier intent regex in chat
The SPA's image-generation flow (aiGenerateImage) was POSTing to
/image/v1/images/generations expecting SDXL Turbo on the other end.
In Vite dev that path is proxied to andrevanzuydam.com:11436
directly; in deployed framework it hit PHP and got 404 ('No image
returned: 404 …'). Add a passthrough proxy in
DevAdmin so the deployed flow works the same as Vite dev. Reads
TINA4_IMAGE_URL (default http://andrevanzuydam.com:11436), 180s
timeout to cover SDXL's slow first-token.
Bundle refresh ships a fuzzier image-intent regex: 'pciture'
(typo), 'pic', 'photo', 'sketch', 'art' all match now. Bare
'draw ' or 'sketch ' also match without needing
the noun. Without this, 'make a pciture of a cow' fell through to
the LLM which (correctly) said it can't generate images — the user
read that as the system lying.
3.11.29
release: v3.11.29 — fputcsv $escape explicit (PHP 8.5)
PHP 8.5 deprecated using fputcsv() without an explicit $escape
argument; PHP 9 will change the default. Pass '\' explicitly so
existing CSV consumers see no behavioural change. Was producing a
deprecation warning on every DatabaseResult::toCsv() call.
Test deprecations: 5 → 4 (3 remaining are setAccessible() calls in
tests/ only, out of scope for framework releases).