dappcore / php-install
Installer and schema-baseline module for CorePHP — fresh install, upgrade, and recovery
Requires
- php: ^8.2
- dappcore/php: *
Requires (Dev)
- pestphp/pest: ^4.0
README
Installer and schema-baseline module for CorePHP applications.
composer require dappcore/php-install
Register \Core\Mod\Install\Boot::class in the application's provider list. To
remove it, drop the provider and composer remove the package — nothing else in
the application depends on it.
Why it exists
A fresh install should not replay the migration history. History is edited: a create migration gets updated in place, and every migration after it then assumes a schema that no clean database has ever had. The run dies there, and it dies only on new installs, so nobody notices until someone installs.
This package keeps a flattened baseline of the schema and folds new migrations into it, so a fresh install loads one file and stops.
The install screen
Visit /install on a fresh checkout. Three steps: requirements → database →
run.
- Requirements checks PHP version, extensions, writable paths,
.envandAPP_KEYbefore anything is written, and every failure carries the command that fixes it. - Database tests credentials before saving them, and again on save — writing
credentials that do not work leaves an application that cannot boot, which is
worse than a failed form.
.envis edited key by key, through a temporary file and a rename, so comments and unrelated settings survive and an interrupted write cannot corrupt it. - Run shows the plan before executing it: load the baseline, run N pending migrations, or — in recovery — list the tables that are missing. It never drops anything.
It closes itself
The installer writes .env and runs migrations, so it must not stay reachable.
Once the schema is up to date the routes answer 404 — not a redirect, which
would confirm they exist. Set INSTALLER_ENABLED=true to reopen it deliberately
for a repair.
Known exposure: the connection test reaches where you point it
The database step connects to a host and port the request supplies. That is the feature — an installer cannot know your database's address — but it does mean that while the installer is open, whoever can reach it can ask the server to open TCP connections, and the deliberately specific error messages ("could not reach the server" versus "reached the server, but could not open the database") distinguish an open port from a closed one.
This is inherent to installing anything, and is bounded rather than eliminated:
the installer answers 404 unless the database genuinely needs work, so the
window is a box mid-setup, not a running application. Do not put a box on a
public address while its installer is open, and do not leave
INSTALLER_ENABLED=true set.
Values bound for .env are refused if they contain line breaks or control
characters — a password of x\nAPP_DEBUG=true would otherwise write a second
setting — and the file is written through a mode-600 temporary file that
preserves the original permissions rather than widening them.
Why it does not use the web middleware group
The application's own middleware is what is not working yet when someone opens the installer. It takes only cookies, a session and CSRF, and forces three settings on its own requests:
| forced | because |
|---|---|
session.driver=file, cache.default=file |
an app configured for redis cannot render a screen that needs a session while redis is down — and that screen is where you go to fix it |
session.domain=null |
a configured SESSION_DOMAIN stops the browser returning the cookie when the installer is opened on an IP or provisional hostname, and the form then rejects itself with a CSRF mismatch it can never get past |
session.secure follows the request |
a secure-only cookie over plain HTTP on a box whose TLS is not up yet — which is the box being installed |
Everything is styled inline: on a fresh checkout public/build does not exist,
and an unstyled page is the last thing wanted at the moment it has to be read.
Commands
schema:rebase
Rebuilds database/schema/{connection}-schema.sql from the current migrations.
| flag | use |
|---|---|
| (none) | fold in newly added migrations; no-ops when current |
--force |
rebuild after editing a migration in place — the migration names are unchanged, so nothing else can detect it |
--check |
report only; exits non-zero when the baseline has fallen behind. Run this in CI |
--database=NAME |
build in an existing throwaway database instead of creating one |
The baseline is always built by migrating an empty database, never by dumping a working one — a working database carries data and whatever drift it has picked up.
Safety:
- A rebase targeting the configured (live) database is refused.
- By default a
<name>_schema_rebasedatabase is created and dropped. Where the application user has noCREATE DATABASEright — which in production it should not — pass--databaseand an existing throwaway database is wiped and reused. - The connection is restored in a
finally, so a failed dump cannot leave the application pointed at the scratch database. - Stored routines are included when the server can enumerate them and skipped
with a warning when it cannot, rather than failing the rebuild. (A MariaDB
upgraded without
mariadb-upgradecannot:mysql.prochas the wrong column count.)
Add the check to CI so drift fails a pull request instead of someone's first install:
- name: Check the schema baseline is current run: php artisan schema:rebase --check
install:status
Reports which of three routes a database needs, and exits non-zero unless it is ready.
| state | meaning |
|---|---|
| fresh | no schema — load the baseline, then anything published since |
| upgrade | consistent but behind — run the outstanding migrations, data untouched |
| recovery | the migrations ledger records work the schema does not have |
| ready | consistent, nothing pending |
Recovery is the state a plain "is it installed?" check cannot see. A migration run that dies partway leaves the earlier migrations recorded, so the ledger claims work the schema is missing. The database looks installed and is not.
Programmatic use
SchemaInspector answers the same questions without a console, for an install
screen or a health endpoint. Every method is safe against a database that does
not exist yet — that is the normal case on a new box, not an error.
use Core\Mod\Install\Schema\InstallState; use Core\Mod\Install\Schema\SchemaInspector; $inspector = app(SchemaInspector::class); match ($inspector->state()) { InstallState::Fresh => /* run the installer */, InstallState::Upgrade => /* offer the pending migrations */, InstallState::Recovery => /* show $inspector->missingTables() */, InstallState::Ready => /* carry on */, };
Licence
EUPL-1.2.