-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
43089d0
commit 0d1dcc0
Showing
6 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/.idea/ | ||
/vendor/ | ||
/composer.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'"); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
); | ||
} | ||
} | ||
} |