rajtik76 / nette-migrations
Laravel-like migrations, schema builder, seeders and factories for Nette Database (SQLite, MySQL, PostgreSQL)
Requires
- php: >=8.4
- ext-pdo: *
- nette/database: ^3.2
- nette/di: ^3.2
- nette/utils: ^4.0
- rajtik76/nette-console: ^0.2
Requires (Dev)
- fakerphp/faker: ^1.24
- nette/phpstan-rules: ^1.0
- nette/tester: ^2.5
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
Suggests
- fakerphp/faker: Fake data in factories (require-dev)
Provides
None
Conflicts
None
Replaces
None
README
Laravel-like database migrations, schema builder, seeders and factories for Nette Database. No third-party migration library.
- SQLite, MySQL, MariaDB and PostgreSQL from one migration file
- batches with rollback, reset, fresh and status
- seeders resolved from the DI container, factories with Faker
make:migration,make:seeder,make:factorygenerators- SQLite foreign keys enabled on every connection
Commands run through rajtik76/nette-console
(php nette); the extension registers itself via package discovery.
Installation
composer require rajtik76/nette-migrations
composer require --dev fakerphp/faker
Map the database namespaces in composer.json:
"autoload": { "psr-4": { "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" } }
Register factories as services, so seeders can get them injected:
search: - in: %rootDir%/database/factories classes: - *Factory
Commands
| Command | Description |
|---|---|
migrate [--seed] |
Run pending migrations as a new batch |
migrate:rollback [--step=N] |
Roll back the last batch, or the last N migrations |
migrate:reset |
Roll back all migrations |
migrate:fresh [--seed] |
Drop all tables and run all migrations |
migrate:status |
List migrations with their batch |
db:seed [--class=UserSeeder] |
Run Database\Seeders\DatabaseSeeder or the given seeder |
make:migration create_posts_table |
--create=table / --table=table, otherwise inferred from the name |
make:seeder User |
database/seeders/UserSeeder.php |
make:factory Post |
database/factories/PostFactory.php for table posts (--table to override) |
In production mode (NETTE_DEBUG=0) destructive commands ask for confirmation; --force skips it.
Migrations
<?php declare(strict_types=1); use Rajtik\Migrations\Migration\Migration; use Rajtik\Migrations\Schema\Blueprint; use Rajtik\Migrations\Schema\Schema; return new class extends Migration { public function up(Schema $schema): void { $schema->create('posts', function (Blueprint $table): void { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('title')->unique(); $table->text('body')->nullable(); $table->boolean('published')->default(false); $table->timestamps(); }); } public function down(Schema $schema): void { $schema->dropIfExists('posts'); } };
Columns: id, integer, bigInteger, foreignId, string, text, boolean, timestamp, timestamps.
Modifiers: nullable, default, useCurrent, primary, unique, index.
Table: primary(...), unique(...), index(...), foreign(...)->references()->on(),
dropColumn, dropIndex, dropUnique, dropForeign.
Schema: create, table, drop, dropIfExists, hasTable, statement, connection.
Each migration runs in a transaction on SQLite and PostgreSQL. MySQL/MariaDB commit DDL implicitly. SQLite can add a foreign key only together with a new column and cannot drop one.
SQLite timestamps follow the Nette formatDateTime driver option. With the default U they are
stored as unix timestamps, and a date string default is converted when the migration compiles.
With any other format, useCurrent() falls back to CURRENT_TIMESTAMP, which SQLite fills in UTC.
SQLite cannot add a useCurrent() column to an existing table; add it nullable and fill it with an update.
Upgrading from 0.1.0
SQLite timestamp columns created by 0.1.0 are declared as lowercase datetime, which Nette
does not read as a unix timestamp. Recreate each one in a migration:
alter table users add column seen_at_new DATETIME null; update users set seen_at_new = case typeof(seen_at) when 'text' then cast(strftime('%s', seen_at) as integer) else seen_at end; alter table users drop column seen_at; alter table users rename column seen_at_new to seen_at;
The recreated column is nullable and has no default. Drop indexes on the column before dropping it.
Seeders and factories
final class UserFactory extends Factory { public function __construct(Explorer $database, private readonly Passwords $passwords) { parent::__construct($database); } protected function table(): string { return 'users'; } protected function definition(): array { return [ 'name' => $this->faker()->name(), 'email' => $this->faker()->unique()->safeEmail(), 'password' => $this->passwords->hash('password'), ]; } } final class DatabaseSeeder extends Seeder { public function __construct(private readonly UserFactory $users) { } public function run(): void { $this->users->create(['email' => 'test@example.com']); $this->users->state(['name' => 'Admin'])->createMany(3); $this->call(PostSeeder::class); } }
Factory: make, makeMany, create, createMany, state(array|Closure).
Configuration
migrations: table: migrations directory: %rootDir%/database/migrations stubs: %rootDir%/stubs # overrides migration*.stub, seeder.stub, factory.stub sqliteForeignKeys: true seeders: namespace: Database\Seeders directory: %rootDir%/database/seeders default: DatabaseSeeder factories: namespace: Database\Factories directory: %rootDir%/database/factories
Checks
composer run ci:check # PHPStan level 8 + Nette Tester
Integration tests use SQLite and, when configured, other databases (dsn|user|password):
MIGRATIONS_TEST_MYSQL='mysql:host=127.0.0.1;port=33306;dbname=migrations_test|root|secret' \
MIGRATIONS_TEST_MARIADB='mysql:host=127.0.0.1;port=33307;dbname=migrations_test|root|secret' \
MIGRATIONS_TEST_PGSQL='pgsql:host=127.0.0.1;port=54329;dbname=migrations_test|postgres|secret' \
composer run tester