Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions .agents/skills/goravel-development/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
name: goravel-development
description: >
Use when writing or modifying Goravel application code: routing, controllers,
facades, configuration, ORM, migrations, console commands, and tests. Covers
both goravel/goravel (full) and goravel/goravel-lite scaffolds.
---

# Goravel Development

Laravel-style web framework for Go. Built on `github.com/goravel/framework`
plus a scaffold repo you clone and adapt. All repos live under
https://github.com/goravel (framework, scaffolds, example, installer, docs,
drivers).

Framework source: `github.com/goravel/framework`. Reference app:
`github.com/goravel/example`. Docs source: `github.com/goravel/docs`.

## Initialize & Run

```shell
go install github.com/goravel/installer/goravel@latest
goravel new blog
# Or clone manually:
# Full scaffold: git clone --depth=1 https://github.com/goravel/goravel.git
# Lite scaffold: git clone --depth=1 https://github.com/goravel/goravel-lite.git
cd <project> && go mod tidy && cp .env.example .env
./artisan key:generate # 32-char APP_KEY for encryption
./artisan jwt:secret # only if using Authentication
go run . # start (air for live reload)
```

## Project Structure

The full scaffold installs every facade. The lite scaffold
ships a subset; install the rest with `./artisan package:install`. Add folders
freely, but don't rename defaults without `WithPaths()` in `bootstrap/app.go`.

- **`app/`** — Application code: HTTP controllers, console commands, models,
middleware, events, listeners, jobs, AI agents/tools. `app/facades/` holds
one thin file per facade delegating to `App().Make*()`.
- **`bootstrap/`** — App bootstrapping. `app.go` calls
`foundation.Setup()...Create()`; `providers.go`, `commands.go`,
`migrations.go` register providers, commands, migrations. `package:install`
rewrites these as facades change.
- **`config/`** — One file per concern; each calls
`facades.Config().Add("<name>", map[string]any{...})` in `init()`. Read env
with `config.Env("KEY", default)`.
- **`database/`** — `migrations/` (schema, scaffold with `make:migration`),
`seeders/` (initial/test data), `factories/` (fake model data for tests).
- **`routes/`** — Route definitions (`web.go`, `grpc.go`); registered in
`bootstrap/app.go::WithRouting`.
- **`resources/views/`** — `*.tmpl` templates rendered via
`ctx.Response().View().Make(...)`.
- **`lang/`**, **`storage/`**, **`public/`**, **`tests/`** — Translations,
runtime files/logs, public assets, feature tests (`testify` suites).
- **`.env` / `artisan` / `main.go`** — Env config, console entry
(`./artisan list`), app launch (`bootstrap.Boot().Start()`).

## Bootstrap & Configuration

`bootstrap/app.go` uses the `foundation.Setup()` builder; `With*` methods are
optional. `package:install` rewrites `bootstrap/app.go` and `providers.go`.

```go
return foundation.Setup().
WithConfig(config.Boot).
WithProviders(Providers).
WithRouting(func() { routes.Web() }).
WithCommands(Commands).
WithMiddleware(func(h configuration.Middleware) { /* ... */ }).
WithPaths(func(p configuration.Paths) { p.App("app") }).
Create()
```

## Facades

Facades are the most important part of Goravel — all functions are implemented
via facades. Each facade resolves a container binding via `App().Make*()` at
call time, keeping access concise (`facades.Cache().Put(...)`) while staying
testable — swap the binding for a mock in tests (see Testing below).

All application facades live in `app/facades/`, one file per facade:

```go
func Cache() cache.Cache { return App().MakeCache() }
// usage: facades.Orm().Query().Get(&users)
```

### Install / Uninstall

The lite scaffold ships only `App`, `Artisan`, `Config`, `Process`. Add the rest
(rewrites `providers.go`, `config/`, `.env.example`, runs `go mod tidy`):

```shell
./artisan package:install Route --default # one facade
./artisan package:install --all --default # all facades + default drivers
./artisan package:uninstall Route
./artisan package:install github.com/goravel/redis # external driver
```

> In the interactive picker, press `x` to select, then `Enter` to confirm.

### Finding Interfaces

Facade return types are in `github.com/goravel/framework/contracts/<module>/`. For GitHub,
read the version in `go.mod` (e.g. `v1.18.0`) and browse
`https://github.com/goravel/framework/tree/v1.18.x/contracts/<module>`
(minor version: `v1.18.0` → `v1.18.x`). Full facade list:
`github.com/goravel/framework/facades/facades.go`.

## Available Commands

```shell
./artisan list # all commands, args, options
./artisan make:<kind> # scaffold (run list to see make:* generators)
```

From Go: `facades.Artisan().Call("list")`.

## Scaffolding Commands (make:*)

AI agents: always run `go run . artisan list | grep make:` to discover all
available scaffolding generators, then use them instead of hand-writing
boilerplate files. The available `make:*` commands depend on which facades
are installed — the list grows as you `package:install` more packages.

Run `go run . artisan make:<name> --help` to see flags and options for a
specific command before using it.

## Testing

`testify` suites + `tests.TestCase`. Scaffold: `./artisan make:test feature/UserTest`.

```go
type UserTestSuite struct {
suite.Suite
tests.TestCase
}
func TestUserTestSuite(t *testing.T) { suite.Run(t, new(UserTestSuite)) }
func (s *UserTestSuite) SetupTest() { s.RefreshDatabase() } // optional
func (s *UserTestSuite) TestIndex() { s.Seed(&seeders.UserSeeder{}) }
```

**Mocking** — use `github.com/goravel/framework/testing/mock`, never hand-roll:

```go
mockCache := mock.Factory().Cache()
mockCache.EXPECT().Get("name", "test").Return("Goravel").Once()
mockCache.AssertExpectations(t)
```

**Docker DB/Cache** — `facades.Testing().Docker().Database()` spins up an
isolated image per package for parallel `go test`. In `TestMain`:
`Build()` → `Ready()` → `Migrate()` → `m.Run()` → `Shutdown()`.

**Env files** — a `.env` in a test package dir overrides root `.env` for that
package. `go test ./... --env=.env.testing` swaps root env.

## Code Conventions

- `any` not `interface{}`. Standard Go formatting/naming; Go version in `go.mod`.
- Prefer `go test <pkg>` / `-run <Test>` over `go test ./...` (slow).
- `assert.*(t, *)` / `require.*(t, *)` directly, not `assert.New(t)`;
`s.*(*, *)` in suites.
- Mocks: `.Once()` or `.Times(n)` always; `.Maybe()` only when necessary;
avoid `mock.Anything` where a concrete value works.
- Table-driven tests with `t.Run` sub-tests.

## Where to Look

- **Reference app:** `github.com/goravel/example` — demonstrates every facade. Search first.
- **Docs:** https://www.goravel.dev (source `github.com/goravel/docs`).

## Custom Extensions

You can extend this skill with project-specific rules by creating
`.agents/skills/goravel-development/CUSTOM.md`. AI agents reading this
skill MUST also read and apply that file when it exists.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=

OPENAI_API_KEY=
OPENAI_BASE_URL=

AI_PROVIDER=
Comment thread
hwbrzzl marked this conversation as resolved.
Comment thread
hwbrzzl marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=

OPENAI_API_KEY=
OPENAI_BASE_URL=

AI_PROVIDER=
9 changes: 9 additions & 0 deletions app/facades/ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package facades

import (
"github.com/goravel/framework/contracts/ai"
)

func AI() ai.AI {
return App().MakeAI()
}
9 changes: 9 additions & 0 deletions app/facades/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package facades

import (
"github.com/goravel/framework/contracts/telemetry"
)

func Telemetry() telemetry.Telemetry {
return App().MakeTelemetry()
}
4 changes: 2 additions & 2 deletions app/http/controllers/validation_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewValidationController() *ValidationController {

func (r *ValidationController) Json(ctx http.Context) http.Response {
ctx.WithValue("ctx", "context")
validator, err := ctx.Request().Validate(map[string]string{
validator, err := ctx.Request().Validate(map[string]any{
"context": "required",
"name": "required",
"date": "required|date",
Expand Down Expand Up @@ -122,7 +122,7 @@ func (r *ValidationController) Form(ctx http.Context) http.Response {
validator, err := facades.Validation().Make(ctx, map[string]any{
"context": ctx.Request().Input("context"),
"name": ctx.Request().Input("name"),
}, map[string]string{
}, map[string]any{
"context": "required",
"name": "required",
}, validation.PrepareForValidation(func(ctx context.Context, data contractsvalidation.Data) error {
Expand Down
2 changes: 1 addition & 1 deletion app/http/controllers/validation_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (s *ValidationControllerTestSuite) TestJson() {
mockValidator := mockFactory.ValidationValidator()
mockContext.EXPECT().WithValue("ctx", "context").Once()
mockContext.EXPECT().Request().Return(mockRequest).Once()
mockRequest.EXPECT().Validate(map[string]string{
mockRequest.EXPECT().Validate(map[string]any{
"context": "required",
"name": "required",
"date": "required|date",
Expand Down
74 changes: 41 additions & 33 deletions app/http/middleware/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,54 @@ import (
)

func Jwt() http.Middleware {
return func(ctx http.Context) {
guard := facades.Config().GetString("auth.defaults.guard")
if ctx.Request().Header("Guard") != "" {
guard = ctx.Request().Header("Guard")
}
return &JwtMiddleware{}
}

token := ctx.Request().Header("Authorization", "")
if token == "" {
_ = ctx.Response().String(http.StatusUnauthorized, "Unauthorized").Abort()
return
}
type JwtMiddleware struct{}

func (j *JwtMiddleware) Handle(ctx http.Context) {
guard := facades.Config().GetString("auth.defaults.guard")
if ctx.Request().Header("Guard") != "" {
guard = ctx.Request().Header("Guard")
}

token := ctx.Request().Header("Authorization", "")
if token == "" {
_ = ctx.Response().String(http.StatusUnauthorized, "Unauthorized").Abort()
return
}

if _, err := facades.Auth(ctx).Guard(guard).Parse(token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
// Refresh token
token, err = facades.Auth(ctx).Guard(guard).Refresh()
if err != nil {
// Refresh time exceeded
ctx.Request().Abort(http.StatusUnauthorized)
return
}

token = "Bearer " + token
} else {
// Token is invalid
if _, err := facades.Auth(ctx).Guard(guard).Parse(token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
// Refresh token
token, err = facades.Auth(ctx).Guard(guard).Refresh()
if err != nil {
// Refresh time exceeded
ctx.Request().Abort(http.StatusUnauthorized)
return
}

token = "Bearer " + token
} else {
// Token is invalid
ctx.Request().Abort(http.StatusUnauthorized)
return
}
}

// You can get User in DB and set it to ctx
// You can get User in DB and set it to ctx

//var user models.User
//if err := facades.Auth().User(ctx, &user); err != nil {
// ctx.Request().AbortWithStatus(http.StatusUnauthorized)
// return
//}
//ctx.WithValue("user", user)
//var user models.User
//if err := facades.Auth().User(ctx, &user); err != nil {
// ctx.Request().AbortWithStatus(http.StatusUnauthorized)
// return
//}
//ctx.WithValue("user", user)

ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}
ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}

func (j *JwtMiddleware) Signature() string {
return "jwt"
}
22 changes: 15 additions & 7 deletions app/http/middleware/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import (
)

func Lang() httpcontract.Middleware {
return func(ctx httpcontract.Context) {
lang := ctx.Request().Input("lang")
if lang == "" {
lang = facades.Config().GetString("app.locale")
}
facades.App().SetLocale(ctx, lang)
return &LangMiddleware{}
}

type LangMiddleware struct{}

ctx.Request().Next()
func (l *LangMiddleware) Handle(ctx httpcontract.Context) {
lang := ctx.Request().Input("lang")
if lang == "" {
lang = facades.Config().GetString("app.locale")
}
facades.App().SetLocale(ctx, lang)

ctx.Request().Next()
}

func (l *LangMiddleware) Signature() string {
return "lang"
}
Loading
Loading