Handling Package Upgrades

While many packages are used to bundle bits of code in a one-time fashion, the format itself is able to handle continuous code deployment over a period of time. Using the version number in a package, we can trigger custom code that adds functionality over time. For example, let's say we have a package that installs a theme:

<?php
namespace Concrete\Package\CustomTheme;

defined('C5_EXECUTE') or die('Access Denied.');

use \Concrete\Core\Package\Package;
use \Concrete\Core\Page\Theme\Theme;

class Controller extends Package
{

    protected $pkgHandle = 'custom_theme';
    protected $appVersionRequired = '5.7.4';
    protected $pkgVersion = '1.0';

    public function getPackageDescription()
    {
        return t('Custom theme description.');
    }

    public function getPackageName()
    {
        return t('Custom theme');
    }

    public function install()
    {
        $pkg = parent::install();
        Theme::add('custom_theme', $pkg);
    }
}

This is version 1.0 of the package. It installs a theme found at packages/custom_theme/themes/custom_theme. Now, let's say we want to add a block type as part of this package. Adding this to installation is easy. First, we import it into our namespace at the top of our file:

use \Concrete\Core\Block\BlockType\BlockType;

Next, we add a line of code to install the block type at the time of package installation:

public function install()
{
    $pkg = parent::install();
    Theme::add('custom_theme', $pkg);
    BlockType::installBlockTypeFromPackage('custom_block_type', $pkg);
}

We just add the new installBlockTypeFromPackage line. However, what about sites that have already installed this package? They won't get the new block type unless they uninstall and reinstall the package, and that might lead to data loss. These sites need an upgrade to the new package.

First, change the $pkgVersion variable to something higher than the initial value. For example, let's make this 1.2

protected $pkgVersion = '1.2';

Now, create an upgrade method in your package controller.

public function upgrade() {
    parent::upgrade();
    $pkg = Package::getByHandle('custom_theme');
    $bt = BlockType::getByHandle('custom_block_type');
    if (!is_object($bt)) {
        $bt = BlockType::installBlockTypeFromPackage('custom_block_type', $pkg);
    }
}

Let's break this function down. First, we run the parent::upgrade() method, which takes care of storing our new version number against the package record in the Concrete CMS database. Then, we get a new instance of the current package. (Note: the package object is returned by the install() method, but it's not returned by the upgrade() method, so you'll need to get it explicitly with the Package::getByHandle() method.) Next, we retrieve our custom block type by handle, and we check to see if it exists. If it doesn't exist, we install it.

That's it! Since the 1.2 version number in the controller file is greater than the 1.0 version stored against the package object in the database, Concrete will signal administrators that the package has an upgrade available. When the package is updated through the dashboard, the update() method will be run. Why do we check to see if the block type exists, if we know we're updating and it doesn't? Well, we don't know that it doesn't exist, actually. This upgrade() method is automatically run any time any upgrade happens, and the code doesn't care what version number it's updating from or to. So it's possible that this code might run again in the future after a second upgrade. At that point, the block type will exist, and we'll want to make sure we don't try to install it a second time.