2

I have three related components - call them comA comB and comC

comA and comB both use comC (comC manages a table of people details. Individuals may be used by comA or comB or both). Some sites want comA+comC, some want comB+comC, some use all three.

Ideally I want to package them so that comA gets installed together with comC, and comB with comC. Not a problem.

I can use blockChildUninstall in the package xml to prevent comC being uninstalled on its own leaving comA or comB as an orphan.

The problem comes on uninstall if both packages are installed because comC can be uninstalled by either package leaving the other package with an orphan comA or comB which will no longer work.

Is there any way to abort the uninstall() of one extension in a package if it detects that there is still an extension installed that depends on it? The only way I can find to abort an uninstall() is to throw an exception right at the top, but in that case it aborts the whole uninstall process, not just the component currently being uninstalled. It also crashes out to an error page that is not well format and just has a 'return to (joomla) control panel link.

The Installer class does include an Abort() method (as well an isPackageUninstall() method which could be useful) - but how does one use them?

Any suggestions?

Currently my only solution is to not use blockChildUninstall and detect if the other package is still installed in the package uninstall() and if it is abort the package uninstall with the message to just uninstall the comA or comB. This is messy - is there a better way?

1 Answer 1

1

So in the absence of any other thoughts this is what I've got working, some compromises but it seems to work.

pkgA includes comA and comC pkgB includes comB and comC

If both packages are installed then uninstalling pkgA would remove comC and leave comB not working, and vice versa.

So in the package Uninstall routine I detect if the other package's main component (comB or comA) is also installed and abort the uninstall with a message to the admin to just uninstall the unwanted component (comA or comB) and leave the package uninstall until after the other package has been uninstalled.

If the package uninstall doesn't detect the other package's main component is present then it can proceed.

This may result in an orphan package being left installed, which is ok because it can then be uninstalled after the other package which will work okay and just report that one or more of its extensions have already been uninstalled.

Because the admin may want to uninstall just comA (or comB) the packages cannot use blockChildUnistall, but this would open the door to uninstalling just comC and breaking the others.

So the problem now is to prevent comC being uninstalled directly on its own. To handle this the packages uninstall() routines set a session variable to say that this is a valid package uninstall.

comC's uninstall routine then tests this session variable and if it is set then it knows that the package has checked the other component is not present and it is okay to proceed. If the session variable is not set that implies that comC is being uninstalled on its own so it needs to check whether either comA or comB are installed and if they are then abort its uninstall.

In J3 when uninstalling a package or component the preflight() and postflight() functions do not fire on uninstall - only the uninstall() function is run. The good news is that this happens before any files or data is touched so it is possible to abort the uninstall and leave everything intact.

Rather than doing this by raising an error which results in a pretty un-useful error page by default, if aborting the install I redirect to the other component's control panel page with a message in the url string which is displayed as an enqueueMessage() on the page.

When uninstalling a package the package uninstall() function runs first then each of the component's uninstall() functions in the order in which they appear in the package xml file.

Here's the basic code for pkgA script

use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;

    function uninstall($parent) {
        $db = Factory::getDBO();
        $db->setQuery('SELECT enabled FROM #__extensions WHERE element = '.$db->quote('com_comb'));
        $res = $db->loadResult();       
        if ($res) {
            $message = 'comB is still installed. If you wish to uninstall comA then just uninstall the component for now.';
            //comA component could have been uninstalled manually so we'll redirect to comB dashboard as we know that exists
            $targ = Uri::base().'index.php?option=com_comB&view=dashboard&err='.urlencode($message);
            header("Location: ".$targ);
            exit();
        }
    //no comB, ok to proceed
        $oldval = Factory::getSession()->set('pkguninst', 'comA');
    //other stuff as required

Then in comC script you need this

use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;

    function uninstall($parent) {       
        $pkguninstall = Factory::getSession()->get('pkguninst');
        if (!$pkguninstall) {
           // this is not a package uninstall so we need to check if xbfilms or xbbooks or xblive are still here
            $db = Factory::getDBO();
            $db->setQuery('SELECT enabled FROM #__extensions WHERE element = '.$db->quote('com_coma').' OR element = '.$db->quote('com_comb'));
            $res = $db->loadResult();
            if ($res) {
                $message = 'Either comA or comB is still installed. comC component must be uninstalled after both comA and comB.';
                $targ = Uri::base().'index.php?option=com_comc&view=dashboard&err='.urlencode($message);
                header("Location: ".$targ);
                exit();
            }
        }
//so either this is a package uninstall so we know its ok or we've done the test above and both of comA and comB have already been uninstalled
        Factory::getSession()->clear('pkguninst');
        //ok to proceed

and to display the message in the components dashboard view.html.php files you need this

    public function display($tpl = null) {
        //get uninstall error message
        $app = Factory::getApplication();
        $err = $app->input->getString('err','');
        if ($err!='') {
            $app->enqueueMessage(urldecode($err),'Error');
        }
    //and the rest as normal

and that seems to work for me. Hope it might someone help else in future.

4
  • element = '.$db->quote('com_coma').' OR element = '.$db->quote('com_comb') could be consolidated into an IN() condition. 'err'.'' looks odd. Why are you doing that empty string concatenation?
    – mickmackusa
    May 31 at 2:41
  • Yes agree about using IN() - just lazily copying something I had used elsewhere looking for only one element. Is there a performance implication ('=' being faster than IN() )?
    – RogerCO
    May 31 at 9:38
  • and the .'' is a typo, should be a comma to get a default value if it is not set getString('err','') I think it is a good habit to always provide a default fallback?
    – RogerCO
    May 31 at 9:41
  • I don't know the performance comparison between IN() versus multiple OR conditions. It just seems more sensible, compact, extensible to use IN(). I suspected you intended to use a comma to declare a fallback, but wasn't certain. Please edit your question to fix the getString() call.
    – mickmackusa
    May 31 at 11:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.