16

I want my package to ship with a built-in composer-plugin.

I have a structure like this:

composer.json
src/
    ...
plugin/
    composer.json
    src/
        ...

The root composer.json is configured like this:

{
    "name": "foo/bar",
    "type": "library",
    "autoload": {
        "psr-4": {
            "Foo\\Bar\\": "src/"
        }
    },
    "repositories": [
        {
            "type": "path",
            "url": "./tools",
            "options": {
                "symlink": false
            }
        }
    ],
    "require": {
        "foo/bar-plugin": "*"
    }
}

And the built-in composer-plugin's plugin/composer.json like this:

{
    "name": "foo/bar-plugin",
    "type": "composer-plugin",
    "require": {
        "composer-plugin-api": "^1",
        "composer/composer": "^1",
        "foo/bar": "*"
    },
    "autoload": {
        "psr-4": {
            "Foo\\Bar\\Plugin\\": "src/"
        }
    },
    "extra": {
        "class": "Foo\\Bar\\Plugin\\MyComposerPlugin"
    }
}

Notice how there's a two-way dependency here - the plugin depends on foo/bar, and the project itself depends on foo/bar-plugin.

Here's where it gets weird. During a fresh installation with e.g. composer install or composer update, everything is fine - the plugin does it's thing, which, right now, means just announcing itself on the console.

Now, after installation, if I type just composer, I'd expect to see the plugin announce itself, same as before, right?

Instead, it generates a fatal "class not found error", as soon as it tries to reference any class belonging to the foo/bar package.

It's as though composer lost track of the fact that foo/bar-plugin requires foo/bar, and for some reason it's classes aren't auto-loadable.

Is there any reason this shouldn't be possible? Why not?

Of course I can just package this stuff in separate external package, but that isn't going to make much sense, since these packages are just going to depend on each other - they're effectively one unit, a packaging them as two packages is going to result in a mess of major version increases with every small change, as basically every release of foo/bar will break foo/bar-plugin.

Ideally, I'd like to simply add the composer-plugin directly into the main package, but it appears that's not possible for some reason? Only a package with type composer-plugin is allowed to add plug-ins, it seems?

Abanoub Makram
  • 463
  • 2
  • 13
mindplay.dk
  • 7,085
  • 3
  • 44
  • 54
  • then why not add the type `composer-plugin` to the main package? It should still be available as a normaly library if installed, no? – cebe Sep 08 '16 at 11:52
  • Unfortunately, this package has a custom type already, as it's being installed by a custom installer. (it's not the custom installer creating the problem though - I have attempted this with a vanilla `library` package as well.) – mindplay.dk Sep 09 '16 at 11:53
  • Do you have to also run `composer install` inside the plugin directory? – mickadoo Sep 26 '16 at 18:29
  • 3
    There is too many variables left open in this question to get a definitive answer. What composer version? The exact output of running `composer` showing the error? There is clearly discrepancy between the file tree listed and your root composer.json - "./tools" vs "./plugin"? Why do you need a circular dependency? General that is a code smell. – spinkus Sep 27 '16 at 04:04
  • Also verify "class not found error" is not emanating from you plugin. – spinkus Sep 27 '16 at 04:06
  • I take it it's not possible to decouple the second dependency so that Package A relies on Package B, but not vice versa? – haz Sep 27 '16 at 07:34
  • What if you run `composer install` or `composer update`? – Tomas Votruba Sep 28 '16 at 20:23
  • 3
    "they're effectively one unit" - If you have two sets of code that are so dependent that both need to always be released together, I would argue they should be one project. You can always break up the repos with git submodules or similar, but one composer project. – Matt S Oct 19 '16 at 19:07
  • 3
    Keep in mind that you can always use [`Scripts`](https://getcomposer.org/doc/articles/scripts.md) instead of a `Plugin`. You might insert the script classes into the script section of `composer.json` in your main project or even one-level above that, in the `custom-installer`. Event-handling wise you get the same effects with scripts; when registered to the right event-handlers they will also run automatically on install and update. – Jens A. Koch Nov 23 '16 at 17:25
  • Why don't you separate this `Plugin` to another composer library and require it in your main project? – Can YILDIZ Jun 13 '17 at 14:26
  • @CanYILDIZ because that makes it untestable - you'd have to actually commit and push to a repository in order to install and test the plugin. Since posting this, I have managed to test a composer plugin, but the approach is horribly clunky: I use `sys_get_temp_dir()` and a random dir-name, copy a `composer.json` template there and run `composer install` etc. using `symfony/process`. Not exactly elegant (or fast) but it's the only working approach I've seen... – mindplay.dk Jun 27 '17 at 11:36
  • Perhaps I am misunderstanding the utility of this setup; however, if your plugin depends on the main package, and the package depends on the plugin: what the heck is the use to have this be a plugin in the first place? If I understand correctly, the 2-way dependency means in essence you just have 1 project here. Setting up as a plugin is just an over-complication with no benefit. It's hard to reason this better with all these foobars which cut the intuitive notion out the equation. I suspect you need to work on separation of concerns to better address the cyclical dependencies. – Cyril Graze Jul 12 '17 at 23:31

1 Answers1

1

If the plugin is essentially a part of your package, you should not use it as such. Composer offers alternatives.

As Jens mentioned in a comment to your question, there is 'scripts' key in composer.json. You can invoke shell commands inside, but also call static class methods.

About plugin solution - composer explicitly mentions this on its site:

Composer makes no assumptions about the state of your dependencies prior to install or update. Therefore, you should not specify scripts that require Composer-managed dependencies in the pre-update-cmd or pre-install-cmd event hooks. If you need to execute scripts prior to install or update please make sure they are self-contained within your root package.

(my side note - this also roughly applies to plugins).

Anyway - to provide you with a solution: discard 'plugin' approach. Instead modify your composer.json file so it looks as follows:

composer.json

{
    "name": "foo/bar",
    "type": "library",
    "autoload": {
        "psr-4": {
            "Foo\\Bar\\": "src/"
        }
    },
    "require": {
    },

    "scripts": {
        "post-install-cmd": [
            "Foo\\Bar\\Composer\\Plugin::postInstall"
        ],
        "post-update-cmd": [
            "Foo\\Bar\\Composer\\Plugin::postUpdate"
        ]        
    }

}

Additionally, in src/Composer folder create Plugin.php:

src/Composer/Plugin.php

<?php

namespace Foo\Bar\Composer;

use Foo\Bar\Test;

/**
 * Composer scripts.
 */
class Plugin
{
    public static function postInstall()
    {
        print_r("POST INSTALL\n");
        print_r(Test::TEST_CONST);
        print_r("\n");
    }

    public static function postUpdate()
    {
        print_r("POST UPDATE\n");
        print_r(Test::TEST_CONST);
        print_r("\n");
    }
}

As you see, it prints constant from Test class. Create it in src/:

src/Test.php

<?php

namespace Foo\Bar;

/**
 * Test class.
 */
class Test
{
    const TEST_CONST = "HERE I AM";
}

Run this and check, how it plays out.

Tomasz Struczyński
  • 3,273
  • 23
  • 28