Skip to content

Commit daea918

Browse files
committed
Change package type to "composer-plugin".
Add new Plugin class which implements Composer's PluginInterface.
1 parent 340af1f commit daea918

3 files changed

Lines changed: 612 additions & 4 deletions

File tree

composer.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cakephp/plugin-installer",
33
"description": "A composer installer for CakePHP 3.0+ plugins.",
4-
"type": "composer-installer",
4+
"type": "composer-plugin",
55
"license": "MIT",
66
"authors": [
77
{
@@ -10,11 +10,12 @@
1010
}
1111
],
1212
"require": {
13-
"php": "^7.2"
13+
"php": "^7.2",
14+
"composer-plugin-api": "^1.0||^2.0"
1415
},
1516
"require-dev": {
1617
"cakephp/cakephp-codesniffer": "^4.1",
17-
"composer/composer": "^2.0@dev",
18+
"composer/composer": "^2.0",
1819
"phpunit/phpunit": "~8.5.0"
1920
},
2021
"autoload": {
@@ -28,7 +29,7 @@
2829
}
2930
},
3031
"extra": {
31-
"class": "Cake\\Composer\\Installer\\PluginInstaller"
32+
"class": "Cake\\Composer\\Plugin"
3233
},
3334
"config": {
3435
"sort-packages": true

src/Plugin.php

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Composer;
5+
6+
use Composer\Composer;
7+
use Composer\EventDispatcher\EventSubscriberInterface;
8+
use Composer\IO\IOInterface;
9+
use Composer\Package\PackageInterface;
10+
use Composer\Plugin\PluginInterface;
11+
use Composer\Script\Event;
12+
use RuntimeException;
13+
14+
class Plugin implements PluginInterface, EventSubscriberInterface
15+
{
16+
/**
17+
* @inheritDoc
18+
*/
19+
public function activate(Composer $composer, IOInterface $io)
20+
{
21+
}
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
public function deactivate(Composer $composer, IOInterface $io)
27+
{
28+
}
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public function uninstall(Composer $composer, IOInterface $io)
34+
{
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
public static function getSubscribedEvents()
41+
{
42+
return [
43+
'post-autoload-dump' => 'postAutoloadDump',
44+
];
45+
}
46+
47+
/**
48+
* Called whenever composer (re)generates the autoloader.
49+
*
50+
* Recreates CakePHP's plugin path map, based on composer information
51+
* and available app plugins.
52+
*
53+
* @param \Composer\Script\Event $event Composer's event object.
54+
* @return void
55+
*/
56+
public function postAutoloadDump(Event $event)
57+
{
58+
$composer = $event->getComposer();
59+
$config = $composer->getConfig();
60+
61+
$vendorDir = realpath($config->get('vendor-dir'));
62+
63+
$packages = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
64+
$extra = $event->getComposer()->getPackage()->getExtra();
65+
if (empty($extra['plugin-paths'])) {
66+
$pluginDirs = [dirname($vendorDir) . DIRECTORY_SEPARATOR . 'plugins'];
67+
} else {
68+
$pluginDirs = $extra['plugin-paths'];
69+
}
70+
71+
$plugins = $this->findPlugins($packages, $pluginDirs, $vendorDir);
72+
73+
$configFile = $this->getConfigFilePath($vendorDir);
74+
$this->writeConfigFile($configFile, $plugins);
75+
}
76+
77+
/**
78+
* Find all available plugins.
79+
*
80+
* Add all composer packages of type `cakephp-plugin`, and all plugins located
81+
* in the plugins directory to a plugin-name indexed array of paths.
82+
*
83+
* @param \Composer\Package\PackageInterface[] $packages Array of \Composer\Package\PackageInterface objects.
84+
* @param array $pluginDirs The path to the plugins dir.
85+
* @param string $vendorDir The path to the vendor dir.
86+
* @return array Plugin name indexed paths to plugins.
87+
*/
88+
public function findPlugins(
89+
array $packages,
90+
array $pluginDirs = ['plugins'],
91+
string $vendorDir = 'vendor'
92+
): array {
93+
$plugins = [];
94+
95+
foreach ($packages as $package) {
96+
if ($package->getType() !== 'cakephp-plugin') {
97+
continue;
98+
}
99+
100+
$ns = $this->getPrimaryNamespace($package);
101+
$path = $vendorDir . DIRECTORY_SEPARATOR . $package->getPrettyName();
102+
$plugins[$ns] = $path;
103+
}
104+
105+
foreach ($pluginDirs as $path) {
106+
$path = $this->getFullPath($path, $vendorDir);
107+
if (is_dir($path)) {
108+
$dir = new \DirectoryIterator($path);
109+
foreach ($dir as $info) {
110+
if (!$info->isDir() || $info->isDot()) {
111+
continue;
112+
}
113+
114+
$name = $info->getFilename();
115+
if ($name[0] === '.') {
116+
continue;
117+
}
118+
119+
$plugins[$name] = $path . DIRECTORY_SEPARATOR . $name;
120+
}
121+
}
122+
}
123+
124+
ksort($plugins);
125+
126+
return $plugins;
127+
}
128+
129+
/**
130+
* Turns relative paths in full paths.
131+
*
132+
* @param string $path Path.
133+
* @param string $vendorDir The path to the vendor dir.
134+
* @return string
135+
*/
136+
public function getFullPath(string $path, string $vendorDir): string
137+
{
138+
if (preg_match('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) {
139+
return rtrim($path, '/');
140+
}
141+
142+
if (substr($path, 0, 2) === './') {
143+
$path = substr($path, 2);
144+
}
145+
146+
return rtrim(dirname($vendorDir) . DIRECTORY_SEPARATOR . $path);
147+
}
148+
149+
/**
150+
* Rewrite the config file with a complete list of plugins.
151+
*
152+
* @param string $configFile The path to the config file.
153+
* @param array $plugins Array of plugins.
154+
* @param string|null $root The root directory. Defaults to a value generated from `$configFile`.
155+
* @return void
156+
*/
157+
public function writeConfigFile(string $configFile, array $plugins, ?string $root = null): void
158+
{
159+
$root = $root ?: dirname(dirname($configFile));
160+
161+
$data = '';
162+
foreach ($plugins as $name => $pluginPath) {
163+
// Normalize to *nix paths.
164+
$pluginPath = str_replace('\\', '/', $pluginPath);
165+
$pluginPath .= '/';
166+
167+
$pluginPath = str_replace(
168+
DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR,
169+
DIRECTORY_SEPARATOR,
170+
$pluginPath
171+
);
172+
173+
// Namespaced plugins should use /
174+
$name = str_replace('\\', '/', $name);
175+
176+
$data .= sprintf(" '%s' => '%s',\n", $name, $pluginPath);
177+
}
178+
179+
$contents = <<<'PHP'
180+
<?php
181+
$baseDir = dirname(dirname(__file__));
182+
183+
return [
184+
'plugins' => [
185+
%s ],
186+
];
187+
188+
PHP;
189+
$contents = sprintf($contents, $data);
190+
191+
// Gross hacks to work around composer smashing `__FILE__` in this
192+
// PHP file when it runs the code through eval()
193+
$uppercase = function ($matches) {
194+
return strtoupper($matches[0]);
195+
};
196+
$contents = preg_replace_callback('/__file__/', $uppercase, $contents);
197+
198+
$root = str_replace(
199+
DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR,
200+
DIRECTORY_SEPARATOR,
201+
$root
202+
);
203+
204+
// Normalize to *nix paths.
205+
$root = str_replace('\\', '/', $root);
206+
$contents = str_replace('\'' . $root, '$baseDir . \'', $contents);
207+
208+
file_put_contents($configFile, $contents);
209+
}
210+
211+
/**
212+
* Path to the plugin config file.
213+
*
214+
* @param string $vendorDir Path to composer-vendor dir.
215+
* @return string Absolute file path.
216+
*/
217+
public function getConfigFilePath(string $vendorDir): string
218+
{
219+
return $vendorDir . DIRECTORY_SEPARATOR . 'cakephp-plugins.php';
220+
}
221+
222+
/**
223+
* Get the primary namespace for a plugin package.
224+
*
225+
* @param \Composer\Package\PackageInterface $package Composer's package object.
226+
* @return string The package's primary namespace.
227+
* @throws \RuntimeException When the package's primary namespace cannot be determined.
228+
*/
229+
public function getPrimaryNamespace(PackageInterface $package): string
230+
{
231+
$primaryNs = null;
232+
$autoLoad = $package->getAutoload();
233+
foreach ($autoLoad as $type => $pathMap) {
234+
if ($type !== 'psr-4') {
235+
continue;
236+
}
237+
$count = count($pathMap);
238+
239+
if ($count === 1) {
240+
$primaryNs = key($pathMap);
241+
break;
242+
}
243+
244+
$matches = preg_grep('#^(\./)?src/?$#', $pathMap);
245+
if ($matches) {
246+
$primaryNs = key($matches);
247+
break;
248+
}
249+
250+
foreach (['', '.'] as $path) {
251+
$key = array_search($path, $pathMap, true);
252+
if ($key !== false) {
253+
$primaryNs = $key;
254+
}
255+
}
256+
break;
257+
}
258+
259+
if (!$primaryNs) {
260+
throw new RuntimeException(
261+
sprintf(
262+
"Unable to get primary namespace for package %s." .
263+
"\nEnsure you have added proper 'autoload' section to your plugin's config" .
264+
" as stated in README on https://github.com/cakephp/plugin-installer",
265+
$package->getName()
266+
)
267+
);
268+
}
269+
270+
return trim($primaryNs, '\\');
271+
}
272+
}

0 commit comments

Comments
 (0)