From a5dc31cbd7b8fe280fb8da20f4ba1812224c6d0d Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 22 Aug 2025 17:30:49 +0800 Subject: [PATCH 01/49] optimize mockery --- en/getting-started/contributions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/getting-started/contributions.md b/en/getting-started/contributions.md index e3f22dddb..c6d7e9f58 100644 --- a/en/getting-started/contributions.md +++ b/en/getting-started/contributions.md @@ -89,7 +89,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; ❀️ From cc6e612cd8033fc8bd6cf8cb02f618276ccb153a Mon Sep 17 00:00:00 2001 From: zoryamba Date: Sun, 5 Oct 2025 15:42:28 +0300 Subject: [PATCH 02/49] add divider and colored outputs (#137) --- en/digging-deeper/artisan-console.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index f849676ec..23b1ccf7e 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -369,6 +369,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 @@ -424,6 +437,15 @@ 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`: From 52e993c1f6150f55825c67cd4c3848ce5602c602 Mon Sep 17 00:00:00 2001 From: toni dy Date: Fri, 17 Oct 2025 09:16:15 +0700 Subject: [PATCH 03/49] Add MongoDB package (#140) * add mongodb package * Update packages.md with test coverage note Added a note about test coverage ordering and corrected a missing asterisk in the table header. --- en/getting-started/packages.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md index d8948799b..88a87e2ed 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% | @@ -12,7 +13,10 @@ You can find extended packages for Goravel here, and you can also create a PR fo | [goravel/oss](https://github.com/goravel/oss) | A OSS disk driver for `facades.Storage()` | 76.5% | | [goravel/installer](https://github.com/goravel/installer) | Goravel installer | 76.2% | | [goravel/cloudinary](https://github.com/goravel/cloudinary) | A Cloudinary disk driver for `facades.Storage() | 75.4% | +| [portofolio-mager/goravel-mongodb](https://github.com/portofolio-mager/goravel-mongodb) | A MongoDB package | 16.9% | | [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. From c52d02c424c2550b22b09326bf91787db798a76f Mon Sep 17 00:00:00 2001 From: toni dy Date: Fri, 17 Oct 2025 11:45:37 +0700 Subject: [PATCH 04/49] Enhance package.md with discoverability tips via GitHub Topic (#141) * Enhance package.md with discoverability tips Added tips for improving package discoverability in the documentation. * Add goravel topic * Enhance topic addition instructions in packages.md Updated instructions for adding topics to a repository for better discoverability. * Clean up blank lines in packages.md Removed extra blank lines in the packages documentation. --- en/getting-started/packages.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md index 88a87e2ed..f50ad9f8a 100644 --- a/en/getting-started/packages.md +++ b/en/getting-started/packages.md @@ -20,3 +20,12 @@ You can find extended packages for Goravel here, and you can also create a PR fo | [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. From 485939399007c52af549eda414f4587da62e5ab6 Mon Sep 17 00:00:00 2001 From: Oleksii Prudkyi Date: Sat, 25 Oct 2025 17:03:34 +0200 Subject: [PATCH 05/49] feat: documentation for console arguments (#132) * feat: documentation for console arguments * fix: text formatting --- en/digging-deeper/artisan-console.md | 58 +++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 23b1ccf7e..7d7f281b6 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -93,11 +93,67 @@ When you write console commands, it's typical to collect user input through `arg Follow the arguments after the command: ```shell -go run . artisan send:emails NAME EMAIL +go run . artisan send:emails SUBJECT EMAIL_1 EMAIL_2 ``` +Definition: + +```go +// send:emails +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) From b9a97817a197aedd474b0956070a6d7d26b3070b Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 4 Jan 2026 10:41:54 +0800 Subject: [PATCH 06/49] feat: [#559] Feature Comparison Table with Laravel --- .vitepress/config/en.ts | 30 ++++++++++---- en/prologue/compare-with-laravel.md | 41 +++++++++++++++++++ .../contributions.md | 0 en/{getting-started => prologue}/privacy.md | 0 en/{getting-started => prologue}/releases.md | 0 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 en/prologue/compare-with-laravel.md rename en/{getting-started => prologue}/contributions.md (100%) rename en/{getting-started => prologue}/privacy.md (100%) rename en/{getting-started => prologue}/releases.md (100%) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 4f6d734bc..ae6c34965 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -8,15 +8,20 @@ export const config = defineConfig({ nav: nav(), sidebar: [ { - text: 'Quickstart', - base: '/getting-started/', - items: sidebarQuickstart() + text: 'Prologue', + base: '/prologue/', + items: sidebarPrologue() }, { text: 'Upgrade', base: '/upgrade/', items: sidebarUpgrade() }, + { + text: 'Getting Started', + base: '/getting-started/', + items: sidebarGettingStarted() + }, { text: 'Architecture Concepts', base: '/architecture-concepts/', @@ -104,7 +109,7 @@ function nav(): DefaultTheme.NavItem[] { ] } -function sidebarQuickstart(): DefaultTheme.SidebarItem[] { +function sidebarGettingStarted(): DefaultTheme.SidebarItem[] { return [ { text: 'Installation', @@ -122,6 +127,15 @@ function sidebarQuickstart(): DefaultTheme.SidebarItem[] { text: 'Compile', link: 'compile' }, + { + text: 'Excellent Packages', + link: 'packages' + } + ] +} + +function sidebarPrologue(): DefaultTheme.SidebarItem[] { + return [ { text: 'Release Notes', link: 'releases' @@ -130,13 +144,13 @@ function sidebarQuickstart(): DefaultTheme.SidebarItem[] { text: 'Contribution Guide', link: 'contributions' }, - { - text: 'Excellent Packages', - link: 'packages' - }, { text: 'Privacy Policy', link: 'privacy' + }, + { + text: 'Compare With Laravel', + link: 'compare-with-laravel' } ] } diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md new file mode 100644 index 000000000..5c96a974f --- /dev/null +++ b/en/prologue/compare-with-laravel.md @@ -0,0 +1,41 @@ +# 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 | +|------------------------|----------------------------------|----------------------------------| +| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | βœ… `go run artisan key:generate` | βœ… `./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.OrderShipped{}).Dispatch()` | βœ… `OrderShipped::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 OrderShipped())` | +| [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) | βœ… | βœ… | +| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… `facades.Queue().Job(&jobs.ProcessPodcast{}).Dispatch()` | βœ… `ProcessPodcast::dispatch()` | +| [Seeder](https://www.goravel.dev/database/seeding.html) | βœ… `facades.Seeder().Call([]seeder.Seeder{&UserSeeder{}})` | βœ… `$this->call([UserSeeder::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().Validate(map[string]string{"name": "required"})` | βœ… `$request->validate(['name' => 'required'])` | +| [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) | βœ… `facades.Grpc().Run()` | 🚧 Not available natively | +| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… Long-running command-line process management | βœ… `Process::run('ls -la')` | +| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… `facades.RateLimiter().TooManyAttempts("key", 5)` | βœ… `RateLimiter::tooManyAttempts('key', 5)` | +| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… Application monitoring and telemetry | 🚧 Not available natively | +| Broadcasting | 🚧 Not available natively | βœ… `broadcast(new OrderShipped($order))` | +| Livewire / Inertia | 🚧 Not available natively | βœ… Full-stack framework for Laravel | +| Notifications | 🚧 Not available natively | βœ… `$user->notify(new InvoicePaid($invoice))` | diff --git a/en/getting-started/contributions.md b/en/prologue/contributions.md similarity index 100% rename from en/getting-started/contributions.md rename to en/prologue/contributions.md 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 From 8d46d8162e1858f525932400e32ea6c5b5485b98 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 4 Jan 2026 11:21:51 +0800 Subject: [PATCH 07/49] optimize --- .vitepress/config/en.ts | 10 ++-- README.md | 2 +- en/README.md | 2 +- en/prologue/compare-with-laravel.md | 74 ++++++++++++++--------------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index ae6c34965..5f8f7056a 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -104,7 +104,7 @@ function nav(): DefaultTheme.NavItem[] { }, { text: 'Translate', - link: '/getting-started/contributions#add-a-new-language' + link: '/prologue/contributions#add-a-new-language' } ] } @@ -144,13 +144,13 @@ function sidebarPrologue(): DefaultTheme.SidebarItem[] { text: 'Contribution Guide', link: 'contributions' }, - { - text: 'Privacy Policy', - link: 'privacy' - }, { text: 'Compare With Laravel', link: 'compare-with-laravel' + }, + { + text: 'Privacy Policy', + link: 'privacy' } ] } diff --git a/README.md b/README.md index 357baae25..fbf4c4981 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is goravel official document. ## Contributing -We welcome contributions to the Goravel documentation. Please see the [contributing guide](en/getting-started/contributions.md) for more information. +We welcome contributions to the Goravel documentation. Please see the [contributing guide](en/prologue/contributions.md) for more information. ## Install diff --git a/en/README.md b/en/README.md index 0c8c0c7a2..c7993cbb6 100644 --- a/en/README.md +++ b/en/README.md @@ -50,7 +50,7 @@ Example [https://github.com/goravel/example](https://github.com/goravel/example) ## Contributors -This project is made possible by everyone who contributes. To contribute, please consult the [Contribution Guide](getting-started/contributions.md). +This project is made possible by everyone who contributes. To contribute, please consult the [Contribution Guide](prologue/contributions.md). diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md index 5c96a974f..0d82c9beb 100644 --- a/en/prologue/compare-with-laravel.md +++ b/en/prologue/compare-with-laravel.md @@ -2,40 +2,40 @@ 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 | -|------------------------|----------------------------------|----------------------------------| -| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | βœ… `go run artisan key:generate` | βœ… `./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.OrderShipped{}).Dispatch()` | βœ… `OrderShipped::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 OrderShipped())` | -| [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) | βœ… | βœ… | -| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… `facades.Queue().Job(&jobs.ProcessPodcast{}).Dispatch()` | βœ… `ProcessPodcast::dispatch()` | -| [Seeder](https://www.goravel.dev/database/seeding.html) | βœ… `facades.Seeder().Call([]seeder.Seeder{&UserSeeder{}})` | βœ… `$this->call([UserSeeder::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().Validate(map[string]string{"name": "required"})` | βœ… `$request->validate(['name' => 'required'])` | -| [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) | βœ… `facades.Grpc().Run()` | 🚧 Not available natively | -| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… Long-running command-line process management | βœ… `Process::run('ls -la')` | -| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… `facades.RateLimiter().TooManyAttempts("key", 5)` | βœ… `RateLimiter::tooManyAttempts('key', 5)` | -| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… Application monitoring and telemetry | 🚧 Not available natively | -| Broadcasting | 🚧 Not available natively | βœ… `broadcast(new OrderShipped($order))` | -| Livewire / Inertia | 🚧 Not available natively | βœ… Full-stack framework for Laravel | -| Notifications | 🚧 Not available natively | βœ… `$user->notify(new InvoicePaid($invoice))` | +| 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) | βœ… | βœ… | | +| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… | βœ… | facades.Queue().Job(&jobs.Process{}).Dispatch()
Process::dispatch() | +| [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') | +| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | Long-running command-line process management
`Process::run('ls -la') | +| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | facades.RateLimiter().TooManyAttempts("key", 5)
RateLimiter::tooManyAttempts('key', 5) | +| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | βœ… | 🚧 | +| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… | 🚧 | +| Broadcasting | 🚧 | βœ… | +| Livewire / Inertia | 🚧 | βœ… | +| Notifications | 🚧 | βœ… | From 65ce02a686a1cd71725c003a57461c27c919e500 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 18:49:50 +0530 Subject: [PATCH 08/49] add introduction to process facade --- en/digging-deeper/processes.md | 140 +++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 en/digging-deeper/processes.md diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md new file mode 100644 index 000000000..37c664def --- /dev/null +++ b/en/digging-deeper/processes.md @@ -0,0 +1,140 @@ +# 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" + "github.com/goravel/framework/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("...") +``` \ No newline at end of file From cbbe9bb70bc77b241a640b51b955d064fa121a3b Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 19:17:10 +0530 Subject: [PATCH 09/49] add docs for process Pipelining --- en/digging-deeper/processes.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index 37c664def..be57b2d62 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -137,4 +137,36 @@ facades.Process().Quietly().Run("...") facades.Process().DisableBuffering().OnOutput(func(typ process.OutputType, b []byte) { // ... }).Run("...") -``` \ No newline at end of file +``` + +### 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() +``` + +#### 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(key string, typ process.OutputType, line []byte) { + // 'key' will be "source" or "filter" +}).Run() +``` From 7f7433f98e30caa3e68bd16e6622ed79abf03e94 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 19:24:06 +0530 Subject: [PATCH 10/49] add warning in process Pipeline configuration --- en/digging-deeper/processes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index be57b2d62..742a2d3ca 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -154,6 +154,19 @@ result, err := facades.Process().Pipe(func(pipe process.Pipe) { }).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, From 4daf386b0dcf1e690c5d3d3697dda0d22e5b8f72 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 23:02:25 +0530 Subject: [PATCH 11/49] add doc for async processes --- en/digging-deeper/processes.md | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index 742a2d3ca..c616e8c51 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -179,7 +179,93 @@ using the `As` method, which is highly useful for debugging complex pipelines. facades.Process().Pipe(func(pipe process.Pipe) { pipe.Command("cat", "access.log").As("source") pipe.Command("grep", "error").As("filter") -}).OnOutput(func(key string, typ process.OutputType, line []byte) { +}).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. +::: From 36a6a8f44165726a8b480bd624f141126fef1672 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 23:30:09 +0530 Subject: [PATCH 12/49] add doc for concurrent processes --- .vitepress/config/en.ts | 4 + en/digging-deeper/processes.md | 132 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 5f8f7056a..5e759c830 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -280,6 +280,10 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { text: 'Color Output', link: 'color' }, + { + text: 'Processes', + link: 'processes' + }, { text: 'Strings', link: 'strings' diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index c616e8c51..b917ace20 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -269,3 +269,135 @@ if process.Running() { 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() +``` From 109d75c25c4c84a638dc4778cf6089425082d71c Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Mon, 12 Jan 2026 00:10:58 +0530 Subject: [PATCH 13/49] add docs for pluralization --- .vitepress/config/en.ts | 8 +++ en/digging-deeper/pluralization.md | 110 +++++++++++++++++++++++++++++ en/digging-deeper/strings.md | 36 ++++++++++ en/digging-deeper/telemetry.md | 5 ++ 4 files changed, 159 insertions(+) create mode 100644 en/digging-deeper/pluralization.md create mode 100644 en/digging-deeper/telemetry.md diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 5e759c830..eb7d532a9 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -295,6 +295,14 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { { text: 'HTTP Client', link: 'http-client' + }, + { + text: 'Pluralization', + link: 'pluralization' + }, + { + text: 'Telemetry', + link: 'telemetry' } ] } diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md new file mode 100644 index 000000000..9f2409db5 --- /dev/null +++ b/en/digging-deeper/pluralization.md @@ -0,0 +1,110 @@ +# 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" + +// Register that "mouse" becomes "mice" +pluralizer.RegisterIrregular("english", pluralizer.Substitution{ + Singular: "mouse", + Plural: "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") +} +``` \ No newline at end of file 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/telemetry.md b/en/digging-deeper/telemetry.md new file mode 100644 index 000000000..537fbfe49 --- /dev/null +++ b/en/digging-deeper/telemetry.md @@ -0,0 +1,5 @@ +# Telemetry + +[[toc]] + +## Introduction From 8b9111d0f7775d5666d2c5901f0938deee9d3d79 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Mon, 12 Jan 2026 00:18:25 +0530 Subject: [PATCH 14/49] add pluralization supported languages --- en/digging-deeper/pluralization.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md index 9f2409db5..b4eb98f9b 100644 --- a/en/digging-deeper/pluralization.md +++ b/en/digging-deeper/pluralization.md @@ -107,4 +107,14 @@ func init() { // Set it as active _ = pluralizer.UseLanguage("my_custom_language") } -``` \ No newline at end of file +``` + +## 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 From c3ae6a4c1b9ca0b174735668ee7d17c9b529e14c Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 22 Aug 2025 17:30:49 +0800 Subject: [PATCH 15/49] optimize mockery --- en/getting-started/contributions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/getting-started/contributions.md b/en/getting-started/contributions.md index 1dfe14046..c185976b4 100644 --- a/en/getting-started/contributions.md +++ b/en/getting-started/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; ❀️ From bd2c1f004bcc134bcfa935b344684fdae950f560 Mon Sep 17 00:00:00 2001 From: zoryamba Date: Sun, 5 Oct 2025 15:42:28 +0300 Subject: [PATCH 16/49] add divider and colored outputs (#137) --- en/digging-deeper/artisan-console.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index ac7e90f6a..ee45f5076 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -360,6 +360,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,6 +428,15 @@ 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`: From de04fd4ffe410c36d4b95242601dcb4415e5108d Mon Sep 17 00:00:00 2001 From: toni dy Date: Fri, 17 Oct 2025 09:16:15 +0700 Subject: [PATCH 17/49] Add MongoDB package (#140) * add mongodb package * Update packages.md with test coverage note Added a note about test coverage ordering and corrected a missing asterisk in the table header. --- en/getting-started/packages.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md index 628b9466f..549693a40 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% | @@ -16,8 +17,11 @@ You can find extended packages for Goravel here, and you can also create a PR fo | [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. From 91d975d3abd26a67627fd55de378c0d715af09c9 Mon Sep 17 00:00:00 2001 From: toni dy Date: Fri, 17 Oct 2025 11:45:37 +0700 Subject: [PATCH 18/49] Enhance package.md with discoverability tips via GitHub Topic (#141) * Enhance package.md with discoverability tips Added tips for improving package discoverability in the documentation. * Add goravel topic * Enhance topic addition instructions in packages.md Updated instructions for adding topics to a repository for better discoverability. * Clean up blank lines in packages.md Removed extra blank lines in the packages documentation. --- en/getting-started/packages.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/en/getting-started/packages.md b/en/getting-started/packages.md index 549693a40..90ddbd267 100644 --- a/en/getting-started/packages.md +++ b/en/getting-started/packages.md @@ -25,3 +25,12 @@ You can find extended packages for Goravel here, and you can also create a PR fo | [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. From 19cb9f3946f5938e0adf1e824cc291a181d43720 Mon Sep 17 00:00:00 2001 From: Oleksii Prudkyi Date: Sat, 25 Oct 2025 17:03:34 +0200 Subject: [PATCH 19/49] feat: documentation for console arguments (#132) * feat: documentation for console arguments * fix: text formatting --- en/digging-deeper/artisan-console.md | 58 +++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index ee45f5076..817c584dd 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -93,11 +93,67 @@ When you write console commands, it's typical to collect user input through `arg Follow the arguments after the command: ```shell -go run . artisan send:emails NAME EMAIL +go run . artisan send:emails SUBJECT EMAIL_1 EMAIL_2 ``` +Definition: + +```go +// send:emails +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) From 4c5867f0e53ee878d7860d8ba3982346fcfc0219 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 4 Jan 2026 10:41:54 +0800 Subject: [PATCH 20/49] feat: [#559] Feature Comparison Table with Laravel --- .vitepress/config/en.ts | 30 ++++++++++---- en/prologue/compare-with-laravel.md | 41 +++++++++++++++++++ .../contributions.md | 0 en/{getting-started => prologue}/privacy.md | 0 en/{getting-started => prologue}/releases.md | 0 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 en/prologue/compare-with-laravel.md rename en/{getting-started => prologue}/contributions.md (100%) rename en/{getting-started => prologue}/privacy.md (100%) rename en/{getting-started => prologue}/releases.md (100%) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 4f6d734bc..ae6c34965 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -8,15 +8,20 @@ export const config = defineConfig({ nav: nav(), sidebar: [ { - text: 'Quickstart', - base: '/getting-started/', - items: sidebarQuickstart() + text: 'Prologue', + base: '/prologue/', + items: sidebarPrologue() }, { text: 'Upgrade', base: '/upgrade/', items: sidebarUpgrade() }, + { + text: 'Getting Started', + base: '/getting-started/', + items: sidebarGettingStarted() + }, { text: 'Architecture Concepts', base: '/architecture-concepts/', @@ -104,7 +109,7 @@ function nav(): DefaultTheme.NavItem[] { ] } -function sidebarQuickstart(): DefaultTheme.SidebarItem[] { +function sidebarGettingStarted(): DefaultTheme.SidebarItem[] { return [ { text: 'Installation', @@ -122,6 +127,15 @@ function sidebarQuickstart(): DefaultTheme.SidebarItem[] { text: 'Compile', link: 'compile' }, + { + text: 'Excellent Packages', + link: 'packages' + } + ] +} + +function sidebarPrologue(): DefaultTheme.SidebarItem[] { + return [ { text: 'Release Notes', link: 'releases' @@ -130,13 +144,13 @@ function sidebarQuickstart(): DefaultTheme.SidebarItem[] { text: 'Contribution Guide', link: 'contributions' }, - { - text: 'Excellent Packages', - link: 'packages' - }, { text: 'Privacy Policy', link: 'privacy' + }, + { + text: 'Compare With Laravel', + link: 'compare-with-laravel' } ] } diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md new file mode 100644 index 000000000..5c96a974f --- /dev/null +++ b/en/prologue/compare-with-laravel.md @@ -0,0 +1,41 @@ +# 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 | +|------------------------|----------------------------------|----------------------------------| +| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | βœ… `go run artisan key:generate` | βœ… `./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.OrderShipped{}).Dispatch()` | βœ… `OrderShipped::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 OrderShipped())` | +| [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) | βœ… | βœ… | +| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… `facades.Queue().Job(&jobs.ProcessPodcast{}).Dispatch()` | βœ… `ProcessPodcast::dispatch()` | +| [Seeder](https://www.goravel.dev/database/seeding.html) | βœ… `facades.Seeder().Call([]seeder.Seeder{&UserSeeder{}})` | βœ… `$this->call([UserSeeder::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().Validate(map[string]string{"name": "required"})` | βœ… `$request->validate(['name' => 'required'])` | +| [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) | βœ… `facades.Grpc().Run()` | 🚧 Not available natively | +| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… Long-running command-line process management | βœ… `Process::run('ls -la')` | +| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… `facades.RateLimiter().TooManyAttempts("key", 5)` | βœ… `RateLimiter::tooManyAttempts('key', 5)` | +| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… Application monitoring and telemetry | 🚧 Not available natively | +| Broadcasting | 🚧 Not available natively | βœ… `broadcast(new OrderShipped($order))` | +| Livewire / Inertia | 🚧 Not available natively | βœ… Full-stack framework for Laravel | +| Notifications | 🚧 Not available natively | βœ… `$user->notify(new InvoicePaid($invoice))` | diff --git a/en/getting-started/contributions.md b/en/prologue/contributions.md similarity index 100% rename from en/getting-started/contributions.md rename to en/prologue/contributions.md 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 From dbe9400e1564cff5c919c665b4b7b8b6a06e5a12 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 4 Jan 2026 11:21:51 +0800 Subject: [PATCH 21/49] optimize --- .vitepress/config/en.ts | 10 ++-- README.md | 2 +- en/README.md | 2 +- en/prologue/compare-with-laravel.md | 74 ++++++++++++++--------------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index ae6c34965..5f8f7056a 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -104,7 +104,7 @@ function nav(): DefaultTheme.NavItem[] { }, { text: 'Translate', - link: '/getting-started/contributions#add-a-new-language' + link: '/prologue/contributions#add-a-new-language' } ] } @@ -144,13 +144,13 @@ function sidebarPrologue(): DefaultTheme.SidebarItem[] { text: 'Contribution Guide', link: 'contributions' }, - { - text: 'Privacy Policy', - link: 'privacy' - }, { text: 'Compare With Laravel', link: 'compare-with-laravel' + }, + { + text: 'Privacy Policy', + link: 'privacy' } ] } diff --git a/README.md b/README.md index 357baae25..fbf4c4981 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is goravel official document. ## Contributing -We welcome contributions to the Goravel documentation. Please see the [contributing guide](en/getting-started/contributions.md) for more information. +We welcome contributions to the Goravel documentation. Please see the [contributing guide](en/prologue/contributions.md) for more information. ## Install diff --git a/en/README.md b/en/README.md index 0c8c0c7a2..c7993cbb6 100644 --- a/en/README.md +++ b/en/README.md @@ -50,7 +50,7 @@ Example [https://github.com/goravel/example](https://github.com/goravel/example) ## Contributors -This project is made possible by everyone who contributes. To contribute, please consult the [Contribution Guide](getting-started/contributions.md). +This project is made possible by everyone who contributes. To contribute, please consult the [Contribution Guide](prologue/contributions.md). diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md index 5c96a974f..0d82c9beb 100644 --- a/en/prologue/compare-with-laravel.md +++ b/en/prologue/compare-with-laravel.md @@ -2,40 +2,40 @@ 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 | -|------------------------|----------------------------------|----------------------------------| -| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | βœ… `go run artisan key:generate` | βœ… `./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.OrderShipped{}).Dispatch()` | βœ… `OrderShipped::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 OrderShipped())` | -| [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) | βœ… | βœ… | -| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… `facades.Queue().Job(&jobs.ProcessPodcast{}).Dispatch()` | βœ… `ProcessPodcast::dispatch()` | -| [Seeder](https://www.goravel.dev/database/seeding.html) | βœ… `facades.Seeder().Call([]seeder.Seeder{&UserSeeder{}})` | βœ… `$this->call([UserSeeder::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().Validate(map[string]string{"name": "required"})` | βœ… `$request->validate(['name' => 'required'])` | -| [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) | βœ… `facades.Grpc().Run()` | 🚧 Not available natively | -| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… Long-running command-line process management | βœ… `Process::run('ls -la')` | -| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… `facades.RateLimiter().TooManyAttempts("key", 5)` | βœ… `RateLimiter::tooManyAttempts('key', 5)` | -| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… Application monitoring and telemetry | 🚧 Not available natively | -| Broadcasting | 🚧 Not available natively | βœ… `broadcast(new OrderShipped($order))` | -| Livewire / Inertia | 🚧 Not available natively | βœ… Full-stack framework for Laravel | -| Notifications | 🚧 Not available natively | βœ… `$user->notify(new InvoicePaid($invoice))` | +| 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) | βœ… | βœ… | | +| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | βœ… | βœ… | facades.Queue().Job(&jobs.Process{}).Dispatch()
Process::dispatch() | +| [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') | +| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | Long-running command-line process management
`Process::run('ls -la') | +| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | facades.RateLimiter().TooManyAttempts("key", 5)
RateLimiter::tooManyAttempts('key', 5) | +| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | βœ… | 🚧 | +| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… | 🚧 | +| Broadcasting | 🚧 | βœ… | +| Livewire / Inertia | 🚧 | βœ… | +| Notifications | 🚧 | βœ… | From d86288365f1d18811b3d3101c157cf36b84997ff Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 18:49:50 +0530 Subject: [PATCH 22/49] add introduction to process facade --- en/digging-deeper/processes.md | 140 +++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 en/digging-deeper/processes.md diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md new file mode 100644 index 000000000..37c664def --- /dev/null +++ b/en/digging-deeper/processes.md @@ -0,0 +1,140 @@ +# 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" + "github.com/goravel/framework/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("...") +``` \ No newline at end of file From e2251a106cd39925f4281f90c5d68905e0c96283 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 19:17:10 +0530 Subject: [PATCH 23/49] add docs for process Pipelining --- en/digging-deeper/processes.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index 37c664def..be57b2d62 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -137,4 +137,36 @@ facades.Process().Quietly().Run("...") facades.Process().DisableBuffering().OnOutput(func(typ process.OutputType, b []byte) { // ... }).Run("...") -``` \ No newline at end of file +``` + +### 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() +``` + +#### 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(key string, typ process.OutputType, line []byte) { + // 'key' will be "source" or "filter" +}).Run() +``` From 6c27f61833379778d6c27aa1c3e6b66c9dde0547 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 19:24:06 +0530 Subject: [PATCH 24/49] add warning in process Pipeline configuration --- en/digging-deeper/processes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index be57b2d62..742a2d3ca 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -154,6 +154,19 @@ result, err := facades.Process().Pipe(func(pipe process.Pipe) { }).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, From f1d9c16f720e1a9c0a16cfef1342e810c28cd8d9 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 23:02:25 +0530 Subject: [PATCH 25/49] add doc for async processes --- en/digging-deeper/processes.md | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index 742a2d3ca..c616e8c51 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -179,7 +179,93 @@ using the `As` method, which is highly useful for debugging complex pipelines. facades.Process().Pipe(func(pipe process.Pipe) { pipe.Command("cat", "access.log").As("source") pipe.Command("grep", "error").As("filter") -}).OnOutput(func(key string, typ process.OutputType, line []byte) { +}).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. +::: From 456e78fe10d5d896e1f0f43471c72ce481253825 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Sun, 11 Jan 2026 23:30:09 +0530 Subject: [PATCH 26/49] add doc for concurrent processes --- .vitepress/config/en.ts | 4 + en/digging-deeper/processes.md | 132 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 5f8f7056a..5e759c830 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -280,6 +280,10 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { text: 'Color Output', link: 'color' }, + { + text: 'Processes', + link: 'processes' + }, { text: 'Strings', link: 'strings' diff --git a/en/digging-deeper/processes.md b/en/digging-deeper/processes.md index c616e8c51..b917ace20 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -269,3 +269,135 @@ if process.Running() { 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() +``` From a4a71fd4ccb36d13c7227532f2b4e22af3a12e6a Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Mon, 12 Jan 2026 00:10:58 +0530 Subject: [PATCH 27/49] add docs for pluralization --- .vitepress/config/en.ts | 8 +++ en/digging-deeper/pluralization.md | 110 +++++++++++++++++++++++++++++ en/digging-deeper/strings.md | 36 ++++++++++ en/digging-deeper/telemetry.md | 5 ++ 4 files changed, 159 insertions(+) create mode 100644 en/digging-deeper/pluralization.md create mode 100644 en/digging-deeper/telemetry.md diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 5e759c830..eb7d532a9 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -295,6 +295,14 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { { text: 'HTTP Client', link: 'http-client' + }, + { + text: 'Pluralization', + link: 'pluralization' + }, + { + text: 'Telemetry', + link: 'telemetry' } ] } diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md new file mode 100644 index 000000000..9f2409db5 --- /dev/null +++ b/en/digging-deeper/pluralization.md @@ -0,0 +1,110 @@ +# 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" + +// Register that "mouse" becomes "mice" +pluralizer.RegisterIrregular("english", pluralizer.Substitution{ + Singular: "mouse", + Plural: "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") +} +``` \ No newline at end of file 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/telemetry.md b/en/digging-deeper/telemetry.md new file mode 100644 index 000000000..537fbfe49 --- /dev/null +++ b/en/digging-deeper/telemetry.md @@ -0,0 +1,5 @@ +# Telemetry + +[[toc]] + +## Introduction From 89bd81fa97d6d272a7fae5f9992c074643153a77 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Mon, 12 Jan 2026 00:18:25 +0530 Subject: [PATCH 28/49] add pluralization supported languages --- en/digging-deeper/pluralization.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md index 9f2409db5..b4eb98f9b 100644 --- a/en/digging-deeper/pluralization.md +++ b/en/digging-deeper/pluralization.md @@ -107,4 +107,14 @@ func init() { // Set it as active _ = pluralizer.UseLanguage("my_custom_language") } -``` \ No newline at end of file +``` + +## 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 From 59a4d3c0d4db14a22ee0d7457981c83a27d03abe Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 14 Jan 2026 15:11:23 +0800 Subject: [PATCH 29/49] mail template --- en/digging-deeper/mail.md | 67 +++++++++++++++++++++++++++++++++ en/upgrade/v1.17.md | 78 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 en/upgrade/v1.17.md 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/upgrade/v1.17.md b/en/upgrade/v1.17.md new file mode 100644 index 000000000..2cb2d9319 --- /dev/null +++ b/en/upgrade/v1.17.md @@ -0,0 +1,78 @@ +# Upgrading To v1.17 From v1.16 + +## Exciting New Features πŸŽ‰ + +- [Add facades.DB()](#add-facades-db) + +## Enhancements πŸš€ + +- [Mail can be sent via template](#mail-can-be-sent-via-template) + +## Breaking Changes πŸ›  + +- [Remove SQL Migration](#remove-sql-migration) + +## 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#68](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. 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"), + }, + }, +}, +``` + +## Feature Introduction + +### 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) From bbfb00bf4e7eeede226c0f4cf02a161eaf8e790f Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 14 Jan 2026 18:23:41 +0800 Subject: [PATCH 30/49] optimize --- en/architecture-concepts/service-providers.md | 8 ++ en/database/migrations.md | 17 +++ en/database/queries.md | 30 ++++- en/orm/getting-started.md | 34 +++++- en/the-basics/views.md | 8 ++ en/upgrade/v1.17.md | 105 +++++++++++++++++- 6 files changed, 194 insertions(+), 8 deletions(-) diff --git a/en/architecture-concepts/service-providers.md b/en/architecture-concepts/service-providers.md index 4bcd2e996..b8b6f0269 100644 --- a/en/architecture-concepts/service-providers.md +++ b/en/architecture-concepts/service-providers.md @@ -38,3 +38,11 @@ func (r *ServiceProvider) Register(app foundation.Application) {} func (r *ServiceProvider) Boot(app foundation.Application) {} ``` + +## Create Service Provider + +You can use the following command to create a new service provider: + +```bash +./artisan make:provider YourServiceProviderName +``` diff --git a/en/database/migrations.md b/en/database/migrations.md index 7abb7831b..79d62f0b9 100644 --- a/en/database/migrations.md +++ b/en/database/migrations.md @@ -22,11 +22,28 @@ The database migration files are stored in the `database/migrations` directory. Use the `make:migration` command to create the migration: ```shell +go run . artisan make:migration go run . artisan make:migration create_users_table ``` This command will generate migration files in the `database/migrations` directory. Each migration file will begin with a timestamp, which Goravel will use to determine the execution order of the migration files. +You can also create a migration for a specific model by using the `-m` or `--model` option: + +```shell +go run . artisan make:migration create_users_table -m User +``` + +The model should be registered in the `bootstrap/app.go` file before running the command. This command will generate a migration file based on the structure defined in the `User` model. + +```go +WithCallback(func() { + facades.Schema().Extend(schema.Extension{ + Models: []any{models.User{}}, + }) +}). +``` + ### Quickly Create Use `create_users_table` to automatically generate a table containing the infrastructure of `users`: diff --git a/en/database/queries.md b/en/database/queries.md index ebf8ed54f..5f3d10891 100644 --- a/en/database/queries.md +++ b/en/database/queries.md @@ -149,12 +149,22 @@ for row := range rows { ### Aggregates -The query builder provides aggregate methods: `Count` `Sum`. +The query builder provides aggregate methods: `Count`, `Sum`, `Avg`, `Min`, `Max`. ```go count, err := facades.DB().Table("users").Count() -sum, err := facades.DB().Table("users").Sum("age") +var sum int +err := facades.DB().Table("users").Sum("age", &sum) + +var avg float64 +err := facades.DB().Table("users").Avg("age", &avg) + +var min int +err := facades.DB().Table("users").Min("age", &min) + +var max int +err := facades.DB().Table("users").Max("age", &max) ``` ### Checking if a Record Exists @@ -345,6 +355,22 @@ err := facades.DB().Table("users").Where("name", "John").WhereExists(func() db.Q }).Get(&users) ``` +### WhereAll / WhereAny / WhereNone + +```go +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 > ?) +``` + ### Other Where Clauses **WhereBetween / OrWhereBetween** diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md index 31284217c..de60e8e51 100644 --- a/en/orm/getting-started.md +++ b/en/orm/getting-started.md @@ -205,6 +205,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 +230,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 +258,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 +445,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 @@ -905,10 +923,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 diff --git a/en/the-basics/views.md b/en/the-basics/views.md index c0ccea931..38be85c63 100644 --- a/en/the-basics/views.md +++ b/en/the-basics/views.md @@ -101,6 +101,14 @@ func (receiver *AppServiceProvider) Boot(app foundation.Application) { } ``` +## CSRF Token Middleware + +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 You can register custom Delims and functions to be used within your views, they can be registered in the configuration `http.drivers.*.template`. diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index 2cb2d9319..19f81de8b 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -2,21 +2,34 @@ ## Exciting New Features πŸŽ‰ -- [Add facades.DB()](#add-facades-db) +- [Simplified code structure](#simplified-code-structure) +- [Process Management](#process-management) +- [Pluralizer package](#pluralizer-package) +- [TODO Install facades](#pluralizer-package) +- [TODO Goravel Lite (optimize service-providers.md)](#pluralizer-package) +- [TODO Telemetry module](#pluralizer-package) ## Enhancements πŸš€ - [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) +- [TODO Runners](#pluralizer-package) ## Breaking Changes πŸ›  -- [Remove SQL Migration](#remove-sql-migration) +- [Remove machinery queue driver](#remove-machinery-queue-driver) ## 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#68](https://github.com/goravel/example/pull/93). +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 @@ -69,10 +82,96 @@ Add new configuration in `config/mail.go`: }, ``` +### 3. If you are using machinery queue driver + +Need to modify accordingly: [Remove machinery queue driver](#remove-machinery-queue-driver) + ## Feature Introduction +### 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. +2. Move configurations in `*_service_providers.go` files to the `bootstrap` folder. +3. Move all facades from framework intenal to `app/facades` folder. +4. Simplify the facades startup process, no need to run facades in `main.go` anymore. + +### Process Management + +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) + ### 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) + +### 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. From 25008aab2f54e9bd02561b2531fa820728b6d2b3 Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 14 Jan 2026 21:55:16 +0800 Subject: [PATCH 31/49] add some features --- en/digging-deeper/http-client.md | 38 +++---- en/orm/getting-started.md | 18 +++- en/the-basics/logging.md | 47 +-------- en/the-basics/validation.md | 19 ++-- en/upgrade/v1.17.md | 169 ++++++++++++++++++++++++++++++- 5 files changed, 211 insertions(+), 80 deletions(-) diff --git a/en/digging-deeper/http-client.md b/en/digging-deeper/http-client.md index d3d0b84cc..332a44dfc 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 @@ -47,6 +27,12 @@ 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: @@ -264,7 +250,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 +260,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) } ``` diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md index de60e8e51..3b8ea98c2 100644 --- a/en/orm/getting-started.md +++ b/en/orm/getting-started.md @@ -172,7 +172,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 +182,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 | 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/validation.md b/en/the-basics/validation.md index ec6220fb0..1162a90dc 100644 --- a/en/the-basics/validation.md +++ b/en/the-basics/validation.md @@ -230,6 +230,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 +256,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 +266,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 +276,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 +292,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 +316,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 +335,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") ``` @@ -466,12 +467,12 @@ 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." } @@ -566,7 +567,7 @@ 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) } diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index 19f81de8b..53e9e3f2b 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -19,11 +19,19 @@ - [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) - [TODO Runners](#pluralizer-package) +- [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 @@ -60,7 +68,33 @@ go get github.com/goravel/minio@latest go mod tidy ``` -### 2. If you want to send mail via template +### 2. Modify Http Client Configuration + +Add new configuration in `config/http.go`: + +```go +-- "client": map[string]any{ +-- "base_url": config.GetString("HTTP_CLIENT_BASE_URL"), +-- "timeout": config.GetDuration("HTTP_CLIENT_TIMEOUT"), +-- "max_idle_conns": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS"), +-- "max_idle_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST"), +-- "max_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_CONN_PER_HOST"), +-- "idle_conn_timeout": config.GetDuration("HTTP_CLIENT_IDLE_CONN_TIMEOUT"), +-- }, +++ "default_client": config.Env("HTTP_CLIENT_DEFAULT", "default"), +++ "clients": map[string]any{ +++ "default": map[string]any{ +++ "base_url": config.Env("HTTP_CLIENT_BASE_URL", ""), +++ "timeout": config.Env("HTTP_CLIENT_TIMEOUT", "30s"), +++ "max_idle_conns": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS", 100), +++ "max_idle_conns_per_host": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST", 2), +++ "max_conns_per_host": config.Env("HTTP_CLIENT_MAX_CONN_PER_HOST", 0), +++ "idle_conn_timeout": config.Env("HTTP_CLIENT_IDLE_CONN_TIMEOUT", "90s"), +++ }, +++ }, +``` + +### 3. If you want to send mail via template Add new configuration in `config/mail.go`: @@ -82,10 +116,26 @@ Add new configuration in `config/mail.go`: }, ``` -### 3. If you are using machinery queue driver +### 4. If you are using machinery queue driver Need to modify accordingly: [Remove machinery queue driver](#remove-machinery-queue-driver) +### 5. If you created a custom log driver + +Need to modify accordingly: [Switch log driver](#switch-log-driver) + +### 6. If you are using Http.Request.Bind function + +Need to modify accordingly: [Remove Http.Request.Bind function](#remove-http-request-bind-function) + +### 7. If you are using validation.Make function or custom rules/filters + +Need to modify accordingly: [Validation supports pass context](#validation-supports-pass-context) + +### 8. If you are using global scopes in Orm + +Need to modify accordingly: [Add WithoutGlobalScopes function for Orm](#add-withoutglobalscopes-function-for-orm) + ## Feature Introduction ### Simplified code structure @@ -109,6 +159,73 @@ Goravel now includes a pluralizer package that helps with converting words betwe [View Document](../digging-deeper/pluralization.md) +### 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. + +### 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) +++ validator, err := facades.Validation().Make(ctx, input, rules) +``` + +Custom rules and filters also support context parameter: + +```go +type Rule interface { + Signature() string +-- Passes(data Data, val any, options ...any) bool +-- Message() string +++ Passes(ctx context.Context, data Data, val any, options ...any) bool +++ Message(ctx context.Context) string +} + +type Filter interface { + Signature() string +-- Handle() any +++ Handle(ctx context.Context) any +} +``` + +### 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 { +++ 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") + }, + } +} +``` + ### 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. @@ -172,6 +289,54 @@ The `make:migration` command has been enhanced to support creating migrations ba [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) +++ Handle(channel string) (Handler, error) +} +``` + +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") +++ response, err := facades.Http().Get("https://github.com") +++ err = response.Bind(&user) +``` From aa124c719ce8f7d8cc2ebc3f88a3e26bdf21d75c Mon Sep 17 00:00:00 2001 From: Bowen Date: Wed, 14 Jan 2026 22:18:30 +0800 Subject: [PATCH 32/49] optimize --- en/upgrade/v1.17.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index 53e9e3f2b..61f9e22f5 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -3,11 +3,11 @@ ## Exciting New Features πŸŽ‰ - [Simplified code structure](#simplified-code-structure) -- [Process Management](#process-management) -- [Pluralizer package](#pluralizer-package) +- [Process Facade](#process-facade) +- [Telemetry Facade](#telemetry-facade) +- [Pluralizer Package](#pluralizer-package) - [TODO Install facades](#pluralizer-package) - [TODO Goravel Lite (optimize service-providers.md)](#pluralizer-package) -- [TODO Telemetry module](#pluralizer-package) ## Enhancements πŸš€ @@ -20,11 +20,11 @@ - [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) -- [TODO Runners](#pluralizer-package) - [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) +- [TODO Runners](#pluralizer-package) ## Breaking Changes πŸ›  @@ -147,12 +147,18 @@ The code structure has been simplified to improve maintainability and readabilit 3. Move all facades from framework intenal to `app/facades` folder. 4. Simplify the facades startup process, no need to run facades in `main.go` anymore. -### Process Management +### 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) +### Telemetry Facade + +Goravel now includes a Telemetry Facade that provides a unified interface for collecting and exporting telemetry data, such as metrics and traces, from your application. This helps you monitor the performance and health of your application effectively. + +[View Document](../digging-deeper/telemetry.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. @@ -163,6 +169,19 @@ Goravel now includes a pluralizer package that helps with converting words betwe 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, +++ "formatter": "json",// the value can be "text" or "json" + }, +}, +``` + ### 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. From bf3e21347aec2733348f99777c0173a44f6d8a4c Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 15 Jan 2026 17:55:46 +0800 Subject: [PATCH 33/49] optimize --- en/architecture-concepts/facades.md | 81 ++++++------- en/architecture-concepts/request-lifecycle.md | 12 +- en/architecture-concepts/service-container.md | 2 +- en/architecture-concepts/service-providers.md | 103 +++++++++++++++-- en/database/getting-started.md | 2 +- en/digging-deeper/artisan-console.md | 22 ++-- en/getting-started/directory-structure.md | 106 ++++++++---------- en/prologue/contributions.md | 47 ++++---- en/upgrade/v1.17.md | 39 +++++-- 9 files changed, 258 insertions(+), 156 deletions(-) diff --git a/en/architecture-concepts/facades.md b/en/architecture-concepts/facades.md index bd2bdd56d..71c3ae6a6 100644 --- a/en/architecture-concepts/facades.md +++ b/en/architecture-concepts/facades.md @@ -4,58 +4,63 @@ ## Introduction -`facades` provide a "static" interface for the core functionality of the application and provide a more flexible, more elegant, and easy-to-test syntax. - -All `facades` of Goravel are defined under `github.com/goravel/framework/facades`. We can easily use `facades`: +`facades` provide a "static" interface for the core functionality of the application and provide a more flexible, more elegant, and easy-to-test syntax. All `facades` of Goravel are defined under the `app/facades` folder: ```go -import "github.com/goravel/framework/facades" +import "app/facades" -facades.Route().Run(facades.Config().GetString("app.host")) +facades.Config().GetString("app.host") ``` ## How Facades Work -`facades` are generally instantiated in the `Register` or `Boot` stage of each module `ServerProvider`. +Each service provider registers its corresponding bindings in the service container, then the service container providers vairous `Make*` functions to build the binding instances. The `facades` in the `app/facades` folder call these `Make*` functions to get the instances from the service container. Let's use the `Route` facade as an example: + +1. The `Route` service provider registers the `binding.Route` binding in the service container: ```go -func (config *ServiceProvider) Register() { - app := Application{} - facades.Config = app.Init() +type ServiceProvider struct {} + +func (r *ServiceProvider) Register(app foundation.Application) { + app.Singleton(binding.Route, func(app foundation.Application) (any, error) { + return NewRoute(app.MakeConfig()) + }) } + +func (r *ServiceProvider) Boot(app foundation.Application) {} ``` -If the `facades` use other `facades`, then instantiate them in the `Boot` phase of the `ServerProvider`: +2. The `Route` facade calls the `MakeRoute()` function to get the `Route` instance from the service container: ```go -func (database *ServiceProvider) Boot() { - app := Application{} - facades.DB = app.Init() +// app/facades/route.go +package facades + +import ( + "github.com/goravel/framework/contracts/route" +) + +func Route() route.Route { + return App().MakeRoute() } ``` -## Facade Class Reference - -| Facade | Document | -| ----------- | ---------------------------------------------------------- | -| App | [Container](../architecture-concepts/service-container.md) | -| Artisan | [Command Console](../digging-deeper/artisan-console.md) | -| Auth | [Authentication](../security/authentication.md) | -| Cache | [Cache](../digging-deeper/cache.md) | -| Config | [Configuration](../getting-started/configuration.md) | -| Crypt | [Encryption](../security/encryption.md) | -| Event | [Event](../digging-deeper/event.md) | -| Gate | [Authorization](../security/authorization.md) | -| Grpc | [Grpc](../the-basics/grpc.md) | -| Hash | [Hashing](../security/hashing.md) | -| Log | [Log](../the-basics/logging.md) | -| Mail | [Mail](../digging-deeper/mail.md) | -| Orm | [ORM](../orm/getting-started.md) | -| Queue | [Queue](../digging-deeper/queues.md) | -| RateLimiter | [RateLimiter](../the-basics/routing.md) | -| Route | [Route](../the-basics/routing.md) | -| Seeder | [Seeder](../database/seeding.md) | -| Schedule | [Schedule](../digging-deeper/task-scheduling.md) | -| Storage | [Storage](../digging-deeper/task-scheduling.md) | -| Testing | [Testing](../testing/getting-started.md) | -| Validation | [Validation](../digging-deeper/task-scheduling.md) | +> Given that the `facades` is exposed to the application, you can also create your own `facades` or override the existing `facades` in the `app/facades` folder. + +## Install/Uninstall Facades + +[goravel/goravel](https://github.com/goravel/goravel) installs all `facades` by default and [goravel/goravel-lite](https://github.com/goravel/goravel-lite) only installs essential `facades` like `Artisan`, `Config`. You can install or uninstall other `facades` as needed via the `package:install` and `package:uninstall` commands. + +```shell +# Install a specific facade +./artisan package:install Route + +# Install all facades +./artisan package:install --all + +# Install all facades with default drivers +./artisan package:install --all --default + +# Uninstall a specific facade +./artisan package:uninstall Route +``` diff --git a/en/architecture-concepts/request-lifecycle.md b/en/architecture-concepts/request-lifecycle.md index 73f7636db..fd1781f33 100644 --- a/en/architecture-concepts/request-lifecycle.md +++ b/en/architecture-concepts/request-lifecycle.md @@ -4,10 +4,14 @@ ## Introduction -The `main.go` file serves as the entry point for all requests in the Goravel application. It utilizes the `bootstrap.Boot()` function to initialize the framework. +Using any tool in the real world feels more intuitive when you know how it works. This document aims to give you a clear, high-level look at how Goravel functions. Don’t worry if you don’t get every term right awayβ€”just aim for a basic sense of how things work, and your expertise will grow as you explore the rest of the docs. -Then a Goravel instance is created by `app := foundation.NewApplication()` in `bootstrap/app.go`. +## Lifecycle Overview -After this, use `app.Boot()` to load the [Service Provider](service-providers.md) registered, and `config.Boot()` to load the configuration files under the config directory. +1. The `main.go` is the entry point of the application, it will call the `bootstrap.Boot()` function to initialize the framework, and use `app.Wait()` to keep the application running. -Finally, start the HTTP server by using `facades.Route().Run(facades.Config().GetString("app.host"))` in `main.go`. +2. The `bootstrap.Boot()` function will initialize a new Goravel application instance by calling `foundation.Setup()`, you can set service providers, routes, and other settings like migrations, schedules via `With*` functions here. Finally, call the `Start()` method to boot the application. + +3. In the `Start()` method, it will first load configuration, then register all service providers and other settings. Finally, boot all service providers, return the application instance. + +4. After the application is started, the http or grpc server will be started automatically if you have configured them. They are controled by [the runners of service providers](./service-providers.md#runners). You can normally use all facades in this stage, but remember that your customize code should be placed before `app.Wait()` in the `main.go` file. diff --git a/en/architecture-concepts/service-container.md b/en/architecture-concepts/service-container.md index e463445e9..78d13c57e 100644 --- a/en/architecture-concepts/service-container.md +++ b/en/architecture-concepts/service-container.md @@ -97,4 +97,4 @@ instance, err := app.MakeWith(key, map[string]any{"id": 1}) ### Other Methods -The framework provides some convenient methods to quickly resolve various facades: `MakeArtisan`, `MakeAuth`, `MakeCache`, `MakeConfig`, `MakeCrypt`, `MakeEvent`, `MakeGate`, `MakeGrpc`, `MakeHash`, `MakeLog`, `MakeMail`, `MakeOrm`, `MakeQueue`, `MakeRateLimiter`, `MakeRoute`, `MakeSchedule`, `MakeStorage`, `MakeValidation`. +The framework provides some convenient methods to quickly resolve various facades: `MakeArtisan`, `MakeAuth`, etc. diff --git a/en/architecture-concepts/service-providers.md b/en/architecture-concepts/service-providers.md index b8b6f0269..080fae8a5 100644 --- a/en/architecture-concepts/service-providers.md +++ b/en/architecture-concepts/service-providers.md @@ -4,26 +4,33 @@ ## Introduction -The most important thing in the kernel boot operation is to load the `ServiceProvider`. All `ServiceProvider` under the application are configured in the `providers` array in `config/app.go`. +The most important thing in the kernel boot operation is to load the `ServiceProvider`. All `ServiceProvider` under the application are configured in the `bootstrap/providers.go` file. First, the kernel will call the `Register` method of all service providers. After all service providers have been registered, the kernel will call the `Boot` method of all `ServiceProvider` again. The `ServiceProvider` is the key to the life cycle of Goravel. They enable the framework to contain various components, such as routing, database, queue, cache, etc. -You can also customize your own provider, it can be stored under `app/providers` and registered in the `providers` array in `config/app.go`. +## Create Service Provider + +Service providers contain a `Register` and a `Boot` method. Within the `Register` method, you should only bind things into [the service container](./service-container.md). You should never attempt to register any event listeners, routes, or any other piece of functionality within the `Register` method. + +The Artisan CLI can generate a new provider via the `make:provider` command. Goravel will automatically register your new provider in your application's `bootstrap/providers.go` file: + +```bash +./artisan make:provider YourServiceProviderName +``` -The framework comes with a blank service provider `app/providers/app_service_provider.go` where you can implement simple boot logic. In bigger projects, you have the option to create new service providers for more precise control. +## Dependency Relationship -ServiceProvider provides an optional method `Relationship() binding.Relationship`, used to declare the dependency relationship of the current ServicerProvider, the ServiceProvider that sets this method will not depend on the registration order, and the ServiceProvider that does not set it will be registered last, for example: +`ServiceProvider` provides an optional method `Relationship() binding.Relationship`, used to declare the dependency relationship, the `ServiceProvider` that sets this method will not depend on the registration order, and the `ServiceProvider` that does not set it will be registered last, for example: ```go -type ServiceProvider struct { -} +type ServiceProvider struct {} func (r *ServiceProvider) Relationship() binding.Relationship { return binding.Relationship{ Bindings: []string{ - BindingSession, + "custom", }, Dependencies: []string{ binding.Config, @@ -34,15 +41,89 @@ func (r *ServiceProvider) Relationship() binding.Relationship { } } +func (r *ServiceProvider) Register(app foundation.Application) { + app.Singleton("custom", func(app foundation.Application) (any, error) { + return New() + }) +} + +func (r *ServiceProvider) Boot(app foundation.Application) {} +``` + +## Runners + +The `ServiceProvider` can also implement the `Runners` interface to run some code when the application starts. It's usually used to start/shutdown the service which is defined in the `ServiceProvider`. For example: `Route`, `Schedule`, `Queue`, etc. You don't need to start/shutdown these services with `Runners` in the `main.go` anymore, Goravel will take care of it. + +A `Runner` contains three methods: `ShouldRun()`, `Run()`, and `Shutdown()`. + +```go +type Runner interface { + // ShouldRun determines whether the runner should be executed. + ShouldRun() bool + // Run starts the runner. + Run() error + // Shutdown gracefully stops the runner. + Shutdown() error +} +``` + +There is an example of a `RouteRunner` defined in the `ServiceProvider` to start and shutdown the `Route` service. + +```go +type ServiceProvider struct {} + func (r *ServiceProvider) Register(app foundation.Application) {} func (r *ServiceProvider) Boot(app foundation.Application) {} + +func (r *ServiceProvider) Runners(app foundation.Application) []foundation.Runner { + return []foundation.Runner{NewRouteRunner(app.MakeConfig(), app.MakeRoute())} +} ``` -## Create Service Provider +```go +package route -You can use the following command to create a new service provider: +import ( + "github.com/goravel/framework/contracts/config" + "github.com/goravel/framework/contracts/route" +) -```bash -./artisan make:provider YourServiceProviderName +type RouteRunner struct { + config config.Config + route route.Route +} + +func NewRouteRunner(config config.Config, route route.Route) *RouteRunner { + return &RouteRunner{ + config: config, + route: route, + } +} + +func (r *RouteRunner) ShouldRun() bool { + return r.route != nil && r.config.GetString("http.default") != "" +} + +func (r *RouteRunner) Run() error { + return r.route.Run() +} + +func (r *RouteRunner) Shutdown() error { + return r.route.Shutdown() +} +``` + +You can also register a single `Runner` in `bootstrap/app.go` to run some code when the application starts. + +```go +func Boot() contractsfoundation.Application { + app := foundation.Setup(). + WithConfig(config.Boot). + Create() + + app.Start(YourRunner) + + return app +} ``` diff --git a/en/database/getting-started.md b/en/database/getting-started.md index 0f935f6dc..49a08b916 100644 --- a/en/database/getting-started.md +++ b/en/database/getting-started.md @@ -59,7 +59,7 @@ We have added two keys, `read` and `write`, in the database configuration. `192. ## Running Native SQL Queries -After configuring the database connection, you can use `facades.DB()` to run queries. `facades.DB` provides various methods for running queries: `Select`, `Insert`, `Update`, `Delete`, and `Statement`. +After configuring the database connection, you can use `facades.DB()` to run queries. `facades.DB()` provides various methods for running queries: `Select`, `Insert`, `Update`, `Delete`, and `Statement`. ### Select diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 817c584dd..277df770a 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -7,16 +7,16 @@ Artisan is the CLI tool that comes with Goravel for interacting with the command line. You can access it using `facades.Artisan()`. This tool has several useful commands that can assist you in the development of your application. Utilize the following command to view all available commands. ```shell -go run . artisan list +./artisan list # or -./artisan list +go run . artisan list ``` Each command also has a "help" feature that shows and explains the arguments and options associated with the command. To see the help screen, just add "help" before the command name. ```shell -go run . artisan help migrate +./artisan help migrate ``` Instead of repeating `go run . artisan ...` command, you may want to add an alias to your shell configuration with the terminal command below: @@ -28,7 +28,7 @@ echo -e "\r\nalias artisan=\"go run . artisan\"" >>~/.zshrc Then you can simply run your commands like this: ```shell -artisan make:controller DemoController +./artisan make:controller DemoController ``` You can also use `artisan` shell script like this: @@ -42,8 +42,8 @@ You can also use `artisan` shell script like this: You can use the `make:command` command to create a new command in the `app/console/commands` directory. Don't worry if this directory does not exist in your application, it will be created the first time you run the `make:command` command: ```shell -go run . artisan make:command SendEmails -go run . artisan make:command user/SendEmails +./artisan make:command SendEmails +./artisan make:command user/SendEmails ``` ### Command Structure @@ -93,7 +93,7 @@ When you write console commands, it's typical to collect user input through `arg Follow the arguments after the command: ```shell -go run . artisan send:emails SUBJECT EMAIL_1 EMAIL_2 +./artisan send:emails SUBJECT EMAIL_1 EMAIL_2 ``` Definition: @@ -198,8 +198,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`. @@ -495,7 +495,7 @@ 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. @@ -534,5 +534,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/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/prologue/contributions.md b/en/prologue/contributions.md index c185976b4..0c027d760 100644 --- a/en/prologue/contributions.md +++ b/en/prologue/contributions.md @@ -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/upgrade/v1.17.md b/en/upgrade/v1.17.md index 61f9e22f5..b30e2acc1 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -2,15 +2,16 @@ ## Exciting New Features πŸŽ‰ -- [Simplified code structure](#simplified-code-structure) +- [Goravel Lite](#goravel-lite) +- [Simplified Code Structure](#simplified-code-structure) - [Process Facade](#process-facade) - [Telemetry Facade](#telemetry-facade) - [Pluralizer Package](#pluralizer-package) -- [TODO Install facades](#pluralizer-package) -- [TODO Goravel Lite (optimize service-providers.md)](#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) @@ -24,7 +25,6 @@ - [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) -- [TODO Runners](#pluralizer-package) ## Breaking Changes πŸ›  @@ -138,14 +138,23 @@ Need to modify accordingly: [Add WithoutGlobalScopes function for Orm](#add-with ## 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. -2. Move configurations in `*_service_providers.go` files to the `bootstrap` folder. -3. Move all facades from framework intenal to `app/facades` folder. -4. Simplify the facades startup process, no need to run facades in `main.go` anymore. +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 @@ -165,6 +174,14 @@ Goravel now includes a pluralizer package that helps with converting words betwe [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. @@ -245,6 +262,12 @@ type User struct { } ``` +### 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. From e09aa368ad5ec25498c854ce3ff0af2056cb9b81 Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 15 Jan 2026 18:54:29 +0800 Subject: [PATCH 34/49] optimize --- en/architecture-concepts/request-lifecycle.md | 13 +- en/digging-deeper/artisan-console.md | 29 +++-- en/digging-deeper/event.md | 46 +++---- en/digging-deeper/queues.md | 29 ++--- en/getting-started/configuration.md | 15 ++- en/the-basics/grpc.md | 92 +++++--------- en/the-basics/middleware.md | 40 ++++--- en/the-basics/request.md | 29 +++-- en/the-basics/session.md | 4 +- en/the-basics/validation.md | 112 ++++++------------ 10 files changed, 185 insertions(+), 224 deletions(-) diff --git a/en/architecture-concepts/request-lifecycle.md b/en/architecture-concepts/request-lifecycle.md index fd1781f33..8b4884726 100644 --- a/en/architecture-concepts/request-lifecycle.md +++ b/en/architecture-concepts/request-lifecycle.md @@ -14,4 +14,15 @@ Using any tool in the real world feels more intuitive when you know how it works 3. In the `Start()` method, it will first load configuration, then register all service providers and other settings. Finally, boot all service providers, return the application instance. -4. After the application is started, the http or grpc server will be started automatically if you have configured them. They are controled by [the runners of service providers](./service-providers.md#runners). You can normally use all facades in this stage, but remember that your customize code should be placed before `app.Wait()` in the `main.go` file. +4. After the application is started, the http or grpc server will be started automatically if you have configured them. They are controled by [the runners of service providers](./service-providers.md#runners). You can normally use all facades in this stage, but remember that your customize code should be placed before `app.Wait()` in the `main.go` file. Or you can add your code into the `WithCallback` function in the `bootstrap/app.go` file to make sure your code is executed after the application is started. + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithConfig(config.Boot). + WithCallback(func() { + // Your custom code here, all facades are available here. + }). + Start() +} +``` diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 277df770a..97aff241d 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -13,13 +13,13 @@ Artisan is the CLI tool that comes with Goravel for interacting with the command go run . artisan list ``` -Each command also has a "help" feature that shows and explains the arguments and options associated with the command. To see the help screen, just add "help" before the command name. +Each command also has a "help" flag that shows and explains the arguments and options associated with the command: ```shell -./artisan help migrate +./artisan migrate --help ``` -Instead of repeating `go run . artisan ...` command, you may want to add an alias to your shell configuration with the terminal command below: +Instead of repeating `./artisan ...` command, you may want to add an alias to your shell configuration with the terminal command below: ```shell echo -e "\r\nalias artisan=\"go run . artisan\"" >>~/.zshrc @@ -28,15 +28,11 @@ echo -e "\r\nalias artisan=\"go run . artisan\"" >>~/.zshrc Then you can simply run your commands like this: ```shell -./artisan make:controller DemoController +artisan make:controller DemoController ``` You can also use `artisan` shell script like this: -```shell -./artisan make:controller DemoController -``` - ### Generating Commands You can use the `make:command` command to create a new command in the `app/console/commands` directory. Don't worry if this directory does not exist in your application, it will be created the first time you run the `make:command` command: @@ -46,6 +42,21 @@ You can use the `make:command` command to create a new command in the `app/conso ./artisan make:command user/SendEmails ``` +### Register Commands + +All commands should be registered via the `WithCommands` function in the `bootstrap/app.go` file: + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithCommands(Commands). + WithConfig(config.Boot). + Start() +} +``` + +A new command created by `make:command` will be register automatically in the `bootstrap/commands.go::Commands()` function and the function will be called by `WithCommands`. You need register the command manually if you create the command file by yourself. + ### Command Structure After generating your command, assign suitable values to the signature and description properties of the struct. The `Handle` method will be called when your command is executed. You need to implement your logic in this method. @@ -506,7 +517,7 @@ 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`. diff --git a/en/digging-deeper/event.md b/en/digging-deeper/event.md index c93a64c43..f0e72b5d2 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 diff --git a/en/digging-deeper/queues.md b/en/digging-deeper/queues.md index 24a6d7a4a..fc97c5758 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 register 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,18 +114,6 @@ 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. 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/the-basics/grpc.md b/en/the-basics/grpc.md index 0fa036298..c66149bb4 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 @@ -61,43 +60,22 @@ 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`. +The interceptor can be defined in the `app/grpc/interceptors` folder. **Server Interceptor** @@ -169,34 +147,26 @@ func init() { } ``` -## Shutdown Grpc +### Register Interceptors -You can call the `Shutdown` method to gracefully shut down Grpc, which will wait for all requests to be processed before shutting down. +Register interceptors in the `WithGrpcServerInterceptors` and `WithGrpcClientInterceptors` functions of the `bootstrap/app.go` file after interceptors were defined. ```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 {} +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithGrpcServerInterceptors(func() []grpc.UnaryServerInterceptor { + return []grpc.UnaryServerInterceptor{ + interceptors.TestServer, + } + }). + WithGrpcClientInterceptors(func() map[string][]grpc.UnaryClientInterceptor { + return map[string][]grpc.UnaryClientInterceptor{ + "default": { + interceptors.TestClient, + }, + } + }). + WithConfig(config.Boot). + Start() +} ``` 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..ec4bbba3c 100644 --- a/en/the-basics/request.md +++ b/en/the-basics/request.md @@ -213,18 +213,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/session.md b/en/the-basics/session.md index bb029d870..10f096ef9 100644 --- a/en/the-basics/session.md +++ b/en/the-basics/session.md @@ -180,7 +180,7 @@ 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 +202,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 1162a90dc..9d2d06be7 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. @@ -440,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 @@ -475,38 +469,18 @@ func (receiver *Uppercase) Passes(ctx context.Context, data validation.Data, val 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 register 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() } ``` @@ -538,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 @@ -574,34 +553,15 @@ func (receiver *ToInt) Handle(ctx context.Context) any { } ``` -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 register 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() } ``` From e32480a58784f6e937aa2448c0037d89965b58b5 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 16 Jan 2026 17:55:16 +0800 Subject: [PATCH 35/49] optimize --- en/architecture-concepts/request-lifecycle.md | 12 +- en/architecture-concepts/service-container.md | 21 ++-- en/architecture-concepts/service-providers.md | 13 ++- en/database/getting-started.md | 12 +- en/database/migrations.md | 42 ++++--- en/database/queries.md | 56 +++++----- en/database/seeding.md | 34 +++--- en/digging-deeper/artisan-console.md | 6 +- en/digging-deeper/event.md | 8 +- en/digging-deeper/filesystem.md | 5 +- en/digging-deeper/http-client.md | 21 ++-- en/digging-deeper/localization.md | 18 +-- en/digging-deeper/package-development.md | 74 ++++++------- en/digging-deeper/processes.md | 3 +- en/digging-deeper/queues.md | 68 +++--------- en/digging-deeper/task-scheduling.md | 103 +++--------------- en/getting-started/compile.md | 40 ++++--- en/getting-started/installation.md | 14 +-- en/getting-started/packages.md | 1 - en/orm/getting-started.md | 14 +-- en/security/authorization.md | 5 +- en/testing/getting-started.md | 3 +- en/testing/mock.md | 24 ++-- en/the-basics/controllers.md | 6 +- en/the-basics/grpc.md | 4 +- en/the-basics/routing.md | 98 +++-------------- en/the-basics/session.md | 2 - en/the-basics/validation.md | 4 +- en/the-basics/views.md | 3 +- 29 files changed, 274 insertions(+), 440 deletions(-) diff --git a/en/architecture-concepts/request-lifecycle.md b/en/architecture-concepts/request-lifecycle.md index 8b4884726..b078a546b 100644 --- a/en/architecture-concepts/request-lifecycle.md +++ b/en/architecture-concepts/request-lifecycle.md @@ -18,11 +18,11 @@ Using any tool in the real world feels more intuitive when you know how it works ```go func Boot() contractsfoundation.Application { - return foundation.Setup(). - WithConfig(config.Boot). - WithCallback(func() { - // Your custom code here, all facades are available here. - }). - Start() + return foundation.Setup(). + WithConfig(config.Boot). + WithCallback(func() { + // Your custom code here, all facades are available here. + }). + Start() } ``` diff --git a/en/architecture-concepts/service-container.md b/en/architecture-concepts/service-container.md index 78d13c57e..8bbed269d 100644 --- a/en/architecture-concepts/service-container.md +++ b/en/architecture-concepts/service-container.md @@ -16,30 +16,27 @@ Almost all of your service container bindings will be registered within [service package route import ( - "github.com/goravel/framework/contracts/foundation" + "github.com/goravel/framework/contracts/foundation" ) const Binding = "goravel.route" -type ServiceProvider struct { -} +type ServiceProvider struct {} func (route *ServiceProvider) Register(app foundation.Application) { - app.Bind(Binding, func(app foundation.Application) (any, error) { - return NewRoute(app.MakeConfig()), nil - }) + app.Bind(Binding, func(app foundation.Application) (any, error) { + return NewRoute(app.MakeConfig()), nil + }) } -func (route *ServiceProvider) Boot(app foundation.Application) { - -} +func (route *ServiceProvider) Boot(app foundation.Application) {} ``` As mentioned, you will typically be interacting with the container within service providers; however, if you would like to interact with the container outside of a service provider, you may do so via the `App` facade: ```go facades.App().Bind("key", func(app foundation.Application) (any, error) { - ... + ... }) ``` @@ -49,7 +46,7 @@ The `Singleton` method binds a class or interface into the container that should ```go app.Singleton(key, func(app foundation.Application) (any, error) { - return NewGin(app.MakeConfig()), nil + return NewGin(app.MakeConfig()), nil }) ``` @@ -67,7 +64,7 @@ If you need some extra parameters to construct the service provider, you can use ```go app.BindWith(Binding, func(app foundation.Application, parameters map[string]any) (any, error) { - return NewRoute(app.MakeConfig()), nil + return NewRoute(app.MakeConfig()), nil }) ``` diff --git a/en/architecture-concepts/service-providers.md b/en/architecture-concepts/service-providers.md index 080fae8a5..03f30d88a 100644 --- a/en/architecture-concepts/service-providers.md +++ b/en/architecture-concepts/service-providers.md @@ -14,12 +14,21 @@ The `ServiceProvider` is the key to the life cycle of Goravel. They enable the f Service providers contain a `Register` and a `Boot` method. Within the `Register` method, you should only bind things into [the service container](./service-container.md). You should never attempt to register any event listeners, routes, or any other piece of functionality within the `Register` method. -The Artisan CLI can generate a new provider via the `make:provider` command. Goravel will automatically register your new provider in your application's `bootstrap/providers.go` file: - ```bash ./artisan make:provider YourServiceProviderName ``` +The Artisan CLI can generate a new provider via the `make:provider` command. The new service provider will be registered automatically in the `bootstrap/providers.go::Providers()` function and the function will be called by `WithProviders`. + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithProvders(Providers). + WithConfig(config.Boot). + Start() +} +``` + ## Dependency Relationship `ServiceProvider` provides an optional method `Relationship() binding.Relationship`, used to declare the dependency relationship, the `ServiceProvider` that sets this method will not depend on the registration order, and the `ServiceProvider` that does not set it will be registered last, for example: diff --git a/en/database/getting-started.md b/en/database/getting-started.md index 49a08b916..898ed2c44 100644 --- a/en/database/getting-started.md +++ b/en/database/getting-started.md @@ -163,15 +163,15 @@ Goravel provides several Artisan commands to help you understand the structure o You can use the `db:show` command to view all tables in the database. ```bash -go run . artisan db:show -go run . artisan db:show --database=postgres +./artisan db:show +./artisan db:show --database=postgres ``` You can also use the `db:table` command to view the structure of a specific table. ```bash -go run . artisan db:table -go run . artisan db:table --database=postgres +./artisan db:table +./artisan db:table --database=postgres ``` ### Table Overview @@ -179,6 +179,6 @@ go run . artisan db:table --database=postgres If you want to get an overview of a single table in the database, you can execute the `db:table` Artisan command. This command provides an overview of a database table, including its columns, types, attributes, keys, and indexes: ```bash -go run . artisan db:table users -go run . artisan db:table users --database=postgres +./artisan db:table users +./artisan db:table users --database=postgres ``` diff --git a/en/database/migrations.md b/en/database/migrations.md index 79d62f0b9..da85e6d3f 100644 --- a/en/database/migrations.md +++ b/en/database/migrations.md @@ -22,8 +22,8 @@ The database migration files are stored in the `database/migrations` directory. Use the `make:migration` command to create the migration: ```shell -go run . artisan make:migration -go run . artisan make:migration create_users_table +./artisan make:migration +./artisan make:migration create_users_table ``` This command will generate migration files in the `database/migrations` directory. Each migration file will begin with a timestamp, which Goravel will use to determine the execution order of the migration files. @@ -31,7 +31,7 @@ This command will generate migration files in the `database/migrations` director You can also create a migration for a specific model by using the `-m` or `--model` option: ```shell -go run . artisan make:migration create_users_table -m User +./artisan make:migration create_users_table -m User ``` The model should be registered in the `bootstrap/app.go` file before running the command. This command will generate a migration file based on the structure defined in the `User` model. @@ -73,7 +73,8 @@ package migrations import ( "github.com/goravel/framework/contracts/database/schema" - "github.com/goravel/framework/facades" + + "goravel/app/facades" ) type M20241207095921CreateUsersTable struct { @@ -127,44 +128,55 @@ func (kernel Kernel) Migrations() []schema.Migration { } ``` +A new migration created by `make:migration` will be registered automatically in the `bootstrap/migrations.go::Migrations()` function and the function will be called by `WithMigrations`. You need register the rule manually if you create the rule file by yourself. + +```go +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithMigrations(Migrations). + WithConfig(config.Boot). + Start() +} +``` + ## Run Migrations To run all of your outstanding migrations, execute the `migrate` Artisan command: ```shell -go run . artisan migrate +./artisan migrate ``` If you would like to see which migrations have run thus far, you may use the `migrate:status` Artisan command: ```shell -go run . artisan migrate:status +./artisan migrate:status ``` ## Rolling Back Migrations -To roll back the latest migration, use the `rollback` Artisan command: +To roll back the latest migration batch, use the `rollback` Artisan command: ```shell -go run . artisan migrate:rollback +./artisan migrate:rollback ``` -If you want to roll back the last "batch" of migrations, which may include multiple migration files, you can specify the `batch` option, the number indicates which batch to roll back: +If you want to roll back multiple migration batches, you can specify the `batch` option, the number indicates which batch to roll back: ```shell -go run . artisan migrate:rollback --batch=2 +./artisan migrate:rollback --batch=2 ``` You may roll back a limited number of migrations by providing the `step` option to the `rollback` command. For example, the following command will roll back the last five migrations: ```shell -go run . artisan migrate:rollback --step=5 +./artisan migrate:rollback --step=5 ``` The `migrate:reset` command will roll back all of your application's migrations: ```shell -go run . artisan migrate:reset +./artisan migrate:reset ``` ### Roll Back & Migrate Using A Single Command @@ -172,13 +184,13 @@ go run . artisan migrate:reset The `migrate:refresh` command will roll back all of your migrations and then execute the `migrate` command. This command effectively re-creates your entire database: ```shell -go run . artisan migrate:refresh +./artisan migrate:refresh ``` You may roll back and re-migrate a limited number of migrations by providing the `step` option to the `refresh` command. For example, the following command will roll back and re-migrate the last five migrations: ```shell -go run . artisan migrate:refresh --step=5 +./artisan migrate:refresh --step=5 ``` ### Drop All Tables & Migrate @@ -186,7 +198,7 @@ go run . artisan migrate:refresh --step=5 The `migrate:fresh` command will drop all tables from the database and then execute the `migrate` command: ```shell -go run . artisan migrate:fresh +./artisan migrate:fresh ``` ## Tables diff --git a/en/database/queries.md b/en/database/queries.md index 5f3d10891..87343a12b 100644 --- a/en/database/queries.md +++ b/en/database/queries.md @@ -76,7 +76,7 @@ You can use the `FindOr` or `FirstOr` method, if the record is not found, it wil ```go var user *User -user, err = facades.DB().Table("users").Where("name", "John").FirstOr(&user, func() error { +err = facades.DB().Table("users").Where("name", "John").FirstOr(&user, func() error { return errors.New("no rows") }) ``` @@ -99,8 +99,10 @@ var products []Product err := facades.DB().Table("products").Each(func(rows []db.Row) error { for _, row := range rows { var product Product - err := row.Scan(&product) - s.NoError(err) + if err := row.Scan(&product); err != nil { + return err + } + products = append(products, product) } @@ -117,8 +119,10 @@ var products []Product err := facades.DB().Table("products").Chunk(2, func(rows []db.Row) error { for _, row := range rows { var product Product - err := row.Scan(&product) - s.NoError(err) + if err := row.Scan(&product); err != nil { + return err + } + products = append(products, product) } @@ -138,10 +142,9 @@ rows, err := facades.DB().Table("products").Cursor() var products []Product for row := range rows { var product Product - err = row.Scan(&product) - - s.NoError(err) - s.True(product.ID > 0) + if err := row.Scan(&product); err != nil { + return err + } products = append(products, product) } @@ -213,7 +216,7 @@ Sometimes you may need to use raw expressions in your queries. You can use the ` ```go import "github.com/goravel/framework/database/db" -facades.DB().Model(&user).Update("age", db.Raw("age - ?", 1)) +res, err := facades.DB().Model(&user).Update("age", db.Raw("age - ?", 1)) ``` ## Select @@ -224,10 +227,10 @@ Of course, you may not always want to retrieve all columns from a database table ```go // Select specific fields -facades.DB().Select("name", "age").Get(&users) +err := facades.DB().Select("name", "age").Get(&users) // Use a subquery -facades.DB().Select("name", db.Raw("(SELECT COUNT(*) FROM posts WHERE users.id = posts.user_id) as post_count")).Get(&users) +err := facades.DB().Select("name", db.Raw("(SELECT COUNT(*) FROM posts WHERE users.id = posts.user_id) as post_count")).Get(&users) ``` ### Distinct @@ -236,7 +239,7 @@ The `Distinct` method will force the query to return unique results: ```go var users []models.User -facades.DB().Distinct("name").Find(&users) +err := facades.DB().Distinct("name").Find(&users) ``` ## Raw Methods @@ -448,9 +451,9 @@ facades.DB().OrderByDesc("name") The `Latest` method makes it easy to sort results by date. By default, results will be sorted by the `created_at` column: ```go -facades.DB().Table("users").Latest().First(&user) +err := facades.DB().Table("users").Latest().First(&user) -facades.DB().Table("users").Latest("updated_at").First(&user) +err := facades.DB().Table("users").Latest("updated_at").First(&user) ``` **InRandomOrder** @@ -458,7 +461,7 @@ facades.DB().Table("users").Latest("updated_at").First(&user) The `InRandomOrder` method is used to sort results randomly: ```go -facades.DB().Table("users").InRandomOrder().First(&user) +err := facades.DB().Table("users").InRandomOrder().First(&user) ``` ### Grouping @@ -484,7 +487,7 @@ Sometimes you may want a clause to only execute when a given condition is true. ```go import "github.com/goravel/framework/contracts/database/db" -facades.DB().Table("users").When(1 == 1, func(query db.Query) db.Query { +err := facades.DB().Table("users").When(1 == 1, func(query db.Query) db.Query { return query.Where("age", 25) }).First(&user) ``` @@ -492,7 +495,7 @@ facades.DB().Table("users").When(1 == 1, func(query db.Query) db.Query { You can also pass another closure as the third parameter to the `When` method. This closure will execute if the first parameter results in false: ```go -facades.DB().Table("users").When(1 != 1, func(query db.Query) db.Query { +err := facades.DB().Table("users").When(1 != 1, func(query db.Query) db.Query { return query.OrderBy("name") }, func(query db.Query) db.Query { return query.OrderBy("id") @@ -575,11 +578,10 @@ result, err := facades.DB().Table("products").Where("id", 1).Update(map[string]a ### Update JSON fields ```go -facades.DB().Table("users").Where("id", 1).Update("options->enabled", true) -facades.DB().Table("users").Where("id", 1).Update("options->languages[0]", "en") -facades.DB().Table("users").Where("id", 1).Update("options->languages", []string{"en", "de"}) - -facades.DB().Table("users").Where("id", 1).Update(map[string]any{ +result, err := facades.DB().Table("users").Where("id", 1).Update("options->enabled", true) +result, err := facades.DB().Table("users").Where("id", 1).Update("options->languages[0]", "en") +result, err := facades.DB().Table("users").Where("id", 1).Update("options->languages", []string{"en", "de"}) +result, err := facades.DB().Table("users").Where("id", 1).Update(map[string]any{ "preferences->dining->meal": "salad", "options->languages[0]": "en", "options->enabled": true, @@ -637,13 +639,13 @@ The query builder also includes some functions that can help you implement "pess To use a "shared lock", you may use the `SharedLock` method. A shared lock prevents the selected rows from being modified until the transaction is committed: ```go -facades.DB().Table("users").Where("votes > ?", 100).SharedLock().Get(&users) +err := facades.DB().Table("users").Where("votes > ?", 100).SharedLock().Get(&users) ``` You can also use the `LockForUpdate` method. Using the "update" lock can prevent rows from being modified or selected by other shared locks: ```go -facades.DB().Table("users").Where("votes > ?", 100).LockForUpdate().Get(&users) +err := facades.DB().Table("users").Where("votes > ?", 100).LockForUpdate().Get(&users) ``` ## Debugging @@ -653,13 +655,13 @@ You can use the `ToSQL` and `ToRawSql` methods to get the current query binding With placeholder SQL: ```go -facades.DB().Table("users").Where("id", 1).ToSql().Get(models.User{}) +err := facades.DB().Table("users").Where("id", 1).ToSql().Get(models.User{}) ``` With bound values SQL: ```go -facades.DB().Table("users").Where("id", 1).ToRawSql().Get(models.User{}) +err := facades.DB().Table("users").Where("id", 1).ToRawSql().Get(models.User{}) ``` The methods that can be called after `ToSql` and `ToRawSql`: `Count`, `Insert`, `Delete`, `First`, `Get`, `Pluck`, `Update`. diff --git a/en/database/seeding.md b/en/database/seeding.md index 4e335eb61..fd984131e 100644 --- a/en/database/seeding.md +++ b/en/database/seeding.md @@ -11,7 +11,7 @@ Goravel includes the ability to seed your database with data using seed struct. To generate a seeder, run the `make:seeder` [Artisan command](../digging-deeper/artisan-console.md). All seeders generated by the framework will be stored in the `database/seeders` directory: ```shell -go run . artisan make:seeder UserSeeder +./artisan make:seeder UserSeeder ``` By default, a seeder struct has two methods: `Signature` and `Run`. The `Signature` method sets the name of the seeder, while the `Run` method is triggered when the `db:seed` Artisan command is executed. You can use the `Run` method to insert data into your database in any way you prefer. @@ -23,8 +23,8 @@ package seeders import ( "github.com/goravel/framework/contracts/database/seeder" - "github.com/goravel/framework/facades" - + + "goravel/app/facades" "goravel/app/models" ) @@ -74,34 +74,32 @@ func (s *DatabaseSeeder) Run() error { You may run the `db:seed` Artisan command to seed your database. By default, the `db:seed` command runs the `database/seeders/database_seeder.go` file, which may in turn invoke other seed classes. However, you may use the `--seeder` option to specify a specific seeder class to run individually: ```shell -go run . artisan db:seed +./artisan db:seed ``` -If you want to execute other seeders when running the `db:seed` command, you can register the seeder in `app/providers/database_service_provider.go`, if the seeder is generated by the `make:seeder` command, the framework will automatically register it. +If you want to execute other seeders when running the `db:seed` command, you can register the seeder in the `bootstrp/app.go::WithSeeders` function, if the seeder is generated by the `make:seeder` command, the framework will automatically register it. ```go -// app/providers/database_service_provider.go -func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) { - facades.Seeder().Register([]seeder.Seeder{ - &seeders.DatabaseSeeder{}, - &seeders.UserSeeder{}, - &seeders.PhotoSeeder{}, - }) +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithSeeders(Seeders). + WithConfig(config.Boot). + Start() } -go run . artisan db:seed --seeder=UserSeeder PhotoSeeder // The signature of seeder +./artisan db:seed --seeder=UserSeeder PhotoSeeder // The signature of seeder ``` You may also seed your database using the `migrate:fresh` and `migrate:refresh` command in combination with the `--seed` option, which will drop all tables and re-run all of your migrations. This command is useful for completely re-building your database. The `--seeder` option may be used to specify a specific seeder to run: ```shell -go run . artisan migrate:fresh --seed +./artisan migrate:fresh --seed -go run . artisan migrate:fresh --seed --seeder=UserSeeder +./artisan migrate:fresh --seed --seeder=UserSeeder -go run . artisan migrate:refresh --seed +./artisan migrate:refresh --seed -go run . artisan migrate:refresh --seed --seeder=UserSeeder +./artisan migrate:refresh --seed --seeder=UserSeeder ``` ### Forcing Seeders To Run In Production @@ -109,5 +107,5 @@ go run . artisan migrate:refresh --seed --seeder=UserSeeder Some seeding operations may cause you to alter or lose data. In order to protect you from running seeding commands against your production database, you will be prompted for confirmation before the seeders are executed in the `production` environment. To force the seeders to run without a prompt, use the `--force` flag: ```shell -go run . artisan db:seed --force +./artisan db:seed --force ``` diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index 97aff241d..adf3e76ae 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -55,7 +55,7 @@ func Boot() contractsfoundation.Application { } ``` -A new command created by `make:command` will be register automatically in the `bootstrap/commands.go::Commands()` function and the function will be called by `WithCommands`. You need register the command manually if you create the command file by yourself. +A new command created by `make:command` will be registered automatically in the `bootstrap/commands.go::Commands()` function and the function will be called by `WithCommands`. You need register the command manually if you create the command file by yourself. ### Command Structure @@ -157,8 +157,8 @@ Get arguments: ```go func (receiver *SendEmails) Handle(ctx console.Context) error { - subject := ctx.ArgumentString("subject"))) - emails := ctx.ArgumentStringSlice("emails"))) + subject := ctx.ArgumentString("subject") + emails := ctx.ArgumentStringSlice("emails") return nil } diff --git a/en/digging-deeper/event.md b/en/digging-deeper/event.md index f0e72b5d2..004eca7ae 100644 --- a/en/digging-deeper/event.md +++ b/en/digging-deeper/event.md @@ -48,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 @@ -67,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" @@ -129,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 332a44dfc..8fd8cf43c 100644 --- a/en/digging-deeper/http-client.md +++ b/en/digging-deeper/http-client.md @@ -20,8 +20,6 @@ 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") ``` @@ -39,18 +37,19 @@ The `framework/contracts/http/client.Response` interface provides the following ```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 */ 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/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/processes.md b/en/digging-deeper/processes.md index b917ace20..f4c020503 100644 --- a/en/digging-deeper/processes.md +++ b/en/digging-deeper/processes.md @@ -17,7 +17,8 @@ Here is how you execute a blocking command: ```go import ( "fmt" - "github.com/goravel/framework/facades" + + "goravel/app/facades" ) func main() { diff --git a/en/digging-deeper/queues.md b/en/digging-deeper/queues.md index fc97c5758..9eec382b2 100644 --- a/en/digging-deeper/queues.md +++ b/en/digging-deeper/queues.md @@ -71,7 +71,7 @@ By default, all of the jobs for your application are stored in the `app/jobs` di ### Register Jobs -A new job created by `make:job` will be register 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. +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 { @@ -116,62 +116,22 @@ func (r *ProcessPodcast) ShouldRetry(err error, attempt int) (retryable bool, de ## 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 @@ -183,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" ) @@ -209,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/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/installation.md b/en/getting-started/installation.md index fc35b9235..3dba04443 100644 --- a/en/getting-started/installation.md +++ b/en/getting-started/installation.md @@ -33,7 +33,7 @@ cd goravel && go mod tidy cp .env.example .env // Generate application key -go run . artisan key:generate +./artisan key:generate ``` Please confirm your network if you encounter slow download dependencies. @@ -147,7 +147,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 +155,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 +163,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 90ddbd267..ec4802cea 100644 --- a/en/getting-started/packages.md +++ b/en/getting-started/packages.md @@ -12,7 +12,6 @@ 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% | diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md index 3b8ea98c2..895822cd0 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: @@ -859,8 +859,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" ) @@ -1024,8 +1024,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: @@ -1066,8 +1066,8 @@ To register an observer, you need to call the `Observe` method on the model you package providers import ( - "github.com/goravel/framework/facades" - + + "goravel/app/facades" "goravel/app/models" "goravel/app/observers" ) diff --git a/en/security/authorization.md b/en/security/authorization.md index abf01af5b..683b9c855 100644 --- a/en/security/authorization.md +++ b/en/security/authorization.md @@ -26,7 +26,8 @@ import ( contractsaccess "github.com/goravel/framework/contracts/auth/access" "github.com/goravel/framework/auth/access" - "github.com/goravel/framework/facades" + + "goravel/app/facades" ) type AuthServiceProvider struct { @@ -60,7 +61,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 { diff --git a/en/testing/getting-started.md b/en/testing/getting-started.md index 2c8a63af7..8498fbb41 100644 --- a/en/testing/getting-started.md +++ b/en/testing/getting-started.md @@ -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..6d92e0c0b 100644 --- a/en/testing/mock.md +++ b/en/testing/mock.md @@ -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..eb89eac6e 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" ) diff --git a/en/the-basics/grpc.md b/en/the-basics/grpc.md index c66149bb4..da7421609 100644 --- a/en/the-basics/grpc.md +++ b/en/the-basics/grpc.md @@ -48,8 +48,8 @@ package routes import ( "github.com/goravel/grpc/protos" - "github.com/goravel/framework/facades" + "goravel/app/facades" "goravel/app/grpc/controllers" ) @@ -123,7 +123,7 @@ the `trace` group can be applied to the configuration item `grpc.clients.interce package config import ( - "github.com/goravel/framework/facades" + "goravel/app/facades" ) func init() { diff --git a/en/the-basics/routing.md b/en/the-basics/routing.md index d0ec49ece..0ae8cf853 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 @@ -259,8 +190,9 @@ Rate limiters are defined using the `facades.RateLimiter()`'s `For` method. The ```go import ( contractshttp "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" "github.com/goravel/framework/http/limit" + + "goravel/app/facades" ) func (receiver *RouteServiceProvider) configureRateLimiting() { diff --git a/en/the-basics/session.md b/en/the-basics/session.md index 10f096ef9..fa5a2c18d 100644 --- a/en/the-basics/session.md +++ b/en/the-basics/session.md @@ -173,8 +173,6 @@ 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") ``` diff --git a/en/the-basics/validation.md b/en/the-basics/validation.md index 9d2d06be7..0d3b37267 100644 --- a/en/the-basics/validation.md +++ b/en/the-basics/validation.md @@ -473,7 +473,7 @@ func (receiver *Uppercase) Message(ctx context.Context) string { ### Register Custom Rules -A new rule created by `make:rule` will be register 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. +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. ```go func Boot() contractsfoundation.Application { @@ -555,7 +555,7 @@ func (receiver *ToInt) Handle(ctx context.Context) any { ### Register Custom Filters -A new rule created by `make:filter` will be register automatically in the `bootstrap/filters.go::Filters()` function and the function will be called by `WithFilters`. You need register the rule manually if you create the rule file by yourself. +A new 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. ```go func Boot() contractsfoundation.Application { diff --git a/en/the-basics/views.md b/en/the-basics/views.md index 38be85c63..6ccfb894d 100644 --- a/en/the-basics/views.md +++ b/en/the-basics/views.md @@ -87,7 +87,8 @@ package providers import ( "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/facades" + + "goravel/app/facades" ) type AppServiceProvider struct { From 4c0100688cb1578c0b15810059ce8b5438850b24 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 16 Jan 2026 18:36:14 +0800 Subject: [PATCH 36/49] optimize --- en/README.md | 71 +++++++++++++++++------- en/database/migrations.md | 28 ++++------ en/digging-deeper/artisan-console.md | 11 ++-- en/orm/getting-started.md | 78 ++++++++++++--------------- en/security/authentication.md | 10 ++-- en/security/authorization.md | 68 +++++++++++------------ en/security/encryption.md | 2 +- en/testing/getting-started.md | 2 +- en/the-basics/controllers.md | 6 +-- en/the-basics/grpc.md | 81 +++++++--------------------- en/the-basics/routing.md | 22 ++++---- en/the-basics/session.md | 20 +++---- en/the-basics/views.md | 25 +++------ 13 files changed, 190 insertions(+), 234 deletions(-) diff --git a/en/README.md b/en/README.md index c7993cbb6..7d5306c98 100644 --- a/en/README.md +++ b/en/README.md @@ -16,21 +16,60 @@ English | [δΈ­ζ–‡](/zh_CN/README.md) ## About Goravel -Goravel is a web application framework with complete functions and excellent scalability. As a starting scaffolding to help Gopher quickly build their own applications. +Goravel is a full-featured, scalable web application framework that provides a starting scaffold to help Gophers quickly build their applications. -The framework's design is consistent with [Laravel](https://github.com/laravel/laravel), simplifying the learning curve for PHPers. Kudos to Laravel! +The framework style is consistent with [Laravel](https://laravel.com/), so PHP developers don’t need to learn a new framework and can still enjoy playing around with Golang, in tribute to Laravel! -We are open to receiving stars, PRs, and issues! +We welcome stars, PRs, and issues! -## Main Function +## Documentation + +Online documentation [https://www.goravel.dev](https://www.goravel.dev) -| | | | | | -| ------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------- | -------------------------------------------- | ----------------------------------------------------- | -| [Config](/getting-started/configuration.md) | [Http](/the-basics/routing.md) | [Authentication](/security/authentication.md) | [Authorization](/security/authorization.md) | [Orm](/orm/getting-started.md) | -| [Migrate](/database/migrations.md) | [Logger](/the-basics/logging.md) | [Cache](/digging-deeper/cache.md) | [Grpc](/the-basics/grpc.md) | [Artisan Console](/digging-deeper/artisan-console.md) | -| [Task Scheduling](/digging-deeper/task-scheduling.md) | [Queue](/digging-deeper/queues.md) | [Event](/digging-deeper/event.md) | [FileStorage](/digging-deeper/filesystem.md) | [Mail](/digging-deeper/mail.md) | -| [Validation](/the-basics/validation.md) | [Mock](/testing/mock.md) | [Hash](/security/hashing.md) | [Crypt](/security/encryption.md) | [Carbon](/digging-deeper/helpers.md) | -| [Package Development](/digging-deeper/package-development.md) | [Testing](/testing/getting-started.md) | [Localization](/digging-deeper/localization.md) | [Session](/the-basics/session.md) | | +Example [https://github.com/goravel/example](https://github.com/goravel/example) + +> To optimize the documentation, please submit a PR to the documentation +> repository [https://github.com/goravel/docs](https://github.com/goravel/docs) + +## Main Features + +| Module Name | Description | +|-------------|-------------| +| [Artisan Console](https://www.goravel.dev/digging-deeper/artisan-console.html) | CLI command-line interface for application management and automation | +| [Authentication](https://www.goravel.dev/security/authentication.html) | User identity verification with JWT and Session drivers | +| [Authorization](https://www.goravel.dev/security/authorization.html) | Permission-based access control using policies and gates | +| [Cache](https://www.goravel.dev/digging-deeper/cache.html) | Store and retrieve data using memory, Redis, or custom drivers | +| [Carbon](https://www.goravel.dev/digging-deeper/helpers.html) | Helper functions for date and time manipulation | +| [Config](https://www.goravel.dev/getting-started/configuration.html) | Application configuration management from files and environment | +| [Crypt](https://www.goravel.dev/security/encryption.html) | Secure data encryption and decryption utilities | +| [DB](https://www.goravel.dev/database/getting-started.html) | Database query builder | +| [Event](https://www.goravel.dev/digging-deeper/event.html) | Application event dispatching and listening system | +| [Factory](https://www.goravel.dev/orm/factories.html) | Generate fake model data for testing purposes | +| [FileStorage](https://www.goravel.dev/digging-deeper/filesystem.html) | File upload, download, and storage across multiple drivers | +| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | High-performance gRPC server and client implementation | +| [Hash](https://www.goravel.dev/security/hashing.html) | Secure password hashing | +| [Http](https://www.goravel.dev/the-basics/routing.html) | HTTP routing, controllers, and middleware management | +| [Http Client](https://www.goravel.dev/digging-deeper/http-client.html) | Make HTTP requests to external APIs and services | +| [Localization](https://www.goravel.dev/digging-deeper/localization.html) | Multi-language translation and locale management | +| [Logger](https://www.goravel.dev/the-basics/logging.html) | Application logging to files, console, or external services | +| [Mail](https://www.goravel.dev/digging-deeper/mail.html) | Send emails via SMTP or queue-based delivery | +| [Mock](https://www.goravel.dev/testing/mock.html) | Create test mocks for facades and dependencies | +| [Migrate](https://www.goravel.dev/database/migrations.html) | Version control for database schema changes | +| [Orm](https://www.goravel.dev/orm/getting-started.html) | Elegant Orm implementation for database operations | +| [Package Development](https://www.goravel.dev/digging-deeper/package-development.html) | Build reusable packages to extend framework functionality | +| [Queue](https://www.goravel.dev/digging-deeper/queues.html) | Defer time-consuming tasks to background job processing | +| [Seeder](https://www.goravel.dev/database/seeding.html) | Populate database tables with test or initial data | +| [Session](https://www.goravel.dev/the-basics/session.html) | Manage user session data across HTTP requests | +| [Task Scheduling](https://www.goravel.dev/digging-deeper/task-scheduling.html) | Schedule recurring tasks using cron-like expressions | +| [Testing](https://www.goravel.dev/testing/getting-started.html) | HTTP testing, mocking, and assertion utilities | +| [Validation](https://www.goravel.dev/the-basics/validation.html) | Validate incoming request data using rules | +| [View](https://www.goravel.dev/the-basics/views.html) | Template rendering engine for HTML responses | +| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | Long-running command-line process management | +| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | Long-running command-line process management | + +## Compare With Laravel + +[For Detail](https://www.goravel.dev/prologue/compare-with-laravel.html) ## Roadmap @@ -38,15 +77,7 @@ We are open to receiving stars, PRs, and issues! ## Excellent Extend Packages -[For Detail](getting-started/packages.md) - -## Documentation - -Online documentation [https://www.goravel.dev](https://www.goravel.dev) - -Example [https://github.com/goravel/example](https://github.com/goravel/example) - -> To optimize the documentation, please submit a PR to the documentation repository [https://github.com/goravel/docs](https://github.com/goravel/docs) +[For Detail](https://www.goravel.dev/getting-started/packages.html) ## Contributors diff --git a/en/database/migrations.md b/en/database/migrations.md index da85e6d3f..a9058acc0 100644 --- a/en/database/migrations.md +++ b/en/database/migrations.md @@ -37,11 +37,16 @@ You can also create a migration for a specific model by using the `-m` or `--mod The model should be registered in the `bootstrap/app.go` file before running the command. This command will generate a migration file based on the structure defined in the `User` model. ```go -WithCallback(func() { - facades.Schema().Extend(schema.Extension{ - Models: []any{models.User{}}, - }) -}). +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithConfig(config.Boot). + WithCallback(func() { + facades.Schema().Extend(schema.Extension{ + Models: []any{models.User{}}, + }) + }). + Start() +} ``` ### Quickly Create @@ -117,18 +122,7 @@ func (r *M20241207095921CreateUsersTable) Connection() string { ## Register Migrations -You need to register the migration files in the `database/kernel.go` file after the migration files are generated, if the migration files are generated by the `make:migration` command, the framework will automatically register them. - -```go -// database/kernel.go -func (kernel Kernel) Migrations() []schema.Migration { - return []schema.Migration{ - &migrations.M20241207095921CreateUsersTable{}, - } -} -``` - -A new migration created by `make:migration` will be registered automatically in the `bootstrap/migrations.go::Migrations()` function and the function will be called by `WithMigrations`. You need register the rule manually if you create the rule file by yourself. +A new migration created by `make:migration` will be registered automatically in the `bootstrap/migrations.go::Migrations()` function and the function will be called by `WithMigrations`. You need register the rule manually if you create the migration file by yourself. ```go func Boot() contractsfoundation.Application { diff --git a/en/digging-deeper/artisan-console.md b/en/digging-deeper/artisan-console.md index adf3e76ae..10ff0c1ec 100644 --- a/en/digging-deeper/artisan-console.md +++ b/en/digging-deeper/artisan-console.md @@ -519,13 +519,14 @@ func (receiver *ConsoleMakeCommand) Extend() command.Extend { ## 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() } ``` diff --git a/en/orm/getting-started.md b/en/orm/getting-started.md index 895822cd0..1d13ca122 100644 --- a/en/orm/getting-started.md +++ b/en/orm/getting-started.md @@ -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 @@ -275,10 +282,10 @@ facades.Orm().Query().WithoutGlobalScopes("name").Get(&users) | 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) | +| WhereAll | [WhereAll](#where) | +| WhereAny | [WhereAny](#where) | | WhereBetween | [WhereBetween](#where) | -| WhereNone | [WhereNone](#where) | +| WhereNone | [WhereNone](#where) | | WhereNotBetween | [WhereNotBetween](#where) | | WhereNotIn | [WhereNotIn](#where) | | WhereNull | [WhereNull](#where) | @@ -1060,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 ( - - "goravel/app/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/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 683b9c855..de8bd05ea 100644 --- a/en/security/authorization.md +++ b/en/security/authorization.md @@ -14,42 +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" - - "goravel/app/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() } ``` @@ -154,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 @@ -193,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 8498fbb41..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. diff --git a/en/the-basics/controllers.md b/en/the-basics/controllers.md index eb89eac6e..5d6a99859 100644 --- a/en/the-basics/controllers.md +++ b/en/the-basics/controllers.md @@ -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 da7421609..0fc638866 100644 --- a/en/the-basics/grpc.md +++ b/en/the-basics/grpc.md @@ -75,49 +75,29 @@ func Boot() contractsfoundation.Application { ## Interceptor -The interceptor can be defined in the `app/grpc/interceptors` folder. - -**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 @@ -135,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", ""), @@ -146,27 +125,3 @@ func init() { }) } ``` - -### Register Interceptors - -Register interceptors in the `WithGrpcServerInterceptors` and `WithGrpcClientInterceptors` functions of the `bootstrap/app.go` file after interceptors were defined. - -```go -func Boot() contractsfoundation.Application { - return foundation.Setup(). - WithGrpcServerInterceptors(func() []grpc.UnaryServerInterceptor { - return []grpc.UnaryServerInterceptor{ - interceptors.TestServer, - } - }). - WithGrpcClientInterceptors(func() map[string][]grpc.UnaryClientInterceptor { - return map[string][]grpc.UnaryClientInterceptor{ - "default": { - interceptors.TestClient, - }, - } - }). - WithConfig(config.Boot). - Start() -} -``` diff --git a/en/the-basics/routing.md b/en/the-basics/routing.md index 0ae8cf853..ac31fa8fc 100644 --- a/en/the-basics/routing.md +++ b/en/the-basics/routing.md @@ -183,22 +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/http/limit" - - "goravel/app/facades" -) - -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 fa5a2c18d..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() } ``` diff --git a/en/the-basics/views.md b/en/the-basics/views.md index 6ccfb894d..5eb4555cb 100644 --- a/en/the-basics/views.md +++ b/en/the-basics/views.md @@ -80,25 +80,16 @@ 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" - - "goravel/app/facades" -) - -type AppServiceProvider struct { -} - -func (receiver *AppServiceProvider) Register(app foundation.Application) { -} - -func (receiver *AppServiceProvider) Boot(app foundation.Application) { - facades.View().Share("key", "value") +func Boot() contractsfoundation.Application { + return foundation.Setup(). + WithConfig(config.Boot). + WithCallback(func() { + facades.View().Share("key", "value") + }). + Start() } ``` From d6a0fe17c4bf86818b98e35bc9c4a30293e8d8c4 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 16 Jan 2026 18:41:10 +0800 Subject: [PATCH 37/49] optimize --- en/architecture-concepts/service-providers.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/en/architecture-concepts/service-providers.md b/en/architecture-concepts/service-providers.md index 03f30d88a..f35916c1d 100644 --- a/en/architecture-concepts/service-providers.md +++ b/en/architecture-concepts/service-providers.md @@ -123,16 +123,17 @@ func (r *RouteRunner) Shutdown() error { } ``` -You can also register a single `Runner` in `bootstrap/app.go` to run some code when the application starts. +You can register your own `Runner` in the `bootstrap/app.go::WithRunners` function to run some code when the application starts. ```go func Boot() contractsfoundation.Application { - app := foundation.Setup(). + return foundation.Setup(). WithConfig(config.Boot). - Create() - - app.Start(YourRunner) - - return app + WithRunners(func() []foundation.Runner{ + return []foundation.Runner{ + NewYourCustomRunner(), + } + }). + Start() } ``` From deb0f77e5e924daef60a7e6485c273c20b098d40 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Thu, 22 Jan 2026 21:56:44 +0530 Subject: [PATCH 38/49] remove telemetry facade documentation --- .vitepress/config/en.ts | 4 ---- en/digging-deeper/telemetry.md | 5 ----- en/upgrade/v1.17.md | 7 ------- 3 files changed, 16 deletions(-) delete mode 100644 en/digging-deeper/telemetry.md diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index eb7d532a9..d0e14e29c 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -299,10 +299,6 @@ function sidebarAdvanced(): DefaultTheme.SidebarItem[] { { text: 'Pluralization', link: 'pluralization' - }, - { - text: 'Telemetry', - link: 'telemetry' } ] } diff --git a/en/digging-deeper/telemetry.md b/en/digging-deeper/telemetry.md deleted file mode 100644 index 537fbfe49..000000000 --- a/en/digging-deeper/telemetry.md +++ /dev/null @@ -1,5 +0,0 @@ -# Telemetry - -[[toc]] - -## Introduction diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index b30e2acc1..dec89b5db 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -5,7 +5,6 @@ - [Goravel Lite](#goravel-lite) - [Simplified Code Structure](#simplified-code-structure) - [Process Facade](#process-facade) -- [Telemetry Facade](#telemetry-facade) - [Pluralizer Package](#pluralizer-package) - [Service Provider Runners](#service-provider-runners) @@ -162,12 +161,6 @@ Goravel now provides an expressive and elegant API around Go's standard `os/exec [View Document](../digging-deeper/processes.md) -### Telemetry Facade - -Goravel now includes a Telemetry Facade that provides a unified interface for collecting and exporting telemetry data, such as metrics and traces, from your application. This helps you monitor the performance and health of your application effectively. - -[View Document](../digging-deeper/telemetry.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. From 4d2bdf3dfeebfded11526eaecd80e9bdae663ff0 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Thu, 22 Jan 2026 22:14:39 +0530 Subject: [PATCH 39/49] optimise pluralization example --- en/digging-deeper/pluralization.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/en/digging-deeper/pluralization.md b/en/digging-deeper/pluralization.md index b4eb98f9b..c39c6b34b 100644 --- a/en/digging-deeper/pluralization.md +++ b/en/digging-deeper/pluralization.md @@ -37,13 +37,13 @@ like in the `Boot` method of a Service Provider. 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" +import ( + "github.com/goravel/framework/support/pluralizer" + "github.com/goravel/framework/support/pluralizer/rules" +) // Register that "mouse" becomes "mice" -pluralizer.RegisterIrregular("english", pluralizer.Substitution{ - Singular: "mouse", - Plural: "mice", -}) +pluralizer.RegisterIrregular("english", rules.NewSubstitution("mouse", "mice")) ``` ### Uninflected Words From aa2dac20f1027c08f4f950ed2ed2aa3d8157924f Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Fri, 23 Jan 2026 00:49:12 +0530 Subject: [PATCH 40/49] add documentation for http client testing --- en/digging-deeper/http-client.md | 183 +++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/en/digging-deeper/http-client.md b/en/digging-deeper/http-client.md index 8fd8cf43c..fc9cc5358 100644 --- a/en/digging-deeper/http-client.md +++ b/en/digging-deeper/http-client.md @@ -300,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. +::: + + From ce6b48c4bad9dfd5d11c1db2697741b5457b148d Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Fri, 23 Jan 2026 01:00:07 +0530 Subject: [PATCH 41/49] fix view.md --- en/the-basics/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/the-basics/views.md b/en/the-basics/views.md index 5eb4555cb..e06cc17b2 100644 --- a/en/the-basics/views.md +++ b/en/the-basics/views.md @@ -98,7 +98,7 @@ func Boot() contractsfoundation.Application { 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. +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 From 5fe5bf86a6184758af7983687c8a1e0df2382648 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 24 Jan 2026 16:45:03 +0800 Subject: [PATCH 42/49] optimize --- en/testing/mock.md | 4 ++-- en/the-basics/request.md | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/en/testing/mock.md b/en/testing/mock.md index 6d92e0c0b..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") } diff --git a/en/the-basics/request.md b/en/the-basics/request.md index ec4bbba3c..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 From 0edb416f1339f7c6b0a621f7e5bf8153187b08aa Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 24 Jan 2026 16:55:39 +0800 Subject: [PATCH 43/49] optimize TODO --- en/README.md | 3 +-- en/prologue/compare-with-laravel.md | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/en/README.md b/en/README.md index 7d5306c98..3832785b2 100644 --- a/en/README.md +++ b/en/README.md @@ -57,6 +57,7 @@ Example [https://github.com/goravel/example](https://github.com/goravel/example) | [Migrate](https://www.goravel.dev/database/migrations.html) | Version control for database schema changes | | [Orm](https://www.goravel.dev/orm/getting-started.html) | Elegant Orm implementation for database operations | | [Package Development](https://www.goravel.dev/digging-deeper/package-development.html) | Build reusable packages to extend framework functionality | +| [Process](https://www.goravel.dev/digging-deeper/process.html) | An expressive and elegant API around Go's standard `os/exec` package | | [Queue](https://www.goravel.dev/digging-deeper/queues.html) | Defer time-consuming tasks to background job processing | | [Seeder](https://www.goravel.dev/database/seeding.html) | Populate database tables with test or initial data | | [Session](https://www.goravel.dev/the-basics/session.html) | Manage user session data across HTTP requests | @@ -64,8 +65,6 @@ Example [https://github.com/goravel/example](https://github.com/goravel/example) | [Testing](https://www.goravel.dev/testing/getting-started.html) | HTTP testing, mocking, and assertion utilities | | [Validation](https://www.goravel.dev/the-basics/validation.html) | Validate incoming request data using rules | | [View](https://www.goravel.dev/the-basics/views.html) | Template rendering engine for HTML responses | -| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | Long-running command-line process management | -| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | Long-running command-line process management | ## Compare With Laravel diff --git a/en/prologue/compare-with-laravel.md b/en/prologue/compare-with-laravel.md index 0d82c9beb..d50752d85 100644 --- a/en/prologue/compare-with-laravel.md +++ b/en/prologue/compare-with-laravel.md @@ -25,17 +25,16 @@ Goravel is heavily inspired by the Laravel framework, aiming to bring similar el | [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') | -| [TODO Process](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | Long-running command-line process management
`Process::run('ls -la') | -| [TODO Rate Limiting](https://www.goravel.dev/digging-deeper/process.html) | βœ… | βœ… | facades.RateLimiter().TooManyAttempts("key", 5)
RateLimiter::tooManyAttempts('key', 5) | -| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | βœ… | 🚧 | -| [TODO Telemetry](https://www.goravel.dev/digging-deeper/process.html) | βœ… | 🚧 | -| Broadcasting | 🚧 | βœ… | -| Livewire / Inertia | 🚧 | βœ… | -| Notifications | 🚧 | βœ… | +| [Grpc](https://www.goravel.dev/the-basics/grpc.html) | βœ… | 🚧 | | +| Notifications | 🚧 | βœ… | | +| Broadcasting | 🚧 | βœ… | | +| Livewire | 🚧 | βœ… | | From eed932c7b320725a0d250a4d073521f9e3146f97 Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 24 Jan 2026 22:25:34 +0800 Subject: [PATCH 44/49] add navigate --- .vitepress/config/en.ts | 8 ++++---- en/upgrade/history.md | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index d0e14e29c..5de4e833b 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -158,12 +158,12 @@ function sidebarPrologue(): DefaultTheme.SidebarItem[] { function sidebarUpgrade(): DefaultTheme.SidebarItem[] { return [ { - text: 'Upgrading To v1.16 From v1.15', - link: 'v1.16' + text: 'Upgrading To v1.17 From v1.16', + link: 'v1.17' }, { - text: 'Upgrading To v1.15 From v1.14', - link: 'v1.15' + text: 'Upgrading To v1.16 From v1.15', + link: 'v1.16' }, { text: 'History', 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) From 7559d4b51778a9153f41cde697317fc80f1a334e Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 24 Jan 2026 22:33:46 +0800 Subject: [PATCH 45/49] add versions --- .vitepress/config/en.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 5de4e833b..3134c9b6c 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -102,6 +102,19 @@ function nav(): DefaultTheme.NavItem[] { text: 'Video Tutorials', link: 'https://www.youtube.com/playlist?list=PL40Xne4u-oXJ0Z5uFiPWHqIMvzZaG_BDf' }, + { + text: 'Versions', + items: [ + { + text: 'v1.17 (Latest)', + link: 'https://www.goravel.dev/' + }, + { + text: 'v1.16', + link: 'https://v116.docs.goravel.dev/' + }, + ] + }, { text: 'Translate', link: '/prologue/contributions#add-a-new-language' From c46a143c7fd29c12a7203f61e6e2578596d18f2e Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 24 Jan 2026 22:35:56 +0800 Subject: [PATCH 46/49] add versions --- .vitepress/config/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index 3134c9b6c..7afdda7bb 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -111,7 +111,7 @@ function nav(): DefaultTheme.NavItem[] { }, { text: 'v1.16', - link: 'https://v116.docs.goravel.dev/' + link: 'https://v116.goravel.dev/' }, ] }, From db64df85f6f844f31682a65acafe48799255bd6c Mon Sep 17 00:00:00 2001 From: Bowen Date: Sun, 25 Jan 2026 10:58:58 +0800 Subject: [PATCH 47/49] optimize home page --- en/getting-started/installation.md | 35 +++++++++++++++++++++++++----- en/index.md | 6 ++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/en/getting-started/installation.md b/en/getting-started/installation.md index 3dba04443..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* @@ -36,23 +40,44 @@ cp .env.example .env ./artisan key:generate ``` -Please confirm your network if you encounter slow download dependencies. +#### 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. -## 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 . 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 From 15dbb0feaff85213cccb32c9d238c28c76c4501a Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 29 Jan 2026 17:56:16 +0800 Subject: [PATCH 48/49] optimize code --- en/upgrade/v1.17.md | 73 +++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index dec89b5db..f782e44bb 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -72,25 +72,25 @@ go mod tidy Add new configuration in `config/http.go`: ```go --- "client": map[string]any{ --- "base_url": config.GetString("HTTP_CLIENT_BASE_URL"), --- "timeout": config.GetDuration("HTTP_CLIENT_TIMEOUT"), --- "max_idle_conns": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS"), --- "max_idle_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST"), --- "max_conns_per_host": config.GetInt("HTTP_CLIENT_MAX_CONN_PER_HOST"), --- "idle_conn_timeout": config.GetDuration("HTTP_CLIENT_IDLE_CONN_TIMEOUT"), --- }, -++ "default_client": config.Env("HTTP_CLIENT_DEFAULT", "default"), -++ "clients": map[string]any{ -++ "default": map[string]any{ -++ "base_url": config.Env("HTTP_CLIENT_BASE_URL", ""), -++ "timeout": config.Env("HTTP_CLIENT_TIMEOUT", "30s"), -++ "max_idle_conns": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS", 100), -++ "max_idle_conns_per_host": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST", 2), -++ "max_conns_per_host": config.Env("HTTP_CLIENT_MAX_CONN_PER_HOST", 0), -++ "idle_conn_timeout": config.Env("HTTP_CLIENT_IDLE_CONN_TIMEOUT", "90s"), -++ }, -++ }, +"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 ++] ``` ### 3. If you want to send mail via template @@ -187,7 +187,8 @@ Goravel's logging system now supports outputting logs in JSON format, making it "level": config.Env("LOG_LEVEL", "debug"), "days": 7, "print": true, -++ "formatter": "json",// the value can be "text" or "json" + // the value can be "text" or "json" + "formatter": "json", // [!code ++] }, }, ``` @@ -203,25 +204,25 @@ Goravel's HTTP client module now supports multiple clients, allowing you to defi 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) -++ validator, err := facades.Validation().Make(ctx, input, rules) +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 --- Message() string -++ Passes(ctx context.Context, data Data, val any, options ...any) bool -++ Message(ctx context.Context) string + 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 -++ Handle(ctx context.Context) any + Handle() any // [!code --] + Handle(ctx context.Context) any // [!code ++] } ``` @@ -245,8 +246,8 @@ type User struct { Name string } --- func (r *User) GlobalScopes() []func(orm.Query) orm.Query { -++ func (r *User) GlobalScopes() map[string]func(orm.Query) orm.Query { +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") @@ -344,8 +345,8 @@ package log type Logger interface { // Handle pass channel config path here --- Handle(channel string) (Hook, error) -++ Handle(channel string) (Handler, error) + Handle(channel string) (Hook, error) // [!code --] + Handle(channel string) (Handler, error) // [!code ++] } ``` @@ -371,7 +372,7 @@ The `Bind` function on the `Http.Request` interface has been removed. You can no ```go var user User --- response, err := facades.Http().Bind(&user).Get("https://github.com") -++ response, err := facades.Http().Get("https://github.com") -++ err = response.Bind(&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 ++] ``` From fa92f3d63554f6c44e032f02b061936f5ad63942 Mon Sep 17 00:00:00 2001 From: Bowen Date: Thu, 29 Jan 2026 17:59:50 +0800 Subject: [PATCH 49/49] add new providers --- en/upgrade/v1.17.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/en/upgrade/v1.17.md b/en/upgrade/v1.17.md index f782e44bb..9c334c0db 100644 --- a/en/upgrade/v1.17.md +++ b/en/upgrade/v1.17.md @@ -67,7 +67,24 @@ go get github.com/goravel/minio@latest go mod tidy ``` -### 2. Modify Http Client Configuration +### 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`: @@ -93,7 +110,7 @@ Add new configuration in `config/http.go`: }, // [!code ++] ``` -### 3. If you want to send mail via template +### 4. If you want to send mail via template Add new configuration in `config/mail.go`: @@ -115,23 +132,23 @@ Add new configuration in `config/mail.go`: }, ``` -### 4. If you are using machinery queue driver +### 5. If you are using machinery queue driver Need to modify accordingly: [Remove machinery queue driver](#remove-machinery-queue-driver) -### 5. If you created a custom log driver +### 6. If you created a custom log driver Need to modify accordingly: [Switch log driver](#switch-log-driver) -### 6. If you are using Http.Request.Bind function +### 7. If you are using Http.Request.Bind function Need to modify accordingly: [Remove Http.Request.Bind function](#remove-http-request-bind-function) -### 7. If you are using validation.Make function or custom rules/filters +### 8. If you are using validation.Make function or custom rules/filters Need to modify accordingly: [Validation supports pass context](#validation-supports-pass-context) -### 8. If you are using global scopes in Orm +### 9. If you are using global scopes in Orm Need to modify accordingly: [Add WithoutGlobalScopes function for Orm](#add-withoutglobalscopes-function-for-orm)