Composer Script Not Running? Unveiling the `post-install-cmd` Trap and the Ultimate Solution
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.
Related Contents
Resolving PHP "could not find driver" Error: Ultimate Guide to Missing PDO Database Drivers
Duration: 00:00 | DP | 2026-07-04 08:03:00VS Code PHP Guide: How to Trace Function Definitions Like PHPStorm
Duration: 00:00 | DP | 2026-07-04 20:27:00Resolving Nginx Permission Denied (13) Errors for WebP Images Generated by PHP Imagick
Duration: 00:00 | DP | 2026-07-05 21:17:00Fixing Nginx 500 Error: Internal Redirection Cycle (SPA vs PHP Config)
Duration: 00:00 | DP | 2026-07-02 21:45:50Fixing Yii2 Upgrade Errors: Bootstrap Namespace Replacement and Installing Legacy Projects in PHP 8.4 via Composer
Duration: 00:00 | DP | 2026-07-24 21:20:41Stop Making Timezone Mistakes in PHP: The Ultimate Guide to time() and UTC
Duration: 00:00 | DP | 2026-06-25 11:29:00Beyond 99.9%: A Deep Dive into a User-Centric Weighted Sampling Algorithm for Availability
Duration: 00:00 | DP | 2026-06-26 12:57:00PHP Log Aggregation Performance Tuning: Database vs. Application Layer - The Ultimate Showdown for Millions of Records
Duration: 00:00 | DP | 2026-01-06 08:05:09MySQL TIMESTAMP vs. DATETIME: The Ultimate Showdown on Time Zones, UTC, and Storage
Duration: 00:00 | DP | 2025-12-02 08:31:40The Ultimate 'Connection Refused' Guide: A PHP PDO & Docker Debugging Saga of a Forgotten Port
Duration: 00:00 | DP | 2025-12-03 09:03:20Nginx vs. Vite: The Smart Way to Handle Asset Path Prefixes in SPAs
Duration: 00:00 | DP | 2025-12-11 13:16:40The Ultimate PHP Guide: How to Correctly Handle and Store Markdown Line Breaks from a Textarea
Duration: 00:00 | DP | 2025-11-20 08:08:00Stop Mixing Code and User Uploads! The Ultimate Guide to a Secure and Scalable PHP MVC Project Structure
Duration: 00:00 | DP | 2026-01-13 08:14:11Mastering PHP: How to Elegantly Filter an Array by Keys Using Values from Another Array
Duration: 00:00 | DP | 2026-01-14 08:15:29Stop Manual Debugging: A Practical Guide to Automated Testing in PHP MVC & CRUD Applications
Duration: 00:00 | DP | 2025-11-16 16:32:33Mastering PHP Switch: How to Handle Multiple Conditions for a Single Case
Duration: 00:00 | DP | 2025-11-17 09:35:40`self::` vs. `static::` in PHP: A Deep Dive into Late Static Binding
Duration: 00:00 | DP | 2025-11-18 02:38:48PHP String Magic: Why `{static::$table}` Fails and 3 Ways to Fix It (Plus Security Tips)
Duration: 00:00 | DP | 2025-11-18 11:10:21Recommended
How to Fix `e.target.closest is not a function` Error with JavaScript mouseleave Events
00:00 | 46Encountered the "Uncaught TypeError: e.target.clos...
The Hidden Pitfall in PHP Transaction Rollbacks: Why Your Catch Block Might Crash
00:00 | 4Discover a common programming mistake in PHP `try....
Solving MySQL's "Cannot TRUNCATE" Error with Foreign Key Constraints
00:00 | 124Encountering "Cannot truncate a table referenced i...
From Zero to Platform: Build Your Own GitHub-Level Login System with NextAuth and Casdoor
00:00 | 121Many developers are puzzled by the complexity of m...