diff --git a/README.md b/README.md
index f557de2..20c48a4 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,25 @@ For personal configs `link` must be defined
}
```
+### 2. Refresh symlinks
+
+Symlinks are created automatically on `composer install`/`update`, but you can
+trigger the process manually with the built-in command:
+
+```bash
+$ composer symlinks:refresh
+```
+
+Add the `--dry-run` flag to preview the operations without touching the
+filesystem:
+
+```bash
+$ composer symlinks:refresh --dry-run
+```
+
+The legacy environment variable `SYMLINKS_DRY_RUN=1` is still honoured during
+Composer hooks for backwards compatibility.
+
### 3. Execute composer
DO NOT use --no-plugins for composer install or update
diff --git a/src/Symlinks/Command/CommandProvider.php b/src/Symlinks/Command/CommandProvider.php
new file mode 100644
index 0000000..028f931
--- /dev/null
+++ b/src/Symlinks/Command/CommandProvider.php
@@ -0,0 +1,16 @@
+setName('symlinks:refresh')
+ ->setDescription('Create symlinks defined in extra.somework/composer-symlinks.')
+ ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show the operations without creating links.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $composer = $this->requireComposer();
+ $io = $this->getIO();
+
+ $dryRun = (bool) $input->getOption('dry-run');
+
+ $event = new Event('symlinks:refresh', $composer, $io);
+ $filesystem = new Filesystem();
+ $factory = new SymlinksFactory($event, $filesystem);
+ $processor = new SymlinksProcessor($filesystem, $dryRun);
+
+ $this->runSymlinks($factory, $processor, $io, $dryRun);
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Symlinks/Plugin.php b/src/Symlinks/Plugin.php
index 1e5a21e..1c99f71 100644
--- a/src/Symlinks/Plugin.php
+++ b/src/Symlinks/Plugin.php
@@ -5,13 +5,17 @@
use Composer\Composer;
use Composer\IO\IOInterface;
+use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
+use Composer\Plugin\Capable;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;
-class Plugin implements PluginInterface
+class Plugin implements PluginInterface, Capable
{
+ use SymlinksExecutionTrait;
+
public function activate(Composer $composer, IOInterface $io): void
{
$eventDispatcher = $composer->getEventDispatcher();
@@ -19,6 +23,13 @@ public function activate(Composer $composer, IOInterface $io): void
$eventDispatcher->addListener(ScriptEvents::POST_UPDATE_CMD, $this->createLinks());
}
+ public function getCapabilities(): array
+ {
+ return [
+ CommandProviderCapability::class => Command\CommandProvider::class,
+ ];
+ }
+
public function deactivate(Composer $composer, IOInterface $io): void
{
}
@@ -35,40 +46,7 @@ protected function createLinks(): callable
$dryRun = getenv('SYMLINKS_DRY_RUN') === '1' || getenv('SYMLINKS_DRY_RUN') === 'true';
$processor = new SymlinksProcessor($fileSystem, $dryRun);
- $symlinks = $factory->process();
- foreach ($symlinks as $symlink) {
- try {
- if (!$processor->processSymlink($symlink)) {
- throw new RuntimeException('Unknown error');
- }
- $event
- ->getIO()
- ->write(sprintf(
- ' %sSymlinking %s to %s',
- $dryRun ? '[DRY RUN] ' : '',
- $symlink->getLink(),
- $symlink->getTarget()
- ));
- } catch (LinkDirectoryError $exception) {
- $event
- ->getIO()
- ->write(sprintf(
- ' Symlinking %s to %s - %s',
- $symlink->getLink(),
- $symlink->getTarget(),
- 'Skipped'
- ));
- } catch (\Exception $exception) {
- $event
- ->getIO()
- ->writeError(sprintf(
- ' Symlinking %s to %s - %s',
- $symlink->getLink(),
- $symlink->getTarget(),
- $exception->getMessage()
- ));
- }
- }
+ $this->runSymlinks($factory, $processor, $event->getIO(), $dryRun);
};
}
}
diff --git a/src/Symlinks/SymlinksExecutionTrait.php b/src/Symlinks/SymlinksExecutionTrait.php
new file mode 100644
index 0000000..ad3c75e
--- /dev/null
+++ b/src/Symlinks/SymlinksExecutionTrait.php
@@ -0,0 +1,46 @@
+process();
+ foreach ($symlinks as $symlink) {
+ try {
+ if (!$processor->processSymlink($symlink)) {
+ throw new RuntimeException('Unknown error');
+ }
+
+ $io->write(sprintf(
+ ' %sSymlinking %s to %s',
+ $dryRun ? '[DRY RUN] ' : '',
+ $symlink->getLink(),
+ $symlink->getTarget()
+ ));
+ } catch (LinkDirectoryError $exception) {
+ $io->write(sprintf(
+ ' Symlinking %s to %s - %s',
+ $symlink->getLink(),
+ $symlink->getTarget(),
+ 'Skipped'
+ ));
+ } catch (\Exception $exception) {
+ $io->writeError(sprintf(
+ ' Symlinking %s to %s - %s',
+ $symlink->getLink(),
+ $symlink->getTarget(),
+ $exception->getMessage()
+ ));
+ }
+ }
+ }
+}
diff --git a/tests/PluginTest.php b/tests/PluginTest.php
index 650fac8..fe72a96 100644
--- a/tests/PluginTest.php
+++ b/tests/PluginTest.php
@@ -5,9 +5,11 @@
use Composer\Composer;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\NullIO;
+use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
use Composer\Script\ScriptEvents;
use PHPUnit\Framework\TestCase;
use SomeWork\Symlinks\Plugin;
+use SomeWork\Symlinks\Command\RefreshCommand;
class PluginTest extends TestCase
{
@@ -35,4 +37,24 @@ public function addListener(string $eventName, $listener, int $priority = 0): vo
$this->assertArrayHasKey(ScriptEvents::POST_UPDATE_CMD, $dispatcher->recorded);
$this->assertIsCallable($dispatcher->recorded[ScriptEvents::POST_INSTALL_CMD][0]);
}
+
+ public function testGetCapabilitiesRegistersCommand(): void
+ {
+ $plugin = new Plugin();
+
+ $capabilities = $plugin->getCapabilities();
+
+ $this->assertArrayHasKey(CommandProviderCapability::class, $capabilities);
+ $this->assertSame(
+ \SomeWork\Symlinks\Command\CommandProvider::class,
+ $capabilities[CommandProviderCapability::class]
+ );
+
+ $providerClass = $capabilities[CommandProviderCapability::class];
+ $provider = new $providerClass();
+ $commands = $provider->getCommands();
+
+ $this->assertNotEmpty($commands);
+ $this->assertInstanceOf(RefreshCommand::class, $commands[0]);
+ }
}
diff --git a/tests/RefreshCommandTest.php b/tests/RefreshCommandTest.php
new file mode 100644
index 0000000..e3f7cb5
--- /dev/null
+++ b/tests/RefreshCommandTest.php
@@ -0,0 +1,101 @@
+createComposer([
+ 'somework/composer-symlinks' => [
+ 'symlinks' => [
+ 'source/file.txt' => 'link.txt',
+ ],
+ ],
+ ]);
+
+ $this->runCommand($composer);
+
+ $this->assertTrue(is_link($tmp . '/link.txt'));
+ $this->assertSame(
+ realpath($tmp . '/source/file.txt'),
+ realpath(dirname($tmp . '/link.txt') . '/' . readlink($tmp . '/link.txt'))
+ );
+
+ chdir($cwd);
+ }
+
+ public function testDryRunOptionDoesNotCreateLinks(): void
+ {
+ $tmp = sys_get_temp_dir() . '/command_' . uniqid();
+ mkdir($tmp);
+ mkdir($tmp . '/source');
+ file_put_contents($tmp . '/source/file.txt', 'data');
+
+ $cwd = getcwd();
+ chdir($tmp);
+
+ $composer = $this->createComposer([
+ 'somework/composer-symlinks' => [
+ 'symlinks' => [
+ 'source/file.txt' => 'link.txt',
+ ],
+ ],
+ ]);
+
+ $this->runCommand($composer, ['--dry-run' => true]);
+
+ $this->assertFalse(file_exists($tmp . '/link.txt'));
+
+ chdir($cwd);
+ }
+
+ private function createComposer(array $extra): Composer
+ {
+ $composer = new Composer();
+ $io = new NullIO();
+ $dispatcher = new EventDispatcher($composer, $io);
+ $composer->setEventDispatcher($dispatcher);
+
+ $package = new RootPackage('test/test', '1.0.0', '1.0.0');
+ $package->setExtra($extra);
+ $composer->setPackage($package);
+
+ $composer->setConfig(new \Composer\Config());
+
+ return $composer;
+ }
+
+ private function runCommand(Composer $composer, array $input = []): void
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $io = new NullIO();
+
+ $command = new RefreshCommand();
+ $command->setComposer($composer);
+ $command->setIO($io);
+ $application->add($command);
+
+ $tester = new CommandTester($application->find('symlinks:refresh'));
+ $tester->execute(array_merge(['command' => 'symlinks:refresh'], $input));
+ }
+}
+