Skip to content

Lifecycle & upgrades

In this page you will learn how to run code when your plugin is installed, updated, activated and deactivated, how to write upgrade routines that are safe to run twice, and which hooks the framework fires along the way.

Covers framework release 2.0.3.

The framework keeps the plugin’s installed version in the database. On a wp-admin page load it compares that stored version with the version your plugin reports:

Stored versionWhat the framework does
NoneRuns install(), then fires woodev_{plugin id}_installed.
Lower than the plugin versionRuns upgrade(), then fires woodev_{plugin id}_updated.
The same, or higherDoes nothing.

After install() or upgrade() it stores the plugin’s current version. You write the routines; the framework decides when to call them.

The work is done by Woodev_Lifecycle. Every plugin gets a default one, and you subclass it to add your routines.

Subclass Woodev_Lifecycle, then return your subclass from init_lifecycle_handler() in the plugin class:

<?php
defined( 'ABSPATH' ) || exit;
final class My_Carrier_Plugin extends \Woodev\Framework\Woocommerce_Plugin {
/**
* The single instance.
*
* @var My_Carrier_Plugin|null
*/
protected static $instance;
/**
* Sets up the plugin.
*/
public function __construct() {
parent::__construct(
'my-carrier',
MY_CARRIER_VERSION,
[
'text_domain' => 'my-carrier',
]
);
}
/**
* Gets the single instance.
*
* @return My_Carrier_Plugin
*/
public static function instance(): self {
return self::$instance ??= new self();
}
/**
* Gets the full path of the plugin's entry file.
*
* @return string
*/
protected function get_file(): string {
return MY_CARRIER_FILE;
}
/**
* Gets the plugin's display name.
*
* @return string
*/
public function get_plugin_name(): string {
return 'My Carrier';
}
/**
* Builds the lifecycle handler that runs install and upgrade routines.
*
* @return void
*/
protected function init_lifecycle_handler() {
$this->lifecycle_handler = new My_Carrier_Lifecycle( $this );
}
}

This override replaces the default handler, so assign $this->lifecycle_handler. If you override the method and leave the property empty, the framework builds the default handler and raises a developer notice under WP_DEBUG. See The plugin class.

Load the My_Carrier_Lifecycle class file the same way you load the plugin class, before the plugin is built.

This lifecycle class is complete. It sets defaults on install, migrates a setting in one upgrade, schedules a cron event in another, and cleans up on deactivation.

<?php
defined( 'ABSPATH' ) || exit;
/**
* Runs My Carrier's install, upgrade, activation and deactivation routines.
*/
class My_Carrier_Lifecycle extends Woodev_Lifecycle {
/**
* The versions that have an upgrade routine, in ascending order.
*
* @var string[]
*/
protected $upgrade_versions = [ '1.1.0', '1.2.0' ];
/**
* Runs once, on the first admin_init request after the first activation.
*
* @return void
*/
protected function install() {
// add_option() never overwrites a value that is already stored.
add_option( 'my_carrier_mode', 'test' );
add_option( 'my_carrier_debug', 'no' );
}
/**
* Upgrades a site that runs a version below 1.1.0.
*
* Version 1.1.0 renamed the "token" setting to "api_key". The routine copies the value
* and leaves the old one in place, so running it twice changes nothing.
*
* @param string $installed_version The version that was installed before the update.
* @return void
*/
protected function upgrade_to_1_1_0( string $installed_version ): void {
$settings = get_option( 'my_carrier_settings', [] );
if ( is_array( $settings ) && isset( $settings['token'] ) && ! isset( $settings['api_key'] ) ) {
$settings['api_key'] = $settings['token'];
update_option( 'my_carrier_settings', $settings );
}
}
/**
* Upgrades a site that runs a version below 1.2.0.
*
* Version 1.2.0 added an hourly sync. Sites that were activated earlier never ran
* activate() for it, so the upgrade schedules it.
*
* @param string $installed_version The version that was installed before the update.
* @return void
*/
protected function upgrade_to_1_2_0( string $installed_version ): void {
$this->schedule_sync();
}
/**
* Runs when the plugin is activated.
*
* @return void
*/
public function activate() {
$this->schedule_sync();
}
/**
* Runs when the plugin is deactivated. The merchant's settings stay in place.
*
* @return void
*/
public function deactivate() {
wp_clear_scheduled_hook( 'my_carrier_sync' );
}
/**
* Schedules the hourly sync unless it is already scheduled.
*
* @return void
*/
private function schedule_sync(): void {
if ( ! wp_next_scheduled( 'my_carrier_sync' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', 'my_carrier_sync' );
}
}
}

Put one-time setup here: default option values and anything else a brand-new site needs. It is protected and empty in the base class.

install_default_settings( array $settings ) writes defaults for you. Each entry is [ 'id' => 'option_name', 'default' => 'value' ]:

<?php
$this->install_default_settings(
[
[
'id' => 'my_carrier_mode',
'default' => 'test',
],
]
);

It uses update_option(), so it overwrites a value that is already stored. If install() could ever run on a site that already has settings, use add_option() instead, as the example above does. See Migrating from v1.

List the versions that need a routine in $upgrade_versions, in ascending order. For each entry, write a method named upgrade_to_ plus the version with dots and dashes turned into underscores:

Entry in $upgrade_versionsMethod the framework calls
'1.1.0'upgrade_to_1_1_0( string $installed_version )
'2.4.0.0'upgrade_to_2_4_0_0( string $installed_version )
'2.0.0-beta.1'upgrade_to_2_0_0_beta_1( string $installed_version )

A routine runs when the installed version is lower than its entry. A site that updates from 1.0.0 to 1.2.0 runs both, in the order of the list. A brand-new site runs install() and no upgrade routine.

Two rules keep this predictable:

  • Do not list a version higher than the version you are releasing. A routine for 1.3.0 would run early, on a 1.2.0 release.
  • If a routine is missing for a listed version, the framework skips that entry without an error. Check the method name when a routine does not run.

Each routine is logged at debug level with its start and end.

WordPress only accepts register_activation_hook() before plugins_loaded, which is too early for a framework plugin. The framework tracks activation instead, with an option, and calls your methods:

  • activate() runs once on the first admin_init request after activation (possibly an admin AJAX request), before install() on a new site.
  • deactivate() runs when the merchant deactivates the plugin.

Deactivation is not uninstallation. Clear your scheduled events in deactivate(), and leave the merchant’s data alone.

Treat every routine as something that may run more than once:

  • It can be retried. The framework stores the new version only after the routine returns. If the routine throws, the version is not stored and the routine runs again on the next admin page load. Catch the failures you can recover from.
  • It can overlap. Two admin requests that arrive together can both see the old version.
  • It must not lose data. Copy before you change, check before you write, and leave old values in place.

A routine that follows these rules does the same thing the second time, which is nothing. In the example above, upgrade_to_1_1_0() copies token only when api_key is missing, and schedule_sync() schedules only when nothing is scheduled.

The framework fires these actions. {id} is the plugin ID as written, so a plugin with ID my-carrier fires woodev_my-carrier_installed.

ActionWhenArguments
woodev_{id}_installedAfter install().None.
woodev_{id}_updatedAfter upgrade().The previous version.
woodev_{id}_activatedAfter activate().None.
woodev_{id}_deactivatedAfter deactivate().None.

Use them to react to a lifecycle event from outside the lifecycle class:

<?php
add_action(
'woodev_my-carrier_updated',
static function ( string $previous_version ): void {
delete_transient( 'my_carrier_tariffs' );
}
);

Register such a hook in init_plugin(), or in the entry file, so it exists before the admin page load that runs the routine.

OptionHolds
woodev_{id}_versionThe installed version.
woodev_{id}_is_activeWhether the plugin has been activated, so activate() runs once.
woodev_{id}_lifecycle_eventsA short history of installs, upgrades and migrations.
woodev_{id}_milestone_messagesMilestone notices that are waiting to be shown.
woodev_{id}_milestone_versionThe version at which milestone tracking started on this site.

The name of the version option comes from get_plugin_version_name(). Never edit or delete these options by hand: deleting the version option makes the next admin page load run install() again.

Read the history with get_event_history() on the lifecycle handler. It returns the newest events first, and each event has a name (install, upgrade or migrate), a time and the version. To record that a site moved over from another plugin, call add_migrate_event( $from_plugin, $from_version, $data ).

A milestone is a short congratulation after a first key action, such as the first printed label. It is usually where a review is requested. Trigger one from the place in your code where the action happens:

<?php
My_Carrier_Plugin::instance()->get_lifecycle_handler()->trigger_milestone(
'first-label',
__( 'you printed your first label!', 'my-carrier' ),
'1.2.0'
);

The arguments are an ID, a message and the plugin version that introduced the feature. Write the message as the middle of a sentence: the framework adds a short exclamation before it and, after it, a request for a review or an offer of support, depending on which URLs the plugin provides (get_reviews_url(), get_support_url()).

The third argument keeps old shops quiet. When milestone tracking started on a site after the version you pass, the site does not see the notice for something it did long ago. Calling trigger_milestone() again for an ID that is already registered does nothing, and the framework stops adding milestones once a merchant has dismissed more than three of them.

The same call is available as an action, woodev_{id}_milestone_reached, with the same three arguments.