topthink/think-installer
A Composer installer plugin for ThinkPHP that places framework extensions into the correct project directories. It customizes Composer install paths so packages land where ThinkPHP expects them, simplifying extension management for ThinkPHP applications.
topthink/think-installer is a Composer plugin, not a runtime library. It exists only so that packages declaring a ThinkPHP-specific type (historically think-extend) get installed somewhere other than vendor/ — the plugin’s supports() / getInstallPath() decide the destination. If you are on ThinkPHP 6+, you almost certainly do not need it: modern topthink/think-* packages are ordinary PSR-4 libraries under vendor/.
Minimal path to a working setup:
# 1. It is usually already pulled in transitively — check first
composer show topthink/think-installer
# 2. If not, require it explicitly (it must be in "require", not "require-dev",
# because it has to be active while other packages are being installed)
composer require topthink/think-installer
# 3. Composer 2.2+ blocks plugins by default — allow it
composer config allow-plugins.topthink/think-installer true
# 4. Re-run so the plugin is loaded and paths are recalculated
composer update --no-scripts -vvv
Where to look first:
vendor/topthink/think-installer/src/ — read the Plugin and installer classes. Two methods tell you everything: supports() (which package type values are intercepted) and getInstallPath() (the literal target directory, e.g. extend/ or application/).composer.json — the "type" field is the trigger. Change it to library and the plugin becomes a no-op for that package.vendor/composer/installed.json — authoritative record of what was installed where.First use case: you composer require a legacy ThinkPHP 5 extension that declares "type": "think-extend", and instead of finding it in vendor/acme/foo/ it appears under extend/acme/foo/. That relocation is the entire value proposition.
Authoring an extension that uses the installer. Set the type in the package’s own composer.json; nothing else is required on the consumer side:
{
"name": "acme/think-hello",
"type": "think-extend",
"require": { "php": ">=7.1" },
"autoload": { "psr-4": { "acme\\hello\\": "src/" } }
}
Remember the namespace still has to resolve at runtime — see the gotchas below.
Keep the plugin in require, always. A plugin listed only in require-dev disappears under composer install --no-dev, so a production build installs the same packages into vendor/ instead of their custom path. That is a silent environment divergence and the single most common failure mode with this package.
Lockfile discipline for CI. Commit composer.lock and pin allow-plugins in composer.json so a fresh CI container does not stall on Composer’s interactive plugin prompt:
{
"config": {
"allow-plugins": { "topthink/think-installer": true }
}
}
Install-order awareness. The plugin activates only after it is itself present. On a cold first install of a project that requires both the plugin and several think-extend packages, Composer resolves everything in one pass and the plugin is active for that run — but if you add the plugin in a later commit, previously installed packages stay in vendor/ until you force a reinstall: composer update --no-scripts or remove and re-add the affected packages.
Private/company registries. If you host a set of internal think-extend modules, this plugin is effectively your installer middleware; every consumer project needs it in require. Document that dependency next to the module registry rather than in each module.
Custom paths (extension point). Unlike composer/installers, path selection here is code, not config — there is no extra.installer-paths to override per project. If you need a different target directory, you must fork the package or write your own tiny plugin:
class Plugin implements PluginInterface, EventSubscriberInterface {
public function getInstallers() {
return [new MyInstaller($this->composer)];
}
}
with MyInstaller extends LibraryInstaller and an overridden getInstallPath(). Register it under a distinct package type you control.
allow-plugins, Composer warns and then proceeds without the plugin, so think-extend packages fall back to the default LibraryInstaller and land in vendor/. You get a working-looking install with the wrong layout. Always run one -vvv install after configuring a new environment to confirm the plugin activated.--no-plugins disables it. Note the distinction: --no-scripts does not disable Composer plugins, --no-plugins does. CI scripts that blanket-apply --no-plugins for “security” will silently change your install paths.vendor/ are outside Composer’s autoloader. The package’s autoload PSR-4 map is generated relative to wherever it was installed, and the framework then has to include or map that path itself. If you get Class not found right after a successful install, this mismatch — not the plugin — is usually the cause. Check vendor/composer/autoload_psr4.php first.vendor-dir overrides can bite. The installer historically derives paths from Composer config. A custom "vendor-dir" or a non-standard project root (Laravel-style public/ bootstrap, Docker volume mounts) can make getInstallPath() resolve somewhere unexpected. composer show -p plus find . -newer composer.json -name composer.json quickly reveals where things actually went.LibraryInstaller::uninstall() cleans the package files but directory trees often survive; sweep them in a post-install script if your deployment asserts on a clean tree.composer/composer isn’t an option for most teams, so the realistic mitigation is migrating extensions to "type": "library" with normal PSR-4 autoloading.allow-plugins entry and a fork risk to every install. Reach for it only when maintaining a TP5-era codebase.composer diagnose → composer show topthink/think-installer (is it installed and which version?) → grep allow-plugins in composer.json → composer install -vvv 2>&1 | grep -i plugin → inspect vendor/composer/installed.json for install-path. That chain resolves essentially every reported problem with this package.How can I help you explore Laravel packages today?