eckinox/installer-plugin
Composer plugin that installs eckinox-metapackage packages by replicating a package’s replicate/ directory into your project, merging folders and overwriting same-named files. Supports custom handler classes to control behavior for new and existing files.
Install the Plugin
Add the plugin to your project’s composer.json under require-dev:
composer require --dev eckinox/installer-plugin
Ensure your composer.json includes the plugin in the extra section:
{
"extra": {
"installer-paths": {
"vendor/eckinox/installer-plugin={$name}"
}
}
}
Create a Metapackage
Build a package with a replicate directory containing files/configs to deploy. Example structure:
my-metapackage/
├── composer.json
├── replicate/
│ ├── .env.example
│ ├── phpunit.xml
│ └── scripts/
│ └── deploy.sh
Define the package type in composer.json:
{
"name": "my-vendor/my-metapackage",
"type": "eckinox-metapackage",
"extra": {
"class": "MyVendor\\MyMetapackage\\ReplicationHandler"
}
}
First Use Case Require the metapackage in your Laravel project:
composer require my-vendor/my-metapackage
Verify files are replicated to your project root (e.g., .env.example appears in your Laravel project).
Basic File Replication
Use the replicate directory to deploy static files (e.g., config templates, scripts):
# Install a metapackage with pre-defined files
composer require vendor/metapackage
replicate/ are copied to the project root.Handler-Driven Customization
Extend Eckinox\Composer\HandlerInterface for dynamic logic:
namespace MyVendor\MyMetapackage;
use Eckinox\Composer\HandlerInterface;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use Composer\IO\IOInterface;
class ReplicationHandler implements HandlerInterface
{
public function __construct(
private PackageInterface $package,
private Filesystem $filesystem,
private IOInterface $io
) {}
public function handleExistingFile(string $packageFilename, string $projectFilename, ?string $currentlyInstalledFilename = null)
{
// Logic for existing files (e.g., log conflicts)
$this->io->writeError("File $projectFilename already exists. Skipping.");
}
public function postFileCreationCallback(string $projectFilename)
{
// Post-processing (e.g., chmod, rename)
if (basename($projectFilename) === 'deploy.sh') {
chmod($projectFilename, 0755);
}
}
}
composer.json under "extra": { "class": "MyVendor\\MyMetapackage\\ReplicationHandler" }.Version-Aware Updates
Leverage $currentlyInstalledFilename to detect changes between versions:
public function handleExistingFile(string $packageFilename, string $projectFilename, ?string $currentlyInstalledFilename = null)
{
if ($currentlyInstalledFilename && md5_file($currentlyInstalledFilename) !== md5_file($packageFilename)) {
$this->io->write("Updated $projectFilename from version " . $this->package->getVersion());
}
}
Laravel Integration
Combine with Laravel’s composer.json scripts for post-install actions:
{
"scripts": {
"post-install-cmd": [
"@php artisan config:clear",
"@php artisan cache:clear"
]
}
}
Project Scaffolding
company-template-metapackagereplicate/ contains:
.env.examplephpunit.xmlwebpack.mix.jscomposer require company/template-metapackage to bootstrap.Environment-Specific Configs
env-config-metapackagereplicate/ includes:
docker-compose.ci.ymldocker-compose.local.yml.env already exists (avoid overwrites).Internal Tooling
dev-tools-metapackagereplicate/ contains:
artisan commands (e.g., php artisan generate:model)php scripts/lint.php).env, composer.json).public function postFileCreationCallback(string $projectFilename) {
if (in_array(basename($projectFilename), ['.env', 'composer.json'])) {
unlink($projectFilename);
$this->io->writeError("Skipped replication of $projectFilename to avoid conflicts.");
}
}
composer.json scripts:
{
"scripts": {
"post-update-cmd": [
"@php artisan config:publish",
"@php artisan view:clear"
]
}
}
Filesystem and IOInterface in PHPUnit:
$handler = new MyHandler($package, $mockFilesystem, $mockIO);
$handler->postFileCreationCallback('/path/to/file');
$this->assertFileExists('/path/to/file');
File Overwrite Behavior
replicate/ overwrite existing files in the project root.handleExistingFile() to log or skip conflicts:
public function handleExistingFile(string $packageFilename, string $projectFilename, ?string $currentlyInstalledFilename = null) {
$this->io->writeError("Conflict: $projectFilename exists. Use --force to overwrite.");
}
config/app.php or routes/web.php can break functionality.Permission Issues
storage/ needs 775).postFileCreationCallback:
public function postFileCreationCallback(string $projectFilename) {
if (str_contains($projectFilename, 'storage/')) {
chmod($projectFilename, 0775);
}
}
safe_mode environments).Handler Autoloading
composer.json includes:
{
"autoload": {
"psr-4": {
"MyVendor\\MyMetapackage\\": "src/"
}
}
}
composer dump-autoload after adding handlers.Path Handling Across OS
\ vs /) can break handlers.$projectFilename = str_replace('\\', '/', $projectFilename);
DIRECTORY_SEPARATOR for cross-platform compatibility.Partial Installs
composer validate --strict
Laravel Cache Invalidation
config/ files won’t trigger Laravel’s cache clearing.composer.json script:
{
"scripts": {
"post-install-cmd": [
"@php artisan config:clear"
]
}
}
Enable Composer Debug Mode
Run Composer with -vvv to debug plugin execution:
composer install -vvv
Eckinox\Composer\Plugin logs.Handler Debugging
Use IOInterface to log handler execution:
public function postFileCreationCallback(string $projectFilename) {
$this->io->write("Replicating: $projectFilename");
// Your logic here
}
Check Partial Installs If
How can I help you explore Laravel packages today?