feat: [#959] add WithCommandsFilter builder to scope registered Artisan commands#1500
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1500 +/- ##
==========================================
+ Coverage 69.43% 69.57% +0.13%
==========================================
Files 378 378
Lines 29782 29813 +31
==========================================
+ Hits 20680 20743 +63
+ Misses 8147 8114 -33
- Partials 955 956 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…an commands
Introduces a parameter-less WithCommandsFilter callback on
ApplicationBuilder that returns the positive list of command
signatures to keep when the framework registers its own Artisan
commands. The list is captured once at Build() time and applied to
both the framework's defaultCommands batch and the
console/console/* batch registered by console.ServiceProvider.
Matching rules (signature-only, no category matching):
- exact match (no wildcard) -> command.Signature()
- glob match (entry contains '*') -> command.Signature() via
path.Match ('?' is a literal character)
Semantics:
- method not called -> keep every command (default)
- callback returns nil -> keep every command (no filter)
- callback returns []string{} -> drop every command
- callback returns entries -> keep only matching commands
The filter also trims the user-added commands registered via
WithCommands, so the user cannot bypass the filter by adding
commands.
Closes goravel/goravel#959
6161684 to
ede0de0
Compare
| Commands([]console.Command) | ||
| // ConsoleCommandsFilter returns the captured positive-list of command | ||
| // signatures to keep (set via WithCommandsFilter). nil means no filter. | ||
| ConsoleCommandsFilter() []string |
There was a problem hiding this comment.
| ConsoleCommandsFilter() []string | |
| CommandsFilter() []string |
There was a problem hiding this comment.
Renamed to CommandsFilter() throughout the codebase.
| app.EXPECT().MakeConfig().Return(configFacade).Maybe() | ||
| app.EXPECT().MakeProcess().Return(processFacade).Maybe() |
There was a problem hiding this comment.
Why use Maybe? It should be avoided. The same as below.
There was a problem hiding this comment.
Replaced .Maybe() with explicit .Once() in all three Boot test functions.
| // WithCommandsFilter registers a callback that returns the positive list of | ||
| // command signatures to keep when the framework registers its own Artisan | ||
| // commands. The callback runs once at Build() time. | ||
| // | ||
| // Each entry in the returned slice is matched in one of two ways: | ||
| // - Exact match (no wildcard) — checked against command.Signature(). | ||
| // - Glob match (the entry contains '*') — checked against | ||
| // command.Signature() using stdpath.Match. '*' matches any sequence | ||
| // of non-'/' characters. '?' is not a wildcard. | ||
| // | ||
| // Category is never consulted. The filter is signature-only. | ||
| // | ||
| // Semantics: | ||
| // - Method not called → keep every command (default). | ||
| // - Callback returns nil → keep every command (no filter). | ||
| // - Callback returns []string{} → drop every command (filter, no matches). | ||
| // - Callback returns entries → keep only commands whose signature | ||
| // matches an entry (exact or glob). | ||
| // | ||
| // The filter applies to user-added commands from WithCommands as well, | ||
| // so the user cannot bypass the filter by adding commands. |
There was a problem hiding this comment.
Simplified the docstring. Also clarified that only * triggers glob matching and ? is matched literally as an exact match.
| publishGroups map[string]map[string]string | ||
| json foundation.Json | ||
| bootedRunners []string | ||
| consoleCommandsFilter []string |
There was a problem hiding this comment.
| consoleCommandsFilter []string | |
| commandsFilter []string |
There was a problem hiding this comment.
Renamed to commandsFilter.
| // WithCommandsFilter registers a callback that returns the positive list of | ||
| // command signatures to keep when the framework registers its own Artisan | ||
| // commands. The callback runs once at Build() time; the user can call | ||
| // facades.Config() inside it to read app.env or any other setting without | ||
| // needing a parameter. | ||
| // | ||
| // Each entry in the returned slice is matched in one of two ways: | ||
| // | ||
| // - Exact match (no wildcard) — checked against command.Signature(). | ||
| // - Glob match (the entry contains '*') — checked against | ||
| // command.Signature() using stdpath.Match. '*' matches any sequence | ||
| // of non-'/' characters. '?' is not a wildcard. | ||
| // | ||
| // Category is never consulted. The filter is signature-only. | ||
| // | ||
| // Semantics: | ||
| // | ||
| // - Method not called → keep every command (default). | ||
| // - Callback returns nil → keep every command (no filter). | ||
| // - Callback returns []string{} → drop every command (filter, no matches). | ||
| // - Callback returns entries → keep only commands whose signature | ||
| // matches an entry (exact or glob). | ||
| // | ||
| // Composition with WithCommands: | ||
| // | ||
| // WithCommands adds extra commands to the framework's set; WithCommandsFilter | ||
| // then trims the combined set. The filter applies to user-added commands | ||
| // too, so the user cannot bypass the filter by adding commands. |
There was a problem hiding this comment.
Done — simplified in the implementation as well.
| func (s *ApplicationTestSuite) TestDefaultCommandsUpDownBySignature() { | ||
| s.app.SetJson(foundationjson.New()) | ||
| s.app.Instance(binding.Route, mocksroute.NewRoute(s.T())) | ||
| s.app.consoleCommandsFilter = []string{"up", "down"} | ||
|
|
||
| commands := s.app.defaultCommands() | ||
| got := commandSignatures(commands) | ||
|
|
||
| s.Contains(got, "up") | ||
| s.Contains(got, "down") | ||
| s.NotContains(got, "about") | ||
| } |
There was a problem hiding this comment.
This case is unnecessary, duplicated.
There was a problem hiding this comment.
Removed the duplicate test.
goravel-coder
left a comment
There was a problem hiding this comment.
Review of PR #1500
Diff: https://critique.work/v/49ae9fcc92287dcc8b95ca92b00f9215
What it does
Adds WithCommandsFilter() to the ApplicationBuilder, allowing users to scope which built-in Artisan commands get registered -- particularly useful for production-only binaries that don't need make:*, package:*, vendor:publish, etc.
What's good
- Comprehensive tests -- table-driven tests cover nil/empty/whitespace/glob/exact/literal-
?cases across both unit (FilterCommandsByAllowlist) and integration-level (defaultCommands,configureCommands, service provider) tests. All pass green. - Go vet clean -- no issues.
- Defensive nil check --
registerCommandsnow checksartisanFacade == nilbefore proceeding. - Clean API --
WithCommandsFilter(func() []string)is a typical builder pattern, callback delayed toBuild()so it can read config/env. - Consistent semantics --
nilallowlist = keep all,[]string{}= drop all, entries = keep only matches. Documented in both contract and implementation. - Filter applies everywhere -- console SP commands,
defaultCommands(), and userWithCommands()commands all go through the same gate.
Issues
Medium: Docstring misleads about path.Match semantics
The docstring claims '?' is not a wildcard, which is technically true for this implementation (only * triggers the glob path), but the docstring also says matching uses stdpath.Match -- which does treat ? as a wildcard. A user reading the docs might think ? behaves like path.Match's ?, but it doesn't because the code only routes patterns containing * to path.Match. Patterns with ? but no * go to exact match.
Suggestion: Clarify in the docstring that the glob path is only triggered by * presence, and patterns without * are exact-matched regardless of ? content.
Low: Double Boot of service providers
Boot() (line 92) and Build() (line 106) both call r.providerRepository.Boot(r). This means if a goravel app calls both Boot() and then Build(), console SP's registerCommands runs twice (once without filter, once with filter). This is pre-existing and not introduced by this PR, but worth noting since the filter now adds a behavioral difference between the two calls.
Low: TestServiceProviderBootNilAllowlistRegistersAll uses .Maybe() for config/process
The test at console/service_provider_test.go:57 asserts the console SP registers exactly 4 commands when filter is nil. This is correct, but the test doesn't verify that MakeConfig() and MakeProcess() are actually called -- they use .Maybe() which can match 0 times. This is fine for testing filtering but could mask regressions if the underlying command constructors change their dependencies.
Verdict
Request changes -- The docstring clarification about ? behavior is the main ask. The code and tests themselves are solid.
- Rename ConsoleCommandsFilter() -> CommandsFilter() (contract, impl, tests, mocks) - Rename consoleCommandsFilter -> commandsFilter (field name) - Simplify WithCommandsFilter docstrings and clarify ? behavior - Replace .Maybe() with explicit .Once() in service provider tests - Remove duplicate TestDefaultCommandsUpDownBySignature test - Add comment on console.Application.Register clarifying filtering is caller's responsibility
| commands func() []console.Command | ||
| config func() | ||
| configuredServiceProviders func() []foundation.ServiceProvider | ||
| commandsFilter func() []string |
Move FilterCommandsByAllowlist into registerCommands() so ALL callers (defaultCommands, configureCommands, Commands()) go through the same gate. Remove redundant filtering from defaultCommands() and simplify configureCommands() to use registerCommands(). Update tests to verify filtering through the Commands() path.
Why
Closes goravel/goravel#959. The framework currently registers every Artisan dev command (about, env:, package:, vendor:publish, make:*, etc.) on every binary, including production server-only builds. There was no supported way to scope this down —
./app --helpin production revealed dev commands, and the surface was always the same regardless of environment.What
Introduces a new
WithCommandsFilter(func() []string)builder method onApplicationBuilder. The callback runs once atBuild()time and returns the positive list of command signatures to keep. The list is captured on theApplicationand applied uniformly to both the framework'sdefaultCommandsbatch (foundation/application.go:defaultCommands) and theconsole/console/*batch registered byconsole.ServiceProvider.Boot.Matching rules (signature-only — category is never consulted):
command.Signature()*) — checked againstcommand.Signature()viapath.Match(*matches any sequence of non-/characters,?is a literal character)Semantics:
nil[]string{}The filter also trims the user-added commands registered via
WithCommands(...), so the user cannot bypass the filter by adding commands. The implementation ofadd first, then filterlives in two places —foundation/application.go:configureCommandsfilters user-added commands, andconsole.ServiceProvider.Bootfilters its own batch.User-facing API
The user can also drive the list from build tags or
ldflagsinstead offacades.Config()— the framework doesn't care, it just consumes the returned slice.Files
console/application.go— newFilterCommandsByAllowlisthelper (signature-only,*-only glob)console/service_provider.go— apply filter to theconsole/console/*batch via the newApplication.ConsoleCommandsFilter()getterfoundation/application.go— newconsoleCommandsFilterfield,Build()capture,defaultCommandsandconfigureCommandsapply the filterfoundation/application_builder.go— newWithCommandsFiltermethodcontracts/foundation/application.go+application_builder.go— interface extensionsmocks/foundation/Application.go+ApplicationBuilder.go— regenerated viago tool mockeryconsole/application_test.go,console/service_provider_test.go(new),foundation/application_test.go,foundation/application_builder_test.goBreaking changes
None. Default behavior is unchanged for every existing app —
WithCommandsFilteris opt-in.Test plan
go build ./...— cleango test ./...— all pass (foundation, console, console/console, plus every downstream package that uses mocks/foundation)go vet ./...— cleanup-down-by-signature, the "add first, then filter" composition betweenWithCommandsandWithCommandsFilter, and thenil/[]string{}/entries distinction on both registration paths.