Skip to content

Commit

Permalink
Add files
Browse files Browse the repository at this point in the history
  • Loading branch information
christianlerch committed Nov 29, 2023
1 parent 43089d0 commit 0d1dcc0
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.idea/
/vendor/
/composer.lock
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Composer Package Development Toolset
A Composer plugin that enables you to develop your Composer packages right inside your project without altering its composer.json/composer.lock.
This works by symlinking the development packages into the vendor directory, replacing existing installations.

## Installation
Add the package to your dev dependencies:
```shell
composer require --dev aldidigitalservices/composer-package-development-toolset
```
When prompted to allow this plugin confirm with `y`.

## Usage
### Development Package Location
The development packages are automatically registered by scanning the `dev-packages` directory.
Its default location is in your project's root directory, ensuring your packages are available in your Docker container and adding code completion for project code in the development packages.
However, the location can be changed by adding the following to your composer.json:

```json
"extra": {
"composer-package-development-toolset": {
"package-dir": "dev-packages"
}
}
```

### Workflow
As your composer.json and composer.lock won't be altered, Composer will remove the development package's symlinks on certain actions to match the content of these files.
This plugin hooks into these actions and restores the symlinks afterwards, ensuring a seamless experience.
31 changes: 31 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "aldidigitalservices/composer-package-development-toolset",
"description": "Develop Composer packages from inside your projects",
"keywords": ["dev"],
"type": "composer-plugin",
"license": "MIT",
"authors": [
{
"name": "Christian Lerch",
"email": "christian.lerch@aldi-sued.com"
}
],
"require": {
"php": "^8.2",
"composer-plugin-api": "^2.3"
},
"require-dev": {
"composer/composer": "^2.5"
},
"autoload": {
"psr-4": {
"ALDIDigitalServices\\ComposerPackageDevelopmentToolset\\": "src/"
}
},
"extra": {
"class": "ALDIDigitalServices\\ComposerPackageDevelopmentToolset\\Plugin"
},
"config": {
"sort-packages": true
}
}
26 changes: 26 additions & 0 deletions src/Filesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace ALDIDigitalServices\ComposerPackageDevelopmentToolset;

use Exception;

class Filesystem
{
public function getContents(string $path): string
{
$contents = file_get_contents($path);

return $contents === false
? throw new Exception("Could not read '$path'")
: $contents;
}

public function setContents(string $path, string $contents): void
{
if (file_put_contents($path, $contents) === false) {
throw new Exception("Could not write '$contents'");
}
}
}
166 changes: 166 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

namespace ALDIDigitalServices\ComposerPackageDevelopmentToolset;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\ScriptEvents;
use Exception;

class Plugin implements PluginInterface, EventSubscriberInterface
{
private readonly Composer $composer;

private readonly IOInterface $io;

private readonly string $workingDirectory;

private readonly Filesystem $filesystem;

private ?array $packagePathsByNameCache = null;

public function activate(Composer $composer, IOInterface $io): void
{
$this->composer = $composer;
$this->io = $io;
$this->workingDirectory = getcwd();
$this->filesystem = new Filesystem();
}

public function deactivate(Composer $composer, IOInterface $io): void
{
}

public function uninstall(Composer $composer, IOInterface $io): void
{
}

public static function getSubscribedEvents(): array
{
return [
ScriptEvents::PRE_INSTALL_CMD => 'removePackageVendorDirectories',
ScriptEvents::PRE_UPDATE_CMD => 'removePackageVendorDirectories',
ScriptEvents::POST_INSTALL_CMD => 'installLocalPackages',
ScriptEvents::POST_UPDATE_CMD => 'installLocalPackages',
];
}

public function removePackageVendorDirectories(): void
{
$packagePathsByName = $this->getPackagePathsByName();
$vendorPath = $this->composer->getConfig()->get('vendor-dir');

foreach ($packagePathsByName as $packageName => $packagePath) {
$packageVendorPath = "$vendorPath/$packageName";

if (is_link($packageVendorPath)) {
unlink($packageVendorPath);
}
}
}

public function installLocalPackages(): void
{
$packagePathsByName = $this->getPackagePathsByName();

if (count($packagePathsByName) === 0) {
return;
}

$composerJsonContents = $this->filesystem->getContents("$this->workingDirectory/composer.json");
$composerLockContents = $this->filesystem->getContents("$this->workingDirectory/composer.lock");
$composerJson = json_decode($composerJsonContents, associative: false, flags: JSON_THROW_ON_ERROR);

try {
foreach ($packagePathsByName as $packageName => $packagePath) {
$this->addPackageRepository($composerJson, $packagePath);
$this->setPackageVersion($composerJson, $packageName);
}

$this->writeComposerJson($composerJson);
$this->updatePackages();
} finally {
$this->filesystem->setContents("$this->workingDirectory/composer.json", $composerJsonContents);
$this->filesystem->setContents("$this->workingDirectory/composer.lock", $composerLockContents);
}
}

private function getPackagePathsByName(): array
{
if ($this->packagePathsByNameCache === null) {
$extra = $this->composer->getPackage()->getExtra();
$packageDir = rtrim($extra['composer-package-development-toolset']['package-dir'] ?? 'dev-packages', '/');
$composerJsonPaths = glob("$this->workingDirectory/$packageDir/*/composer.json");

if ($composerJsonPaths === false) {
throw new Exception('Could not search for packages');
}

$this->packagePathsByNameCache = [];

foreach ($composerJsonPaths as $composerJsonPath) {
$composerJsonContents = $this->filesystem->getContents($composerJsonPath);
$composerJson = json_decode($composerJsonContents, associative: false, flags: JSON_THROW_ON_ERROR);
$name = $composerJson->name ?? throw new Exception("$composerJsonPath has no name attribute");
// phpcs:ignore Squiz.PHP.NonExecutableCode.Unreachable -- https://github.com/squizlabs/PHP_CodeSniffer/issues/2857
$this->packagePathsByNameCache[$name] = preg_replace('|/composer.json$|', '', $composerJsonPath);
}
}

return $this->packagePathsByNameCache;
}

private function addPackageRepository(object $composerJson, string $packagePath): void
{
array_unshift($composerJson->repositories, (object)[
'type' => 'path',
'url' => $packagePath,
'options' => [
'symlink' => true,
],
]);
}

private function setPackageVersion(object $composerJson, string $packageName): void
{
$composerJson->require->$packageName = '@dev';
}

private function writeComposerJson(object $composerJson): void
{
$composerJsonContents = json_encode(
$composerJson,
flags: JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
);

$this->filesystem->setContents("$this->workingDirectory/composer.json", $composerJsonContents);
}

private function updatePackages(): void
{
$packageNameList = implode(' ', array_keys($this->getPackagePathsByName()));

$this->io->writeError("Linking dev packages: <info>$packageNameList</info>");

$command = implode(' ', [
'composer',
'--no-plugins',
'--no-scripts',
"--working-dir=$this->workingDirectory",
'update',
'--no-audit',
$packageNameList,
]);

if ($this->composer->getLoop()->getProcessExecutor()->execute($command, $out) !== 0) {
$this->io->error(
'Could not link dev packages:' . PHP_EOL .
$this->composer->getLoop()->getProcessExecutor()->getErrorOutput(),
);
}
}
}

0 comments on commit 0d1dcc0

Please sign in to comment.