Skip to content

9 August 2026

3 changes:

  • pramnos init works in a freshly created project again
  • Scaffolded Docker images ship the database CLI client
  • A failed schema import is no longer silent

pramnos init works in a freshly created project again

app/app.php is now treated as an optional file when the kernel boots, so the console front controller no longer fatals in a project that has not been scaffolded yet — which is exactly the project init is meant to scaffold.

The problem

The documented way to start a new project is:

mkdir my-app && cd my-app
composer init -n
composer require mrpc/pramnosframework
php vendor/bin/pramnos init

The last line died before printing anything:

PHP Warning:  require(/path/my-app/app/app.php): Failed to open stream: No such file or directory
              in .../Pramnos/Application/Application.php on line 148
PHP Fatal error:  Uncaught Error: Failed opening required '/path/my-app/app/app.php'

Pramnos\Console\Application::__construct() builds an internal Application::getInstance() for every command (commands need it for the database, the app namespace, and so on). That constructor did a bare require APP_PATH . '/app.php' — but in a brand-new project app/app.php does not exist yet, and creating it is init's whole job. A require failure is an uncatchable fatal, so getInstance()'s try/catch could not soften it either: the framework could not be bootstrapped into a fresh directory at all.

The fix

Reading the application configuration moved into a small, tolerant loader:

protected static function loadApplicationInfo($file)
{
    if (!file_exists($file)) {
        return array();
    }
    $info = require $file;
    return (is_array($info) || is_object($info)) ? $info : array();
}

Both constructor branches (default app and named app) now go through it. A missing config yields an empty $applicationInfo; a file that returns a scalar (a half-written config, or one missing its return) degrades to the same empty array instead of assigning a scalar that would break every later $app->applicationInfo['…'] read. An existing file — including one returning an array-like object — is returned exactly as before.

This is safe because every consumer already reads the individual keys defensively (isset() / ??) with its own default — MakeCommandBase, for example, falls back to the App namespace. For a scaffolded project nothing changes: the file exists and is returned verbatim.

Result

php vendor/bin/pramnos list   # works in an empty project
php vendor/bin/pramnos init   # scaffolds, syncs dependencies, prints the summary

Tests

tests/Unit/Application/ApplicationInfoLoadingTest.php — missing file → [], existing file → returned verbatim, object config → preserved, scalar return → [].

Why it started failing

The bare require dates back to 2020 (96604e22), but until recently it was never reached in a fresh project. With no app/app.php, getInstance() fell back to ['namespace' => 'Pramnos'], which resolved to \Pramnos\Application — a namespace, not a class. class_exists() said no, nothing was instantiated, and getInstance() handed back null. The console application carried a null internalApplication, init never touched it, and the scaffold ran fine.

f5891202 (make an app-specific Application subclass optional, 29 Jul 2026) gave that resolution a fallback to the base kernel. Correct in itself — but it means the kernel really is instantiated in a project that has no config yet, so the constructor's require finally ran, and fataled.

The fix restores the fresh-project flow without giving up that fallback: the console now gets a working base kernel instead of a null, and a missing config is simply an empty config.

Compatibility

Only previously-fatal cases change behaviour, so no working setup is affected. The trade-off worth knowing: a deployment that loses its app/app.php (or has a wrong APP_PATH) no longer dies loudly — it boots with an empty config, meaning no addons, no configured middleware and no features. That failure is now quiet rather than immediate.

Scaffolded Docker images ship the database CLI client

pramnos init now installs postgresql-client or default-mysql-client — whichever matches the project's database — into the generated Dockerfile, so a schema dump import actually runs instead of silently doing nothing.

The silent no-op

TestEnvironment::setup() — called by every scaffolded project's tests/bootstrap.php — imports an optional schema dump by shelling out to the database's command-line client:

'PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -f %s > /dev/null 2>&1'

Note the redirect. If the client is not installed, the shell's "command not found" goes to /dev/null, shell_exec() returns nothing, no exception is raised — and the test database is simply left empty. Every later test then fails somewhere else, for reasons that have nothing to do with the real cause.

Neither the generated image nor the framework's own dev image had ever installed those clients (the PHP driverspdo_pgsql, pdo_mysql, mysqli — were always there, and they are what everything else uses). The gap stayed invisible because the tests covering the import branch asserted nothing at all: they wrapped the call in try { … assertTrue(true); } catch (\Exception) { assertTrue(true); }, which passes whatever happens.

What changed

  • Generated projects: scaffoldDocker() adds the client matching the selected engine — postgresql-client for postgresql/timescaledb, default-mysql-client for mysql. Only one of the two, so the image does not grow for nothing. It also makes ./dockerbash a usable place to inspect the database by hand.
  • This repository's dev image: the same two packages, so the framework's own suite exercises the import for real.
  • The tests that hid it: the assertion-free test_real_setup_* trio is gone. In its place are tests that assert an observable effect — the dump now creates a probe table, and the test checks that the table exists in the freshly created database. A dump of SELECT 1; (what they used before) leaves no trace at all, so it could never tell an import that ran from one that did nothing.

Rebuilding

Existing environments need one rebuild to pick the client up:

docker-compose build php-apache-environment   # this repository
docker-compose build app                      # a scaffolded project

Until then the import tests skip with an explicit "the 'psql' client is not installed in this container" message rather than failing — the missing binary is an environment gap, not a regression in the code under test.

A failed schema import is no longer silent

TestEnvironment now checks the exit status of the psql / mysql import and raises a RuntimeException carrying the client's own output, instead of discarding every error and handing back an empty test database.

Before

Both import branches sent everything to /dev/null and ignored the exit status:

'PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -f %s > /dev/null 2>&1'

So a missing client binary, a syntax error in the dump, wrong credentials — all looked identical to success. The database was created but never populated, and the suite then failed much later, somewhere unrelated. Worse, psql exits 0 even when individual statements fail, so the status alone would not have been enough anyway.

After

  • Both commands drop the redirect and run through a new TestEnvironment::runImport(), which captures stdout+stderr and the exit status.
  • Non-zero exit → RuntimeException including the client's diagnostics.
  • Status 127 gets its own message — the client binary is not installed, and the bare "psql: not found" would not explain the consequence.
  • The PostgreSQL import gained -v ON_ERROR_STOP=1, so a dump whose statements fail actually reports failure rather than exiting 0.
RuntimeException: Schema import failed: psql exited with status 3:
ERROR:  relation "a_table_that_does_not_exist" does not exist

Compatibility

runCommand() is untouched, and a successful import behaves exactly as before. The change is only visible where an import was already broken: what used to be a silent empty database is now an exception at the point of failure. If a project relied on the import quietly doing nothing (for example a dump that no longer applies), remove the schema path from the TestEnvironment::setup() call instead.

Tests

TestEnvironmentTest — a dump that fails mid-file raises with psql's diagnostics in the message, a missing binary is reported as such, any other non-zero status carries status and output, and a zero status stays silent.