ryan

Module Dependencies

20 posts in this topic

I put in module dependencies into the 2.2 dev version yesterday, and just wanted to explain how they work, both for the user side and the developer side.

What it means and what it does

"Module dependency" means that when you install plugin modules, they can now require that other prerequisite modules are installed first before they are. Likewise, if one module requires another, then it prevents modules in use from being uninstalled. Ultimately this leads to more clarity in the system, and makes it easier for one module to rely upon another. Likewise, it prevents the issue of being able to uninstall one module that's actively used by another. It's a convenience and a safety.

The module dependency update in 2.2 also adds the ability for related modules to establish a simple parent-child relationship. One module can say that it is responsible for the installation of another, and so you can have modules installed or uninstalled as a group. Meaning, if you installed a plugin that includes 3 different modules to operate, then PW will only let you install the one designated by the developer, and the others will be installed automatically immediately after it. The same goes for the uninstall process.

How to use now

Attached are a couple screenshots that show how it looks from the Modules page in PW. If you want to test out dependencies, this feature is in the "dev" branch of the current ProcessWire distribution. If you are up to date on your git pulls, or you just installed a fresh copy with git, then you just "git checkout dev" to get to it. Of course, only do this on a test or development site, not a production site.

FOR MODULE DEVELOPERS


Using these dependencies is completely optional from a module development standpoint. While these dependencies don't exist in the current stable version of PW (2.1) there's no harm in implementing them now, as PW 2.1 will just ignore them. To test dependencies in PW 2.2, you'll just want to switch to the dev branch as mentioned in the previous section.

When to use dependencies

If you know that your module needs some other module in order to run property, it's a good idea to take advantage of dependencies. Especially given how easy they are to implement.

Another situation where you'd want to use dependencies is with plugins that include more than one module. For instance, a fictional plugin called "Log Master" may have an "autoload" module called LogMaster to hook into various things and log them, and another called ProcessLogMaster to display them in the admin. You don't want someone to be able to install ProcessLogMaster without first installing LogMaster. Furthermore, you might want to have ProcessLogMaster automatically install (and uninstall) at the same time as LogMaster. In this light, dependencies can add a lot of convenience for the person installing your module, and less support and documentation burden for you.

Using the "requires" dependency

Dependencies are defined in your getModuleInfo() function. To establish a dependency, all you need to do is add a "requires" property to the array returned by your getModuleInfo() function. That "requires" property may be a string or an array. If it's a string, it's assumed to contain just the name of one module. If it's an array, it's assumed contain one or more module names.

Lets say that I was creating a "Hello World" module that required the "LazyCron" module. Here's how you'd do it:

HelloWorld.module

<?php
public static function getModuleInfo() {
   return array(
       'title' => 'Hello World',
       'version' => 101,
       'author' => 'Ryan Cramer',
       'summary' => 'Just an example',
       'requires' => "LazyCron"  // added this line
       ); 
);

You can also specify 'requires' as an array, which is what you'd want to do if you wanted to specify that multiple other modules are required (though you can of course use an array for 1 module too):

HelloWorld.module

<?php
public static function getModuleInfo() {
   return array(
       'title' => 'Hello World',
       'version' => 101,
       'author' => 'Ryan Cramer',
       'summary' => 'Just an example',
       'requires' => array("LazyCron", "AdminBar")  // added this line
       ); 
);

That's all that you need to do to specify a module dependency. Once you do that, ProcessWire won't let your module be installed until LazyCron and AdminBar are installed. Likewise, ProcessWire won't let LazyCron or AdminBar be uninstalled so long as HelloWorld is installed.

Using the "installs" dependency

If you want to specify that your module installs and uninstalls other modules, then you should add an "installs" property to the array returned by getModuleInfo. Like with "requires", the property may be a string or an array, with an array is required for multiple items. Using this property is your module's way of telling ProcessWire that it is the parent of those modules and they should be installed–and uninstalled–at the same.

Like with PW 2.1, your module's install() and uninstall() methods may handle the installation/uninstallation of any other modules too... but it doesn't have to. PW will automatically install/uninstall any modules specified in "installs" immediately after your module, unless your module already took care of it.

It's a good idea to specify those child modules in your "installs" even if your install() function is already taking care of installing them. That way, PW can still note the relationship in the admin.

Below are examples of how to add the "installs" property to your getModuleInfo(). We'll use the LogMaster example mentioned earlier on.

LogMaster.module – parent module

<?php
public static function getModuleInfo() {
   return array(
       'title' => 'Log Master',
       'version' => 101,
       'author' => 'Lumberjack Bob',
       'summary' => 'Log all actions on your site',
       'requires' => 'LazyCron', 
       'installs' => 'ProcessLogMaster'  // added this line
       ); 
);

Now you've told PW that ProcessLogMaster should be installed at the same time as LogMaster. If LogMaster doesn't install ProcessLogMaster, then ProcessWire will install ProcessLogMaster automatically after it installs LogMaster.

One thing to note about uninstalls: PW will only uninstall modules that specify the parent module in their "requires". If they don't, then PW will leave them installed when the parent module is uninstalled. After all, why should PW uninstall a module that doesn't require the other one being uninstalled? But I'm guessing that in most (not all) instances where you would use "installs", you'll also want to use "requires" with the child modules, pointing to the parent module:

ProcessLogMaster.module – child module

<?php
public static function getModuleInfo() {
   return array(
       'title' => 'Log Master (Process)',
       'version' => 100,
       'author' => 'Lumberjack Bob',
       'summary' => 'Displays the logged actions in the admin',
       'requires' => 'LogMaster', // added this line
       ); 
);

Using the above example, this ProcessLogMaster will be automatically uninstalled with LogMaster since it indicates that it requires LogMaster, and LogMaster indicates that it installs ProcessLogMaster.

SCREENSHOTS


1. This shows how the module dependencies are highlighted on the Modules list page. Note that the install option is disabled for those modules that require another to be installed first.

2. This shows the screen that appears when you click on the LanguagesSupport module. It demonstrates the group of modules (children) connected with LanguageSupport (parent) module.

3. This shows an example of a module (FieldtypeComments) that has a module requiring it (CommentFilterAkismet). That CommentFilterAkismet module is an optional component to the Comments module, so is not installed by default when FieldtypeComments is installed. Since CommentFilterAkismet requires FieldtypeComments, CommentFilterAkismet would have to be uninstalled before FieldtypeComments could be uninstalled.

post-1-132614281798_thumb.png

post-1-132614281841_thumb.png

post-1-132614281861_thumb.png

1 person likes this

Share this post


Link to post
Share on other sites

Awesome work again Ryan! Thanks for the implementation. This will help a lot. I will surely at some point test this out.

Just to mention something regarding modules. When I put a lot of modules in at once it show me a nice list of the new modules found, but after installing the first it's gone... It would be nice to somehow (don't know how this could work) have that list being saved and actually being a link list or "install" list on top. If that would be done with ajax, or a "install all" feature that would also help and improve  the experience more or less. What you think about something like this?

Share this post


Link to post
Share on other sites

Cool Ryan, I'll add this to my scheduled pages module soon, since it relies on LazyCron.

/Jasper

Share this post


Link to post
Share on other sites

Thanks guys, let me know how it works for you.

Just to mention something regarding modules. When I put a lot of modules in at once it show me a nice list of the new modules found, but after installing the first it's gone... It would be nice to somehow (don't know how this could work) have that list being saved and actually being a link list or "install" list on top. If that would be done with ajax, or a "install all" feature that would also help and improve  the experience more or less. What you think about something like this?

I like your ideas here, good thinking! Perhaps we just maintain another section of modules that represents the uninstalled list. At the moment I'm a little scared of an "install all" in the same request, but perhaps I shouldn't be–definitely something to think about and perhaps implement in the future.

Share this post


Link to post
Share on other sites

If a module requires another why do I have to install the required manually? I guess the module which requires the other should install it automatically...

Share this post


Link to post
Share on other sites
If a module requires another why do I have to install the required manually? I guess the module which requires the other should install it automatically...

In that case you want to use 'installs' rather than 'requires'. Either that, or don't use dependencies at all and just $this->modules->get('module'); in your install(), which will install it automatically (if it's files are in the /modules/ directory).

Use 'requires' for things like other 3rd party modules that the user might have to install separately, or modules that the user might have to configure after installing. The 'requires' option is also useful for preventing un-installation of modules. If one module requires another, then the one required can't be uninstalled so long as the one requiring it is installed.

1 person likes this

Share this post


Link to post
Share on other sites

I'm currently working on a couple modules that require newer versions of ProcessWire and newer versions of specific modules (as dependencies). Currently, there is no way to specify versions with module dependencies other than having your install() method throw an exception. As a result, module dependencies have been updated (on dev branch, 2.4.1) to include support for module versions, as well as PHP and ProcessWire versions. The best way to describe it is to show an example of how you'd use it. I'll take an example from the first post here and update it (note the 'requires' line at the bottom): 

public static function getModuleInfo() {
  return array(
    'title' => 'Log Master (Process)',
    'version' => 100,
    'author' => 'Lumberjack Bob',
    'summary' => 'Displays the logged actions in the admin',
    'requires' => 'ProcessHello>=101, ProcessWire>=2.4.0, PHP>=5.4.0', 
    );
);

The 'requires' line above specifies that this module has the following requirements: 

  • ProcessHello module version 1.0.1 or newer
  • ProcessWire version 2.4.0 or newer
  • PHP version 5.4.0 or newer

If no particular version of a module is required then you'd simply omit the operator and version, as you've done with dependencies in the past. Another thing to note above is that the 'requires' line accepts a CSV string. Before, you had to use an array if you wanted to specify multiple dependencies. You can still use an array if you want to, but it's not required. The above 'requires' line as an array would look like this:

'requires' => array('ProcessHello>=1.0.1', 'ProcessWire>=2.4.0', 'PHP>=5.4.0')

For ProcessWire or PHP versions, you need to specify a 2-3 part version string like "2.4.0". But for modules, you can specify either an integer or a 3 part version string. It's more consistent, and thus a little bit preferable to specify an integer (like in my first example) since that is already how versions are specified in the getModuleInfo 'version' property (i.e. 101 rather than 1.0.1). However, it doesn't really matter. 

8 people like this

Share this post


Link to post
Share on other sites

Cool stuff - I like the PW and PHP version requirements.

Since we have a way to automatically install modules, can we have "installs" go and try to fetch the module from the modules directory? It would be really neat if it did that as long as people don't abuse it.

Share this post


Link to post
Share on other sites

I'm currently working on a couple modules that require newer versions of ProcessWire and newer versions of specific modules (as dependencies). Currently, there is no way to specify versions with module dependencies other than having your install() method throw an exception. As a result, module dependencies have been updated (on dev branch, 2.4.1) to include support for module versions, as well as PHP and ProcessWire versions. The best way to describe it is to show an example of how you'd use it. I'll take an example from the first post here and update it (note the 'requires' line at the bottom): 

public static function getModuleInfo() {
  return array(
    'title' => 'Log Master (Process)',
    'version' => 100,
    'author' => 'Lumberjack Bob',
    'summary' => 'Displays the logged actions in the admin',
    'requires' => 'ProcessHello>=101, ProcessWire>=2.4.0, PHP>=5.4.0', 
    );
);

The 'requires' line above specifies that this module has the following requirements: 

  • ProcessHello module version 1.0.1 or newer
  • ProcessWire version 2.4.0 or newer
  • PHP version 5.4.0 or newer

If no particular version of a module is required then you'd simply omit the operator and version, as you've done with dependencies in the past. Another thing to note above is that the 'requires' line accepts a CSV string. Before, you had to use an array if you wanted to specify multiple dependencies. You can still use an array if you want to, but it's not required. The above 'requires' line as an array would look like this:

'requires' => array('ProcessHello>=1.0.1', 'ProcessWire>=2.4.0', 'PHP>=5.4.0')

For ProcessWire or PHP versions, you need to specify a 2-3 part version string like "2.4.0". But for modules, you can specify either an integer or a 3 part version string. It's more consistent, and thus a little bit preferable to specify an integer (like in my first example) since that is already how versions are specified in the getModuleInfo 'version' property (i.e. 101 rather than 1.0.1). However, it doesn't really matter. 

Those are nice features. I'm still a little foggy about what now is in what version of PW, and how to use these requires, installs best. But that's maybe just me.

Generally, it's getting little hard to follow PW development changes and additions (goes fast) and get a clear picture, for me at least (maybe other developers too), and keep track of them. 

What I thinking about is about modules that should still work in 2.3 - 2.4 - 2.5. There so many changes across core and modules that the only way to make sure and account of differences using conditional code. What is the simplest way to make a check? 

if(wire("config")->version >= "2.4.0")

does not work obviously. Is there an easy API I'm missing?

And something on the js config side could exists too?  config.version ?

------- 

Side note about your example:

I guess this example won't work cause this addition is after 2.4.0?

'requires' => 'ProcessHello>=101, ProcessWire>=2.4.0, PHP>=5.4.0', 

What about the "permission" key on the module info? Was there any additions or changes lately? Can there be multiple "permission".

Share this post


Link to post
Share on other sites

@soma: are your questions answered somewhere else? I'm interested in reading it. :)

Share this post


Link to post
Share on other sites

I have another question regarding this:

If I have 3 modules belonging together: 

  • FieldtypeExample   (has "requires: ProcessExample")
  • InputfieldExample   (has "requires: ProcessExample")
  • ProcessExample     (should be the parent, has "installs: FieldtypeExample, InputfieldExample")

If I copy all together into a site/modules directory, I only get a install-button for ProcessExample, what is fine and as expected.

If I install it, it auto-installs the other two modules, what is fine, too.

If I build a field upon the FieldtypeExample, I cannot uninstall the FieldtypeExample-Module unless I have removed all Fields of that type from all Templates and have deleted all Fields of that type, (in this order).

But if I use the ParentModule (ProcessExample) to uninstall all 3 modules, it doesn't handle this. It just uninstalls the InputfieldExample-Module and itself. It lets the FieldtypeExample-Module installed.

Can I prevent it from uninstalling itself somehow from within its ___uninstall() routine?

Another thing is that I simply can uninstall one of the children, leaving the other both modules installed. Also not what should be able for my usecase.

Share this post


Link to post
Share on other sites
if(wire("config")->version >= "2.4.0")

Soma, that won't work because that's just a string comparison. You'd need to use PHP's version_compare() function instead. There is also $modules->versionCompare($currentVersion, $requiredVersion, $operator); that achieves the same thing, but accepts version numbers as either strings or integers (and you can mix and match if needed). 

And something on the js config side could exists too?  config.version ?

Of course you could pass through the version for PW or any module to JS anywhere you wanted, via $config->js() or in a data attribute of some markup, etc. I think the need is likely rare though, since requests originate on the PHP side and any version checking is usually better done there. 

What about the "permission" key on the module info? Was there any additions or changes lately? Can there be multiple "permission".

No changes to the permission property, other than that it can now be used on any module type (not limited to just Process modules anymore). Usage is the same as before and it specifies the single permission name needed for PW to execute the module, (i.e. 'permission' => 'widget-edit'). Though there is a 'permissions' property (in 2.5) where you can specify permissions that you want PW to automatically install with your module (and likewise uninstall), i.e. 

'permissions' => array('
  'widget-edit' => 'Edit a widget',
  'widget-add' => 'Add a widget'
  )

Your module is responsible for permission checking, outside of the one permission you may have specified in order for the module to be executed. For instance, if you had an executeAdd() method in your module, you might like to do a if(!$this->user->hasPermission('widget-add')) before rendering an "add" button, as well as at the top of your executeAdd() method. 

Can I prevent it from uninstalling itself somehow from within its ___uninstall() routine?

Yes, you can throw an exception from either install() or uninstall() to cancel the operation. Though in your scenario, I would usually make the Fieldtype module the parent one (rather than the Process), since it is the one module in your set that may be externally linked (i.e. via fields that are using it). 

1 person likes this

Share this post


Link to post
Share on other sites

Yes, you can throw an exception from either install() or uninstall() to cancel the operation. Though in your scenario, I would usually make the Fieldtype module the parent one (rather than the Process), since it is the one module in your set that may be externally linked (i.e. via fields that are using it). 

I have already tested this with the Fieldtype set as the parent.

Fieldtype installs child1, child2

child1 requires Fieldtype

child2 requires Fieldtype

With this setup I simply can click into one of the children and uninstall it! This way I can uninstall the ProcessModule but let the Fieldtype and Inputfield there, but not operable. This should not be doable in our case.

-----

I don't want to throw an exception, I would like to test if some criteria are matched when a user try to uninstall any of the modules, and if the criteria are not matched, guide him on how to go further.

Or is that the (only) way how I can 'guide' him, throwing a short warning message, may be with pointing him to other resources?

Share this post


Link to post
Share on other sites

I am also struggling with this a little at the moment.

If the parent has requires and installs set to the child module I can install the parent and have the child install. Also it prevents me from uninstalling the child directly (which I want), but when I go to uninstall the parent, it can't uninstall the child automatically because the parent needs it at the point of uninstall still.

If I remove the requires from the parent, then the parent can uninstall the child, but then someone could also manually uninstall the child, which I would like to prevent. Is this possible?

1 person likes this

Share this post


Link to post
Share on other sites

@adrian:

I have found a workaround for me. I set it up like this:

parent installs: child1, child2
parent requires: 

child1 requires: parent
child1 installs:

child2 requires: parent
child2 installs:

I would not need this with my workaround but found it to be usefull to display all those infos about requires and installs in the modules admin.

In the Parent uninstall method I use this code:

    public function ___uninstall() {

        $GLOBALS['UNINSTALLROUTINE_RUNNING_PARENTNAME'] = array();
        foreach(array('ModuleNameChild1', 'ModuleNameChild2') as $mod) {
            $this->modules->resetCache();
            if (!$this->modules->isInstalled($mod)) continue;
            $GLOBALS['UNINSTALLROUTINE_RUNNING_PARENTNAME'][] = $mod;
            $m = $this->modules->get($mod);
            $m->uninstall();
        }

        parent::___uninstall();
    }

and in the Children uninstall methods:

    public function ___uninstall() {

        if (!isset($GLOBALS['UNINSTALLROUTINE_RUNNING_PARENTNAME']) || !in_array(__CLASS__, $GLOBALS['UNINSTALLROUTINE_RUNNING_PARENTNAME'])) {
            throw new WireException("Please only use the ParentModulesName to uninstall the complete ModulesPackage!");
        }
        
        parent::___uninstall();
        
        ...

This way you can use the ParentModule to uninstall all, but you cannot uninstall one of the Children.

A bit weird is, that the children modules get uninstalled two times, :) , this is because of the auto-install / auto-uninstall routine of PW modules dependencies. But if one would not specify dependencies, this could be avoided, but also the dependency messages will not be displayed then.

1 person likes this

Share this post


Link to post
Share on other sites

Thanks horst - I may implement this, but I am still hoping that Ryan will fix what seems to be confusing behavior, or is it just me?

Share this post


Link to post
Share on other sites

@Ryan

What's the current best practice for checking up on module dependencies upon PHP extensions? Can this be done using the requires field of the module info array? Or should I be checking for the extensions as part of my ___install() method and throwing an exception if they aren't present and active? If I do throw an exception, is that caught and reported to the user doing the installation of the module via the admin interface?

Thanks in advance!

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.