mnsami/composer-custom-directory-installer

A composer plugin, to help install packages of different types in custom paths.

Maintainers

Package info

github.com/mnsami/composer-custom-directory-installer

Type:composer-plugin

pkg:composer/mnsami/composer-custom-directory-installer

Fund package maintenance!

mnsami

Statistics

Installs: 5 174 165

Dependents: 69

Suggesters: 0

Stars: 140

Open Issues: 0


README

A Composer plugin to install packages in custom directories outside the default vendor folder.

This is not another composer-installer library for supporting non-composer package types such as application. By default it handles library-type packages, but you can extend it to any Composer package type via extra.installer-types in your root composer.json.

Table of Contents

https://getcomposer.org/doc/04-schema.md#type

The type of the package. It defaults to library.

Package types are used for custom installation logic. If you have a package that needs some special logic, you can define a custom type. This could be a symfony-bundle, a wordpress-plugin or a typo3-module. These types will all be specific to certain projects, and they will need to provide an installer capable of installing packages of that type.

Why this plugin?

Composer has a built-in installer-paths key, but it only works if the package itself declares a dependency on composer/installers — something most packages on Packagist don't do. If you add an installer-paths rule for a package that doesn't opt in, Composer silently ignores it and installs the package to vendor/ anyway.

This plugin removes that constraint. It intercepts installation at the root-project level, so any package can be redirected to a custom path without requiring any changes to the package itself.

In short: if you've ever added installer-paths and nothing happened, this plugin is the fix.

Requirements

  • PHP >= 8.1
  • Composer 2.x

Installation

Add the plugin to the require section of your composer.json:

"require": {
    "mnsami/composer-custom-directory-installer": "^2.1"
}

Important — Composer 2.2+ plugin trust:
Composer 2.2 and later require you to explicitly allow third-party plugins. Add the following to your composer.json:

"config": {
    "allow-plugins": {
        "mnsami/composer-custom-directory-installer": true
    }
}

Without this, Composer will either prompt interactively or block the plugin entirely in non-interactive (CI) environments.

How to use

In the extra section of your root composer.json, define the custom directory for each package:

"extra": {
    "installer-paths": {
        "./monolog/": ["monolog/monolog"]
    }
}

This tells Composer to install monolog/monolog into the ./monolog/ directory instead of vendor/monolog/monolog.

Path Variables

You can use the following variables in your installer-paths to build dynamic paths:

Variable Description Example value
{$vendor} The vendor portion of the package name monolog
{$name} The package name (or installer-name override) monolog
{$type} The Composer package type library
"extra": {
    "installer-paths": {
        "./customlibs/{$vendor}/db/{$name}": ["doctrine/orm"],
        "./custom/{$type}/{$vendor}/{$name}": ["acme/*"]
    }
}

Path Variable Flags

You can append transformation flags after a pipe (|) to modify how a variable is substituted:

{$token|flags}

Flags are applied left-to-right in the order given:

Flag Transformation Example input Example output
F Capitalize first letter (ucfirst) my-package My-package
P Strip hyphens/underscores and capitalize each following word my-package myPackage
U Uppercase all characters my-package MY-PACKAGE

Flags can be combined. FP together produces PascalCase (capitalize first + strip separators):

Expression Input Output
{$name|F} my-package My-package
{$name|P} my-package myPackage
{$name|FP} my-package MyPackage
{$name|U} my-package MY-PACKAGE
{$vendor|U} acme ACME

Example:

"installer-paths": {
    "src/{$vendor|U}/{$name|FP}/": ["acme/my-package"],
    "modules/{$name|FP}/":         ["type:drupal-module"]
}

For a package acme/my-package (type library), this resolves to src/ACME/MyPackage/.

Matching Strategies

installer-paths supports three matching strategies. Precedence is evaluated globally across all entries: all entries are first scanned for an exact name match, then for a type match, then for a wildcard match. An exact match in a later-listed entry always wins over a wildcard match in an earlier-listed entry.

1. Exact package name (highest precedence)

Matches one specific package:

"installer-paths": {
    "./libs/monolog/": ["monolog/monolog"]
}

2. Package type prefix

Matches all packages of a given Composer type using the type: prefix:

"installer-paths": {
    "./wp-content/plugins/{$name}/": ["type:wordpress-plugin"]
}

3. Wildcard vendor glob (lowest precedence)

Matches all packages from a given vendor using *:

"installer-paths": {
    "./acme-libs/{$name}/": ["acme/*"]
}

Custom installer-name

A package can override the {$name} variable by setting installer-name in its own extra section (inside the package's composer.json, not the root project):

"extra": {
    "installer-name": "my-custom-name"
}

When set, {$name} in the path template will resolve to my-custom-name instead of the package's actual name.

Supporting Custom Package Types

By default the plugin only handles the library package type. To install packages of other types (e.g. drupal-module, wordpress-plugin) into custom directories, declare those types in extra.installer-types:

"extra": {
    "installer-types": ["drupal-module", "wordpress-plugin"],
    "installer-paths": {
        "web/modules/{$name}/": ["type:drupal-module"],
        "wp-content/plugins/{$name}/": ["type:wordpress-plugin"]
    }
}

The plugin will claim any package whose type appears in installer-types and apply the matching installer-paths rule. Without this list, packages of non-library types are left to Composer's default installer.

Complete example

{
    "require": {
        "mnsami/composer-custom-directory-installer": "^2.1",
        "monolog/monolog": "*",
        "acme/foo": "*",
        "acme/bar": "*"
    },
    "config": {
        "allow-plugins": {
            "mnsami/composer-custom-directory-installer": true
        }
    },
    "extra": {
        "installer-types": ["wordpress-plugin"],
        "installer-paths": {
            "./logger/":              ["monolog/monolog"],
            "./acme/{$name}/":        ["acme/*"],
            "./plugins/{$name}/":     ["type:wordpress-plugin"]
        }
    }
}

Real-world use cases

WordPress with WPackagist

Most WordPress plugins available via WPackagist don't require composer/installers, so native installer-paths silently fails for them. This plugin makes it work:

{
    "require": {
        "mnsami/composer-custom-directory-installer": "^2.1",
        "wpackagist-plugin/contact-form-7": "*",
        "wpackagist-theme/twentytwentyfour": "*"
    },
    "config": {
        "allow-plugins": {
            "mnsami/composer-custom-directory-installer": true
        }
    },
    "extra": {
        "installer-types": ["wordpress-plugin", "wordpress-theme"],
        "installer-paths": {
            "wp-content/plugins/{$name}/": ["type:wordpress-plugin"],
            "wp-content/themes/{$name}/":  ["type:wordpress-theme"]
        }
    }
}

Docker / keeping vendor outside the web root

Put one package (e.g. a CLI tool) in a path that's bind-mounted into a container, while everything else stays in vendor/:

"extra": {
    "installer-paths": {
        "/tools/{$name}/": ["vendor/some-cli-tool"]
    }
}

Monorepos with sibling library directories

Route internal packages to their canonical location in the repo tree:

"extra": {
    "installer-types": ["company-module"],
    "installer-paths": {
        "../modules/{$name}/": ["type:company-module"]
    }
}

PascalCase or namespaced paths

Use path variable flags to match the naming convention your framework expects:

"extra": {
    "installer-paths": {
        "src/Modules/{$name|FP}/": ["acme/*"]
    }
}

acme/my-module installs to src/Modules/MyModule/.

Migrating from oomphinc/composer-installers-extender

oomphinc/composer-installers-extender has not had a functional release since December 2021. This plugin is a maintained, drop-in alternative with a superset of its features (path variable flags, three-pass precedence, traversal guards).

Before:

"require": {
    "composer/installers": "^2.0",
    "oomphinc/composer-installers-extender": "^2.0"
},
"config": {
    "allow-plugins": {
        "composer/installers": true,
        "oomphinc/composer-installers-extender": true
    }
},
"extra": {
    "installer-types": ["drupal-module"],
    "installer-paths": {
        "web/modules/contrib/{$name}/": ["type:drupal-module"]
    }
}

After:

"require": {
    "mnsami/composer-custom-directory-installer": "^2.1"
},
"config": {
    "allow-plugins": {
        "mnsami/composer-custom-directory-installer": true
    }
},
"extra": {
    "installer-types": ["drupal-module"],
    "installer-paths": {
        "web/modules/contrib/{$name}/": ["type:drupal-module"]
    }
}

The extra.installer-types and extra.installer-paths keys are identical — only the require and allow-plugins entries change. If you required composer/installers solely for path routing (not for any package's own declared dependency), you can remove it too.

Security

Resolved install paths are validated against two attack vectors:

  • Directory traversal — a resolved path containing .. throws an InvalidArgumentException.
  • Absolute path injection — a resolved path that is absolute (starting with / or a Windows drive letter) throws an InvalidArgumentException. This can occur when a package's installer-name is set to an absolute path value.

Both checks apply after all {$variable} substitutions are complete.

Upgrading from v1.x

v1.x v2.x
PHP >= 5.3 >= 8.1
Composer 1.x / 2.x 2.x only
Require string "1.*" "^2.1"
type: matching No Yes
Wildcard vendor/* No Yes
{$type} variable No Yes
allow-plugins needed No Yes (Composer 2.2+)
installer-types support No Yes
Path variable flags (|F, |P, |U) No Yes

Existing installer-paths configurations (exact package names) are fully backwards-compatible and require no changes.

Note

Composer type: project is not supported by this installer, as packages with type project only make sense to be used with application shells like symfony/framework-standard-edition.