Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/src/AbstractLaravel.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,14 @@ public function resolveProvider($provider)
public function registerConfiguredProviders()
{
$providers = Collection::make($this['config']->get('app.providers'))
->filter(function ($provider) {
if (!is_string($provider) || class_exists($provider)) {
return true;
}
// Stale custom/config/app/providers/*.php left behind by a removed package
error_log('[EvolutionCMS] Skipped missing service provider "' . $provider . '"');
return false;
})
->partition(function ($provider) {
return Str::startsWith($provider, 'Illuminate\\');
});
Expand Down
43 changes: 42 additions & 1 deletion core/src/Console/Packages/PackageCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ class PackageCommand extends Command
* @var string
*/
protected $aliasesDir = EVO_CORE_PATH . 'custom/config/app/aliases/';
/**
* Track provider config files generated during current discovery run.
* @var array<string,bool>
*/
protected $discoveredProviderFiles = [];

/**
* Track aliases generated during current discovery run.
* @var array<string,bool>
Expand Down Expand Up @@ -154,9 +160,13 @@ public function handle()
if (file_exists($this->composer)) {
$this->parseComposer($this->composer);
}
$this->cleanupProviders();
$this->cleanupAliases();

unlink(EVO_CORE_PATH . 'storage/bootstrap/services.php');
$servicesCache = EVO_CORE_PATH . 'storage/bootstrap/services.php';
if (file_exists($servicesCache)) {
unlink($servicesCache);
}
}

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

$this->discoveredProviderFiles[$fileName] = true;

if (file_put_contents($this->configDir . $fileName, $fileContent)) {
$this->getOutput()->write('<info>' . $value . ($priority > 0 ? " (priority: {$priority})" : '') . '</info>');
} else {
Expand Down Expand Up @@ -314,6 +326,35 @@ protected function processAlias(string $alias, string $class): void
@file_put_contents($this->aliasesDir . $fileName, $content);
}

/**
* Remove provider config files whose class no longer exists.
*
* Keeps the app bootable after a package is removed via composer
* while its generated provider file is still in place.
*/
protected function cleanupProviders(): void
{
foreach (glob($this->configDir . '*.php') ?: [] as $file) {
if (isset($this->discoveredProviderFiles[basename($file)])) {
continue;
}

try {
$class = include $file;
} catch (\Throwable $exception) {
continue;
}

if (!is_string($class) || $class === '' || class_exists($class)) {
continue;
}

@unlink($file);
$this->getOutput()->write('<comment>Removed stale provider config ' . basename($file) . ' (' . $class . ' not found)</comment>');
$this->line('');
}
}

/**
* Remove auto-generated alias files that are no longer discovered.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

use EvolutionCMS\Console\Packages\PackageCommand;
use Illuminate\Console\OutputStyle;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;

if (!defined('EVO_CORE_PATH')) {
define('EVO_CORE_PATH', dirname(__DIR__, 3) . '/');
}

function buildStaleProviderCommand(string $configDir): array
{
$command = (new ReflectionClass(PackageCommand::class))->newInstanceWithoutConstructor();

$configProperty = new ReflectionProperty(PackageCommand::class, 'configDir');
$configProperty->setAccessible(true);
$configProperty->setValue($command, $configDir);

$buffer = new BufferedOutput();
$outputProperty = new ReflectionProperty(PackageCommand::class, 'output');
$outputProperty->setAccessible(true);
$outputProperty->setValue($command, new OutputStyle(new ArrayInput([]), $buffer));

return [$command, $buffer];
}

function removeStaleProviderDir(string $dir): void
{
foreach (glob($dir . '*.php') ?: [] as $file) {
unlink($file);
}
if (is_dir($dir)) {
rmdir($dir);
}
}

beforeEach(function () {
$this->providersDir = sys_get_temp_dir() . '/evo-stale-providers-' . uniqid() . '/';
mkdir($this->providersDir, 0775, true);
});

afterEach(function () {
removeStaleProviderDir($this->providersDir);
});

test('package discovery removes provider config files whose class no longer exists', function () {
file_put_contents($this->providersDir . 'sCommerceServiceProvider.php', "<?php \nreturn Seiger\sCommerce\sCommerceServiceProvider::class;");
file_put_contents($this->providersDir . '001_MissingServiceProvider.php', "<?php\n// Priority 1\nreturn Vendor\Missing\MissingServiceProvider::class;");
file_put_contents($this->providersDir . 'Evolution_Auth.php', "<?php \nreturn EvolutionCMS\Providers\AuthServiceProvider::class;");
file_put_contents($this->providersDir . 'Broken.php', "<?php\nreturn ['not', 'a', 'class'];");

[$command, $buffer] = buildStaleProviderCommand($this->providersDir);

$method = new ReflectionMethod(PackageCommand::class, 'cleanupProviders');
$method->setAccessible(true);
$method->invoke($command);

expect(file_exists($this->providersDir . 'sCommerceServiceProvider.php'))->toBeFalse()
->and(file_exists($this->providersDir . '001_MissingServiceProvider.php'))->toBeFalse()
->and(file_exists($this->providersDir . 'Evolution_Auth.php'))->toBeTrue()
->and(file_exists($this->providersDir . 'Broken.php'))->toBeTrue()
->and($buffer->fetch())->toContain('Removed stale provider config sCommerceServiceProvider.php');
});

test('package discovery keeps provider files generated during the current run', function () {
file_put_contents($this->providersDir . 'FreshServiceProvider.php', "<?php \nreturn Vendor\Fresh\FreshServiceProvider::class;");

[$command] = buildStaleProviderCommand($this->providersDir);

$discovered = new ReflectionProperty(PackageCommand::class, 'discoveredProviderFiles');
$discovered->setAccessible(true);
$discovered->setValue($command, ['FreshServiceProvider.php' => true]);

$method = new ReflectionMethod(PackageCommand::class, 'cleanupProviders');
$method->setAccessible(true);
$method->invoke($command);

expect(file_exists($this->providersDir . 'FreshServiceProvider.php'))->toBeTrue();
});

test('configured provider registration skips classes that do not exist', function () {
$source = (string) file_get_contents(dirname(__DIR__, 3) . '/src/AbstractLaravel.php');

expect($source)
->toContain('public function registerConfiguredProviders()')
->toContain('class_exists($provider)')
->toContain('Skipped missing service provider');
});
Loading