Skip to content

Commit 186f259

Browse files
committed
Introduce a custom installer plugin for Composer dependencies.
1 parent be6a610 commit 186f259

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed

composer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,18 @@
2323
"squizlabs/php_codesniffer": "3.13.2",
2424
"wp-coding-standards/wpcs": "~3.2.0",
2525
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
26+
"wordpress/custom-installer-plugin": "@dev",
2627
"yoast/phpunit-polyfills": "^1.1.0"
2728
},
29+
"repositories": [
30+
{
31+
"type": "path",
32+
"url": "tools/composer"
33+
}
34+
],
2835
"config": {
2936
"allow-plugins": {
37+
"wordpress/custom-installer-plugin": true,
3038
"dealerdirect/phpcodesniffer-composer-installer": true
3139
},
3240
"lock": false

tools/composer/composer.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "wordpress/custom-installer-plugin",
3+
"description": "Custom Composer installer plugin for WordPress vendor dependencies",
4+
"type": "composer-plugin",
5+
"license": "GPL-2.0-or-later",
6+
"require": {
7+
"composer-plugin-api": "^2.0"
8+
},
9+
"autoload": {
10+
"psr-4": {
11+
"WordPress\\Composer\\": "src/"
12+
}
13+
},
14+
"extra": {
15+
"class": "WordPress\\Composer\\CustomInstallerPlugin"
16+
}
17+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
/**
3+
* Installer for handling custom installation paths.
4+
*/
5+
6+
namespace WordPress\Composer;
7+
8+
use Composer\PartialComposer;
9+
use Composer\IO\IOInterface;
10+
use Composer\Installer\LibraryInstaller;
11+
use Composer\Installer\BinaryInstaller;
12+
use Composer\Package\PackageInterface;
13+
use Composer\Repository\InstalledRepositoryInterface;
14+
use Composer\Util\Filesystem;
15+
use React\Promise\PromiseInterface;
16+
17+
/**
18+
* Installer class.
19+
*/
20+
final class CustomInstaller extends LibraryInstaller {
21+
/**
22+
* Custom installer paths configuration.
23+
*/
24+
private array $installerPaths = array();
25+
26+
/**
27+
* Initializes library installer.
28+
*/
29+
public function __construct(
30+
IOInterface $io,
31+
PartialComposer $composer,
32+
?string $type = 'library',
33+
?Filesystem $filesystem = null,
34+
?BinaryInstaller $binaryInstaller = null
35+
) {
36+
parent::__construct( $io, $composer, $type, $filesystem, $binaryInstaller );
37+
38+
$this->installerPaths = $this->composer->getPackage()->getExtra()['installer-paths'];
39+
}
40+
41+
/**
42+
* Check if this installer supports the given package type.
43+
*
44+
* @param string $packageType The package type.
45+
* @return bool
46+
*/
47+
public function supports( $packageType ) {
48+
return true;
49+
}
50+
51+
/**
52+
* Get the installation path for a package.
53+
*
54+
* @param PackageInterface $package The package.
55+
* @return string The installation path.
56+
*/
57+
public function getInstallPath( PackageInterface $package ) {
58+
$packageName = $package->getName();
59+
60+
if ( ! isset( $this->installerPaths[ $packageName ] ) ) {
61+
return parent::getInstallPath( $package );
62+
}
63+
64+
return realpath( getcwd() ) . '/' . $this->installerPaths[ $packageName ]['target'];
65+
}
66+
67+
/**
68+
* Install a package.
69+
*
70+
* @param InstalledRepositoryInterface $repo The installed repository.
71+
* @param PackageInterface $package The package to install.
72+
* @return PromiseInterface|null
73+
*/
74+
public function install( InstalledRepositoryInterface $repo, PackageInterface $package ) {
75+
$installer = parent::install( $repo, $package );
76+
77+
if ( $installer instanceof PromiseInterface ) {
78+
return $installer->then( fn() => $this->modifyPaths( $package ) );
79+
}
80+
81+
$this->modifyPaths( $package );
82+
return null;
83+
}
84+
85+
/**
86+
* Update a package.
87+
*
88+
* @param InstalledRepositoryInterface $repo The installed repository.
89+
* @param PackageInterface $initial The initial package.
90+
* @param PackageInterface $target The target package.
91+
* @return PromiseInterface|null
92+
*/
93+
public function update( InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target ) {
94+
$updater = parent::update( $repo, $initial, $target );
95+
96+
if ( $updater instanceof PromiseInterface ) {
97+
return $updater->then( fn() => $this->modifyPaths( $target ) );
98+
}
99+
100+
$this->modifyPaths( $target );
101+
return null;
102+
}
103+
104+
/**
105+
* Modify installation paths based on source subdirectory and ignore patterns.
106+
*
107+
* @param PackageInterface $package The package.
108+
*/
109+
private function modifyPaths( PackageInterface $package ): void {
110+
$installPath = $this->getInstallPath( $package );
111+
112+
$this->applySourceSubdirectory( $package, $installPath );
113+
$this->applyIgnorePatterns( $package, $installPath );
114+
}
115+
116+
/**
117+
* Apply ignore patterns to remove unwanted files after installation.
118+
*
119+
* @param PackageInterface $package The package.
120+
* @param string $installPath The installation path.
121+
*/
122+
private function applyIgnorePatterns( PackageInterface $package, string $installPath ): void {
123+
$packageName = $package->getName();
124+
125+
if ( ! isset( $this->installerPaths[ $packageName ]['ignore'] ) ) {
126+
return;
127+
}
128+
129+
$filesystem = new Filesystem();
130+
131+
foreach ( $this->installerPaths[ $packageName ]['ignore'] as $pattern ) {
132+
$matches = glob( $installPath . '/' . $pattern );
133+
134+
if ( empty( $matches ) ) {
135+
throw new \RuntimeException( "Failed to glob pattern '{$pattern}' in package '{$package->getName()}'." );
136+
}
137+
138+
foreach ( $matches as $path ) {
139+
$filesystem->remove( $path );
140+
}
141+
}
142+
}
143+
144+
/**
145+
* Apply source subdirectory to flatten directory structure.
146+
*
147+
* @param PackageInterface $package The package.
148+
* @param string $installPath The installation path.
149+
*/
150+
private function applySourceSubdirectory( PackageInterface $package, string $installPath ): void {
151+
$packageName = $package->getName();
152+
153+
if ( ! isset( $this->installerPaths[ $packageName ]['source'] ) ) {
154+
return;
155+
}
156+
157+
$sourceSubdir = $this->installerPaths[ $packageName ]['source'];
158+
$sourceDir = $installPath . '/' . $sourceSubdir;
159+
160+
if ( ! is_dir( $sourceDir ) ) {
161+
throw new \RuntimeException( "Source directory '{$sourceSubdir}' does not exist in package '{$packageName}'." );
162+
}
163+
164+
$filesystem = new Filesystem();
165+
$tempDir = $installPath . '_temp';
166+
167+
$filesystem->rename( $sourceDir, $tempDir );
168+
$filesystem->removeDirectory( $installPath );
169+
$filesystem->rename( $tempDir, $installPath );
170+
}
171+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
/**
3+
* Custom Composer installer plugin for WordPress.
4+
*/
5+
6+
namespace WordPress\Composer;
7+
8+
use Composer\Composer;
9+
use Composer\IO\IOInterface;
10+
use Composer\Plugin\PluginInterface;
11+
12+
/**
13+
* Custom installer plugin class.
14+
*/
15+
final class CustomInstallerPlugin implements PluginInterface {
16+
/**
17+
* Apply plugin.
18+
*
19+
* @param Composer $composer The Composer instance.
20+
* @param IOInterface $io The IO interface.
21+
*/
22+
public function activate( Composer $composer, IOInterface $io ) {
23+
$installer = new CustomInstaller( $io, $composer );
24+
$composer->getInstallationManager()->addInstaller( $installer );
25+
}
26+
27+
/**
28+
* Remove any hooks from Composer.
29+
*
30+
* @param Composer $composer The Composer instance.
31+
* @param IOInterface $io The IO interface.
32+
*/
33+
public function deactivate( Composer $composer, IOInterface $io ) {
34+
// Nothing to do here.
35+
}
36+
37+
/**
38+
* Prepare the plugin to be uninstalled.
39+
*
40+
* @param Composer $composer The Composer instance.
41+
* @param IOInterface $io The IO interface.
42+
*/
43+
public function uninstall( Composer $composer, IOInterface $io ) {
44+
// Nothing to do here.
45+
}
46+
}

0 commit comments

Comments
 (0)