restruct / silverstripe-database-migrations
Database migration utilities for SilverStripe: table renames, classname remapping, column renames, and table merges during dev/build
Installs: 22
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:silverstripe-vendormodule
pkg:composer/restruct/silverstripe-database-migrations
Requires
- silverstripe/framework: ^5.0
README
Database migration utilities for SilverStripe 5.
Handles table renames, classname value remapping, and column renames during dev/build.
Features
- ClassName Remapping: Reads
$legacy_classnamesfrom DataObjects and injects intoDatabaseAdmin.classname_value_remapping - Table Renames: Reads
$legacy_table_namesfrom DataObjects and renames tables before schema updates - Column Renames: Rename columns via config (useful for fixing name collisions)
- Conflict Handling: Automatically handles cases where both old and new tables exist
Usage
DataObject Migrations
Add legacy mappings directly to your DataObjects:
class MyModel extends DataObject { private static $table_name = 'MyModel'; // Old class names that should map to this class private static $legacy_classnames = [ 'Old\Namespace\MyModel', 'Another\Old\MyModel', ]; // Old table names that should be renamed to this class's table private static $legacy_table_names = [ 'OldTableName', ]; }
Config-based Migrations
For join tables, versioned tables, or other non-DataObject tables:
Restruct\SilverStripe\Migrations\DatabaseMigrationExtension: # Table renames table_mappings: OldJoinTable: NewJoinTable OldModel_Versions: NewModel_Versions # Classname remappings (see explanation below) classname_mappings: 'Old\Namespace\SomeClass': 'New\Namespace\SomeClass' # Column renames: [table => [old_column => new_column]] column_renames: MyTable: old_column_name: new_column_name # Table merges: merge deprecated tables into their replacements table_merges: OldBlockType: target: NewBlockType # Target table to merge into columns: # Column mapping (optional) OldImageID: NewImageID marker: # Set a value on migrated records (optional) table: Element # Table containing the marker column column: Style # Column name value: 'old-style' # Value to set versioned: true # Auto-handle _Live and _Versions tables
Running Migrations
vendor/bin/sake dev/build flush=1
Migrations run automatically before SilverStripe processes any schema updates.
How ClassName Remapping Works
SilverStripe stores the fully-qualified class name in a ClassName column for polymorphic queries. When you rename or move a class, existing database records still reference the old class name:
| ID | ClassName | Title |
|----|----------------------------|----------|
| 1 | Old\Namespace\MyModel | Record 1 |
| 2 | Old\Namespace\MyModel | Record 2 |
Without remapping, SilverStripe cannot instantiate these records because the old class no longer exists.
What happens during dev/build:
-
The extension collects mappings from:
$legacy_classnameson each DataObjectclassname_mappingsconfig (for cases where you can't modify the class)
-
Injects them into SilverStripe's built-in
DatabaseAdmin.classname_value_remapping -
SilverStripe runs UPDATE queries to fix the values:
UPDATE MyModel SET ClassName = 'New\Namespace\MyModel' WHERE ClassName = 'Old\Namespace\MyModel'
After remapping:
| ID | ClassName | Title |
|----|----------------------------|----------|
| 1 | New\Namespace\MyModel | Record 1 |
| 2 | New\Namespace\MyModel | Record 2 |
When to use $legacy_classnames vs classname_mappings config
Use $legacy_classnames on your DataObject when:
- You control the class and can add the config to it
Use classname_mappings in YAML config when:
- The old class has been deleted from the codebase (you can't add config to a non-existent class)
- The class is from a vendor/third-party module you can't modify
- You prefer centralised configuration in one YAML file
Common use cases:
- Namespace changes (SS3→SS4/5 upgrades)
- Refactoring/renaming classes
- Merging multiple classes into one
- Moving classes between modules
How Table Migrations Work
The extension hooks into DatabaseAdmin::onBeforeBuild() and renames tables before SilverStripe processes schema updates.
Conflict handling: If both old and new tables exist:
- If the new table is empty: moves it aside as
_obsolete_NewTableand renames old→new - If both have data: logs a warning for manual resolution
How Table Merges Work
Table merges handle the case where you're consolidating two block types (or similar DataObjects) into one. This is different from a simple rename because the target table already has its own data.
Use case: Merging BlockBanner into BlockHero with a "banner" style variation:
Restruct\SilverStripe\Migrations\DatabaseMigrationExtension: # First: remap ClassName values so records point to the new class classname_mappings: 'App\Blocks\BlockBanner': 'App\Blocks\BlockHero' # Second: rename columns if needed (runs before merge) column_renames: BlockBanner: BannerImageID: HeroImageID # Third: merge table data (runs after schema build) table_merges: BlockBanner: target: BlockHero columns: HeroImageID: HeroImageID # Source → target column mapping marker: table: Element # BaseElement stores Style in Element table column: Style value: 'banner-style' # Mark migrated records versioned: true # Handle _Live and _Versions tables
What happens during dev/build:
-
onBeforeBuild (before schema):
- ClassName remapping →
BlockBannerrecords now haveClassName = 'BlockHero' - Column renames →
BannerImageIDbecomesHeroImageID
- ClassName remapping →
-
Schema build (SilverStripe):
- Creates/updates
BlockHerotable structure
- Creates/updates
-
onAfterBuild (after schema):
- Table merge:
- Inserts
BlockBannerrecords that don't exist inBlockHero - Updates existing
BlockHerorecords where columns are empty - Sets
Element.Style = 'banner-style'on migrated records - Handles
_Liveand_Versionstables ifversioned: true - Moves
BlockBannertables to_obsolete_BlockBanner(with counter suffix if already exists)
- Inserts
- Table merge:
Marker field: The marker config is useful for:
- Distinguishing migrated records from original ones
- Setting a style/variant value so the correct template is rendered
- Audit trail of which records came from the deprecated type
After Migrations: Cleanup Options
Once migrations have successfully run on all environments (dev, staging, production), you have two options:
Option 1: Remove migration config (recommended for one-time migrations)
After deployment, you can safely remove:
$legacy_classnamesand$legacy_table_namesfrom your DataObjectstable_mappings,classname_mappings, andcolumn_renamesfrom YAML config
The migrations are idempotent (they check if old tables/values exist before acting), so keeping them has no functional impact. However, removing them:
- Keeps your codebase clean
- Slightly improves
dev/buildperformance (fewer checks) - Makes it clear which migrations are historical vs active
Option 2: Keep migration config (recommended for distributed systems)
You may want to keep migration config in place when:
- Multiple databases need migrating at different times (e.g., client installations)
- Database restores from old backups might reintroduce legacy data
- Future-proofing against re-running migrations on cloned/restored environments
Performance impact: Minimal. The extension iterates through DataObject classes once during dev/build to collect mappings. Table/column checks only run if mappings are found, and bail out early if old tables/columns don't exist.
Hybrid approach
Keep migration config during a transition period, then remove it in a future release once all environments are confirmed migrated.
License
MIT