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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ We provide a few helper commands:
- `php artisan modules:clear` — clear the module cache
- `php artisan modules:sync` — update project configs (like `phpunit.xml`) with module settings
- `php artisan modules:list` — list all modules
- `php artisan modules:remove` — delete a module and remove it from your `composer.json`

#### Laravel “`make:`” Commands

Expand Down
102 changes: 102 additions & 0 deletions src/Console/Commands/ModulesRemove.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace InterNACHI\Modular\Console\Commands;

use Composer\Factory;
use Composer\Json\JsonFile;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use InterNACHI\Modular\Support\ModuleConfig;
use InterNACHI\Modular\Support\ModuleRegistry;

class ModulesRemove extends Command
{
protected $signature = 'modules:remove
{name : The name of the module}
{--force : Remove the module without asking for confirmation}';

protected $description = 'Delete a module and remove it from the application composer.json file';

public function __construct(
protected Filesystem $filesystem,
protected ModuleRegistry $module_registry
) {
parent::__construct();
}

public function handle(): int
{
$module = $this->module_registry->module($this->argument('name'));

if (null === $module) {
$this->error("Module '{$this->argument('name')}' does not exist.");
return 1;
}

if (! $this->option('force') && ! $this->confirm("This will permanently delete {$module->base_path}. Continue?", false)) {
$this->line('Module was not removed.');
return 1;
}

$composer_name = $this->composerName($module);

$this->filesystem->deleteDirectory($module->base_path);
$this->line(" - Deleted <info>{$module->base_path}</info>");

if ($composer_name) {
$this->removeFromCoreComposerConfig($composer_name);
}

$this->call(ModulesClear::class);

$this->module_registry->reload();

if ($composer_name) {
$this->newLine();
$this->line("Please run <comment>composer update {$composer_name}</comment>");
$this->newLine();
}

return 0;
}

protected function composerName(ModuleConfig $module): ?string
{
$composer_file = $module->path('composer.json');

if (! $this->filesystem->isFile($composer_file)) {
return null;
}

return json_decode($this->filesystem->get($composer_file), true)['name'] ?? null;
}

protected function removeFromCoreComposerConfig(string $composer_name): void
{
// We're going to move into the Laravel base directory while
// we're updating the composer file so that we're sure we update
// the correct composer.json file (we'll restore CWD at the end)
$original_working_dir = getcwd();
chdir($this->laravel->basePath());

$json_file = new JsonFile(Factory::getComposerFile());
$definition = $json_file->read();

$has_changes = false;

foreach (['require', 'require-dev'] as $key) {
if (isset($definition[$key][$composer_name])) {
unset($definition[$key][$composer_name]);
$this->line(" - Removed <info>{$composer_name}</info> from {$key}");
$has_changes = true;
}
}

if ($has_changes) {
$json_file->write($definition);
$this->line(" - Wrote to <info>{$json_file->getPath()}</info>");
}

chdir($original_working_dir);
}
}
2 changes: 2 additions & 0 deletions src/Support/ModularServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use InterNACHI\Modular\Console\Commands\ModulesCache;
use InterNACHI\Modular\Console\Commands\ModulesClear;
use InterNACHI\Modular\Console\Commands\ModulesList;
use InterNACHI\Modular\Console\Commands\ModulesRemove;
use InterNACHI\Modular\Console\Commands\ModulesSync;
use InterNACHI\Modular\PluginRegistry;
use InterNACHI\Modular\Plugins\ArtisanPlugin;
Expand Down Expand Up @@ -102,6 +103,7 @@ public function boot(): void
ModulesClear::class,
ModulesSync::class,
ModulesList::class,
ModulesRemove::class,
]);
}
}
Expand Down
78 changes: 78 additions & 0 deletions tests/Commands/ModulesRemoveTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace InterNACHI\Modular\Tests\Commands;

use InterNACHI\Modular\Console\Commands\ModulesRemove;
use InterNACHI\Modular\Support\Facades\Modules;
use InterNACHI\Modular\Tests\Concerns\WritesToAppFilesystem;
use InterNACHI\Modular\Tests\TestCase;

class ModulesRemoveTest extends TestCase
{
use WritesToAppFilesystem;

public function test_it_removes_a_module(): void
{
$module = $this->makeModule('test-module');

$this->assertTrue($this->filesystem()->isDirectory($module->base_path));
$this->assertArrayHasKey('modules/test-module', $this->appComposerRequirements());

$this->artisan(ModulesRemove::class, ['name' => 'test-module', '--force' => true])
->assertExitCode(0);

$this->assertFalse($this->filesystem()->isDirectory($module->base_path));
$this->assertArrayNotHasKey('modules/test-module', $this->appComposerRequirements());
$this->assertNull(Modules::module('test-module'));
}

public function test_it_only_removes_the_given_module(): void
{
$this->makeModule('test-module');
$other = $this->makeModule('other-module');

$this->artisan(ModulesRemove::class, ['name' => 'test-module', '--force' => true])
->assertExitCode(0);

$this->assertTrue($this->filesystem()->isDirectory($other->base_path));
$this->assertArrayHasKey('modules/other-module', $this->appComposerRequirements());
$this->assertNotNull(Modules::module('other-module'));
}

public function test_it_asks_for_confirmation(): void
{
$module = $this->makeModule('test-module');

$this->artisan(ModulesRemove::class, ['name' => 'test-module'])
->expectsConfirmation("This will permanently delete {$module->base_path}. Continue?", 'yes')
->assertExitCode(0);

$this->assertFalse($this->filesystem()->isDirectory($module->base_path));
}

public function test_it_does_not_remove_the_module_if_confirmation_is_declined(): void
{
$module = $this->makeModule('test-module');

$this->artisan(ModulesRemove::class, ['name' => 'test-module'])
->expectsConfirmation("This will permanently delete {$module->base_path}. Continue?", 'no')
->assertExitCode(1);

$this->assertTrue($this->filesystem()->isDirectory($module->base_path));
$this->assertArrayHasKey('modules/test-module', $this->appComposerRequirements());
}

public function test_it_fails_for_unknown_modules(): void
{
$this->artisan(ModulesRemove::class, ['name' => 'does-not-exist', '--force' => true])
->expectsOutput("Module 'does-not-exist' does not exist.")
->assertExitCode(1);
}

protected function appComposerRequirements(): array
{
$composer_file = $this->getApplicationBasePath().'/composer.json';

return json_decode($this->filesystem()->get($composer_file), true)['require'] ?? [];
}
}