Patches are versioned upgrade scripts: small PHP files that run once, automatically, when a module is updated, to bring the database in line with code changes. This page covers how they're structured, when they run, and how to write one safely.
A patch is a script inside a module's directory that runs once, to handle whatever a module upgrade needs done — typically a database change. Epesi tracks which patches have already run in a patches database table, keyed by an ID generated from the patch's file location and name. Rename a patch or move it to a different module, and Epesi treats it as new and runs it again.
If a patch fails, Epesi retries it every time patches are run.
Patch location and filename
Every patch lives in a patches directory inside a module — e.g. modules/CRM/Contacts/patches.
Name new patches in this format:
YYYYMMDD_patch_description.php
where YYYYMMDD is the date. Example: 20121129_admin_display.php.
Patches run in date order based on that filename — except any patch whose name doesn't follow this format, which runs before all the others. That's reserved for system-level changes that must happen first: for instance, a patch that updates the database to match a new field-definition structure needs to run before any patch that then adds a field using that new structure.
The system update procedure runs patches automatically. To run them without a full system update, open Admin tools, select Patches, and run the procedure from there.
To add a patch to your module, create a patches directory inside it. Patches only run on upgrade — they're skipped (but marked as applied) during a fresh module install, since a new install already gets the current schema. For example: if your module's first release had a recordset with one field, and you later added a second field by updating the install procedure (the RBO class), you'd also write a patch that adds that same field to already-installed copies of the module.
Guidelines
die(). Throw exceptions instead, and only when the situation truly needs admin intervention — an exception halts the whole patch run.defined("_VALID_ACCESS") || die('Direct access forbidden');Simple example
A minimal patch that adds a field to a recordset:
defined("_VALID_ACCESS") || die('Direct access forbidden');
$my_recordset = new Custom_MyModule_Recordset();
// field definition - as in the new install method
$field = new RBO_Field_Text(_M('Sample'), 128);
$field->set_visible()->set_required();
// check if field already exists
$fields = Utils_RecordBrowserCommon::init($my_recordset->table_name());
if (!isset($fields[$field->name])) {
$my_recordset->new_record_field($field);
}
Database operations
For tables you created with DB::CreateTable(...), use these helpers instead of raw SQL:
PatchUtil::db_add_column($table_name, $table_column, $table_column_def)
PatchUtil::db_drop_column($table_name, $table_column)
PatchUtil::db_rename_column($table_name, $old_table_column, $new_table_column, $table_column_def)
PatchUtil::db_alter_column($table_name, $table_column_name, $table_column_def)
To create or drop a table, use the standard DB static methods.
A patch with several steps needs a way to tell whether it's already applied a given change, to avoid redoing it. When the system itself can't tell you that, use a checkpoint.
A checkpoint:
Checkpoint data is stored in the data directory, under patch_<patch_id> — e.g. patch_af467809ee1e033d54ba1dd98f0c8bba. Inside, each checkpoint gets its own file, named by the MD5 hash of the checkpoint name — a checkpoint named test would be stored in:
098f6bcd4621d373cade4e832627b4f6.dat // md5('test').dat
Each file holds a serialized object. Data is serialized to disk every time you set a checkpoint variable.
Example:
defined("_VALID_ACCESS") || die('Direct access forbidden');
$rs_checkpoint = Patch::checkpoint('recordset');
if (!$rs_checkpoint->is_done()) {
// do something
$rs_checkpoint->done(); // updates the checkpoint's .dat file
}
$another_checkpoint = Patch::checkpoint('other');
$i = $another_checkpoint->get('i', 0); // 0 is the default if the variable doesn't exist yet
while ($i < 10) {
Patch::require_time(3); // require at least 3 seconds -- see Time requirements below
// some lengthy operation
$i += 1;
$another_checkpoint->set('i', $i); // updates the checkpoint's .dat file
}
Most servers cap script execution time, and an Epesi update is just a script — if it runs too long, the server kills it mid-update. To avoid that, split time-consuming work into chunks, save progress with checkpoints between chunks, and call require_time() to check how much time is left before starting the next chunk.
Use require_time() any time your code might take a while — for example, processing every record in a table, when you can't know in advance how many there are.
If a patch doesn't have enough time left for the chunk it's about to run, execution stops at the require_time() call. Patches are assumed to have roughly 30 seconds per run — keep chunks well under that. If you request more than 30 seconds in the very first second of a patch run, that chunk still executes, but it may get killed by the server's own time limit regardless.
Example:
defined("_VALID_ACCESS") || die('Direct access forbidden');
$checkpoint = Patch::checkpoint('process_records');
if ($checkpoint->is_done() == false) {
$id = $checkpoint->get('id', 0);
$records = Utils_RecordBrowserCommon::get_records('contact', array('>id' => $id), array(), array(':id' => 'ASC')); // order by ID
foreach ($records as $r_id => $r) {
Patch::require_time(3); // require at least 3 seconds
// process record here
// save id
$checkpoint->set('id', $r_id);
}
$checkpoint->done();
}
You can also use a checkpoint's own require_time(), which calculates the time between consecutive calls and requires the longest interval seen so far. This is safer, but can slow down the overall patch run — if one cycle happens to take much longer than the rest, every later call will then require that same, inflated amount of time.
Example:
1st cycle requires the default. Takes 5 seconds.
2nd cycle requires 5 seconds. Takes 20 seconds.
3rd cycle requires 20 seconds — not enough time left (25 seconds have passed, only 5 remain). Execution stops.
Patches run again on the next request:
1st cycle requires 20 seconds. Takes 5 seconds.
2nd cycle requires 20 seconds. Takes 5 seconds.
3rd cycle requires 20 seconds — still not enough (about 10 seconds passed, slightly less than 20 remain). Execution stops.
Run again...
etc.
Modified example:
defined("_VALID_ACCESS") || die('Direct access forbidden');
$checkpoint = Patch::checkpoint('process_records');
if ($checkpoint->is_done() == false) {
$id = $checkpoint->get('id', 0);
$records = Utils_RecordBrowserCommon::get_records('contact', array('>id' => $id), array(), array(':id' => 'ASC')); // order by ID
foreach ($records as $r_id => $r) {
$checkpoint->require_time(3); // requires 3 seconds on the first call; every call after that requires the max of all previous calls
// process record here
// save id
$checkpoint->set('id', $r_id);
}
$checkpoint->done();
}
The patches table stores one identifier column. When a patch has been applied, Epesi computes its identifier and stores it there. The identifier is simply the MD5 hash of the patch's relative path (with forward slashes, even on Windows).
For example:
modules/CRM/Contacts/patches/20140812_description_callbacks.php
has the identifier af467809ee1e033d54ba1dd98f0c8bba.
While developing a patch, if you need to run it again, delete its identifier row from the patches table.