Must the package handle be unique when submitting?

Permalink
If I want to create an add-on with a free and a payed version, does it work to submit two different packages but with the same handle to the marketplace?

If not, what is the best way to make a free and payed version that is essentially the same package (most code is the same, so renaming all classes, package references, etc would be a nuisance)?

ConcreteConversion
 
JohntheFish replied on at Permalink Best Answer Reply
JohntheFish
Unfortunately the package handle must be unique.

Once inside the package, file/class names do not have to be unique, but you do need to make sure that you don't get the same class loaded from 2 different places (or PHP will puke)

You can achieve that by various means, including any of
- the paid version forcibly uninstalling the free version during the install/upgrade process
- conditional declaration of classes (based on their not already existing)
- free and paid version classes inheriting from the same (and identical) parent class
- the paid version requiring the free version as a prerequisite, and building facilities out from that
- a set of scripts to build your packages that add name prefixes to the various classes (and files) for free and paid versions
- setting the package handle to load classes as a global constant from both packages on_start() handlers, so once the paid version is installed, if both packages are present the classes from the paid version are loaded in preference.
- Creating your own wrapper about 'Loader' to do the above.

I have used some of the above variations myself where I bundle the same classes with many of my addon packages. I have seen others used in various marketplace packages as they came through the PRB.

Whatever you use, please make sure it is thoroughly tested before submitting.
ConcreteConversion replied on at Permalink Reply
ConcreteConversion
Franz asked me to submit my ideas here for feedback, so this is what I'm currently doing to have multiple packages but with same DB and codebase:

First I'm deciding on a base package handle, let's call it "my_addon". To that I append the different versions, so in the Pro/Free case there'll be "my_addon_pro" and "my_addon_free".

The database tables can be named anything, but I'm prepending the base handle to them.

For sharing the codebase between versions, I'm using the build tool Phing together with a very useful preprocessor:https://github.com/tmuguet/PreProcessorFilter...

This means that the code will look like this:

public function someMethod() {
   #if PRO
   $this->unlockFeature();
   #endif
   ...
}
#if PRO
private function unlockFeature() {
   ...
}
#endif


The preprocessor even works for coffee/javascript! So this is great for a common codebase, except that all the class names are the same, which will lead to problems when both versions are installed at the same time. This is fine for me since the code is the same anyway, and it was one of the suggestions of John to uninstall the other version when upgrading.

There is one issue though: Both packages will exist at the same time when installing, so they cannot have the same package controller name. This can be solved with a little "sed -i 's/\bMyAddonPackage\b/MyAddonVERSIONPackage/g' controller.php" at the end of the build process.

Referring to the package in the code is common, so we need a constant to refer to the current package. It can be setup in the constructor of the package controller, together with the package handle and stuff:

class MyAddonPackage extends Package
{
   public function __construct() {
      $this->appVersionRequired = '5.6.1';
      #if PRO      
      $this->pkgHandle = 'my_addon_pro';
      $this->pkgVersion = '0.9.0';
      #else
      $this->pkgHandle = 'my_addon_free';
      $this->pkgVersion = '0.9.0';
      #endif
      // Test if already defined to avoid package clashes when installing.
      if(!defined('MY_ADDON')) {
         define('MY_ADDON', $this->pkgHandle);
      }


Now it's easy to refer to the correct package in the rest of the code:

Loader::library('some_library', MY_ADDON);


The single pages will use the base package handle (no version appended), so when adding a single page in install():

$page = SinglePage::add('/dashboard/my_addon/usefulPage', $pkg);


When doing the forced uninstall there is one important thing to do, transferring settings. The single pages will be automatically replaced, but the Config settings must be copied to the new version before uninstalling. Here's how I do it:

public function install() {
   $pkg = parent::install();
   $oldHandle = $pkg->getPackageHandle() == 'my_addon_free' ? 'my_addon_pro' : 'my_addon_free';
   $oldPkg = Package::getByHandle($oldHandle);
   if($oldPkg) {
      // Copy config values to the current package, then uninstall the previous one.
      foreach(Config::getListByPackage($oldPkg) as $config) {
         $pkg->saveConfig($config->key, $config->value);
      }
      $oldPkg->uninstall();
   }
   ...


I'm not using anything else than Config settings, so I haven't delved into how to transfer other things...!

Finally for uninstalling, The database tables should only be removed when none of the packages are installed! This must be tested during uninstall, so the uninstall method will look like this:

public function uninstall()   {
   parent::uninstall();
   $installed = Package::getByHandle('my_addon_free') || Package::getByHandle('my_addon_pro');
   if(!$installed) {
      // Remove DB.
   }
}


That's it, and it seems to work all right. I'd be glad to hear what you guys think, if anything can be improved or if I've missed something.

/Andreas
andrew replied on at Permalink Reply
andrew
Neat. Hadn't heard of the Phing preprocessor stuff before. Sounds like a good way to share code bases in your source control system and then just be able to spit out multiple packages as part of the build process. I think as long as your careful then this should work. Definitely be careful with anything that you have that has a package variable that you want to persist across an uninstall – since it's going to get uninstalled unless you override that behavior (e.g. front-end blocks.) But in general I think if you're careful this will totally let you override/upgrade a package with a completely new package and keep all your old data (while unlocking new features.)
ConcreteConversion replied on at Permalink Reply
ConcreteConversion
Hi Andrew, I'm glad you think this is a decent approach. But I see your point about blocks, I guess they are difficult to move across different packages. Would it be possible to change the package owner of the installed block when upgrading (since the same code will be available, just at another location)? It would be nice to hear your ideas here.
ConcreteConversion replied on at Permalink Reply
ConcreteConversion
Hello, great suggestions! I think the forced uninstall will be the best, so thanks for that idea. :)