GuidesComposer package managerPHP dependency management

Composer Installer Plugins: Customizing Package Paths and Managing Dependencies

Composercomposer.jsonComposer Installers plugincustom installer classpackage typeinstallation pathsvendor directorydependency versioningPostScriptsVCS repositoryplugin architecture

A default Composer setup installs everything into the same directory — which quickly becomes unreadable and impractical. The goal here is to keep the main application in its own location and vendor packages in a dedicated directory. The exact structure depends on the application, but a common and workable assumption is that the application lives in /app/docroot/ and vendors in /app/vendor.

By the end of this guide, the target directory structure looks like this:

app

  • build
  • composer.json
  • docroot
  • vendor

Composer Plugins

Composer's installation flow looks straightforward on the surface, but it's actually designed to be extended. Plugins can modify and augment nearly every aspect of how Composer behaves — including where it installs packages.

The most relevant capability here is changing the target installation path for packages, which is handled through a custom Composer plugin.

The Composer Installers Plugin

The Composer Installers plugin (https://github.com/composer/installers) is the natural starting point. It ships with installers for many widely used package types, lets you customize install paths, and is distributed under a free license — meaning it can be downloaded and modified to fit specific needs. For projects built on custom application frameworks, this plugin provides the scaffolding for a custom installer without requiring a full build from scratch.

There are two mechanisms for controlling where a package gets installed:

  1. By package name — a direct path mapping keyed to a specific package
  2. By package type — a fallback mechanism that matches based on the declared type in composer.json

Path Mapping by Package Name

The Composer Installers plugin handles the name-based approach out of the box. After installing the plugin, add the following to the main composer.json file:

"extra": {
    "installer-paths": {
        "../docroot/": ["vendor/app"]
    }
}

The path comes first, followed by the exact package it applies to. Note that the path is relative to the location of the composer.json file — in this case, located in your_app/build/. For applications that don't use add-ons or plugins, this is sufficient.

Path Mapping by Package Type

When the project can incorporate additional packages — think of open-source platforms like Piwik or WordPress, both of which have large plugin ecosystems — the name-based approach doesn't scale. This is where package type comes in.

After attempting to match a package by name, Composer Installers falls back to matching by package type. Many package types for popular platforms are already handled by the Composer Installers plugin, so it's worth checking the documentation before building something custom.

For cases that aren't covered publicly, or where contributing to the upstream project isn't appropriate, a custom type-based installer is the answer.

Naming Conventions for Package Types

Package types follow a standard naming convention:

{main_application_name}-{sub_application_type}

Examples: cakephp-plugin, wordpress-plugin. The sub-type can be anything relevant — plugin, theme, module, component, etc. The prefix immediately identifies which master application the package belongs to. This convention is baked into the default logic in Composer Installers but can be changed to suit any application's needs.

Customizing the Installer Plugin

The simplest approach is to fork and modify the existing Composer Installers plugin rather than building one from scratch. The following steps cover what's needed:

  1. Download the original Installers plugin from https://github.com/composer/installers
  2. Open installers/src/Composer/Installers/Installer.php in an editor
  3. Locate the $supportedTypes array at approximately line 15 — this is where new custom package types are registered. Add an entry using the package type name as the key and the installer class name as the value:
    'myComposerType' => 'myInstallerClass',
    
    It's also possible to map a custom installer to standard Composer types like application.
  4. Create the installer class, for example at installers/src/Composer/Installers/MyInstaller.php. A working example can be found at https://github.com/mgazdzik/installers
  5. Define install paths using the $locations array inside the new class — one entry per sub-type (plugin, theme, extension, etc.)

That's the full scope of the customization. The result is a tailored installer that routes specific package types to the right directories.

Making the Custom Installer Available

Once the custom Installers package is ready, it needs to be hosted somewhere Composer can reach it — a local repository or any VCS-based source will work.

To use it, add the following to the require section of composer.json:

"require": {
    "composer/installers": "dev-master"
}

Important: this entry should appear first in the require section. Composer processes requirements in order, and the installers need to be downloaded and initialized before any other vendors are resolved. If they aren't loaded first, packages that depend on them for their install path will be placed in the wrong location.

A Key Caveat: Composer Update and Nested Packages

There's a behaviour worth understanding before relying on this setup in a real deployment: when composer update runs on a master application, Composer purges that package's directory and creates a fresh clone. Any plugins installed inside that directory are lost.

Composer does not detect that those nested vendors have been removed, and it will not re-download them. The result is a master application stripped of its plugins.

The reverse scenario — updating a plugin that lives inside the master application — works fine. Whether this is a practical problem depends on the specific lifecycle of the application in question.

There are patterns for avoiding this issue (notably using post-install and post-update scripts), but the key takeaway is to be aware of this mechanism early. Discovering that all plugins have disappeared after an update, without understanding why, is a common source of confusion.

Versioning and Dependency Constraints

As the number of Composer-managed packages grows, consistent versioning becomes increasingly important. Tracking versions carefully prevents confusion when navigating a project's commit history, and it unlocks one of Composer's more powerful features: two-way dependency verification.

Each composer.json file can declare its own dependencies. A particularly useful pattern is to specify a minimum required version of the master application inside a plugin's composer.json. When a developer later modifies the plugin in a way that requires changes to the master application, that requirement can be encoded directly in the package metadata.

If a user then tries to install an incompatible combination — a new plugin version with an outdated master application, for example — Composer will produce an error and halt the install or update. This mechanism extends across the entire dependency graph: it covers plugin-to-application relationships, plugin-to-plugin relationships, and even minimum PHP version requirements.

The practical benefit is that well-maintained versions and dependency definitions create hard constraints between compatible versions of a codebase, reducing the chance of deploying an application with incompatible add-ons.

That said, dependency constraint design requires careful thought. Overly tight constraints can make development and maintenance significantly more painful over time.

Summary

This guide covered how to customize Composer package installation paths and structure a multi-package project correctly. The key takeaways:

  • Composer's installation flow can be extended using plugins, and the Composer Installers plugin provides a solid foundation for custom path logic
  • Package installation paths can be controlled by package name (direct mapping) or package type (convention-based fallback using {app}-{type} naming)
  • Custom installer types are registered in the $supportedTypes array in Installer.php
  • The installer package's require entry should always appear first in the require section so it loads before other dependencies
  • Running composer update on a master application will purge its directory, potentially removing nested plugins — this needs to be accounted for in deployment workflows (e.g., via PostScripts)
  • Proper versioning and dependency constraints in each composer.json enable Composer's two-way verification, preventing incompatible package combinations from being installed

With these building blocks in place, it becomes straightforward to deploy a complete codebase to a well-organized directory structure, with each component landing exactly where it belongs.