
How to safely use Composer in add-ons with PHP-Scoper
ExpressionEngine does not ship with a universal Composer file or support installing add-ons via Composer. However, many add-on developers use Composer internally to include popular packages and avoid reinventing the wheel.
While powerful, this approach can create conflicts when two or more add-ons require the same third-party package. If they use the same version, everything works smoothly. But when versions differ, trouble can arise—and the only way to spot a potential conflict is by digging through source code. Let’s be honest: nobody does that.
If you develop and distribute add-ons, dependency conflicts are always a risk. The only reliable solution is scoping your dependencies.
Scoping means updating your add-on’s vendor references from something like:
use Illuminate\Database\DatabaseManager;
You’ll update it to something like this:
use BoldMinded\Queue\Dependency\Illuminate\Database\DatabaseManager;
With scoping in place, multiple add-ons can use the same package—even different versions (e.g., Laravel 7, 9, or 12)—without conflicts. Otherwise, you might receive support tickets about cryptic errors like:
“Call to undefined method GuzzleHttp\Utils::chooseHandler()”
In such cases, Add-on A might use a newer Guzzle version that includes chooseHandler(). But if Add-on B loads afterward with an older Guzzle version lacking that method, everything breaks.
To prevent these issues, I use PHP-Scoper for all my add-ons. In this guide, I’ll walk through how to update an add-on using PHP-Scoper. (Full transparency: I learned to use PHP-Scoper by studying ExpressionEngine’s repository, which also scopes some dependencies.)
You could do this manually by editing package files in your vendor
folder and adding the vendor
folder to your add-on’s zip file for distribution, but doing that would be very time consuming, and you’ll have to repeat that work the next time you update a package to the latest version. This is exactly the kind of thing we want to automate, and that is what PHP-Scoper does. It reads all the files in your vendor
folder, updates class names, use statements, and various other things necessary to isolate the files in your vendor
directory to just your add-on.
For this example, I’ll use the Queue module.
First, we’re going to make sure our Git ignore settings are updated. We’ll be adding two new new directories, vendor-build
and vendor-bin
. We want to keep vendor-bin
and it’s contents and commit them to Git, but we want to ignore the vendor
directory within vendor-bin
. So we’ll update our .gitignore
file to contain the following:
/addons/queue/vendor
/addons/queue/vendor-build
/addons/queue/vendor-bin/php-scoper/vendor/
PHP-Scoper needs to be installed with it’s own vendor
directory. Create a vendor-bin/php-scoper
folder and scope.inc.php
file in your add-on directory.
Your folder should look similar to this:
addons/
myaddon/
Model/
Service/
scoper.inc.php
views/
vendor-bin/
php-scoper/
composer.json
Add the following to your new vendor-bin/php-scoper/composer.json
file with the following contents. The reason we add all of this to a separate directory is if PHP-Scoper is installed into the main vendor
directory, it will try to update it’s own namespaces, and we don’t want that.
{
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"humbug/php-scoper": "^0.17.5"
}
}
Next, you’ll need to tell PHP-Scoper how to scope your add-on’s dependencies. Below is a full example of the scoper.inc.php
file used for the Queue module. The main setting to update is the prefix
, which defines the namespace PHP-Scoper will apply to your vendor files.
The 'finders'
property can be updated to include any files or folders you want to ignore.
The 'patchers'
property is kind of a special one. In this example, which is a little unique, and I’ve only had to do this for 2 of my add-ons, highlights some edge cases where PHP-Scoper just doesn’t work. It’s important to note that I didn’t just know to add these patchers. I added them after receiving random errors after running the scoper. If you run into such issues then it’ll be trial and error for you to make the appropriate updates through the patchers. In most cases you can leave patchers as an empty array.
'patchers' => []
Here is a full example of the scoper.inc.php
file used for the Queue add-on highlighting the shenanigans in the patchers
array. For comparison the patchers
array for all my other add-ons, except Publisher, is empty and the overall file is much simpler.
<?php
use Isolated\Symfony\Component\Finder\Finder;
return [
'prefix' => 'BoldMinded\\Queue\\Dependency',
'finders' => [
Finder::create()
->files()
->ignoreVCS(true)
->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
->exclude([
'bin',
'doc',
'test',
'test_old',
'tests',
'Tests',
'vendor-bin',
'vendor',
])
->in('vendor'),
],
'exclude-files' => [],
'patchers' => [
static function (string $filePath, string $prefix, string $contents): string {
if (strpos($filePath, 'helpers.php') !== false) {
$contents = str_replace(
"function_exists('BoldMinded\\\\Queue\\\\Dependency\\\\",
"function_exists('",
$contents
);
$contents = str_replace(
'namespace BoldMinded\Queue\Dependency;',
'',
$contents
);
}
// Because the AWS package we're using from EE's Cloud Files add-on expects this.
if (strpos($filePath, 'SqsQueue.php') !== false) {
$contents = str_replace(
'use BoldMinded\Queue\Dependency\Aws\Sqs\SqsClient;',
'use BoldMinded\Queue\Queue\Connectors\SqsClient;',
$contents
);
}
// https://boldminded.com/support/ticket/2715
// Fix deprecation warning in PHP 8.1 without upgrading all packages which don't work in PHP 7.4 :/ if (strpos($filePath, 'CarbonInterface.php') !== false) {
$contents = str_replace(
'public function jsonSerialize();',
'#[\ReturnTypeWillChange]' . "\n" .' public function jsonSerialize();',
$contents
);
}
if (strpos($filePath, 'Creator.php') !== false) {
$contents = str_replace(
'static::setLastErrors(parent::getLastErrors());',
'if (is_array(parent::getLastErrors())) { static::setLastErrors(parent::getLastErrors()); }',
$contents
);
}
return $contents;
},
],
'exclude-namespaces' => [
'BoldMinded\Queue',
'Composer\Autoload',
'PHPUnit',
],
'exclude-classes' => [
],
'exclude-functions' => [
// 'mb_str_split',
],
'exclude-constants' => [
// 'STDIN',
],
'expose-global-constants' => true,
'expose-global-classes' => true,
'expose-global-functions' => true,
'expose-namespaces' => [],
'expose-classes' => [],
'expose-functions' => [],
'expose-constants' => [],
];
Below is the full composer.json
file for Queue. We’ll talk more about the individual parts soon. For now, you will need to run composer require bamarni/composer-bin-plugin
which will ask you if it should allow plugins to run, and will add the config.allow-plugins
section to your composer.json file.
The extra
section may also need to be added manually to your composer.json file. Note that the target-directory
property links to the newly created vendor-bin
directory mentioned above.
{
"name": "boldminded/queue",
"description": "ExpressionEngine's missing queue module",
"license": "proprietary",
"require": {
"litzinger/basee": "dev-master",
"bamarni/composer-bin-plugin": "^1.8.2",
"illuminate/queue": "^12.1",
"illuminate/bus": "^12.1",
"illuminate/contracts": "^12.1",
"nesbot/carbon": "3.8.6",
"illuminate/events": "^12.1",
"illuminate/redis": "^12.1"
},
"config": {
"vendor-dir": "addons/queue/vendor",
"preferred-install": "dist",
"platform": {
"php": "8.2"
},
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"extra": {
"bamarni-bin": {
"bin-links": false,
"target-directory": "addons/queue/vendor-bin",
"forward-command": false
}
},
"autoload": {
"psr-4": {
"BoldMinded\\Queue\\" : "addons/queue"
}
},
"scripts": {
"scope": [
"rm -rf ./addons/queue/vendor-build",
"./addons/queue/vendor-bin/php-scoper/vendor/bin/php-scoper add-prefix --output-dir=vendor-build --working-dir=addons/queue --force",
"COMPOSER_VENDOR_DIR=addons/queue/vendor-build composer dump-autoload"
],
"post-install-cmd": [
"@composer bin all install --ansi",
"@scope"
],
"post-update-cmd": [
"@composer bin all install --ansi",
"@scope"
]
},
"require-dev": {
"rector/rector": "^2.0"
}
}
Then at the top of your add-ons addon.setup.php
file add the path to the vendor-build
directory.
<?php
require_once PATH_THIRD . 'queue/vendor-build/autoload.php';
At this point, everything should be configured. When you load the ExpressionEngine control panel with your add-on installed, you might encounter some errors. That’s expected until you run:
composer install
Or, if you’re paying close attention to the composer.json example above, you can run composer scope
on its own.
Last thing to remember, when you package up your add-on as a zip to share with the world, be sure to delete the vendor
and vendor-bin
folders. You should only ship the vendor-build
folder, which includes everything that is in vendor
(minus dev only dependencies) except all the files should have a prefixed namespace at the top of them, thus making them scoped to your add-on.
I hope this helps other add-on developers who are distributing software and leveraging Composer. Don’t let past experiences with vendor conflicts discourage you—Composer can be safely and effectively integrated into your add-ons. By scoping your dependencies, you can avoid conflicts, improve maintainability, and confidently take advantage of modern PHP packages in your development workflow.
Comments 0
Be the first to comment!