Skip to content

mnsami/composer-custom-directory-installer

Repository files navigation

composer-custom-directory-installer

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.

About

A composer plugin, to install differenty types of composer packages in custom directories outside the default composer default installation path which is in the vendor folder.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages