From 360a837dc88ead6469631f2bdce6b3e088fdeffb Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 9 Jun 2026 15:16:40 +0800 Subject: [PATCH 01/35] docs: update v1.18 documentation --- .vitepress/config/en.ts | 12 +- en/digging-deeper/artisan-console.md | 27 ++- en/digging-deeper/collections.md | 292 ++++++++++++++++++++++ en/digging-deeper/helpers.md | 2 + en/digging-deeper/package-development.md | 33 +++ en/getting-started/compile.md | 12 + en/getting-started/configuration.md | 45 ++++ en/orm/getting-started.md | 32 +++ en/security/authentication.md | 13 +- en/the-basics/validation.md | 296 +++++++++++++---------- en/upgrade/history.md | 1 + en/upgrade/v1.18.md | 167 +++++++++++++ 12 files changed, 796 insertions(+), 136 deletions(-) create mode 100644 en/digging-deeper/collections.md create mode 100644 en/upgrade/v1.18.md diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 7afdda7bb..88e585e98 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -171,12 +171,12 @@ function sidebarPrologue(): DefaultTheme.SidebarItem[] { function sidebarUpgrade(): DefaultTheme.SidebarItem[] { return [ { - text: 'Upgrading To v1.17 From v1.16', - link: 'v1.17' + text: 'Upgrading To v1.18 From v1.17', + link: 'v1.18' }, { - text: 'Upgrading To v1.16 From v1.15', - link: 'v1.16' + text: 'Upgrading To v1.17 From v1.16', + link: 'v1.17' }, { text: 'History', @@ -301,6 +301,10 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { text: 'Strings', link: 'strings' }, + { + text: 'Collections', + link: 'collections' + }, { text: 'Helpers', link: 'helpers' diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index c886c2fb0..920be3001 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -31,7 +31,7 @@ Then you can simply run your commands like this: artisan make:controller DemoController ``` -You can also use `artisan` shell script like this: +You can also use the `artisan` shell script to run built-in commands. ### Generating Commands @@ -450,6 +450,31 @@ ctx.NewLine() ctx.NewLine(2) ``` +#### Tables + +You may use the `Table` method to render structured data in a tabular format. The method accepts headers and rows, and writes the rendered table directly to the console: + +```go +func (receiver *SendEmails) Handle(ctx console.Context) error { + headers := []string{"ID", "Email", "Status"} + rows := [][]string{ + {"1", "a@example.com", "Queued"}, + {"2", "b@example.com", "Sent"}, + } + + ctx.Table(headers, rows) + + return nil +} +``` + +You can pass a `console.TableOption` as the third argument to customize borders, dimensions, and styles. +```go +ctx.Table(headers, rows, console.TableOption{ + Width: 80, +}) +``` + #### Progress Bars For long-running tasks, it is often helpful to provide the user with some indication of how much time the task will take. You may use the `WithProgressBar` method to display a progress bar. diff --git a/en/digging-deeper/collections.md b/en/digging-deeper/collections.md new file mode 100644 index 000000000..3f3c86aba --- /dev/null +++ b/en/digging-deeper/collections.md @@ -0,0 +1,292 @@ +# Collections + +[[toc]] + +## Introduction + +Goravel provides a fluent collection API through the `github.com/goravel/framework/support/collect` package. Collections make it convenient to filter, transform, aggregate, and inspect slice data with chainable methods. + +The package provides two collection types: + +- `Collection`: an eager collection that works with data already loaded into memory. +- `LazyCollection`: a lazy collection that evaluates pipelines only when a terminal method is called. + +```go +import "github.com/goravel/framework/support/collect" +``` + +## Creating Collections + +Use `collect.New` to create a collection from variadic arguments: + +```go +numbers := collect.New(1, 2, 3, 4, 5) +``` + +Use `collect.Of` to create a collection from an existing slice: + +```go +items := []string{"apple", "banana", "cherry"} +fruits := collect.Of(items) +``` + +You may retrieve all underlying items with `All`: + +```go +fruits.All() // []string{"apple", "banana", "cherry"} +``` + +## Basic Operations + +Collections provide common methods for reading and modifying items: + +```go +numbers := collect.New(1, 2, 3, 4, 5) + +numbers.Count() // 5 +numbers.IsEmpty() // false +numbers.IsNotEmpty() // true +*numbers.First() // 1 +*numbers.Last() // 5 +numbers.Contains(3) // true +numbers.Search(4) // 3 +``` + +Some methods mutate the current collection and return it for chaining: + +```go +numbers.Push(6).Prepend(0) +numbers.All() // []int{0, 1, 2, 3, 4, 5, 6} +``` + +Mutating methods include `Push`, `Prepend`, `Pop`, `Pull`, `Shift`, `Unshift`, `Forget`, `Put`, `Splice`, and `Transform`. Methods such as `Push`, `Prepend`, `Forget`, `Put`, and `Transform` return the current collection, while methods such as `Pop`, `Pull`, `Shift`, and `Splice` return the removed item or items. + +## Filtering And Mapping + +Use `Filter` and `Reject` to keep or remove items by callback: + +```go +numbers := collect.New(1, 2, 3, 4, 5, 6) + +evens := numbers.Filter(func(n int, _ int) bool { + return n%2 == 0 +}) + +evens.All() // []int{2, 4, 6} +``` + +Use `Map` to transform each item. The `Map` method returns `*collect.Collection[any]` because the callback can return any type: + +```go +labels := numbers.Map(func(n int, i int) any { + return fmt.Sprintf("item_%d_%d", i, n) +}) + +labels.All() // []any{"item_0_1", "item_1_2", ...} +``` + +Other transformation helpers include `FlatMap`, `MapInto`, `MapSpread`, `MapToDictionary`, `MapToGroups`, and `MapWithKeys`. + +Use `Unique`, `UniqueBy`, and `Duplicates` to work with repeated values. The `Duplicates` method returns each duplicated value once: + +```go +numbers := collect.New(1, 2, 2, 3, 3, 3) + +numbers.Unique().All() // []int{1, 2, 3} +numbers.Duplicates().All() // []int{2, 3} +``` + +## Working With Structs + +The `Where` method supports callback filtering and Laravel-style field comparisons. + +```go +type User struct { + ID int + Name string + Age int + Country string + Balance float64 + DeletedAt *time.Time +} + +users := collect.Of([]User{ + {ID: 1, Name: "Alice", Age: 25, Country: "FR", Balance: 150}, + {ID: 2, Name: "Bob", Age: 30, Country: "US", Balance: 80}, + {ID: 3, Name: "Charlie", Age: 25, Country: "FR", Balance: 200}, +}) + +frenchUsers := users.Where("Country", "FR") +youngUsers := users.Where("Age", "=", 25) +numericAgeUsers := users.Where("Age", "=", "25") +richUsers := users.Where("Balance", ">", 100) +adultUsers := users.Where(func(user User) bool { + return user.Age >= 18 +}) +``` + +Supported comparison operators are `=`, `==`, `!=`, `>`, `>=`, `<`, `<=`, `like`, and `not like`. Equality comparisons support simple mixed numeric values, so an integer field can match a numeric string such as `"25"`. + +You may also use field-specific helpers: + +```go +activeUsers := users.WhereNull("DeletedAt") +deletedUsers := users.WhereNotNull("DeletedAt") +selectedUsers := users.WhereIn("Country", []any{"FR", "US"}) +otherUsers := users.WhereNotIn("Country", []any{"FR"}) +names := users.Pluck("Name") +``` + +## Aggregates + +Collection aggregate helpers calculate values from the items: + +```go +numbers := collect.New(1, 2, 3, 4, 5) + +numbers.Sum(func(n int) float64 { return float64(n) }) // 15 +numbers.Avg(func(n int) float64 { return float64(n) }) // 3 +numbers.Min(func(n int) float64 { return float64(n) }) // 1 +numbers.Max(func(n int) float64 { return float64(n) }) // 5 +``` + +Use `Reduce` when you need to build a single value from the collection: + +```go +total := numbers.Reduce(func(acc any, n int, _ int) any { + return acc.(int) + n +}, 0) +``` + +## Sorting And Slicing + +Collections include helpers for ordering and slicing data: + +```go +numbers := collect.New(5, 1, 4, 2, 3) + +numbers.Sort(func(a, b int) bool { return a < b }).All() // []int{1, 2, 3, 4, 5} +numbers.Reverse().All() // []int{3, 2, 4, 1, 5} +numbers.Take(3).All() // []int{5, 1, 4} +numbers.Skip(2).All() // []int{4, 2, 3} +numbers.Slice(2).All() // []int{4, 2, 3} +numbers.Chunk(2) // [][]int{{5, 1}, {4, 2}, {3}} +``` + +Use `Splice` when you need to remove or insert items in place. It mutates the current collection and returns the removed items. A negative `deleteCount` is treated as `0`, so it inserts without removing existing items: + +```go +letters := collect.New("a", "b", "c", "d") + +removed := letters.Splice(1, 2, "x") +removed.All() // []string{"b", "c"} +letters.All() // []string{"a", "x", "d"} + +inserted := letters.Splice(1, -1, "y") +inserted.All() // []string{} +letters.All() // []string{"a", "y", "x", "d"} +``` + +For structs, use `SortBy` or `SortByDesc` with a key function: + +```go +sortedUsers := users.SortBy(func(user User) string { + return user.Name +}) +``` + +## Conditional Operations + +Use `When` and `Unless` to conditionally apply transformations while keeping a fluent chain: + +```go +result := collect.New(1, 2, 3, 4, 5). + When(true, func(c *collect.Collection[int]) *collect.Collection[int] { + return c.Filter(func(n int, _ int) bool { return n > 2 }) + }). + Unless(false, func(c *collect.Collection[int]) *collect.Collection[int] { + return c.Take(2) + }) + +result.All() // []int{3, 4} +``` + +Use `Tap` to run side effects without breaking the chain: + +```go +numbers.Tap(func(c *collect.Collection[int]) { + fmt.Println("Processing", c.Count(), "items") +}) +``` + +## Lazy Collections + +Lazy collections are useful for large datasets or generated values because intermediate operations are not executed until a terminal method or helper, such as `All`, `Count`, `First`, `Collect`, `Sum`, or `LazyReduce`, is called. + +Create lazy collections from slices, ranges, generators, functions, repeated values, or channels: + +```go +lazy := collect.LazyOf([]int{1, 2, 3, 4, 5}) +lazy = collect.LazyNew(1, 2, 3, 4, 5) + +rangeItems := collect.LazyRange(1, 1000000) +generated := collect.LazyGenerate(func(i int) int { + return i * 2 +}, 100) +repeated := collect.LazyRepeat("Goravel", 3) +``` + +Chain lazy operations and consume only the items you need: + +```go +result := collect.LazyRange(1, 1000000). + Filter(func(n int, _ int) bool { return n%100 == 0 }). + Take(5). + All() + +result // []int{100, 200, 300, 400, 500} +``` + +Lazy collections handle early-exit operations safely for finite channel producers. Methods such as `First`, `FirstWhere`, `Contains`, `Every`, `IsEmpty`, `Take`, and `TakeWhile` drain remaining input when they stop early. + +Use `Collect` to convert a lazy collection into an eager collection: + +```go +eager := collect.LazyRange(1, 10).Collect() +``` + +When mapping a lazy collection to another concrete type, use the generic `LazyMap` helper: + +```go +numbers := collect.LazyOf([]string{"1", "2", "3"}) + +ints := collect.LazyMap(numbers, func(value string, _ int) int { + n, _ := strconv.Atoi(value) + return n +}) + +ints.All() // []int{1, 2, 3} +``` + +## Method Overview + +Common eager collection methods include: + +| Category | Methods | +| --- | --- | +| Create | `New`, `Of` | +| Read | `All`, `Count`, `First`, `Last`, `Get`, `Has`, `Contains`, `ContainsStrict`, `DoesntContain`, `Search`, `SearchBy`, `Random` | +| Transform | `Map`, `FlatMap`, `MapInto`, `MapSpread`, `Transform`, `Reverse`, `Shuffle`, `Concat`, `Collapse`, `Unique`, `UniqueBy`, `Duplicates` | +| Filter | `Filter`, `Reject`, `Where`, `WhereIn`, `WhereNotIn`, `WhereNull`, `WhereNotNull` | +| Aggregate | `Sum`, `Avg`, `Min`, `Max`, `Median`, `Mode`, `Reduce`, `CountBy` | +| Slice | `Chunk`, `Slice`, `Take`, `Skip`, `Split`, `ForPage`, `Splice` | +| Utility | `Clone`, `Tap`, `Pipe`, `ToJson`, `Join`, `KeyBy`, `Pluck`, `Zip` | + +Common lazy collection methods include: + +| Category | Methods | +| --- | --- | +| Create | `LazyNew`, `LazyOf`, `LazyRange`, `LazyGenerate`, `LazyFromFunc`, `LazyFromChannel`, `LazyRepeat` | +| Pipeline | `Filter`, `Reject`, `Map`, `FlatMap`, `Where`, `WhereIn`, `WhereNotIn`, `Take`, `Skip`, `TakeWhile`, `SkipWhile`, `Unique`, `UniqueBy`, `Sort` | +| Terminal | `All`, `Count`, `First`, `FirstWhere`, `Last`, `Contains`, `Every`, `IsEmpty`, `IsNotEmpty`, `Collect`, `Sum`, `Avg`, `Min`, `Max`, `ToJson`, `Iterator` | +| Generic | `LazyMap`, `LazyReduce` | diff --git a/en/digging-deeper/helpers.md b/en/digging-deeper/helpers.md index edffc28ff..cccbd5f7a 100644 --- a/en/digging-deeper/helpers.md +++ b/en/digging-deeper/helpers.md @@ -599,6 +599,8 @@ convert.Pointer(1) // *int(1) import "github.com/goravel/framework/support/collect" ``` +The helpers below operate directly on slices or maps. For fluent eager and lazy collections, see [Collections](collections.md). + ### `collect.Count()` The `collect.Count()` function returns the number of items in the given collection: diff --git a/en/digging-deeper/package-development.md b/en/digging-deeper/package-development.md index a7d450417..bd39a11a9 100644 --- a/en/digging-deeper/package-development.md +++ b/en/digging-deeper/package-development.md @@ -79,6 +79,39 @@ func main() { } ``` +If the package import path is long or its default package name is not convenient, setup modify helpers support an import alias in the `" "` format. Use the same aliased import string when installing and uninstalling: + +```go +func main() { + setup := packages.Setup(os.Args) + + serviceProvider := "&admin.ServiceProvider{}" + moduleImport := "admin " + setup.Paths().Module().Import() + + setup.Install( + modify.RegisterProvider(moduleImport, serviceProvider), + ).Uninstall( + modify.UnregisterProvider(moduleImport, serviceProvider), + ).Execute() +} +``` + +After installation, the generated import uses the alias and the registered item can reference that alias: + +```go +import ( + admin "github.com/example/goravel-admin" +) + +func Providers() []foundation.ServiceProvider { + return []foundation.ServiceProvider{ + &admin.ServiceProvider{}, + } +} +``` + +Alias import strings are also supported by setup helpers that add and remove routes, middleware, commands, jobs, migrations, rules, filters, seeders, and service providers. + ## Resources ### Configuration diff --git a/en/getting-started/compile.md b/en/getting-started/compile.md index 046268496..c05e8ac72 100644 --- a/en/getting-started/compile.md +++ b/en/getting-started/compile.md @@ -21,8 +21,20 @@ The Goravel project can be compiled with the following command: # Specify the output file name ./artisan build --name=goravel ./artisan build -n=goravel + +# Run go generate before building +./artisan build --generate +./artisan build -g +``` + +The `--generate` flag runs `go generate ./...` before the build step. It is disabled by default and can be combined with other build options: + +```shell +./artisan build --generate --os=linux --arch=amd64 ``` +When `--generate` is combined with target options such as `--os` or `--arch`, the generate step uses the same build environment. + ## Manual compilation ### Regular compilation diff --git a/en/getting-started/configuration.md b/en/getting-started/configuration.md index c7cf7c56b..320e8e676 100644 --- a/en/getting-started/configuration.md +++ b/en/getting-started/configuration.md @@ -71,3 +71,48 @@ You can use the `artisan about` command to view the framework version, configura ```bash ./artisan about ``` + +### Maintenance Mode + +You may use the `down` command to put your application into maintenance mode. The command stores maintenance metadata at the `framework/maintenance.json` storage path: + +```shell +./artisan down +``` + +You may provide a custom reason and HTTP status code for the maintenance response: + +```shell +./artisan down --reason="Upgrading database" --status=503 +``` + +The `down` command also supports redirecting users to a path or rendering a view while the application is in maintenance mode: + +```shell +./artisan down --redirect=/maintenance +./artisan down --render=errors/503 +``` + +When using `--render`, the view must already exist. If `--redirect` or `--render` is provided, the `--reason` response body is not used. + +You may allow temporary access to the application by setting a secret. Users can bypass maintenance mode by visiting the application with the matching `secret` query parameter: + +```shell +./artisan down --secret=let-me-in +``` + +```text +https://example.com?secret=let-me-in +``` + +You can also let Goravel generate a random secret for you: + +```shell +./artisan down --with-secret +``` + +To bring the application out of maintenance mode, run the `up` command: + +```shell +./artisan up +``` diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md index c93719378..156b038ab 100644 --- a/en/orm/getting-started.md +++ b/en/orm/getting-started.md @@ -201,6 +201,37 @@ func (r *User) GlobalScopes() map[string]func(contractsorm.Query) contractsorm.Q } ``` +You can access the context passed by `facades.Orm().WithContext(ctx)` from the query inside a global scope: + +```go +import ( + "context" + + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/orm" + "github.com/goravel/framework/facades" +) + +type User struct { + orm.Model + Name string +} + +func (r *User) GlobalScopes() map[string]func(contractsorm.Query) contractsorm.Query { + return map[string]func(contractsorm.Query) contractsorm.Query{ + "tenant": func(query contractsorm.Query) contractsorm.Query { + tenantID, _ := query.Context().Value("tenant_id").(uint) + + return query.Where("tenant_id", tenantID) + }, + } +} + +ctx := context.WithValue(context.Background(), "tenant_id", uint(1)) +var users []User +err := facades.Orm().WithContext(ctx).Query().Find(&users) +``` + If you want to remove global scopes in a query, you can use the `WithoutGlobalScopes` function: ```go @@ -228,6 +259,7 @@ facades.Orm().Query().WithoutGlobalScopes("name").Get(&users) | Avg | [Avg](#Avarage) | | BeginTransaction | [Begin transaction](#transaction) | | Commit | [Commit transaction](#transaction) | +| Context | [Inject Context](#inject-context) | | Count | [Count](#count) | | Create | [Create](#create) | | Cursor | [Cursor](#cursor) | diff --git a/en/security/authentication.md b/en/security/authentication.md index edf9ea084..080bfd127 100644 --- a/en/security/authentication.md +++ b/en/security/authentication.md @@ -111,11 +111,22 @@ err := facades.Auth(ctx).Logout() ```go token, err := facades.Auth(ctx).Guard("admin").LoginUsingID(1) err := facades.Auth(ctx).Guard("admin").Parse(token) -token, err := facades.Auth(ctx).Guard("admin").User(&user) +err := facades.Auth(ctx).Guard("admin").User(&user) ``` > When the default guard is not used, the `Guard` method must be called before calling the above methods. +JWT tokens are bound to the guard that generated them. If you parse a token with a different guard, Goravel returns `auth.ErrorGuardMismatch`: + +```go +token, err := facades.Auth(ctx).Guard("user").LoginUsingID(1) +payload, err := facades.Auth(ctx).Guard("admin").Parse(token) + +if errors.Is(err, auth.ErrorGuardMismatch) { + // The token was issued by another guard. +} +``` + ## Custom Driver ### Add Custom Guard diff --git a/en/the-basics/validation.md b/en/the-basics/validation.md index c6a4a50ea..aa474a75a 100644 --- a/en/the-basics/validation.md +++ b/en/the-basics/validation.md @@ -50,10 +50,10 @@ Now we are ready to fill in our `Store` method with the logic to validate the ne ```go func (r *PostController) Store(ctx http.Context) { - validator, err := ctx.Request().Validate(map[string]string{ - "title": "required|max_len:255", + validator, err := ctx.Request().Validate(map[string]any{ + "title": "required|max:255", "body": "required", - "code": "required|regex:^\d{4,6}$", + "code": "required|regex:^[0-9]{4,6}$", }) } ``` @@ -63,8 +63,8 @@ func (r *PostController) Store(ctx http.Context) { If the incoming HTTP request contains "nested" field data, you may specify these fields in your validation rules using the "dot" syntax: ```go -validator, err := ctx.Request().Validate(map[string]string{ - "title": "required|max_len:255", +validator, err := ctx.Request().Validate(map[string]any{ + "title": "required|max:255", "author.name": "required", "author.description": "required", }) @@ -75,11 +75,22 @@ validator, err := ctx.Request().Validate(map[string]string{ If the incoming HTTP request contains "array" field data, you may specify these fields in your validation rules using the `*` syntax: ```go -validator, err := ctx.Request().Validate(map[string]string{ +validator, err := ctx.Request().Validate(map[string]any{ "tags.*": "required", }) ``` +Wildcard rules preserve the original slice shape in validated data: + +```go +validator, err := facades.Validation().Make(ctx, + map[string]any{"scores": []int{1, 2}}, + map[string]any{"scores.*": "required|integer"}, +) + +scores := validator.Validated()["scores"].([]int) // []int{1, 2} +``` + ## Form Request Validation ### Creating Form Requests @@ -91,7 +102,7 @@ For more complex validation scenarios, you may wish to create a "form request". ./artisan make:request user/StorePostRequest ``` -The generated form request class will be placed in the `app/http/requests` directory. If this directory does not exist, it will be created when you run the `make:request` command. Each form request generated by Goravel has six methods: `Authorize`, `Rules`. In addition, you can customize the `Filters`, `Messages`, `Attributes` and `PrepareForValidation` methods for further operations. +The generated form request class will be placed in the `app/http/requests` directory. If this directory does not exist, it will be created when you run the `make:request` command. Each form request generated by Goravel has `Authorize` and `Rules` methods. You can also customize the optional `Filters`, `Messages`, `Attributes`, and `PrepareForValidation` methods for further operations. The `Authorize` method is responsible for determining if the currently authenticated user can perform the action represented by the request, while the `Rules` method returns the validation rules that should apply to the request's data: @@ -102,7 +113,6 @@ import ( "mime/multipart" "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" ) type StorePostRequest struct { @@ -115,10 +125,10 @@ func (r *StorePostRequest) Authorize(ctx http.Context) error { return nil } -func (r *StorePostRequest) Rules(ctx http.Context) map[string]string { - return map[string]string{ +func (r *StorePostRequest) Rules(ctx http.Context) map[string]any { + return map[string]any{ // The keys are consistent with the incoming keys. - "name": "required|max_len:255", + "name": "required|max:255", "file": "required|file", "files": "required|array", "files.*": "required|file", @@ -165,12 +175,13 @@ func (r *StorePostRequest) Authorize(ctx http.Context) error { ### Filter Input Data -You can format the input data by improving the `Filters` method of the form request. This method should return an map of `attribute/filter`: +You can format the input data by improving the `Filters` method of the form request. This method should return a map of `attribute/filter` pairs. Filter values may be strings or `[]string` values: ```go -func (r *StorePostRequest) Filters(ctx http.Context) map[string]string { - return map[string]string{ +func (r *StorePostRequest) Filters(ctx http.Context) map[string]any { + return map[string]any{ "name": "trim", + "age": []string{"trim", "to_int"}, } } ``` @@ -215,7 +226,7 @@ func (r *StorePostRequest) PrepareForValidation(ctx http.Context, data validatio ## Manually Creating Validators -If you do not want to use the `Validate` method on the request, you may create a validator instance manually using the `facades.Validator`. The `Make` method of the facade generates a new validator instance: +If you do not want to use the `Validate` method on the request, you may create a validator instance manually using `facades.Validation()`. The `Make` method of the facade generates a new validator instance: ```go func (r *PostController) Store(ctx http.Context) http.Response { @@ -224,8 +235,8 @@ func (r *PostController) Store(ctx http.Context) http.Response { map[string]any{ "name": "Goravel", }, - map[string]string{ - "title": "required|max_len:255", + map[string]any{ + "title": "required|max:255", "body": "required", }) @@ -239,11 +250,11 @@ func (r *PostController) Store(ctx http.Context) http.Response { } ``` -The first argument passed to the `Make` method is the data under validation which can be `map[string]any` or `struct`. The second argument is an array of validation rules to be applied to the data. +After `ctx`, the data argument passed to the `Make` method can be `map[string]any`, `struct`, `url.Values`, `map[string][]string`, `*http.Request`, or another supported request data source. The next argument is a `map[string]any` of validation rules. Rule values may be strings or `[]string` values. ### Customizing The Error Messages -If needed, you may provide custom error messages that a validator instance should use instead of the default error messages provided by Goravel. You may pass the custom messages as the third argument to the `Make` method (also applicable to `ctx.Request().Validate()`): +If needed, you may provide custom error messages that a validator instance should use instead of the default error messages provided by Goravel. You may pass custom messages with `validation.Messages` (also applicable to `ctx.Request().Validate()`): ```go validator, err := facades.Validation().Make(ctx, input, rules, validation.Messages(map[string]string{ @@ -261,9 +272,11 @@ validator, err := facades.Validation().Make(ctx, input, rules, validation.Messag })) ``` +Explicit message overrides have priority over custom rule defaults. Goravel resolves messages in this order: `field.rule` message, then `rule` message, then the custom rule's `Message()` return value. + ### Specifying Custom Attribute Values -Many of Goravel's built-in error messages include an `:attribute` placeholder that is replaced with the name of the field or attribute under validation. To customize the values used to replace these placeholders for specific fields, you may pass an array of custom attributes as the third argument to the `Make` method (also applicable to `ctx.Request().Validate()`): +Many of Goravel's built-in error messages include an `:attribute` placeholder that is replaced with the name of the field or attribute under validation. To customize the values used to replace these placeholders for specific fields, you may pass custom attributes with `validation.Attributes` (also applicable to `ctx.Request().Validate()`): ```go validator, err := facades.Validation().Make(ctx, input, rules, validation.Attributes(map[string]string{ @@ -273,17 +286,19 @@ validator, err := facades.Validation().Make(ctx, input, rules, validation.Attrib ### Format Data Before Validation -You can format the data before validating the data for more flexible data validation, and you can pass the method of formatting the data as the third parameter to the `Make` method (also applicable to `ctx.Request().Validate()`): +You can format the data before validating the data for more flexible data validation, and you can pass the formatting callback with `validation.PrepareForValidation` (also applicable to `ctx.Request().Validate()`): ```go import ( + "context" + validationcontract "github.com/goravel/framework/contracts/validation" "github.com/goravel/framework/validation" ) func (r *PostController) Store(ctx http.Context) http.Response { validator, err := facades.Validation().Make(ctx, input, rules, - validation.PrepareForValidation(func(ctx http.Context, data validationcontract.Data) error { + validation.PrepareForValidation(func(ctx context.Context, data validationcontract.Data) error { if name, exist := data.Get("name"); exist { return data.Set("name", name) } @@ -297,9 +312,21 @@ func (r *PostController) Store(ctx http.Context) http.Response { ## Working With Validated Input -After validating incoming request data using form requests or manually created validator instances, you still want to bind the request data to a `struct`, there are two ways to do this: +After validating incoming request data using form requests or manually created validator instances, you can retrieve the validated data or bind the request data to a `struct`. + +1. After validation passes, use the `Validated` method to retrieve only fields covered by validation rules. Excluded fields are omitted, and wildcard slice rules keep the original slice shape: + +```go +rules := map[string]any{ + "name": "required|string", + "scores.*": "required|integer", +} + +validator, err := ctx.Request().Validate(rules) +validated := validator.Validated() +``` -1. Use the `Bind` method, this will bind all incoming data, including unvalidated data: +2. Use the `Bind` method to bind data to a struct after validation succeeds. `Bind` merges validated data back over the original request data, so fields without rules can still be bound: ```go validator, err := ctx.Request().Validate(rules) @@ -311,7 +338,7 @@ var user models.User err := validator.Bind(&user) ``` -2. The incoming data is automatically bound to the form when you use request for validation: +3. The incoming data is automatically bound to the form request when you use request validation: ```go var storePost requests.StorePostRequest @@ -321,7 +348,7 @@ fmt.Println(storePost.Name) ## Working With Error Messages -### Retrieving one Error Message For A Field (Random) +### Retrieving One Error Message For A Field ```go validator, err := ctx.Request().Validate(rules) @@ -352,79 +379,62 @@ if validator.Errors().Has("email") { ## Available Validation Rules -Below is a list of all available validation rules and their function: - -| Name | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `required` | Check value is required and cannot be zero value. For example, field type is `bool`, the passing value is `false`, it can not pass the validation. | -| `required_if` | `required_if:anotherfield,value,...` The field under validation must be present and not empty if the anotherField field is equal to any value. | -| `required_unless` | `required_unless:anotherfield,value,...` The field under validation must be present and not empty unless the anotherField field is equal to any value. | -| `required_with` | `required_with:foo,bar,...` The field under validation must be present and not empty only if any of the other specified fields are present. | -| `required_with_all` | `required_with_all:foo,bar,...` The field under validation must be present and not empty only if all of the other specified fields are present. | -| `required_without` | `required_without:foo,bar,...` The field under validation must be present and not empty only when any of the other specified fields are not present. | -| `required_without_all` | `required_without_all:foo,bar,...` The field under validation must be present and not empty only when all of the other specified fields are not present. | -| `int` | Check value is `intX` `uintX` type, and support size checking. eg: `int` `int:2` `int:2,12`. Notice: [Points for using rules](#int) | -| `uint` | Check value is `uint(uintX)` type, `value >= 0` | -| `bool` | Check value is bool string(`true`: "1", "on", "yes", "true", `false`: "0", "off", "no", "false"). | -| `string` | Check value is string type, and support size checking. eg:`string` `string:2` `string:2,12` | -| `float` | Check value is `float(floatX)` type | -| `slice` | Check value is slice type(`[]intX` `[]uintX` `[]byte` `[]string`) | -| `in` | `in:foo,bar,…` Check if the value is in the given enumeration | -| `not_in` | `not_in:foo,bar,…` Check if the value is not in the given enumeration | -| `starts_with` | `starts_with:foo` Check if the input string value is starts with the given sub-string | -| `ends_with` | `ends_with:foo` Check if the input string value is ends with the given sub-string | -| `between` | `between:min,max` Check that the value is a number and is within the given range | -| `max` | `max:value` Check value is less than or equal to the given value(`intX` `uintX` `floatX`) | -| `min` | `min:value` Check value is greater than or equal to the given value(`intX` `uintX` `floatX`) | -| `eq` | `eq:value` Check that the input value is equal to the given value | -| `ne` | `ne:value` Check that the input value is not equal to the given value | -| `lt` | `lt:value` Check value is less than the given value(`intX` `uintX` `floatX`) | -| `gt` | `gt:value` Check value is greater than the given value(`intX` `uintX` `floatX`) | -| `len` | `len:value` Check value length is equals to the given size(`string` `array` `slice` `map`) | -| `min_len` | `min_len:value` Check the minimum length of the value is the given size(`string` `array` `slice` `map`) | -| `max_len` | `max_len:value` Check the maximum length of the value is the given size(`string` `array` `slice` `map`) | -| `email` | Check value is email address string | -| `array` | Check value is array, slice type | -| `map` | Check value is a MAP type | -| `eq_field` | `eq_field:field` Check that the field value is equals to the value of another field | -| `ne_field` | `ne_field:field` Check that the field value is not equals to the value of another field | -| `gt_field` | `gt_field:field` Check that the field value is greater than the value of another field | -| `gte_field` | `gte_field:field` Check that the field value is greater than or equal to the value of another field | -| `lt_field` | `lt_field:field` Check that the field value is less than the value of another field | -| `lte_field` | `lte_field:field` Check if the field value is less than or equal to the value of another field | -| `file` | Verify if it is an uploaded file | -| `image` | Check if it is an uploaded image file and support suffix check | -| `date` | Check the field value is date string | -| `gt_date` | `gt_date:value` Check that the input value is greater than the given date string | -| `lt_date` | `lt_date:value` Check that the input value is less than the given date string | -| `gte_date` | `gte_date:value` Check that the input value is greater than or equal to the given date string | -| `lte_date` | `lte_date:value` Check that the input value is less than or equal to the given date string | -| `alpha` | Verify that the value contains only alphabetic characters | -| `alpha_num` | Check that only letters, numbers are included | -| `alpha_dash` | Check to include only letters, numbers, dashes ( - ), and underscores ( \_ ) | -| `json` | Check value is JSON string | -| `number` | Check value is number string `>= 0` | -| `full_url` | Check value is full URL string(must start with http,https) | -| `ip` | Check value is IP(v4 or v6) string | -| `ipv4` | Check value is IPv4 string | -| `ipv6` | Check value is IPv6 string | -| `regex` | Check if the value can pass the regular verification | -| `uuid` | Check value is UUID string | -| `uuid3` | Check value is UUID3 string | -| `uuid4` | Check value is UUID4 string | -| `uuid5` | Check value is UUID5 string | - -### Points For Using Rules - -#### int - -When using `ctx.Request().Validate(rules)` for validation, the incoming `int` type data will be parsed by `json.Unmarshal` into `float64` type, which will cause the int rule validation to fail. - -**Solutions** - -Option 1: Add [`validation.PrepareForValidation`](#Format-Data-Before-Validation), format the data before validating the data; - -Option 2: Use `facades.Validation().Make()` for rule validation; +Validation rule names now use snake_case by default. Rules and filters are defined with `map[string]any`; each value must be either a pipe-separated `string` or a `[]string` of rule names. + +```go +rules := map[string]any{ + "title": "required|string|max:255", + "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, +} +``` + +Use `[]string` when a `regex` or `not_regex` pattern contains `|` and you need to add more rules after it. + +| Category | Rules | +| --- | --- | +| Required | `required`, `required_if`, `required_unless`, `required_with`, `required_with_all`, `required_without`, `required_without_all`, `required_if_accepted`, `required_if_declined` | +| Presence | `filled`, `present`, `present_if`, `present_unless`, `present_with`, `present_with_all`, `missing`, `missing_if`, `missing_unless`, `missing_with`, `missing_with_all` | +| Accepted / Declined | `accepted`, `accepted_if`, `declined`, `declined_if` | +| Prohibited | `prohibited`, `prohibited_if`, `prohibited_unless`, `prohibited_if_accepted`, `prohibited_if_declined`, `prohibits` | +| Type | `string`, `integer`, `int`, `uint`, `numeric`, `boolean`, `bool`, `float`, `array`, `list`, `slice`, `map` | +| Size | `size`, `min`, `max`, `between`, `gt`, `gte`, `lt`, `lte` | +| Numeric | `digits`, `digits_between`, `decimal`, `multiple_of`, `min_digits`, `max_digits` | +| String Format | `alpha`, `alpha_num`, `alpha_dash`, `ascii`, `email`, `url`, `active_url`, `ip`, `ipv4`, `ipv6`, `mac_address`, `mac`, `json`, `uuid`, `uuid3`, `uuid4`, `uuid5`, `ulid`, `hex_color`, `regex`, `not_regex`, `lowercase`, `uppercase` | +| String Content | `starts_with`, `doesnt_start_with`, `ends_with`, `doesnt_end_with`, `contains`, `doesnt_contain`, `confirmed` | +| Comparison | `same`, `different`, `eq`, `ne`, `in`, `not_in`, `in_array`, `in_array_keys` | +| Date | `date`, `date_format`, `date_equals`, `before`, `before_or_equal`, `after`, `after_or_equal`, `timezone` | +| Exclusion | `exclude`, `exclude_if`, `exclude_unless`, `exclude_with`, `exclude_without` | +| File | `file`, `image`, `mimes`, `mimetypes`, `extensions`, `dimensions`, `encoding` | +| Control | `bail`, `nullable`, `sometimes` | +| Array / Database | `distinct`, `required_array_keys`, `exists`, `unique` | + +The `size`, `min`, `max`, `between`, `gt`, `gte`, `lt`, and `lte` rules are type-aware. They compare numeric values for numeric fields, string length for strings, element count for arrays, slices, and maps, and file size for files. + +The `exists` rule uses `exists:table,column1,column2,...`. The `unique` rule uses `unique:table,column,idColumn,except1,except2,...`. Both rules support `connection.table` syntax for the table parameter. + +The `active_url` rule performs a DNS lookup for the URL host. Use it carefully on request hot paths because DNS resolution can add latency. + +### Deprecated Rule Aliases + +The following aliases remain backward-compatible, but will be removed in the next major version. Prefer the new snake_case names: + +| Deprecated | Use Instead | +| --- | --- | +| `len` | `size` | +| `min_len` | `min` | +| `max_len` | `max` | +| `eq_field` | `same` | +| `ne_field` | `different` | +| `gt_field` | `gt` | +| `gte_field` | `gte` | +| `lt_field` | `lt` | +| `lte_field` | `lte` | +| `gt_date` | `after` | +| `lt_date` | `before` | +| `gte_date` | `after_or_equal` | +| `lte_date` | `before_or_equal` | +| `number` | `numeric` | +| `full_url` | `url` | ## Custom Validation Rules @@ -447,6 +457,7 @@ After creating the rule, we need to define its behavior. A rule object has two m package rules import ( + "context" "strings" "github.com/goravel/framework/contracts/validation" @@ -486,37 +497,63 @@ func Boot() contractsfoundation.Application { ## Available Validation Filters -| Name | Description | -| ------------------------------ | ---------------------------------------------------------------------------------------------------- | -| `int/toInt` | Convert value(string/intX/floatX) to `int` type `v.FilterRule("id", "int")` | -| `uint/toUint` | Convert value(string/intX/floatX) to `uint` type `v.FilterRule("id", "uint")` | -| `int64/toInt64` | Convert value(string/intX/floatX) to `int64` type `v.FilterRule("id", "int64")` | -| `float/toFloat` | Convert value(string/intX/floatX) to `float` type | -| `bool/toBool` | Convert string value to bool. (`true`: "1", "on", "yes", "true", `false`: "0", "off", "no", "false") | -| `trim/trimSpace` | Clean up whitespace characters on both sides of the string | -| `ltrim/trimLeft` | Clean up whitespace characters on left sides of the string | -| `rtrim/trimRight` | Clean up whitespace characters on right sides of the string | -| `int/integer` | Convert value(string/intX/floatX) to `int` type `v.FilterRule("id", "int")` | -| `lower/lowercase` | Convert string to lowercase | -| `upper/uppercase` | Convert string to uppercase | -| `lcFirst/lowerFirst` | Convert the first character of a string to lowercase | -| `ucFirst/upperFirst` | Convert the first character of a string to uppercase | -| `ucWord/upperWord` | Convert the first character of each word to uppercase | -| `camel/camelCase` | Convert string to camel naming style | -| `snake/snakeCase` | Convert string to snake naming style | -| `escapeJs/escapeJS` | Escape JS string. | -| `escapeHtml/escapeHTML` | Escape HTML string. | -| `str2ints/strToInts` | Convert string to int slice `[]int` | -| `str2time/strToTime` | Convert date string to `time.Time`. | -| `str2arr/str2array/strToArray` | Convert string to string slice `[]string` | - -## Custom filter +Validation filters use snake_case names by default. Filters run before validation and can be declared on form requests or with `validation.Filters` when manually creating validators. + +| Category | Filters | +| --- | --- | +| String Cleaning | `trim`, `ltrim`, `rtrim` | +| Case Conversion | `lower`, `upper`, `title`, `ucfirst`, `lcfirst` | +| Naming Style | `camel`, `snake` | +| Type Conversion | `to_int`, `to_int64`, `to_uint`, `to_float`, `to_bool`, `to_string`, `to_time` | +| Short Type Aliases | `int`, `int64`, `uint`, `float`, `bool` | +| Escaping / Encoding | `strip_tags`, `escape_js`, `escape_html`, `url_encode`, `url_decode` | +| String Splitting | `str_to_ints`, `str_to_array`, `str_to_time` | + +```go +validator, err := facades.Validation().Make(ctx, input, rules, validation.Filters(map[string]any{ + "name": "trim", + "age": []string{"trim", "to_int"}, +})) +``` + +### Deprecated Filter Aliases + +The following aliases remain backward-compatible, but will be removed in the next major version. Prefer the new snake_case names: + +| Deprecated | Use Instead | +| --- | --- | +| `trimSpace` | `trim` | +| `trimLeft` | `ltrim` | +| `trimRight` | `rtrim` | +| `lowercase` | `lower` | +| `uppercase` | `upper` | +| `lcFirst`, `lowerFirst` | `lcfirst` | +| `ucFirst`, `upperFirst` | `ucfirst` | +| `ucWord`, `upperWord` | `title` | +| `camelCase` | `camel` | +| `snakeCase` | `snake` | +| `toInt`, `integer` | `to_int` | +| `toUint` | `to_uint` | +| `toInt64` | `to_int64` | +| `toFloat` | `to_float` | +| `toBool` | `to_bool` | +| `toString` | `to_string` | +| `toTime`, `str2time`, `strToTime` | `to_time` or `str_to_time` | +| `escapeJs`, `escapeJS` | `escape_js` | +| `escapeHtml`, `escapeHTML` | `escape_html` | +| `urlEncode` | `url_encode` | +| `urlDecode` | `url_decode` | +| `stripTags` | `strip_tags` | +| `str2ints`, `strToInts` | `str_to_ints` | +| `str2arr`, `str2array`, `strToArray` | `str_to_array` | + +## Custom Filters Goravel provides a variety of helpful filters, however, you may wish to specify some of your own. ### Creating Custom Filters -To generate a new rule object, you can simply use the `make:filter` Artisan command. Let's use this command to generate a rule that converts a string to an integer. This rule is already built into the framework, we just create it as an example. Goravel will save this new filter in the `app/filters` directory. If this directory does not exist, Goravel will create it when you run the Artisan command to create the rule: +To generate a new filter object, you can simply use the `make:filter` Artisan command. Let's use this command to generate a filter that converts a string to an integer. This filter is already built into the framework, we just create it as an example. Goravel will save this new filter in the `app/filters` directory. If this directory does not exist, Goravel will create it when you run the Artisan command to create the filter: ```go ./artisan make:filter ToInt @@ -531,10 +568,9 @@ One filter contains two methods: `Signature` and `Handle`. The `Signature` metho package filters import ( - "strings" + "context" "github.com/spf13/cast" - "github.com/goravel/framework/contracts/validation" ) type ToInt struct { @@ -542,20 +578,20 @@ type ToInt struct { // Signature The signature of the filter. func (receiver *ToInt) Signature() string { - return "ToInt" + return "to_int_custom" } // Handle defines the filter function to apply. func (receiver *ToInt) Handle(ctx context.Context) any { return func (val any) int { - return cast.ToString(val) + return cast.ToInt(val) } } ``` ### Register Custom Filters -A new rule created by `make:filter` will be registered automatically in the `bootstrap/filters.go::Filters()` function and the function will be called by `WithFilters`. You need register the rule manually if you create the rule file by yourself. +A new filter created by `make:filter` will be registered automatically in the `bootstrap/filters.go::Filters()` function and the function will be called by `WithFilters`. You need register the filter manually if you create the filter file by yourself. ```go func Boot() contractsfoundation.Application { diff --git a/en/upgrade/history.md b/en/upgrade/history.md index 920e4b67f..6138c1c5e 100644 --- a/en/upgrade/history.md +++ b/en/upgrade/history.md @@ -1,5 +1,6 @@ # History Upgrade +- [Upgrading To v1.16 From v1.15](v1.16.md) - [Upgrading To v1.15 From v1.14](v1.15.md) - [Upgrading To v1.14 From v1.13](v1.14.md) - [Upgrading To v1.13 From v1.12](v1.13.md) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md new file mode 100644 index 000000000..3962dbab5 --- /dev/null +++ b/en/upgrade/v1.18.md @@ -0,0 +1,167 @@ +# Upgrading To v1.18 From v1.17 + +## Enhancements 🚀 + +- [Laravel-style collections](#laravel-style-collections) +- [Native validation engine and built-in rules](#native-validation-engine-and-built-in-rules) +- [Maintenance mode commands](#maintenance-mode-commands) +- [Build command supports go generate](#build-command-supports-go-generate) +- [Orm query exposes context](#orm-query-exposes-context) +- [Package setup supports import aliases](#package-setup-supports-import-aliases) +- [Artisan console supports table output](#artisan-console-supports-table-output) + +## Upgrade Guide + +### 1. Update dependencies + +```shell +go get github.com/goravel/framework@latest +go mod tidy + +// If using installer +goravel upgrade +``` + +### 2. Update validation rule and filter maps + +Validation rules and filters now use `map[string]any` instead of `map[string]string`. Update request validation calls, manual validator calls, and form request `Rules` / `Filters` methods: + +```diff +-ctx.Request().Validate(map[string]string{ ++ctx.Request().Validate(map[string]any{ + "title": "required|max:255", + }) +``` + +Rule and filter values may be either pipe-separated strings or `[]string`. Prefer `[]string` when a `regex` or `not_regex` pattern contains `|` and must be followed by more rules: + +```go +rules := map[string]any{ + "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, +} +``` + +## Feature Introduction + +### Native validation engine and built-in rules + +Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set. + +```go +validator, err := facades.Validation().Make(ctx, + map[string]any{ + "email": " User@Goravel.dev ", + "scores": []int{1, 2}, + }, + map[string]any{ + "email": "required|email", + "scores.*": "required|integer", + }, + validation.Filters(map[string]any{ + "email": "trim|lower", + }), +) + +validated := validator.Validated() +scores := validated["scores"].([]int) +``` + +[View Document](../the-basics/validation.md) + +### Laravel-style collections + +Goravel now provides fluent eager and lazy collection utilities in the `support/collect` package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style `Where` comparisons for structs, aggregate values, and process large datasets with `LazyCollection`. + +```go +numbers := collect.New(1, 2, 3, 4, 5, 6) + +evens := numbers.Filter(func(n int, _ int) bool { + return n%2 == 0 +}) + +evens.All() // []int{2, 4, 6} +``` + +[View Document](../digging-deeper/collections.md) + +### Maintenance mode commands + +Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `down` command writes maintenance metadata to storage, and the `up` command removes it. + +```shell +./artisan down +./artisan up +``` + +The `down` command supports response customization with options such as `--reason`, `--status`, `--redirect`, `--render`, `--secret`, and `--with-secret`. + +```shell +./artisan down --reason="Upgrading database" --status=503 +./artisan down --redirect=/maintenance +./artisan down --render=errors/503 +./artisan down --with-secret +``` + +[View Document](../getting-started/configuration.md#maintenance-mode) + +### Build command supports go generate + +The `build` command now supports the `--generate` flag and `-g` alias. When enabled, Goravel runs `go generate ./...` before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior. + +```shell +./artisan build --generate +./artisan build -g +``` + +[View Document](../getting-started/compile.md#compile-command) + +### Orm query exposes context + +Orm queries now provide a `Context()` accessor. Code that receives a `contractsorm.Query`, such as model global scopes, can read the context passed by `facades.Orm().WithContext(ctx)`. + +```go +"tenant": func(query contractsorm.Query) contractsorm.Query { + tenantID, _ := query.Context().Value("tenant_id").(uint) + + return query.Where("tenant_id", tenantID) +} +``` + +[View Document](../orm/getting-started.md#setting-global-scope) + +### Package setup supports import aliases + +Package setup modify helpers now support import aliases in the `" "` format. This keeps generated bootstrap files readable when package import paths are long or when the package should be referenced with a custom name. + +```go +serviceProvider := "&admin.ServiceProvider{}" +moduleImport := "admin " + setup.Paths().Module().Import() + +setup.Install( + modify.RegisterProvider(moduleImport, serviceProvider), +).Uninstall( + modify.UnregisterProvider(moduleImport, serviceProvider), +).Execute() +``` + +[View Document](../digging-deeper/package-development.md#install-the-package) + +### Artisan console supports table output + +Goravel now provides the `ctx.Table` method for rendering structured command output. It accepts table headers, table rows, and an optional `console.TableOption` for border, style, column, width, and height customization. + +```go +func (receiver *ReportCommand) Handle(ctx console.Context) error { + headers := []string{"ID", "Name", "Status"} + rows := [][]string{ + {"1", "Goravel", "Active"}, + {"2", "Framework", "Ready"}, + } + + ctx.Table(headers, rows) + + return nil +} +``` + +[View Document](../digging-deeper/artisan-console.md#tables) From 607ccfc89bcd3c1992f99b171e21683d73b4207f Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 9 Jun 2026 16:33:39 +0800 Subject: [PATCH 02/35] optimize --- en/prologue/contributions.md | 1 - en/the-basics/grpc.md | 69 ++++++++++++++++++++++++++++++++++++ en/the-basics/logging.md | 16 +++++++++ en/upgrade/v1.18.md | 51 ++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) diff --git a/en/prologue/contributions.md b/en/prologue/contributions.md index 210a3f161..86cb76f9f 100644 --- a/en/prologue/contributions.md +++ b/en/prologue/contributions.md @@ -135,7 +135,6 @@ You can find or create an issue in [Issue List](https://github.com/goravel/gorav | [goravel/mysql](https://github.com/goravel/mysql) | The MySQL driver of Database module | | [goravel/sqlserver](https://github.com/goravel/sqlserver) | The SQLServer driver of Database module | | [goravel/sqlite](https://github.com/goravel/sqlite) | The SQLite driver of Database module | -| [goravel/file-rotatelogs](https://github.com/goravel/file-rotatelogs) | Providers log splitting functionality for Log module | | [goravel/.github](https://github.com/goravel/.github) | [Community health file](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file) | ## Code of Conduct diff --git a/en/the-basics/grpc.md b/en/the-basics/grpc.md index a7f996cee..7a79f90ce 100644 --- a/en/the-basics/grpc.md +++ b/en/the-basics/grpc.md @@ -10,6 +10,75 @@ Grpc module can be operated by `facades.Grpc()`. Goravel provides an elegant way In the `config/grpc.go` file, you can configure the Grpc module, where `grpc.host` configures the domain name of the server, and `grpc.servers` configures the servers which the client will connect to. +## Transport Credentials + +By default, gRPC clients use insecure transport credentials and the gRPC server listens without TLS. You can register `credentials.TransportCredentials` during application bootstrap when your services require TLS or mTLS. + +### Server Credentials + +Register server credentials before the gRPC server is created: + +```go +import "google.golang.org/grpc/credentials" + +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithGrpcServerCredentials(func() credentials.TransportCredentials { + creds, err := credentials.NewServerTLSFromFile( + "storage/certs/server.crt", + "storage/certs/server.key", + ) + if err != nil { + panic(err) + } + + return creds + }). + Create() +} +``` + +### Client Credentials + +Register client credential groups in `bootstrap/app.go`, then reference the group name in `config/grpc.go` with `grpc.servers..credentials`: + +```go +import "google.golang.org/grpc/credentials" + +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials { + userCreds, err := credentials.NewClientTLSFromFile( + "storage/certs/ca.crt", + "user.example.com", + ) + if err != nil { + panic(err) + } + + return map[string]credentials.TransportCredentials{ + "user": userCreds, + } + }). + Create() +} +``` + +```go +// config/grpc.go +"servers": map[string]any{ + "user": map[string]any{ + "host": config.Env("GRPC_USER_HOST", ""), + "port": config.Env("GRPC_USER_PORT", ""), + "credentials": "user", + "interceptors": []string{}, + "stats_handlers": []string{}, + }, +}, +``` + +If a client has no `credentials` value, Goravel uses insecure credentials for backward compatibility. If the configured group is not registered, Goravel logs a warning and falls back to insecure credentials. For mTLS, return credentials created with `credentials.NewTLS` and a `tls.Config` that includes the required client certificates or trusted CA pools. + ## Controllers Controllers can be defined in the `app/grpc/controllers` directory. diff --git a/en/the-basics/logging.md b/en/the-basics/logging.md index e57224e92..e56ab4b6c 100644 --- a/en/the-basics/logging.md +++ b/en/the-basics/logging.md @@ -29,6 +29,22 @@ The `print` configuration in `single` and `daily` drivers can control log output facades.Log().WithContext(ctx) ``` +When `WithContext` is used, Goravel writes context key-value pairs to the log output. Framework-internal keys such as `GoravelAuthJwt` and `goravel_http_client_name` are excluded automatically to avoid leaking sensitive or noisy values. + +You can exclude additional context keys by adding `context.exclude` to `config/logging.go`: + +```go +// config/logging.go +"context": map[string]any{ + "exclude": []any{ + "access_token", + "secret_key", + }, +}, +``` + +String entries match string context keys and typed string context keys by their displayed name. Comparable non-string entries match the exact context key value, which is useful for struct sentinel keys. Uncomparable entries, such as slices or maps, are ignored. + ## Write log messages ```go diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 3962dbab5..b838977db 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -6,9 +6,11 @@ - [Native validation engine and built-in rules](#native-validation-engine-and-built-in-rules) - [Maintenance mode commands](#maintenance-mode-commands) - [Build command supports go generate](#build-command-supports-go-generate) +- [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) - [Orm query exposes context](#orm-query-exposes-context) - [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Artisan console supports table output](#artisan-console-supports-table-output) +- [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) ## Upgrade Guide @@ -16,6 +18,28 @@ ```shell go get github.com/goravel/framework@latest + +// If using gin +go get github.com/goravel/gin@latest + +// If using fiber +go get github.com/goravel/fiber@latest + +// If using redis +go get github.com/goravel/redis@latest + +// If using S3 +go get github.com/goravel/s3@latest + +// If using Oss +go get github.com/goravel/oss@latest + +// If using Cos +go get github.com/goravel/cos@latest + +// If using Minio +go get github.com/goravel/minio@latest + go mod tidy // If using installer @@ -115,6 +139,27 @@ The `build` command now supports the `--generate` flag and `-g` alias. When enab [View Document](../getting-started/compile.md#compile-command) +### gRPC supports TLS and mTLS credentials + +Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use `WithGrpcServerCredentials` to enable TLS or mTLS for the server, and `WithGrpcClientCredentials` to register client credential groups that can be referenced by `grpc.servers..credentials`. + +```go +return foundation.Setup(). + WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials { + userCreds, err := credentials.NewClientTLSFromFile("storage/certs/ca.crt", "user.example.com") + if err != nil { + panic(err) + } + + return map[string]credentials.TransportCredentials{ + "user": userCreds, + } + }). + Create() +``` + +[View Document](../the-basics/grpc.md#transport-credentials) + ### Orm query exposes context Orm queries now provide a `Context()` accessor. Code that receives a `contractsorm.Query`, such as model global scopes, can read the context passed by `facades.Orm().WithContext(ctx)`. @@ -165,3 +210,9 @@ func (receiver *ReportCommand) Handle(ctx console.Context) error { ``` [View Document](../digging-deeper/artisan-console.md#tables) + +### Log WithContext filters framework context keys out + +Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. + +[View Document](../the-basics/logging.md#inject-context) From 98e621573304677bee31edee42a900cb33260c1a Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 10 Jun 2026 16:21:45 +0800 Subject: [PATCH 03/35] optimize --- en/ai/sdk.md | 875 ++++++++++++++++++++++++++++ en/getting-started/configuration.md | 11 +- en/upgrade/v1.18.md | 41 +- 3 files changed, 925 insertions(+), 2 deletions(-) create mode 100644 en/ai/sdk.md diff --git a/en/ai/sdk.md b/en/ai/sdk.md new file mode 100644 index 000000000..bd713e881 --- /dev/null +++ b/en/ai/sdk.md @@ -0,0 +1,875 @@ +# AI SDK + +[[toc]] + +## Introduction + +The AI SDK provides a unified API for interacting with AI providers in Goravel applications. It introduces an `AI` facade, stateful conversations, agent classes, provider/model options, prompt attachments, streaming responses, image generation, audio generation, and transcription. + +The core AI module manages conversations and provider resolution. Provider implementations are installed separately, such as `goravel/openai`, `goravel/anthropic`, and `goravel/gemini`. + +## Installation + +### Install AI Facade + +Install the AI facade and core service provider with the `package:install` command: + +```shell +./artisan package:install ai +``` + +This makes `facades.AI()` available and registers the `make:agent` Artisan command. + +### Install Providers + +Install at least one provider package before prompting an agent: + +```shell +./artisan package:install github.com/goravel/openai +./artisan package:install github.com/goravel/anthropic +./artisan package:install github.com/goravel/gemini +``` + +Each provider package registers its own service provider and updates `config/ai.go` so `ai.providers..via` resolves through the package facade. + +### Provider Configuration + +The provider installers update `config/ai.go` automatically. For example, `goravel/openai` adds an OpenAI provider similar to this: + +```go +package config + +import ( + "github.com/goravel/framework/contracts/ai" + openaifacades "github.com/goravel/openai/facades" + "goravel/app/facades" +) + +func init() { + config := facades.Config() + config.Add("ai", map[string]any{ + "default": "openai", + "providers": map[string]any{ + "openai": map[string]any{ + "key": config.Env("OPENAI_API_KEY", ""), + "url": config.Env("OPENAI_BASE_URL", ""), + "via": func() (ai.Provider, error) { + return openaifacades.OpenAI("openai") + }, + "models": map[string]any{ + "text": map[string]any{ + "default": "", + "max_tokens": 0, + }, + "audio": map[string]any{ + "default": "", + }, + "transcription": map[string]any{ + "default": "", + }, + "image": map[string]any{ + "default": "", + }, + }, + }, + }, + }) +} +``` + +Then add the provider credentials to `.env`: + +```ini +OPENAI_API_KEY= +OPENAI_BASE_URL= +``` + +`OPENAI_BASE_URL` is optional. Use it when routing requests through a proxy or an OpenAI-compatible endpoint. If a model default is empty, the provider package uses its own default model. Set `models.text.max_tokens` to limit generated text tokens; leave it as `0` to use the provider default. + +## Creating Agents + +Agents define the system instructions and any initial conversation context that should be sent to the provider. + +You can generate an agent with Artisan: + +```shell +./artisan make:agent SupportAgent +./artisan make:agent user/SupportAgent +``` + +The generated file is placed under `app/agents` and contains the required methods: + +```go +package agents + +import "github.com/goravel/framework/contracts/ai" + +type SupportAgent struct { +} + +func (r *SupportAgent) Instructions() string { + return "You are a concise support assistant for a Goravel application." +} + +func (r *SupportAgent) Messages() []ai.Message { + return []ai.Message{ + {Role: ai.RoleAssistant, Content: "Ask clarifying questions when needed."}, + } +} + +func (r *SupportAgent) Middleware() []ai.Middleware { + return nil +} + +func (r *SupportAgent) Tools() []ai.Tool { + return nil +} +``` + +`Instructions` becomes the system prompt. `Messages` returns the initial conversation history copied into each new conversation. `Middleware` returns the default prompt middleware applied to each new conversation. `Tools` returns the callable tools the model may invoke. + +## Prompting + +Use `facades.AI().Agent` to create a conversation for an agent, then call `Prompt`: + +```go +conversation, err := facades.AI().Agent(&agents.SupportAgent{}) +if err != nil { + return err +} + +response, err := conversation.Prompt("How do I create a controller?") +if err != nil { + return err +} + +fmt.Println(response.Text()) +``` + +The response exposes generated text, usage metadata, and any tool calls returned by the provider: + +```go +text := response.Text() +usage := response.Usage() +toolCalls := response.ToolCalls() + +fmt.Println(text) +fmt.Println(usage.Input(), usage.Output(), usage.Total()) +fmt.Println(toolCalls) +``` + +Use `Then` to run a callback after a response is resolved: + +```go +import ( + "github.com/goravel/framework/contracts/ai" + "goravel/app/facades" +) + +response.Then(func(response ai.AgentResponse) { + facades.Log().Info(response.Text()) +}) +``` + +You can override the configured provider or model for a single conversation: + +```go +conversation, err := facades.AI().Agent( + &agents.SupportAgent{}, + frameworkai.WithProvider("openai"), + frameworkai.WithModel("gpt-5.4"), +) +``` + +If the request should use a specific Go context, call `WithContext` before creating the conversation: + +```go +conversation, err := facades.AI().WithContext(ctx).Agent(&agents.SupportAgent{}) +``` + +## Conversation History + +A conversation stores runtime messages in memory. After a successful `Prompt`, Goravel appends the user input and assistant response to the conversation history: + +```go +conversation, err := facades.AI().Agent(&agents.SupportAgent{}) +if err != nil { + return err +} + +_, err = conversation.Prompt("Hello") +if err != nil { + return err +} + +messages := conversation.Messages() +``` + +Use `Reset` to discard runtime messages and restore the initial messages returned by the agent: + +```go +conversation.Reset() +``` + +## Attachments + +Attachments let you send request-scoped documents and images with a single `Prompt` or `Stream` call. Goravel resolves attachments lazily from common sources and does not persist the binary content in conversation history. + +The attachment examples use these imports: + +```go +import ( + "fmt" + + frameworkai "github.com/goravel/framework/ai" + "github.com/goravel/framework/ai/document" + "github.com/goravel/framework/ai/image" +) +``` + +Attach files with `WithAttachments`: + +```go +response, err := conversation.Prompt("Summarize these files", frameworkai.WithAttachments( + document.FromPath("storage/app/reports/quarterly.pdf"), + image.FromPath("storage/app/charts/revenue.png", image.WithMimeType("image/png")), +)) +if err != nil { + return err +} + +fmt.Println(response.Text()) +``` + +The helper packages are available from `github.com/goravel/framework/ai/document` and `github.com/goravel/framework/ai/image`. The same constructors are also available from the root `github.com/goravel/framework/ai` package as `DocumentFromPath`, `ImageFromPath`, and related helpers. + +Supported attachment sources: + +| Source | Document Helper | Image Helper | +| --- | --- | --- | +| Bytes | `document.FromByte` | `image.FromByte` | +| String | `document.FromString` | - | +| Base64 | `document.FromBase64` | `image.FromBase64` | +| Reader | `document.FromReader` | `image.FromReader` | +| Local path | `document.FromPath` | `image.FromPath` | +| Storage | `document.FromStorage` | `image.FromStorage` | +| URL | `document.FromURL` | `image.FromURL` | +| Uploaded file | `document.FromUpload` | `image.FromUpload` | +| Provider file ID | `document.FromID` | `image.FromID` | + +Use `WithMimeType` to override the detected MIME type. Use `WithDisk` with `FromStorage` when the attachment should be read from a non-default filesystem disk: + +```go +attachment := document.FromStorage( + "reports/monthly.pdf", + document.WithDisk("s3"), + document.WithMimeType("application/pdf"), +) +``` + +`Stream` accepts the same attachment option: + +```go +stream, err := conversation.Stream("Describe this chart", frameworkai.WithAttachments( + image.FromPath("storage/app/charts/revenue.png"), +)) +``` + +### Uploading Attachments + +If a provider supports file uploads, call `Put` on an attachment to upload it and receive a provider-managed file handle: + +```go +file, err := document.FromPath("storage/app/reports/quarterly.pdf").Put( + ctx, + frameworkai.WithProvider("openai"), +) +if err != nil { + return err +} + +fmt.Println(file.ID()) +``` + +Providers that do not support uploads return an explicit error. The OpenAI provider supports uploads and uses the Responses API for prompts, streaming, tool calling, and attachments. + +You may attach a provider-managed file by ID without uploading it again: + +```go +file := document.FromID("file-abc123") + +response, err := conversation.Prompt("Summarize this file", frameworkai.WithAttachments(file)) +if err != nil { + return err +} +``` + +Use `Get` to resolve file metadata or content from the provider, and `Delete` to remove the provider-managed file: + +```go +file := document.FromID("file-abc123") + +resolved, err := file.Get(ctx, frameworkai.WithProvider("openai")) +if err != nil { + return err +} + +content, err := resolved.Content(ctx) +if err != nil { + return err +} + +fmt.Println(resolved.ID(), resolved.MimeType(), len(content)) + +err = file.Delete(ctx, frameworkai.WithProvider("openai")) +``` + +Use `image.FromID` for provider-managed image files. + +## Image Generation + +Use `facades.AI().Image` to generate images from a prompt: + +```go +import ( + "fmt" + + frameworkai "github.com/goravel/framework/ai" + "goravel/app/facades" +) + +response, err := facades.AI().Image("A friendly gopher writing Goravel docs"). + Square(). + Quality(frameworkai.ImageQualityHigh). + Generate() +if err != nil { + return err +} + +content, err := response.Content() +if err != nil { + return err +} + +fmt.Println(response.MimeType(), len(content)) +``` + +You may set the provider and model on the fluent image request: + +```go +response, err := facades.AI().Image("A launch banner for Goravel v1.18"). + Provider("openai"). + Model("gpt-image-2"). + Landscape(). + Generate() +``` + +Use image attachments when editing existing images: + +```go +import ( + frameworkai "github.com/goravel/framework/ai" + "github.com/goravel/framework/ai/image" + "goravel/app/facades" +) + +response, err := facades.AI().Image("Turn this chart into a watercolor illustration"). + Attachments(image.FromPath("storage/app/charts/revenue.png", image.WithMimeType("image/png"))). + Quality(frameworkai.ImageQualityMedium). + Generate() +``` + +The generated image response exposes bytes, MIME type, usage metadata, storage helpers, and a `Then` callback: + +```go +import ( + "github.com/goravel/framework/contracts/ai" + "goravel/app/facades" +) + +response.Then(func(response ai.ImageResponse) { + facades.Log().Info(response.MimeType()) +}) +``` + +Store generated images directly on the configured filesystem: + +```go +path, err := response.Store("public") +if err != nil { + return err +} + +path, err = response.StoreAs("images/gopher.png", "public") +``` + +You may also generate and store in one step: + +```go +path, err := facades.AI().Image("A Goravel mascot"). + Portrait(). + StoreAs("images/mascot.png", "public") +``` + +## Audio Generation + +Use `facades.AI().Audio` to generate speech from text: + +```go +import ( + "fmt" + "time" + + "goravel/app/facades" +) + +response, err := facades.AI().Audio("Welcome to Goravel"). + Provider("openai"). + Model("gpt-4o-mini-tts"). + Male(). + Instructions("Speak slowly and warmly."). + Timeout(30 * time.Second). + Generate() +if err != nil { + return err +} + +content, err := response.Content() +if err != nil { + return err +} + +fmt.Println(response.MimeType(), len(content)) +``` + +Use `Female` for the default female voice, `Male` for the default male voice, or `Voice` for a provider-specific voice: + +```go +response, err := facades.AI().Audio("Your report is ready."). + Voice("alloy"). + Generate() +``` + +Audio responses expose bytes, MIME type, usage metadata, storage helpers, and a `Then` callback: + +```go +import ( + "github.com/goravel/framework/contracts/ai" + "goravel/app/facades" +) + +response.Then(func(response ai.AudioResponse) { + facades.Log().Info(response.MimeType()) +}) +``` + +Store generated audio directly on the configured filesystem: + +```go +path, err := response.Store("public") +if err != nil { + return err +} + +path, err = response.StoreAs("audio/welcome.mp3", "public") +``` + +You may also generate and store in one step: + +```go +path, err := facades.AI().Audio("Welcome to Goravel"). + Female(). + StoreAs("audio/welcome.mp3", "public") +``` + +## Transcription + +Use `facades.AI().Transcription` to convert audio files to text. The input must implement `ai.StorableFile`, so you can reuse document attachments or any custom file type that exposes `FileName`, `MimeType`, and `Content`: + +```go +import ( + "fmt" + "time" + + "github.com/goravel/framework/ai/document" + "goravel/app/facades" +) + +response, err := facades.AI().Transcription(document.FromPath("storage/app/audio/meeting.mp3")). + Provider("openai"). + Model("gpt-4o-mini-transcribe"). + Language("en"). + Diarize(). + Timeout(30 * time.Second). + Generate() +if err != nil { + return err +} + +fmt.Println(response.Text()) +``` + +Use `Segments` when the provider returns timestamps or speaker labels: + +```go +for _, segment := range response.Segments() { + fmt.Println(segment.Speaker, segment.Start, segment.End, segment.Text) +} +``` + +Transcription responses expose transcript text, optional segments, usage metadata, and a `Then` callback: + +```go +import ( + "github.com/goravel/framework/contracts/ai" + "goravel/app/facades" +) + +response.Then(func(response ai.TranscriptionResponse) { + facades.Log().Info(response.Text()) +}) +``` + +## Middleware + +Prompt middleware intercepts requests before they reach the provider. Middleware can mutate the prompt, call the next middleware/provider, short-circuit the provider call by returning a response directly, or register callbacks that run after the final response is available. + +The middleware examples use these imports: + +```go +import ( + "context" + "strings" + + frameworkai "github.com/goravel/framework/ai" + "github.com/goravel/framework/contracts/ai" + "goravel/app/agents" + "goravel/app/facades" +) +``` + +Middleware implements the `ai.Middleware` contract: + +```go +type TrimPromptMiddleware struct { +} + +func (r *TrimPromptMiddleware) Handle(ctx context.Context, prompt ai.AgentPrompt, next ai.Next) (ai.AgentResponse, error) { + prompt.Input = strings.TrimSpace(prompt.Input) + + return next(ctx, prompt) +} +``` + +You may also post-process the final response with `Then`: + +```go +type LogResponseMiddleware struct { +} + +func (r *LogResponseMiddleware) Handle(ctx context.Context, prompt ai.AgentPrompt, next ai.Next) (ai.AgentResponse, error) { + response, err := next(ctx, prompt) + if err != nil { + return nil, err + } + + return response.Then(func(response ai.AgentResponse) { + facades.Log().Info(response.Text()) + }), nil +} +``` + +Apply middleware to a single conversation with `WithMiddleware`: + +```go +conversation, err := facades.AI().Agent( + &agents.SupportAgent{}, + frameworkai.WithMiddleware(&TrimPromptMiddleware{}), +) +``` + +To apply middleware every time an agent is used, return it from the agent's `Middleware` method: + +```go +func (r *SupportAgent) Middleware() []ai.Middleware { + return []ai.Middleware{ + &TrimPromptMiddleware{}, + &LogResponseMiddleware{}, + } +} +``` + +Agent middleware runs before middleware passed with `WithMiddleware`. The same middleware pipeline is used by both `Prompt` and `Stream`, so cross-cutting behavior such as prompt enrichment, conversation memory, guards, and response logging can live in one place. + +## Tools + +Tools allow an agent to expose callable capabilities to the model. A tool has a unique name, a description, a JSON Schema parameter definition, and an `Execute` method that returns the tool result as a string. + +```go +type WeatherTool struct { +} + +func (r *WeatherTool) Name() string { + return "get_weather" +} + +func (r *WeatherTool) Description() string { + return "Returns the current weather for a city." +} + +func (r *WeatherTool) Parameters() map[string]any { + return map[string]any{ + "type": "object", + "properties": map[string]any{ + "city": map[string]any{ + "type": "string", + }, + }, + "required": []string{"city"}, + } +} + +func (r *WeatherTool) Execute(ctx context.Context, args map[string]any) (string, error) { + city, _ := args["city"].(string) + + return fmt.Sprintf("Sunny, 25 C in %s", city), nil +} +``` + +Return the tool from your agent's `Tools` method: + +```go +func (r *SupportAgent) Tools() []ai.Tool { + return []ai.Tool{ + &WeatherTool{}, + } +} +``` + +When the model requests a tool call during `Prompt`, Goravel executes the tool, appends the tool result to the conversation, and re-prompts the model until it returns a final text response: + +```go +conversation, err := facades.AI().Agent(&agents.SupportAgent{}) +if err != nil { + return err +} + +response, err := conversation.Prompt("What's the weather in London?") +if err != nil { + return err +} + +fmt.Println(response.Text()) +``` + +Goravel limits the tool-call loop to prevent a model from requesting tools indefinitely. + +## Streaming + +Use `Stream` when you want token deltas as they are produced by the provider: + +```go +stream, err := conversation.Stream("Write a short release note.") +if err != nil { + return err +} + +err = stream.Each(func(event ai.StreamEvent) error { + switch event.Type { + case ai.StreamEventTypeTextDelta: + fmt.Print(event.Delta) + case ai.StreamEventTypeToolCall: + for _, toolCall := range event.ToolCalls { + fmt.Println("calling tool:", toolCall.Name) + } + case ai.StreamEventTypeDone: + if event.Usage != nil { + fmt.Println(event.Usage.Total()) + } + case ai.StreamEventTypeError: + return errors.New(event.Error) + } + + return nil +}) +``` + +Streaming supports the same conversation options, middleware pipeline, and tool-call loop as `Prompt`. When the model requests tools, the stream emits a `StreamEventTypeToolCall` event, Goravel executes the tools, and the stream continues with the final model response. The stream appends the user input, tool calls, tool results, and final assistant text to the conversation history only after the stream completes successfully. + +### Completion Callback + +Use `Then` to run logic after the stream has completed and the final response is available: + +```go +stream.Then(func(response ai.AgentResponse) { + facades.Log().Info(response.Text()) +}) +``` + +### HTTP Streaming + +`HTTPResponse` converts a stream into a Server-Sent Events response. This is useful in controllers and routes: + +```go +func Chat(ctx http.Context) http.Response { + conversation, err := facades.AI().WithContext(ctx).Agent(&agents.SupportAgent{}) + if err != nil { + return ctx.Response().String(500, err.Error()) + } + + stream, err := conversation.Stream(ctx.Request().Input("message")) + if err != nil { + return ctx.Response().String(500, err.Error()) + } + + return stream.HTTPResponse(ctx) +} +``` + +By default, Goravel renders SSE events with `Content-Type: text/event-stream`. You may customize the status code or event renderer: + +```go +return stream.HTTPResponse( + ctx, + frameworkai.WithStreamCode(200), + frameworkai.WithStreamRender(func(w http.StreamWriter, event ai.StreamEvent) error { + if _, err := w.WriteString(event.Delta); err != nil { + return err + } + + return w.Flush() + }), +) +``` + +## Providers + +### First-party Providers + +Goravel provides these first-party AI provider packages: + +| Provider | Package | Install Command | +| --- | --- | --- | +| OpenAI | [goravel/openai](https://github.com/goravel/openai) | `./artisan package:install github.com/goravel/openai` | +| Anthropic | [goravel/anthropic](https://github.com/goravel/anthropic) | `./artisan package:install github.com/goravel/anthropic` | +| Gemini | [goravel/gemini](https://github.com/goravel/gemini) | `./artisan package:install github.com/goravel/gemini` | + +Provider packages implement the AI provider contracts and may support different capabilities. Check the provider package README for supported features and configuration details. + +The OpenAI provider uses the Responses API for prompts, streaming, tool calling, and attachments. It also supports image generation, image edits, audio generation, transcription, media storage helpers, and provider-managed files. + +### Custom Providers + +You may build a custom provider by following the same structure as `goravel/openai`, `goravel/anthropic`, or `goravel/gemini`: implement the AI provider contracts, register a service provider, expose a facade that resolves the provider, then configure it in `config/ai.go`. + +The provider resolver accepts either an `ai.Provider` instance or a `func() (ai.Provider, error)` in the provider's `via` configuration. + +```go +"providers": map[string]any{ + "custom": map[string]any{ + "via": func() (ai.Provider, error) { + return &CustomProvider{}, nil + }, + }, +}, +``` + +A provider must implement both `Prompt` and `Stream`: + +```go +import ( + "context" + + "github.com/goravel/framework/contracts/ai" +) + +type CustomProvider struct { +} + +func (r *CustomProvider) Prompt(ctx context.Context, prompt ai.AgentPrompt) (ai.AgentResponse, error) { + // Use prompt.Attachments when calling providers that support documents or images. + // Use prompt.Tools when calling providers that support tool calling. + // Use prompt.ProviderState to keep provider-specific state across tool-call loops. + return nil, nil +} + +func (r *CustomProvider) Stream(ctx context.Context, prompt ai.AgentPrompt) (ai.StreamableAgentResponse, error) { + // Return tool calls on the final streamed response when the model requests tools. + return nil, nil +} +``` + +The agent response implementation should expose generated text with `Text`, usage metadata with `Usage`, requested tool invocations with `ToolCalls`, and completion callbacks with `Then`. For streaming, populate `StreamEvent.ToolCalls` when emitting `StreamEventTypeToolCall` events. + +Providers that support image generation should implement `ImageProvider`: + +```go +func (r *CustomProvider) Image(ctx context.Context, prompt ai.ImagePrompt) (ai.ImageResponse, error) { + // Use prompt.Prompt, prompt.Model, prompt.Size, prompt.Quality, prompt.Attachments, and prompt.Timeout. + return nil, nil +} +``` + +An image response should expose generated bytes with `Content`, the MIME type with `MimeType`, usage metadata with `Usage`, storage helpers with `Store` / `StoreAs`, and completion callbacks with `Then`. + +Providers that support audio generation should implement `AudioProvider`: + +```go +func (r *CustomProvider) Audio(ctx context.Context, prompt ai.AudioPrompt) (ai.AudioResponse, error) { + // Use prompt.Prompt, prompt.Model, prompt.Voice, prompt.Instructions, and prompt.Timeout. + return nil, nil +} +``` + +An audio response should expose generated bytes with `Content`, the MIME type with `MimeType`, usage metadata with `Usage`, storage helpers with `Store` / `StoreAs`, and completion callbacks with `Then`. + +Providers that support speech-to-text should implement `TranscriptionProvider`: + +```go +func (r *CustomProvider) Transcription(ctx context.Context, prompt ai.TranscriptionPrompt) (ai.TranscriptionResponse, error) { + // Use prompt.File, prompt.Model, prompt.Language, prompt.Diarize, and prompt.Timeout. + return nil, nil +} +``` + +A transcription response should expose transcript text with `Text`, optional timestamp or speaker segments with `Segments`, usage metadata with `Usage`, and completion callbacks with `Then`. + +Providers that support attachment uploads should also implement `FileProvider`: + +```go +import ( + "context" + + "github.com/goravel/framework/contracts/ai" +) + +func (r *CustomProvider) PutFile(ctx context.Context, file ai.StorableFile) (ai.FileResponse, error) { + content, err := file.Content(ctx) + if err != nil { + return nil, err + } + + _ = content + _ = file.FileName() + _ = file.MimeType() + + return nil, nil +} + +func (r *CustomProvider) GetFile(ctx context.Context, id string) (ai.FileResponse, error) { + return nil, nil +} + +func (r *CustomProvider) DeleteFile(ctx context.Context, id string) error { + return nil +} +``` + +Then select it per conversation: + +```go +conversation, err := facades.AI().Agent( + &agents.SupportAgent{}, + frameworkai.WithProvider("custom"), +) +``` diff --git a/en/getting-started/configuration.md b/en/getting-started/configuration.md index 320e8e676..d3c79e9df 100644 --- a/en/getting-started/configuration.md +++ b/en/getting-started/configuration.md @@ -74,7 +74,7 @@ You can use the `artisan about` command to view the framework version, configura ### Maintenance Mode -You may use the `down` command to put your application into maintenance mode. The command stores maintenance metadata at the `framework/maintenance.json` storage path: +You may use the `down` command to put your application into maintenance mode. By default, Goravel uses the `file` maintenance driver and stores maintenance metadata at the `framework/maintenance.json` storage path: ```shell ./artisan down @@ -111,6 +111,15 @@ You can also let Goravel generate a random secret for you: ./artisan down --with-secret ``` +If your application runs on multiple servers, you may use the `cache` maintenance driver so all servers share the same maintenance state. Configure the driver and, optionally, the cache store name in your `.env` file: + +```ini +APP_MAINTENANCE_DRIVER=cache +APP_MAINTENANCE_STORE=redis +``` + +If `APP_MAINTENANCE_STORE` is not set, Goravel uses the default cache store. Running `down` or `up` on one server updates the maintenance state for every server using the same cache store. + To bring the application out of maintenance mode, run the `up` command: ```shell diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index b838977db..cf3e34c5d 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -1,5 +1,9 @@ # Upgrading To v1.18 From v1.17 +## Exciting New Features 🎉 + +- [AI SDK](#ai-sdk) + ## Enhancements 🚀 - [Laravel-style collections](#laravel-style-collections) @@ -67,6 +71,34 @@ rules := map[string]any{ ## Feature Introduction +### AI SDK + +Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, generated media, and speech workflows through one facade. Install the facade with `./artisan package:install ai`, then install a provider package such as `goravel/openai`, `goravel/anthropic`, or `goravel/gemini`. + +The initial release includes: + +- Agent conversations with `make:agent`, `Prompt`, `Stream`, tool calling, prompt middleware. +- Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs. +- Image and audio generation with `Store` / `StoreAs` helpers, plus transcription with language and diarization options. + +A minimal prompt looks like this: + +```go +conversation, err := facades.AI().Agent(&agents.SupportAgent{}) +if err != nil { + return err +} + +response, err := conversation.Prompt("How do I create a controller?") +if err != nil { + return err +} + +fmt.Println(response.Text()) +``` + +[View Document](../ai/sdk.md) + ### Native validation engine and built-in rules Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set. @@ -110,7 +142,7 @@ evens.All() // []int{2, 4, 6} ### Maintenance mode commands -Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `down` command writes maintenance metadata to storage, and the `up` command removes it. +Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `file` maintenance driver is used by default, so the `down` command writes maintenance metadata to storage and the `up` command removes it. ```shell ./artisan down @@ -126,6 +158,13 @@ The `down` command supports response customization with options such as `--reaso ./artisan down --with-secret ``` +For multi-server deployments, set `APP_MAINTENANCE_DRIVER=cache` to store maintenance state in a shared cache store. Use `APP_MAINTENANCE_STORE` to choose a named cache store, such as Redis: + +```ini +APP_MAINTENANCE_DRIVER=cache +APP_MAINTENANCE_STORE=redis +``` + [View Document](../getting-started/configuration.md#maintenance-mode) ### Build command supports go generate From f3bca8539bdb206716bfcf5f2ec019760789721c Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 10 Jun 2026 18:37:58 +0800 Subject: [PATCH 04/35] docs: update AI provider failover --- en/ai/sdk.md | 67 +++++++++++++++++++++++++++++++++++++++++++++ en/upgrade/v1.18.md | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/en/ai/sdk.md b/en/ai/sdk.md index bd713e881..74aad696c 100644 --- a/en/ai/sdk.md +++ b/en/ai/sdk.md @@ -52,6 +52,12 @@ func init() { "providers": map[string]any{ "openai": map[string]any{ "key": config.Env("OPENAI_API_KEY", ""), + "failover": map[string][]string{ + "context_length_exceeded": { + "maximum context length", + "/(?i)context.*length/", + }, + }, "url": config.Env("OPENAI_BASE_URL", ""), "via": func() (ai.Provider, error) { return openaifacades.OpenAI("openai") @@ -86,6 +92,8 @@ OPENAI_BASE_URL= `OPENAI_BASE_URL` is optional. Use it when routing requests through a proxy or an OpenAI-compatible endpoint. If a model default is empty, the provider package uses its own default model. Set `models.text.max_tokens` to limit generated text tokens; leave it as `0` to use the provider default. +The `failover` map is optional. Provider packages may use it to map provider-specific error messages to failover reasons. Plain strings use substring matching, and slash-delimited strings use Go regular expressions. + ## Creating Agents Agents define the system instructions and any initial conversation context that should be sent to the provider. @@ -181,6 +189,15 @@ conversation, err := facades.AI().Agent( ) ``` +Pass additional provider names to `WithProvider` to create an ordered failover chain. Goravel tries the next provider only when the current provider returns a failover error: + +```go +conversation, err := facades.AI().Agent( + &agents.SupportAgent{}, + frameworkai.WithProvider("openai", "anthropic"), +) +``` + If the request should use a specific Go context, call `WithContext` before creating the conversation: ```go @@ -758,6 +775,29 @@ Provider packages implement the AI provider contracts and may support different The OpenAI provider uses the Responses API for prompts, streaming, tool calling, and attachments. It also supports image generation, image edits, audio generation, transcription, media storage helpers, and provider-managed files. +### Provider Failover + +When a provider chain is configured with `WithProvider("primary", "backup")`, Goravel retries the next provider only for errors that implement `ai.FailoverError`. If every provider in the chain fails with a failover error, the last error is returned. Streaming responses can fail over before output starts; after output starts, the stream returns the current provider error instead of switching providers. + +Configure `ai.providers.openai.failover` to add OpenAI-specific error message mappings: + +```go +"openai": map[string]any{ + "key": config.Env("OPENAI_API_KEY", ""), + "failover": map[string][]string{ + "context_length_exceeded": { + "maximum context length", + "/(?i)context.*length/", + }, + }, + "via": func() (ai.Provider, error) { + return openaifacades.OpenAI("openai") + }, +}, +``` + +Each `failover` key is the reason returned by `FailoverError.Reason()`. Empty reasons or patterns are ignored. Invalid regular expressions return an error while resolving the provider. + ### Custom Providers You may build a custom provider by following the same structure as `goravel/openai`, `goravel/anthropic`, or `goravel/gemini`: implement the AI provider contracts, register a service provider, expose a facade that resolves the provider, then configure it in `config/ai.go`. @@ -801,6 +841,33 @@ func (r *CustomProvider) Stream(ctx context.Context, prompt ai.AgentPrompt) (ai. The agent response implementation should expose generated text with `Text`, usage metadata with `Usage`, requested tool invocations with `ToolCalls`, and completion callbacks with `Then`. For streaming, populate `StreamEvent.ToolCalls` when emitting `StreamEventTypeToolCall` events. +Custom providers can return `frameworkai.NewFailoverError` for provider-specific retryable errors: + +```go +import ( + frameworkai "github.com/goravel/framework/ai" + "github.com/goravel/framework/contracts/ai" +) + +return nil, frameworkai.NewFailoverError("custom", ai.FailoverReason("rate_limited"), err) +``` + +They can also compile configured `failover` rules with `frameworkai.NewFailoverRules` and wrap matching errors before returning them: + +```go +rules, err := frameworkai.NewFailoverRules("custom", providerConfig.Failover) +if err != nil { + return nil, err +} + +response, err := r.callProvider(ctx, prompt) +if err != nil { + return nil, rules.Wrap("custom", err) +} + +return response, nil +``` + Providers that support image generation should implement `ImageProvider`: ```go diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index cf3e34c5d..4fbd3933d 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -77,7 +77,7 @@ Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, ge The initial release includes: -- Agent conversations with `make:agent`, `Prompt`, `Stream`, tool calling, prompt middleware. +- Agent conversations with `make:agent`, `Prompt`, `Stream`, tool calling, prompt middleware, provider failover. - Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs. - Image and audio generation with `Store` / `StoreAs` helpers, plus transcription with language and diarization options. From 5222c2cb83c0b292cb33e9961d3e697b6bf6a9ec Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Fri, 12 Jun 2026 10:29:40 +0530 Subject: [PATCH 05/35] add telemetry docs --- .vitepress/config/en.ts | 4 + en/digging-deeper/telemetry.md | 502 +++++++++++++++++++++++++++++++++ en/upgrade/v1.18.md | 25 ++ 3 files changed, 531 insertions(+) create mode 100644 en/digging-deeper/telemetry.md diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 88e585e98..b00568696 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -316,6 +316,10 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { { text: 'Pluralization', link: 'pluralization' + }, + { + text: 'Telemetry', + link: 'telemetry' } ] } diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md new file mode 100644 index 000000000..c4e2a9612 --- /dev/null +++ b/en/digging-deeper/telemetry.md @@ -0,0 +1,502 @@ +# Telemetry + +[[toc]] + +## Introduction + +Goravel provides an observability module built on top of [OpenTelemetry](https://opentelemetry.io) that can be operated using `facades.Telemetry()`. It allows you to collect traces, metrics, and logs from your application and export them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. + +The module is integrated with the application lifecycle: providers are configured in a single configuration file, buffered data is flushed automatically when the application shuts down, and built-in instrumentation is available for the HTTP server, the HTTP client, gRPC, and the logger. + +If you are new to OpenTelemetry, the module revolves around three signals: + +- **Traces** record the full path of a request as it travels through your services. Each trace is a tree of spans, where a span represents a single timed operation, such as an HTTP request, a database query, or a function call. +- **Metrics** are numerical measurements aggregated over time, such as request counts, durations, or memory usage. +- **Logs** are timestamped records of events, which can be linked to the trace that produced them. + +## Installation + +The telemetry module is optional, you can install it using the `package:install` command: + +```shell +./artisan package:install Telemetry +``` + +This command performs the following actions: + +- Creates the `config/telemetry.go` configuration file; +- Creates the `facades/telemetry.go` facade file; +- Registers `&telemetry.ServiceProvider{}` in `bootstrap/providers.go`; +- Adds an `otel` channel to `config/logging.go` for log export. + +## Configuration + +All of the configuration options live in the `config/telemetry.go` file. The `service` section defines the identity attached to every trace, metric, and log record, this is what observability platforms use to group your data: + +```go +"service": map[string]any{ + "name": config.Env("APP_NAME", "goravel"), + "version": config.Env("APP_VERSION", ""), + "environment": config.Env("APP_ENV", ""), +}, +``` + +You can attach additional static metadata (e.g., `k8s.pod.name`, `region`, `team`) to all telemetry data using the `resource` section. + +### Enabling Signals + +Each signal (traces, metrics, and logs) is disabled by default. To enable a signal, point its `exporter` option to one of the exporter definitions in the `exporters` section. The easiest way is through your `.env` file: + +```ini +OTEL_TRACES_EXPORTER=otlptrace +OTEL_METRICS_EXPORTER=otlpmetric +OTEL_LOGS_EXPORTER=otlplog +``` + +Setting an exporter to an empty string disables the corresponding signal entirely. + +::: tip +During local development, you can set any of these to `console` to print telemetry data directly to stdout instead of sending it to a backend. +::: + +### Exporters + +The `exporters` section defines how the data leaves your application. Each entry is referenced by name from the signal sections, three drivers are supported: `otlp`, `console`, and `custom`. + +#### OTLP + +The `otlp` driver sends data to any OpenTelemetry collector or vendor endpoint, using either `http/protobuf` (port 4318) or `grpc` (port 4317): + +```go +"otlptrace": map[string]any{ + "driver": "otlp", + "endpoint": config.Env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "localhost:4318"), + + // Protocol: "http/protobuf" or "grpc". + "protocol": config.Env("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "http/protobuf"), + + // Set to false to require TLS/SSL. + "insecure": config.Env("OTEL_EXPORTER_OTLP_TRACES_INSECURE", true), + + // Compression: "gzip" or "" (none). + "compression": config.Env("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", ""), + + // TLS certificate file paths. Leave empty to use system roots. + "tls": map[string]any{ + "ca": config.Env("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", ""), + "cert": config.Env("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", ""), + "key": config.Env("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", ""), + }, + + // Retry with exponential backoff on export failure. + "retry": map[string]any{ + "enabled": true, + "initial_interval": "5s", + "max_interval": "30s", + "max_elapsed_time": "1m", + }, +}, +``` + +The `endpoint` option accepts either a bare `host:port` pair or a full URL. When a URL with a scheme is provided (e.g., `https://otlp.example.com/v1/traces`), the scheme and path determine the TLS setting and export path, and the `insecure` option is ignored. + +If your backend requires authentication, such as a vendor API key, you may attach headers to every export request using the `headers` option: + +```go +"otlptrace": map[string]any{ + "driver": "otlp", + "endpoint": config.Env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "localhost:4318"), + "headers": map[string]string{ + "x-api-key": config.Env("OTEL_EXPORTER_API_KEY", ""), + }, +}, +``` + +The metric exporter additionally supports the `metric_temporality` option: `cumulative` (Prometheus), `delta` (Datadog/StatsD), or `lowmemory`. + +::: tip +To see your traces locally, you can run Jaeger with a single command, it accepts OTLP on the default ports and requires no extra configuration: + +```shell +docker run --rm -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/jaeger:latest +``` + +Then set `OTEL_TRACES_EXPORTER=otlptrace` and open `http://localhost:16686`. +::: + +#### Console + +The `console` driver prints telemetry data to stdout, which is useful for debugging your instrumentation locally: + +```go +"console": map[string]any{ + "driver": "console", + "pretty_print": true, +}, +``` + +#### Custom + +If you need to export data to a destination that is not supported out of the box, you may provide your own exporter using the `custom` driver. The `via` key accepts either a ready-made instance or a factory function, depending on the signal the exporter is used for: + +| Signal | Instance | Factory | +| ------- | ----------------------- | ---------------------------------------------------- | +| Traces | `sdktrace.SpanExporter` | `func(context.Context) (sdktrace.SpanExporter, error)` | +| Metrics | `sdkmetric.Reader` | `func(context.Context) (sdkmetric.Reader, error)` | +| Logs | `sdklog.Exporter` | `func(context.Context) (sdklog.Exporter, error)` | + +For example, to write spans to a file instead of stdout: + +```go +import ( + "context" + "os" + + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +"custom": map[string]any{ + "driver": "custom", + "via": func(ctx context.Context) (sdktrace.SpanExporter, error) { + file, err := os.Create("storage/logs/traces.json") + if err != nil { + return nil, err + } + + return stdouttrace.New(stdouttrace.WithWriter(file)) + }, +}, +``` + +### Sampling + +Recording every trace can be expensive in high-traffic applications. The `traces.sampler` section controls which traces are recorded: + +```go +"sampler": map[string]any{ + // If true, respects the sampling decision of the upstream service. + "parent": config.Env("OTEL_TRACES_SAMPLER_PARENT", true), + + // "always_on", "always_off" or "traceidratio" + "type": config.Env("OTEL_TRACES_SAMPLER_TYPE", "always_on"), + + // The ratio for "traceidratio" sampling, e.g., 0.1 records ~10% of traces. + "ratio": config.Env("OTEL_TRACES_SAMPLER_RATIO", 0.05), +}, +``` + +When `parent` is enabled, your service follows the sampling decision already made by the calling service, ensuring distributed traces are never broken in the middle. + +### Processors + +Traces and logs are handed to their exporters through a processor. The default `batch` processor buffers data and pushes it on an interval, which is the recommended setting for production. The `simple` processor exports each record synchronously and should only be used for debugging: + +```go +"processor": map[string]any{ + "type": config.Env("OTEL_TRACE_PROCESSOR_TYPE", "batch"), + "interval": config.Env("OTEL_TRACE_EXPORT_INTERVAL", "5s"), + "timeout": config.Env("OTEL_TRACE_EXPORT_TIMEOUT", "30s"), +}, +``` + +Metrics use a periodic reader instead, configured by `metrics.reader.interval` (default `60s`). + +## Tracing + +### Creating Spans + +To create a span, request a tracer from the `Telemetry` facade using the `Tracer` method, then call `Start`. The first argument is a `context.Context`, if the context already contains a span (for example, one started by the HTTP middleware), the new span is automatically attached as its child: + +```go +import "goravel/app/facades" + +tracer := facades.Telemetry().Tracer("app") + +ctx, span := tracer.Start(ctx.Context(), "process-order") +defer span.End() + +// Pass ctx down to create child spans +orderItems(ctx) +``` + +The name passed to `Tracer` (and `Meter`) identifies the instrumentation scope, typically your application or package name, and is shown alongside each span in your backend. + +### Span Attributes + +You can attach key-value attributes to a span using the `SetAttributes` method. The `telemetry` package re-exports the OpenTelemetry attribute helpers so you don't need to import additional packages: + +```go +import "github.com/goravel/framework/telemetry" + +span.SetAttributes( + telemetry.String("order.id", "1234"), + telemetry.Int("order.items", 3), + telemetry.Bool("order.gift", false), +) +``` + +Attributes can also be set at creation time using the `WithAttributes` option: + +```go +ctx, span := tracer.Start(ctx, "process-order", telemetry.WithAttributes( + telemetry.String("order.id", "1234"), +)) +``` + +### Span Events + +Events mark a point in time within a span, such as a cache miss or a retry attempt. You can add them using the `AddEvent` method: + +```go +span.AddEvent("cache_miss", telemetry.WithAttributes( + telemetry.String("cache.key", "user:42"), +)) +``` + +### Recording Errors + +When an operation fails, you should record the error on the span and mark its status, so failed traces can be filtered in your backend: + +```go +if err != nil { + span.RecordError(err) + span.SetStatus(telemetry.CodeError, "failed to process order") + + return err +} +``` + +### Span Kinds + +By default, spans are created with the `internal` kind. When a span represents a boundary like publishing a message to a queue or consuming one, you can declare its role using the `WithSpanKind` option: + +```go +ctx, span := tracer.Start(ctx, "orders.publish", telemetry.WithSpanKind(telemetry.SpanKindProducer)) +``` + +Available kinds: `SpanKindInternal`, `SpanKindServer`, `SpanKindClient`, `SpanKindProducer`, `SpanKindConsumer`. + +### The Current Span + +You don't always need to create a new span, often you just want to enrich the one that is already active, such as the span started by the [HTTP server middleware](#http-server). You can retrieve it from the context using the `SpanFromContext` function: + +```go +import "go.opentelemetry.io/otel/trace" + +func (r *OrderController) Store(ctx http.Context) http.Response { + span := trace.SpanFromContext(ctx.Context()) + span.SetAttributes(telemetry.String("order.id", "1234")) + + // ... +} +``` + +If there is no active span in the context, a no-op span is returned, so it is always safe to call. + +## Metrics + +To record metrics, request a meter from the `Telemetry` facade using the `Meter` method, then create instruments from it. Instruments are safe for concurrent use and should be created once and reused: + +```go +import ( + "go.opentelemetry.io/otel/metric" + + "goravel/app/facades" + "github.com/goravel/framework/telemetry" +) + +meter := facades.Telemetry().Meter("app") +``` + +A **counter** only goes up, ideal for counting processed orders or sent emails: + +```go +counter, err := meter.Int64Counter("orders.processed", + metric.WithDescription("Number of processed orders"), +) + +counter.Add(ctx, 1, metric.WithAttributes( + telemetry.String("payment.method", "card"), +)) +``` + +An **up-down counter** can also decrease, useful for tracking in-flight values: + +```go +inFlight, err := meter.Int64UpDownCounter("jobs.in_flight") + +inFlight.Add(ctx, 1) +defer inFlight.Add(ctx, -1) +``` + +A **histogram** records a distribution of values, such as durations or payload sizes: + +```go +histogram, err := meter.Float64Histogram("order.process.duration", + metric.WithUnit("s"), + metric.WithDescription("Duration of order processing"), +) + +histogram.Record(ctx, time.Since(start).Seconds()) +``` + +An **observable gauge** reports a value that is sampled rather than recorded, such as a queue depth or the number of open connections. Instead of calling it yourself, you register a callback that is invoked on every collection cycle: + +```go +_, err := meter.Int64ObservableGauge("queue.depth", + metric.WithInt64Callback(func(ctx context.Context, observer metric.Int64Observer) error { + observer.Observe(int64(queue.Len())) + + return nil + }), +) +``` + +Metrics are collected and pushed to the exporter periodically based on the `metrics.reader.interval` configuration. + +## Logs + +During installation, an `otel` channel is added to your `config/logging.go` file: + +```go +"otel": map[string]any{ + "driver": "otel", + "instrument_name": config.GetString("APP_NAME", "goravel/log"), +}, +``` + +Add the channel to your logging stack and every entry written through `facades.Log()` will also be exported as an OpenTelemetry log record, with levels, structured fields, and stack traces mapped automatically: + +```go +"stack": map[string]any{ + "driver": "stack", + "channels": []string{"daily", "otel"}, +}, +``` + +To correlate logs with the active trace, write them using `WithContext`, the trace and span IDs are attached to the record so your backend can link logs to the exact request that produced them: + +```go +facades.Log().WithContext(ctx).Info("order processed") +``` + +You can also temporarily stop exporting logs without touching your logging configuration by setting `telemetry.instrumentation.log.enabled` to `false`. If you need full control over the emitted records, you may bypass the logging facade and emit OpenTelemetry log records directly using `facades.Telemetry().Logger("app")`. + +## Automatic Instrumentation + +Goravel ships with built-in instrumentation for the most common components. Each one can be toggled in the `instrumentation` section of `config/telemetry.go`. + +### HTTP Server + +The HTTP server middleware extracts the incoming trace context, starts a server span for every request, and records standard metrics (`http.server.request.duration`, `http.server.request.body.size`, `http.server.response.body.size`). Register it in the `bootstrap/app.go` file: + +```go +import ( + telemetryhttp "github.com/goravel/framework/telemetry/instrumentation/http" +) + +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithMiddleware(func(handler configuration.Middleware) { + handler.Append(telemetryhttp.Telemetry()) + }). + Create() +} +``` + +You can skip noisy endpoints using the `excluded_paths` and `excluded_methods` configuration options: + +```go +"http_server": map[string]any{ + "enabled": config.Env("OTEL_HTTP_SERVER_ENABLED", true), + "excluded_paths": []string{"/health"}, + "excluded_methods": []string{"OPTIONS"}, +}, +``` + +For more advanced control, the middleware accepts options: `WithFilter` to skip requests programmatically, `WithSpanNameFormatter` to customize span names, and `WithMetricAttributes` to attach extra attributes to recorded metrics. + +```go +handler.Append(telemetryhttp.Telemetry( + telemetryhttp.WithFilter(func(ctx http.Context) bool { + return ctx.Request().Path() != "/internal" + }), +)) +``` + +### HTTP Client + +Outgoing requests made through the [HTTP Client](./http-client.md) are instrumented automatically, no setup required. The active trace context is injected into outgoing headers, so downstream Goravel services continue the same trace. + +You can disable it globally via `telemetry.instrumentation.http_client.enabled`, or per client by setting `enable_telemetry` to `false` in the corresponding client configuration in `config/http.go`. + +### gRPC + +gRPC instrumentation is provided through stats handlers. Register them in the `bootstrap/app.go` file using the `WithGrpcServerStatsHandlers` and `WithGrpcClientStatsHandlers` functions: + +```go +import ( + "google.golang.org/grpc/stats" + + telemetrygrpc "github.com/goravel/framework/telemetry/instrumentation/grpc" +) + +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithGrpcServerStatsHandlers(func() []stats.Handler { + return []stats.Handler{telemetrygrpc.NewServerStatsHandler()} + }). + WithGrpcClientStatsHandlers(func() map[string][]stats.Handler { + return map[string][]stats.Handler{ + "default": {telemetrygrpc.NewClientStatsHandler()}, + } + }). + Create() +} +``` + +Both handlers accept options such as `WithFilter`, `WithSpanAttributes`, and `WithMetricAttributes`, and can be toggled via the `grpc_server.enabled` and `grpc_client.enabled` configuration options. + +## Context Propagation + +The `propagators` configuration option defines how trace context crosses process boundaries. The default is the W3C `tracecontext` standard; `baggage`, `b3`, and `b3multi` (Zipkin) are also supported and can be combined as a comma-separated list. + +The built-in HTTP and gRPC instrumentation propagate context automatically. If you communicate over a custom transport (e.g., a message queue), you can inject and extract the context manually using the `Propagator` method: + +```go +import "github.com/goravel/framework/telemetry" + +// Producer: inject the trace context into the message +carrier := telemetry.PropagationMapCarrier(message.Headers) +facades.Telemetry().Propagator().Inject(ctx, carrier) + +// Consumer: continue the trace from the message +ctx := facades.Telemetry().Propagator().Extract(context.Background(), telemetry.PropagationMapCarrier(message.Headers)) +``` + +If you need the identifiers of the current trace, for example, to return them in an error response, you can read them from the context: + +```go +import "go.opentelemetry.io/otel/trace" + +spanCtx := trace.SpanContextFromContext(ctx) + +traceID := spanCtx.TraceID().String() +spanID := spanCtx.SpanID().String() +``` + +## Flushing & Shutdown + +You don't need to manage the telemetry lifecycle yourself: when the application stops, Goravel automatically flushes all buffered data and shuts the providers down, waiting at most `shutdown_timeout` (default `15s`). + +If you need to push buffered data immediately without stopping the providers, for example, before a serverless function freezes, you can use the `ForceFlush` method: + +```go +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() + +if err := facades.Telemetry().ForceFlush(ctx); err != nil { + facades.Log().Error(err) +} +``` diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 4fbd3933d..9c3b3afde 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -3,6 +3,7 @@ ## Exciting New Features 🎉 - [AI SDK](#ai-sdk) +- [Telemetry](#telemetry) ## Enhancements 🚀 @@ -99,6 +100,30 @@ fmt.Println(response.Text()) [View Document](../ai/sdk.md) +### Telemetry + +Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with `./artisan package:install Telemetry`. + +The initial release includes: + +- Traces, metrics, and logs configured from a single `config/telemetry.go` file, with OTLP, console, and custom exporters, sampling, and batching controls. +- Built-in instrumentation for the HTTP server, the HTTP client, gRPC, and the logger, with trace context propagation across services. +- Automatic flush and shutdown when the application stops. + +A manual span looks like this: + +```go +ctx, span := facades.Telemetry().Tracer("app").Start(ctx.Context(), "process-order") +defer span.End() + +span.SetAttributes(telemetry.String("order.id", "1234")) + +// Pass ctx down to create child spans +processItems(ctx) +``` + +[View Document](../digging-deeper/telemetry.md) + ### Native validation engine and built-in rules Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set. From 7ec19874fdf873b18c02d32701e1111c19704699 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Fri, 12 Jun 2026 18:14:19 +0530 Subject: [PATCH 06/35] docs: use complete in-project examples in telemetry docs Co-Authored-By: Claude Fable 5 --- en/digging-deeper/telemetry.md | 185 ++++++++++++++++++++++++++------- 1 file changed, 146 insertions(+), 39 deletions(-) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index c4e2a9612..ecdbb3bb4 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -206,18 +206,69 @@ Metrics use a periodic reader instead, configured by `metrics.reader.interval` ( ### Creating Spans -To create a span, request a tracer from the `Telemetry` facade using the `Tracer` method, then call `Start`. The first argument is a `context.Context`, if the context already contains a span (for example, one started by the HTTP middleware), the new span is automatically attached as its child: +To create a span, request a tracer from the `Telemetry` facade using the `Tracer` method, then call `Start`. The first argument is a `context.Context`, if the context already contains a span (for example, one started by the HTTP middleware), the new span is automatically attached as its child. + +The following service traces an order through its processing steps: the `Process` method opens a span, records what happened on it, and passes the returned context down so that `chargePayment` becomes a child span within the same trace: ```go -import "goravel/app/facades" +// app/services/order_service.go +package services + +import ( + "context" + + "github.com/goravel/framework/telemetry" + + "goravel/app/facades" +) + +type OrderService struct { +} + +func (r *OrderService) Process(ctx context.Context, orderID string) error { + ctx, span := facades.Telemetry().Tracer("app").Start(ctx, "order.process") + defer span.End() + + span.SetAttributes(telemetry.String("order.id", orderID)) + + if err := r.chargePayment(ctx, orderID); err != nil { + span.RecordError(err) + span.SetStatus(telemetry.CodeError, "failed to charge payment") + + return err + } + + span.AddEvent("payment_charged") + + return nil +} -tracer := facades.Telemetry().Tracer("app") +func (r *OrderService) chargePayment(ctx context.Context, orderID string) error { + // This span automatically becomes a child of "order.process". + _, span := facades.Telemetry().Tracer("app").Start(ctx, "order.charge_payment") + defer span.End() -ctx, span := tracer.Start(ctx.Context(), "process-order") -defer span.End() + // Charge the payment... -// Pass ctx down to create child spans -orderItems(ctx) + return nil +} +``` + +When the service is called from a controller, pass `ctx.Context()`. If the [HTTP server middleware](#http-server) is registered, the spans are attached to the request's trace, so your backend shows the full picture: the HTTP request, the order processing, and the payment charge as one tree: + +```go +// app/http/controllers/order_controller.go +func (r *OrderController) Store(ctx http.Context) http.Response { + if err := r.orders.Process(ctx.Context(), ctx.Request().Input("order_id")); err != nil { + return ctx.Response().Status(http.StatusInternalServerError).Json(http.Json{ + "error": "failed to process order", + }) + } + + return ctx.Response().Success().Json(http.Json{ + "message": "order processed", + }) +} ``` The name passed to `Tracer` (and `Meter`) identifies the instrumentation scope, typically your application or package name, and is shown alongside each span in your backend. @@ -256,7 +307,7 @@ span.AddEvent("cache_miss", telemetry.WithAttributes( ### Recording Errors -When an operation fails, you should record the error on the span and mark its status, so failed traces can be filtered in your backend: +When an operation fails, two calls work together: `RecordError` attaches the error to the span as an event, and `SetStatus` marks the whole span as failed so it can be filtered in your backend. Calling `RecordError` alone does not change the span status: ```go if err != nil { @@ -296,29 +347,63 @@ If there is no active span in the context, a no-op span is returned, so it is al ## Metrics -To record metrics, request a meter from the `Telemetry` facade using the `Meter` method, then create instruments from it. Instruments are safe for concurrent use and should be created once and reused: +To record metrics, request a meter from the `Telemetry` facade using the `Meter` method, then create instruments from it. Instruments are safe for concurrent use and should be created once and reused, a common pattern is to create them when the service is constructed and record values in its methods. + +The following service uses a **counter** (a value that only goes up, ideal for counting processed payments or sent emails) and a **histogram** (a distribution of values, such as durations or payload sizes): ```go +// app/services/payment_service.go +package services + import ( + "context" + "time" + "go.opentelemetry.io/otel/metric" - "goravel/app/facades" "github.com/goravel/framework/telemetry" + + "goravel/app/facades" ) -meter := facades.Telemetry().Meter("app") -``` +type PaymentService struct { + processed metric.Int64Counter + duration metric.Float64Histogram +} -A **counter** only goes up, ideal for counting processed orders or sent emails: +func NewPaymentService() (*PaymentService, error) { + meter := facades.Telemetry().Meter("app") + + processed, err := meter.Int64Counter("payments.processed", + metric.WithDescription("Number of processed payments"), + ) + if err != nil { + return nil, err + } + + duration, err := meter.Float64Histogram("payments.duration", + metric.WithUnit("s"), + metric.WithDescription("Duration of payment processing"), + ) + if err != nil { + return nil, err + } + + return &PaymentService{processed: processed, duration: duration}, nil +} -```go -counter, err := meter.Int64Counter("orders.processed", - metric.WithDescription("Number of processed orders"), -) +func (r *PaymentService) Charge(ctx context.Context, method string) error { + start := time.Now() -counter.Add(ctx, 1, metric.WithAttributes( - telemetry.String("payment.method", "card"), -)) + // Charge the payment... + + r.processed.Add(ctx, 1, metric.WithAttributes( + telemetry.String("payment.method", method), + )) + r.duration.Record(ctx, time.Since(start).Seconds()) + + return nil +} ``` An **up-down counter** can also decrease, useful for tracking in-flight values: @@ -330,17 +415,6 @@ inFlight.Add(ctx, 1) defer inFlight.Add(ctx, -1) ``` -A **histogram** records a distribution of values, such as durations or payload sizes: - -```go -histogram, err := meter.Float64Histogram("order.process.duration", - metric.WithUnit("s"), - metric.WithDescription("Duration of order processing"), -) - -histogram.Record(ctx, time.Since(start).Seconds()) -``` - An **observable gauge** reports a value that is sampled rather than recorded, such as a queue depth or the number of open connections. Instead of calling it yourself, you register a callback that is invoked on every collection cycle: ```go @@ -378,7 +452,15 @@ Add the channel to your logging stack and every entry written through `facades.L To correlate logs with the active trace, write them using `WithContext`, the trace and span IDs are attached to the record so your backend can link logs to the exact request that produced them: ```go -facades.Log().WithContext(ctx).Info("order processed") +func (r *OrderController) Store(ctx http.Context) http.Response { + facades.Log().WithContext(ctx.Context()). + With(map[string]any{ + "order_id": ctx.Request().Input("order_id"), + }). + Info("order received") + + // ... +} ``` You can also temporarily stop exporting logs without touching your logging configuration by setting `telemetry.instrumentation.log.enabled` to `false`. If you need full control over the emitted records, you may bypass the logging facade and emit OpenTelemetry log records directly using `facades.Telemetry().Logger("app")`. @@ -462,17 +544,42 @@ Both handlers accept options such as `WithFilter`, `WithSpanAttributes`, and `Wi The `propagators` configuration option defines how trace context crosses process boundaries. The default is the W3C `tracecontext` standard; `baggage`, `b3`, and `b3multi` (Zipkin) are also supported and can be combined as a comma-separated list. -The built-in HTTP and gRPC instrumentation propagate context automatically. If you communicate over a custom transport (e.g., a message queue), you can inject and extract the context manually using the `Propagator` method: +The built-in HTTP and gRPC instrumentation propagate context automatically. If you communicate over a custom transport, such as a message queue, you can carry the trace across the boundary yourself using the `Propagator` method: the producer injects the active context into the message headers, and the consumer extracts it and continues the same trace: ```go -import "github.com/goravel/framework/telemetry" +import ( + "context" -// Producer: inject the trace context into the message -carrier := telemetry.PropagationMapCarrier(message.Headers) -facades.Telemetry().Propagator().Inject(ctx, carrier) + "github.com/goravel/framework/telemetry" -// Consumer: continue the trace from the message -ctx := facades.Telemetry().Propagator().Extract(context.Background(), telemetry.PropagationMapCarrier(message.Headers)) + "goravel/app/facades" +) + +type Message struct { + Headers map[string]string + Body []byte +} + +func (r *OrderPublisher) Publish(ctx context.Context, message *Message) error { + // Attach the active trace context to the message + facades.Telemetry().Propagator().Inject(ctx, telemetry.PropagationMapCarrier(message.Headers)) + + // Send the message to the broker... + + return nil +} + +func (r *OrderConsumer) Consume(message *Message) error { + // Continue the trace started by the producer + ctx := facades.Telemetry().Propagator().Extract(context.Background(), telemetry.PropagationMapCarrier(message.Headers)) + + ctx, span := facades.Telemetry().Tracer("app").Start(ctx, "orders.consume", + telemetry.WithSpanKind(telemetry.SpanKindConsumer), + ) + defer span.End() + + return r.process(ctx, message) +} ``` If you need the identifiers of the current trace, for example, to return them in an error response, you can read them from the context: From 07d009d66c26aba11fcda0f289daee38cc391bc4 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 13 Jun 2026 10:16:57 +0800 Subject: [PATCH 07/35] optimize --- en/getting-started/configuration.md | 11 ++++++++++- en/upgrade/v1.18.md | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/en/getting-started/configuration.md b/en/getting-started/configuration.md index d3c79e9df..50e0bfdcf 100644 --- a/en/getting-started/configuration.md +++ b/en/getting-started/configuration.md @@ -74,7 +74,7 @@ You can use the `artisan about` command to view the framework version, configura ### Maintenance Mode -You may use the `down` command to put your application into maintenance mode. By default, Goravel uses the `file` maintenance driver and stores maintenance metadata at the `framework/maintenance.json` storage path: +If you installed the Route facade, you can use the `down` command to put your application into maintenance mode. By default, Goravel uses the `file` maintenance driver and stores maintenance metadata at the `framework/maintenance.json` storage path: ```shell ./artisan down @@ -111,6 +111,15 @@ You can also let Goravel generate a random secret for you: ./artisan down --with-secret ``` +The maintenance driver and cache store are configured in `config/app.go`. The default configuration reads from the `APP_MAINTENANCE_DRIVER` and `APP_MAINTENANCE_STORE` environment variables: + +```go +"maintenance": map[string]any{ + "driver": config.Env("APP_MAINTENANCE_DRIVER", "file"), + "store": config.Env("APP_MAINTENANCE_STORE", ""), +}, +``` + If your application runs on multiple servers, you may use the `cache` maintenance driver so all servers share the same maintenance state. Configure the driver and, optionally, the cache store name in your `.env` file: ```ini diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 9c3b3afde..694d5a555 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -183,6 +183,15 @@ The `down` command supports response customization with options such as `--reaso ./artisan down --with-secret ``` +Maintenance settings are read from `app.maintenance` in `config/app.go`. New Route installations add this configuration automatically. Existing applications can add it manually: + +```go +"maintenance": map[string]any{ + "driver": config.Env("APP_MAINTENANCE_DRIVER", "file"), + "store": config.Env("APP_MAINTENANCE_STORE", ""), +}, +``` + For multi-server deployments, set `APP_MAINTENANCE_DRIVER=cache` to store maintenance state in a shared cache store. Use `APP_MAINTENANCE_STORE` to choose a named cache store, such as Redis: ```ini From 3f3f96d828765f650b622d3118ec0862f18ec914 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 13 Jun 2026 10:42:27 +0800 Subject: [PATCH 08/35] optimize --- en/ai/sdk.md | 42 +++++++++++++++++++++++++++++++++++++++--- en/upgrade/v1.18.md | 2 +- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/en/ai/sdk.md b/en/ai/sdk.md index 74aad696c..765893f19 100644 --- a/en/ai/sdk.md +++ b/en/ai/sdk.md @@ -18,7 +18,7 @@ Install the AI facade and core service provider with the `package:install` comma ./artisan package:install ai ``` -This makes `facades.AI()` available and registers the `make:agent` Artisan command. +This makes `facades.AI()` available and registers the `make:agent` and `make:tool` Artisan commands. ### Install Providers @@ -622,6 +622,42 @@ Agent middleware runs before middleware passed with `WithMiddleware`. The same m Tools allow an agent to expose callable capabilities to the model. A tool has a unique name, a description, a JSON Schema parameter definition, and an `Execute` method that returns the tool result as a string. +You can generate a tool with Artisan: + +```shell +./artisan make:tool WeatherTool +./artisan make:tool user/WeatherTool +``` + +The generated file is placed under `app/tools`. Nested names create subdirectories, and `--force` or `-f` overwrites an existing tool file. + +```go +package tools + +import "context" + +type WeatherTool struct { +} + +func (r *WeatherTool) Name() string { + return "weather_tool" +} + +func (r *WeatherTool) Description() string { + return "A description of the tool." +} + +func (r *WeatherTool) Parameters() map[string]any { + return nil +} + +func (r *WeatherTool) Execute(ctx context.Context, args map[string]any) (string, error) { + return "", nil +} +``` + +After generating the tool, update `Description`, `Parameters`, and `Execute` for the capability you want to expose: + ```go type WeatherTool struct { } @@ -653,12 +689,12 @@ func (r *WeatherTool) Execute(ctx context.Context, args map[string]any) (string, } ``` -Return the tool from your agent's `Tools` method: +Import your application's tools package, then return the tool from your agent's `Tools` method: ```go func (r *SupportAgent) Tools() []ai.Tool { return []ai.Tool{ - &WeatherTool{}, + &tools.WeatherTool{}, } } ``` diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 694d5a555..ac494c36b 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -78,7 +78,7 @@ Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, ge The initial release includes: -- Agent conversations with `make:agent`, `Prompt`, `Stream`, tool calling, prompt middleware, provider failover. +- Agent conversations with `make:agent`, `make:tool`, `Prompt`, `Stream`, tool calling, prompt middleware, provider failover. - Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs. - Image and audio generation with `Store` / `StoreAs` helpers, plus transcription with language and diarization options. From fa015b241e8dac4c7b7c1b0e56704f9710dd2498 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 13 Jun 2026 11:13:03 +0800 Subject: [PATCH 09/35] optimize --- en/ai/sdk.md | 28 +++++++++++++++++++---- en/getting-started/directory-structure.md | 19 +++++++++++++-- en/upgrade/v1.18.md | 2 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/en/ai/sdk.md b/en/ai/sdk.md index 765893f19..72b4569b6 100644 --- a/en/ai/sdk.md +++ b/en/ai/sdk.md @@ -105,7 +105,7 @@ You can generate an agent with Artisan: ./artisan make:agent user/SupportAgent ``` -The generated file is placed under `app/agents` and contains the required methods: +The generated file is placed under `app/ai/agents` and contains the required methods: ```go package agents @@ -560,7 +560,7 @@ import ( frameworkai "github.com/goravel/framework/ai" "github.com/goravel/framework/contracts/ai" - "goravel/app/agents" + "goravel/app/ai/agents" "goravel/app/facades" ) ``` @@ -629,7 +629,7 @@ You can generate a tool with Artisan: ./artisan make:tool user/WeatherTool ``` -The generated file is placed under `app/tools`. Nested names create subdirectories, and `--force` or `-f` overwrites an existing tool file. +The generated file is placed under `app/ai/tools`. Nested names create subdirectories, and `--force` or `-f` overwrites an existing tool file. ```go package tools @@ -689,7 +689,7 @@ func (r *WeatherTool) Execute(ctx context.Context, args map[string]any) (string, } ``` -Import your application's tools package, then return the tool from your agent's `Tools` method: +Import your application's tools package, such as `goravel/app/ai/tools`, then return the tool from your agent's `Tools` method: ```go func (r *SupportAgent) Tools() []ai.Tool { @@ -717,6 +717,26 @@ fmt.Println(response.Text()) Goravel limits the tool-call loop to prevent a model from requesting tools indefinitely. +## Custom Generator Paths + +By default, `make:agent` writes to `app/ai/agents` and `make:tool` writes to `app/ai/tools`. You may customize those paths in `bootstrap/app.go` with `WithPaths`: + +```go +package bootstrap + +import ( + "github.com/goravel/framework/contracts/foundation/configuration" + "github.com/goravel/framework/foundation" +) + +var App = foundation.Setup(). + WithPaths(func(paths configuration.Paths) { + paths.Agents("internal/ai/agents") + paths.Tools("internal/ai/tools") + }). + Create() +``` + ## Streaming Use `Stream` when you want token deltas as they are produced by the provider: diff --git a/en/getting-started/directory-structure.md b/en/getting-started/directory-structure.md index 4d9e480c3..f7e1065de 100644 --- a/en/getting-started/directory-structure.md +++ b/en/getting-started/directory-structure.md @@ -11,6 +11,9 @@ The default file structure can make you better start project advancement, and yo ``` goravel/ ├── app/ # Core application logic +│ ├── ai/ # AI agents and tools +│ │ ├── agents/ # AI agent classes +│ │ └── tools/ # AI tool classes │ ├── console/ # Artisan console commands │ ├── grpc/ # gRPC controllers and middleware │ ├── http/ # HTTP controllers and middleware @@ -39,7 +42,7 @@ goravel/ ## Customize Directory Structure -You can customize the directory structure by calling the `WithPath()` function in the `bootstrap/app.go` file. For example, if you want to change the default `app` directory to `src`, you can modify the `bootstrap/app.go` file as follows: +You can customize the directory structure by calling the `WithPaths()` function in the `bootstrap/app.go` file. For example, if you want to change the default `app` directory to `src`, you can modify the `bootstrap/app.go` file as follows: ```go func Boot() contractsfoundation.Application { @@ -52,4 +55,16 @@ func Boot() contractsfoundation.Application { } ``` -There are many other paths you can customize, such as `Config`, `Database`, `Routes`, `Storage`, and `Resources`. Just call the corresponding method on the `paths` object to set your desired directory. +There are many other paths you can customize, such as `Config`, `Database`, `Routes`, `Storage`, and `Resources`. AI generator paths can also be customized with `Agents` and `Tools`: + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithPaths(func(paths configuration.Paths) { + paths.Agents("internal/ai/agents") + paths.Tools("internal/ai/tools") + }). + WithConfig(config.Boot). + Create() +} +``` diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index ac494c36b..297739119 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -78,7 +78,7 @@ Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, ge The initial release includes: -- Agent conversations with `make:agent`, `make:tool`, `Prompt`, `Stream`, tool calling, prompt middleware, provider failover. +- Agent conversations with `make:agent`, `make:tool`, customizable AI generator paths, `Prompt`, `Stream`, tool calling, prompt middleware, provider failover. - Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs. - Image and audio generation with `Store` / `StoreAs` helpers, plus transcription with language and diarization options. From ba2f259856060ad712ac9032905f7dbcb3f1244e Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 13 Jun 2026 17:17:21 +0800 Subject: [PATCH 10/35] docs: document mail content breaking change --- en/digging-deeper/mail.md | 28 ++++++++++++++--- en/upgrade/v1.18.md | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/en/digging-deeper/mail.md b/en/digging-deeper/mail.md index d4b9937dd..ca78e930c 100644 --- a/en/digging-deeper/mail.md +++ b/en/digging-deeper/mail.md @@ -25,6 +25,17 @@ err := facades.Mail().To([]string{"example@example.com"}). Send() ``` +To send raw plain-text body content, set the `Text` field. Goravel sends `Text` as the plain-text email part and does not render it as a template path: + +```go +import "github.com/goravel/framework/mail" + +err := facades.Mail().To([]string{"example@example.com"}). + Subject("Subject"). + Content(mail.Content{Text: "Hello Goravel"}). + Send() +``` + ## Send Mail By Queue ```go @@ -104,7 +115,10 @@ func (m *OrderShipped) Attachments() []string { } func (m *OrderShipped) Content() *mail.Content { - return &mail.Content{Html: "

Hello Goravel

"} + return &mail.Content{ + Html: "

Hello Goravel

", + Text: "Hello Goravel", + } } func (m *OrderShipped) Envelope() *mail.Envelope { @@ -162,16 +176,23 @@ Create your email templates in the specified views directory. For example:

Thank you for joining {{.AppName}}.

``` +```text +# resources/views/mail/welcome.txt +Welcome {{.Name}}! +Thank you for joining {{.AppName}}. +``` + ### Sending Emails with Templates -You can use the `Content` method to specify the template and pass dynamic data: +You can use the `Content` method to specify the template and pass dynamic data. Use `HtmlView` for HTML templates and `TextView` for plain-text templates: ```go facades.Mail(). To([]string{"user@example.com"}). Subject("Welcome"). Content(mail.Content{ - View: "welcome.tmpl", + HtmlView: "welcome.html", + TextView: "welcome.txt", With: map[string]any{ "Name": "John", "AppName": "Goravel", @@ -197,4 +218,3 @@ You can also register custom template engines in the configuration: }, } ``` - diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 297739119..e0e83047c 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -17,6 +17,10 @@ - [Artisan console supports table output](#artisan-console-supports-table-output) - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) +## Breaking Changes 🛠 + +- [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) + ## Upgrade Guide ### 1. Update dependencies @@ -70,6 +74,34 @@ rules := map[string]any{ } ``` +### 3. Update mail template content fields + +If your application uses mail templates, update every `mail.Content` value that points to a template file: + +- Replace `View` with `HtmlView` for HTML templates. +- Replace `Text` with `TextView` if the value is a plain-text template path. +- Keep `Text` only when the value is the raw plain-text email body. + +For template-based emails, migrate fields like this: + +```diff + Content(mail.Content{ +- View: "welcome.html", +- Text: "welcome.txt", ++ HtmlView: "welcome.html", ++ TextView: "welcome.txt", + With: map[string]any{ + "Name": "Goravel", + }, + }) +``` + +For raw plain-text emails, use `Text` directly: + +```go +Content(mail.Content{Text: "Hello Goravel"}) +``` + ## Feature Introduction ### AI SDK @@ -289,3 +321,34 @@ func (receiver *ReportCommand) Handle(ctx console.Context) error { Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. [View Document](../the-basics/logging.md#inject-context) + +### Mail content separates raw text from template views + +Goravel now treats `mail.Content.Text` as raw plain-text email body content. Template paths are explicit: use `HtmlView` for HTML templates and `TextView` for plain-text templates. + +Use `Html` and `Text` when the message body is already available: + +```go +func (m *WelcomeMail) Content() *mail.Content { + return &mail.Content{ + Html: "

Hello Goravel

", + Text: "Hello Goravel", + } +} +``` + +Use `HtmlView` and `TextView` when the message body should be rendered from templates: + +```go +func (m *WelcomeMail) Content() *mail.Content { + return &mail.Content{ + HtmlView: "welcome.html", + TextView: "welcome.txt", + With: map[string]any{ + "Name": "Goravel", + }, + } +} +``` + +[View Document](../digging-deeper/mail.md#using-template) From 26ab56748d475f3c389266b7b751a4cff194df5a Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 14 Jun 2026 13:23:30 +0800 Subject: [PATCH 11/35] optimize --- en/ai/sdk.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/en/ai/sdk.md b/en/ai/sdk.md index 72b4569b6..b023114f3 100644 --- a/en/ai/sdk.md +++ b/en/ai/sdk.md @@ -284,6 +284,18 @@ attachment := document.FromStorage( ) ``` +Use `WithTitle` to set a display title for an attachment. This is especially useful when creating attachments from strings or byte slices, where there is no natural filename: + +```go +attachment := frameworkai.DocumentFromString( + "The alpha release ships on July 1, 2026.", + frameworkai.WithMimeType("text/plain"), + frameworkai.WithTitle("Release Schedule"), +) +``` + +When an attachment resolves from a natural source (path, URL, storage, or upload), the resolved filename overrides the title. Use `WithTitle` on attachments where you want a descriptive label instead of an auto-detected name. The option is available on all attachment constructors, including `document.WithTitle` and `image.WithTitle`. + `Stream` accepts the same attachment option: ```go From c79854af649afda1456818dfd2f0175a7414bc12 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 19 Jun 2026 17:20:21 +0800 Subject: [PATCH 12/35] optimize --- en/digging-deeper/artisan-console.md | 52 ++++++++++++++++++++++++++++ en/upgrade/v1.18.md | 45 ++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 920be3001..0be67552a 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -529,6 +529,58 @@ ctx.Divider() // ---------- ctx.Divider("=>") // =>=>=>=>=> ``` +## Graceful Shutdown + +By default, pressing `Ctrl+C` (or sending `SIGTERM`) cancels the `console.Context` passed to `Handle`. The framework runs `Handle` in a goroutine, so it returns `context.Canceled` as soon as the signal fires and the process exits. Commands that need to release resources — closing network listeners, draining queues, flushing buffers — can opt into a cleanup callback by implementing the optional `console.Shutdownable` interface. + +```go +type Shutdownable interface { + Shutdown(ctx Context) error +} +``` + +When a command implements `Shutdownable`, the framework races `Handle` against the signal context. If `Handle` returns first, the framework then calls `Shutdown` with a fresh `console.Context` (the original is already cancelled) and a 30s budget so the command can finish any cleanup. If the signal fires first, the framework calls `Shutdown` with the same fresh context and 30s budget, then returns; `Handle` is left to run on its own in the goroutine and the process exits. + +`console.Context` now embeds `context.Context`, so commands can use `<-ctx.Done()` directly, pass `ctx` to functions that expect a `context.Context`, and call `ctx.Deadline()` / `ctx.Err()` / `ctx.Value(key)` without an accessor. + +```go +package commands + +import ( + "errors" + "net/http" + + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/console/command" +) + +type Serve struct { + server *http.Server +} + +func (r *Serve) Signature() string { return "serve" } +func (r *Serve) Description() string { return "Start the HTTP server" } +func (r *Serve) Extend() command.Extend { + return command.Extend{Category: "server"} +} + +func (r *Serve) Handle(ctx console.Context) error { + if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +func (r *Serve) Shutdown(ctx console.Context) error { + ctx.Info("received signal, shutting down gracefully...") + return r.server.Shutdown(ctx) +} +``` + +The built-in `schedule:run` command is a real example. Its `Handle` blocks on `schedule.Run()`, and on signal the framework calls `Shutdown`, which delegates to `schedule.Shutdown(ctx)` so scheduled tasks get a chance to finish their work. + +Commands that do not implement `Shutdownable` keep the original behavior — the process exits as soon as the signal is received. + ## Category You can set a set of commands to the same category, convenient in `./artisan list`: diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index e0e83047c..e662f402c 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -15,6 +15,7 @@ - [Orm query exposes context](#orm-query-exposes-context) - [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Artisan console supports table output](#artisan-console-supports-table-output) +- [Artisan console supports graceful shutdown via Shutdownable](#artisan-console-supports-graceful-shutdown-via-shutdownable) - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) ## Breaking Changes 🛠 @@ -316,6 +317,50 @@ func (receiver *ReportCommand) Handle(ctx console.Context) error { [View Document](../digging-deeper/artisan-console.md#tables) +### Artisan console supports graceful shutdown via Shutdownable + +Console commands can now opt into graceful shutdown by implementing the optional `console.Shutdownable` interface. The framework wires a `signal.NotifyContext` through every command, so pressing `Ctrl+C` (or sending `SIGTERM`) cancels the context passed to `Handle`. If the command also implements `Shutdown(ctx Context) error`, the framework calls it with a fresh context and a 30s budget for cleanup before the process exits. `Handle` runs in a goroutine, so the signal response is immediate. + +`console.Context` now embeds `context.Context`, so commands can use `<-ctx.Done()` directly, pass `ctx` to functions that expect `context.Context`, and call `ctx.Deadline()` / `ctx.Err()` / `ctx.Value(key)` without an accessor. The `schedule:run` command is the canonical example — it implements `Shutdown` to forward cleanup to `schedule.Shutdown(ctx)`. + +```go +package commands + +import ( + "errors" + "net/http" + + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/console/command" +) + +type Serve struct { + server *http.Server +} + +func (r *Serve) Signature() string { return "serve" } +func (r *Serve) Description() string { return "Start the HTTP server" } +func (r *Serve) Extend() command.Extend { + return command.Extend{Category: "server"} +} + +func (r *Serve) Handle(ctx console.Context) error { + if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +func (r *Serve) Shutdown(ctx console.Context) error { + ctx.Info("received signal, shutting down gracefully...") + return r.server.Shutdown(ctx) +} +``` + +Commands that do not implement `Shutdownable` keep the original behavior — the process exits as soon as the signal is received. + +[View Document](../digging-deeper/artisan-console.md#graceful-shutdown) + ### Log WithContext filters framework context keys out Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. From 74fe674d40a7fcf664aa06590beddcd99905eb56 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 20 Jun 2026 08:48:02 +0800 Subject: [PATCH 13/35] optimize --- en/architecture-concepts/service-providers.md | 22 ++++++---- en/getting-started/configuration.md | 43 +++++++++++++++++++ en/upgrade/v1.18.md | 36 ++++++++++++++++ 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/en/architecture-concepts/service-providers.md b/en/architecture-concepts/service-providers.md index a1c4a3efc..9fb3bde8b 100644 --- a/en/architecture-concepts/service-providers.md +++ b/en/architecture-concepts/service-providers.md @@ -76,7 +76,7 @@ type Runner interface { } ``` -There is an example of a `RouteRunner` defined in the `ServiceProvider` to start and shutdown the `Route` service. +There is an example of an `HTTPRunner` defined in the `ServiceProvider` to start and shutdown the HTTP server. Its signature is `goravel:http`, which means it can be selectively disabled from `app.Start()` via the [app.disabled_runners](../getting-started/configuration.md#disabled-runners) config option. ```go type ServiceProvider struct {} @@ -86,39 +86,43 @@ func (r *ServiceProvider) Register(app foundation.Application) {} func (r *ServiceProvider) Boot(app foundation.Application) {} func (r *ServiceProvider) Runners(app foundation.Application) []foundation.Runner { - return []foundation.Runner{NewRouteRunner(app.MakeConfig(), app.MakeRoute())} + return []foundation.Runner{NewHTTPRunner(app.MakeConfig(), app.MakeRoute())} } ``` ```go -package route +package http import ( "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/route" ) -type RouteRunner struct { +type HTTPRunner struct { config config.Config route route.Route } -func NewRouteRunner(config config.Config, route route.Route) *RouteRunner { - return &RouteRunner{ +func NewHTTPRunner(config config.Config, route route.Route) *HTTPRunner { + return &HTTPRunner{ config: config, route: route, } } -func (r *RouteRunner) ShouldRun() bool { +func (r *HTTPRunner) Signature() string { + return "goravel:http" +} + +func (r *HTTPRunner) ShouldRun() bool { return r.route != nil && r.config.GetString("http.default") != "" } -func (r *RouteRunner) Run() error { +func (r *HTTPRunner) Run() error { return r.route.Run() } -func (r *RouteRunner) Shutdown() error { +func (r *HTTPRunner) Shutdown() error { return r.route.Shutdown() } ``` diff --git a/en/getting-started/configuration.md b/en/getting-started/configuration.md index 50e0bfdcf..2320996f6 100644 --- a/en/getting-started/configuration.md +++ b/en/getting-started/configuration.md @@ -134,3 +134,46 @@ To bring the application out of maintenance mode, run the `up` command: ```shell ./artisan up ``` + +### Disabled Runners + +When the application starts via `app.Start()`, Goravel automatically runs the auto-run [runners](../architecture-concepts/service-providers.md#runners) registered by service providers, such as the HTTP server, gRPC server, queue worker, scheduler, and telemetry. You can selectively skip specific runners in the main process by setting the `app.disabled_runners` option in `config/app.go`. The framework evaluates this list centrally, so the same option works for any current or future runner. + +```go +// config/app.go +"app": map[string]any{ + "env": config.Env("APP_ENV", "production"), + "debug": config.Env("APP_DEBUG", false), + "disabled_runners": []string{"goravel:schedule"}, +}, +``` + +The value is a slice of glob patterns matched against each runner's signature. The framework uses Go's [`path.Match`](https://pkg.go.dev/path#Match) for matching, so `*` is the only wildcard. The framework runner signatures are: + +| Signature | Runner | Started by | +| --- | --- | --- | +| `goravel:http` | `HTTPRunner` (HTTP server) | `http` service provider | +| `goravel:grpc` | `GrpcRunner` (gRPC server) | `grpc` service provider | +| `goravel:queue` | `QueueRunner` (queue worker) | `queue` service provider | +| `goravel:schedule` | `ScheduleRunner` (scheduler) | `schedule` service provider | +| `goravel:telemetry` | `TelemetryRunner` | `telemetry` service provider | + +Common patterns: + +```go +// Web container — only skip the scheduler so it runs on a dedicated container +"disabled_runners": []string{"goravel:schedule"}, + +// Scheduler-only container — disable http, queue, grpc +"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"}, + +// Disable every framework runner +"disabled_runners": []string{"goravel:*"}, + +// Kill switch: run no auto-run runners in the main process +"disabled_runners": []string{"*"}, +``` + +Patterns are evaluated in order and the first match wins. Invalid patterns (for example, an unmatched bracket) are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. + +User-defined runners that you register with `WithRunners` are not affected by the `goravel:*` namespace. Use the runner's own signature to disable it. diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index e662f402c..9af42cb64 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -17,6 +17,7 @@ - [Artisan console supports table output](#artisan-console-supports-table-output) - [Artisan console supports graceful shutdown via Shutdownable](#artisan-console-supports-graceful-shutdown-via-shutdownable) - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) +- [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) ## Breaking Changes 🛠 @@ -397,3 +398,38 @@ func (m *WelcomeMail) Content() *mail.Content { ``` [View Document](../digging-deeper/mail.md#using-template) + +### Runners can be disabled via config option + +Goravel v1.18 replaces the internal `app.auto_run` flag with a public, glob-aware `app.disabled_runners` config option. It lets you selectively skip auto-run [runners](../architecture-concepts/service-providers.md#runners) in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping `goravel:http`, `goravel:queue`, and `goravel:grpc` runners. + +The value is a slice of glob patterns matched against each runner's signature with Go's [`path.Match`](https://pkg.go.dev/path#Match): + +```go +// config/app.go +"app": map[string]any{ + "env": "production", + "debug": false, + "disabled_runners": []string{"goravel:schedule"}, +}, +``` + +The framework runner signatures are `goravel:http`, `goravel:grpc`, `goravel:queue`, `goravel:schedule`, and `goravel:telemetry`. Patterns are evaluated in order and the first match wins, so you can mix exact and wildcard matches: + +```go +// Web container — only skip the scheduler +"disabled_runners": []string{"goravel:schedule"}, + +// Scheduler-only container — disable http, queue, grpc +"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"}, + +// Disable every framework runner +"disabled_runners": []string{"goravel:*"}, + +// Kill switch: run no auto-run runners in the main process +"disabled_runners": []string{"*"}, +``` + +Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in `foundation.configureRunners()`, so any new runner added in the future picks up the same behavior for free. + +[View Document](../getting-started/configuration.md#disabled-runners) From bc7bdf5e0f1d56bf16a2b865a1f6dffce4731538 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 20 Jun 2026 13:51:40 +0800 Subject: [PATCH 14/35] optimize --- en/digging-deeper/artisan-console.md | 37 ++++++++++++++++++++++++++++ en/upgrade/v1.18.md | 26 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 0be67552a..f28f78897 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -57,6 +57,43 @@ func Boot() contractsfoundation.Application { A new command created by `make:command` will be registered automatically in the `bootstrap/commands.go::Commands()` function and the function will be called by `WithCommands`. You need register the command manually if you create the command file by yourself. +### Filtering Commands + +You may want to scope which built-in Artisan commands are registered in different environments — for example, hiding dev commands like `make:*`, `package:*`, and `vendor:publish` in production. The `WithCommandsFilter` method on `ApplicationBuilder` lets you return a positive list of command signatures to keep: + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithCommands(Commands). + WithCommandsFilter(func() []string { + if facades.Config().GetString("app.env") == "production" { + return []string{ + "up", "down", "key:generate", "about", + "schedule:*", // glob + "queue:*", // glob + } + } + return nil // keep everything in other environments + }). + WithConfig(config.Boot). + Create() +} +``` + +The callback runs once at build time and each entry is matched against `command.Signature()` in one of two ways: + +- **Exact match** (no wildcard) — the signature must match the entry exactly. +- **Glob match** (entry contains `*`) — checked using `path.Match`. `*` matches any sequence of non-`/` characters. + +The return value determines the filtering behavior: + +- **Method not called** — all commands are kept (default). +- **Return `nil`** — all commands are kept (no filter applied). +- **Return `[]string{}`** — all commands are dropped. +- **Return entries** — only commands whose signature matches an entry are kept. + +> Note: The filter applies to all commands including those added via `WithCommands`, so the user cannot bypass the filter by adding commands manually. + ### Command Structure After generating your command, assign suitable values to the signature and description properties of the struct. The `Handle` method will be called when your command is executed. You need to implement your logic in this method. diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 9af42cb64..f9ee0fb89 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -16,6 +16,7 @@ - [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Artisan console supports table output](#artisan-console-supports-table-output) - [Artisan console supports graceful shutdown via Shutdownable](#artisan-console-supports-graceful-shutdown-via-shutdownable) +- [Artisan console supports commands filtering](#artisan-console-supports-commands-filtering) - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) - [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) @@ -362,6 +363,31 @@ Commands that do not implement `Shutdownable` keep the original behavior — the [View Document](../digging-deeper/artisan-console.md#graceful-shutdown) +### Artisan console supports commands filtering + +`ApplicationBuilder` now provides a `WithCommandsFilter` method to scope which built-in Artisan commands are registered — useful for production binaries that don't need `make:*`, `package:*`, or `vendor:publish`. The callback returns a positive list of command signatures to keep, with support for exact match and glob (`*`) matching: + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithCommands(Commands). + WithCommandsFilter(func() []string { + if facades.Config().GetString("app.env") == "production" { + return []string{ + "up", "down", "key:generate", "about", + "schedule:*", + "queue:*", + } + } + return nil // keep everything in other environments + }). + WithConfig(config.Boot). + Create() +} +``` + +[View Document](../digging-deeper/artisan-console.md#filtering-commands) + ### Log WithContext filters framework context keys out Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. From c5825c62f8f0e4bdd859764d23600b040c8b87a6 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 20 Jun 2026 22:13:54 +0800 Subject: [PATCH 15/35] optimize --- en/digging-deeper/package-development.md | 12 ++++++++++++ en/the-basics/views.md | 17 ++++++++++++++++- en/upgrade/v1.18.md | 14 ++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/package-development.md b/en/digging-deeper/package-development.md index bd39a11a9..d1885a133 100644 --- a/en/digging-deeper/package-development.md +++ b/en/digging-deeper/package-development.md @@ -161,6 +161,18 @@ func (receiver *ServiceProvider) Boot(app foundation.Application) { } ``` +### Views + +If your package provides default [views](../the-basics/views.md), register their directory using `LoadViewsFrom` in your service provider's `Boot` method: + +```go +func (receiver *ServiceProvider) Boot(app foundation.Application) { + facades.View().LoadViewsFrom("/path/to/package/views") +} +``` + +The application's `resources/views` directory takes priority over registered package views, so users can override any package view by creating a file with the same name. + ## Commands You can register `Artisan` command by the `Commands` method, you can run the commands using [Artisan CLI](../digging-deeper/artisan-console.md) after registering them. diff --git a/en/the-basics/views.md b/en/the-basics/views.md index 295ec21c9..125071131 100644 --- a/en/the-basics/views.md +++ b/en/the-basics/views.md @@ -58,7 +58,7 @@ ctx.Response().View().First([]string{"custom/admin.tmpl", "admin.tmpl"}, map[str ### Determining If A View Exists -If you need to determine if a view exists, you can use the `facades.View()` method: +If you need to determine if a view exists, you can use the `facades.View()` method. It checks `resources/views/` as well as any directories registered with `LoadViewsFrom`: ```go if facades.View().Exist("welcome.tmpl") { @@ -66,6 +66,21 @@ if facades.View().Exist("welcome.tmpl") { } ``` +## Registering Views From Packages + +Extension packages may register their own view directory using the `LoadViewsFrom` method. This allows packages to provide default views without cluttering the user's `resources/views` directory: + +```go +// In an extension package's service_provider.go +func (r *ServiceProvider) Boot(app foundation.Application) { + facades.View().LoadViewsFrom("/path/to/package/views") +} +``` + +When a view is rendered, the application's `resources/views` directory takes priority — users can override any package view by creating a file with the same name in `resources/views`. If the view is not found there, the registered package view directories are searched in registration order as fallbacks. + +The `Exist` method also checks registered package view paths in addition to `resources/views/`. + ## Passing Data To Views As you saw in the previous examples, you may pass an array of data to views to make that data available to the view. Please note, the format of the passed data needs to change according to the template driver used, in the following example, using the default `html/template` driver: diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index f9ee0fb89..e33962c67 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -18,6 +18,7 @@ - [Artisan console supports graceful shutdown via Shutdownable](#artisan-console-supports-graceful-shutdown-via-shutdownable) - [Artisan console supports commands filtering](#artisan-console-supports-commands-filtering) - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) +- [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) - [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) ## Breaking Changes 🛠 @@ -425,6 +426,19 @@ func (m *WelcomeMail) Content() *mail.Content { [View Document](../digging-deeper/mail.md#using-template) +### Package views via LoadViewsFrom + +Extension packages can now register their own view directories with `facades.View().LoadViewsFrom(path)`. The application's `resources/views` directory takes priority, and registered package views serve as defaults. The `Exist` method also checks registered package view paths. + +```go +// In an extension package's service_provider.go +func (r *ServiceProvider) Boot(app foundation.Application) { + facades.View().LoadViewsFrom("/path/to/package/views") +} +``` + +[View Document](../the-basics/views.md#registering-views-from-packages) + ### Runners can be disabled via config option Goravel v1.18 replaces the internal `app.auto_run` flag with a public, glob-aware `app.disabled_runners` config option. It lets you selectively skip auto-run [runners](../architecture-concepts/service-providers.md#runners) in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping `goravel:http`, `goravel:queue`, and `goravel:grpc` runners. From 00e3e6edee1498f514680ddc10a3c09560464fb9 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 21 Jun 2026 14:03:55 +0800 Subject: [PATCH 16/35] optimize --- en/the-basics/validation.md | 61 +++++++++++++++++++++++++++++++++++++ en/upgrade/v1.18.md | 16 ++++++++++ 2 files changed, 77 insertions(+) diff --git a/en/the-basics/validation.md b/en/the-basics/validation.md index aa474a75a..631338165 100644 --- a/en/the-basics/validation.md +++ b/en/the-basics/validation.md @@ -377,6 +377,52 @@ if validator.Errors().Has("email") { } ``` +## Translation & Localization + +### Using Translation for Validation Messages + +Validation error messages are now powered by the framework's [localization](../digging-deeper/localization.md) system. Default messages are embedded in the framework and automatically use the application's current locale. The message lookup follows this order: + +1. Form-request `Messages()` or explicit `validation.Messages()` overrides (`field.rule`, then `rule`) +2. Custom rule's `Message()` method +3. Translation file lookup with `validation.*` keys (e.g., `validation.required`, `validation.min.string`) +4. Embedded default messages + +For size-based rules like `min`, `max`, `between`, `size`, `gt`, `gte`, `lt`, and `lte`, the translation key includes the attribute type suffix (e.g., `min.string`, `min.numeric`, `min.array`, `min.file`). + +### Publishing Validation Language Files + +Use the `lang:publish` Artisan command to publish the validation language files to your application's `lang` directory: + +```shell +go run . artisan lang:publish +``` + +This copies the validation translation files from the framework into `lang/en/validation.json`, where you can customize the default error messages. + +To overwrite existing files, use the `--force` (or `-f`) flag: + +```shell +go run . artisan lang:publish --force +``` + +### Customizing Default Messages + +After publishing, edit `lang/en/validation.json` to customize any validation message. For example: + +```json +{ + "required": "You forgot the :attribute field!", + "email": "The :attribute must be a valid email address." +} +``` + +These custom messages will be used by all validators in your application, unless overridden by form request `Messages()` or explicit `validation.Messages()`. + +### Translating to Other Languages + +To support additional languages, create a copy of the published `lang/en/validation.json` in the corresponding language directory (e.g., `lang/zh/validation.json`) and translate the messages. The framework will use the appropriate language file based on the current locale set by `facades.App().SetLocale(ctx, "zh")`. + ## Available Validation Rules Validation rule names now use snake_case by default. Rules and filters are defined with `map[string]any`; each value must be either a pipe-separated `string` or a `[]string` of rule names. @@ -482,6 +528,21 @@ func (receiver *Uppercase) Message(ctx context.Context) string { } ``` +Custom rule messages support the following placeholders: + +| Placeholder | Description | +| --- | --- | +| `:attribute` | The field name or custom attribute | +| `:value` | The field value being validated | +| `:option0`, `:option1`, ... | Rule parameters passed to `Passes()` | + +```go +// Example with all placeholders +func (receiver *Between) Message(ctx context.Context) string { + return "The :attribute value :value must be between :option0 and :option1." +} +``` + ### Register Custom Rules A new rule created by `make:rule` will be registered automatically in the `bootstrap/rules.go::Rules()` function and the function will be called by `WithRules`. You need register the rule manually if you create the rule file by yourself. diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index e33962c67..0b1151ff9 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -9,6 +9,7 @@ - [Laravel-style collections](#laravel-style-collections) - [Native validation engine and built-in rules](#native-validation-engine-and-built-in-rules) +- [Validation supports translation-based error messages and lang:publish command](#validation-supports-translation-based-error-messages-and-langpublish-command) - [Maintenance mode commands](#maintenance-mode-commands) - [Build command supports go generate](#build-command-supports-go-generate) - [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) @@ -185,6 +186,21 @@ scores := validated["scores"].([]int) [View Document](../the-basics/validation.md) +### Validation supports translation-based error messages and lang:publish command + +Validation error messages are now powered by the framework's [localization](../digging-deeper/localization.md) system. Default messages are embedded as `lang/en/validation.json` and use `validation.*` translation keys (e.g., `validation.required`, `validation.min.string`). + +Use the new `lang:publish` Artisan command to publish the validation language files to your application: + +```shell +go run . artisan lang:publish +go run . artisan lang:publish --force +``` + +After publishing, edit `lang/en/validation.json` to customize default error messages. Create additional language directories (e.g., `lang/zh/validation.json`) for other locales. Custom rule messages now support `:value` and `:optionN` placeholders. + +[View Document](../the-basics/validation.md#translation--localization) + ### Laravel-style collections Goravel now provides fluent eager and lazy collection utilities in the `support/collect` package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style `Where` comparisons for structs, aggregate values, and process large datasets with `LazyCollection`. From a260ac54585ddc14915bbe0e55e7449b906b759c Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 23 Jun 2026 16:38:06 +0800 Subject: [PATCH 17/35] optimize --- en/the-basics/grpc.md | 12 ++++++------ en/upgrade/v1.18.md | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/en/the-basics/grpc.md b/en/the-basics/grpc.md index 7a79f90ce..29f05274d 100644 --- a/en/the-basics/grpc.md +++ b/en/the-basics/grpc.md @@ -8,7 +8,7 @@ Grpc module can be operated by `facades.Grpc()`. Goravel provides an elegant way ## Configuration -In the `config/grpc.go` file, you can configure the Grpc module, where `grpc.host` configures the domain name of the server, and `grpc.servers` configures the servers which the client will connect to. +In the `config/grpc.go` file, you can configure the Grpc module, where `grpc.host` configures the domain name of the server, and `grpc.clients` configures the clients which will connect to gRPC services. ## Transport Credentials @@ -40,7 +40,7 @@ func Boot() contractsfoundation.Application { ### Client Credentials -Register client credential groups in `bootstrap/app.go`, then reference the group name in `config/grpc.go` with `grpc.servers..credentials`: +Register client credential groups in `bootstrap/app.go`, then reference the group name in `config/grpc.go` with `grpc.clients..credentials`: ```go import "google.golang.org/grpc/credentials" @@ -232,9 +232,9 @@ func Boot() foundation.Application { } ``` -### Apply Interceptors to Servers +### Apply Interceptors to Clients -The `default` in the example above is a group name that can be applied to the configuration item `grpc.servers.interceptors`. In this way, the client will be applied to all interceptors under the group. +The `default` in the example above is a group name that can be applied to the configuration item `grpc.clients.interceptors`. In this way, the client will use all interceptors under the group. ```go package config @@ -249,7 +249,7 @@ func init() { "host": config.Env("GRPC_HOST", ""), "port": config.Env("GRPC_PORT", ""), - "servers": map[string]any{ + "clients": map[string]any{ "user": map[string]any{ "host": config.Env("GRPC_USER_HOST", ""), "port": config.Env("GRPC_USER_PORT", ""), @@ -392,7 +392,7 @@ func init() { "host": config.Env("GRPC_HOST", ""), "port": config.Env("GRPC_PORT", ""), - "servers": map[string]any{ + "clients": map[string]any{ "user": map[string]any{ "host": config.Env("GRPC_USER_HOST", ""), "port": config.Env("GRPC_USER_PORT", ""), diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 0b1151ff9..c12acfd8a 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -25,6 +25,7 @@ ## Breaking Changes 🛠 - [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) +- [gRPC config key renamed from servers to clients](#grpc-config-key-renamed-from-servers-to-clients) ## Upgrade Guide @@ -107,6 +108,28 @@ For raw plain-text emails, use `Text` directly: Content(mail.Content{Text: "Hello Goravel"}) ``` +### 4. Rename grpc.servers config key to grpc.clients + +The gRPC config key `grpc.servers` has been renamed to `grpc.clients` to better reflect that these entries configure outbound client connections. Update `config/grpc.go`: + +```diff + config.Add("grpc", map[string]any{ + "host": config.Env("GRPC_HOST", ""), + "port": config.Env("GRPC_PORT", ""), +- "servers": map[string]any{ ++ "clients": map[string]any{ + "user": map[string]any{ + "host": config.Env("GRPC_USER_HOST", ""), + "port": config.Env("GRPC_USER_PORT", ""), + "interceptors": []string{}, + "stats_handlers": []string{}, + }, + }, + }) +``` + +Any code that reads config values at paths like `grpc.servers.*` must be updated to `grpc.clients.*`. + ## Feature Introduction ### AI SDK @@ -266,7 +289,7 @@ The `build` command now supports the `--generate` flag and `-g` alias. When enab ### gRPC supports TLS and mTLS credentials -Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use `WithGrpcServerCredentials` to enable TLS or mTLS for the server, and `WithGrpcClientCredentials` to register client credential groups that can be referenced by `grpc.servers..credentials`. +Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use `WithGrpcServerCredentials` to enable TLS or mTLS for the server, and `WithGrpcClientCredentials` to register client credential groups that can be referenced by `grpc.clients..credentials`. ```go return foundation.Setup(). From 99117c17413f80adc65d173312c82dc374855ad1 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Fri, 26 Jun 2026 12:19:01 +0530 Subject: [PATCH 18/35] docs: explain the Tracer/Meter instrumentation scope name Document that the Tracer and Meter argument is the instrumentation scope name that identifies the code producing the span or metric, recorded as otel.scope.name. Covers the Tracing and Metrics sections and the v1.18 upgrade example, answering what the "app" argument means. --- en/digging-deeper/telemetry.md | 4 ++++ en/upgrade/v1.18.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index ecdbb3bb4..466ea5622 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -208,6 +208,8 @@ Metrics use a periodic reader instead, configured by `metrics.reader.interval` ( To create a span, request a tracer from the `Telemetry` facade using the `Tracer` method, then call `Start`. The first argument is a `context.Context`, if the context already contains a span (for example, one started by the HTTP middleware), the new span is automatically attached as its child. +The `Tracer` argument (`"app"` above) is the **instrumentation scope name**: it identifies the code that produced the span. Use a stable name that points back to the instrumenting code, by convention the package import path (for example `github.com/you/app/services`) or, for application code, the application or module name. It is recorded on every span as `otel.scope.name`, so your backend can group and filter spans by the component that emitted them. The framework's built-in instrumentation uses its own scope (for example `github.com/goravel/framework/telemetry/instrumentation/http`), which keeps your spans distinct from the framework's. + The following service traces an order through its processing steps: the `Process` method opens a span, records what happened on it, and passes the returned context down so that `chargePayment` becomes a child span within the same trace: ```go @@ -349,6 +351,8 @@ If there is no active span in the context, a no-op span is returned, so it is al To record metrics, request a meter from the `Telemetry` facade using the `Meter` method, then create instruments from it. Instruments are safe for concurrent use and should be created once and reused, a common pattern is to create them when the service is constructed and record values in its methods. +Like `Tracer`, the `Meter` argument is the **instrumentation scope name** that identifies the code producing the metrics (see [Creating Spans](#creating-spans)). It is recorded as `otel.scope.name` on the exported metrics. + The following service uses a **counter** (a value that only goes up, ideal for counting processed payments or sent emails) and a **histogram** (a distribution of values, such as durations or payload sizes): ```go diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index c12acfd8a..df5b90f28 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -182,6 +182,8 @@ span.SetAttributes(telemetry.String("order.id", "1234")) processItems(ctx) ``` +The `Tracer` argument (`"app"`) is the instrumentation scope name that identifies the code producing the span; it is recorded as `otel.scope.name`. By convention it is the package import path or, for application code, the application name. + [View Document](../digging-deeper/telemetry.md) ### Native validation engine and built-in rules From f09f7e46e9f3b7373029faefc3d0f8b5dc6ab171 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 27 Jun 2026 11:32:08 +0800 Subject: [PATCH 19/35] docs: add mock factory docs for AI, DB, Route, Schedule, Schema, Session, Testing facades --- en/testing/mock.md | 223 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/en/testing/mock.md b/en/testing/mock.md index 4f96d1e4b..a8c321e76 100644 --- a/en/testing/mock.md +++ b/en/testing/mock.md @@ -6,6 +6,48 @@ All functions of Goravel are implemented using `facades`, and all `facades` are made up of interfaces. So with the mock function from [stretchr/testify](http://github.com/stretchr/testify), Goravel can deliver an exceptional testing experience. +## Mock facades.AI + +```go +import ( + "testing" + + "github.com/goravel/framework/testing/mock" + mocksai "github.com/goravel/framework/mocks/ai" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func AI() (string, error) { + conversation, err := facades.AI().Agent(myAgent) + if err != nil { + return "", err + } + + resp, err := conversation.Prompt("Hello") + if err != nil { + return "", err + } + + return resp.Content(), nil +} + +func TestAI(t *testing.T) { + mockFactory := mock.Factory() + mockAI := mockFactory.AI() + mockConversation := mocksai.NewConversation(t) + mockResponse := mocksai.NewAgentResponse(t) + mockAI.EXPECT().Agent(mock.Anything).Return(mockConversation, nil).Once() + mockConversation.EXPECT().Prompt("Hello").Return(mockResponse, nil).Once() + mockResponse.EXPECT().Content().Return("mock response").Once() + + content, err := AI() + assert.Nil(t, err) + assert.Equal(t, "mock response", content) +} +``` + ## Mock facades.App ```go @@ -157,6 +199,36 @@ func TestCrypt(t *testing.T) { } ``` +## Mock facades.DB + +```go +import ( + "testing" + + "github.com/goravel/framework/contracts/database/db" + "github.com/goravel/framework/testing/mock" + mocksdb "github.com/goravel/framework/mocks/database/db" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func DB() error { + _, err := facades.DB().Table("users").Insert(map[string]any{"name": "Goravel"}) + return err +} + +func TestDB(t *testing.T) { + mockFactory := mock.Factory() + mockDB := mockFactory.DB() + mockQuery := mocksdb.NewQuery(t) + mockDB.EXPECT().Table("users").Return(mockQuery).Once() + mockQuery.EXPECT().Insert(mock.Anything).Return(&db.Result{RowsAffected: 1}, nil).Once() + + assert.Nil(t, DB()) +} +``` + ## Mock facades.Event ```go @@ -476,6 +548,157 @@ func TestStorage(t *testing.T) { ``` +## Mock facades.Testing + +```go +import ( + "testing" + + "github.com/goravel/framework/testing/mock" + mockstesting "github.com/goravel/framework/mocks/testing" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func Testing() bool { + return facades.Testing().Docker().Ready() +} + +func TestTesting(t *testing.T) { + mockFactory := mock.Factory() + mockTesting := mockFactory.Testing() + mockDocker := mockstesting.NewDocker(t) + mockTesting.EXPECT().Docker().Return(mockDocker).Once() + mockDocker.EXPECT().Ready().Return(true).Once() + + assert.True(t, Testing()) +} +``` + +## Mock facades.Session + +```go +import ( + "testing" + + "github.com/goravel/framework/testing/mock" + mockssession "github.com/goravel/framework/mocks/session" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func Session() error { + driver, err := facades.Session().Driver("redis") + if err != nil { + return err + } + + _, err = facades.Session().BuildSession(driver) + return err +} + +func TestSession(t *testing.T) { + mockFactory := mock.Factory() + mockSession := mockFactory.Session() + mockDriver := mockssession.NewDriver(t) + mockSession.EXPECT().Driver("redis").Return(mockDriver, nil).Once() + mockSession.EXPECT().BuildSession(mockDriver).Return(nil, nil).Once() + + assert.Nil(t, Session()) +} +``` + +## Mock facades.Schema + +```go +import ( + "testing" + + contractschema "github.com/goravel/framework/contracts/database/schema" + "github.com/goravel/framework/testing/mock" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func Schema() error { + return facades.Schema().Create("users", func(blueprint contractschema.Blueprint) { + blueprint.ID() + blueprint.String("name") + }) +} + +func TestSchema(t *testing.T) { + mockFactory := mock.Factory() + mockSchema := mockFactory.Schema() + mockSchema.EXPECT().Create("users", mock.Anything).Return(nil).Once() + + assert.Nil(t, Schema()) +} +``` + +## Mock facades.Schedule + +```go +import ( + "testing" + + "github.com/goravel/framework/testing/mock" + mocksschedule "github.com/goravel/framework/mocks/schedule" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func Schedule() error { + return facades.Schedule().Command("my-command").EveryMinute() +} + +func TestSchedule(t *testing.T) { + mockFactory := mock.Factory() + mockSchedule := mockFactory.Schedule() + mockEvent := mocksschedule.NewEvent(t) + mockSchedule.EXPECT().Command("my-command").Return(mockEvent).Once() + mockEvent.EXPECT().EveryMinute().Return(nil).Once() + + assert.Nil(t, Schedule()) +} +``` + +## Mock facades.Route + +```go +import ( + "testing" + + "github.com/goravel/framework/http" + "github.com/goravel/framework/testing/mock" + "github.com/stretchr/testify/assert" + + "goravel/app/facades" +) + +func Route() { + facades.Route().Get("/api/users", func(ctx http.Context) http.Response { + return ctx.Response().Success().Json(http.Json{ + "users": []string{"Goravel"}, + }) + }) +} + +func TestRoute(t *testing.T) { + mockFactory := mock.Factory() + mockRoute := mockFactory.Route() + mockRoute.EXPECT().Get("/api/users", mock.Anything).Once() + + assert.NotPanics(t, func() { + Route() + }) +} +``` + ## Mock facades.Validation ```go From dd8cfb898d8dcd12f00d1ea17420f9593d758b29 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 27 Jun 2026 15:52:59 +0800 Subject: [PATCH 20/35] optimize --- en/the-basics/middleware.md | 28 ++++++++++++++++++++++++++ en/the-basics/routing.md | 40 +++++++++++++++++++++++++++++++++++-- en/upgrade/v1.18.md | 31 ++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/en/the-basics/middleware.md b/en/the-basics/middleware.md index beed2adbb..702b74f85 100644 --- a/en/the-basics/middleware.md +++ b/en/the-basics/middleware.md @@ -71,6 +71,34 @@ import "github.com/goravel/framework/http/middleware" facades.Route().Middleware(middleware.Auth()).Get("users", userController.Show) ``` +### Excluding Middleware + +The `WithoutMiddleware` method allows specific routes to bypass certain middleware that would otherwise be applied by parent groups. This is useful for public endpoints, webhook handlers, or routes that should skip authentication or rate limiting. + +Use `WithoutMiddleware` on individual routes after `Middleware` is applied to a group: + +```go +facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) { + router.Get("/dashboard", dashboardController.Index) + + // This route excludes the throttle middleware + router.Get("/api/webhook", webhookController.Handle). + WithoutMiddleware(middleware.Throttle("global")) +}) +``` + +You can also exclude middleware for an entire group: + +```go +facades.Route().Middleware(middleware.Auth()). + WithoutMiddleware(middleware.Auth()). + Group(func(router route.Router) { + router.Get("/public", publicController.Index) + }) +``` + +> **Note**: Middleware exclusion uses reflection to compare types. Closure-based middleware (inline `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for reliable exclusion. + ## Abort Request In middleware, if you need to interrupt the request, you can use the `Abort` method. diff --git a/en/the-basics/routing.md b/en/the-basics/routing.md index c02ca1cbe..a8c667e5d 100644 --- a/en/the-basics/routing.md +++ b/en/the-basics/routing.md @@ -58,8 +58,9 @@ Use the `route:list` command to view routes list: | Static | [File Routing](#file-routing) | | StaticFile | [File Routing](#file-routing) | | StaticFS | [File Routing](#file-routing) | -| Middleware | [Middleware](#middleware) | -| GetRoutes | [Get All Routes](#get-all-routes) | +| Middleware | [Middleware](#middleware) | +| WithoutMiddleware | [Without Middleware](#without-middleware) | +| GetRoutes | [Get All Routes](#get-all-routes) | | Name | [Set Route Name](#set-route-name) | | Info | [Get Route Info](#get-route-info) | @@ -151,6 +152,41 @@ facades.Route().Middleware(middleware.Cors()).Get("users", userController.Show) Detail [Middleware](./middleware.md) +## Without Middleware + +The `WithoutMiddleware` method allows you to exclude specific middleware from routes that would otherwise be applied by parent groups. This is useful for webhook endpoints, public APIs, or any route that should bypass certain middleware (like authentication or rate limiting). + +### Route-Level Exclusion + +```go +import "github.com/goravel/framework/http/middleware" + +facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) { + // This route has both middlewares + router.Get("/dashboard", dashboardController.Index) + + // This route excludes the throttle middleware + router.Get("/api/webhook", webhookController.Handle). + WithoutMiddleware(middleware.Throttle("global")) +}) +``` + +### Group-Level Exclusion + +Exclude middleware for all routes within a group: + +```go +facades.Route().Middleware(middleware.Auth()). + WithoutMiddleware(middleware.Auth()). + Group(func(router route.Router) { + // All routes in this group bypass Auth middleware + router.Get("/public", publicController.Index) + router.Get("/public/{id}", publicController.Show) + }) +``` + +> **Note**: The middleware comparison uses reflection to match middleware types. Closure-based middleware (created inline with `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for `WithoutMiddleware` to work correctly. + ## Get All Routes ```go diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index df5b90f28..b7a3eaed3 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -21,6 +21,7 @@ - [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) - [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) - [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) +- [WithoutMiddleware for routes](#withoutmiddleware-for-routes) ## Breaking Changes 🛠 @@ -514,3 +515,33 @@ The framework runner signatures are `goravel:http`, `goravel:grpc`, `goravel:que Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in `foundation.configureRunners()`, so any new runner added in the future picks up the same behavior for free. [View Document](../getting-started/configuration.md#disabled-runners) + +### WithoutMiddleware for routes + +Goravel v1.18 introduces the `WithoutMiddleware` method for both routes and route groups, similar to Laravel's `withoutMiddleware()`. This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting. + +Exclude middleware on individual routes from within a middleware group: + +```go +facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) { + router.Get("/dashboard", dashboardController.Index) + + // This route excludes the throttle middleware + router.Get("/api/webhook", webhookController.Handle). + WithoutMiddleware(middleware.Throttle("global")) +}) +``` + +Exclude middleware for all routes within a group: + +```go +facades.Route().Middleware(middleware.Auth()). + WithoutMiddleware(middleware.Auth()). + Group(func(router route.Router) { + router.Get("/public", publicController.Index) + }) +``` + +> **Note**: Middleware comparison uses reflection to match middleware types. Closure-based middleware (inline `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for reliable exclusion. + +[View Document](../the-basics/routing.md#without-middleware) From 7551b2f2e9587b3ec84872b19efe609000c0540e Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 28 Jun 2026 10:41:10 +0800 Subject: [PATCH 21/35] optimize --- en/the-basics/middleware.md | 14 +++++++---- en/the-basics/routing.md | 2 +- en/upgrade/v1.18.md | 48 +++++++++++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/en/the-basics/middleware.md b/en/the-basics/middleware.md index 702b74f85..5e1b0429a 100644 --- a/en/the-basics/middleware.md +++ b/en/the-basics/middleware.md @@ -17,10 +17,14 @@ import ( "github.com/goravel/framework/contracts/http" ) -func Auth() http.Middleware { - return func(ctx http.Context) { - ctx.Request().Next() - } +type Auth struct{} + +func (a *Auth) Handle(ctx http.Context) { + ctx.Request().Next() +} + +func (a *Auth) Signature() string { + return "auth" } ``` @@ -97,7 +101,7 @@ facades.Route().Middleware(middleware.Auth()). }) ``` -> **Note**: Middleware exclusion uses reflection to compare types. Closure-based middleware (inline `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for reliable exclusion. +> **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. ## Abort Request diff --git a/en/the-basics/routing.md b/en/the-basics/routing.md index a8c667e5d..7e8b129ea 100644 --- a/en/the-basics/routing.md +++ b/en/the-basics/routing.md @@ -185,7 +185,7 @@ facades.Route().Middleware(middleware.Auth()). }) ``` -> **Note**: The middleware comparison uses reflection to match middleware types. Closure-based middleware (created inline with `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for `WithoutMiddleware` to work correctly. +> **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. ## Get All Routes diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index b7a3eaed3..103808ded 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -25,6 +25,7 @@ ## Breaking Changes 🛠 +- [HTTP Middleware changed from func to interface](#http-middleware-changed-from-func-to-interface) - [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) - [gRPC config key renamed from servers to clients](#grpc-config-key-renamed-from-servers-to-clients) @@ -62,7 +63,29 @@ go mod tidy goravel upgrade ``` -### 2. Update validation rule and filter maps +### 2. Migrate middleware from func to interface + +`contracts/http.Middleware` has changed from `func(Context)` to an interface with `Handle(Context)` and `Signature() string` methods. Update all custom middleware to implement the new interface: + +```diff +-func Auth() http.Middleware { +- return func(ctx http.Context) { +- ctx.Request().Next() +- } ++type Auth struct{} ++ ++func (a *Auth) Handle(ctx http.Context) { ++ ctx.Request().Next() + } ++ ++func (a *Auth) Signature() string { ++ return "auth" ++} +``` + +The `Signature()` method provides a stable identity for each middleware, enabling `WithoutMiddleware` to reliably exclude specific middleware by signature rather than reflection. + +### 3. Update validation rule and filter maps Validation rules and filters now use `map[string]any` instead of `map[string]string`. Update request validation calls, manual validator calls, and form request `Rules` / `Filters` methods: @@ -81,7 +104,7 @@ rules := map[string]any{ } ``` -### 3. Update mail template content fields +### 4. Update mail template content fields If your application uses mail templates, update every `mail.Content` value that points to a template file: @@ -109,7 +132,7 @@ For raw plain-text emails, use `Text` directly: Content(mail.Content{Text: "Hello Goravel"}) ``` -### 4. Rename grpc.servers config key to grpc.clients +### 5. Rename grpc.servers config key to grpc.clients The gRPC config key `grpc.servers` has been renamed to `grpc.clients` to better reflect that these entries configure outbound client connections. Update `config/grpc.go`: @@ -516,6 +539,23 @@ Invalid patterns are logged as warnings and the runner is left running, so a typ [View Document](../getting-started/configuration.md#disabled-runners) +### HTTP Middleware changed from func to interface + +`contracts/http.Middleware` has changed from a function type `func(Context)` to an interface: + +```go +type Middleware interface { + Handle(Context) + Signature() string +} +``` + +This change provides each middleware with a stable identity via the `Signature()` method, enabling `WithoutMiddleware` to reliably exclude specific middleware by signature comparison. All built-in framework middlewares (`Cors`, `Timeout`, `Throttle`, `VerifyCsrfToken`, etc.) have been updated to return structs that implement this interface. + +Custom middleware must be updated to implement the new interface. See the [upgrade guide](#_2-migrate-middleware-from-func-to-interface) for migration steps. + +[View Document](../the-basics/middleware.md) + ### WithoutMiddleware for routes Goravel v1.18 introduces the `WithoutMiddleware` method for both routes and route groups, similar to Laravel's `withoutMiddleware()`. This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting. @@ -542,6 +582,6 @@ facades.Route().Middleware(middleware.Auth()). }) ``` -> **Note**: Middleware comparison uses reflection to match middleware types. Closure-based middleware (inline `func(ctx http.Context)`) share a single type and cannot be individually excluded. Use struct-based middleware for reliable exclusion. +> **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. [View Document](../the-basics/routing.md#without-middleware) From 991ebca8c086e137566c5fa43d30b00c3731c206 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 28 Jun 2026 11:17:05 +0800 Subject: [PATCH 22/35] optimize --- en/upgrade/v1.18.md | 272 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 103808ded..1ebfe32e5 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -31,6 +31,10 @@ ## Upgrade Guide +As [Golang v1.24 is no longer maintained](https://endoflife.date/go), the Golang version of Goravel supports has been upgraded from 1.24 to 1.25. + +goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: [goravel/example#93](https://github.com/goravel/example/pull/93). + ### 1. Update dependencies ```shell @@ -585,3 +589,271 @@ facades.Route().Middleware(middleware.Auth()). > **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. [View Document](../the-basics/routing.md#without-middleware) + +## Upgrade Guide for AI Agents + +This chapter is designed for AI coding agents (e.g. Cursor, Copilot, Codex) to perform the v1.17→v1.18 upgrade deterministically. You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically. + +````markdown +# Upgrade Guide for Goravel v1.18 + +**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.25 is installed, Goravel version ≥ 1.17.0. + +## Step 1: Update Go module dependencies + +**Detection**: Check which facade packages are already installed: + +```shell +rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null +``` + +**Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`: + +```shell +go get github.com/goravel/framework@latest + +# For each facade found in detection, run the corresponding go get. +# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest +# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest +# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest +# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest +# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest +# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest +# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest + +go mod tidy +``` + +**Verification**: + +```shell +go build ./... +``` + +## Step 2: Migrate HTTP middleware from func to interface + +`contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated. + +### 2a. Detect all custom middleware functions + +```shell +rg -n 'func\s+\w+\s*\(.*\)\s+http\.Middleware' --type go +rg -n 'func\s+\w+\s*\(.*\)\s+contractshttp\.Middleware' --type go +``` + +Also search for anonymous middleware (inline `return func(ctx http.Context)`): + +```shell +rg -n 'return\s+func\(ctx\s+http\.Context\)' --type go +``` + +### 2b. Convert each middleware + +For each detected middleware, keep the factory function signature and replace the closure body with a struct literal: + +```diff + func Auth() http.Middleware { +- return func(ctx http.Context) { +- ctx.Request().Next() +- } ++ return &Auth{} + } + ++type Auth struct{} ++ ++func (a *Auth) Handle(ctx http.Context) { ++ ctx.Request().Next() ++} ++ ++func (a *Auth) Signature() string { ++ return "auth" ++} +``` + +If the original factory accepted parameters, pass them to the struct literal: + +```diff + func Throttle(name string) http.Middleware { +- return func(ctx http.Context) { +- // throttle logic using name +- } ++ return &Throttle{Name: name} + } ++ ++type Throttle struct{ Name string } ++ ++func (t *Throttle) Handle(ctx http.Context) { /* throttle logic using t.Name */ } ++func (t *Throttle) Signature() string { return "throttle-" + t.Name } +``` + +Call sites like `middleware.Auth()` stay unchanged — the factory function still returns `http.Middleware`, but now returns a struct instead of a closure. + +### 2c. Verification + +```shell +# Ensure no remaining func-returning middleware patterns +rg -n 'func.*http\.Middleware' --type go + +# Build must succeed +go build ./... +``` + +## Step 3: Update validation rule and filter maps + +Validation rules and filters now use `map[string]any` instead of `map[string]string`. + +### 3a. Detect affected files + +```shell +rg -n 'map\[string\]string.*Validate\(' --type go +rg -n 'map\[string\]string.*Make\(' --type go +rg -n 'Rules\(\).*map\[string\]string' --type go +rg -n 'Filters\(\).*map\[string\]string' --type go +``` + +Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations: + +```shell +rg -n '\.Validate\(.*map\[string\]' --type go +rg -n 'validation\.Make\(.*map\[string\]' --type go +``` + +### 3b. Apply the transformation + +Replace `map[string]string` with `map[string]any` in all validation contexts: + +```diff +-ctx.Request().Validate(map[string]string{ ++ctx.Request().Validate(map[string]any{ + "title": "required|max:255", + }) +``` + +If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`: + +```diff + rules := map[string]string{ +- "slug": "required|regex:^(news|docs)-[a-z]+$|string", ++ "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, + } +``` + +### 3c. Verification + +```shell +# Ensure no remaining map[string]string in validation contexts +rg -n 'Validate\(.*map\[string\]string' --type go +rg -n 'Make\(.*map\[string\]string' --type go + +go build ./... +``` + +## Step 4: Update mail template content fields + +Mail content fields are now explicit: `HtmlView` for HTML templates, `TextView` for plain-text templates, `Html`/`Text` for raw body content. + +### 4a. Detect affected files + +```shell +rg -n '\.View\s*:' --type go -g '!vendor/**' -g '!storage/**' +rg -n 'Content\(.*mail\.Content\{' --type go +rg -n '&mail\.Content\{' --type go +``` + +### 4b. Apply the transformation + +For each `mail.Content{}` literal: + +| Old field | New field | Condition | +|-----------|-----------|-----------| +| `View:` | `HtmlView:` | Always (was for HTML templates) | +| `Text:` (value ends in `.html`, `.txt`, `.tmpl`, or is a file path) | `TextView:` | When the value was a template path | +| `Text:` (value is a raw string, not a path) | `Text:` | Keep as-is for raw body | + +**Example — template-based email**: +```diff + Content(mail.Content{ +- View: "welcome.html", +- Text: "welcome.txt", ++ HtmlView: "welcome.html", ++ TextView: "welcome.txt", + With: map[string]any{ + "Name": "Goravel", + }, + }) +``` + +**Example — raw body email (no change)**: +```go +Content(mail.Content{Text: "Hello Goravel"}) +``` + +If a file uses `Html:` / `Text:` already (raw body pattern from v1.17+), no change is needed. + +### 4c. Verification + +```shell +# Ensure no remaining View: in mail Content structs +rg -n '\.View\s*:.*\.html' --type go + +go build ./... +``` + +## Step 5: Rename gRPC config key servers to clients + +### 5a. Detect affected files + +```shell +rg -n '"servers"' --type go -g '*grpc*' +rg -n 'grpc\.servers' --type go +``` + +### 5b. Apply the transformation + +In `config/grpc.go`, rename the config key: + +```diff + config.Add("grpc", map[string]any{ + "host": config.Env("GRPC_HOST", ""), +- "servers": map[string]any{ ++ "clients": map[string]any{ + "user": map[string]any{ + "host": config.Env("GRPC_USER_HOST", ""), + ... + }, + }, + }) +``` + +In application code, update any config path references: + +```diff +-facades.Config().Get("grpc.servers.user.host") ++facades.Config().Get("grpc.clients.user.host") +``` + +### 5c. Verification + +```shell +# Ensure no remaining "servers" key in grpc config +rg -n '"servers"' --type go -g '*grpc*' +rg -n 'grpc\.servers' --type go + +go build ./... +``` + +## Step 6: Final verification + +After completing all steps, run the full verification suite: + +```shell +go build ./... +go vet ./... +go test ./... 2>&1 || true +``` + +If tests fail, the most common causes are: +- Test middleware functions that were not migrated (step 2). +- Test validation calls using `map[string]string` (step 3). +- Test mail Content structs with old field names (step 4). +```` From a5934a143f2acf333298b4baa33b0409e41459fc Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 28 Jun 2026 13:21:00 +0530 Subject: [PATCH 23/35] docs: document automatic database instrumentation for telemetry --- en/digging-deeper/telemetry.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index 466ea5622..56a1697a2 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -544,6 +544,29 @@ func Boot() contractsfoundation.Application { Both handlers accept options such as `WithFilter`, `WithSpanAttributes`, and `WithMetricAttributes`, and can be toggled via the `grpc_server.enabled` and `grpc_client.enabled` configuration options. +### Database + +Queries executed through the [ORM](../orm/getting-started.md) (`facades.Orm()`) and the [Query Builder](../database/queries.md) (`facades.DB()`) are instrumented automatically, no setup required. Each query records a client span along with the `db.client.operation.duration` metric, and every connection pool reports `db.client.connection.count` and `db.client.connection.max`, so you can watch pool usage and saturation. + +You can disable it via the `telemetry.instrumentation.database.enabled` configuration option (the `OTEL_DATABASE_ENABLED` environment variable). + +For structured queries, the table comes from the builder, so the span is named after the operation and table and carries the `db.collection.name` attribute. Nothing extra is required: + +```go +facades.DB().Table("users").Where("id", 1).Get(&users) // span: SELECT users +``` + +Raw queries run through `Select` or `Statement` pass an opaque SQL string that the framework does not parse, so the span is named after the operation alone. Tag the context with the table to restore the full name and the `db.collection.name` attribute: + +```go +import ( + instrumentationdatabase "github.com/goravel/framework/telemetry/instrumentation/database" +) + +queryCtx := instrumentationdatabase.ContextWithTable(ctx.Context(), "users") +facades.DB().WithContext(queryCtx).Select(&users, "SELECT * FROM users WHERE id = ?", 1) // span: SELECT users +``` + ## Context Propagation The `propagators` configuration option defines how trace context crosses process boundaries. The default is the W3C `tracecontext` standard; `baggage`, `b3`, and `b3multi` (Zipkin) are also supported and can be combined as a comma-separated list. From 073b820b860ce0ac6b01d7351448c9a023c8aa4c Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 28 Jun 2026 13:29:51 +0530 Subject: [PATCH 24/35] docs: optimise database telemetry example to pass context directly --- en/digging-deeper/telemetry.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index 56a1697a2..923d97ee6 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -556,14 +556,14 @@ For structured queries, the table comes from the builder, so the span is named a facades.DB().Table("users").Where("id", 1).Get(&users) // span: SELECT users ``` -Raw queries run through `Select` or `Statement` pass an opaque SQL string that the framework does not parse, so the span is named after the operation alone. Tag the context with the table to restore the full name and the `db.collection.name` attribute: +Raw queries run through `Select` or `Statement` pass an opaque SQL string that the framework does not parse, so the span is named after the operation alone. Use `ContextWithTable` to tag the table onto the request context to restore the full name and the `db.collection.name` attribute: ```go import ( instrumentationdatabase "github.com/goravel/framework/telemetry/instrumentation/database" ) -queryCtx := instrumentationdatabase.ContextWithTable(ctx.Context(), "users") +queryCtx := instrumentationdatabase.ContextWithTable(ctx, "users") facades.DB().WithContext(queryCtx).Select(&users, "SELECT * FROM users WHERE id = ?", 1) // span: SELECT users ``` From 7181529d1473223ee1fb5a854478757032f38a40 Mon Sep 17 00:00:00 2001 From: Bowen Date: Mon, 29 Jun 2026 11:36:47 +0800 Subject: [PATCH 25/35] optimize --- en/upgrade/v1.18.md | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 1ebfe32e5..c00e1abf5 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -162,7 +162,7 @@ Any code that reads config values at paths like `grpc.servers.*` must be updated ### AI SDK -Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, generated media, and speech workflows through one facade. Install the facade with `./artisan package:install ai`, then install a provider package such as `goravel/openai`, `goravel/anthropic`, or `goravel/gemini`. +Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, generated media, and speech workflows through one facade. Install the facade with `go run .artisan package:install AI`, then install a provider package such as `goravel/openai`, `goravel/anthropic`, or `goravel/gemini`. The initial release includes: @@ -190,7 +190,7 @@ fmt.Println(response.Text()) ### Telemetry -Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with `./artisan package:install Telemetry`. +Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with `go run . artisan package:install Telemetry`. The initial release includes: @@ -275,17 +275,17 @@ evens.All() // []int{2, 4, 6} Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `file` maintenance driver is used by default, so the `down` command writes maintenance metadata to storage and the `up` command removes it. ```shell -./artisan down -./artisan up +go run . artisan down +go run . artisan up ``` The `down` command supports response customization with options such as `--reason`, `--status`, `--redirect`, `--render`, `--secret`, and `--with-secret`. ```shell -./artisan down --reason="Upgrading database" --status=503 -./artisan down --redirect=/maintenance -./artisan down --render=errors/503 -./artisan down --with-secret +go run . artisan down --reason="Upgrading database" --status=503 +go run . artisan down --redirect=/maintenance +go run . artisan down --render=errors/503 +go run . artisan down --with-secret ``` Maintenance settings are read from `app.maintenance` in `config/app.go`. New Route installations add this configuration automatically. Existing applications can add it manually: @@ -311,8 +311,8 @@ APP_MAINTENANCE_STORE=redis The `build` command now supports the `--generate` flag and `-g` alias. When enabled, Goravel runs `go generate ./...` before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior. ```shell -./artisan build --generate -./artisan build -g +go run . artisan build --generate +go run . artisan build -g ``` [View Document](../getting-started/compile.md#compile-command) @@ -610,26 +610,20 @@ rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null **Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`: ```shell -go get github.com/goravel/framework@latest +go get github.com/goravel/framework@master # For each facade found in detection, run the corresponding go get. -# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest -# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest -# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest -# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest -# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest -# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest -# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest +# If goravel/gin is in go.sum: go get github.com/goravel/gin@master +# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@master +# If goravel/redis is in go.sum: go get github.com/goravel/redis@master +# If goravel/s3 is in go.sum: go get github.com/goravel/s3@master +# If goravel/oss is in go.sum: go get github.com/goravel/oss@master +# If goravel/cos is in go.sum: go get github.com/goravel/cos@master +# If goravel/minio is in go.sum: go get github.com/goravel/minio@master go mod tidy ``` -**Verification**: - -```shell -go build ./... -``` - ## Step 2: Migrate HTTP middleware from func to interface `contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated. From 2465dbba17beacfc73d68126ab623159264eb043 Mon Sep 17 00:00:00 2001 From: Bowen Date: Mon, 29 Jun 2026 11:37:38 +0800 Subject: [PATCH 26/35] optimize --- en/upgrade/v1.18.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index c00e1abf5..7631539e5 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -610,16 +610,16 @@ rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null **Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`: ```shell -go get github.com/goravel/framework@master +go get github.com/goravel/framework@latest # For each facade found in detection, run the corresponding go get. -# If goravel/gin is in go.sum: go get github.com/goravel/gin@master -# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@master -# If goravel/redis is in go.sum: go get github.com/goravel/redis@master -# If goravel/s3 is in go.sum: go get github.com/goravel/s3@master -# If goravel/oss is in go.sum: go get github.com/goravel/oss@master -# If goravel/cos is in go.sum: go get github.com/goravel/cos@master -# If goravel/minio is in go.sum: go get github.com/goravel/minio@master +# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest +# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest +# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest +# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest +# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest +# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest +# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest go mod tidy ``` From c7491ce0b1f8cc06641f7665d9d249fc94e06cb4 Mon Sep 17 00:00:00 2001 From: Bowen Date: Mon, 29 Jun 2026 17:40:56 +0800 Subject: [PATCH 27/35] optimize --- en/upgrade/v1.18.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 7631539e5..e768e3ec9 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -33,7 +33,7 @@ As [Golang v1.24 is no longer maintained](https://endoflife.date/go), the Golang version of Goravel supports has been upgraded from 1.24 to 1.25. -goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: [goravel/example#93](https://github.com/goravel/example/pull/93). +goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: [goravel/example#133](https://github.com/goravel/example/pull/133). ### 1. Update dependencies From e757dd59d49dd63f5ad7810ba1bd5ade5b9ea244 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Mon, 29 Jun 2026 23:14:26 +0530 Subject: [PATCH 28/35] docs: pass context directly to telemetry instead of ctx.Context() --- en/digging-deeper/telemetry.md | 8 ++++---- en/upgrade/v1.18.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index 923d97ee6..af2aeb59d 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -256,12 +256,12 @@ func (r *OrderService) chargePayment(ctx context.Context, orderID string) error } ``` -When the service is called from a controller, pass `ctx.Context()`. If the [HTTP server middleware](#http-server) is registered, the spans are attached to the request's trace, so your backend shows the full picture: the HTTP request, the order processing, and the payment charge as one tree: +When the service is called from a controller, pass `ctx` directly (`http.Context` implements `context.Context`). If the [HTTP server middleware](#http-server) is registered, the spans are attached to the request's trace, so your backend shows the full picture: the HTTP request, the order processing, and the payment charge as one tree: ```go // app/http/controllers/order_controller.go func (r *OrderController) Store(ctx http.Context) http.Response { - if err := r.orders.Process(ctx.Context(), ctx.Request().Input("order_id")); err != nil { + if err := r.orders.Process(ctx, ctx.Request().Input("order_id")); err != nil { return ctx.Response().Status(http.StatusInternalServerError).Json(http.Json{ "error": "failed to process order", }) @@ -338,7 +338,7 @@ You don't always need to create a new span, often you just want to enrich the on import "go.opentelemetry.io/otel/trace" func (r *OrderController) Store(ctx http.Context) http.Response { - span := trace.SpanFromContext(ctx.Context()) + span := trace.SpanFromContext(ctx) span.SetAttributes(telemetry.String("order.id", "1234")) // ... @@ -457,7 +457,7 @@ To correlate logs with the active trace, write them using `WithContext`, the tra ```go func (r *OrderController) Store(ctx http.Context) http.Response { - facades.Log().WithContext(ctx.Context()). + facades.Log().WithContext(ctx). With(map[string]any{ "order_id": ctx.Request().Input("order_id"), }). diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index e768e3ec9..697d3f485 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -201,7 +201,7 @@ The initial release includes: A manual span looks like this: ```go -ctx, span := facades.Telemetry().Tracer("app").Start(ctx.Context(), "process-order") +ctx, span := facades.Telemetry().Tracer("app").Start(ctx, "process-order") defer span.End() span.SetAttributes(telemetry.String("order.id", "1234")) From 39507b29651ba5541d0d67086bb61c2875b436ff Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Tue, 30 Jun 2026 21:18:54 +0530 Subject: [PATCH 29/35] docs: drop redundant context.Context note in telemetry example --- en/digging-deeper/telemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md index af2aeb59d..7ccd88faa 100644 --- a/en/digging-deeper/telemetry.md +++ b/en/digging-deeper/telemetry.md @@ -256,7 +256,7 @@ func (r *OrderService) chargePayment(ctx context.Context, orderID string) error } ``` -When the service is called from a controller, pass `ctx` directly (`http.Context` implements `context.Context`). If the [HTTP server middleware](#http-server) is registered, the spans are attached to the request's trace, so your backend shows the full picture: the HTTP request, the order processing, and the payment charge as one tree: +When the service is called from a controller, pass `ctx`. If the [HTTP server middleware](#http-server) is registered, the spans are attached to the request's trace, so your backend shows the full picture: the HTTP request, the order processing, and the payment charge as one tree: ```go // app/http/controllers/order_controller.go From 24d21ffc9528b2ffd2404dd4f293cf7c809a9a33 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Tue, 30 Jun 2026 21:22:46 +0530 Subject: [PATCH 30/35] docs: include database in v1.18 built-in instrumentation list --- en/upgrade/v1.18.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 697d3f485..f925ede98 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -195,7 +195,7 @@ Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for co The initial release includes: - Traces, metrics, and logs configured from a single `config/telemetry.go` file, with OTLP, console, and custom exporters, sampling, and batching controls. -- Built-in instrumentation for the HTTP server, the HTTP client, gRPC, and the logger, with trace context propagation across services. +- Built-in instrumentation for the HTTP server, the HTTP client, gRPC, the database, and the logger, with trace context propagation across services. - Automatic flush and shutdown when the application stops. A manual span looks like this: From b9f156c56fbee5984ef19292ea2fab7229c01c2d Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 2 Jul 2026 16:34:03 +0800 Subject: [PATCH 31/35] optimize --- .vitepress/config/en.ts | 14 + en/ai/sdk.md | 17 ++ en/getting-started/packages.md | 3 + en/upgrade/v1.18.md | 506 +++++++++++++-------------------- 4 files changed, 233 insertions(+), 307 deletions(-) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index b00568696..d242a5b18 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -37,6 +37,11 @@ export const config = defineConfig({ base: '/digging-deeper/', items: sidebarAdvanced() }, + { + text: 'AI', + base: '/ai/', + items: sidebarAI() + }, { text: 'Security', base: '/security/', @@ -324,6 +329,15 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { ] } +function sidebarAI(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'AI SDK', + link: 'sdk' + } + ] +} + function sidebarSecurity(): DefaultTheme.SidebarItem[] { return [ { diff --git a/en/ai/sdk.md b/en/ai/sdk.md index b023114f3..e51a3de71 100644 --- a/en/ai/sdk.md +++ b/en/ai/sdk.md @@ -1008,3 +1008,20 @@ conversation, err := facades.AI().Agent( frameworkai.WithProvider("custom"), ) ``` + +## AI Agent Development Skill + +The `goravel-lite` scaffold ships a `.agents/skills/goravel-development/SKILL.md` skill file that teaches AI coding agents (Cursor, Copilot, Codex, etc.) how to work with Goravel projects. The skill covers project structure, facades, routing, ORM, testing conventions, and Artisan scaffolding commands. + +When an AI agent reads this skill, it already knows: + +- How to bootstrap a Goravel project and use `package:install` to add facades. +- The facade pattern, testing with `testify` suites, and mock helpers. +- Artisan `make:*` generators for controllers, middleware, models, migrations, and more. +- Where to find contract interfaces, reference implementations, and docs. + +### Customization + +You can extend the built-in skill with project-specific rules by creating `.agents/skills/goravel-development/CUSTOM.md`. AI agents will read and apply both files automatically. Add conventions unique to your project, such as preferred import aliases, domain-specific naming, or custom directory layouts. + +The skill is versioned alongside the `goravel-lite` scaffold and is updated when you upgrade the framework. After upgrading, review the skill file for new conventions and merge your `CUSTOM.md` rules as needed. diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md index 73c6e2d8d..ef4e9f347 100644 --- a/en/getting-started/packages.md +++ b/en/getting-started/packages.md @@ -6,6 +6,7 @@ You can find extended packages for Goravel here, and you can also create a PR fo | --------------------------------------------------------------------------------- | --------------------------------------------- | ------------- | | [goravel/gin](https://github.com/goravel/gin) | The Gin driver for `facades.Route()` | 83.1% | | [goravel/fiber](https://github.com/goravel/fiber) | The Fiber driver for `facades.Route()` | 81.0% | +| [goravel/openai](https://github.com/goravel/openai) | An OpenAI driver for `facades.AI()` | 80.4% | | [goravel/cos](https://github.com/goravel/cos) | A COS disk driver for `facades.Storage()` | 80.2% | | [goravel/minio](https://github.com/goravel/minio) | A Minio disk driver for `facades.Storage()` | 79.6% | | [goravel/redis](https://github.com/goravel/redis) | A Redis disk driver for `facades.Cache()` | 79.1% | @@ -13,9 +14,11 @@ You can find extended packages for Goravel here, and you can also create a PR fo | [goravel/oss](https://github.com/goravel/oss) | A OSS disk driver for `facades.Storage()` | 76.5% | | [goravel/installer](https://github.com/goravel/installer) | Goravel installer | 76.2% | | [goravel/postgres](https://github.com/goravel/postgres) | A Postgres database driver | 73.7% | +| [goravel/anthropic](https://github.com/goravel/anthropic) | An Anthropic driver for `facades.AI()` | 73.3% | | [goravel/mysql](https://github.com/goravel/mysql) | A MySQL database driver | 73.3% | | [goravel/sqlserver](https://github.com/goravel/sqlserver) | A Sqlserver database driver | 60.6% | | [goravel/sqlite](https://github.com/goravel/sqlite) | A Sqlite database driver | 45.2% | +| [goravel/gemini](https://github.com/goravel/gemini) | A Gemini driver for `facades.AI()` | 51.8% | | [portofolio-mager/goravel-mongodb](https://github.com/portofolio-mager/goravel-mongodb) | A MongoDB package | 16.9% | | [hulutech-web/goravel-kit-cli](https://github.com/hulutech-web/goravel-kit-cli) | A goravel scaffold commandline tool | 15.2% | | [hulutech-web/goravel-workflow](https://github.com/hulutech-web/goravel-workflow) | A workflow package | 4.4% | diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index f925ede98..132e75589 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -35,63 +35,129 @@ As [Golang v1.24 is no longer maintained](https://endoflife.date/go), the Golang goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: [goravel/example#133](https://github.com/goravel/example/pull/133). -### 1. Update dependencies +You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically. -```shell -go get github.com/goravel/framework@latest +````markdown +# Upgrade Guide for Goravel v1.18 -// If using gin -go get github.com/goravel/gin@latest +**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.25 is installed, Goravel version ≥ 1.17.0. -// If using fiber -go get github.com/goravel/fiber@latest +## Step 1: Update Go module dependencies -// If using redis -go get github.com/goravel/redis@latest +**Detection**: Check which facade packages are already installed: -// If using S3 -go get github.com/goravel/s3@latest +```shell +rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null +``` -// If using Oss -go get github.com/goravel/oss@latest +**Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`: -// If using Cos -go get github.com/goravel/cos@latest +```shell +go get github.com/goravel/framework@latest -// If using Minio -go get github.com/goravel/minio@latest +# For each facade found in detection, run the corresponding go get. +# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest +# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest +# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest +# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest +# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest +# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest +# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest go mod tidy +``` -// If using installer -goravel upgrade +## Step 2: Migrate HTTP middleware from func to interface + +`contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated. + +### 2a. Detect all custom middleware functions + +```shell +rg -n 'func\s+\w+\s*\(.*\)\s+http\.Middleware' --type go +rg -n 'func\s+\w+\s*\(.*\)\s+contractshttp\.Middleware' --type go +``` + +Also search for anonymous middleware (inline `return func(ctx http.Context)`): + +```shell +rg -n 'return\s+func\(ctx\s+http\.Context\)' --type go ``` -### 2. Migrate middleware from func to interface +### 2b. Convert each middleware -`contracts/http.Middleware` has changed from `func(Context)` to an interface with `Handle(Context)` and `Signature() string` methods. Update all custom middleware to implement the new interface: +For each detected middleware, keep the factory function signature and replace the closure body with a struct literal: ```diff --func Auth() http.Middleware { + func Auth() http.Middleware { - return func(ctx http.Context) { - ctx.Request().Next() - } ++ return &Auth{} + } + +type Auth struct{} + +func (a *Auth) Handle(ctx http.Context) { + ctx.Request().Next() - } ++} + +func (a *Auth) Signature() string { + return "auth" +} ``` -The `Signature()` method provides a stable identity for each middleware, enabling `WithoutMiddleware` to reliably exclude specific middleware by signature rather than reflection. +If the original factory accepted parameters, pass them to the struct literal: + +```diff + func Throttle(name string) http.Middleware { +- return func(ctx http.Context) { +- // throttle logic using name +- } ++ return &Throttle{Name: name} + } ++ ++type Throttle struct{ Name string } ++ ++func (t *Throttle) Handle(ctx http.Context) { /* throttle logic using t.Name */ } ++func (t *Throttle) Signature() string { return "throttle-" + t.Name } +``` + +Call sites like `middleware.Auth()` stay unchanged — the factory function still returns `http.Middleware`, but now returns a struct instead of a closure. -### 3. Update validation rule and filter maps +### 2c. Verification -Validation rules and filters now use `map[string]any` instead of `map[string]string`. Update request validation calls, manual validator calls, and form request `Rules` / `Filters` methods: +```shell +# Ensure no remaining func-returning middleware patterns +rg -n 'func.*http\.Middleware' --type go + +# Build must succeed +go build ./... +``` + +## Step 3: Update validation rule and filter maps + +Validation rules and filters now use `map[string]any` instead of `map[string]string`. + +### 3a. Detect affected files + +```shell +rg -n 'map\[string\]string.*Validate\(' --type go +rg -n 'map\[string\]string.*Make\(' --type go +rg -n 'Rules\(\).*map\[string\]string' --type go +rg -n 'Filters\(\).*map\[string\]string' --type go +``` + +Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations: + +```shell +rg -n '\.Validate\(.*map\[string\]' --type go +rg -n 'validation\.Make\(.*map\[string\]' --type go +``` + +### 3b. Apply the transformation + +Replace `map[string]string` with `map[string]any` in all validation contexts: ```diff -ctx.Request().Validate(map[string]string{ @@ -100,24 +166,48 @@ Validation rules and filters now use `map[string]any` instead of `map[string]str }) ``` -Rule and filter values may be either pipe-separated strings or `[]string`. Prefer `[]string` when a `regex` or `not_regex` pattern contains `|` and must be followed by more rules: +If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`: -```go -rules := map[string]any{ - "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, -} +```diff + rules := map[string]string{ +- "slug": "required|regex:^(news|docs)-[a-z]+$|string", ++ "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, + } ``` -### 4. Update mail template content fields +### 3c. Verification + +```shell +# Ensure no remaining map[string]string in validation contexts +rg -n 'Validate\(.*map\[string\]string' --type go +rg -n 'Make\(.*map\[string\]string' --type go + +go build ./... +``` -If your application uses mail templates, update every `mail.Content` value that points to a template file: +## Step 4: Update mail template content fields -- Replace `View` with `HtmlView` for HTML templates. -- Replace `Text` with `TextView` if the value is a plain-text template path. -- Keep `Text` only when the value is the raw plain-text email body. +Mail content fields are now explicit: `HtmlView` for HTML templates, `TextView` for plain-text templates, `Html`/`Text` for raw body content. -For template-based emails, migrate fields like this: +### 4a. Detect affected files +```shell +rg -n '\.View\s*:' --type go -g '!vendor/**' -g '!storage/**' +rg -n 'Content\(.*mail\.Content\{' --type go +rg -n '&mail\.Content\{' --type go +``` + +### 4b. Apply the transformation + +For each `mail.Content{}` literal: + +| Old field | New field | Condition | +|-----------|-----------|-----------| +| `View:` | `HtmlView:` | Always (was for HTML templates) | +| `Text:` (value ends in `.html`, `.txt`, `.tmpl`, or is a file path) | `TextView:` | When the value was a template path | +| `Text:` (value is a raw string, not a path) | `Text:` | Keep as-is for raw body | + +**Example — template-based email**: ```diff Content(mail.Content{ - View: "welcome.html", @@ -130,33 +220,96 @@ For template-based emails, migrate fields like this: }) ``` -For raw plain-text emails, use `Text` directly: - +**Example — raw body email (no change)**: ```go Content(mail.Content{Text: "Hello Goravel"}) ``` -### 5. Rename grpc.servers config key to grpc.clients +If a file uses `Html:` / `Text:` already (raw body pattern from v1.17+), no change is needed. + +### 4c. Verification + +```shell +# Ensure no remaining View: in mail Content structs +rg -n '\.View\s*:.*\.html' --type go -The gRPC config key `grpc.servers` has been renamed to `grpc.clients` to better reflect that these entries configure outbound client connections. Update `config/grpc.go`: +go build ./... +``` + +## Step 5: Rename gRPC config key servers to clients + +### 5a. Detect affected files + +```shell +rg -n '"servers"' --type go -g '*grpc*' +rg -n 'grpc\.servers' --type go +``` + +### 5b. Apply the transformation + +In `config/grpc.go`, rename the config key: ```diff config.Add("grpc", map[string]any{ "host": config.Env("GRPC_HOST", ""), - "port": config.Env("GRPC_PORT", ""), - "servers": map[string]any{ + "clients": map[string]any{ "user": map[string]any{ "host": config.Env("GRPC_USER_HOST", ""), - "port": config.Env("GRPC_USER_PORT", ""), - "interceptors": []string{}, - "stats_handlers": []string{}, + ... }, }, }) ``` -Any code that reads config values at paths like `grpc.servers.*` must be updated to `grpc.clients.*`. +In application code, update any config path references: + +```diff +-facades.Config().Get("grpc.servers.user.host") ++facades.Config().Get("grpc.clients.user.host") +``` + +### 5c. Verification + +```shell +# Ensure no remaining "servers" key in grpc config +rg -n '"servers"' --type go -g '*grpc*' +rg -n 'grpc\.servers' --type go + +go build ./... +``` + +## Step 6: Install the goravel-development skill + +Download the Goravel AI coding conventions skill from goravel-lite so you can reference project structure, facades, testing patterns, and Artisan commands during the upgrade: + +```shell +mkdir -p .agents/skills/goravel-development +curl -fsSL -o .agents/skills/goravel-development/SKILL.md \ + https://raw.githubusercontent.com/goravel/goravel-lite/master/.agents/skills/goravel-development/SKILL.md +``` + +### 6a. Verification + +```shell +ls -l .agents/skills/goravel-development/SKILL.md +``` + +## Step 7: Final verification + +After completing all steps, run the full verification suite: + +```shell +go build ./... +go vet ./... +go test ./... 2>&1 || true +``` + +If tests fail, the most common causes are: +- Test middleware functions that were not migrated (step 2). +- Test validation calls using `map[string]string` (step 3). +- Test mail Content structs with old field names (step 4). +```` ## Feature Introduction @@ -590,264 +743,3 @@ facades.Route().Middleware(middleware.Auth()). [View Document](../the-basics/routing.md#without-middleware) -## Upgrade Guide for AI Agents - -This chapter is designed for AI coding agents (e.g. Cursor, Copilot, Codex) to perform the v1.17→v1.18 upgrade deterministically. You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically. - -````markdown -# Upgrade Guide for Goravel v1.18 - -**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.25 is installed, Goravel version ≥ 1.17.0. - -## Step 1: Update Go module dependencies - -**Detection**: Check which facade packages are already installed: - -```shell -rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null -``` - -**Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`: - -```shell -go get github.com/goravel/framework@latest - -# For each facade found in detection, run the corresponding go get. -# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest -# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest -# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest -# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest -# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest -# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest -# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest - -go mod tidy -``` - -## Step 2: Migrate HTTP middleware from func to interface - -`contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated. - -### 2a. Detect all custom middleware functions - -```shell -rg -n 'func\s+\w+\s*\(.*\)\s+http\.Middleware' --type go -rg -n 'func\s+\w+\s*\(.*\)\s+contractshttp\.Middleware' --type go -``` - -Also search for anonymous middleware (inline `return func(ctx http.Context)`): - -```shell -rg -n 'return\s+func\(ctx\s+http\.Context\)' --type go -``` - -### 2b. Convert each middleware - -For each detected middleware, keep the factory function signature and replace the closure body with a struct literal: - -```diff - func Auth() http.Middleware { -- return func(ctx http.Context) { -- ctx.Request().Next() -- } -+ return &Auth{} - } - -+type Auth struct{} -+ -+func (a *Auth) Handle(ctx http.Context) { -+ ctx.Request().Next() -+} -+ -+func (a *Auth) Signature() string { -+ return "auth" -+} -``` - -If the original factory accepted parameters, pass them to the struct literal: - -```diff - func Throttle(name string) http.Middleware { -- return func(ctx http.Context) { -- // throttle logic using name -- } -+ return &Throttle{Name: name} - } -+ -+type Throttle struct{ Name string } -+ -+func (t *Throttle) Handle(ctx http.Context) { /* throttle logic using t.Name */ } -+func (t *Throttle) Signature() string { return "throttle-" + t.Name } -``` - -Call sites like `middleware.Auth()` stay unchanged — the factory function still returns `http.Middleware`, but now returns a struct instead of a closure. - -### 2c. Verification - -```shell -# Ensure no remaining func-returning middleware patterns -rg -n 'func.*http\.Middleware' --type go - -# Build must succeed -go build ./... -``` - -## Step 3: Update validation rule and filter maps - -Validation rules and filters now use `map[string]any` instead of `map[string]string`. - -### 3a. Detect affected files - -```shell -rg -n 'map\[string\]string.*Validate\(' --type go -rg -n 'map\[string\]string.*Make\(' --type go -rg -n 'Rules\(\).*map\[string\]string' --type go -rg -n 'Filters\(\).*map\[string\]string' --type go -``` - -Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations: - -```shell -rg -n '\.Validate\(.*map\[string\]' --type go -rg -n 'validation\.Make\(.*map\[string\]' --type go -``` - -### 3b. Apply the transformation - -Replace `map[string]string` with `map[string]any` in all validation contexts: - -```diff --ctx.Request().Validate(map[string]string{ -+ctx.Request().Validate(map[string]any{ - "title": "required|max:255", - }) -``` - -If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`: - -```diff - rules := map[string]string{ -- "slug": "required|regex:^(news|docs)-[a-z]+$|string", -+ "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"}, - } -``` - -### 3c. Verification - -```shell -# Ensure no remaining map[string]string in validation contexts -rg -n 'Validate\(.*map\[string\]string' --type go -rg -n 'Make\(.*map\[string\]string' --type go - -go build ./... -``` - -## Step 4: Update mail template content fields - -Mail content fields are now explicit: `HtmlView` for HTML templates, `TextView` for plain-text templates, `Html`/`Text` for raw body content. - -### 4a. Detect affected files - -```shell -rg -n '\.View\s*:' --type go -g '!vendor/**' -g '!storage/**' -rg -n 'Content\(.*mail\.Content\{' --type go -rg -n '&mail\.Content\{' --type go -``` - -### 4b. Apply the transformation - -For each `mail.Content{}` literal: - -| Old field | New field | Condition | -|-----------|-----------|-----------| -| `View:` | `HtmlView:` | Always (was for HTML templates) | -| `Text:` (value ends in `.html`, `.txt`, `.tmpl`, or is a file path) | `TextView:` | When the value was a template path | -| `Text:` (value is a raw string, not a path) | `Text:` | Keep as-is for raw body | - -**Example — template-based email**: -```diff - Content(mail.Content{ -- View: "welcome.html", -- Text: "welcome.txt", -+ HtmlView: "welcome.html", -+ TextView: "welcome.txt", - With: map[string]any{ - "Name": "Goravel", - }, - }) -``` - -**Example — raw body email (no change)**: -```go -Content(mail.Content{Text: "Hello Goravel"}) -``` - -If a file uses `Html:` / `Text:` already (raw body pattern from v1.17+), no change is needed. - -### 4c. Verification - -```shell -# Ensure no remaining View: in mail Content structs -rg -n '\.View\s*:.*\.html' --type go - -go build ./... -``` - -## Step 5: Rename gRPC config key servers to clients - -### 5a. Detect affected files - -```shell -rg -n '"servers"' --type go -g '*grpc*' -rg -n 'grpc\.servers' --type go -``` - -### 5b. Apply the transformation - -In `config/grpc.go`, rename the config key: - -```diff - config.Add("grpc", map[string]any{ - "host": config.Env("GRPC_HOST", ""), -- "servers": map[string]any{ -+ "clients": map[string]any{ - "user": map[string]any{ - "host": config.Env("GRPC_USER_HOST", ""), - ... - }, - }, - }) -``` - -In application code, update any config path references: - -```diff --facades.Config().Get("grpc.servers.user.host") -+facades.Config().Get("grpc.clients.user.host") -``` - -### 5c. Verification - -```shell -# Ensure no remaining "servers" key in grpc config -rg -n '"servers"' --type go -g '*grpc*' -rg -n 'grpc\.servers' --type go - -go build ./... -``` - -## Step 6: Final verification - -After completing all steps, run the full verification suite: - -```shell -go build ./... -go vet ./... -go test ./... 2>&1 || true -``` - -If tests fail, the most common causes are: -- Test middleware functions that were not migrated (step 2). -- Test validation calls using `map[string]string` (step 3). -- Test mail Content structs with old field names (step 4). -```` From e3f45f82f76d58fb8a2a0abfa1182233b0425e2c Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 2 Jul 2026 17:34:54 +0800 Subject: [PATCH 32/35] optimize --- en/upgrade/v1.18.md | 158 ++++++++++++++++++++++++++------------------ 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 132e75589..47599baf1 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -9,7 +9,7 @@ - [Laravel-style collections](#laravel-style-collections) - [Native validation engine and built-in rules](#native-validation-engine-and-built-in-rules) -- [Validation supports translation-based error messages and lang:publish command](#validation-supports-translation-based-error-messages-and-langpublish-command) +- [Validation supports translation-based error messages and lang:publish command](#validation-supports-translation-based-error-messages-and-lang-publish-command) - [Maintenance mode commands](#maintenance-mode-commands) - [Build command supports go generate](#build-command-supports-go-generate) - [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) @@ -367,6 +367,22 @@ The `Tracer` argument (`"app"`) is the instrumentation scope name that identifie [View Document](../digging-deeper/telemetry.md) +### Laravel-style collections + +Goravel now provides fluent eager and lazy collection utilities in the `support/collect` package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style `Where` comparisons for structs, aggregate values, and process large datasets with `LazyCollection`. + +```go +numbers := collect.New(1, 2, 3, 4, 5, 6) + +evens := numbers.Filter(func(n int, _ int) bool { + return n%2 == 0 +}) + +evens.All() // []int{2, 4, 6} +``` + +[View Document](../digging-deeper/collections.md) + ### Native validation engine and built-in rules Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set. @@ -407,22 +423,6 @@ After publishing, edit `lang/en/validation.json` to customize default error mess [View Document](../the-basics/validation.md#translation--localization) -### Laravel-style collections - -Goravel now provides fluent eager and lazy collection utilities in the `support/collect` package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style `Where` comparisons for structs, aggregate values, and process large datasets with `LazyCollection`. - -```go -numbers := collect.New(1, 2, 3, 4, 5, 6) - -evens := numbers.Filter(func(n int, _ int) bool { - return n%2 == 0 -}) - -evens.All() // []int{2, 4, 6} -``` - -[View Document](../digging-deeper/collections.md) - ### Maintenance mode commands Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `file` maintenance driver is used by default, so the `down` command writes maintenance metadata to storage and the `up` command removes it. @@ -617,37 +617,6 @@ Previously, `facades.Log().WithContext(ctx)` could write framework-internal cont [View Document](../the-basics/logging.md#inject-context) -### Mail content separates raw text from template views - -Goravel now treats `mail.Content.Text` as raw plain-text email body content. Template paths are explicit: use `HtmlView` for HTML templates and `TextView` for plain-text templates. - -Use `Html` and `Text` when the message body is already available: - -```go -func (m *WelcomeMail) Content() *mail.Content { - return &mail.Content{ - Html: "

Hello Goravel

", - Text: "Hello Goravel", - } -} -``` - -Use `HtmlView` and `TextView` when the message body should be rendered from templates: - -```go -func (m *WelcomeMail) Content() *mail.Content { - return &mail.Content{ - HtmlView: "welcome.html", - TextView: "welcome.txt", - With: map[string]any{ - "Name": "Goravel", - }, - } -} -``` - -[View Document](../digging-deeper/mail.md#using-template) - ### Package views via LoadViewsFrom Extension packages can now register their own view directories with `facades.View().LoadViewsFrom(path)`. The application's `resources/views` directory takes priority, and registered package views serve as defaults. The `Exist` method also checks registered package view paths. @@ -696,23 +665,6 @@ Invalid patterns are logged as warnings and the runner is left running, so a typ [View Document](../getting-started/configuration.md#disabled-runners) -### HTTP Middleware changed from func to interface - -`contracts/http.Middleware` has changed from a function type `func(Context)` to an interface: - -```go -type Middleware interface { - Handle(Context) - Signature() string -} -``` - -This change provides each middleware with a stable identity via the `Signature()` method, enabling `WithoutMiddleware` to reliably exclude specific middleware by signature comparison. All built-in framework middlewares (`Cors`, `Timeout`, `Throttle`, `VerifyCsrfToken`, etc.) have been updated to return structs that implement this interface. - -Custom middleware must be updated to implement the new interface. See the [upgrade guide](#_2-migrate-middleware-from-func-to-interface) for migration steps. - -[View Document](../the-basics/middleware.md) - ### WithoutMiddleware for routes Goravel v1.18 introduces the `WithoutMiddleware` method for both routes and route groups, similar to Laravel's `withoutMiddleware()`. This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting. @@ -743,3 +695,79 @@ facades.Route().Middleware(middleware.Auth()). [View Document](../the-basics/routing.md#without-middleware) +### HTTP Middleware changed from func to interface + +`contracts/http.Middleware` has changed from a function type `func(Context)` to an interface: + +```go +type Middleware interface { + Handle(Context) + Signature() string +} +``` + +This change provides each middleware with a stable identity via the `Signature()` method, enabling `WithoutMiddleware` to reliably exclude specific middleware by signature comparison. All built-in framework middlewares (`Cors`, `Timeout`, `Throttle`, `VerifyCsrfToken`, etc.) have been updated to return structs that implement this interface. + +Custom middleware must be updated to implement the new interface. See the [upgrade guide](#_2-migrate-middleware-from-func-to-interface) for migration steps. + +[View Document](../the-basics/middleware.md) + +### Mail content separates raw text from template views + +Goravel now treats `mail.Content.Text` as raw plain-text email body content. Template paths are explicit: use `HtmlView` for HTML templates and `TextView` for plain-text templates. + +Use `Html` and `Text` when the message body is already available: + +```go +func (m *WelcomeMail) Content() *mail.Content { + return &mail.Content{ + Html: "

Hello Goravel

", + Text: "Hello Goravel", + } +} +``` + +Use `HtmlView` and `TextView` when the message body should be rendered from templates: + +```go +func (m *WelcomeMail) Content() *mail.Content { + return &mail.Content{ + HtmlView: "welcome.html", + TextView: "welcome.txt", + With: map[string]any{ + "Name": "Goravel", + }, + } +} +``` + +[View Document](../digging-deeper/mail.md#using-template) + +### gRPC config key renamed from servers to clients + +The gRPC configuration key `servers` has been renamed to `clients` in `config/grpc.go` to accurately reflect that these entries register gRPC clients (consumers of remote services) rather than servers. + +In `config/grpc.go`, rename the top-level key: + +```diff + config.Add("grpc", map[string]any{ + "host": config.Env("GRPC_HOST", ""), +- "servers": map[string]any{ ++ "clients": map[string]any{ + "user": map[string]any{ + "host": config.Env("GRPC_USER_HOST", ""), + ... + }, + }, + }) +``` + +In application code, update any config path references from `grpc.servers` to `grpc.clients`: + +```diff +-facades.Config().Get("grpc.servers.user.host") ++facades.Config().Get("grpc.clients.user.host") +``` + +[View Document](../the-basics/grpc.md) + From ea9497785f203e5b831695269692ee0a17aefa58 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 3 Jul 2026 16:04:53 +0800 Subject: [PATCH 33/35] optimize --- en/upgrade/v1.18.md | 108 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 47599baf1..9b5d593c2 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -25,6 +25,7 @@ ## Breaking Changes 🛠 +- [Validation engine rewritten natively](#validation-engine-rewritten-natively) - [HTTP Middleware changed from func to interface](#http-middleware-changed-from-func-to-interface) - [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) - [gRPC config key renamed from servers to clients](#grpc-config-key-renamed-from-servers-to-clients) @@ -146,6 +147,8 @@ rg -n 'map\[string\]string.*Validate\(' --type go rg -n 'map\[string\]string.*Make\(' --type go rg -n 'Rules\(\).*map\[string\]string' --type go rg -n 'Filters\(\).*map\[string\]string' --type go +rg -n 'Messages\(\).*map\[string\]string' --type go +rg -n 'Attributes\(\).*map\[string\]string' --type go ``` Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations: @@ -157,7 +160,7 @@ rg -n 'validation\.Make\(.*map\[string\]' --type go ### 3b. Apply the transformation -Replace `map[string]string` with `map[string]any` in all validation contexts: +Replace `map[string]string` with `map[string]any` in `Validate` and `Make` calls: ```diff -ctx.Request().Validate(map[string]string{ @@ -166,6 +169,32 @@ Replace `map[string]string` with `map[string]any` in all validation contexts: }) ``` +Also update form request method signatures — `Rules()`, `Messages()`, `Attributes()`, and `Filters()`: + +```diff +-func (r *UserCreate) Rules(ctx http.Context) map[string]string { +- return map[string]string{ ++func (r *UserCreate) Rules(ctx http.Context) map[string]any { ++ return map[string]any{ + "name": "required", + } + } + +-func (r *UserCreate) Messages(ctx http.Context) map[string]string { +- return map[string]string{} ++func (r *UserCreate) Messages(ctx http.Context) map[string]any { ++ return map[string]any{} + } + +-func (r *UserCreate) Filters(ctx http.Context) map[string]string { +- return map[string]string{ ++func (r *UserCreate) Filters(ctx http.Context) map[string]any { ++ return map[string]any{ + "name": "trim", + } + } +``` + If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`: ```diff @@ -181,6 +210,9 @@ If a `regex` or `not_regex` rule value contains `|` and is followed by more rule # Ensure no remaining map[string]string in validation contexts rg -n 'Validate\(.*map\[string\]string' --type go rg -n 'Make\(.*map\[string\]string' --type go +rg -n 'Rules\(.*map\[string\]string' --type go +rg -n 'Messages\(.*map\[string\]string' --type go +rg -n 'Filters\(.*map\[string\]string' --type go go build ./... ``` @@ -279,7 +311,57 @@ rg -n 'grpc\.servers' --type go go build ./... ``` -## Step 6: Install the goravel-development skill +## Step 6: Update validation error messages and custom rule names + +The new native validation engine ([goravel/framework#1397](https://github.com/goravel/framework/pull/1397)) produces Laravel-style error messages and evaluates all rules (including wildcards) differently. Any tests that assert on specific validation error messages or JSON error responses must be updated. + +### 6a. Detect affected tests + +```shell +rg -n 'Equal.*message.*required' --type go +rg -n 'Equal.*message.*regex' --type go +rg -n 'Equal.*message.*date' --type go +rg -n 'Required.*is required to not be empty' --type go +``` + +### 6b. Update error message expectations + +Replace old-style error message assertions with the new Laravel-style messages: + +| Old message | New message | +|-------------|-------------| +| `"name is required to not be empty"` | `"The name field is required."` | +| `"code value does not pass the regex check"` | `"The code field format is invalid."` | +| `"date value should be a date string"` | `"The date field must be a valid date."` | + +Example test diff: + +```diff +-s.Equal("{\"message\":{\"name\":{\"required\":\"name is required to not be empty\"}}}", content) ++s.Equal("{\"message\":{\"name\":{\"required\":\"The name field is required.\"}}}", content) +``` + +> **Note**: Wildcard rules (e.g., `scores.*`, `tags.*`) now consistently appear in error output even when no input is provided. Add assertions for these if your tests check the full error response body. + +### 6c. Rename custom rules that conflict with built-in rules + +The new engine includes built-in rules (`required`, `email`, `exists`, `unique`, etc.) that may conflict with custom rule names. Rename any custom rule whose `Signature()` matches a built-in rule name: + +```diff + func (receiver *Exists) Signature() string { +- return "exists" ++ return "exists_custom" + } +``` + +### 6d. Verification + +```shell +go build ./... +go test ./... 2>&1 || true +``` + +## Step 7: Install the goravel-development skill Download the Goravel AI coding conventions skill from goravel-lite so you can reference project structure, facades, testing patterns, and Artisan commands during the upgrade: @@ -289,13 +371,13 @@ curl -fsSL -o .agents/skills/goravel-development/SKILL.md \ https://raw.githubusercontent.com/goravel/goravel-lite/master/.agents/skills/goravel-development/SKILL.md ``` -### 6a. Verification +### 7a. Verification ```shell ls -l .agents/skills/goravel-development/SKILL.md ``` -## Step 7: Final verification +## Step 8: Final verification After completing all steps, run the full verification suite: @@ -309,6 +391,8 @@ If tests fail, the most common causes are: - Test middleware functions that were not migrated (step 2). - Test validation calls using `map[string]string` (step 3). - Test mail Content structs with old field names (step 4). +- Test validation error message assertions with old format (step 6). +- Custom rule signatures conflicting with built-in rule names (step 6). ```` ## Feature Introduction @@ -695,6 +779,22 @@ facades.Route().Middleware(middleware.Auth()). [View Document](../the-basics/routing.md#without-middleware) +### Validation engine rewritten natively + +The validation engine was rewritten in [goravel/framework#1397](https://github.com/goravel/framework/pull/1397) — replacing the external `gookit/validate` dependency with a native implementation that is fully compatible with Laravel's validation API. This change introduces several downstream effects: + +1. **Error message format changed** from old-style messages (e.g., `"name is required to not be empty"`) to Laravel-style messages (e.g., `"The name field is required."`). Tests that assert on specific validation error messages must be updated. + +2. **Wildcard rules (`*.`) now consistently report errors.** The old engine might have skipped wildcard-field errors depending on input; the new engine always evaluates and reports them. + +3. **Form request method signatures changed** — `Rules()`, `Messages()`, `Attributes()`, and `Filters()` now return `map[string]any` instead of `map[string]string`. + +4. **Custom validation rule names may conflict** with new built-in rules. Rename custom rules that share names with built-in rules (e.g., `exists` → `exists_custom`). + +Error messages are now powered by the [localization](../digging-deeper/localization.md) system. Default messages are embedded as `lang/en/validation.json` and use `validation.*` translation keys. Use `go run . artisan lang:publish` to publish and customize them. + +[View Document](../the-basics/validation.md) + ### HTTP Middleware changed from func to interface `contracts/http.Middleware` has changed from a function type `func(Context)` to an interface: From 8716635ac48ab3c60338f4dcd3ed0463f4f81ab3 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 4 Jul 2026 10:19:49 +0800 Subject: [PATCH 34/35] optimize --- en/digging-deeper/queues.md | 33 +++++++++++ en/prologue/contributions.md | 49 ++++++++++++++++ en/upgrade/v1.18.md | 105 +++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/en/digging-deeper/queues.md b/en/digging-deeper/queues.md index 791d13655..44ca4b28b 100644 --- a/en/digging-deeper/queues.md +++ b/en/digging-deeper/queues.md @@ -58,6 +58,39 @@ After implementing the custom driver, you can add the configuration to `config/q }, ``` +### Batch-Receive Drivers + +For high-throughput scenarios (e.g., Kafka, RabbitMQ with consumer channels), you can optionally implement the `DriverWithReceive` interface to enable batch message consumption with blocking semantics. When a driver implements this interface, the queue worker automatically uses `Receive` instead of `Pop`, reducing polling overhead and improving throughput: + +```go +import ( + "context" + "github.com/goravel/framework/contracts/queue" +) + +type KafkaDriver struct{} + +// Implement the base Driver interface +func (d *KafkaDriver) Driver() string { + return "kafka" +} + +func (d *KafkaDriver) Push(task queue.Task, queue string) error { + // push job to Kafka +} + +func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { + // standard single-job pop +} + +// Implement the optional DriverWithReceive for batch consumption +func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) { + // batch receive up to `count` jobs, blocking until at least one is available or ctx expires +} +``` + +When `Receive` is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The `context.Context` is canceled on worker shutdown, ensuring clean termination. + ## Creating Jobs ### Generating Job Classes diff --git a/en/prologue/contributions.md b/en/prologue/contributions.md index 86cb76f9f..19b783271 100644 --- a/en/prologue/contributions.md +++ b/en/prologue/contributions.md @@ -137,6 +137,55 @@ You can find or create an issue in [Issue List](https://github.com/goravel/gorav | [goravel/sqlite](https://github.com/goravel/sqlite) | The SQLite driver of Database module | | [goravel/.github](https://github.com/goravel/.github) | [Community health file](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file) | +## Agent Skills + +Goravel provides agent skills for AI coding assistants (Claude Code, OpenCode, etc.) to help contributors follow Goravel development conventions such as testing, naming, and pull request workflows. Skills are sourced from the [goravel/agents](https://github.com/goravel/agents) repository. + +First, install the `goravel` CLI if you haven't already: + +```shell +go install github.com/goravel/goravel@latest +``` + +### List Available Skills + +View all available Goravel agent skills: + +```shell +goravel skill:list + +// Show skills with descriptions +goravel skill:list --detail +``` + +### Install Skills + +Install all Goravel agent skills to `~/.agents/skills` by default: + +```shell +goravel skill:install +``` + +Install specific skills by name: + +```shell +goravel skill:install goravel-testing goravel-planning +``` + +Install to a custom directory with `--path`: + +```shell +goravel skill:install --path ~/goravel-skills +``` + +Overwrite existing skills with `--force`: + +```shell +goravel skill:install --force goravel-testing +``` + +> **Note**: Existing skills are preserved unless `--force` is used. + ## Code of Conduct The Goravel code of conduct is derived from the Laravel code of conduct. Any violations of the code of conduct may be reported to Bowen. diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 9b5d593c2..729e7ad63 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -13,6 +13,7 @@ - [Maintenance mode commands](#maintenance-mode-commands) - [Build command supports go generate](#build-command-supports-go-generate) - [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) +- [Fiber driver context decoupled from Fiber instance lifecycle](#fiber-driver-context-decoupled-from-fiber-instance-lifecycle) - [Orm query exposes context](#orm-query-exposes-context) - [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Artisan console supports table output](#artisan-console-supports-table-output) @@ -22,6 +23,10 @@ - [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) - [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) - [WithoutMiddleware for routes](#withoutmiddleware-for-routes) +- [Fiber driver template configuration replaced with DefaultTemplate](#fiber-driver-template-configuration-replaced-with-defaulttemplate) +- [Installer supports agent skill management commands](#installer-supports-agent-skill-management-commands) +- [Queue drivers support batch-consuming via DriverWithReceive](#queue-drivers-support-batch-consuming-via-driverwithreceive) +- [Cache prefix supports empty string](#cache-prefix-supports-empty-string) ## Breaking Changes 🛠 @@ -29,6 +34,7 @@ - [HTTP Middleware changed from func to interface](#http-middleware-changed-from-func-to-interface) - [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) - [gRPC config key renamed from servers to clients](#grpc-config-key-renamed-from-servers-to-clients) +- [HTTP driver timeout middleware rewritten for concurrent safety](#http-driver-timeout-middleware-rewritten-for-concurrent-safety) ## Upgrade Guide @@ -395,6 +401,14 @@ If tests fail, the most common causes are: - Custom rule signatures conflicting with built-in rule names (step 6). ```` +### HTTP driver timeout middleware rewritten for concurrent safety + +The timeout middleware in both the Gin driver ([goravel/gin#222](https://github.com/goravel/gin/pull/222)) and the Fiber driver ([goravel/fiber#263](https://github.com/goravel/fiber/pull/263)) has been rewritten to fix a concurrency bug ([goravel#966](https://github.com/goravel/goravel/issues/966)). The previous implementation ran the handler chain in a background goroutine and could return a timeout response while the handler kept running on request-scoped state that was already eligible for reuse, causing cross-request response contamination under high concurrency. + +Both drivers now delegate to their framework's native timeout middleware (`gin-contrib/timeout` for Gin, Fiber's built-in timeout for Fiber), which properly abandons timed-out request contexts instead of recycling them. The `Timeout(...)` middleware signature is unchanged and `ctx.Done()` still reflects request deadlines — no application code changes are required. + +[View Document](../the-basics/routing.md) + ## Feature Introduction ### AI SDK @@ -407,6 +421,8 @@ The initial release includes: - Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs. - Image and audio generation with `Store` / `StoreAs` helpers, plus transcription with language and diarization options. +The `goravel-lite` scaffold also ships an *AI Agent Development Skill* which teaches AI coding agents (Cursor, Copilot, Codex, etc.) how to work with Goravel projects. The skill covers project structure, facades, routing, ORM, testing conventions, and Artisan scaffolding commands. When an AI agent reads this skill, it already knows how to bootstrap a Goravel project, use `package:install` to add facades, work with the facade pattern, write tests with `testify` suites and mock helpers, and use Artisan `make:*` generators. You can extend the skill with project-specific rules by creating `.agents/skills/goravel-development/CUSTOM.md`. The skill is versioned alongside `goravel-lite` and is updated when you upgrade the framework. + A minimal prompt looks like this: ```go @@ -714,6 +730,8 @@ func (r *ServiceProvider) Boot(app foundation.Application) { [View Document](../the-basics/views.md#registering-views-from-packages) +> **Note**: If you want to use this feature in the fiber driver, please check [Fiber driver template configuration replaced with DefaultTemplate](#fiber-driver-template-configuration-replaced-with-defaulttemplate). + ### Runners can be disabled via config option Goravel v1.18 replaces the internal `app.auto_run` flag with a public, glob-aware `app.disabled_runners` config option. It lets you selectively skip auto-run [runners](../architecture-concepts/service-providers.md#runners) in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping `goravel:http`, `goravel:queue`, and `goravel:grpc` runners. @@ -871,3 +889,90 @@ In application code, update any config path references from `grpc.servers` to `g [View Document](../the-basics/grpc.md) +### Fiber driver context decoupled from Fiber instance lifecycle + +When a request timed out with the Fiber driver, the `context.Context` interface methods (`Done()`, `Deadline()`, `Err()`) delegated to the underlying `fiber.Ctx`, which became invalid after the request was recycled by fasthttp. This caused nil-pointer panics when background goroutines (e.g. `database/sql`) still held a reference to the request context ([goravel/goravel#951](https://github.com/goravel/goravel/issues/951)). + +The fix ([goravel/fiber#253](https://github.com/goravel/fiber/pull/253)) stores the user context and values as struct fields on the goravel `Context` struct instead of delegating to `fiber.Ctx`, so timeout signals propagate correctly and background goroutines can safely access the context after the request returns. + +No application code changes are required. + +### Fiber driver template configuration replaced with DefaultTemplate + +To support package views via `LoadViewsFrom`, the Fiber driver ([goravel/fiber#273](https://github.com/goravel/fiber/pull/273)) has replaced its dependency on `gofiber/template/html/v3` with a built-in `DefaultTemplate()` that loads views from `resources/views` and any registered package views automatically. If your `config/http.go` has a custom `template` configuration using `html.New(path.Resource("views"), ".tmpl")` and you want to use the [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) feature, update it to use `goravelfiber.DefaultTemplate()` instead: + +```diff + // config/http.go — fiber driver + +import goravelfiber "github.com/goravel/fiber" + + "template": func() (fiber.Views, error) { +- return html.New(path.Resource("views"), ".tmpl"), nil ++ return goravelfiber.DefaultTemplate(), nil + }, +``` + +For custom delimiters or FuncMap, use `goravelfiber.NewTemplate(goravelfiber.RenderOptions{...})`: + +```go +import goravelfiber "github.com/goravel/fiber" + +"template": func() (fiber.Views, error) { + return goravelfiber.NewTemplate(goravelfiber.RenderOptions{ + Delims: &goravelfiber.Delims{Left: "{[", Right: "]}"}, + }) +}, +``` + +If no `template` configuration is defined, `DefaultTemplate()` is used automatically — no code change is required. + +### Installer supports agent skill management commands + +The `goravel` CLI now supports `goravel skill:list` and `goravel skill:install` for discovering and installing Goravel agent skills. These commands are designed for contributors who want to install AI coding assistant skills to help follow Goravel development conventions such as testing, naming, and pull request workflows. + +Skills are sourced from the [goravel/agents](https://github.com/goravel/agents) repository and installed to `~/.agents/skills` by default. You can filter by name, choose a custom destination with `--path`, and overwrite existing skills with `--force`. + +[View Document](../prologue/contributions.md#agent-skills) + +### Queue drivers support batch-consuming via DriverWithReceive + +Custom queue drivers can now optionally implement the `DriverWithReceive` interface ([goravel/framework#1435](https://github.com/goravel/framework/pull/1435)) to support blocking batch message consumption. When a driver implements this interface, the queue worker automatically uses `Receive` instead of `Pop`, reducing polling overhead for high-throughput drivers like Kafka or RabbitMQ. + +```go +import ( + "context" + "github.com/goravel/framework/contracts/queue" +) + +type KafkaDriver struct{} + +// Base Driver interface methods (required) +func (d *KafkaDriver) Driver() string { return "kafka" } +func (d *KafkaDriver) Push(task queue.Task, queue string) error { return nil } +func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { return nil, nil } + +// Optional DriverWithReceive for batch consumption +func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) { + // blocking batch receive up to `count` jobs, returns when ctx expires +} +``` + +When `Receive` is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The `context.Context` is canceled on worker shutdown, ensuring clean termination. No application code changes are required for existing drivers. + +[View Document](../digging-deeper/queues.md#batch-receive-drivers) + +### Cache prefix supports empty string + +The Redis cache driver now respects an empty `prefix` configuration. Previously, even with an empty prefix, a colon (`:`) separator was always prepended to every key, making keys inconsistent when accessing Redis directly. Now, if the `prefix` option in `config/cache.go` is set to an empty string, no prefix or separator is applied: + +```go +// config/cache.go +"prefix": "", +``` + +With this configuration, a cache key like `users` is stored as `users` in Redis, rather than `:users`. + +No application code changes are required for existing projects with a non-empty prefix. + +[View Document](../digging-deeper/cache.md#cache-key-prefix) + From 2f58f2c1816f1b7000bacadf7e6cb0d7855e8ef7 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 4 Jul 2026 18:33:08 +0800 Subject: [PATCH 35/35] optimize --- en/upgrade/v1.18.md | 408 +++++++++++++++++++++++--------------------- 1 file changed, 217 insertions(+), 191 deletions(-) diff --git a/en/upgrade/v1.18.md b/en/upgrade/v1.18.md index 729e7ad63..46239520b 100644 --- a/en/upgrade/v1.18.md +++ b/en/upgrade/v1.18.md @@ -11,31 +11,33 @@ - [Native validation engine and built-in rules](#native-validation-engine-and-built-in-rules) - [Validation supports translation-based error messages and lang:publish command](#validation-supports-translation-based-error-messages-and-lang-publish-command) - [Maintenance mode commands](#maintenance-mode-commands) +- [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) +- [WithoutMiddleware for routes](#withoutmiddleware-for-routes) - [Build command supports go generate](#build-command-supports-go-generate) -- [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) -- [Fiber driver context decoupled from Fiber instance lifecycle](#fiber-driver-context-decoupled-from-fiber-instance-lifecycle) -- [Orm query exposes context](#orm-query-exposes-context) -- [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Artisan console supports table output](#artisan-console-supports-table-output) - [Artisan console supports graceful shutdown via Shutdownable](#artisan-console-supports-graceful-shutdown-via-shutdownable) - [Artisan console supports commands filtering](#artisan-console-supports-commands-filtering) -- [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) +- [gRPC supports TLS and mTLS credentials](#grpc-supports-tls-and-mtls-credentials) +- [Orm query exposes context](#orm-query-exposes-context) - [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) -- [Runners can be disabled via config option](#runners-can-be-disabled-via-config-option) -- [WithoutMiddleware for routes](#withoutmiddleware-for-routes) -- [Fiber driver template configuration replaced with DefaultTemplate](#fiber-driver-template-configuration-replaced-with-defaulttemplate) -- [Installer supports agent skill management commands](#installer-supports-agent-skill-management-commands) +- [Package setup supports import aliases](#package-setup-supports-import-aliases) - [Queue drivers support batch-consuming via DriverWithReceive](#queue-drivers-support-batch-consuming-via-driverwithreceive) +- [Log WithContext filters framework context keys out](#log-withcontext-filters-framework-context-keys-out) +- [Installer supports agent skill management commands](#installer-supports-agent-skill-management-commands) +- [Fiber driver context decoupled from Fiber instance lifecycle](#fiber-driver-context-decoupled-from-fiber-instance-lifecycle) +- [Fiber driver template configuration replaced with DefaultTemplate](#fiber-driver-template-configuration-replaced-with-defaulttemplate) - [Cache prefix supports empty string](#cache-prefix-supports-empty-string) + ## Breaking Changes 🛠 -- [Validation engine rewritten natively](#validation-engine-rewritten-natively) - [HTTP Middleware changed from func to interface](#http-middleware-changed-from-func-to-interface) +- [Validation engine rewritten natively](#validation-engine-rewritten-natively) - [Mail content separates raw text from template views](#mail-content-separates-raw-text-from-template-views) - [gRPC config key renamed from servers to clients](#grpc-config-key-renamed-from-servers-to-clients) - [HTTP driver timeout middleware rewritten for concurrent safety](#http-driver-timeout-middleware-rewritten-for-concurrent-safety) + ## Upgrade Guide As [Golang v1.24 is no longer maintained](https://endoflife.date/go), the Golang version of Goravel supports has been upgraded from 1.24 to 1.25. @@ -400,15 +402,6 @@ If tests fail, the most common causes are: - Test validation error message assertions with old format (step 6). - Custom rule signatures conflicting with built-in rule names (step 6). ```` - -### HTTP driver timeout middleware rewritten for concurrent safety - -The timeout middleware in both the Gin driver ([goravel/gin#222](https://github.com/goravel/gin/pull/222)) and the Fiber driver ([goravel/fiber#263](https://github.com/goravel/fiber/pull/263)) has been rewritten to fix a concurrency bug ([goravel#966](https://github.com/goravel/goravel/issues/966)). The previous implementation ran the handler chain in a background goroutine and could return a timeout response while the handler kept running on request-scoped state that was already eligible for reuse, causing cross-request response contamination under high concurrency. - -Both drivers now delegate to their framework's native timeout middleware (`gin-contrib/timeout` for Gin, Fiber's built-in timeout for Fiber), which properly abandons timed-out request contexts instead of recycling them. The `Timeout(...)` middleware signature is unchanged and `ctx.Done()` still reflects request deadlines — no application code changes are required. - -[View Document](../the-basics/routing.md) - ## Feature Introduction ### AI SDK @@ -441,6 +434,7 @@ fmt.Println(response.Text()) [View Document](../ai/sdk.md) + ### Telemetry Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with `go run . artisan package:install Telemetry`. @@ -467,6 +461,7 @@ The `Tracer` argument (`"app"`) is the instrumentation scope name that identifie [View Document](../digging-deeper/telemetry.md) + ### Laravel-style collections Goravel now provides fluent eager and lazy collection utilities in the `support/collect` package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style `Where` comparisons for structs, aggregate values, and process large datasets with `LazyCollection`. @@ -483,6 +478,7 @@ evens.All() // []int{2, 4, 6} [View Document](../digging-deeper/collections.md) + ### Native validation engine and built-in rules Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set. @@ -508,6 +504,7 @@ scores := validated["scores"].([]int) [View Document](../the-basics/validation.md) + ### Validation supports translation-based error messages and lang:publish command Validation error messages are now powered by the framework's [localization](../digging-deeper/localization.md) system. Default messages are embedded as `lang/en/validation.json` and use `validation.*` translation keys (e.g., `validation.required`, `validation.min.string`). @@ -523,6 +520,7 @@ After publishing, edit `lang/en/validation.json` to customize default error mess [View Document](../the-basics/validation.md#translation--localization) + ### Maintenance mode commands Goravel now provides the `artisan down` and `artisan up` commands for application maintenance mode. The `file` maintenance driver is used by default, so the `down` command writes maintenance metadata to storage and the `up` command removes it. @@ -559,68 +557,85 @@ APP_MAINTENANCE_STORE=redis [View Document](../getting-started/configuration.md#maintenance-mode) -### Build command supports go generate -The `build` command now supports the `--generate` flag and `-g` alias. When enabled, Goravel runs `go generate ./...` before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior. +### Runners can be disabled via config option -```shell -go run . artisan build --generate -go run . artisan build -g -``` +Goravel v1.18 replaces the internal `app.auto_run` flag with a public, glob-aware `app.disabled_runners` config option. It lets you selectively skip auto-run [runners](../architecture-concepts/service-providers.md#runners) in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping `goravel:http`, `goravel:queue`, and `goravel:grpc` runners. -[View Document](../getting-started/compile.md#compile-command) +The value is a slice of glob patterns matched against each runner's signature with Go's [`path.Match`](https://pkg.go.dev/path#Match): -### gRPC supports TLS and mTLS credentials +```go +// config/app.go +"app": map[string]any{ + "env": "production", + "debug": false, + "disabled_runners": []string{"goravel:schedule"}, +}, +``` -Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use `WithGrpcServerCredentials` to enable TLS or mTLS for the server, and `WithGrpcClientCredentials` to register client credential groups that can be referenced by `grpc.clients..credentials`. +The framework runner signatures are `goravel:http`, `goravel:grpc`, `goravel:queue`, `goravel:schedule`, and `goravel:telemetry`. Patterns are evaluated in order and the first match wins, so you can mix exact and wildcard matches: ```go -return foundation.Setup(). - WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials { - userCreds, err := credentials.NewClientTLSFromFile("storage/certs/ca.crt", "user.example.com") - if err != nil { - panic(err) - } +// Web container — only skip the scheduler +"disabled_runners": []string{"goravel:schedule"}, - return map[string]credentials.TransportCredentials{ - "user": userCreds, - } - }). - Create() +// Scheduler-only container — disable http, queue, grpc +"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"}, + +// Disable every framework runner +"disabled_runners": []string{"goravel:*"}, + +// Kill switch: run no auto-run runners in the main process +"disabled_runners": []string{"*"}, ``` -[View Document](../the-basics/grpc.md#transport-credentials) +Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in `foundation.configureRunners()`, so any new runner added in the future picks up the same behavior for free. -### Orm query exposes context +[View Document](../getting-started/configuration.md#disabled-runners) -Orm queries now provide a `Context()` accessor. Code that receives a `contractsorm.Query`, such as model global scopes, can read the context passed by `facades.Orm().WithContext(ctx)`. + +### WithoutMiddleware for routes + +Goravel v1.18 introduces the `WithoutMiddleware` method for both routes and route groups, similar to Laravel's `withoutMiddleware()`. This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting. + +Exclude middleware on individual routes from within a middleware group: ```go -"tenant": func(query contractsorm.Query) contractsorm.Query { - tenantID, _ := query.Context().Value("tenant_id").(uint) +facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) { + router.Get("/dashboard", dashboardController.Index) - return query.Where("tenant_id", tenantID) -} + // This route excludes the throttle middleware + router.Get("/api/webhook", webhookController.Handle). + WithoutMiddleware(middleware.Throttle("global")) +}) ``` -[View Document](../orm/getting-started.md#setting-global-scope) +Exclude middleware for all routes within a group: -### Package setup supports import aliases +```go +facades.Route().Middleware(middleware.Auth()). + WithoutMiddleware(middleware.Auth()). + Group(func(router route.Router) { + router.Get("/public", publicController.Index) + }) +``` -Package setup modify helpers now support import aliases in the `" "` format. This keeps generated bootstrap files readable when package import paths are long or when the package should be referenced with a custom name. +> **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. -```go -serviceProvider := "&admin.ServiceProvider{}" -moduleImport := "admin " + setup.Paths().Module().Import() +[View Document](../the-basics/routing.md#without-middleware) -setup.Install( - modify.RegisterProvider(moduleImport, serviceProvider), -).Uninstall( - modify.UnregisterProvider(moduleImport, serviceProvider), -).Execute() + +### Build command supports go generate + +The `build` command now supports the `--generate` flag and `-g` alias. When enabled, Goravel runs `go generate ./...` before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior. + +```shell +go run . artisan build --generate +go run . artisan build -g ``` -[View Document](../digging-deeper/package-development.md#install-the-package) +[View Document](../getting-started/compile.md#compile-command) + ### Artisan console supports table output @@ -642,6 +657,7 @@ func (receiver *ReportCommand) Handle(ctx console.Context) error { [View Document](../digging-deeper/artisan-console.md#tables) + ### Artisan console supports graceful shutdown via Shutdownable Console commands can now opt into graceful shutdown by implementing the optional `console.Shutdownable` interface. The framework wires a `signal.NotifyContext` through every command, so pressing `Ctrl+C` (or sending `SIGTERM`) cancels the context passed to `Handle`. If the command also implements `Shutdown(ctx Context) error`, the framework calls it with a fresh context and a 30s budget for cleanup before the process exits. `Handle` runs in a goroutine, so the signal response is immediate. @@ -686,6 +702,7 @@ Commands that do not implement `Shutdownable` keep the original behavior — the [View Document](../digging-deeper/artisan-console.md#graceful-shutdown) + ### Artisan console supports commands filtering `ApplicationBuilder` now provides a `WithCommandsFilter` method to scope which built-in Artisan commands are registered — useful for production binaries that don't need `make:*`, `package:*`, or `vendor:publish`. The callback returns a positive list of command signatures to keep, with support for exact match and glob (`*`) matching: @@ -711,11 +728,43 @@ func Boot() contractsfoundation.Application { [View Document](../digging-deeper/artisan-console.md#filtering-commands) -### Log WithContext filters framework context keys out -Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. +### gRPC supports TLS and mTLS credentials + +Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use `WithGrpcServerCredentials` to enable TLS or mTLS for the server, and `WithGrpcClientCredentials` to register client credential groups that can be referenced by `grpc.clients..credentials`. + +```go +return foundation.Setup(). + WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials { + userCreds, err := credentials.NewClientTLSFromFile("storage/certs/ca.crt", "user.example.com") + if err != nil { + panic(err) + } + + return map[string]credentials.TransportCredentials{ + "user": userCreds, + } + }). + Create() +``` + +[View Document](../the-basics/grpc.md#transport-credentials) + + +### Orm query exposes context + +Orm queries now provide a `Context()` accessor. Code that receives a `contractsorm.Query`, such as model global scopes, can read the context passed by `facades.Orm().WithContext(ctx)`. + +```go +"tenant": func(query contractsorm.Query) contractsorm.Query { + tenantID, _ := query.Context().Value("tenant_id").(uint) + + return query.Where("tenant_id", tenantID) +} +``` + +[View Document](../orm/getting-started.md#setting-global-scope) -[View Document](../the-basics/logging.md#inject-context) ### Package views via LoadViewsFrom @@ -732,86 +781,124 @@ func (r *ServiceProvider) Boot(app foundation.Application) { > **Note**: If you want to use this feature in the fiber driver, please check [Fiber driver template configuration replaced with DefaultTemplate](#fiber-driver-template-configuration-replaced-with-defaulttemplate). -### Runners can be disabled via config option -Goravel v1.18 replaces the internal `app.auto_run` flag with a public, glob-aware `app.disabled_runners` config option. It lets you selectively skip auto-run [runners](../architecture-concepts/service-providers.md#runners) in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping `goravel:http`, `goravel:queue`, and `goravel:grpc` runners. +### Package setup supports import aliases -The value is a slice of glob patterns matched against each runner's signature with Go's [`path.Match`](https://pkg.go.dev/path#Match): +Package setup modify helpers now support import aliases in the `" "` format. This keeps generated bootstrap files readable when package import paths are long or when the package should be referenced with a custom name. ```go -// config/app.go -"app": map[string]any{ - "env": "production", - "debug": false, - "disabled_runners": []string{"goravel:schedule"}, -}, +serviceProvider := "&admin.ServiceProvider{}" +moduleImport := "admin " + setup.Paths().Module().Import() + +setup.Install( + modify.RegisterProvider(moduleImport, serviceProvider), +).Uninstall( + modify.UnregisterProvider(moduleImport, serviceProvider), +).Execute() ``` -The framework runner signatures are `goravel:http`, `goravel:grpc`, `goravel:queue`, `goravel:schedule`, and `goravel:telemetry`. Patterns are evaluated in order and the first match wins, so you can mix exact and wildcard matches: +[View Document](../digging-deeper/package-development.md#install-the-package) + + +### Queue drivers support batch-consuming via DriverWithReceive + +Custom queue drivers can now optionally implement the `DriverWithReceive` interface ([goravel/framework#1435](https://github.com/goravel/framework/pull/1435)) to support blocking batch message consumption. When a driver implements this interface, the queue worker automatically uses `Receive` instead of `Pop`, reducing polling overhead for high-throughput drivers like Kafka or RabbitMQ. ```go -// Web container — only skip the scheduler -"disabled_runners": []string{"goravel:schedule"}, +import ( + "context" + "github.com/goravel/framework/contracts/queue" +) -// Scheduler-only container — disable http, queue, grpc -"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"}, +type KafkaDriver struct{} -// Disable every framework runner -"disabled_runners": []string{"goravel:*"}, +// Base Driver interface methods (required) +func (d *KafkaDriver) Driver() string { return "kafka" } +func (d *KafkaDriver) Push(task queue.Task, queue string) error { return nil } +func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { return nil, nil } -// Kill switch: run no auto-run runners in the main process -"disabled_runners": []string{"*"}, +// Optional DriverWithReceive for batch consumption +func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) { + // blocking batch receive up to `count` jobs, returns when ctx expires +} ``` -Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in `foundation.configureRunners()`, so any new runner added in the future picks up the same behavior for free. +When `Receive` is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The `context.Context` is canceled on worker shutdown, ensuring clean termination. No application code changes are required for existing drivers. -[View Document](../getting-started/configuration.md#disabled-runners) +[View Document](../digging-deeper/queues.md#batch-receive-drivers) -### WithoutMiddleware for routes -Goravel v1.18 introduces the `WithoutMiddleware` method for both routes and route groups, similar to Laravel's `withoutMiddleware()`. This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting. +### Log WithContext filters framework context keys out -Exclude middleware on individual routes from within a middleware group: +Previously, `facades.Log().WithContext(ctx)` could write framework-internal context keys such as `GoravelAuthJwt` and `goravel_http_client_name` to log output. Goravel now excludes these keys by default, and you can add `logging.context.exclude` for project-specific context keys. -```go -facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) { - router.Get("/dashboard", dashboardController.Index) +[View Document](../the-basics/logging.md#inject-context) - // This route excludes the throttle middleware - router.Get("/api/webhook", webhookController.Handle). - WithoutMiddleware(middleware.Throttle("global")) -}) + +### Installer supports agent skill management commands + +The `goravel` CLI now supports `goravel skill:list` and `goravel skill:install` for discovering and installing Goravel agent skills. These commands are designed for contributors who want to install AI coding assistant skills to help follow Goravel development conventions such as testing, naming, and pull request workflows. + +Skills are sourced from the [goravel/agents](https://github.com/goravel/agents) repository and installed to `~/.agents/skills` by default. You can filter by name, choose a custom destination with `--path`, and overwrite existing skills with `--force`. + +[View Document](../prologue/contributions.md#agent-skills) + + +### Fiber driver context decoupled from Fiber instance lifecycle + +When a request timed out with the Fiber driver, the `context.Context` interface methods (`Done()`, `Deadline()`, `Err()`) delegated to the underlying `fiber.Ctx`, which became invalid after the request was recycled by fasthttp. This caused nil-pointer panics when background goroutines (e.g. `database/sql`) still held a reference to the request context ([goravel/goravel#951](https://github.com/goravel/goravel/issues/951)). + +The fix ([goravel/fiber#253](https://github.com/goravel/fiber/pull/253)) stores the user context and values as struct fields on the goravel `Context` struct instead of delegating to `fiber.Ctx`, so timeout signals propagate correctly and background goroutines can safely access the context after the request returns. + +No application code changes are required. + + +### Fiber driver template configuration replaced with DefaultTemplate + +To support package views via `LoadViewsFrom`, the Fiber driver ([goravel/fiber#273](https://github.com/goravel/fiber/pull/273)) has replaced its dependency on `gofiber/template/html/v3` with a built-in `DefaultTemplate()` that loads views from `resources/views` and any registered package views automatically. If your `config/http.go` has a custom `template` configuration using `html.New(path.Resource("views"), ".tmpl")` and you want to use the [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) feature, update it to use `goravelfiber.DefaultTemplate()` instead: + +```diff + // config/http.go — fiber driver + +import goravelfiber "github.com/goravel/fiber" + + "template": func() (fiber.Views, error) { +- return html.New(path.Resource("views"), ".tmpl"), nil ++ return goravelfiber.DefaultTemplate(), nil + }, ``` -Exclude middleware for all routes within a group: +For custom delimiters or FuncMap, use `goravelfiber.NewTemplate(goravelfiber.RenderOptions{...})`: ```go -facades.Route().Middleware(middleware.Auth()). - WithoutMiddleware(middleware.Auth()). - Group(func(router route.Router) { - router.Get("/public", publicController.Index) - }) +import goravelfiber "github.com/goravel/fiber" + +"template": func() (fiber.Views, error) { + return goravelfiber.NewTemplate(goravelfiber.RenderOptions{ + Delims: &goravelfiber.Delims{Left: "{[", Right: "]}"}, + }) +}, ``` -> **Note**: Middleware exclusion uses the `Signature()` method to identify middlewares. Make sure each middleware returns a unique signature for `WithoutMiddleware` to work correctly. The built-in framework middlewares already provide unique signatures. +If no `template` configuration is defined, `DefaultTemplate()` is used automatically — no code change is required. -[View Document](../the-basics/routing.md#without-middleware) -### Validation engine rewritten natively +### Cache prefix supports empty string -The validation engine was rewritten in [goravel/framework#1397](https://github.com/goravel/framework/pull/1397) — replacing the external `gookit/validate` dependency with a native implementation that is fully compatible with Laravel's validation API. This change introduces several downstream effects: +The Redis cache driver now respects an empty `prefix` configuration. Previously, even with an empty prefix, a colon (`:`) separator was always prepended to every key, making keys inconsistent when accessing Redis directly. Now, if the `prefix` option in `config/cache.go` is set to an empty string, no prefix or separator is applied: -1. **Error message format changed** from old-style messages (e.g., `"name is required to not be empty"`) to Laravel-style messages (e.g., `"The name field is required."`). Tests that assert on specific validation error messages must be updated. +```go +// config/cache.go +"prefix": "", +``` -2. **Wildcard rules (`*.`) now consistently report errors.** The old engine might have skipped wildcard-field errors depending on input; the new engine always evaluates and reports them. +With this configuration, a cache key like `users` is stored as `users` in Redis, rather than `:users`. -3. **Form request method signatures changed** — `Rules()`, `Messages()`, `Attributes()`, and `Filters()` now return `map[string]any` instead of `map[string]string`. +No application code changes are required for existing projects with a non-empty prefix. -4. **Custom validation rule names may conflict** with new built-in rules. Rename custom rules that share names with built-in rules (e.g., `exists` → `exists_custom`). +[View Document](../digging-deeper/cache.md#cache-key-prefix) -Error messages are now powered by the [localization](../digging-deeper/localization.md) system. Default messages are embedded as `lang/en/validation.json` and use `validation.*` translation keys. Use `go run . artisan lang:publish` to publish and customize them. -[View Document](../the-basics/validation.md) ### HTTP Middleware changed from func to interface @@ -830,6 +917,24 @@ Custom middleware must be updated to implement the new interface. See the [upgra [View Document](../the-basics/middleware.md) + +### Validation engine rewritten natively + +The validation engine was rewritten in [goravel/framework#1397](https://github.com/goravel/framework/pull/1397) — replacing the external `gookit/validate` dependency with a native implementation that is fully compatible with Laravel's validation API. This change introduces several downstream effects: + +1. **Error message format changed** from old-style messages (e.g., `"name is required to not be empty"`) to Laravel-style messages (e.g., `"The name field is required."`). Tests that assert on specific validation error messages must be updated. + +2. **Wildcard rules (`*.`) now consistently report errors.** The old engine might have skipped wildcard-field errors depending on input; the new engine always evaluates and reports them. + +3. **Form request method signatures changed** — `Rules()`, `Messages()`, `Attributes()`, and `Filters()` now return `map[string]any` instead of `map[string]string`. + +4. **Custom validation rule names may conflict** with new built-in rules. Rename custom rules that share names with built-in rules (e.g., `exists` → `exists_custom`). + +Error messages are now powered by the [localization](../digging-deeper/localization.md) system. Default messages are embedded as `lang/en/validation.json` and use `validation.*` translation keys. Use `go run . artisan lang:publish` to publish and customize them. + +[View Document](../the-basics/validation.md) + + ### Mail content separates raw text from template views Goravel now treats `mail.Content.Text` as raw plain-text email body content. Template paths are explicit: use `HtmlView` for HTML templates and `TextView` for plain-text templates. @@ -861,6 +966,7 @@ func (m *WelcomeMail) Content() *mail.Content { [View Document](../digging-deeper/mail.md#using-template) + ### gRPC config key renamed from servers to clients The gRPC configuration key `servers` has been renamed to `clients` in `config/grpc.go` to accurately reflect that these entries register gRPC clients (consumers of remote services) rather than servers. @@ -889,90 +995,10 @@ In application code, update any config path references from `grpc.servers` to `g [View Document](../the-basics/grpc.md) -### Fiber driver context decoupled from Fiber instance lifecycle - -When a request timed out with the Fiber driver, the `context.Context` interface methods (`Done()`, `Deadline()`, `Err()`) delegated to the underlying `fiber.Ctx`, which became invalid after the request was recycled by fasthttp. This caused nil-pointer panics when background goroutines (e.g. `database/sql`) still held a reference to the request context ([goravel/goravel#951](https://github.com/goravel/goravel/issues/951)). - -The fix ([goravel/fiber#253](https://github.com/goravel/fiber/pull/253)) stores the user context and values as struct fields on the goravel `Context` struct instead of delegating to `fiber.Ctx`, so timeout signals propagate correctly and background goroutines can safely access the context after the request returns. - -No application code changes are required. - -### Fiber driver template configuration replaced with DefaultTemplate - -To support package views via `LoadViewsFrom`, the Fiber driver ([goravel/fiber#273](https://github.com/goravel/fiber/pull/273)) has replaced its dependency on `gofiber/template/html/v3` with a built-in `DefaultTemplate()` that loads views from `resources/views` and any registered package views automatically. If your `config/http.go` has a custom `template` configuration using `html.New(path.Resource("views"), ".tmpl")` and you want to use the [Package views via LoadViewsFrom](#package-views-via-loadviewsfrom) feature, update it to use `goravelfiber.DefaultTemplate()` instead: - -```diff - // config/http.go — fiber driver - -import goravelfiber "github.com/goravel/fiber" - - "template": func() (fiber.Views, error) { -- return html.New(path.Resource("views"), ".tmpl"), nil -+ return goravelfiber.DefaultTemplate(), nil - }, -``` - -For custom delimiters or FuncMap, use `goravelfiber.NewTemplate(goravelfiber.RenderOptions{...})`: - -```go -import goravelfiber "github.com/goravel/fiber" - -"template": func() (fiber.Views, error) { - return goravelfiber.NewTemplate(goravelfiber.RenderOptions{ - Delims: &goravelfiber.Delims{Left: "{[", Right: "]}"}, - }) -}, -``` - -If no `template` configuration is defined, `DefaultTemplate()` is used automatically — no code change is required. - -### Installer supports agent skill management commands - -The `goravel` CLI now supports `goravel skill:list` and `goravel skill:install` for discovering and installing Goravel agent skills. These commands are designed for contributors who want to install AI coding assistant skills to help follow Goravel development conventions such as testing, naming, and pull request workflows. - -Skills are sourced from the [goravel/agents](https://github.com/goravel/agents) repository and installed to `~/.agents/skills` by default. You can filter by name, choose a custom destination with `--path`, and overwrite existing skills with `--force`. - -[View Document](../prologue/contributions.md#agent-skills) - -### Queue drivers support batch-consuming via DriverWithReceive - -Custom queue drivers can now optionally implement the `DriverWithReceive` interface ([goravel/framework#1435](https://github.com/goravel/framework/pull/1435)) to support blocking batch message consumption. When a driver implements this interface, the queue worker automatically uses `Receive` instead of `Pop`, reducing polling overhead for high-throughput drivers like Kafka or RabbitMQ. - -```go -import ( - "context" - "github.com/goravel/framework/contracts/queue" -) - -type KafkaDriver struct{} - -// Base Driver interface methods (required) -func (d *KafkaDriver) Driver() string { return "kafka" } -func (d *KafkaDriver) Push(task queue.Task, queue string) error { return nil } -func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { return nil, nil } - -// Optional DriverWithReceive for batch consumption -func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) { - // blocking batch receive up to `count` jobs, returns when ctx expires -} -``` - -When `Receive` is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The `context.Context` is canceled on worker shutdown, ensuring clean termination. No application code changes are required for existing drivers. - -[View Document](../digging-deeper/queues.md#batch-receive-drivers) - -### Cache prefix supports empty string - -The Redis cache driver now respects an empty `prefix` configuration. Previously, even with an empty prefix, a colon (`:`) separator was always prepended to every key, making keys inconsistent when accessing Redis directly. Now, if the `prefix` option in `config/cache.go` is set to an empty string, no prefix or separator is applied: - -```go -// config/cache.go -"prefix": "", -``` - -With this configuration, a cache key like `users` is stored as `users` in Redis, rather than `:users`. +### HTTP driver timeout middleware rewritten for concurrent safety -No application code changes are required for existing projects with a non-empty prefix. +The timeout middleware in both the Gin driver ([goravel/gin#222](https://github.com/goravel/gin/pull/222)) and the Fiber driver ([goravel/fiber#263](https://github.com/goravel/fiber/pull/263)) has been rewritten to fix a concurrency bug ([goravel#966](https://github.com/goravel/goravel/issues/966)). The previous implementation ran the handler chain in a background goroutine and could return a timeout response while the handler kept running on request-scoped state that was already eligible for reuse, causing cross-request response contamination under high concurrency. -[View Document](../digging-deeper/cache.md#cache-key-prefix) +Both drivers now delegate to their framework's native timeout middleware (`gin-contrib/timeout` for Gin, Fiber's built-in timeout for Fiber), which properly abandons timed-out request contexts instead of recycling them. The `Timeout(...)` middleware signature is unchanged and `ctx.Done()` still reflects request deadlines — no application code changes are required. +[View Document](../the-basics/routing.md)