aimeos/upscheme: PHP schema migrations that describe the target state, not the steps
Database migrations and schema updates made easy
At a glance
- What is it?
- A Doctrine DBAL wrapper built around declarative table definitions, aimed squarely at PHP applications where third party extensions all want to alter the same tables.
- Who is it for?
- upscheme is a good fit for PHP applications whose schema is owned by more than one party. Writing a table definition rather than a sequence of alterations means the migration can be run against any starting state, including one where a third party extension has already added a column, which is exactly the situation Doctrine's time ordered migration classes handle badly.
- Can I use it commercially?
- Yes, with conditions. LGPL-3.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One composer command is the whole install story
The README gives exactly one command, and it is the right one:
composer req aimeos/upschemeEverything after that points at upscheme.org. The repository itself is small in the way a library should be: `src/` for the implementation, `tests/` beside `phpunit.xml`, `stubs/` for code generation templates, and a `.circleci/` directory for continuous integration. The presence of `stubs/` is a hint worth following, since the README's table of contents includes a generate from database section, meaning you can point the tool at an existing schema and have it write migration files for you.
The licence is LGPL-3.0, which is a differentiator in a small world of migration libraries. For a package that runs as part of your application's request cycle and can be extended with custom methods and custom column types, LGPL is a deliberate choice rather than an accident, and it is worth checking against your own distribution model before you commit to it.
Topics cover the ground the package actually serves: `database-migrations`, `database-schema`, `mysql`, `postgresql`, `sql-server`, `sqlite`, `sqlsrv` and `rdbms`.
A declarative table definition instead of an ordered script
This is the core idea, and the README shows it with a single example. You write down what the table should look like when you are finished, and the tool works out the difference against whatever is actually there:
$this->db()->table( 'test', function( $t ) {
$t->engine = 'InnoDB';
$t->id();
$t->string( 'domain', 32 );
$t->string( 'code', 64 )->opt( 'charset', 'binary', ['mariadb', 'mysql'] );
$t->string( 'label', 255 );
$t->int( 'pos' )->default( 0 );
$t->smallint( 'status' );
$t->default();
$t->unique( ['domain', 'code'] );
$t->index( ['status', 'pos'] );
} );Several things are happening at once. `$t->opt( 'charset', 'binary', ['mariadb', 'mysql'] )` applies an option only on the named platforms, which is how one definition serves MySQL and PostgreSQL. `$t->default()` is a shorthand for the timestamp columns the platform expects. And the closing lines declare constraints as part of the same definition rather than as separate statements.
The README is explicit that it will add and modify columns and table properties but will not delete anything for safety reasons. That is a deliberate asymmetry: destructive changes stay manual, and the tool handles the additive part where mistakes are expensive.
Why the README spends most of its length attacking Doctrine migrations
The project's positioning is unusually direct. The README names two incumbents, Doctrine DBAL and Doctrine migrations, and argues against each in turn.
The DBAL argument is about verbosity. The README includes the DBAL equivalent of the same table so the reader can count for themselves: create a schema manager, create a from schema and a to schema, look up or create the table, add each option and column individually, handle the MySQL specific column option with a platform check, add the primary key, add the unique index and the secondary index, and only then diff and execute the generated statements. For the seven column example above that is well over thirty lines against upscheme's eleven.
The migrations argument is architectural and more interesting. Doctrine migrations name migration classes by creation time to enforce an order, and record what has run in a table in your database. The README lists three consequences of that design: dependencies between third party extensions become hard to express, the tracking table can drift out of sync with reality, and using `down()` risks data loss. If your application installs extensions that each add columns to your tables, all three are real problems rather than theoretical ones.
Ordering replaced by before and after hooks
The answer to the ordering problem is a pair of hooks, and the README names them as `before` and `after` in the sentence introducing the fix for extension dependencies. The idea follows from the declarative model. If a migration no longer has to run at a particular point in a queue, because it describes a target state and computes the difference, then two migrations touching the same table can run in either order and both arrive at a correct schema.
That is the whole argument for a state based migration system, and it is stronger than it first sounds. Extensions that add columns, create their own tables and index them become safe to install in any order, because there is no order to get wrong. The cost is that you lose the ability to express operations that are inherently ordered, such as a data transformation that must see a specific column state. For those, a custom SQL escape hatch appears in the table of contents under executing custom SQL, which is the sensible place for the genuinely sequential parts of a migration.
The hooks themselves are documented on upscheme.org rather than in the visible README text, so their exact signature and when each one runs are things to confirm on the site.
An API organised by database object rather than by SQL verb
The table of contents is the clearest statement of the library's design. The sections run: database, tables, columns, foreign keys, sequences, indexes, customizing upscheme, and upgrade. Each has a matching set of operations, and the pattern repeats: create or add, check existence, change or rename, drop, and a trailing entry naming the methods available on that object.
Existence checks as a first class operation is the detail that shows the philosophy. Before issuing DDL you ask whether the object is already there, which is what lets one migration file be safely re-run and what lets a definition be applied to a partially migrated database. It also explains why renaming is offered for tables, columns and indexes but not for everything: a rename is the operation you need most when a third party extension and your own migration disagree about a name.
Sequences get their own section because they are a real portability problem. MySQL has no native sequence objects in the usual sense, so cross database code has to abstract them, and having `seq()` style creation, existence checking and dropping in the API removes a recurring source of platform conditionals.
Customizing upscheme covers two extension points: adding custom methods and implementing custom column types. For an application with a house style for identifiers or timestamps, that is the difference between using the tool and working around it.
Maintenance state, testing and what the repository does not show
The repository is not archived and the last push was on 2026-07-29, so the project is being worked on. There are no published releases in this repository at all, which means version history and changelogs live on Packagist rather than in the tags, and the README badge points at the Packagist licence endpoint for the same reason.
Coverage is instrumented through Coveralls and builds run on CircleCI, both wired through the badge block at the top of the README. Neither badge tells you anything about how well the tool is covered in practice, and neither will tell you which database backends the test matrix actually exercises, which for a package advertising MySQL, PostgreSQL, SQL Server and SQLite support is the question that matters most.
Documentation lives at upscheme.org, and the repository is the thin wrapper around it. That arrangement is normal for a library and works against you in one specific way: the source of truth for API behaviour is a website, so code search, static analysis and offline reading all become less convenient. Reading `src/` is the fallback when the site is not specific enough, and with a project this size that is a reasonable way to settle an argument about what a method actually does.
Editorial conclusion
upscheme is a good fit for PHP applications whose schema is owned by more than one party. Writing a table definition rather than a sequence of alterations means the migration can be run against any starting state, including one where a third party extension has already added a column, which is exactly the situation Doctrine's time ordered migration classes handle badly. The surface area is well organised and the documentation at upscheme.org covers database objects, tables, columns, foreign keys, sequences, indexes and extension points in that order. Two caveats are worth stating plainly. There are no published releases in this repository, so you track versions through Packagist, and the visible README text covers the motivation and the schema API in more depth than the installation and integration steps, so plan on reading the site as well.
Frequently asked questions
How do I install upscheme and run my first migration?
The README's only command is `composer req aimeos/upscheme`. The integration steps that follow, including how migrations are registered with the application and how the before and after hooks are declared, are documented on upscheme.org rather than in the repository. The `stubs/` directory in the tree suggests you can also generate migration files from an existing schema rather than writing them by hand.
How does upscheme compare with Doctrine migrations and DBAL?
Upscheme is built on Doctrine DBAL but replaces both the verbose API and the time ordered migration model. You describe the target state of a table with a closure and let the library compute the DDL, and because migrations are state based they can run in any order, which matters when third party extensions add columns to the same tables. Doctrine migrations name classes by creation time and track execution in a database table, which the README identifies as the source of extension dependency problems.
Which databases does upscheme support?
The repository topics name MySQL, PostgreSQL, SQL Server, SQLite and sqlsrv, and the API is built on Doctrine DBAL, which handles the platform differences. The README's example shows how per database options are applied, with a charset set to binary only on the mariadb and mysql platforms, so one migration file can serve several backends. The test matrix behind those claims is on CircleCI and is not spelled out in the visible documentation.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/aimeos-upscheme)Was this useful?