Repository navigation
Proposal: modules, a feature-oriented layout for Goravel applications #1030
protibimbok
started this conversation in
Ideas
Replies: 1 comment
|
goravel/framework#1613 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
Goravel applications are laid out by type:
app/http/controllers,routes/,database/migrations,resources/views. That works well until an application has several distinct features, at which point one feature is spread over six directories.I would like to propose first-class modules: a feature lives in one directory (
modules/blog/) with its own routes, controllers, migrations, views, translations, config and frontend entries, and the application lists its modules in one file.Design principles
bootstrap/modules.gois the only place modules are named. A directory undermodules/that is not in the list does nothing. Removing a module is deleting its line.modular.Module. What it contributes is detected by type assertion: implement the interface and the behaviour is wired, leave it out and the matching directory is ignored even if it exists.ginandfiberare unchanged. They see more views in the view registry, more routes throughroute.Routerand more global middleware.WithModulesbehaves exactly as today.A module
Everything else is an optional interface.
The module's name namespaces what it contributes:
resources/views/index.tmplctx.Response().View().Make("blog/index.tmpl", …)lang/en/messages.jsonfacades.Lang(ctx).Get("blog/messages.title")facades.Config().GetInt("modules.blog.per_page")resources/js/app.js{{ vite "@blog/app.js" }}A module is a package of the application's Go module. It has no
go.modof its own, and its directory is derived from its Go package path, so there is nothing to configure per module.Capabilities
foundation.ProvidesServicesServiceProviders() []foundation.ServiceProviderWithProviderslistmodular.ProvidesConfigConfig() map[string]anyconfig.Add("modules.<name>", …)modular.ProvidesGlobalMiddlewareGlobalMiddleware() []http.MiddlewareWithMiddlewarecallback runsmodular.ProvidesRoutesRoutes(router route.Router)modular.routes.webgroup (prefix and middleware from config)modular.ProvidesApiRoutesApiRoutes(router route.Router)modular.routes.apigroup (default prefixapi)modular.ProvidesMigrationsMigrations() []schema.Migrationmodular.ProvidesSeedersSeeders() []seeder.Seederseeder.Registermodular.ProvidesCommandsCommands() []console.Commandartisan.Register, subject toWithCommandsFilterandWithCommandHooksmodular.ProvidesJobsJobs() []queue.Jobqueue.Registermodular.ProvidesEventsEvents() map[event.Event][]event.ListenerWithEventsmodular.ProvidesScheduleSchedule() []schedule.Eventschedule.Registermodular.ProvidesViewsViews() fs.FSView.LoadViewsFromFS, names prefixed with<name>/modular.ProvidesTranslationsTranslations() fs.FSapp.lang_fs, groups prefixed with<name>/modular.ProvidesViteEntriesViteEntries() []stringvite:inputs, built by the pluginmodular.ProvidesViteAliasViteAliases() map[string]string@…aliases for Vite and for{{ vite }}/{{ viteAsset }}modular.ProvidesStaticStatic() map[string]modular.Pathroute.StaticFSon the root routermodular.OrderedOrder() intmodular.LocatedPath() stringCapabilities mirror the builder's
With*setters only where a service provider cannot do the same job. Validation rules and filters, template functions and runners are registered in the module's service providerBoot, as a package would do today.Registry
Available as
facades.Modular(). The typical use is an optional integration between modules:if facades.Modular().Has("billing") { … }.Runtime
Application.Build()merges each module's contributions into the step that already handles the application's own. No new boot phase is introduced for providers, events, commands and so on.WithConfigmodules.<name>config; module translations added toapp.lang_fsRegisterWithMiddlewareBoot<name>/WithRouting, then module static prefixes, then each module's web and api groupsWho wins
The application always overrides a module:
blog/index.tmplresources/views/blog/index.tmplin the applicationblog/messages.*lang/en/blog/messages.jsonin the applicationWithMiddlewarecallback sees the module middleware and can reorder or replace itmodular.routes.web.prefix.vite.config.jswins over a module'sVite
artisan vite:inputsgains amodulesfield:{"inputs":["@blog/app.js","resources/js/app.js"], "modules":[{"name":"blog","path":"modules/blog", "entries":["modules/blog/resources/js/app.js"]}]}goravel-vite-pluginreads it.vite.config.jsdoes not change:@<name>is aliased tomodules/<name>/resources/js, mirroring the existing@→resources/js;refresh: true, module views, routes and lang files are watched.Aliases also work in templates:
{{ vite "@blog/app.js" }} {{ vite "@/app.js" "@blog/app.js" }}For this the plugin writes its alias table into the manifest (reserved key
goravel:aliases) and into the hot file, which becomes JSON ({"url": "…", "aliases": {…}}). The framework resolves the alias, cleans the path and refuses anything that ends outside the project. The plugin applies the same rule, so dev and build resolve to the same file.Commands
goravel new <app> --modularartisan modular:initconfig/modular.go,bootstrap/modules.go,.WithModules(Modules)inbootstrap/app.go. Idempotent; prints the manual step when the builder chain cannot be edited safely.artisan make:module Blog [--api] [--plain] [--order=N] [--force]modules/blog/and adds it tobootstrap/modules.goartisan modular:list [--json]artisan modular:publishartisan make:<thing> … --module=blogmake:*generator writes into the module.make:migration,make:command,make:seeder,make:jobandmake:provideralso register the item on the module struct.Third-party modules
A module can also come from another Go module (
go get github.com/acme/goravel-blog, then add&blog.Module{}to the list). Go code, embedded views and translations work as for any module. The frontend is the difficulty: Vite and Tailwind cannot read files from the Go module cache.Such a module implements
ProvidesSources, andartisan modular:publish(wired togo generate) copies its frontend sources and views tomodules/<name>/, committed with the application.vite:inputsfails when that copy is missing or out of date, so a build never ships stale assets silently.This is the part of the design I am least sure belongs in the first version (see below).
What does not change
WithModules.ginandfiber.config/app.go+Boot()layout: the registry is not bound there and themodular:*commands refuse with a clear error.vite.config.jsof existing applications. The JSON hot file is read by the new plugin and the new framework version together; they need to be released in step.Limits
*translation group (lang/en.json); it cannot be namespaced.ProvidesStaticare served as-is. Use a Vite asset for cache-busted URLs.lang/en/blog/messages.jsonreplaces the module's group as a whole; keys it omits do not fall back to the module.WithModulesand theBuild()mergemodular:init,make:module,modular:listvite:inputsmodules and alias resolution (with the plugin PR)--moduleon themake:*generatorsmodular:publish), if wanted--modular, documentationAll reactions