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
PHP 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:21Can SHA256 Be "Decrypted"? A Deep Dive into Hash Function Determinism and One-Way Properties
Duration: 00:00 | DP | 2025-11-19 04:13:29The Magic of PHP Enums: Elegantly Convert an Enum to a Key-Value Array with One Line of Code
Duration: 00:00 | DP | 2025-12-16 03:39:10Files Mysteriously Missing in PHPStorm? Check Your Project View First!
Duration: 00:00 | DP | 2026-01-15 08:16:46One-Click Code Cleanup: The Ultimate Guide to PhpStorm's Reformat Code Shortcut
Duration: 00:00 | DP | 2026-02-03 09:34:00Upgrading to PHP 8.4? How to Fix the `session.sid_length` Deprecation Warning
Duration: 00:00 | DP | 2025-11-20 22:51:17Streamline Your Yii2 Console: How to Hide Core Commands and Display Only Your Own
Duration: 00:00 | DP | 2025-12-17 16:26:40From Guzzle to Native cURL: A Masterclass in Refactoring a PHP Translator Component
Duration: 00:00 | DP | 2025-11-21 07:22:51Recommended
Are Your PHP Prefixes Truly Unique? A Deep Dive into Collision Probability from `mt_rand` to `random_bytes`
00:00 | 32Generating unique identifiers in PHP is a common t...
Say Goodbye to Clutter: Master Sublime Text Code Folding with These Essential Shortcuts
00:00 | 28When working with large code files, code folding i...
Solving MySQL's "Cannot TRUNCATE" Error with Foreign Key Constraints
00:00 | 8Encountering "Cannot truncate a table referenced i...
Unlocking the MySQL Self-Referencing FK Trap: Why Does ON UPDATE CASCADE Fail?
00:00 | 16Encountering Error 1451 when batch updating a tabl...