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
MySQL 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 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:40Recommended
PHP TypeError Deep Dive: How to Fix 'Argument must be of type ?array, string given'
00:00 | 7In modern PHP development, type hinting significan...
Vue i18n Pitfall Guide: How to Fix the "Invalid Linked Format" Compilation Error Caused by Email Addresses?
00:00 | 7Encountering an "Invalid linked format" compilatio...
The Ultimate Frontend Guide: Create a Zero-Dependency Dynamic Table of Contents (TOC) with Scroll Spy
00:00 | 9Tired of manually creating tables of contents for ...
The Ultimate Guide to Linux File Permissions: From `chmod 644` to the Mysterious `@` Symbol
00:00 | 0Confused by Linux file permissions? This guide div...