Skip to content

Commit d577961

Browse files
authored
Merge pull request #2464 from elcreator/fix-uninstall-providers
fix(core): fatal error about missing provider after package uninstall
2 parents 2c8d73b + 351d1b0 commit d577961

3 files changed

Lines changed: 139 additions & 1 deletion

File tree

‎core/src/AbstractLaravel.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,14 @@ public function resolveProvider($provider)
368368
public function registerConfiguredProviders()
369369
{
370370
$providers = Collection::make($this['config']->get('app.providers'))
371+
->filter(function ($provider) {
372+
if (!is_string($provider) || class_exists($provider)) {
373+
return true;
374+
}
375+
// Stale custom/config/app/providers/*.php left behind by a removed package
376+
error_log('[EvolutionCMS] Skipped missing service provider "' . $provider . '"');
377+
return false;
378+
})
371379
->partition(function ($provider) {
372380
return Str::startsWith($provider, 'Illuminate\\');
373381
});

‎core/src/Console/Packages/PackageCommand.php‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ class PackageCommand extends Command
5959
* @var string
6060
*/
6161
protected $aliasesDir = EVO_CORE_PATH . 'custom/config/app/aliases/';
62+
/**
63+
* Track provider config files generated during current discovery run.
64+
* @var array<string,bool>
65+
*/
66+
protected $discoveredProviderFiles = [];
67+
6268
/**
6369
* Track aliases generated during current discovery run.
6470
* @var array<string,bool>
@@ -154,9 +160,13 @@ public function handle()
154160
if (file_exists($this->composer)) {
155161
$this->parseComposer($this->composer);
156162
}
163+
$this->cleanupProviders();
157164
$this->cleanupAliases();
158165

159-
unlink(EVO_CORE_PATH . 'storage/bootstrap/services.php');
166+
$servicesCache = EVO_CORE_PATH . 'storage/bootstrap/services.php';
167+
if (file_exists($servicesCache)) {
168+
unlink($servicesCache);
169+
}
160170
}
161171

162172
/**
@@ -280,6 +290,8 @@ protected function process(string $value, int $priority = 0)
280290
$fileContent = "<?php \nreturn " . $value . "::class;";
281291
}
282292

293+
$this->discoveredProviderFiles[$fileName] = true;
294+
283295
if (file_put_contents($this->configDir . $fileName, $fileContent)) {
284296
$this->getOutput()->write('<info>' . $value . ($priority > 0 ? " (priority: {$priority})" : '') . '</info>');
285297
} else {
@@ -314,6 +326,35 @@ protected function processAlias(string $alias, string $class): void
314326
@file_put_contents($this->aliasesDir . $fileName, $content);
315327
}
316328

329+
/**
330+
* Remove provider config files whose class no longer exists.
331+
*
332+
* Keeps the app bootable after a package is removed via composer
333+
* while its generated provider file is still in place.
334+
*/
335+
protected function cleanupProviders(): void
336+
{
337+
foreach (glob($this->configDir . '*.php') ?: [] as $file) {
338+
if (isset($this->discoveredProviderFiles[basename($file)])) {
339+
continue;
340+
}
341+
342+
try {
343+
$class = include $file;
344+
} catch (\Throwable $exception) {
345+
continue;
346+
}
347+
348+
if (!is_string($class) || $class === '' || class_exists($class)) {
349+
continue;
350+
}
351+
352+
@unlink($file);
353+
$this->getOutput()->write('<comment>Removed stale provider config ' . basename($file) . ' (' . $class . ' not found)</comment>');
354+
$this->line('');
355+
}
356+
}
357+
317358
/**
318359
* Remove auto-generated alias files that are no longer discovered.
319360
*
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
use EvolutionCMS\Console\Packages\PackageCommand;
4+
use Illuminate\Console\OutputStyle;
5+
use Symfony\Component\Console\Input\ArrayInput;
6+
use Symfony\Component\Console\Output\BufferedOutput;
7+
8+
if (!defined('EVO_CORE_PATH')) {
9+
define('EVO_CORE_PATH', dirname(__DIR__, 3) . '/');
10+
}
11+
12+
function buildStaleProviderCommand(string $configDir): array
13+
{
14+
$command = (new ReflectionClass(PackageCommand::class))->newInstanceWithoutConstructor();
15+
16+
$configProperty = new ReflectionProperty(PackageCommand::class, 'configDir');
17+
$configProperty->setAccessible(true);
18+
$configProperty->setValue($command, $configDir);
19+
20+
$buffer = new BufferedOutput();
21+
$outputProperty = new ReflectionProperty(PackageCommand::class, 'output');
22+
$outputProperty->setAccessible(true);
23+
$outputProperty->setValue($command, new OutputStyle(new ArrayInput([]), $buffer));
24+
25+
return [$command, $buffer];
26+
}
27+
28+
function removeStaleProviderDir(string $dir): void
29+
{
30+
foreach (glob($dir . '*.php') ?: [] as $file) {
31+
unlink($file);
32+
}
33+
if (is_dir($dir)) {
34+
rmdir($dir);
35+
}
36+
}
37+
38+
beforeEach(function () {
39+
$this->providersDir = sys_get_temp_dir() . '/evo-stale-providers-' . uniqid() . '/';
40+
mkdir($this->providersDir, 0775, true);
41+
});
42+
43+
afterEach(function () {
44+
removeStaleProviderDir($this->providersDir);
45+
});
46+
47+
test('package discovery removes provider config files whose class no longer exists', function () {
48+
file_put_contents($this->providersDir . 'sCommerceServiceProvider.php', "<?php \nreturn Seiger\sCommerce\sCommerceServiceProvider::class;");
49+
file_put_contents($this->providersDir . '001_MissingServiceProvider.php', "<?php\n// Priority 1\nreturn Vendor\Missing\MissingServiceProvider::class;");
50+
file_put_contents($this->providersDir . 'Evolution_Auth.php', "<?php \nreturn EvolutionCMS\Providers\AuthServiceProvider::class;");
51+
file_put_contents($this->providersDir . 'Broken.php', "<?php\nreturn ['not', 'a', 'class'];");
52+
53+
[$command, $buffer] = buildStaleProviderCommand($this->providersDir);
54+
55+
$method = new ReflectionMethod(PackageCommand::class, 'cleanupProviders');
56+
$method->setAccessible(true);
57+
$method->invoke($command);
58+
59+
expect(file_exists($this->providersDir . 'sCommerceServiceProvider.php'))->toBeFalse()
60+
->and(file_exists($this->providersDir . '001_MissingServiceProvider.php'))->toBeFalse()
61+
->and(file_exists($this->providersDir . 'Evolution_Auth.php'))->toBeTrue()
62+
->and(file_exists($this->providersDir . 'Broken.php'))->toBeTrue()
63+
->and($buffer->fetch())->toContain('Removed stale provider config sCommerceServiceProvider.php');
64+
});
65+
66+
test('package discovery keeps provider files generated during the current run', function () {
67+
file_put_contents($this->providersDir . 'FreshServiceProvider.php', "<?php \nreturn Vendor\Fresh\FreshServiceProvider::class;");
68+
69+
[$command] = buildStaleProviderCommand($this->providersDir);
70+
71+
$discovered = new ReflectionProperty(PackageCommand::class, 'discoveredProviderFiles');
72+
$discovered->setAccessible(true);
73+
$discovered->setValue($command, ['FreshServiceProvider.php' => true]);
74+
75+
$method = new ReflectionMethod(PackageCommand::class, 'cleanupProviders');
76+
$method->setAccessible(true);
77+
$method->invoke($command);
78+
79+
expect(file_exists($this->providersDir . 'FreshServiceProvider.php'))->toBeTrue();
80+
});
81+
82+
test('configured provider registration skips classes that do not exist', function () {
83+
$source = (string) file_get_contents(dirname(__DIR__, 3) . '/src/AbstractLaravel.php');
84+
85+
expect($source)
86+
->toContain('public function registerConfiguredProviders()')
87+
->toContain('class_exists($provider)')
88+
->toContain('Skipped missing service provider');
89+
});

0 commit comments

Comments
 (0)