+func (receiver *SendEmails) Extend() command.Extend {
+ return command.Extend{
+ Arguments: []command.Argument{
+ &command.ArgumentString{
+ Name: "subject",
+ Usage: "subject of email",
+ Required: true,
+ },
+ &command.ArgumentStringSlice{
+ Name: "emails",
+ Usage: "target emails",
+ Min: 1,
+ Max: -1,
+ },
+ },
+ }
+}
+```
+
+Supported agrument types : `ArgumentFloat32`, `ArgumentFloat64`, `ArgumentInt`, `ArgumentInt8`, `ArgumentInt16`, `ArgumentInt32`, `ArgumentInt64`, `ArgumentString`, `ArgumentUint`, `ArgumentUint8`, `ArgumentUint16`, `ArgumentUint32`, `ArgumentUint64`, `ArgumentTimestamp`, `ArgumentFloat32Slice`, `ArgumentFloat64Slice`, `ArgumentIntSlice`, `ArgumentInt8Slice`, `ArgumentInt16Slice`, `ArgumentInt32Slice`, `ArgumentInt64Slice`, `ArgumentStringSlice`, `ArgumentUintSlice`, `ArgumentUint8Slice`, `ArgumentUint16Slice`, `ArgumentUint32Slice`, `ArgumentUint64Slice`, `ArgumentTimestampSlice`
+
+Argument types with single value support next fields:
+
+```go
+ Name string // the name of this argument
+ Value T // the default value of this argument
+ Usage string // the usage text to show
+ Required bool // if this argument is required
+```
+
+Slice argument types fields:
+
+```go
+ Name string // the name of this argument
+ Value T // the default value of this argument
+ Usage string // the usage text to show
+ Min int // the min num of occurrences of this argument
+ Max int // the max num of occurrences of this argument, set to -1 for unlimited
```
+Timestamp arguments additionally supports `Layouts []string` field, that should be filled with [supported layouts](https://pkg.go.dev/time#pkg-constants)
+
Get arguments:
+```go
+func (receiver *SendEmails) Handle(ctx console.Context) error {
+ subject := ctx.ArgumentString("subject")
+ emails := ctx.ArgumentStringSlice("emails")
+
+ return nil
+}
+```
+
+Alternatively, it is possible to access arguments directly:
```go
func (receiver *SendEmails) Handle(ctx console.Context) error {
name := ctx.Argument(0)
@@ -142,8 +209,8 @@ func (receiver *ListCommand) Handle(ctx console.Context) error {
Usage:
```shell
-go run . artisan emails --lang Chinese
-go run . artisan emails -l Chinese
+./artisan emails --lang Chinese
+./artisan emails -l Chinese
```
Except `command.StringFlag`, we can also use other type `Flag` and `Option*`: `StringSliceFlag`, `BoolFlag`, `Float64Flag`, `Float64SliceFlag`, `IntFlag`, `IntSliceFlag`, `Int64Flag`, `Int64SliceFlag`.
@@ -360,6 +427,19 @@ func (receiver *SendEmails) Handle(ctx console.Context) error {
}
```
+There are few helpers to write to console with respective color:
+
+```go
+ctx.Green("This is a green message")
+ctx.Greenln("This is a green line message")
+ctx.Red("This is a red message")
+ctx.Redln("This is a red line message")
+ctx.Yellow("This is a yellow message")
+ctx.Yellowln("This is a yellow line message")
+ctx.Black("This is a black message")
+ctx.Blackln("This is a black line message")
+```
+
You can use the `NewLine` method to write a new line to the console:
```go
@@ -415,9 +495,18 @@ err := ctx.Spinner("Loading...", console.SpinnerOption{
})
```
+### Divider
+
+To show terminal-width divider you may use `Divider` method.
+
+```go
+ctx.Divider() // ----------
+ctx.Divider("=>") // =>=>=>=>=>
+```
+
## Category
-You can set a set of commands to the same category, convenient in `go run . artisan list`:
+You can set a set of commands to the same category, convenient in `./artisan list`:
```go
// Extend The console command extend.
@@ -428,15 +517,16 @@ func (receiver *ConsoleMakeCommand) Extend() command.Extend {
}
```
-## Registering Commands
+## Register Commands
-All of your console commands need to be registered within the `Commands` function in `app\console\kernel.go`.
+A new migration 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 rule manually if you create the command file by yourself.
```go
-func (kernel Kernel) Commands() []console.Command {
- return []console.Command{
- &commands.SendEmails{},
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithCommands(Commands).
+ WithConfig(config.Boot).
+ Start()
}
```
@@ -456,5 +546,5 @@ facades.Route().Get("/", func(c *gin.Context) {
Some commands print colors by default, such as the `list` command. However, in some terminals or logs, the color values may be garbled. You can use the `--no-ansi` option to disable the print colors:
```shell
-go run . artisan list --no-ansi
+./artisan list --no-ansi
```
diff --git a/en/digging-deeper/event.md b/en/digging-deeper/event.md
index c93a64c43..004eca7ae 100644
--- a/en/digging-deeper/event.md
+++ b/en/digging-deeper/event.md
@@ -6,34 +6,24 @@
Goravel's events provide a simple observer pattern implementation, allowing you to subscribe and listen to various events that occur within your application. Event classes are typically stored in the `app/events` directory, while their listeners are stored in `app/listeners`. Don't worry if you don't see these directories in your application as they will be created for you as you generate events and listeners using Artisan console commands.
-Events serve as a great way to decouple various aspects of your application, as a single event can have multiple listeners that do not depend on each other. For example, you may wish to send a Slack notification to your user each time an order is shipped. Instead of coupling your order processing code to your Slack notification code, you can raise an `app\events\OrderShipped` event which a listener can receive and use to dispatch a Slack notification.
+Events serve as a great way to decouple various aspects of your application, as a single event can have multiple listeners that do not depend on each other. For example, you may wish to send a Slack notification to your user each time an order is shipped. Instead of coupling your order processing code to your Slack notification code, you can raise an `app/events/OrderShipped` event which a listener can receive and use to dispatch a Slack notification.
-## Registering Events & Listeners
+## Register Events & Listeners
-The `app\providers\EventServiceProvider` included with your Goravel application provides a convenient place to register all of your application's event listeners. The `listen` method contains an array of all events (keys) and their listeners (values). You may add as many events to this array as your application requires. For example, let's add an `OrderShipped` event:
+All events and listeners should be registered via the `WithEvents` function in the `bootstrap/app.go` file:
```go
-package providers
-
-import (
- "github.com/goravel/framework/contracts/event"
- "github.com/goravel/framework/facades"
-
- "goravel/app/events"
- "goravel/app/listeners"
-)
-
-type EventServiceProvider struct {
-}
-
-...
-
-func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener {
- return map[event.Event][]event.Listener{
- &events.OrderShipped{}: {
- &listeners.SendShipmentNotification{},
- },
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithEvents(func() map[event.Event][]event.Listener {
+ return map[event.Event][]event.Listener{
+ events.NewOrderShipped(): {
+ listeners.NewSendShipmentNotification(),
+ },
+ }
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
@@ -42,11 +32,11 @@ func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener
You can use the `make:event` and `make:listener` Artisan commands to generate individual events and listeners:
```go
-go run . artisan make:event PodcastProcessed
-go run . artisan make:event user/PodcastProcessed
+./artisan make:event PodcastProcessed
+./artisan make:event user/PodcastProcessed
-go run . artisan make:listener SendPodcastNotification
-go run . artisan make:listener user/SendPodcastNotification
+./artisan make:listener SendPodcastNotification
+./artisan make:listener user/SendPodcastNotification
```
## Defining Events
@@ -58,8 +48,7 @@ package events
import "github.com/goravel/framework/contracts/event"
-type OrderShipped struct {
-}
+type OrderShipped struct {}
func (receiver *OrderShipped) Handle(args []event.Arg) ([]event.Arg, error) {
return args, nil
@@ -77,8 +66,7 @@ import (
"github.com/goravel/framework/contracts/event"
)
-type SendShipmentNotification struct {
-}
+type SendShipmentNotification struct {}
func (receiver *SendShipmentNotification) Signature() string {
return "send_shipment_notification"
@@ -139,9 +127,9 @@ package controllers
import (
"github.com/goravel/framework/contracts/event"
"github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/facades"
"goravel/app/events"
+ "goravel/app/facades"
)
type UserController struct {
diff --git a/en/digging-deeper/filesystem.md b/en/digging-deeper/filesystem.md
index 811a43a30..465ba7196 100644
--- a/en/digging-deeper/filesystem.md
+++ b/en/digging-deeper/filesystem.md
@@ -12,7 +12,6 @@ The Goravel provides simple drivers for working with local filesystems, Amazon S
| OSS | [https://github.com/goravel/oss](https://github.com/goravel/oss) |
| COS | [https://github.com/goravel/cos](https://github.com/goravel/cos) |
| Minio | [https://github.com/goravel/minio](https://github.com/goravel/minio) |
-| Cloudinary | [https://github.com/goravel/cloudinary](https://github.com/goravel/cloudinary) |
## Configuration
@@ -61,7 +60,7 @@ facades.Storage().WithContext(ctx).Put("avatars/1.png", "Contents")
The `Get` method may be used to retrieve the contents of a file. The raw string contents of the file will be returned by the method. Remember, all file paths should be specified relative to the disk's `root` location:
```go
-contents := facades.Storage().Get("file.jpg")
+content := facades.Storage().Get("file.jpg")
```
The `Exists` method may be used to determine if a file exists on the disk:
@@ -338,4 +337,4 @@ type Driver interface {
}
```
-> Note: Since the configuration has not been loaded when the custom driver is registered, so please use `facades.Config().Env` to obtain the configuration in the custom driver.
+> Note: Since the configuration has not been loaded when the custom driver is registered, so please use `facades.Config().Env()` to obtain the configuration in the custom driver.
diff --git a/en/digging-deeper/http-client.md b/en/digging-deeper/http-client.md
index d3d0b84cc..fc9cc5358 100644
--- a/en/digging-deeper/http-client.md
+++ b/en/digging-deeper/http-client.md
@@ -11,27 +11,7 @@ all designed to enhance the developer experience.
## Configuration
-Goravel's HTTP client is built on top of the `net/http.Client` for making HTTP requests. If you need to tweak its internal settings,
-just update the `client` property in the `config/http.go` file.
-Here are the available configuration options:
-
-- `base_url`: Sets the root URL for relative paths. Automatically prefixes requests that don't start with `http://` or `https://`.
-- `timeout`(`DEFAULT`: `30s`): Global timeout duration for complete request lifecycle (connection + any redirects + reading the response body). A Timeout of zero means no timeout.
-- `max_idle_conns`: Maximum number of idle (keep-alive) connections across all hosts. Zero means no limit.
-- `max_idle_conns_per_host`: Maximum idle (keep-alive) connections to keep per-host
-- `max_conns_per_host`: Limits the total number of connections per host, including connections in the dialing, active, and idle states. Zero means no limit.
-- `idle_conn_timeout`: Maximum amount of the time of an idle (keep-alive) connection will remain idle before closing itself.
-
-```go
-"client": map[string]any{
- "base_url": config.GetString("HTTP_CLIENT_BASE_URL"), // "https://api.example.com"
- "timeout": config.GetDuration("HTTP_CLIENT_TIMEOUT"), // 30 * time.Second
- "max_idle_conns": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS"), // 100
- "max_idle_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST"), // 10
- "max_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_CONN_PER_HOST"), // 0
- "idle_conn_timeout": config.GetDuration("HTTP_CLIENT_IDLE_CONN_TIMEOUT"), // 90 * time.Second
-}
-```
+Goravel's HTTP client is built on top of the `net/http.Client` for making HTTP requests. If you need to tweak its internal settings, just update the `clients` property in the `config/http.go` file.
## Making Requests
@@ -40,31 +20,36 @@ The Http facade provides a convenient way to make HTTP requests using familiar v
**Example: GET Request**
```go
-import "github.com/goravel/framework/facades"
-
response, err := facades.Http().Get("https://example.com")
```
Each HTTP verb method returns a `response` of type `framework/contracts/http/client.Response` and an `err` if the request fails.
+You can use the `Client` function to specify which HTTP client configuration to use:
+
+```go
+response, err := facades.Http().Client("github").Get("https://example.com")
+```
+
### Response Interface
The `framework/contracts/http/client.Response` interface provides the following methods to interact with the HTTP response:
```go
type Response interface {
+ Bind(value any) error // Bind the response body to a struct
Body() (string, error) // Get the response body as a string
- ClientError() bool // Check if the status code is in the 4xx range
+ ClientError() bool // Check if the status code is in the 4xx range
Cookie(name string) *http.Cookie // Get a specific cookie
- Cookies() []*http.Cookie // Get all response cookies
- Failed() bool // Check if the status code is not in the 2xx range
- Header(name string) string // Get the value of a specific header
- Headers() http.Header // Get all response headers
+ Cookies() []*http.Cookie // Get all response cookies
+ Failed() bool // Check if the status code is not in the 2xx range
+ Header(name string) string // Get the value of a specific header
+ Headers() http.Header // Get all response headers
Json() (map[string]any, error) // Decode the response body as JSON into a map
- Redirect() bool // Check if the response is a redirect (3xx status code)
- ServerError() bool // Check if the status code is in the 5xx range
- Status() int // Get the HTTP status code
- Successful() bool // Check if the status code is in the 2xx range
+ Redirect() bool // Check if the response is a redirect (3xx status code)
+ ServerError() bool // Check if the status code is in the 5xx range
+ Status() int // Get the HTTP status code
+ Successful() bool // Check if the status code is in the 2xx range
/* status code related methods */
@@ -264,7 +249,7 @@ response, err := facades.Http().WithContext(ctx).Get("https://example.com")
### Bind Response
-You can use the `Bind` method directly on the `Http` facade to specify the struct that the response should be bound to.
+You can use the `Bind` method to specify the struct that the response should be bound to.
```go
type User struct {
@@ -274,12 +259,18 @@ type User struct {
func main() {
var user User
- response, err := facades.Http().Bind(&user).AcceptJson().Get("https://jsonplaceholder.typicode.com/users/1")
+ response, err := facades.Http().AcceptJson().Get("https://jsonplaceholder.typicode.com/users/1")
if err != nil {
fmt.Println("Error making request:", err)
return
}
+ err = response.Bind(&user)
+ if err != nil {
+ fmt.Println("Error binding response:", err)
+ return
+ }
+
fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}
```
@@ -309,3 +300,186 @@ response, err := facades.Http().
WithoutCookie("language").
Get("https://example.com")
```
+
+## Testing
+
+When testing your application, you often want to avoid making real network requests to external APIs. Whether it's to
+speed up tests, avoid rate limits, or simulate failure scenarios, Goravel makes this easy. The `Http` facade provides a
+powerful `Fake` method that allows you to instruct the HTTP client to return stubbed (dummy) responses when requests are made.
+
+### Faking Responses
+
+To start faking requests, pass a map to the `Fake` method. The keys represent the URL patterns or client names
+you want to intercept, and the values represent the responses to return. You can use `*` as a wildcard character.
+
+The `Http` facade provides a convenient `Response` builder to construct various types of fake responses.
+
+```go
+facades.Http().Fake(map[string]any{
+ // Stub a specific URL
+ "https://github.com/goravel/framework": facades.Http().Response().Json(200, map[string]string{"foo": "bar"}),
+
+ // Stub a wildcard pattern
+ "https://google.com/*": facades.Http().Response().String(200, "Hello World"),
+
+ // Stub a specific Client (defined in config/http.go)
+ "github": facades.Http().Response().OK(),
+})
+```
+
+**Fallback URLs**
+
+Any request that does not match a pattern defined in `Fake` will be executed normally over the network. To prevent this,
+you can define a fallback pattern using the single `*` wildcard, which will match all unmatched URLs.
+
+```go
+facades.Http().Fake(map[string]any{
+ "https://github.com/*": facades.Http().Response().Json(200, map[string]string{"id": "1"}),
+ "*": facades.Http().Response().Status(404),
+})
+```
+
+**Implicit Conversions**
+
+For convenience, you do not always need to use the `Response` builder. You can pass simple `int`, `string`, or `map`
+values, and Goravel will automatically convert them into responses.
+
+```go
+facades.Http().Fake(map[string]any{
+ "https://goravel.dev/*": "Hello World", // String -> 200 OK with body
+ "https://github.com/*": map[string]string{"a": "b"}, // Map -> 200 OK JSON
+ "https://stripe.com/*": 500, // Int -> Status code only
+})
+```
+
+### Fake Response Builder
+
+The `facades.Http().Response()` method provides a fluent interface to build custom responses easily.
+
+```go
+// Create a response using a file content
+facades.Http().Response().File(200, "./tests/fixtures/user.json")
+
+// Create a JSON response
+facades.Http().Response().Json(201, map[string]any{"created": true})
+
+// Create a response with custom headers
+headers := http.Header{}
+headers.Add("X-Custom", "Value")
+facades.Http().Response().Make(200, "Body Content", headers)
+
+// Standard status helpers
+facades.Http().Response().OK()
+facades.Http().Response().Status(403)
+```
+
+### Faking Response Sequences
+
+Sometimes you may need to specify that a single URL should return a series of different responses in order,
+such as when testing retries or rate-limiting logic. You can use the `Sequence` method to build this flow.
+
+```go
+facades.Http().Fake(map[string]any{
+ "github": facades.Http().Sequence().
+ PushStatus(500). // 1st Request: Server Error
+ PushString(429, "Rate Limit"). // 2nd Request: Rate Limit
+ PushStatus(200), // 3rd Request: Success
+})
+```
+
+**Empty Sequences**
+
+When all responses in a sequence have been consumed, any further requests will cause the client to return an error.
+If you wish to specify a default response instead of failing, use the `WhenEmpty` method:
+
+```go
+facades.Http().Fake(map[string]any{
+ "github": facades.Http().Sequence().
+ PushStatus(200).
+ WhenEmpty(facades.Http().Response().Status(404)),
+})
+```
+
+### Inspecting Requests
+
+When faking responses, it is crucial to verify that the correct requests were actually sent by your application.
+You can use the `AssertSent` method to inspect the request and return a boolean indicating if it matches your expectations.
+
+```go
+facades.Http().AssertSent(func(req client.Request) bool {
+ return req.Url() == "https://api.example.com/users" &&
+ req.Method() == "POST" &&
+ req.Input("role") == "admin" &&
+ req.Header("Authorization") != ""
+})
+```
+
+**Other Assertions**
+
+You can also assert that a specific request was *not* sent, or check the total number of requests sent:
+
+```go
+// Assert a request was NOT sent
+facades.Http().AssertNotSent(func(req client.Request) bool {
+ return req.Url() == "https://api.example.com/legacy-endpoint"
+})
+
+// Assert that no requests were sent at all
+facades.Http().AssertNothingSent()
+
+// Assert that exactly 3 requests were sent
+facades.Http().AssertSentCount(3)
+```
+
+### Preventing Stray Requests
+
+To ensure your tests are strictly isolated and do not accidentally hit real external APIs, you can use the
+`PreventStrayRequests` method. After calling this, any request that does not match a defined Fake rule will cause the
+test to panic with an exception.
+
+```go
+facades.Http().Fake(map[string]any{
+ "github": facades.Http().Response().OK(),
+}).PreventStrayRequests()
+
+// This request is mocked and succeeds
+facades.Http().Client("github").Get("/")
+
+// This request is NOT mocked and will panic
+facades.Http().Get("https://google.com")
+```
+
+**Allowing Specific Strays**
+
+If you need to block most requests but allow specific internal services (like a local test server),
+you can use `AllowStrayRequests`:
+
+```go
+facades.Http().PreventStrayRequests().AllowStrayRequests([]string{
+ "http://localhost:8080/*",
+})
+```
+
+### Resetting State
+
+The `Http` facade is a singleton, meaning mocked responses persist across the entire runtime of your test suite unless
+cleared. To avoid "leaking" mocks from one test to another, you should strictly use the `Reset` method in
+your test cleanup or setup.
+
+```go
+func TestExternalApi(t *testing.T) {
+ defer facades.Http().Reset()
+
+ facades.Http().Fake(nil)
+
+ // ... assertions
+}
+```
+
+::: warning Global State & Parallel Testing
+The `Fake` and `Reset` methods mutate the global state of the HTTP client factory. Because of this, **you should avoid
+running tests that mock the HTTP client in parallel** (using `t.Parallel()`). Doing so may result in race conditions
+where one test resets the mocks while another is still running.
+:::
+
+
diff --git a/en/digging-deeper/localization.md b/en/digging-deeper/localization.md
index bd40154d7..53b8a335e 100644
--- a/en/digging-deeper/localization.md
+++ b/en/digging-deeper/localization.md
@@ -30,11 +30,11 @@ The default language of the application is stored in the `locale` configuration
You can also use the `SetLocale` method provided by the App Facade to modify the default language for a single `HTTP` request at runtime:
-```
+```go
facades.Route().Get("/", func(ctx http.Context) http.Response {
- facades.App().SetLocale(ctx, "en")
+ facades.App().SetLocale(ctx, "en")
- return ctx.Response()
+ return ctx.Response()
})
```
@@ -57,7 +57,7 @@ if facades.App().IsLocale(ctx, "en") {}
In language files, you can define single-level or multi-level structures:
-```
+```json
// lang/en.json
{
"name": "It's your name",
@@ -101,7 +101,7 @@ facades.Lang(ctx).Get("role/user.required.user_id")
You can define placeholders in translation strings. All placeholders have the prefix `:`. For example, you can use a placeholder to define a welcome message:
-```
+```json
{
"welcome": "Welcome, :name"
}
@@ -109,7 +109,7 @@ You can define placeholders in translation strings. All placeholders have the pr
To replace placeholders when retrieving a translation string, you can pass a translation option with the replacement map as the second parameter to the `facades.Lang(ctx).Get()` method:
-```
+```go
facades.Lang(ctx).Get("welcome", translation.Option{
Replace: map[string]string{
"name": "Goravel",
@@ -121,7 +121,7 @@ facades.Lang(ctx).Get("welcome", translation.Option{
Pluralization is a complex problem because different languages have various pluralization rules. However, Goravel can help you translate strings based on the pluralization rules you define. By using the `|` character, you can differentiate between the singular and plural forms of a string:
-```
+```json
{
"apples": "There is one apple|There are many apples"
}
@@ -129,7 +129,7 @@ Pluralization is a complex problem because different languages have various plur
You can even create more complex pluralization rules by specifying translation strings for multiple value ranges:
-```
+```json
{
"apples": "{0} There are none|[1,19] There are some|[20,*] There are many"
}
@@ -137,7 +137,7 @@ You can even create more complex pluralization rules by specifying translation s
After defining a translation string with pluralization options, you can use the `facades.Lang(ctx).Choice()` method to retrieve the line for a given `count`. In this example, because the count is greater than 1, the plural form of the translation string is returned:
-```
+```go
facades.Lang(ctx).Choice("messages.apples", 10)
```
diff --git a/en/digging-deeper/mail.md b/en/digging-deeper/mail.md
index 93c494039..d4b9937dd 100644
--- a/en/digging-deeper/mail.md
+++ b/en/digging-deeper/mail.md
@@ -131,3 +131,70 @@ Then you can use the `Mailalbe` in the `Send` and `Queue` methods:
err := facades.Mail().Send(mails.NewOrderShipped())
err := facades.Mail().Queue(mails.NewOrderShipped())
```
+
+## Using Template
+
+The mail module now supports using templates directly with the `html/template` engine. This allows you to render email templates with dynamic data.
+
+### Configuration
+
+To enable template support, configure the `config/mail.go` file:
+
+```go
+"template": map[string]any{
+ "default": config.Env("MAIL_TEMPLATE_ENGINE", "html"),
+ "engines": map[string]any{
+ "html": map[string]any{
+ "driver": "html",
+ "path": config.Env("MAIL_VIEWS_PATH", "resources/views/mail"),
+ },
+ },
+}
+```
+
+### Creating Templates
+
+Create your email templates in the specified views directory. For example:
+
+```html
+
+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:
+
+```go
+facades.Mail().
+ To([]string{"user@example.com"}).
+ Subject("Welcome").
+ Content(mail.Content{
+ View: "welcome.tmpl",
+ With: map[string]any{
+ "Name": "John",
+ "AppName": "Goravel",
+ },
+ }).
+ Send()
+```
+
+### Custom Template Engines
+
+You can also register custom template engines in the configuration:
+
+```go
+"template": map[string]any{
+ "default": "blade",
+ "engines": map[string]any{
+ "blade": map[string]any{
+ "driver": "custom",
+ "via": func() (mail.Template, error) {
+ return NewBladeTemplateEngine(), nil
+ },
+ },
+ },
+}
+```
+
diff --git a/en/digging-deeper/package-development.md b/en/digging-deeper/package-development.md
index 55a0f2f5f..328f3b80e 100644
--- a/en/digging-deeper/package-development.md
+++ b/en/digging-deeper/package-development.md
@@ -13,22 +13,20 @@ Here is an example for building a third-party package: [goravel/example-package]
You can use easily create a package template using the Artisan command:
```shell
-go run . artisan make:package sms
+./artisan make:package sms
```
The created files are saved by default in the root `packages` folder, you can use `--root` option to customize:
```shell
-go run . artisan make:package --root=pkg sms
+./artisan make:package --root=pkg sms
```
## Service Providers
[Service providers](../architecture-concepts/service-providers.md) act as the bridge between your package and Goravel. They are typically located in the root of the package as a `service_provider.go` file. Their main function is to bind items into Goravel's service container and guide Goravel in loading package resources.
-## Usage
-
-### Auto Install
+## Install The Package
When creating a package, if it contains a `setup/setup.go` file, you can define the package installation logic in this file, and then users can use the `package:install` command to install the package:
@@ -40,45 +38,45 @@ The following is an explanation of the installation process defined in the `setu
```go
// setup/setup.go
+package main
+
+import (
+ "os"
+
+ "github.com/goravel/framework/packages"
+ "github.com/goravel/framework/packages/modify"
+ "github.com/goravel/framework/support/file"
+ "github.com/goravel/framework/support/path"
+)
+
func main() {
- // When installing in this way, the configuration file will be published to the project's config directory.
- // You can also manually publish this configuration file: ./artisan vendor:publish --package=github.com/goravel/example-package
- config, err := supportfile.GetPackageContent(packages.GetModulePath(), "setup/config/hello.go")
+ setup := packages.Setup(os.Args)
+
+ // The config file will be published to the project's config directory automatically when installing by this way.
+ // You can also publish this config file manually: ./artisan vendor:publish --package=github.com/goravel/example-package
+ config, err := file.GetPackageContent(setup.Paths().Module().String(), "setup/config/hello.go")
if err != nil {
panic(err)
}
- // Execute installation or uninstallation operations based on user input parameters
- packages.Setup(os.Args).
- // Define installation process
- Install(
- // Find config/app.go file
- modify.GoFile(path.Config("app.go")).
- // Find imports and add import: examplepackage "github.com/goravel/example-package"
- Find(match.Imports()).Modify(modify.AddImport(packages.GetModulePath(), "examplepackage")).
- // Find providers and register service providers: &examplepackage.ServiceProvider{},note that you need to add the import first, then you can register the service provider
- Find(match.Providers()).Modify(modify.Register("&examplepackage.ServiceProvider{}")),
- // Find hello.go file, create or overwrite file
- modify.File(path.Config("hello.go")).Overwrite(config),
- ).
- // Define uninstallation process
- Uninstall(
- // Find config/app.go file
- modify.GoFile(path.Config("app.go")).
- // Find providers and unregister service providers: &examplepackage.ServiceProvider{}
- Find(match.Providers()).Modify(modify.Unregister("&examplepackage.ServiceProvider{}")).
- // Find imports and delete import: examplepackage "github.com/goravel/example-package",note that you need to unregister the service provider first, then you can delete the import
- Find(match.Imports()).Modify(modify.RemoveImport(packages.GetModulePath(), "examplepackage")),
- // Find hello.go file, delete file
- modify.File(path.Config("hello.go")).Remove(),
- ).
- // Execute installation or uninstallation operations
- Execute()
-```
+ serviceProvider := "&hello.ServiceProvider{}"
+ moduleImport := setup.Paths().Module().Import()
+
+ setup.Install(
+ // Add the service provider to the providers slice in bootstrap/providers.go
+ modify.AddProviderApply(moduleImport, serviceProvider),
-### Manual Install
+ // Add the config file to the config directory
+ modify.File(path.Config("hello.go")).Overwrite(config),
+ ).Uninstall(
+ // Remove the config file from the config directory
+ modify.File(path.Config("hello.go")).Remove(),
-Register the `ServiceProvider` in the package to `config/app.go::providers`, then export `facades` to the application. For detailed steps, refer to [goravel/example-package](https://github.com/goravel/example-package).
+ // Remove the service provider from the providers slice in bootstrap/providers.go
+ modify.RemoveProviderApply(moduleImport, serviceProvider),
+ ).Execute()
+}
+```
## Resources
@@ -173,7 +171,7 @@ func (receiver *ServiceProvider) Boot(app foundation.Application) {
In the project, You can publish the resources registered in a package using `vendor:publish` Artisan command:
```shell
-go run . artisan vendor:publish --package={You package name}
+./artisan vendor:publish --package={You package name}
```
The command can use the following options:
diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md
new file mode 100644
index 000000000..c39c6b34b
--- /dev/null
+++ b/en/digging-deeper/pluralization.md
@@ -0,0 +1,120 @@
+# Pluralization
+
+[[toc]]
+
+## Introduction
+
+Strings are important for any web application. Goravel provides simple utilities to convert words between singular
+and plural forms. It supports **English** by default, but you can add other languages or custom rules easily.
+
+## Basic Usage
+
+You can use the `Plural` and `Singular` methods from the `pluralizer` package. These handle most English words automatically.
+
+```go
+import "github.com/goravel/framework/support/pluralizer"
+
+// Pluralize words
+pluralizer.Plural("goose") // "geese"
+pluralizer.Plural("car") // "cars"
+
+// Singularize words
+pluralizer.Singular("geese") // "goose"
+pluralizer.Singular("cars") // "car"
+```
+
+## Custom Rules
+
+Sometimes the default rules are not enough for specific words. Goravel lets you add your own rules to handle these cases.
+
+::: warning
+Adding rules changes how pluralization works globally. You should do this when your application starts,
+like in the `Boot` method of a Service Provider.
+:::
+
+### Irregular Words
+
+If a word has a unique plural form, you can register it as an "irregular" word. This handles changes in both directions.
+
+```go
+import (
+ "github.com/goravel/framework/support/pluralizer"
+ "github.com/goravel/framework/support/pluralizer/rules"
+)
+
+// Register that "mouse" becomes "mice"
+pluralizer.RegisterIrregular("english", rules.NewSubstitution("mouse", "mice"))
+```
+
+### Uninflected Words
+
+Some words like "fish" or "media" do not change form or are always plural. You can mark these as "uninflected"
+so the pluralizer skips them.
+
+```go
+// "sheep" stays "sheep" in singular and plural
+pluralizer.RegisterUninflected("english", "sheep", "fish")
+
+// "media" is always treated as plural
+pluralizer.RegisterPluralUninflected("english", "media")
+
+// "data" is always treated as singular
+pluralizer.RegisterSingularUninflected("english", "data")
+```
+
+## Language Support
+
+Goravel uses "english" by default, but you can switch languages or add new ones if you need to.
+
+### Switching Languages
+
+If you have other languages registered, you can switch the active one using `UseLanguage`.
+
+```go
+if err := pluralizer.UseLanguage("spanish"); err != nil {
+ panic(err)
+}
+
+// Get the current language name
+name := pluralizer.GetLanguage().Name()
+```
+
+### Adding New Languages
+
+To add a language, you need to implement the `Language` interface. This defines how words change in that language.
+
+```go
+import "github.com/goravel/framework/contracts/support/pluralizer"
+
+type Language interface {
+ Name() string
+ SingularRuleset() pluralizer.Ruleset
+ PluralRuleset() pluralizer.Ruleset
+}
+```
+
+After implementing your language struct, register it and set it as active.
+
+```go
+import "github.com/goravel/framework/support/pluralizer"
+
+func init() {
+ // Register the new language
+ if err := pluralizer.RegisterLanguage(&MyCustomLanguage{}); err != nil {
+ panic(err)
+ }
+
+ // Set it as active
+ _ = pluralizer.UseLanguage("my_custom_language")
+}
+```
+
+## Supported Languages
+
+Currently, the pluralizer supports the following languages out of the box:
+
+| Language | Code | Source |
+|:---------|:----------|:-------------------------------------------------------------------------------------------|
+| English | `english` | [View Source](https://github.com/goravel/framework/tree/master/support/pluralizer/english) |
+
+*More languages will be added in future releases. You are welcome to contribute new languages via Pull Request.*
\ No newline at end of file
diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md
new file mode 100644
index 000000000..f4c020503
--- /dev/null
+++ b/en/digging-deeper/processes.md
@@ -0,0 +1,404 @@
+# Processes
+
+[[toc]]
+
+## Introduction
+
+Goravel provides an expressive and elegant API around Go's standard `os/exec` package, allowing you to invoke external commands from your application seamlessly. By default, Go's process handling can be verbose; Goravel's `Process` facade simplifies this common task, offering a fluent interface for executing commands, handling output, and managing asynchronous processes.
+
+## Invoking Processes
+
+### Running Processes
+
+To run a process, you can use the `Run` or `Start` methods. The `Run` method will execute the command and wait for it to finish, while the `Start` method triggers the process asynchronously and returns control immediately.
+
+Here is how you execute a blocking command:
+
+```go
+import (
+ "fmt"
+
+ "goravel/app/facades"
+)
+
+func main() {
+ result, err := facades.Process().Run("echo", "Hello, World!")
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(result.Output())
+}
+```
+
+The `Run` method returns a `Result` interface and an `error`. The error will be non-nil if the process failed to start or encountered a system error. The `Result` interface gives you convenient access to the output and status of the command:
+
+```go
+result, _ := facades.Process().Run("ls", "-la")
+
+result.Command() // string: The original command
+result.Error() // error: The error returned by the command execution
+result.ErrorOutput() // string: Output from Stderr
+result.ExitCode() // int: The exit code (e.g., 0, 1)
+result.Failed() // bool: True if the exit code was not 0
+result.Output() // string: Output from Stdout
+```
+
+### Process Options
+
+You often need to customize how a command runs, such as where it runs or what environment variables it sees. The `Process` facade provides a fluent API for this.
+
+#### Path
+You can use the `Path` method to specify the working directory for the command. If you don't set this, the process will execute in the current working directory of your application.
+
+```go
+result, _ := facades.Process().Path("/var/www/html").Run("ls", "-la")
+```
+
+#### Timeout
+To prevent a process from hanging indefinitely, you can enforce a timeout. If the process runs longer than the specified duration, it will be killed.
+
+```go
+import "time"
+
+result, _ := facades.Process().Timeout(10 * time.Mintue).Run("sleep", "20")
+```
+
+#### Environment Variables
+You can pass custom environment variables to the process using the `Env` method. The process will also inherit the system's environment variables.
+
+```go
+// Passes FOO=BAR along with existing system envs
+result, _ := facades.Process().Env(map[string]string{
+ "FOO": "BAR",
+ "API_KEY": "secret",
+}).Run("printenv")
+```
+
+#### Input (Stdin)
+If your command expects input from standard input (stdin), you can provide it using the `Input` method. This accepts an `io.Reader`.
+
+```go
+import "strings"
+
+// Pipes "Hello Goravel" into the cat command
+result, _ := facades.Process().
+ Input(strings.NewReader("Hello Goravel")).
+ Run("cat")
+```
+
+### Process Output
+
+You can access the process output after execution using the `Output` (standard output) and `ErrorOutput` (standard error) methods on the result object.
+
+```go
+result, _ := facades.Process().Run("ls", "-la");
+
+fmt.Println(result.Output())
+fmt.Println(result.ErrorOutput())
+```
+
+If you need to process the output in real-time (streaming), you may register a callback using the `OnOutput` method. The callback receives two arguments: the output type (stdout or stderr) and the byte slice containing the output data.
+
+```go
+import (
+ "fmt"
+ "github.com/goravel/framework/contracts/process"
+)
+
+result, _ := facades.Process().OnOutput(func(typ process.OutputType, b []byte) {
+ // Handle real-time streaming here
+ fmt.Print(string(b))
+}).Run("ls", "-la")
+```
+
+If you only need to verify that the output contains a specific string after execution, you can use the `SeeInOutput` or `SeeInErrorOutput` helper methods.
+
+```go
+result, _ := facades.Process().Run("ls", "-la")
+
+if result.SeeInOutput("go.mod") {
+ // The file exists
+}
+```
+
+#### Disabling Process Output
+
+If your process writes a large amount of data, you may want to control how it is stored.
+
+Using `Quietly` will prevent the output from bubbling up to the console or logs during execution, but the data will still be collected and available via `result.Output()`.
+
+If you do not need to access the final output at all and want to save memory, you can use `DisableBuffering`. This prevents the output from being stored in the result object, though you can still inspect the stream in real-time using `OnOutput`.
+
+```go
+// Captures output but doesn't print it during execution
+facades.Process().Quietly().Run("...")
+
+// Does not capture output (saves memory), but allows streaming
+facades.Process().DisableBuffering().OnOutput(func(typ process.OutputType, b []byte) {
+ // ...
+}).Run("...")
+```
+
+### Pipelines
+
+Sometimes you need to pipe the output of one process into the input of another. The `Process` facade makes this easy
+using the `Pipe` method, which allows you to chain multiple commands together synchronously.
+
+```go
+import "github.com/goravel/framework/contracts/process"
+
+result, err := facades.Process().Pipe(func(pipe process.Pipe) {
+ pipe.Command("echo", "Hello, World!")
+ pipe.Command("grep", "World")
+ pipe.Command("tr", "a-z", "A-Z")
+}).Run()
+```
+
+::: warning
+Process options such as `Timeout`, `Env`, or `Input` must be configured **after** the `Pipe` method is called.
+Any configuration applied before the `Pipe` call will be ignored.
+
+```go
+// Correct: Configuration applied after Pipe
+facades.Process().Pipe(...).Timeout(10 * time.Second).Run()
+
+// Incorrect: Timeout will be ignored
+facades.Process().Timeout(10 * time.Second).Pipe(...).Run()
+```
+:::
+
+#### Pipeline Output & Keys
+
+You can inspect the output of the pipeline in real-time using the `OnOutput` method. When used with a pipe,
+the callback signature changes to include a `key` (string), allowing you to identify which command produced the output.
+
+By default, the `key` is the numeric index of the command. However, you can assign a readable label to each command
+using the `As` method, which is highly useful for debugging complex pipelines.
+
+```go
+facades.Process().Pipe(func(pipe process.Pipe) {
+ pipe.Command("cat", "access.log").As("source")
+ pipe.Command("grep", "error").As("filter")
+}).OnOutput(func(typ process.OutputType, line []byte, key string) {
+ // 'key' will be "source" or "filter"
+}).Run()
+```
+
+## Asynchronous Processes
+
+While the `Run` method waits for the process to complete, `Start` can be used to invoke a process asynchronously.
+This allows the process to run in the background while your application continues executing other tasks. The `Start` method returns a `Running` interface.
+
+```go
+import "time"
+
+process, _ := facades.Process().Timeout(10 * time.Second).Start("sleep", "5")
+
+// Continue doing other work...
+
+result := process.Wait()
+```
+
+To check if a process has finished without blocking, you may use the `Done` method. This returns a standard Go channel
+that closes when the process exits, making it ideal for use in `select` statements.
+
+```go
+process, _ := facades.Process().Start("sleep", "5")
+
+select {
+case <-process.Done():
+ // Process finished successfully
+case <-time.After(1 * time.Second):
+ // Custom logic if it takes too long
+}
+
+result := process.Wait()
+```
+
+::: warning
+Even if you use the `Done` channel to detect completion, you **must** call `Wait()` afterwards.
+This ensures the process is properly "reaped" by the operating system and cleans up underlying resources.
+:::
+
+### Process IDs & Signals
+
+You can retrieve the operating system's process ID (PID) for a running process using the `PID` method.
+
+```go
+process, _ := facades.Process().Start("ls", "-la")
+
+println(process.PID())
+```
+
+#### Sending Signals
+
+Goravel provides methods to interact with the process lifecycle. You can send a specific OS signal using
+the `Signal` method, or use the `Stop` helper to attempt a graceful shutdown.
+
+The `Stop` method is particularly useful: it will first send a termination signal (defaulting to `SIGTERM`).
+If the process does not exit within the provided timeout, it will be forcibly killed (`SIGKILL`).
+
+```go
+import (
+ "os"
+ "time"
+)
+
+process, _ := facades.Process().Start("sleep", "60")
+
+// Manually send a signal
+process.Signal(os.Interrupt)
+
+// Attempt to stop gracefully, wait 5 seconds, then force kill
+process.Stop(5 * time.Second)
+```
+
+### Checking Process State
+
+You can inspect the current state of the process using the `Running` method. This is primarily useful for debugging
+or health checks, as it provides a snapshot of whether the process is currently active.
+
+```go
+// Snapshot check (useful for logs or metrics)
+if process.Running() {
+ fmt.Println("Process is still active...")
+}
+```
+
+::: tip
+If you need to execute code **when** the process finishes, do not poll `Running()`. Instead, use the `Done()` channel
+or the `Wait()` method, which are much more efficient than repeatedly checking the status.
+:::
+
+## Concurrent Processes
+
+Goravel makes it easy to manage a pool of concurrent processes, allowing you to execute multiple commands simultaneously.
+This is particularly useful for batch processing or running independent tasks in parallel.
+
+### Executing Pools
+
+To run a pool of processes, you may use the `Pool` method. This accepts a closure where you define the commands you wish to execute.
+
+By default, the `Pool` method waits for all processes to complete and returns a map of results keyed by the process name (or index).
+
+```go
+results, err := facades.Process().Pool(func(pool process.Pool) {
+ pool.Command("sleep", "1").As("first")
+ pool.Command("sleep", "2").As("second")
+}).Run()
+
+if err != nil {
+ panic(err)
+}
+
+// Access results by their assigned key
+println(results["first"].Output())
+println(results["second"].Output())
+```
+
+### Naming Processes
+
+By default, processes in a pool are keyed by their numeric index (e.g., "0", "1"). However, for clarity and easier access
+to results, you should assign a unique name to each process using the `As` method:
+
+```go
+pool.Command("cat", "system.log").As("system")
+```
+
+### Pool Options
+
+The `Pool` builder provides several methods to control the execution behavior of the entire batch.
+
+#### Concurrency
+You can control the maximum number of processes running simultaneously using the `Concurrency` method.
+
+```go
+facades.Process().Pool(func(pool process.Pool) {
+ // Define 10 commands...
+}).Concurrency(2).Run()
+```
+
+#### Total Timeout
+You can enforce a global timeout for the entire pool execution using the `Timeout` method. If the pool takes longer
+than this duration, all running processes will be terminated.
+
+```go
+facades.Process().Pool(...).Timeout(1 * time.Minute).Run()
+```
+
+### Asynchronous Pools
+
+If you need to run the pool in the background while your application performs other tasks, you can use the `Start`
+method instead of `Run`. This returns a `RunningPool` handle.
+
+```go
+runningPool, err := facades.Process().Pool(func(pool process.Pool) {
+ pool.Command("sleep", "5").As("long_task")
+}).Start()
+
+// Check if the pool is still running
+if runningPool.Running() {
+ fmt.Println("Pool is active...")
+}
+
+// Wait for all processes to finish and gather results
+results := runningPool.Wait()
+```
+
+#### Interacting with Running Pools
+
+The `RunningPool` interface provides several methods to manage the active pool:
+
+* **`PIDs()`**: Returns a map of Process IDs keyed by the command name.
+* **`Signal(os.Signal)`**: Sends a signal to all running processes in the pool.
+* **`Stop(timeout, signal)`**: Gracefully stops all processes.
+* **`Done()`**: Returns a channel that closes when the pool finishes, useful for `select` statements.
+
+```go
+select {
+case <-runningPool.Done():
+ // All processes finished
+case <-time.After(10 * time.Second):
+ // Force stop all processes if they take too long
+ runningPool.Stop(1 * time.Second)
+}
+```
+
+### Pool Output
+
+You can inspect the output of the pool in real-time using the `OnOutput` method.
+
+::: warning
+The `OnOutput` callback may be invoked concurrently from multiple goroutines. Ensure your callback logic is thread-safe.
+:::
+
+```go
+facades.Process().Pool(func(pool process.Pool) {
+ pool.Command("ping", "google.com").As("ping")
+}).OnOutput(func(typ process.OutputType, line []byte, key string) {
+ // key will be "ping"
+ fmt.Printf("[%s] %s", key, string(line))
+}).Run()
+```
+
+### Per-Process Configuration
+
+Inside the pool definition, each command supports individual configuration methods similar to single processes:
+
+* **`Path(string)`**: Sets the working directory.
+* **`Env(map[string]string)`**: Sets environment variables.
+* **`Input(io.Reader)`**: Sets standard input.
+* **`Timeout(time.Duration)`**: Sets a timeout for the specific command.
+* **`Quietly()`**: Disables output capturing for this specific command.
+* **`DisableBuffering()`**: Disables memory buffering (useful for high-volume output).
+
+```go
+facades.Process().Pool(func(pool process.Pool) {
+ pool.Command("find", "/", "-name", "*.log").
+ As("search").
+ Path("/var/www").
+ Timeout(10 * time.Second).
+ DisableBuffering()
+}).Run()
+```
diff --git a/en/digging-deeper/queues.md b/en/digging-deeper/queues.md
index 24a6d7a4a..9eec382b2 100644
--- a/en/digging-deeper/queues.md
+++ b/en/digging-deeper/queues.md
@@ -65,8 +65,21 @@ After implementing the custom driver, you can add the configuration to `config/q
By default, all of the jobs for your application are stored in the `app/jobs` directory. If the `app/Jobs` directory doesn't exist, it will be created when you run the `make:job` Artisan command:
```shell
-go run . artisan make:job ProcessPodcast
-go run . artisan make:job user/ProcessPodcast
+./artisan make:job ProcessPodcast
+./artisan make:job user/ProcessPodcast
+```
+
+### Register Jobs
+
+A new job created by `make:job` will be registered automatically in the `bootstrap/jobs.go::Jobs()` function and the function will be called by `WithJobs`. You need register the job manually if you create the job file by yourself.
+
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithJobs(Jobs).
+ WithConfig(config.Boot).
+ Start()
+}
```
### Class Structure
@@ -101,76 +114,24 @@ func (r *ProcessPodcast) ShouldRetry(err error, attempt int) (retryable bool, de
}
```
-### Register Job
-
-After creating the job, you need to register it in `app/provides/queue_service_provider.go`, so that it can be called correctly. If the job is generated by the `make:job` command, the framework will automatically register it.
-
-```go
-func (receiver *QueueServiceProvider) Jobs() []queue.Job {
- return []queue.Job{
- &jobs.Test{},
- }
-}
-```
-
## Start Queue Server
-Start the queue server in `main.go` in the root directory.
+The default queue worker will be run by the runner of queue seriver provider, if you want to start multiple queue workers with different configuration, you can create [a runner](../architecture-concepts/service-providers.md#runners) and add it to the `WithRunners` function in the `bootstrap/app.go` file:
```go
-package main
-
-import (
- "github.com/goravel/framework/facades"
-
- "goravel/bootstrap"
-)
-
-func main() {
- // This bootstraps the framework and gets it ready for use.
- bootstrap.Boot()
-
- // Start queue server by facades.Queue().
- go func() {
- if err := facades.Queue().Worker().Run(); err != nil {
- facades.Log().Errorf("Queue run error: %v", err)
- }
- }()
-
- select {}
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithRunners(func() []contractsfoundation.Runner {
+ return []contractsfoundation.Runner{
+ YourRunner,
+ }
+ }).
+ Start()
}
```
-Different parameters can be passed in the `facades.Queue().Worker` method, you can monitor multiple queues by starting multiple `facades.Queue().Worker`.
-
-```go
-// No parameters, default listens to the configuration in the `config/queue.go`, and the number of concurrency is 1
-go func() {
- if err := facades.Queue().Worker().Run(); err != nil {
- facades.Log().Errorf("Queue run error: %v", err)
- }
-}()
-
-// Monitor processing queue for redis link, and the number of concurrency is 10, and the number of retries is 3
-go func() {
- if err := facades.Queue().Worker(queue.Args{
- Connection: "redis",
- Queue: "processing",
- Concurrent: 10,
- Tries: 3,
- }).Run(); err != nil {
- facades.Log().Errorf("Queue run error: %v", err)
- }
-}()
-```
-
-## Stop Queue Server
-
-When the queue server is running, you can stop the queue server by calling the `Shutdown` method, this method will wait for the current running tasks to complete before stopping the queue.
-
-```go
-err := facades.Queue().Worker().Shutdown()
-```
+You can check [the default queue runner](https://github.com/goravel/framework/blob/master/queue/runners.go) for reference.
## Dispatching Jobs
@@ -182,8 +143,8 @@ package controllers
import (
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
"goravel/app/jobs"
)
@@ -208,8 +169,8 @@ package controllers
import (
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
"goravel/app/jobs"
)
diff --git a/en/digging-deeper/strings.md b/en/digging-deeper/strings.md
index 63698ad1b..dc8d7fafd 100644
--- a/en/digging-deeper/strings.md
+++ b/en/digging-deeper/strings.md
@@ -537,6 +537,30 @@ str.Of("Goravel").Pipe(func(s string) string {
}).String() // "Goravel Framework"
```
+### `Plural`
+
+The `Plural` method converts a singular string to its plural form. This function supports any of
+the languages supported by the [pluralizer](pluralization.md).
+
+```go
+import "github.com/goravel/framework/support/str"
+
+plural := str.Of("goose").Plural().String()
+// "geese"
+```
+
+You may provide an integer argument to the function to retrieve the singular or plural form of the string:
+
+```go
+import "github.com/goravel/framework/support/str"
+
+plural := str.Of("goose").Plural(2).String()
+// "geese"
+
+plural = str.Of("goose").Plural(1).String()
+// "goose"
+```
+
### `Prepend`
The `Prepend` method prepends the given value to the string.
@@ -653,6 +677,18 @@ str.Of(" Goravel ").RTrim().String() // " Goravel"
str.Of("/framework/").RTrim("/").String() // "/framework"
```
+### `Singular`
+
+The `Singular` method converts a string to its singular form. This function supports any of
+the languages supported by the [pluralizer](pluralization.md).
+
+```go
+import "github.com/goravel/framework/support/str"
+
+singular := str.Of("heroes").Singular().String()
+// "hero"
+```
+
### `Snake`
The `Snake` method converts the string to `snake_case`.
diff --git a/en/digging-deeper/task-scheduling.md b/en/digging-deeper/task-scheduling.md
index 5ac10fae0..45d7e3e2c 100644
--- a/en/digging-deeper/task-scheduling.md
+++ b/en/digging-deeper/task-scheduling.md
@@ -10,28 +10,20 @@ Goravel's command scheduler offers a fresh approach to managing scheduled tasks
## Defining Schedules
-To schedule tasks for your application, you can define them in the `Schedule` method in `app\console\kernel.go`. Let's consider an example to understand this better. In this case, we want to schedule a closure that will run every day at midnight. Inside this closure, we will execute a database query to clear a table:
+To schedule tasks for your application, you can define them in the `WithSchedule` function in the `bootstrap/app.go` file. Let's consider an example to understand this better. In this case, we want to schedule a closure that will run every day at midnight. Inside this closure, we will execute a database query to clear a table:
```go
-package console
-
-import (
- "github.com/goravel/framework/contracts/console"
- "github.com/goravel/framework/contracts/schedule"
- "github.com/goravel/framework/facades"
-
- "goravel/app/models"
-)
-
-type Kernel struct {
-}
-
-func (kernel Kernel) Schedule() []schedule.Event {
- return []schedule.Event{
- facades.Schedule().Call(func() {
- facades.Orm().Query().Where("1 = 1").Delete(&models.User{})
- }).Daily(),
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithSchedule(func() []schedule.Event {
+ return []schedule.Event{
+ facades.Schedule().Call(func() {
+ facades.Orm().Query().Where("1 = 1").Delete(&models.User{})
+ }).Daily(),
+ }
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
@@ -40,22 +32,7 @@ func (kernel Kernel) Schedule() []schedule.Event {
In addition to scheduling closures, you can also schedule [Artisan commands](./artisan-console.md). For example, you may use the `Command` method to schedule an Artisan command using either the command's name or class.
```go
-package console
-
-import (
- "github.com/goravel/framework/contracts/console"
- "github.com/goravel/framework/contracts/schedule"
- "github.com/goravel/framework/facades"
-)
-
-type Kernel struct {
-}
-
-func (kernel *Kernel) Schedule() []schedule.Event {
- return []schedule.Event{
- facades.Schedule().Command("send:emails name").Daily(),
- }
-}
+facades.Schedule().Command("send:emails name").Daily(),
```
### Logging Level
@@ -144,64 +121,12 @@ facades.Schedule().Call(func() {
## Running The Scheduler
-Now that we have learned how to define scheduled tasks, let's discuss how to actually run them on our server.
-
-Add `go facades.Schedule().Run()` to the root `main.go` file.
-
-```go
-package main
-
-import (
- "github.com/goravel/framework/facades"
-
- "goravel/bootstrap"
-)
-
-func main() {
- // This bootstraps the framework and gets it ready for use.
- bootstrap.Boot()
-
- // Start schedule by facades.Schedule
- go facades.Schedule().Run()
-
- select {}
-}
-```
-
-You can also use the `schedule:run` command to manually run tasks:
+The scheduler will be run automatically when calling `Start()` in the `bootstrap/app.go` file. You can also run tasks manually :
```shell
./artisan schedule:run
```
-## Stopping The Scheduler
-
-You can call the `Shutdown` method to gracefully shut down the scheduler. This method will wait for all tasks to complete before shutting down.
-
-```go
-// main.go
-bootstrap.Boot()
-
-// Create a channel to listen for OS signals
-quit := make(chan os.Signal)
-signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
-
-// Start schedule by facades.Schedule
-go facades.Schedule().Run()
-
-// Listen for the OS signal
-go func() {
- <-quit
- if err := facades.Schedule().Shutdown(); err != nil {
- facades.Log().Errorf("Schedule Shutdown error: %v", err)
- }
-
- os.Exit(0)
-}()
-
-select {}
-```
-
## View All Tasks
You can use the `schedule:list` command to view all tasks:
diff --git a/en/getting-started/compile.md b/en/getting-started/compile.md
index 54306a8df..046268496 100644
--- a/en/getting-started/compile.md
+++ b/en/getting-started/compile.md
@@ -6,21 +6,21 @@
The Goravel project can be compiled with the following command:
-```
-// Select the system to compile
-go run . artisan build
+```shell
+# Select the system to compile
+./artisan build
-// Specify the system to compile
-go run . artisan build --os=linux
-go run . artisan build -o=linux
+# Specify the system to compile
+./artisan build --os=linux
+./artisan build -o=linux
-// Static compilation
-go run . artisan build --static
-go run . artisan build -s
+# Static compilation
+./artisan build --static
+./artisan build -s
-// Specify the output file name
-go run . artisan build --name=goravel
-go run . artisan build -n=goravel
+# Specify the output file name
+./artisan build --name=goravel
+./artisan build -n=goravel
```
## Manual compilation
@@ -36,11 +36,10 @@ go build .
The Following files and folders need to be uploaded to the server during deployment:
```
-./main // Compile the resulting binary file
.env
-./public
-./storage
-./resources
+./main // Compile the resulting binary file
+./public // if exists
+./resources // if exists
```
### Static compilation
@@ -94,12 +93,15 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/re
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
WORKDIR /www
+COPY --from=builder /build/.env /www/.env
COPY --from=builder /build/main /www/
+
+# If exists
COPY --from=builder /build/database/ /www/database/
COPY --from=builder /build/public/ /www/public/
COPY --from=builder /build/storage/ /www/storage/
COPY --from=builder /build/resources/ /www/resources/
-COPY --from=builder /build/.env /www/.env
+
ENTRYPOINT ["/www/main"]
```
@@ -137,7 +139,3 @@ import (
_ "time/tzdata"
)
```
-
-## Reduce package size
-
-Commenting out the unused `ServiceProvider` in `config/app.go::providers` will effectively reduce the packaging volume.
diff --git a/en/getting-started/configuration.md b/en/getting-started/configuration.md
index a423598c9..bdfecdb5e 100644
--- a/en/getting-started/configuration.md
+++ b/en/getting-started/configuration.md
@@ -16,6 +16,19 @@ Note that the `.env` file should not be added to version control, because when m
In addition, if an intruder gains access to your code repository, there will be a risk of exposing sensitive configuration. If you want to add a new configuration item, you can add it to the `.env.example` file to synchronize the configuration of all developers.
+
+### Register Configuration
+
+All configuration files will be registered via the `WithConfig` function in the `bootstrap/app.go` file. Given that it's a `init` function in the config file, you don't need to register each configuration file one by one. Just call the `WithConfig` function as follows:
+
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ Start()
+}
+```
+
## Retrieve Environment Configuration
Use the following method to obtain the configuration items in the `.env` file:
@@ -56,5 +69,5 @@ facades.Config().Add("path.with.dot", map[string]any{"case3": "value3"})
You can use the `artisan about` command to view the framework version, configuration, etc.
```bash
-go run . artisan about
+./artisan about
```
diff --git a/en/getting-started/directory-structure.md b/en/getting-started/directory-structure.md
index 05d3fd9d5..24373e54c 100644
--- a/en/getting-started/directory-structure.md
+++ b/en/getting-started/directory-structure.md
@@ -6,62 +6,50 @@
The default file structure can make you better start project advancement, and you can also add new folders freely, but do not modify the default folders.
-## Root Directory
-
-### `app` Directory
-
-`app` contains the core code of the program. Almost all the logic in the program will be in this folder.
-
-### `bootstrap` Directory
-
-The `bootstrap` directory contains the framework startup file `app.go`.
-
-### `config` Directory
-
-The `config` directory contains all configuration files of the application. It is best to browse through these files and familiarize yourself with all the available options.
-
-### `database` Directory
-
-The `database` directory contains database migration files.
-
-### `public` Directory
-
-The `public` directory contains some static resources, such as images, certificates, etc.
-
-### `resources` Directory
-
-The `resources` directory contains your [views](../the-basics/views.md) as well as your raw, un-compiled assets such as CSS or JavaScript.
-
-### `routes` Directory
-
-The `routes` directory contains all the route definitions of the application.
-
-### `storage` Directory
-
-The `storage` directory contains the `logs` directory, and the `logs` directory contains the application log files.
-
-### `tests` Directory
-
-The `tests` directory contains your automated tests.
-
-## `app` Directory
-
-### `console` Directory
-
-The `console` directory contains all the custom `Artisan` commands of the application, and the console boot file `kernel.go`, which can be registered in this file [Task Scheduling](../digging-deeper/task-scheduling.md)
-
-### `http` Directory
-
-The `http` directory contains controllers, middleware, etc., and almost all requests that enter the application via the Web are processed here.
-
-### `grpc` Directory
-
-The `grpc` directory contains controllers, middleware, etc., and almost all requests that enter the application via the Grpc are processed here.
-
-### `models` Directory
-
-The `models` directory contains all data models.
-
-### `providers` Directory
-
-The `providers` directory contains all [Service Providers](../architecture-concepts/service-providers.md) in the program. The service provider guides the application to respond to incoming requests by binding services, registering for events, or performing any other tasks.
+## Folder Tree
+
+```
+goravel/
+├── app/ # Core application logic
+│ ├── console/ # Artisan console commands
+│ ├── grpc/ # gRPC controllers and middleware
+│ ├── http/ # HTTP controllers and middleware
+│ │ ├── controllers/ # HTTP request handlers
+│ │ ├── middleware/ # HTTP middleware (auth, cors, etc.)
+│ ├── models/ # Database models and ORM entities
+│ └── providers/ # Service providers
+├── bootstrap/ # Application bootstrapping
+│ └── app.go # Framework initialization and startup
+├── config/ # Application configuration files
+├── database/ # Database related files
+│ ├── migrations/ # Database migration files
+│ ├── seeders/ # Database seeders
+├── resources/ # Raw, uncompiled assets
+│ └── views/ # View templates
+├── routes/ # Application route definitions
+├── storage/ # Application storage
+├── tests/ # Automated tests
+├── .air.toml # Air hot reload configuration
+├── .env.example # Environment variables template
+├── artisan # Artisan console entry script
+├── go.mod # Go module dependencies
+├── go.sum # Go module checksums
+├── main.go # Application entry point
+```
+
+## 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:
+
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithPaths(func(paths configuration.Paths) {
+ paths.App("src")
+ }).
+ WithConfig(config.Boot).
+ Start()
+}
+```
+
+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.
diff --git a/en/getting-started/installation.md b/en/getting-started/installation.md
index fc35b9235..d97aee581 100644
--- a/en/getting-started/installation.md
+++ b/en/getting-started/installation.md
@@ -22,6 +22,10 @@ goravel new blog
### Manual Installation
+#### goravel/goravel
+
+The complete framework with full features.
+
```shell
// Download framework
git clone --depth=1 https://github.com/goravel/goravel.git && rm -rf goravel/.git*
@@ -33,26 +37,47 @@ cd goravel && go mod tidy
cp .env.example .env
// Generate application key
-go run . artisan key:generate
+./artisan key:generate
+```
+
+#### goravel/goravel-lite
+
+The lite framework with only essential features, suitable for building microservices or small applications. You can install additional facades as needed.
+
+```shell
+// Download framework
+git clone --depth=1 https://github.com/goravel/goravel-lite.git && rm -rf goravel-lite/.git*
+s
+// Install dependencies
+cd goravel-lite && go mod tidy
+
+// Create .env environment configuration file
+cp .env.example .env
+
+// Generate application key
+./artisan key:generate
+
+// Install additional facades as needed, for example:
+./artisan package:install Cache
```
-Please confirm your network if you encounter slow download dependencies.
+> Please confirm your network if you encounter slow download dependencies.
-## Start HTTP Service
+## Start Services
-### Start Service According To .env File In The Root Directory
+### Start Services According To .env File In The Root Directory
```shell
go run .
```
-### Specify .env File To Start Service
+### Specify .env File To Start Services
```shell
go run . --env=./.env
```
-### Start Service Using Environment Variables
+### Start Services Using Environment Variables
```shell
APP_ENV=production APP_DEBUG=true go run .
@@ -147,7 +172,7 @@ All configuration files of the Goravel framework are placed in the `config` dire
You need to generate the application key after Goravel is installed locally. Running the command below, a 32-bit string will be generated on the `APP_KEY` key in the `.env` file. This key is mainly used for data encryption and decryption.
```shell
-go run . artisan key:generate
+./artisan key:generate
```
### Generate JWT Token
@@ -155,7 +180,7 @@ go run . artisan key:generate
You need to generate JWT Token if you use [Authentication](../security/authentication.md).
```shell
-go run . artisan jwt:secret
+./artisan jwt:secret
```
### Encrypt and decrypt env file
@@ -163,17 +188,17 @@ go run . artisan jwt:secret
You may want to add the production environment env file to version control, but you don't want to expose sensitive information, you can use the `env:encrypt` command to encrypt the env file:
```shell
-go run . artisan env:encrypt
+./artisan env:encrypt
// Specify the file name and key
-go run . artisan env:encrypt --name .env.safe --key BgcELROHL8sAV568T7Fiki7krjLHOkUc
+./artisan env:encrypt --name .env.safe --key BgcELROHL8sAV568T7Fiki7krjLHOkUc
```
Then use the `env:decrypt` command to decrypt the env file in the production environment:
```shell
-GORAVEL_ENV_ENCRYPTION_KEY=BgcELROHL8sAV568T7Fiki7krjLHOkUc go run . artisan env:decrypt
+GORAVEL_ENV_ENCRYPTION_KEY=BgcELROHL8sAV568T7Fiki7krjLHOkUc ./artisan env:decrypt
// or
-go run . artisan env:decrypt --name .env.safe --key BgcELROHL8sAV568T7Fiki7krjLHOkUc
+./artisan env:decrypt --name .env.safe --key BgcELROHL8sAV568T7Fiki7krjLHOkUc
```
diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md
index 628b9466f..ec4802cea 100644
--- a/en/getting-started/packages.md
+++ b/en/getting-started/packages.md
@@ -1,7 +1,8 @@
# Excellent Extend Packages
You can find extended packages for Goravel here, and you can also create a PR for [goravel/docs](https://github.com/goravel/docs) to commit your owner package, please improve the test coverage of your package as much as possible.
-| Package | Description | Test Coverage |
+
+| Package | Description | Test Coverage* |
| --------------------------------------------------------------------------------- | --------------------------------------------- | ------------- |
| [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% |
@@ -11,13 +12,24 @@ You can find extended packages for Goravel here, and you can also create a PR fo
| [goravel/s3](https://github.com/goravel/s3) | A S3 disk driver for `facades.Storage()` | 77.8% |
| [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/cloudinary](https://github.com/goravel/cloudinary) | A Cloudinary disk driver for `facades.Storage() | 75.4% |
| [goravel/postgres](https://github.com/goravel/postgres) | A Postgres database driver | 73.7% |
| [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% |
+| [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% |
| [hulutech-web/goravel-crud](https://github.com/hulutech-web/goravel-crud) | A goravel crud package | 4.2% |
| [hulutech-web/tinker](https://github.com/hulutech-web/tinker) | A goravel tinker package | 3.6% |
| [hulutech-web/goravel-socket](https://github.com/hulutech-web/goravel-socket) | A webSocket package | 0% |
+
+***Note**: The packages have been ordered based on their test rate.
+
+💡 Tip: To help more developers discover your work, you can also add relevant topics to your repository.
+Recommended topic: [`goravel-package`](https://github.com/topics/goravel-package)
+
+- On GitHub, navigate to the main page of the repository.
+- In the top right corner of the page, to the right of "About", click gear icon (settings).
+- Under "Topics", start to type the topic you want to add to your repository to display a dropdown menu of any matching topics.
+- Click the topic you want to add or continue typing to create a new topic. For example: `goravel-package`, `goravel` and any other relevant keywords.
+- Click `Save changes` — this will make your package more discoverable.
diff --git a/en/index.md b/en/index.md
index 2b57bb024..a04908d40 100644
--- a/en/index.md
+++ b/en/index.md
@@ -49,7 +49,7 @@ head:
hero:
name: Goravel
text: Full-featured Golang Development Framework
- tagline: High-performance, full-featured, easy-to-extend, PHPers' first choice.
+ tagline: Componentization, High-performance, easy-to-extend, PHPers' first choice.
# image: /logo.svg
actions:
- theme: brand
@@ -60,14 +60,14 @@ hero:
link: https://github.com/goravel/goravel
target: _blank
features:
+ - title: 💻 Componentization
+ details: Modular design, each module is independent and can be used separately, making development and maintenance more convenient.
- title: 🚀 High-performance
details: Built with Golang, integrating multiple excellent extensions to create fast and responsive applications.
- title: 🧰 Full-featured
details: Includes ORM, HTTP, queue, task scheduling, logging, caching, and other basic tools for all web development.
- title: 📈 Easy-to-extend
details: The module provides multiple drivers, and can develop extension packages according to needs, suitable for applications of any scale, from small projects to large systems.
- - title: 💻 Easy-to-deploy
- details: Provides multiple packaging commands, supports Docker image deployment, no additional dependencies.
- title: 👥 Active community
details: Supported by an active community, they contribute to its development, documentation, and continuous support.
- title: 🔄 PHPers' first choice
diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md
index 31284217c..1d13ca122 100644
--- a/en/orm/getting-started.md
+++ b/en/orm/getting-started.md
@@ -22,8 +22,8 @@ For example, the model name is `UserOrder`, and the table name is `user_orders`.
Use the `make:model` command to create a model:
```shell
-go run . artisan make:model User
-go run . artisan make:model user/User
+./artisan make:model User
+./artisan make:model user/User
```
Created model file is located in `app/models/user.go` file, the content is as follows:
@@ -103,27 +103,34 @@ func (r *UserData) Scan(value any) (err error) {
./artisan make:model --table=users -f User
```
-If the data table has a field type that the framework cannot recognize, you can call the `facades.Schema().Extend` method to extend the field type in the `Boot` method of the `app/providers/database_service_provider.go` file:
+If the data table has a field type that the framework cannot recognize, you can call the `facades.Schema().Extend` method to extend the field type in the `bootstrap/app.go::WithCallback` function:
```go
import "github.com/goravel/framework/contracts/schema"
-facades.Schema().Extend(&schema.Extension{
- GoTypes: []schema.GoType{
- {
- Pattern: "uuid",
- Type: "uuid.UUID",
- NullType: "uuid.NullUUID",
- Imports: []string{"github.com/google/uuid"},
- },
- {
- Pattern: "point",
- Type: "geom.Point",
- NullType: "*geom.Point",
- Imports: []string{"github.com/twpayne/go-geom"},
- },
- },
-})
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.Schema().Extend(&schema.Extension{
+ GoTypes: []schema.GoType{
+ {
+ Pattern: "uuid",
+ Type: "uuid.UUID",
+ NullType: "uuid.NullUUID",
+ Imports: []string{"github.com/google/uuid"},
+ },
+ {
+ Pattern: "point",
+ Type: "geom.Point",
+ NullType: "*geom.Point",
+ Imports: []string{"github.com/twpayne/go-geom"},
+ },
+ },
+ })
+ }).
+ Start()
+}
```
### Specify Table Name
@@ -172,7 +179,7 @@ func (r *User) Connection() string {
### Setting Global Scope
-Model supports setting the `GlobalScope` method, which restricts the scope of the query, update, and delete operations:
+Model supports setting the `GlobalScopes` method, which restricts the scope of the query, update, and delete operations:
```go
import "github.com/goravel/framework/contracts/orm"
@@ -182,15 +189,25 @@ type User struct {
Name string
}
-func (r *User) GlobalScopes() []func(orm.Query) orm.Query {
- return []func(orm.Query) orm.Query{
- func(query orm.Query) orm.Query {
+func (r *User) GlobalScopes() map[string]func(orm.Query) orm.Query {
+ return map[string]func(orm.Query) orm.Query{
+ "name": func(query orm.Query) orm.Query {
return query.Where("name", "goravel")
},
}
}
```
+If you want to remove global scopes in a query, you can use the `WithoutGlobalScopes` function:
+
+```go
+// Remove all global scopes
+facades.Orm().Query().WithoutGlobalScopes().Get(&users)
+
+// Remove specified global scope
+facades.Orm().Query().WithoutGlobalScopes("name").Get(&users)
+```
+
## facades.Orm() available functions
| Name | Action |
@@ -205,6 +222,7 @@ func (r *User) GlobalScopes() []func(orm.Query) orm.Query {
| Functions | Action |
| --------------------------- | ----------------------------------------------------------------------------- |
+| Avg | [Avg](#Avarage) |
| BeginTransaction | [Begin transaction](#transaction) |
| Commit | [Commit transaction](#transaction) |
| Count | [Count](#count) |
@@ -229,6 +247,8 @@ func (r *User) GlobalScopes() []func(orm.Query) orm.Query {
| Join | [Join](#join) |
| Limit | [Limit](#limit) |
| LockForUpdate | [Pessimistic Locking](#pessimistic-locking) |
+| Max | [Max](#Avarage) |
+| Min | [Min](#Avarage) |
| Model | [Specify a model](#specify-table-query) |
| Offset | [Offset](#offset) |
| Order | [Order](#order) |
@@ -255,14 +275,17 @@ func (r *User) GlobalScopes() []func(orm.Query) orm.Query {
| Scopes | [Scopes](#scopes) |
| Select | [Specify Fields](#specify-fields) |
| SharedLock | [Pessimistic Locking](#pessimistic-locking) |
-| Sum | [Sum](#sum) |
+| Sum | [Sum](#Avarage) |
| Table | [Specify a table](#specify-table-query) |
| ToSql | [Get SQL](#get-sql) |
| ToRawSql | [Get SQL](#get-sql) |
| Update | [Update a single column](#update-a-single-column) |
| UpdateOrCreate | [Update or create](#update-or-create) |
| Where | [Where](#where) |
+| WhereAll | [WhereAll](#where) |
+| WhereAny | [WhereAny](#where) |
| WhereBetween | [WhereBetween](#where) |
+| WhereNone | [WhereNone](#where) |
| WhereNotBetween | [WhereNotBetween](#where) |
| WhereNotIn | [WhereNotIn](#where) |
| WhereNull | [WhereNull](#where) |
@@ -439,6 +462,18 @@ facades.Orm().Query().OrWhere("name", "tom")
facades.Orm().Query().OrWhereNotIn("name", []any{"a"})
facades.Orm().Query().OrWhereNull("name")
facades.Orm().Query().OrWhereIn("name", []any{"a"})
+
+var products []Product
+facades.DB().Table("products").WhereAll([]string{"weight", "height"}, "=", 200).Find(&products)
+// SQL: SELECT * FROM products WHERE weight = ? AND height = ?
+
+var users []User
+facades.DB().Table("users").WhereAny([]string{"name", "email"}, "=", "John").Find(&users)
+// SQL: SELECT * FROM users WHERE (name = ? OR email = ?)
+
+var products []Product
+facades.DB().Table("products").WhereNone([]string{"age", "score"}, ">", 18).Find(&products)
+// SQL: SELECT * FROM products WHERE NOT (age > ?) AND NOT (score > ?)
```
Query JSON columns
@@ -831,8 +866,8 @@ You can execute a transaction by `Transaction` function.
```go
import (
"github.com/goravel/framework/contracts/database/orm"
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
"goravel/app/models"
)
@@ -905,10 +940,20 @@ var users []models.User
facades.Orm().Query().Where("votes > ?", 100).LockForUpdate().Get(&users)
```
-### Sum
+### Avarage
```go
-sum, err := facades.Orm().Query().Model(models.User{}).Sum("id")
+var sum int
+err := facades.Orm().Query().Model(models.User{}).Sum("id", &sum)
+
+var avg float64
+err := facades.Orm().Query().Model(models.User{}).Average("age", &avg)
+
+var max int
+err := facades.Orm().Query().Model(models.User{}).Max("age", &max)
+
+var min int
+err := facades.Orm().Query().Model(models.User{}).Min("age", &min)
```
## Events
@@ -986,8 +1031,8 @@ func (u *User) DispatchesEvents() map[contractsorm.EventType]func(contractsorm.E
If you are listening to many events on a given model, you may use observers to group all of your listeners into a single class. Observer classes have method names that reflect the Eloquent events you wish to listen for. Each of these methods receives the affected model as their only argument. The `make:observer` Artisan command is the easiest way to create a new observer class:
```shell
-go run . artisan make:observer UserObserver
-go run . artisan make:observer user/UserObserver
+./artisan make:observer UserObserver
+./artisan make:observer user/UserObserver
```
This command will place the new observer in your `app/observers` directory. If this directory does not exist, Artisan will create it for you. Your fresh observer will look like the following:
@@ -1022,31 +1067,16 @@ func (u *UserObserver) ForceDeleted(event orm.Event) error {
The template observer only contains some events, you can add other events according to your needs.
-To register an observer, you need to call the `Observe` method on the model you wish to observe. You may register observers in the `Boot` method of your application's `app/providers/event_service_provider.go::Boot` service provider:
+To register an observer, you need to call the `Observe` method on the model you wish to observe. You can register observers in the `bootstrap/app.go::WithCallback` function:
```go
-package providers
-
-import (
- "github.com/goravel/framework/facades"
-
- "goravel/app/models"
- "goravel/app/observers"
-)
-
-type EventServiceProvider struct {
-}
-
-func (receiver *EventServiceProvider) Register(app foundation.Application) {
- facades.Event().Register(receiver.listen())
-}
-
-func (receiver *EventServiceProvider) Boot(app foundation.Application) {
- facades.Orm().Observe(models.User{}, &observers.UserObserver{})
-}
-
-func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener {
- return map[event.Event][]event.Listener{}
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.Orm().Observe(models.User{}, &observers.UserObserver{})
+ }).
+ Start()
}
```
diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md
new file mode 100644
index 000000000..d50752d85
--- /dev/null
+++ b/en/prologue/compare-with-laravel.md
@@ -0,0 +1,40 @@
+# Compare With Laravel
+
+Goravel is heavily inspired by the Laravel framework, aiming to bring similar elegance and simplicity to Go developers. Here are some key comparisons between Goravel and Laravel to help you understand how Goravel aligns with Laravel's features:
+
+| Feature | Goravel | Laravel | Code Example |
+|------------------------|----------------------------------|----------------------------------|----------------------------------|
+| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | ✅ | ✅ | ./artisan key:generate
php artisan key:generate |
+| [Authentication](https://www.goravel.dev/security/authentication.html) | ✅ | ✅ | facades.Auth(ctx).Login(&user)
Auth::login($user) |
+| [Authorization](https://www.goravel.dev/security/authorization.html) | ✅ | ✅ | facades.Gate().Allows("update", user)
Gate::allows('update', $user) |
+| [Cache](https://www.goravel.dev/digging-deeper/cache.html) | ✅ | ✅ | facades.Cache().Put("key", "value", time.Minute)
Cache::put('key', 'value', 60) |
+| [Carbon](https://www.goravel.dev/digging-deeper/helpers.html) | ✅ | ✅ | carbon.Now().AddDays(1)
Carbon::now()->addDays(1) |
+| [Config](https://www.goravel.dev/getting-started/configuration.html) | ✅ | ✅ | facades.Config().GetString("app.name")
config('app.name') |
+| [Crypt](https://www.goravel.dev/security/encryption.html) | ✅ | ✅ | facades.Crypt().EncryptString("text")
Crypt::encryptString('text') |
+| [DB](https://www.goravel.dev/database/getting-started.html) | ✅ | ✅ | facades.DB().Table("users").Get(&users)
DB::table('users')->get() |
+| [Event](https://www.goravel.dev/digging-deeper/event.html) | ✅ | ✅ | facades.Event().Job(&events.Order{}).Dispatch()
Order::dispatch() |
+| [Factory](https://www.goravel.dev/orm/factories.html) | ✅ | ✅ | facades.Orm().Factory().Make(&user)
User::factory()->make() |
+| [FileStorage](https://www.goravel.dev/digging-deeper/filesystem.html) | ✅ | ✅ | facades.Storage().Put("file.txt", "content")
Storage::put('file.txt', 'content') |
+| [Hash](https://www.goravel.dev/security/hashing.html) | ✅ | ✅ | facades.Hash().Make("password")
Hash::make('password') |
+| [Http](https://www.goravel.dev/the-basics/routing.html) | ✅ | ✅ | facades.Route().Get("/", controller.Index)
Route::get('/', [Controller::class, 'index']) |
+| [Http Client](https://www.goravel.dev/digging-deeper/http-client.html) | ✅ | ✅ | facades.Http().Get("https://api.com")
Http::get('https://api.com') |
+| [Localization](https://www.goravel.dev/digging-deeper/localization.html) | ✅ | ✅ | facades.Lang(ctx).Get("messages.welcome")
__('messages.welcome') |
+| [Logger](https://www.goravel.dev/the-basics/logging.html) | ✅ | ✅ | facades.Log().Info("message")
Log::info('message') |
+| [Mail](https://www.goravel.dev/digging-deeper/mail.html) | ✅ | ✅ | facades.Mail().To("user@example.com").Send()
Mail::to('user@example.com')->send(new Order()) |
+| [Mock](https://www.goravel.dev/testing/mock.html) | ✅ | ✅ | |
+| [Migrate](https://www.goravel.dev/database/migrations.html) | ✅ | ✅ | ./artisan migrate
php artisan migrate |
+| [Orm](https://www.goravel.dev/orm/getting-started.html) | ✅ | ✅ | facades.Orm().Query().Find(&user, 1)
User::find(1) |
+| [Package Development](https://www.goravel.dev/digging-deeper/package-development.html) | ✅ | ✅ | |
+| [Process](https://www.goravel.dev/digging-deeper/process.html) | ✅ | ✅ | facades.Process().Run("ls", "-la")
`Process::run('ls -la') |
+| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | ✅ | ✅ | facades.Queue().Job(&jobs.Process{}).Dispatch()
Process::dispatch() |
+| [Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | ✅ | ✅ | facades.RateLimiter().For("global", ...)
RateLimiter::for('global', ...) |
+| [Seeder](https://www.goravel.dev/database/seeding.html) | ✅ | ✅ | facades.Seeder().Call([]seeder.Seeder{&User{}})
$this->call([User::class]) |
+| [Session](https://www.goravel.dev/the-basics/session.html) | ✅ | ✅ | ctx.Request().Session().Put("key", "value")
session(['key' => 'value']) |
+| [Task Scheduling](https://www.goravel.dev/digging-deeper/task-scheduling.html) | ✅ | ✅ | facades.Schedule().Command("emails:send").Daily()
Schedule::command('emails:send')->daily() |
+| [Testing](https://www.goravel.dev/testing/getting-started.html) | ✅ | ✅ | |
+| [Validation](https://www.goravel.dev/the-basics/validation.html) | ✅ | ✅ | ctx.Request().ValidateRequest()
$request->validate() |
+| [View](https://www.goravel.dev/the-basics/views.html) | ✅ | ✅ | ctx.Response().View().Make("welcome.tmpl")
view('welcome') |
+| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | ✅ | 🚧 | |
+| Notifications | 🚧 | ✅ | |
+| Broadcasting | 🚧 | ✅ | |
+| Livewire | 🚧 | ✅ | |
diff --git a/en/getting-started/contributions.md b/en/prologue/contributions.md
similarity index 83%
rename from en/getting-started/contributions.md
rename to en/prologue/contributions.md
index 1dfe14046..0c027d760 100644
--- a/en/getting-started/contributions.md
+++ b/en/prologue/contributions.md
@@ -98,7 +98,7 @@ You can find or create an issue in [Issue List](https://github.com/goravel/gorav
- You can check out [this article](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) if you are new to the process;
- During the development process, if you encounter a problem, you can describe the problem in detail in issue at any time for future communication, but before that, please make sure that you have tried to solve the problem through Google and other methods as much as possible;
- Before creating a PR, please improve the unit test coverage as much as possible to provide more stable functions;
-- If you modify any file under the `contracts` folder, please run the `go run github.com/vektra/mockery/v2` command in the root directory to generate the mock file;
+- If you modify any file under the `contracts` folder, please run the `go tool mockery` command in the root directory to generate the mock file;
- When the PR is developed, please add the `Review Ready `, the maintainer will review it in a timely manner.
- After the PR is merged, the issue will be closed automatically if the description in the PR is set correctly;
- Goravel greatly appreciates your contribution and will add you to the home contribution list at the next release; ❤️
@@ -111,29 +111,30 @@ You can find or create an issue in [Issue List](https://github.com/goravel/gorav
## Goravel Repository
-| Repository | Action |
-| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [goravel/goravel](https://github.com/goravel/goravel) | Goravel artisans |
-| [goravel/framework](https://github.com/goravel/framework) | Goravel main repository |
-| [goravel/example](https://github.com/goravel/example) | Goravel example |
-| [goravel/example-client](https://github.com/goravel/example-client) | Example for Grpc client |
-| [goravel/example-proto](https://github.com/goravel/example-proto) | The proto dependency of example |
-| [goravel/example-package](https://github.com/goravel/example-package) | Example for package |
-| [goravel/docs](https://github.com/goravel/docs) | Document |
-| [goravel/docs-web](https://github.com/goravel/docs-web) | Goravel Website |
-| [goravel/s3](https://github.com/goravel/s3) | The S3 driver of Storage module |
-| [goravel/oss](https://github.com/goravel/oss) | The OSS driver of Storage module |
-| [goravel/cos](https://github.com/goravel/cos) | The COS driver of Storage module |
-| [goravel/minio](https://github.com/goravel/minio) | The Minio driver of Storage module |
-| [goravel/cloudinary](https://github.com/goravel/cloudinary) | The Cloudinary driver of Storage module |
-| [goravel/redis](https://github.com/goravel/redis) | The Redis driver of Cache module |
-| [goravel/gin](https://github.com/goravel/gin) | The Gin driver of Route module |
-| [goravel/fiber](https://github.com/goravel/fiber) | The Fiber driver of Route module |
-| [goravel/postgres](https://github.com/goravel/postgres) | The Postgres driver of Database module |
-| [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 |
+| Repository | Action |
+| --------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [goravel/goravel](https://github.com/goravel/goravel) | Goravel artisans |
+| [goravel/goravel-lite](https://github.com/goravel/goravel-lite) | Goravel artisans of lite |
+| [goravel/framework](https://github.com/goravel/framework) | Goravel main repository |
+| [goravel/example](https://github.com/goravel/example) | Goravel example |
+| [goravel/example-client](https://github.com/goravel/example-client) | Example for Grpc client |
+| [goravel/example-proto](https://github.com/goravel/example-proto) | The proto dependency of example |
+| [goravel/example-package](https://github.com/goravel/example-package) | Example for package |
+| [goravel/installer](https://github.com/goravel/installer) | A command-line tool that helps you to install the Goravel framework |
+| [goravel/release](https://github.com/goravel/release) | Prover a simple way to release framework and packages version |
+| [goravel/docs](https://github.com/goravel/docs) | Document |
+| [goravel/s3](https://github.com/goravel/s3) | The S3 driver of Storage module |
+| [goravel/oss](https://github.com/goravel/oss) | The OSS driver of Storage module |
+| [goravel/cos](https://github.com/goravel/cos) | The COS driver of Storage module |
+| [goravel/minio](https://github.com/goravel/minio) | The Minio driver of Storage module |
+| [goravel/redis](https://github.com/goravel/redis) | The Redis driver of Cache module |
+| [goravel/gin](https://github.com/goravel/gin) | The Gin driver of Route module |
+| [goravel/fiber](https://github.com/goravel/fiber) | The Fiber driver of Route module |
+| [goravel/postgres](https://github.com/goravel/postgres) | The Postgres driver of Database module |
+| [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/getting-started/privacy.md b/en/prologue/privacy.md
similarity index 100%
rename from en/getting-started/privacy.md
rename to en/prologue/privacy.md
diff --git a/en/getting-started/releases.md b/en/prologue/releases.md
similarity index 100%
rename from en/getting-started/releases.md
rename to en/prologue/releases.md
diff --git a/en/security/authentication.md b/en/security/authentication.md
index 9e6d20b43..edf9ea084 100644
--- a/en/security/authentication.md
+++ b/en/security/authentication.md
@@ -14,16 +14,16 @@ You can configure the parameters of JWT in the `config/jwt.go` file, such as `se
### Different JWT Guard supports different configurations
-You can set TTL, Secret and RefreshTTL for each Guard separately in the `config/auth.go` file, if not set, the `jwt.ttl` configuration is used by default.
+You can set TTL, Secret and RefreshTTL for each Guard separately in the `config/auth.go` file, if not set, these three configurations are used by the `config/jwt.go` file as default.
```go
// config/auth.go
"guards": map[string]any{
"user": map[string]any{
"driver": "jwt",
-++ "ttl": 60,
-++ "refresh_ttl": 0,
-++ "secret": "your-secret",
+++ "ttl": 60,
+++ "refresh_ttl": 0,
+++ "secret": "your-secret",
},
},
```
@@ -31,7 +31,7 @@ You can set TTL, Secret and RefreshTTL for each Guard separately in the `config/
## Generate JWT Token
```shell
-go run . artisan jwt:secret
+./artisan jwt:secret
```
## Generate Token Using User
diff --git a/en/security/authorization.md b/en/security/authorization.md
index abf01af5b..de8bd05ea 100644
--- a/en/security/authorization.md
+++ b/en/security/authorization.md
@@ -14,41 +14,29 @@ It's not necessary to exclusively use gates or policies when building an applica
### Writing Gates
-Gates serve as closures that verify whether a user is authorized to perform a specific action. They are commonly set up in the `app/providers/auth_service_provider.go` file's `Boot` method using the Gate facade.
+Gates serve as closures that verify whether a user is authorized to perform a specific action. They are commonly set up in the `bootstrap/app.go::WithCallback` function using the Gate facade.
In this scenario, we will establish a gate to check if a user can modify a particular Post model by comparing its ID to the user_id of the post's creator.
```go
-package providers
-
-import (
- "context"
-
- contractsaccess "github.com/goravel/framework/contracts/auth/access"
- "github.com/goravel/framework/auth/access"
- "github.com/goravel/framework/facades"
-)
-
-type AuthServiceProvider struct {
-}
-
-func (receiver *AuthServiceProvider) Register(app foundation.Application) {
-
-}
-
-func (receiver *AuthServiceProvider) Boot(app foundation.Application) {
- facades.Gate().Define("update-post",
- func(ctx context.Context, arguments map[string]any) contractsaccess.Response {
- user := ctx.Value("user").(models.User)
- post := arguments["post"].(models.Post)
-
- if user.ID == post.UserID {
- return access.NewAllowResponse()
- } else {
- return access.NewDenyResponse("error")
- }
- },
- )
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.Gate().Define("update-post",
+ func(ctx context.Context, arguments map[string]any) contractsaccess.Response {
+ user := ctx.Value("user").(models.User)
+ post := arguments["post"].(models.Post)
+
+ if user.ID == post.UserID {
+ return access.NewAllowResponse()
+ } else {
+ return access.NewDenyResponse("error")
+ }
+ },
+ )
+ }).
+ Start()
}
```
@@ -60,7 +48,7 @@ To authorize an action using gates, you should use the `Allows` or `Denies` meth
package controllers
import (
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
)
type UserController struct {
@@ -153,9 +141,9 @@ facades.Gate().WithContext(ctx).Allows("update-post", map[string]any{
You can use the `make:policy` Artisan command to generate a policy. The generated policy will be saved in the `app/policies` directory. If the directory does not exist in your application, Goravel will create it for you.
-```go
-go run . artisan make:policy PostPolicy
-go run . artisan make:policy user/PostPolicy
+```shell
+./artisan make:policy PostPolicy
+./artisan make:policy user/PostPolicy
```
### Writing Policies
@@ -192,10 +180,17 @@ func (r *PostPolicy) Update(ctx context.Context, arguments map[string]any) contr
}
```
-Then we can register the policy to `app/providers/auth_service_provider.go`:
+Then we can register the policy to the `bootstrap/app.go::WithCallback` function:
```go
-facades.Gate().Define("update-post", policies.NewPostPolicy().Update)
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.Gate().Define("update-post", policies.NewPostPolicy().Update)
+ }).
+ Start()
+}
```
As you work on authorizing different actions, you can add more methods to your policy. For instance, you can create `View` or `Delete` methods to authorize various model-related actions. Feel free to name your policy methods as you see fit.
diff --git a/en/security/encryption.md b/en/security/encryption.md
index 3c1a43d1a..6675e72b4 100644
--- a/en/security/encryption.md
+++ b/en/security/encryption.md
@@ -8,7 +8,7 @@ Goravel's encryption services provide a simple, convenient interface for encrypt
## Configuration
-Before using Goravel's encrypter, you must set the `key` configuration option in your `config/app.go` configuration file. This option is driven by the `APP_KEY` environment variable. Use the `go run . artisan key:generate` command to generate this variable's value since the `key:generate` command will utilize Golang's secure random bytes generator to create a secure cryptographic key for your application.
+Before using Goravel's encrypter, you must set the `key` configuration option in your `config/app.go` configuration file. This option is driven by the `APP_KEY` environment variable. Use the `./artisan key:generate` command to generate this variable's value since the `key:generate` command will utilize Golang's secure random bytes generator to create a secure cryptographic key for your application.
## Using The Encrypter
diff --git a/en/testing/getting-started.md b/en/testing/getting-started.md
index 2c8a63af7..a8a0fd682 100644
--- a/en/testing/getting-started.md
+++ b/en/testing/getting-started.md
@@ -39,7 +39,7 @@ There is a `TestCase` Struct in Goravel, and the Struct will provide some conven
To create a new test case, use the `make:test` Artisan command:
```shell
-go run . artisan make:test feature/UserTest
+./artisan make:test feature/UserTest
```
Our test cases are written using the suite function of the [stretchr/testify](https://github.com/stretchr/testify) package by default. This function enables us to configure pre-test, post-test, sub-test, and assertion, among other things, which results in more organized test cases. For further information, kindly refer to the official documentation.
@@ -256,8 +256,7 @@ import (
"os"
"testing"
- "github.com/goravel/framework/facades"
-
+ "goravel/app/facades"
"goravel/database/seeders"
)
diff --git a/en/testing/mock.md b/en/testing/mock.md
index 02111d7c5..4f96d1e4b 100644
--- a/en/testing/mock.md
+++ b/en/testing/mock.md
@@ -9,6 +9,8 @@ All functions of Goravel are implemented using `facades`, and all `facades` are
## Mock facades.App
```go
+import "github.com/goravel/framework/testing/mock"
+
func CurrentLocale() string {
return facades.App().CurrentLocale(context.Background())
}
@@ -26,8 +28,6 @@ func TestCurrentLocale(t *testing.T) {
## Mock facades.Artisan
```go
-import "github.com/goravel/framework/testing/mock"
-
func ArtisanCall() {
facades.Artisan().Call("list")
}
@@ -54,7 +54,8 @@ import (
"github.com/goravel/framework/http"
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func Auth() error {
@@ -127,9 +128,10 @@ func TestConfig(t *testing.T) {
import (
"testing"
- "github.com/goravel/framework/facades"
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
+
+ "goravel/app/facades"
)
func Crypt(str string) (string, error) {
@@ -187,9 +189,10 @@ func TestEvent(t *testing.T) {
import (
"testing"
- "github.com/goravel/framework/facades"
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
+
+ "goravel/app/facades"
)
func Gate() bool {
@@ -223,7 +226,8 @@ import (
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func Grpc() (*grpc.ClientConn, error) {
@@ -253,7 +257,8 @@ import (
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func Hash() (string, error) {
@@ -443,7 +448,8 @@ import (
"github.com/goravel/framework/filesystem"
"github.com/goravel/framework/testing/mock"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func Storage() (string, error) {
@@ -478,7 +484,8 @@ import (
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func Validation() string {
@@ -522,7 +529,8 @@ import (
"github.com/goravel/framework/testing/mock"
"github.com/stretchr/testify/assert"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
func View() bool {
diff --git a/en/the-basics/controllers.md b/en/the-basics/controllers.md
index 263c3c30d..5d6a99859 100644
--- a/en/the-basics/controllers.md
+++ b/en/the-basics/controllers.md
@@ -15,7 +15,8 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/facades"
+
+ "goravel/app/facades"
)
type UserController struct {
@@ -41,8 +42,7 @@ The route define:
package routes
import (
- "github.com/goravel/framework/facades"
-
+ "goravel/app/facades"
"goravel/app/http/controllers"
)
@@ -55,8 +55,8 @@ func Api() {
### Create Controller
```shell
-go run . artisan make:controller UserController
-go run . artisan make:controller user/UserController
+./artisan make:controller UserController
+./artisan make:controller user/UserController
```
## Resource Controllers
@@ -66,7 +66,7 @@ If you think of each Eloquent model in your application as a "resource", it is t
Because of this common use case, Goravel resource routing assigns the typical create, read, update, and delete ("CRUD") routes to a controller with a single line of code. To get started, we can use the `make:controller` Artisan command's `--resource` option to quickly create a controller to handle these actions:
```shell
-go run . artisan make:controller --resource PhotoController
+./artisan make:controller --resource PhotoController
```
This command will generate a controller at `app/http/controllers/photo_controller.go`. The controller will contain a method for each of the available resource operations. Next, you may register a resource route that points to the controller:
diff --git a/en/the-basics/grpc.md b/en/the-basics/grpc.md
index 0fa036298..0fc638866 100644
--- a/en/the-basics/grpc.md
+++ b/en/the-basics/grpc.md
@@ -12,7 +12,7 @@ In the `config/grpc.go` file, you can configure the Grpc module, where `grpc.hos
## Controllers
-Controllers can be defined in the `/app/grpc/controllers` directory.
+Controllers can be defined in the `app/grpc/controllers` directory.
```go
// app/grpc/controllers
@@ -25,8 +25,7 @@ import (
"github.com/goravel/grpc/protos"
)
-type UserController struct {
-}
+type UserController struct {}
func NewUserController() *UserController {
return &UserController{}
@@ -41,7 +40,7 @@ func (r *UserController) Show(ctx context.Context, req *protos.UserRequest) (pro
## Define routing
-All routing files can be defined in the `/routes` directory, such as `/routes/grpc.go`. Then bind routes in the `app/providers/grpc_service_provider.go` file.
+All routing files can be defined in the `routes` directory, such as `routes/grpc.go`.
```go
// routes/grpc.go
@@ -49,8 +48,8 @@ package routes
import (
"github.com/goravel/grpc/protos"
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
"goravel/app/grpc/controllers"
)
@@ -61,91 +60,50 @@ func Grpc() {
### Register routing
-Register routing in the `app/providers/grpc_service_provider.go` file after routing was defined.
+Register routing in the `bootstrap/app.go::WithRouting` function after routing was defined.
```go
-// app/providers/grpc_service_provider.go
-package providers
-
-import (
- "goravel/routes"
-)
-
-type GrpcServiceProvider struct {
-}
-
-func (router *GrpcServiceProvider) Register() {
-
-}
-
-func (router *GrpcServiceProvider) Boot() {
- routes.Grpc()
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithRouting(func() {
+ routes.Grpc()
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
-## Start Grpc Server
-
-Start Grpc in the `main.go` file.
-
-```go
-go func() {
- if err := facades.Grpc().Run(); err != nil {
- facades.Log().Errorf("Grpc run error: %v", err)
- }
-}()
-```
-
## Interceptor
-The interceptor can be defined in the `app/grpc/inteceptors` folder, and then registered to `app/grpc/kernel.go`.
-
-**Server Interceptor**
-
-You can set the server interceptors in the `app/grpc/kernel.go:UnaryServerInterceptors` method. For example:
+The interceptor can be defined in the `app/grpc/interceptors` folder, and register them in the `WithGrpcServerInterceptors` and `WithGrpcClientInterceptors` functions of the `bootstrap/app.go` file.
```go
-// app/grpc/kernel.go
-import (
- "goravel/app/grpc/interceptors"
-
- "google.golang.org/grpc"
-)
-
-func (kernel *Kernel) UnaryServerInterceptors() []grpc.UnaryServerInterceptor {
- return []grpc.UnaryServerInterceptor{
- interceptors.Server,
- }
-}
-```
-
-**Client Interceptor**
-
-You can set the client interceptor in the `app/grpc/kernel.go:UnaryClientInterceptorGroups` method, the method can group interceptors. For example, `interceptors.Client` is included under the `trace` group.
-
-```go
-// app/grpc/kernel.go
-import (
- "goravel/app/grpc/interceptors"
-
- "google.golang.org/grpc"
-)
-
-func (kernel *Kernel) UnaryClientInterceptorGroups() map[string][]grpc.UnaryClientInterceptor {
- return map[string][]grpc.UnaryClientInterceptor{
- "trace": {
- interceptors.Client,
- },
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithGrpcServerInterceptors(func() []grpc.UnaryServerInterceptor {
+ return []grpc.UnaryServerInterceptor{
+ interceptors.TestServer,
+ }
+ }).
+ WithGrpcClientInterceptors(func() map[string][]grpc.UnaryClientInterceptor {
+ return map[string][]grpc.UnaryClientInterceptor{
+ "default": {
+ interceptors.TestClient,
+ },
+ }
+ }).
+ Start()
}
```
-the `trace` group can be applied to the configuration item `grpc.clients.interceptors`, in this way, the Client will be applied to all interceptors under the group. For example:
+The `default` in the example above is a group name can be applied to the configuration item `grpc.clients.interceptors`, in this way, the Client will be applied to all interceptors under the group. For example:
```go
package config
import (
- "github.com/goravel/framework/facades"
+ "goravel/app/facades"
)
func init() {
@@ -157,7 +115,6 @@ func init() {
"host": config.Env("GRPC_HOST", ""),
// Configure your client host and interceptors.
- // Interceptors can be the group name of UnaryClientInterceptorGroups in app/grpc/kernel.go.
"clients": map[string]any{
"user": map[string]any{
"host": config.Env("GRPC_USER_HOST", ""),
@@ -168,35 +125,3 @@ func init() {
})
}
```
-
-## Shutdown Grpc
-
-You can call the `Shutdown` method to gracefully shut down Grpc, which will wait for all requests to be processed before shutting down.
-
-```go
-// main.go
-bootstrap.Boot()
-
-// Create a channel to listen for OS signals
-quit := make(chan os.Signal)
-signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
-
-// Start Grpc by facades.Grpc
-go func() {
- if err := facades.Grpc().Run(); err != nil {
- facades.Log().Errorf("Grpc run error: %v", err)
- }
-}()
-
-// Listen for the OS signal
-go func() {
- <-quit
- if err := facades.Grpc().Shutdown(); err != nil {
- facades.Log().Errorf("Grpc Shutdown error: %v", err)
- }
-
- os.Exit(0)
-}()
-
-select {}
-```
diff --git a/en/the-basics/logging.md b/en/the-basics/logging.md
index bed537fc5..e57224e92 100644
--- a/en/the-basics/logging.md
+++ b/en/the-basics/logging.md
@@ -90,7 +90,7 @@ Then include a `via` option to implement a `framework\contracts\log\Logger` stru
// config/logging.go
"custom": map[string]interface{}{
"driver": "custom",
- "via": &CustomTest{},
+ "via": &CustomLogger{},
},
```
@@ -104,48 +104,11 @@ package log
type Logger interface {
// Handle pass channel config path here
- Handle(channel string) (Hook, error)
+ Handle(channel string) (Handler, error)
}
```
-files can be stored in the `app/extensions` folder (modifiable). Example:
+You can check the daily and single log drivers for reference:
-```go
-package extensions
-
-import (
- "fmt"
-
- "github.com/goravel/framework/contracts/log"
-)
-
-type Logger struct {
-}
-
-// Handle pass channel config path here
-func (logger *Logger) Handle(channel string) (log.Hook, error) {
- return &Hook{}, nil
-}
-
-type Hook struct {
-}
-
-// Levels monitoring level
-func (h *Hook) Levels() []log.Level {
- return []log.Level{
- log.DebugLevel,
- log.InfoLevel,
- log.WarningLevel,
- log.ErrorLevel,
- log.FatalLevel,
- log.PanicLevel,
- }
-}
-
-// Fire execute logic when trigger
-func (h *Hook) Fire(entry log.Entry) error {
- fmt.Printf("context=%v level=%v time=%v message=%s", entry.Context(), entry.Level(), entry.Time(), entry.Message())
-
- return nil
-}
-```
+- [Daily](https://github.com/goravel/framework/blob/master/log/logger/daily.go)
+- [Single](https://github.com/goravel/framework/blob/master/log/logger/single.go)
diff --git a/en/the-basics/middleware.md b/en/the-basics/middleware.md
index 7152486f6..49ec00b38 100644
--- a/en/the-basics/middleware.md
+++ b/en/the-basics/middleware.md
@@ -27,38 +27,40 @@ func Auth() http.Middleware {
### Create Middleware By Command
```
-go run . artisan make:middleware Auth
+./artisan make:middleware Auth
// Support nested folders
-go run . artisan make:middleware user/Auth
+./artisan make:middleware user/Auth
```
## Register Middleware
### Global Middleware
-If you want to apply middleware for every HTTP request of your application, you only need to register the middleware in the `Middleware` in the `app/http/kernel.go` file.
+If you want to apply middleware for every HTTP request of your application, you only need to register the middleware in the `WithMiddleware` function in the `bootstrap/app.go` file.
```go
-// app/http/kernel.go
-package http
-
-import (
- "github.com/goravel/framework/contracts/http"
-
- "goravel/app/http/middleware"
-)
-
-type Kernel struct {
-}
-
-func (kernel *Kernel) Middleware() []http.Middleware {
- return []http.Middleware{
- middleware.Auth(),
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithMiddleware(func(handler configuration.Middleware) {
+ handler.Append(
+ middleware.Custom(),
+ )
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
+The `handler` provides multiple functions to manage middleware:
+
+- `Append(middlewares ...http.Middleware)`: Append middleware to the end of the middleware stack.
+- `GetGlobalMiddleware() []http.Middleware`: Get all global middleware.
+- `GetRecover() func(ctx http.Context, err any)`: Get the custom recovery function.
+- `Prepend(middlewares ...http.Middleware)`: Prepend middleware to the beginning of the middleware stack.
+- `Recover(fn func(ctx http.Context, err any)) Middleware`: Set a custom recovery function to handle panics.
+- `Use(middleware ...http.Middleware) Middleware`: Replace the current middleware stack with the given middleware.
+
### Assign Middleware for Routing
You can register the middleware for some routing separately:
diff --git a/en/the-basics/request.md b/en/the-basics/request.md
index 3f2a92239..a8bd2315a 100644
--- a/en/the-basics/request.md
+++ b/en/the-basics/request.md
@@ -13,9 +13,7 @@ The `http.Context` instance is automatically injected into the controller:
```go
import "github.com/goravel/framework/contracts/http"
-facades.Route().Get("/", func(ctx http.Context) {
-
-})
+facades.Route().Get("/", func(ctx http.Context) http.Response {})
```
### Retrieving The Request Path
@@ -213,18 +211,21 @@ ctx := ctx.Context()
## Custom Recovery
-You can set a custom `recovery` by calling the `Recover` method in the `app/providers/route_service_provider.go` file.
-
-```go
-// app/providers/route_service_provider.go
-func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
- // Add HTTP middleware
- facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...)
- facades.Route().Recover(func(ctx http.Context, err error) {
- ctx.Request().Abort()
- // or
- // ctx.Response().String(500, "Internal Server Error").Abort()
- })
- ...
+You can set a custom `recovery` in the `bootstrap/app.go::WithMiddleware` function.
+
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithMiddleware(func(handler configuration.Middleware) {
+ handler.Append(
+ httpmiddleware.Throttle("global"),
+ sessionmiddleware.StartSession(),
+ ).Recover(func(ctx http.Context, err any) {
+ facades.Log().Error(err)
+ _ = ctx.Response().String(contractshttp.StatusInternalServerError, "recover").Abort()
+ })
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
diff --git a/en/the-basics/routing.md b/en/the-basics/routing.md
index d0ec49ece..ac31fa8fc 100644
--- a/en/the-basics/routing.md
+++ b/en/the-basics/routing.md
@@ -17,96 +17,27 @@ Goravel uses [gin](https://github.com/gin-gonic/gin) as its default HTTP driver.
## Default Routing File
-To define routing files, simply navigate to the `/routes` directory. By default, the framework utilizes a sample route located in `/routes/web.go`. To establish routing binding, the `func Web()` method is registered in the `app/providers/route_service_provider.go` file.
+To define routing files, simply navigate to the `routes` directory. By default, the framework utilizes a sample route located in `routes/web.go` and it is registered in the `bootstrap/app.go::WithRouting` function.
-If you require more precise management, you can add routing files to the `/routes` directory and register them in the `app/providers/route_service_provider.go` file.
-
-## Get Routes List
-
-Use the `route:list` command to view routes list:
-
-```shell
-./artisan route:list
-```
-
-## Start HTTP Server
-
-Start the HTTP server in `main.go` in the root directory by calling `facades.Route().Run()`. This will automatically fetch the `route.host` configuration.
+If you require more precise management, you can add routing files to the `routes` directory and register them in the `bootstrap/app.go::WithRouting` function as well.
```go
-package main
-
-import (
- "github.com/goravel/framework/facades"
-
- "goravel/bootstrap"
-)
-
-func main() {
- // This bootstraps the framework and gets it ready for use.
- bootstrap.Boot()
-
- // Start http server by facades.Route().
- go func() {
- if err := facades.Route().Run(); err != nil {
- facades.Log().Errorf("Route run error: %v", err)
- }
- }()
-
- select {}
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithRouting(func() {
+ routes.Web()
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
-## Start HTTPS Server
-
-Please complete the configuration of `http.tls` in `config/http.go` before using HTTPS, the `facades.Route().RunTLS()` method will start the HTTPS server according to the relevant configuration:
-
-```go
-// main.go
-if err := facades.Route().RunTLS(); err != nil {
- facades.Log().Errorf("Route run error: %v", err)
-}
-```
-
-You can also use `facades.Route().RunTLSWithCert()` method to customize the host and certificate.
-
-```go
-// main.go
-if err := facades.Route().RunTLSWithCert("127.0.0.1:3000", "ca.pem", "ca.key"); err != nil {
- facades.Log().Errorf("Route run error: %v", err)
-}
-```
-
-## Close HTTP/HTTPS Server
-
-You can gracefully close the HTTP/HTTPS server by calling the `Shutdown` method, which will wait for all requests to be processed before closing.
-
-```go
-// main.go
-bootstrap.Boot()
-
-// Create a channel to listen for OS signals
-quit := make(chan os.Signal)
-signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
-
-// Start http server by facades.Route().
-go func() {
- if err := facades.Route().Run(); err != nil {
- facades.Log().Errorf("Route run error: %v", err)
- }
-}()
-
-// Listen for the OS signal
-go func() {
- <-quit
- if err := facades.Route().Shutdown(); err != nil {
- facades.Log().Errorf("Route shutdown error: %v", err)
- }
+## Get Routes List
- os.Exit(0)
-}()
+Use the `route:list` command to view routes list:
-select {}
+```shell
+./artisan route:list
```
### Routing Methods
@@ -252,21 +183,20 @@ facades.Route().Fallback(func(ctx http.Context) http.Response {
### Defining Rate Limiters
-Goravel includes powerful and customizable rate-limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs. Typically, this should be done within the `configureRateLimiting` method of your application's `app/providers/route_service_provider.go` class.
+Goravel includes powerful and customizable rate-limiting services that you may utilize to restrict the amount of traffic for a given route or group of routes. To get started, you should define rate limiter configurations that meet your application's needs, then register them in the `bootstrap/app.go::WithCallback` function.
Rate limiters are defined using the `facades.RateLimiter()`'s `For` method. The `For` method accepts a rate limiter name and a closure that returns the limit configuration that should apply to routes that are assigned to the rate limiter. The rate limiter name may be any string you wish:
```go
-import (
- contractshttp "github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/facades"
- "github.com/goravel/framework/http/limit"
-)
-
-func (receiver *RouteServiceProvider) configureRateLimiting() {
- facades.RateLimiter().For("global", func(ctx contractshttp.Context) contractshttp.Limit {
- return limit.PerMinute(1000)
- })
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.RateLimiter().For("global", func(ctx contractshttp.Context) contractshttp.Limit {
+ return limit.PerMinute(1000)
+ })
+ }).
+ Start()
}
```
diff --git a/en/the-basics/session.md b/en/the-basics/session.md
index bb029d870..bcba1bcc3 100644
--- a/en/the-basics/session.md
+++ b/en/the-basics/session.md
@@ -12,18 +12,18 @@ The `session` configuration file is located at `config/session.go`. The default
### Register Middleware
-By default, Goravel does not start a session automatically. However, it provides middleware to start a session. You can register the session middleware in the `app/http/kernel.go` file to apply it to all routes, or you can add it to specific routes:
+By default, Goravel does not start a session automatically. However, it provides middleware to start a session. You can register the middleware in the `WithMiddleware` function in the `bootstrap/app.go` file, or you can add it to specific routes:
```go
-import (
- "github.com/goravel/framework/contracts/http"
- "github.com/goravel/framework/session/middleware"
-)
-
-func (kernel Kernel) Middleware() []http.Middleware {
- return []http.Middleware{
- middleware.StartSession(),
- }
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithMiddleware(func(handler configuration.Middleware) {
+ handler.Append(
+ middleware.StartSession(),
+ )
+ }).
+ WithConfig(config.Boot).
+ Start()
}
```
@@ -173,14 +173,12 @@ ctx.Request().Session().Now("status", "Task was successful!")
Use the `Session` facade to build a custom session. The `Session` facade provides the `BuildSession` method, which takes a driver instance and an optional session ID if you want to specify a custom session ID:
```go
-import "github.com/goravel/framework/facades"
-
session := facades.Session().BuildSession(driver, "sessionID")
```
### Add Custom Session Drivers
-#### Implementing The Driver
+#### Implementing Driver
To implement a custom session driver, driver must implement the `contracts/session/driver` interface.
@@ -202,7 +200,7 @@ type Driver interface {
}
```
-#### Registering The Driver
+#### Register Driver
After implementing the driver, you only need to add it to the `config/session.go` configuration file:
diff --git a/en/the-basics/validation.md b/en/the-basics/validation.md
index ec6220fb0..0d3b37267 100644
--- a/en/the-basics/validation.md
+++ b/en/the-basics/validation.md
@@ -35,23 +35,13 @@ import (
"github.com/goravel/framework/contracts/http"
)
-type PostController struct {
- // Dependent services
-}
+type PostController struct {}
func NewPostController() *PostController {
- return &PostController{
- // Inject services
- }
-}
-
-func (r *PostController) Create(ctx http.Context) {
-
+ return &PostController{}
}
-func (r *PostController) Store(ctx http.Context) {
-
-}
+func (r *PostController) Store(ctx http.Context) {}
```
### Writing The Validation Logic
@@ -97,8 +87,8 @@ validator, err := ctx.Request().Validate(map[string]string{
For more complex validation scenarios, you may wish to create a "form request". Form requests are custom request classes that encapsulate their own validation and authorization logic. To create a form request class, you may use the `make:request` Artisan CLI command:
```go
-go run . artisan make:request StorePostRequest
-go run . artisan make:request user/StorePostRequest
+./artisan make:request StorePostRequest
+./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.
@@ -230,6 +220,7 @@ If you do not want to use the `Validate` method on the request, you may create a
```go
func (r *PostController) Store(ctx http.Context) http.Response {
validator, _ := facades.Validation().Make(
+ ctx,
map[string]any{
"name": "Goravel",
},
@@ -255,7 +246,7 @@ The first argument passed to the `Make` method is the data under validation whic
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()`):
```go
-validator, err := facades.Validation().Make(input, rules, validation.Messages(map[string]string{
+validator, err := facades.Validation().Make(ctx, input, rules, validation.Messages(map[string]string{
"required": "The :attribute field is required.",
}))
```
@@ -265,7 +256,7 @@ validator, err := facades.Validation().Make(input, rules, validation.Messages(ma
Sometimes you may wish to specify a custom error message only for a specific attribute. You may do so using "dot" notation. Specify the attribute's name first, followed by the rule (also applicable to `ctx.Request().Validate()`):
```go
-validator, err := facades.Validation().Make(input, rules, validation.Messages(map[string]string{
+validator, err := facades.Validation().Make(ctx, input, rules, validation.Messages(map[string]string{
"email.required": "We need to know your email address!",
}))
```
@@ -275,7 +266,7 @@ validator, err := facades.Validation().Make(input, rules, validation.Messages(ma
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()`):
```go
-validator, err := facades.Validation().Make(input, rules, validation.Attributes(map[string]string{
+validator, err := facades.Validation().Make(ctx, input, rules, validation.Attributes(map[string]string{
"email": "email address",
}))
```
@@ -291,7 +282,7 @@ import (
)
func (r *PostController) Store(ctx http.Context) http.Response {
- validator, err := facades.Validation().Make(input, rules,
+ validator, err := facades.Validation().Make(ctx, input, rules,
validation.PrepareForValidation(func(ctx http.Context, data validationcontract.Data) error {
if name, exist := data.Get("name"); exist {
return data.Set("name", name)
@@ -315,7 +306,7 @@ validator, err := ctx.Request().Validate(rules)
var user models.User
err := validator.Bind(&user)
-validator, err := facades.Validation().Make(input, rules)
+validator, err := facades.Validation().Make(ctx, input, rules)
var user models.User
err := validator.Bind(&user)
```
@@ -334,7 +325,7 @@ fmt.Println(storePost.Name)
```go
validator, err := ctx.Request().Validate(rules)
-validator, err := facades.Validation().Make(input, rules)
+validator, err := facades.Validation().Make(ctx, input, rules)
message := validator.Errors().One("email")
```
@@ -439,13 +430,17 @@ Option 2: Use `facades.Validation().Make()` for rule validation;
Goravel provides a variety of helpful validation rules; however, you may wish to specify some of your own. One method of registering custom validation rules is using rule objects. To generate a new rule object, you can simply use the `make:rule` Artisan command.
+### Creating Custom Rules
+
For instance, if you want to verify that a string is uppercase, you can create a rule with this command. Goravel will then save this new rule in the `app/rules` directory. If this directory does not exist, Goravel will create it when you run the Artisan command to create your rule.
```go
-go run . artisan make:rule Uppercase
-go run . artisan make:rule user/Uppercase
+./artisan make:rule Uppercase
+./artisan make:rule user/Uppercase
```
+### Defining Custom Rules
+
After creating the rule, we need to define its behavior. A rule object has two methods: `Passes` and `Message`. The Passes method receives all data, including the data to be validated and the validation parameters. It should return `true` or `false` depending on whether the attribute value is valid. The `Message` method should return the error message for validation that should be used when the validation fails.
```go
@@ -466,46 +461,26 @@ func (receiver *Uppercase) Signature() string {
}
// Passes Determine if the validation rule passes.
-func (receiver *Uppercase) Passes(data validation.Data, val any, options ...any) bool {
+func (receiver *Uppercase) Passes(ctx context.Context, data validation.Data, val any, options ...any) bool {
return strings.ToUpper(val.(string)) == val.(string)
}
// Message Get the validation error message.
-func (receiver *Uppercase) Message() string {
+func (receiver *Uppercase) Message(ctx context.Context) string {
return "The :attribute must be uppercase."
}
-
```
-Then you need to register the rule to the `rules` method in the `app/providers/validation_service_provider.go` file, and the rule can be used like other rules, if the rule is generated by the `make:rule` command, the framework will automatically register it.
-
-```go
-package providers
-
-import (
- "github.com/goravel/framework/contracts/validation"
- "github.com/goravel/framework/facades"
-
- "goravel/app/rules"
-)
-
-type ValidationServiceProvider struct {
-}
-
-func (receiver *ValidationServiceProvider) Register() {
-
-}
+### Register Custom Rules
-func (receiver *ValidationServiceProvider) Boot() {
- if err := facades.Validation().AddRules(receiver.rules()); err != nil {
- facades.Log().Errorf("add rules error: %+v", err)
- }
-}
+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.
-func (receiver *ValidationServiceProvider) rules() []validation.Rule {
- return []validation.Rule{
- &rules.Uppercase{},
- }
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithRules(Rules).
+ WithConfig(config.Boot).
+ Start()
}
```
@@ -537,14 +512,19 @@ func (receiver *ValidationServiceProvider) rules() []validation.Rule {
## Custom filter
-Goravel provides a variety of helpful filters, however, you may wish to specify some of your own. 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:
+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:
```go
-go run . artisan make:filter ToInt
-// or
-go run . artisan make:filter user/ToInt
+./artisan make:filter ToInt
+./artisan make:filter user/ToInt
```
+### Defining Custom Filters
+
One filter contains two methods: `Signature` and `Handle`. The `Signature` method sets the name of the filter. The `Handle` method performs the specific filtering logic:
```go
@@ -566,41 +546,22 @@ func (receiver *ToInt) Signature() string {
}
// Handle defines the filter function to apply.
-func (receiver *ToInt) Handle() any {
+func (receiver *ToInt) Handle(ctx context.Context) any {
return func (val any) int {
return cast.ToString(val)
}
}
```
-Then you need to register the filter to the `filters` method in the `app/providers/validation_service_provider.go` file, and the filter can be used like others, if the filter is generated by the `make:filter` command, the framework will automatically register it.
-
-```go
-package providers
+### Register Custom Filters
-import (
- "github.com/goravel/framework/contracts/validation"
- "github.com/goravel/framework/facades"
+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.
- "goravel/app/filters"
-)
-
-type ValidationServiceProvider struct {
-}
-
-func (receiver *ValidationServiceProvider) Register() {
-
-}
-
-func (receiver *ValidationServiceProvider) Boot() {
- if err := facades.Validation().AddFilters(receiver.filters()); err != nil {
- facades.Log().Errorf("add filters error: %+v", err)
- }
-}
-
-func (receiver *ValidationServiceProvider) filters() []validation.Filter {
- return []validation.Filter{
- &filters.ToInt{},
- }
+```go
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithFilters(Filters).
+ WithConfig(config.Boot).
+ Start()
}
```
diff --git a/en/the-basics/views.md b/en/the-basics/views.md
index c0ccea931..e06cc17b2 100644
--- a/en/the-basics/views.md
+++ b/en/the-basics/views.md
@@ -80,26 +80,26 @@ facades.Route().Get("/", func(ctx http.Context) http.Response {
### Sharing Data With All Views
-Occasionally, you may need to share data with all views that are rendered by your application. You may do so using the `Share` method in `facades.View()`. Typically, you should place calls to the `Share` method within a service provider's `Boot` method. You are free to add them to the `app/providers/app_service_provider.go` class or generate a separate service provider to house them:
+Occasionally, you may need to share data with all views that are rendered by your application. You may do so using the `Share` function in `facades.View()`. Typically, you should place calls to the `Share` function in the `bootstrap/app.go::WithCallback` function:
```go
-package providers
-
-import (
- "github.com/goravel/framework/contracts/foundation"
- "github.com/goravel/framework/facades"
-)
-
-type AppServiceProvider struct {
+func Boot() contractsfoundation.Application {
+ return foundation.Setup().
+ WithConfig(config.Boot).
+ WithCallback(func() {
+ facades.View().Share("key", "value")
+ }).
+ Start()
}
+```
-func (receiver *AppServiceProvider) Register(app foundation.Application) {
-}
+## CSRF Token Middleware
-func (receiver *AppServiceProvider) Boot(app foundation.Application) {
- facades.View().Share("key", "value")
-}
-```
+This middleware can be applied to routes to ensure that requests are coming from authenticated sources to against Cross-Site Request Forgery (CSRF) attacks.
+
+1. Register the middleware (`github.com/goravel/framework/http/middleware::VerifyCsrfToken(exceptPaths)`) to global or a specific route.
+2. Add {{ .csrf_token }} to your form in the view file.
+3. The middleware will automatically verify the token on form submission.
## Register Custom Delims And Functions
diff --git a/en/upgrade/history.md b/en/upgrade/history.md
index d79286b2c..920e4b67f 100644
--- a/en/upgrade/history.md
+++ b/en/upgrade/history.md
@@ -1,5 +1,6 @@
# History Upgrade
+- [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)
- [Upgrading To v1.12 From v1.11](v1.12.md)
diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md
new file mode 100644
index 000000000..9c334c0db
--- /dev/null
+++ b/en/upgrade/v1.17.md
@@ -0,0 +1,395 @@
+# Upgrading To v1.17 From v1.16
+
+## Exciting New Features 🎉
+
+- [Goravel Lite](#goravel-lite)
+- [Simplified Code Structure](#simplified-code-structure)
+- [Process Facade](#process-facade)
+- [Pluralizer Package](#pluralizer-package)
+- [Service Provider Runners](#service-provider-runners)
+
+## Enhancements 🚀
+
+- [facades can be installed and uninstalled as needed](#facades-can-be-installed-and-uninstalled-as-needed)
+- [Mail can be sent via template](#mail-can-be-sent-via-template)
+- [The build command supports more flags](#the-build-command-supports-more-flags)
+- [Add and modify functions for DB and Orm](#add-and-modify-functions-for-db-and-orm)
+- [Add new functions for Schema](#add-new-functions-for-schema)
+- [Add new commands](#add-new-commands)
+- [Add csrf token middleware](#add-csrf-token-middleware)
+- [Command supports configure Arguments](#command-supports-configure-arguments)
+- [make:migration supports create migration via model](#make-migration-supports-create-migration-via-model)
+- [Optimize gRPC client connections](#optimize-grpc-connections)
+- [Log supports print json format](#log-supports-print-json-format)
+- [Http supports multiple clients](#http-supports-multiple-clients)
+- [Validation supports pass context](#validation-supports-pass-context)
+- [Add WithoutGlobalScopes function for Orm](#add-withoutglobalscopes-function-for-orm)
+
+## Breaking Changes 🛠
+
+- [Remove machinery queue driver](#remove-machinery-queue-driver)
+- [Switch log driver](#switch-log-driver)
+- [Optimize migrate:rollback command](#optimize-migrate-rollback-command)
+- [Remove Http.Request.Bind function](#remove-http-request-bind-function)
+
+## Upgrade Guide
+
+As [Golang v1.23 is no longer maintained](https://endoflife.date/go), the Golang version of Goravel supports has been upgraded from 1.23 to 1.24.
+
+goravel/example project from v1.16 to v1.17 PR can be used as an upgrade reference: [goravel/example#93](https://github.com/goravel/example/pull/93).
+
+### 1. Update Dependencies
+
+```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
+```
+
+### 2. Add New Service Providers
+
+```go
+// config/app.go
+import (
+ ...
+ "github.com/goravel/framework/facades/process"
+ "github.com/goravel/framework/facades/view"
+)
+
+"providers": []foundation.ServiceProvider{
+ ...
+ &process.ServiceProvider{}, // [!code ++]
+ &view.ServiceProvider{}, // [!code ++]
+},
+```
+
+### 3. Modify Http Client Configuration
+
+Add new configuration in `config/http.go`:
+
+```go
+"client": map[string]any{ // [!code --]
+ "base_url": config.GetString("HTTP_CLIENT_BASE_URL"), // [!code --]
+ "timeout": config.GetDuration("HTTP_CLIENT_TIMEOUT"), // [!code --]
+ "max_idle_conns": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS"), // [!code --]
+ "max_idle_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST"), // [!code --]
+ "max_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_CONN_PER_HOST"), // [!code --]
+ "idle_conn_timeout": config.GetDuration("HTTP_CLIENT_IDLE_CONN_TIMEOUT"), // [!code --]
+}, // [!code --]
+"default_client": config.Env("HTTP_CLIENT_DEFAULT", "default"), // [!code ++]
+"clients": map[string]any{ // [!code ++]
+ "default": map[string]any{ // [!code ++]
+ "base_url": config.Env("HTTP_CLIENT_BASE_URL", ""), // [!code ++]
+ "timeout": config.Env("HTTP_CLIENT_TIMEOUT", "30s"), // [!code ++]
+ "max_idle_conns": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS", 100), // [!code ++]
+ "max_idle_conns_per_host": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST", 2), // [!code ++]
+ "max_conns_per_host": config.Env("HTTP_CLIENT_MAX_CONN_PER_HOST", 0), // [!code ++]
+ "idle_conn_timeout": config.Env("HTTP_CLIENT_IDLE_CONN_TIMEOUT", "90s"), // [!code ++]
+ }, // [!code ++]
+}, // [!code ++]
+```
+
+### 4. If you want to send mail via template
+
+Add new configuration in `config/mail.go`:
+
+```go
+// Template Configuration
+//
+// This controls template rendering for email views. Template engines are cached
+// globally and support both built-in drivers and custom implementations via factories.
+//
+// Available Drivers: "html", "custom"
+"template": map[string]any{
+ "default": config.Env("MAIL_TEMPLATE_ENGINE", "html"),
+ "engines": map[string]any{
+ "html": map[string]any{
+ "driver": "html",
+ "path": config.Env("MAIL_VIEWS_PATH", "resources/views/mail"),
+ },
+ },
+},
+```
+
+### 5. If you are using machinery queue driver
+
+Need to modify accordingly: [Remove machinery queue driver](#remove-machinery-queue-driver)
+
+### 6. If you created a custom log driver
+
+Need to modify accordingly: [Switch log driver](#switch-log-driver)
+
+### 7. If you are using Http.Request.Bind function
+
+Need to modify accordingly: [Remove Http.Request.Bind function](#remove-http-request-bind-function)
+
+### 8. If you are using validation.Make function or custom rules/filters
+
+Need to modify accordingly: [Validation supports pass context](#validation-supports-pass-context)
+
+### 9. If you are using global scopes in Orm
+
+Need to modify accordingly: [Add WithoutGlobalScopes function for Orm](#add-withoutglobalscopes-function-for-orm)
+
+## Feature Introduction
+
+### Goravel Lite
+
+We are excited to introduce [Goravel Lite](https://github.com/goravel/goravel-lite), a lightweight version of the Goravel framework, designed for developers who want to build applications with minimal dependencies and a smaller footprint. It provides the core features of Goravel while allowing developers to add only the components they need.
+
+- Only includes essential facades: `Artisan`, `Config`, reducing bloat and improving performance.
+- Other facades can be installed and uninstalled as needed via the `package:install` and `package:uninstall` commands.
+- `goravel/goravel` == `goravel/goravel-lite` + all facades. Any improvements for `goravel/goravel` should be submitted to `goravel/goravel-lite`, then a specify github action will sync the changes to `goravel/goravel`. And `goravel/goravel` is still the main repository for users to use Goravel framework.
+- `goravel/installer` will use `goravel/goravel-lite` as the default framework to create new Goravel projects, users can choose to install needed facades.
+
+### Simplified code structure
+
+The code structure has been simplified to improve maintainability and readability to consitent with Laravel. The old structure can still be used, it's a backward compatible change. It's a bit complicated to migrate from old structure to new structure, there is an example PR for reference if needed: [goravel/example#91](https://github.com/goravel/example/pull/91).
+
+1. Remove all `kenel.go` and default `*_service_providers.go` files. Move configurations in `*_service_providers.go` files to the `bootstrap` folder.
+2. Move all facades from framework intenal to the `app/facades` folder, users can customize or extend them as needed.
+3. Simplify the facades startup process, no need to run facades in `main.go` anymorem, please refer to [Service Provider Runners](../architecture-concepts/service-providers.md#runners) for more details.
+4. Support customize directory structure via the `WithPath()` function, please refer to [Customize Directory Structure](../getting-started/directory-structure.md#customize-directory-structure) for more details.
+
+### Process Facade
+
+Goravel now provides an expressive and elegant API around Go's standard `os/exec` package, allowing you to invoke external commands from your application seamlessly.
+
+[View Document](../digging-deeper/processes.md)
+
+### Pluralizer package
+
+Goravel now includes a pluralizer package that helps with converting words between singular and plural forms based on language rules. This is useful for applications that need to handle different languages and their grammatical rules.
+
+[View Document](../digging-deeper/pluralization.md)
+
+### Service Provider Runners
+
+Service Providers can now implement the `Runners` interface to run code when the application starts. This is typically used to start or shutdown services defined in the Service Provider, such as Route, Schedule, Queue, etc. You no longer need to start or shutdown these services manually in `main.go`, as Goravel will handle it for you.
+
+This feature is only available when using [the new simplified code structure](#simplified-code-structure).
+
+[View Document](../architecture-concepts/service-providers.md#runners)
+
+### Log supports print json format
+
+Goravel's logging system now supports outputting logs in JSON format, making it easier to integrate with log management systems and perform structured logging.
+
+```go
+"channels": map[string]any{
+ "daily": map[string]any{
+ "driver": "daily",
+ "path": "storage/logs/goravel.log",
+ "level": config.Env("LOG_LEVEL", "debug"),
+ "days": 7,
+ "print": true,
+ // the value can be "text" or "json"
+ "formatter": "json", // [!code ++]
+ },
+},
+```
+
+### Http supports multiple clients
+
+Goravel's HTTP client module now supports multiple clients, allowing you to define and use different HTTP clients with their own configurations for various purposes within your application.
+
+[View Document](../digging-deeper/http-client.md#making-requests)
+
+### Validation supports pass context
+
+Goravel's validation module now supports passing context to validation functions, enabling more flexible and dynamic validation scenarios based on the context of the request or operation.
+
+```go
+validator, err := facades.Validation().Make(input, rules) // [!code --]
+validator, err := facades.Validation().Make(ctx, input, rules) // [!code ++]
+```
+
+Custom rules and filters also support context parameter:
+
+```go
+type Rule interface {
+ Signature() string
+ Passes(data Data, val any, options ...any) bool // [!code --]
+ Message() string // [!code --]
+ Passes(ctx context.Context, data Data, val any, options ...any) bool // [!code ++]
+ Message(ctx context.Context) string // [!code ++]
+}
+
+type Filter interface {
+ Signature() string
+ Handle() any // [!code --]
+ Handle(ctx context.Context) any // [!code ++]
+}
+```
+
+### Add WithoutGlobalScopes function for Orm
+
+A new `WithoutGlobalScopes` function has been added to the Orm package, allowing you to exclude all global scopes from a query. This is useful when you want to retrieve records without the constraints imposed by global scopes.
+
+```go
+var user []User
+err := facades.Orm().Model(&User{}).WithoutGlobalScopes().Get(&user)
+err := facades.Orm().Model(&User{}).WithoutGlobalScopes("name").Get(&user)
+```
+
+In order to support set global scope name, the `GlobalScope` interface of model has been modified:
+
+```go
+import "github.com/goravel/framework/contracts/orm"
+
+type User struct {
+ orm.Model
+ Name string
+}
+
+func (r *User) GlobalScopes() []func(orm.Query) orm.Query { // [!code --]
+func (r *User) GlobalScopes() map[string]func(orm.Query) orm.Query { // [!code ++]
+ return map[string]func(orm.Query) orm.Query{
+ "name": func(query orm.Query) orm.Query {
+ return query.Where("name", "goravel")
+ },
+ }
+}
+```
+
+### facades can be installed and uninstalled as needed
+
+Follow the release of [Goravel Lite](#goravel-lite), now facades can be installed and uninstalled as needed via the `package:install` and `package:uninstall` commands. Notice: this feature is only available when you project is built with Goravel Lite or upgrades to [the new simplified code structure](#simplified-code-structure).
+
+[View Document](../architecture-concepts/facades.md#install-uninstall-facades)
+
+### Mail can be sent via template
+
+Mail sending now supports templates, allowing you to send emails using predefined templates for better consistency and easier management. The template supports http and custom drivers.
+
+[View Document](../digging-deeper/mail.md#using-template)
+
+### The build command supports more flags
+
+Add new `--arch` and `--static` flags to the `build` command, allowing you to specify the target architecture and whether to build a static binary.
+
+### Add and modify functions for DB and Orm
+
+New functions:
+
+- `Avg(column string, dest any) error`: Calculate the average value of a column.
+- `Min(column string, dest any) error`: Find the minimum value of a column.
+- `Max(column string, dest any) error`: Find the maximum value of a column.
+- `WhereAll(columns []string, args ...any) Query`: Adds a "where all columns match" clause to the query.
+- `WhereAny(columns []string, args ...any) Query`: Adds a "where any column matches" clause to the query.
+- `WhereNone(columns []string, args ...any) Query`: Adds a "where none of the columns match" clause to the query.
+
+Modified functions:
+
+```go
+// Before
+Sum(column string) (int64, error)
+
+// After
+Sum(column string, dest any) error
+```
+
+### Add new functions for Schema
+
+New functions have been added to the Schema package to enhance database schema management capabilities.
+
+- `DateTimes`: Create `created_at` and `updated_at` columns on the table.
+- `ForeignID`: Create an unsigned big integer column for foreign keys.
+- `ForeignUlid`: Create a ULID column for foreign keys.
+- `ForeignUuid`: Create a UUID column for foreign keys.
+
+### Add new commands
+
+- `make:provider`: Create a new service provider class.
+- `make:view`: Create a new view template file.
+
+### Add csrf token middleware
+
+A new middleware has been added to protect against Cross-Site Request Forgery (CSRF) attacks. This middleware can be applied to routes to ensure that requests are coming from authenticated sources.
+
+[View Document](../the-basics/views.md#csrf-token-middleware)
+
+### Command supports configure Arguments
+
+Commands now support configuring arguments, allowing you to define and handle command-line arguments more effectively within your custom commands.
+
+[View Document](../digging-deeper/artisan-console.md#arguments)
+
+### make:migration supports create migration via model
+
+The `make:migration` command has been enhanced to support creating migrations based on existing models. This allows for quicker migration creation by leveraging the structure defined in your models.
+
+[View Document](../database/migrations.md#create-migrations)
+
+### Optimize gRPC client connections
+
+A client connection pool has been implemented for gRPC clients to optimize performance and resource utilization. This allows for reusing existing connections rather than creating new ones for each request, reducing latency and improving efficiency.
+
+### Remove machinery queue driver
+
+The machinery queue driver has been removed totally given it's deprecated in v1.16. Please follow the guidelines in the [v1.16 upgrade document](./v1.16.md#optimize-queue-module-configuration) to migrate to other queue drivers if you are still using it.
+
+### Switch log driver
+
+The log driver has been switched from `logrus` to `log/slog` for better performance and structured logging capabilities.
+
+The custom log driver interface has been modified:
+
+```go
+// framework/contracts/log/Logger
+package log
+
+type Logger interface {
+ // Handle pass channel config path here
+ Handle(channel string) (Hook, error) // [!code --]
+ Handle(channel string) (Handler, error) // [!code ++]
+}
+```
+
+You can use `log.HookToHandler` function to adapt old custom log drivers:
+
+```go
+type CustomLogger struct {}
+
+func (logger *CustomLogger) Handle(channel string) (log.Handler, error) {
+ hook := /* your old custom log driver logic */
+
+ return log.HookToHandler(hook), nil
+}
+```
+
+### Optimize migrate:rollback command
+
+Previously, the `migrate:rollback` command only rolls back one migration at a time, it's not consistent with Laravel's behavior. Now the command will roll back all migrations from the last batch by default. You can still use the `--step` option to specify the number of batches to roll back.
+
+### Remove Http.Request.Bind function
+
+The `Bind` function on the `Http.Request` interface has been removed. You can now use the `Bind` method via `response`.
+
+```go
+var user User
+response, err := facades.Http().Bind(&user).Get("https://github.com") // [!code --]
+response, err := facades.Http().Get("https://github.com") // [!code ++]
+err = response.Bind(&user) // [!code ++]
+```