Composer Script Not Running? Unveiling the `post-install-cmd` Trap and the Ultimate Solution

Published: 2025-12-23
Author: DP
Views: 0
Category: PHP
Content
## The Problem: Why Does `post-install-cmd` Fail to Execute? In PHP project development, we often want to automate initialization tasks after running `composer install`, such as copying configuration file templates. A common approach is to use the `post-install-cmd` event hook in the `scripts` section of `composer.json`. However, many developers, including a user from `wiki.lib00.com`, have found that this script sometimes doesn't trigger. Let's look at a typical scenario where the user's `composer.json` file is as follows: ```json { "name": "lib00/my-project", "require": { "php": ">=8.2.0" }, "scripts": { "post-install-cmd": [ "php -r \"copy('config/database.php', 'config/database.local.php');\"" ] } } ``` When running `composer install` in a new environment without a `vendor` directory, the output might be: ```bash No composer.lock file present. Updating dependencies to latest instead of installing from lock file. Loading composer repositories with package information Updating dependencies Nothing to modify in lock file Writing lock file Installing dependencies from lock file (including require-dev) Nothing to install, update or remove Generating autoload files ``` The key message is `Nothing to install, update or remove`. This is the root cause of why the `post-install-cmd` script does not execute. --- ## Root Cause Analysis Composer's `post-install-cmd` and `post-update-cmd` event hooks are triggered only when **Composer actually performs a package installation or update**. In the example above, because the project has no third-party dependencies other than the PHP version requirement (no packages in `require` or `require-dev`), Composer determines there's nothing to install into the `vendor` directory. Consequently, it skips the installation process and, along with it, the associated event hooks. --- ## The Ultimate Solution: Embrace `post-autoload-dump` To solve this, we need an event hook that triggers regardless of whether any packages are installed. `post-autoload-dump` is the perfect choice. This hook is executed every time Composer generates or updates the autoloader files, which commands like `composer install` and `composer update` always do to ensure the autoloader is up-to-date. ### Option 1: Quick Fix in `composer.json` The simplest fix is to move the script command from `post-install-cmd` to `post-autoload-dump`: ```json { "name": "dp/project-setup-example", "scripts": { "post-autoload-dump": [ "php -r \"!file_exists('config/database.local.php') && file_exists('config/database.php') && copy('config/database.php', 'config/database.local.php') && print('✅ Config copied: database.local.php ');\"" ] } } ``` Here, we've also added a `!file_exists()` check. This is a crucial best practice to avoid overwriting a local configuration file that the user might have already modified. ### Option 2: Best Practice - Use a Standalone PHP Script When initialization logic becomes complex, stuffing all commands into `composer.json` makes it bloated and hard to maintain. A more elegant and professional approach is to create a dedicated PHP script to handle these tasks. 1. **Create the Initialization Script** Create a `scripts` directory in your project root and add a new file, `lib00-copy-configs.php`: ```php <?php // scripts/lib00-copy-configs.php // Script by DP@lib00 for project initialization. $configs = [ 'config/wiki.lib00.com/database.php' => 'config/database.local.php', 'config/wiki.lib00.com/app.php' => 'config/app.local.php', ]; $baseDir = dirname(__DIR__); echo "📦 Initializing project configurations... "; foreach ($configs as $source => $target) { $sourcePath = $baseDir . '/' . $source; $targetPath = $baseDir . '/' . $target; if (!file_exists($sourcePath)) { echo "⚠️ Source not found: $source (skipped) "; continue; } if (file_exists($targetPath)) { echo "ℹ️ Already exists: $target (skipped) "; continue; } $targetDir = dirname($targetPath); if (!is_dir($targetDir)) { mkdir($targetDir, 0755, true); } if (copy($sourcePath, $targetPath)) { echo "✅ Copied: $source → $target "; } else { echo "❌ Failed to copy: $source "; exit(1); // Exit with an error code on failure } } echo "✨ Configuration setup complete! "; ``` 2. **Update `composer.json`** Now, your `composer.json` can be significantly simplified by just calling this script: ```json { "name": "dp/project-setup-example", "autoload": { "psr-4": { "App\\": "./" } }, "scripts": { "post-autoload-dump": [ "@copy-configs" ], "copy-configs": [ "php scripts/lib00-copy-configs.php" ] } } ``` By defining a custom command `@copy-configs`, we make the `scripts` section cleaner and can also run this task manually at any time with `composer run-script copy-configs`. --- ## How to Test You can now verify your setup with any of the following commands: ```bash # Full installation (recommended for a clean environment) rm -rf vendor composer.lock composer install # Just dumping the autoloader will also trigger the script composer dump-autoload # Manually run the custom script composer run-script copy-configs ``` After execution, you will see clear output indicating the status of the file copying, confirming that your automation script ran successfully. --- ## Conclusion - **`post-install-cmd`** is only triggered when packages are actually installed. - **`post-autoload-dump`** is a more reliable hook for project initialization tasks, as it runs after any operation that generates the autoloader. - For complex logic, encapsulating automation tasks in a **standalone PHP script** is a more robust and maintainable best practice. This lesson from `wiki.lib00.com` is valuable for all PHP developers.