Skip to content
Merged
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ If you run the command with `--force`, the installer becomes aggressive:
- existing files inside published folders are overwritten
- appendable content is appended again, even if it is already present

### Filtering files

You can pass `--filter` one or more times to publish only files whose target path contains any of the given strings (case-insensitive):

```bash
php artisan my-install-command --filter=Services
php artisan my-install-command --filter=Services --filter=Models
```

This is useful when you want to re-publish specific files without running the full install. Note that `--filter` only affects file publishing; Composer and Node package installation is unaffected.

### Publish overview

After publishing files, the command prints an overview of what happened:
Expand Down
25 changes: 19 additions & 6 deletions src/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
use Illuminate\Console\Command;
use Illuminate\Process\Exceptions\ProcessFailedException;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\InputOption;

abstract class InstallCommand extends Command
{
protected FileInstaller $fileInstaller;
protected ComposerPackageInstaller $composerPackageInstaller;
protected NodePackageInstaller $nodePackageInstaller;

protected function configure(): void
{
parent::configure();

$this->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing files');
$this->addOption('filter', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Filter publishable files by target path');
}

public function handle(
FileInstaller $fileInstaller,
ComposerPackageInstaller $composerPackageInstaller,
Expand Down Expand Up @@ -49,40 +58,44 @@ protected function installFiles(): void
{
$fileCollection = collect($this->publishableFiles());
$force = (bool) $this->option('force');
$filters = $this->option('filter');
$publishResults = [];

$this->info("🗄 Installing files...");

$fileCollection
->filter(fn ($publishableFile) => $publishableFile instanceof PublishableFile)
->each(function (PublishableFile $publishableFile) use ($force, &$publishResults) {
->each(function (PublishableFile $publishableFile) use ($force, $filters, &$publishResults) {
$publishResults[] = $this->fileInstaller->publishFile(
path: $publishableFile->path,
target: $publishableFile->target,
force: $force
force: $force,
filters: $filters,
);
});

$fileCollection
->filter(fn ($publishableFile) => $publishableFile instanceof PublishableFolder)
->each(function (PublishableFolder $publishableFolder) use ($force, &$publishResults) {
->each(function (PublishableFolder $publishableFolder) use ($force, $filters, &$publishResults) {
$folderPublishResults = $this->fileInstaller->publishFolder(
path: $publishableFolder->path,
target: $publishableFolder->target,
force: $force
force: $force,
filters: $filters,
);

$publishResults = [...$publishResults, ...$folderPublishResults];
});

$fileCollection
->filter(fn ($publishableFile) => $publishableFile instanceof AppendableFile)
->each(function (AppendableFile $appendableFile) use ($force, &$publishResults) {
->each(function (AppendableFile $appendableFile) use ($force, $filters, &$publishResults) {
$publishResults[] = $this->fileInstaller->appendToFile(
path: $appendableFile->path,
target: $appendableFile->target,
search: $appendableFile->search,
force: $force
force: $force,
filters: $filters,
);
});

Expand Down
29 changes: 24 additions & 5 deletions src/Installers/FileInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ public function __construct(
protected Filesystem $filesystem,
) {}

public function publishFile(string $path, string $target, bool $force = true): PublishResult
public function publishFile(string $path, string $target, bool $force = true, array $filters = []): PublishResult
{
if (! $this->matchesFilters($target, $filters)) {
return PublishResult::skipped(path: $path, target: $target);
}

if ($this->filesystem->exists($target) && !$force) {
return PublishResult::skipped(path: $path, target: $target);
}
Expand All @@ -31,24 +35,28 @@ public function publishFile(string $path, string $target, bool $force = true): P
}

/** @return array<PublishResult> */
public function publishFolder(string $path, string $target, bool $force = true): array
public function publishFolder(string $path, string $target, bool $force = true, array $filters = []): array
{
$sourceDirectory = rtrim($path, DIRECTORY_SEPARATOR);
$targetDirectory = rtrim($target, DIRECTORY_SEPARATOR);

return collect($this->filesystem->allFiles($sourceDirectory))
->map(function (SplFileInfo $sourceFile) use ($sourceDirectory, $targetDirectory, $force) {
->map(function (SplFileInfo $sourceFile) use ($sourceDirectory, $targetDirectory, $force, $filters) {
$sourcePath = $sourceFile->getPathname();
$relativePath = ltrim(str_replace($sourceDirectory, '', $sourcePath), DIRECTORY_SEPARATOR);
$targetPath = $targetDirectory . DIRECTORY_SEPARATOR . $relativePath;

return $this->publishFile($sourcePath, $targetPath, $force);
return $this->publishFile($sourcePath, $targetPath, $force, $filters);
})
->all();
}

public function appendToFile(string $path, string $target, ?string $search, bool $force = true): PublishResult
public function appendToFile(string $path, string $target, ?string $search, bool $force = true, array $filters = []): PublishResult
{
if (! $this->matchesFilters($target, $filters)) {
return PublishResult::skipped(path: $path, target: $target);
}

$contentToAppend = $this->filesystem->get($path);

if (!$this->filesystem->exists($target)) {
Expand Down Expand Up @@ -99,4 +107,15 @@ public function fileContainsString(string $path, ?string $search): bool
needle: $search ?? ''
);
}

private function matchesFilters(string $target, array $filters): bool
{
if (empty($filters)) {
return true;
}

return collect($filters)->contains(
fn ($filter) => str_contains(strtolower($target), strtolower($filter))
);
}
}
79 changes: 78 additions & 1 deletion tests/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,81 @@ public function it_shows_publish_overview_for_skipped_files(): void

$command->expectsOutput('📄 Publish overview: 0 published, 4 skipped.');
}
}

#[Test]
public function it_can_filter_publishable_files_by_target_path(): void
{
$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['Services']])
->expectsOutput('📄 Publish overview: 1 published, 3 skipped.')
->assertExitCode(0);

$this->assertFileExists(app_path('Services/UserService.php'));
}

#[Test]
public function it_can_filter_publishable_folders_by_target_path(): void
{
$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['resources']])
->expectsOutput('📄 Publish overview: 2 published, 2 skipped.')
->assertExitCode(0);

$this->assertFileExists(base_path('resources/vendor/stubs/js/app.js'));
$this->assertFileExists(base_path('resources/vendor/stubs/views/layouts/app.blade.php'));
}

#[Test]
public function it_can_filter_individual_files_inside_publishable_folders(): void
{
File::deleteDirectory(base_path('resources/vendor/stubs'));

$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['js']])
->expectsOutput('📄 Publish overview: 1 published, 3 skipped.')
->assertExitCode(0);

$this->assertFileExists(base_path('resources/vendor/stubs/js/app.js'));
$this->assertFileDoesNotExist(base_path('resources/vendor/stubs/views/layouts/app.blade.php'));
}

#[Test]
public function it_can_filter_appendable_files_by_target_path(): void
{
File::delete(app_path('Models/User.php'));

$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['Models']])
->expectsOutput('📄 Publish overview: 1 published, 3 skipped.')
->assertExitCode(0);

$this->assertFileExists(app_path('Models/User.php'));
}

#[Test]
public function it_shows_skipped_when_no_files_match_the_filter(): void
{
$this->artisan(InstallCommand::class, ['--filter' => ['NonExistentPath']])
->expectsOutput('📄 Publish overview: 0 published, 4 skipped.')
->assertExitCode(0);
}

#[Test]
public function it_performs_case_insensitive_filtering(): void
{
$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['services']])
->expectsOutput('📄 Publish overview: 1 published, 3 skipped.')
->assertExitCode(0);

$this->assertFileExists(app_path('Services/UserService.php'));
}

#[Test]
public function it_can_filter_with_multiple_values(): void
{
File::delete(app_path('Models/User.php'));

$this->artisan(InstallCommand::class, ['--force' => true, '--filter' => ['Services', 'Models']])
->expectsOutput('📄 Publish overview: 2 published, 2 skipped.')
->assertExitCode(0);

$this->assertFileExists(app_path('Services/UserService.php'));
$this->assertFileExists(app_path('Models/User.php'));
}
}
2 changes: 1 addition & 1 deletion tests/Support/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

class InstallCommand extends BaseInstallCommand
{
protected $signature = 'app:install-command {--force : Overwrite existing files}';
protected $signature = 'app:install-command';

protected function publishableFiles(): array
{
Expand Down
Loading