Skip to content

Instantly share code, notes, and snippets.

@leek
Last active October 30, 2025 22:29
Show Gist options
  • Select an option

  • Save leek/48929e26b96982e5ed52ca108f04b2aa to your computer and use it in GitHub Desktop.

Select an option

Save leek/48929e26b96982e5ed52ca108f04b2aa to your computer and use it in GitHub Desktop.
Report which published Laravel config files differ from their vendor originals (and optionally delete unchanged ones).
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class ConfigDiffVendorCommand extends Command
{
protected $signature = 'config:diff-vendor
{--delete : Delete config files that are identical to their vendor version}
{--normalize : Normalize whitespace and line endings before comparing}';
protected $description = 'Report which published config files differ from their vendor originals (and optionally delete unchanged ones).';
public function handle(Filesystem $fs): int
{
$vendorConfigs = glob(base_path('vendor/*/*/config/*.php')) ?: [];
if (empty($vendorConfigs)) {
$this->info('No vendor config files found.');
return self::SUCCESS;
}
// Build a map of vendor config basenames for quick lookup
$vendorConfigBasenames = [];
foreach ($vendorConfigs as $vendorFile) {
$vendorConfigBasenames[basename($vendorFile)] = true;
}
$unchanged = [];
$modified = [];
$missing = [];
$orphaned = [];
// Check for orphaned config files (no vendor counterpart)
$localConfigs = glob(config_path('*.php')) ?: [];
foreach ($localConfigs as $localFile) {
$basename = basename($localFile);
if (! isset($vendorConfigBasenames[$basename])) {
$orphaned[] = $localFile;
}
}
foreach ($vendorConfigs as $vendorFile) {
$target = config_path(basename($vendorFile));
if (! $fs->exists($target)) {
$missing[] = $target;
continue;
}
$v = $fs->get($vendorFile);
$t = $fs->get($target);
if ($this->option('normalize')) {
$v = $this->normalize($v);
$t = $this->normalize($t);
}
if (hash('sha256', $v) === hash('sha256', $t)) {
$unchanged[] = $target;
} else {
try {
/** @var array $vendorArr */
$vendorArr = include $vendorFile;
/** @var array $targetArr */
$targetArr = include $target;
// Sort recursively to ignore ordering differences
$vendorArr = \Illuminate\Support\Arr::sortRecursive($vendorArr);
$targetArr = \Illuminate\Support\Arr::sortRecursive($targetArr);
if ($vendorArr === $targetArr) {
$unchanged[] = $target;
} else {
$diffPercentage = $this->calculateDiffPercentage($v, $t);
$modified[] = ['path' => $target, 'diff' => $diffPercentage];
}
} catch (\Throwable $e) {
// Fallback to byte comparison if include fails
$diffPercentage = $this->calculateDiffPercentage($v, $t);
$modified[] = ['path' => $target, 'diff' => $diffPercentage];
}
}
}
// Output
if ($modified) {
// Sort by difference percentage (most changed first)
usort($modified, fn ($a, $b) => $b['diff'] <=> $a['diff']);
$this->line(PHP_EOL . '<info>MODIFIED</info>');
foreach ($modified as $item) {
$diffColor = $this->getDiffColor($item['diff']);
$this->line(" • {$item['path']} <{$diffColor}>[{$item['diff']}% different]</>");
}
}
if ($unchanged) {
$this->line(PHP_EOL . '<comment>UNCHANGED (matches vendor)</comment>');
foreach ($unchanged as $f) {
$this->line(" • {$f}");
}
}
if ($orphaned) {
$this->line(PHP_EOL . '<fg=red>ORPHANED (no vendor counterpart - likely from uninstalled packages)</>');
foreach ($orphaned as $f) {
$this->line(" • {$f}");
}
}
if ($missing) {
$this->line(PHP_EOL . '<fg=gray>MISSING (not published locally)</>');
foreach ($missing as $f) {
$this->line(" • {$f}");
}
}
// Optional delete
if ($this->option('delete') && $unchanged) {
if ($this->confirm('Delete ' . count($unchanged) . ' unchanged config file(s)?')) {
foreach ($unchanged as $f) {
$fs->delete($f);
$this->line(" ✖ deleted {$f}");
}
}
}
$this->line(PHP_EOL . 'Done.');
return self::SUCCESS;
}
private function normalize(string $s): string
{
// Strip php open tags, comments, normalize whitespace + line endings
$s = preg_replace('/^\s*<\?php\s*/', '', $s);
$s = preg_replace('!/\*.*?\*/!s', '', $s); // block comments
$s = preg_replace('![ \t]*//.*$!m', '', $s); // line comments
$s = str_replace(["\r\n", "\r"], "\n", $s);
$s = preg_replace("/[ \t]+/m", ' ', $s);
// convert any 2+ spaces to single space
$s = preg_replace('/[ ]{2,}/', ' ', $s);
$s = trim($s);
return $s;
}
private function calculateDiffPercentage(string $vendor, string $local): float
{
if ($vendor === $local) {
return 0.0;
}
if (strlen($vendor) === 0 && strlen($local) === 0) {
return 0.0;
}
// Calculate similarity percentage using similar_text
$similarity = 0;
similar_text($vendor, $local, $similarity);
// Return difference percentage (inverse of similarity)
return round(100 - $similarity, 1);
}
private function getDiffColor(float $percentage): string
{
return match (true) {
$percentage < 5 => 'fg=green', // Very minor changes
$percentage < 15 => 'fg=yellow', // Small changes
$percentage < 30 => 'fg=magenta', // Moderate changes
default => 'fg=red', // Significant changes
};
}
}
@leek
Copy link
Author

leek commented Oct 24, 2025

CleanShot 2025-10-24 at 17 39 41@2x-optimised

@leek
Copy link
Author

leek commented Oct 30, 2025

Created this as a package which also works for migrations, views, language files, and configs:
https://github.com/leek/laravel-vendor-cleanup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment