Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Introduce a custom installer plugin for Composer dependencies.
  • Loading branch information
johnbillion committed Nov 6, 2025
commit 186f259abd47eb284006fbb4d62e4a82ed1d1c65
8 changes: 8 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@
"squizlabs/php_codesniffer": "3.13.2",
"wp-coding-standards/wpcs": "~3.2.0",
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
"wordpress/custom-installer-plugin": "@dev",
"yoast/phpunit-polyfills": "^1.1.0"
},
"repositories": [
{
"type": "path",
"url": "tools/composer"
}
],
"config": {
"allow-plugins": {
"wordpress/custom-installer-plugin": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"lock": false
Expand Down
17 changes: 17 additions & 0 deletions tools/composer/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "wordpress/custom-installer-plugin",
"description": "Custom Composer installer plugin for WordPress vendor dependencies",
"type": "composer-plugin",
"license": "GPL-2.0-or-later",
"require": {
"composer-plugin-api": "^2.0"
},
"autoload": {
"psr-4": {
"WordPress\\Composer\\": "src/"
}
},
"extra": {
"class": "WordPress\\Composer\\CustomInstallerPlugin"
}
}
171 changes: 171 additions & 0 deletions tools/composer/src/CustomInstaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
/**
* Installer for handling custom installation paths.
*/

namespace WordPress\Composer;

use Composer\PartialComposer;
use Composer\IO\IOInterface;
use Composer\Installer\LibraryInstaller;
use Composer\Installer\BinaryInstaller;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;

/**
* Installer class.
*/
final class CustomInstaller extends LibraryInstaller {
/**
* Custom installer paths configuration.
*/
private array $installerPaths = array();

/**
* Initializes library installer.
*/
public function __construct(
IOInterface $io,
PartialComposer $composer,
?string $type = 'library',
?Filesystem $filesystem = null,
?BinaryInstaller $binaryInstaller = null
) {
parent::__construct( $io, $composer, $type, $filesystem, $binaryInstaller );

$this->installerPaths = $this->composer->getPackage()->getExtra()['installer-paths'];
}

/**
* Check if this installer supports the given package type.
*
* @param string $packageType The package type.
* @return bool
*/
public function supports( $packageType ) {
return true;
}

/**
* Get the installation path for a package.
*
* @param PackageInterface $package The package.
* @return string The installation path.
*/
public function getInstallPath( PackageInterface $package ) {
$packageName = $package->getName();

if ( ! isset( $this->installerPaths[ $packageName ] ) ) {
return parent::getInstallPath( $package );
}

return realpath( getcwd() ) . '/' . $this->installerPaths[ $packageName ]['target'];
}

/**
* Install a package.
*
* @param InstalledRepositoryInterface $repo The installed repository.
* @param PackageInterface $package The package to install.
* @return PromiseInterface|null
*/
public function install( InstalledRepositoryInterface $repo, PackageInterface $package ) {
$installer = parent::install( $repo, $package );

if ( $installer instanceof PromiseInterface ) {
return $installer->then( fn() => $this->modifyPaths( $package ) );
}

$this->modifyPaths( $package );
return null;
}

/**
* Update a package.
*
* @param InstalledRepositoryInterface $repo The installed repository.
* @param PackageInterface $initial The initial package.
* @param PackageInterface $target The target package.
* @return PromiseInterface|null
*/
public function update( InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target ) {
$updater = parent::update( $repo, $initial, $target );

if ( $updater instanceof PromiseInterface ) {
return $updater->then( fn() => $this->modifyPaths( $target ) );
}

$this->modifyPaths( $target );
return null;
}

/**
* Modify installation paths based on source subdirectory and ignore patterns.
*
* @param PackageInterface $package The package.
*/
private function modifyPaths( PackageInterface $package ): void {
$installPath = $this->getInstallPath( $package );

$this->applySourceSubdirectory( $package, $installPath );
$this->applyIgnorePatterns( $package, $installPath );
}

/**
* Apply ignore patterns to remove unwanted files after installation.
*
* @param PackageInterface $package The package.
* @param string $installPath The installation path.
*/
private function applyIgnorePatterns( PackageInterface $package, string $installPath ): void {
$packageName = $package->getName();

if ( ! isset( $this->installerPaths[ $packageName ]['ignore'] ) ) {
return;
}

$filesystem = new Filesystem();

foreach ( $this->installerPaths[ $packageName ]['ignore'] as $pattern ) {
$matches = glob( $installPath . '/' . $pattern );

if ( empty( $matches ) ) {
throw new \RuntimeException( "Failed to glob pattern '{$pattern}' in package '{$package->getName()}'." );
}

foreach ( $matches as $path ) {
$filesystem->remove( $path );
}
}
}

/**
* Apply source subdirectory to flatten directory structure.
*
* @param PackageInterface $package The package.
* @param string $installPath The installation path.
*/
private function applySourceSubdirectory( PackageInterface $package, string $installPath ): void {
$packageName = $package->getName();

if ( ! isset( $this->installerPaths[ $packageName ]['source'] ) ) {
return;
}

$sourceSubdir = $this->installerPaths[ $packageName ]['source'];
$sourceDir = $installPath . '/' . $sourceSubdir;

if ( ! is_dir( $sourceDir ) ) {
throw new \RuntimeException( "Source directory '{$sourceSubdir}' does not exist in package '{$packageName}'." );
}

$filesystem = new Filesystem();
$tempDir = $installPath . '_temp';

$filesystem->rename( $sourceDir, $tempDir );
$filesystem->removeDirectory( $installPath );
$filesystem->rename( $tempDir, $installPath );
}
}
46 changes: 46 additions & 0 deletions tools/composer/src/CustomInstallerPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
/**
* Custom Composer installer plugin for WordPress.
*/

namespace WordPress\Composer;

use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;

/**
* Custom installer plugin class.
*/
final class CustomInstallerPlugin implements PluginInterface {
/**
* Apply plugin.
*
* @param Composer $composer The Composer instance.
* @param IOInterface $io The IO interface.
*/
public function activate( Composer $composer, IOInterface $io ) {
$installer = new CustomInstaller( $io, $composer );
$composer->getInstallationManager()->addInstaller( $installer );
}

/**
* Remove any hooks from Composer.
*
* @param Composer $composer The Composer instance.
* @param IOInterface $io The IO interface.
*/
public function deactivate( Composer $composer, IOInterface $io ) {
// Nothing to do here.
}

/**
* Prepare the plugin to be uninstalled.
*
* @param Composer $composer The Composer instance.
* @param IOInterface $io The IO interface.
*/
public function uninstall( Composer $composer, IOInterface $io ) {
// Nothing to do here.
}
}