Last active
October 30, 2025 22:29
-
-
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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 | |
| }; | |
| } | |
| } |
Author
leek
commented
Oct 24, 2025
Author
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