autoworkflow 3.1.4 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Echo Framework Skill
|
|
2
|
+
|
|
3
|
+
## Application Setup
|
|
4
|
+
\`\`\`go
|
|
5
|
+
package main
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"net/http"
|
|
9
|
+
"os"
|
|
10
|
+
|
|
11
|
+
"github.com/labstack/echo/v4"
|
|
12
|
+
"github.com/labstack/echo/v4/middleware"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func main() {
|
|
16
|
+
e := echo.New()
|
|
17
|
+
|
|
18
|
+
// Hide banner in production
|
|
19
|
+
e.HideBanner = os.Getenv("ENV") == "production"
|
|
20
|
+
|
|
21
|
+
// Global middleware
|
|
22
|
+
e.Use(middleware.Logger())
|
|
23
|
+
e.Use(middleware.Recover())
|
|
24
|
+
e.Use(middleware.RequestID())
|
|
25
|
+
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
|
26
|
+
AllowOrigins: []string{"*"},
|
|
27
|
+
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
|
|
28
|
+
AllowHeaders: []string{echo.HeaderAuthorization, echo.HeaderContentType},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
// Custom error handler
|
|
32
|
+
e.HTTPErrorHandler = customErrorHandler
|
|
33
|
+
|
|
34
|
+
// Health check
|
|
35
|
+
e.GET("/health", func(c echo.Context) error {
|
|
36
|
+
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
setupRoutes(e)
|
|
40
|
+
|
|
41
|
+
e.Logger.Fatal(e.Start(":8080"))
|
|
42
|
+
}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
## Route Groups
|
|
46
|
+
\`\`\`go
|
|
47
|
+
func setupRoutes(e *echo.Echo) {
|
|
48
|
+
api := e.Group("/api/v1")
|
|
49
|
+
|
|
50
|
+
// Public routes
|
|
51
|
+
auth := api.Group("/auth")
|
|
52
|
+
auth.POST("/login", loginHandler)
|
|
53
|
+
auth.POST("/register", registerHandler)
|
|
54
|
+
|
|
55
|
+
// Protected routes
|
|
56
|
+
protected := api.Group("")
|
|
57
|
+
protected.Use(AuthMiddleware)
|
|
58
|
+
|
|
59
|
+
// Users
|
|
60
|
+
users := protected.Group("/users")
|
|
61
|
+
users.GET("", listUsersHandler)
|
|
62
|
+
users.GET("/:id", getUserHandler)
|
|
63
|
+
users.PUT("/:id", updateUserHandler)
|
|
64
|
+
users.DELETE("/:id", deleteUserHandler)
|
|
65
|
+
|
|
66
|
+
// Posts with rate limiting
|
|
67
|
+
posts := protected.Group("/posts")
|
|
68
|
+
posts.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(10)))
|
|
69
|
+
posts.GET("", listPostsHandler)
|
|
70
|
+
posts.POST("", createPostHandler)
|
|
71
|
+
}
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
## Request Binding and Validation
|
|
75
|
+
\`\`\`go
|
|
76
|
+
import "github.com/go-playground/validator/v10"
|
|
77
|
+
|
|
78
|
+
// Custom validator
|
|
79
|
+
type CustomValidator struct {
|
|
80
|
+
validator *validator.Validate
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (cv *CustomValidator) Validate(i interface{}) error {
|
|
84
|
+
if err := cv.validator.Struct(i); err != nil {
|
|
85
|
+
return echo.NewHTTPError(http.StatusBadRequest, formatValidationErrors(err))
|
|
86
|
+
}
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Register validator
|
|
91
|
+
func main() {
|
|
92
|
+
e := echo.New()
|
|
93
|
+
e.Validator = &CustomValidator{validator: validator.New()}
|
|
94
|
+
// ...
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Request DTOs
|
|
98
|
+
type CreateUserRequest struct {
|
|
99
|
+
Email string \`json:"email" validate:"required,email"\`
|
|
100
|
+
Name string \`json:"name" validate:"required,min=2,max=100"\`
|
|
101
|
+
Password string \`json:"password" validate:"required,min=8"\`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type QueryParams struct {
|
|
105
|
+
Page int \`query:"page" validate:"omitempty,min=1"\`
|
|
106
|
+
PerPage int \`query:"per_page" validate:"omitempty,min=1,max=100"\`
|
|
107
|
+
Sort string \`query:"sort" validate:"omitempty,oneof=asc desc"\`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func createUserHandler(c echo.Context) error {
|
|
111
|
+
var req CreateUserRequest
|
|
112
|
+
if err := c.Bind(&req); err != nil {
|
|
113
|
+
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
|
|
114
|
+
}
|
|
115
|
+
if err := c.Validate(&req); err != nil {
|
|
116
|
+
return err // Already formatted by custom validator
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
user, err := userService.Create(c.Request().Context(), req)
|
|
120
|
+
if err != nil {
|
|
121
|
+
return err
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return c.JSON(http.StatusCreated, user)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func listUsersHandler(c echo.Context) error {
|
|
128
|
+
var query QueryParams
|
|
129
|
+
if err := c.Bind(&query); err != nil {
|
|
130
|
+
return echo.NewHTTPError(http.StatusBadRequest, "invalid query parameters")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Defaults
|
|
134
|
+
if query.Page == 0 {
|
|
135
|
+
query.Page = 1
|
|
136
|
+
}
|
|
137
|
+
if query.PerPage == 0 {
|
|
138
|
+
query.PerPage = 20
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
users, total, err := userService.List(c.Request().Context(), query)
|
|
142
|
+
if err != nil {
|
|
143
|
+
return err
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
147
|
+
"data": users,
|
|
148
|
+
"total": total,
|
|
149
|
+
"page": query.Page,
|
|
150
|
+
"per_page": query.PerPage,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
## Custom Middleware
|
|
156
|
+
\`\`\`go
|
|
157
|
+
// Auth Middleware
|
|
158
|
+
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|
159
|
+
return func(c echo.Context) error {
|
|
160
|
+
authHeader := c.Request().Header.Get("Authorization")
|
|
161
|
+
if authHeader == "" {
|
|
162
|
+
return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
166
|
+
claims, err := validateToken(tokenString)
|
|
167
|
+
if err != nil {
|
|
168
|
+
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Store in context
|
|
172
|
+
c.Set("userID", claims.UserID)
|
|
173
|
+
c.Set("userRole", claims.Role)
|
|
174
|
+
|
|
175
|
+
return next(c)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Using Echo's JWT middleware
|
|
180
|
+
import "github.com/labstack/echo-jwt/v4"
|
|
181
|
+
|
|
182
|
+
func setupJWTMiddleware() echo.MiddlewareFunc {
|
|
183
|
+
return echojwt.WithConfig(echojwt.Config{
|
|
184
|
+
SigningKey: []byte(os.Getenv("JWT_SECRET")),
|
|
185
|
+
NewClaimsFunc: func(c echo.Context) jwt.Claims {
|
|
186
|
+
return new(CustomClaims)
|
|
187
|
+
},
|
|
188
|
+
SuccessHandler: func(c echo.Context) {
|
|
189
|
+
token := c.Get("user").(*jwt.Token)
|
|
190
|
+
claims := token.Claims.(*CustomClaims)
|
|
191
|
+
c.Set("userID", claims.UserID)
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Skipper for selective middleware
|
|
197
|
+
func AuthSkipper(c echo.Context) bool {
|
|
198
|
+
// Skip auth for public routes
|
|
199
|
+
return strings.HasPrefix(c.Path(), "/api/v1/auth")
|
|
200
|
+
}
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
## Error Handling
|
|
204
|
+
\`\`\`go
|
|
205
|
+
func customErrorHandler(err error, c echo.Context) {
|
|
206
|
+
var (
|
|
207
|
+
code = http.StatusInternalServerError
|
|
208
|
+
message = "internal server error"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// Check for echo.HTTPError
|
|
212
|
+
var he *echo.HTTPError
|
|
213
|
+
if errors.As(err, &he) {
|
|
214
|
+
code = he.Code
|
|
215
|
+
if m, ok := he.Message.(string); ok {
|
|
216
|
+
message = m
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for custom errors
|
|
221
|
+
switch {
|
|
222
|
+
case errors.Is(err, ErrNotFound):
|
|
223
|
+
code = http.StatusNotFound
|
|
224
|
+
message = "resource not found"
|
|
225
|
+
case errors.Is(err, ErrUnauthorized):
|
|
226
|
+
code = http.StatusUnauthorized
|
|
227
|
+
message = "unauthorized"
|
|
228
|
+
case errors.Is(err, ErrForbidden):
|
|
229
|
+
code = http.StatusForbidden
|
|
230
|
+
message = "forbidden"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Log internal errors
|
|
234
|
+
if code == http.StatusInternalServerError {
|
|
235
|
+
c.Logger().Error(err)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Send response (check if already committed)
|
|
239
|
+
if !c.Response().Committed {
|
|
240
|
+
c.JSON(code, map[string]string{"error": message})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
\`\`\`
|
|
244
|
+
|
|
245
|
+
## Testing
|
|
246
|
+
\`\`\`go
|
|
247
|
+
package handler_test
|
|
248
|
+
|
|
249
|
+
import (
|
|
250
|
+
"bytes"
|
|
251
|
+
"encoding/json"
|
|
252
|
+
"net/http"
|
|
253
|
+
"net/http/httptest"
|
|
254
|
+
"testing"
|
|
255
|
+
|
|
256
|
+
"github.com/labstack/echo/v4"
|
|
257
|
+
"github.com/stretchr/testify/assert"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
func setupTestEcho() *echo.Echo {
|
|
261
|
+
e := echo.New()
|
|
262
|
+
e.Validator = &CustomValidator{validator: validator.New()}
|
|
263
|
+
e.HTTPErrorHandler = customErrorHandler
|
|
264
|
+
setupRoutes(e)
|
|
265
|
+
return e
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func TestCreateUser(t *testing.T) {
|
|
269
|
+
e := setupTestEcho()
|
|
270
|
+
|
|
271
|
+
tests := []struct {
|
|
272
|
+
name string
|
|
273
|
+
body map[string]interface{}
|
|
274
|
+
wantStatus int
|
|
275
|
+
}{
|
|
276
|
+
{
|
|
277
|
+
name: "valid user",
|
|
278
|
+
body: map[string]interface{}{
|
|
279
|
+
"email": "test@example.com",
|
|
280
|
+
"name": "Test User",
|
|
281
|
+
"password": "password123",
|
|
282
|
+
},
|
|
283
|
+
wantStatus: http.StatusCreated,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "missing email",
|
|
287
|
+
body: map[string]interface{}{
|
|
288
|
+
"name": "Test User",
|
|
289
|
+
"password": "password123",
|
|
290
|
+
},
|
|
291
|
+
wantStatus: http.StatusBadRequest,
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for _, tt := range tests {
|
|
296
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
297
|
+
body, _ := json.Marshal(tt.body)
|
|
298
|
+
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBuffer(body))
|
|
299
|
+
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
300
|
+
rec := httptest.NewRecorder()
|
|
301
|
+
|
|
302
|
+
e.ServeHTTP(rec, req)
|
|
303
|
+
|
|
304
|
+
assert.Equal(t, tt.wantStatus, rec.Code)
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
## ✅ DO
|
|
311
|
+
- Register a custom validator with \`e.Validator\`
|
|
312
|
+
- Use \`c.Request().Context()\` for passing context to services
|
|
313
|
+
- Use built-in middleware (Logger, Recover, RequestID, CORS)
|
|
314
|
+
- Implement custom HTTPErrorHandler for consistent error responses
|
|
315
|
+
- Use \`echo.NewHTTPError\` for HTTP errors
|
|
316
|
+
|
|
317
|
+
## ❌ DON'T
|
|
318
|
+
- Don't forget to register validator before using \`c.Validate()\`
|
|
319
|
+
- Don't write response after calling \`next(c)\` in middleware
|
|
320
|
+
- Don't ignore \`c.Response().Committed\` in error handler
|
|
321
|
+
- Don't use \`c.String()\` for JSON responses - use \`c.JSON()\`
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Eloquent ORM Skill
|
|
2
|
+
|
|
3
|
+
## Model Definition
|
|
4
|
+
\`\`\`php
|
|
5
|
+
<?php
|
|
6
|
+
|
|
7
|
+
namespace App\\Models;
|
|
8
|
+
|
|
9
|
+
use Illuminate\\Database\\Eloquent\\{Model, SoftDeletes, Factories\\HasFactory};
|
|
10
|
+
use Illuminate\\Database\\Eloquent\\Casts\\Attribute;
|
|
11
|
+
use Illuminate\\Database\\Eloquent\\Relations\\{HasMany, BelongsTo, BelongsToMany};
|
|
12
|
+
|
|
13
|
+
class User extends Model
|
|
14
|
+
{
|
|
15
|
+
use HasFactory, SoftDeletes;
|
|
16
|
+
|
|
17
|
+
protected $fillable = ['email', 'name', 'status'];
|
|
18
|
+
protected $hidden = ['password', 'remember_token'];
|
|
19
|
+
protected $casts = [
|
|
20
|
+
'email_verified_at' => 'datetime',
|
|
21
|
+
'settings' => 'array',
|
|
22
|
+
'is_admin' => 'boolean',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Relationships
|
|
26
|
+
public function posts(): HasMany
|
|
27
|
+
{
|
|
28
|
+
return $this->hasMany(Post::class);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function team(): BelongsTo
|
|
32
|
+
{
|
|
33
|
+
return $this->belongsTo(Team::class);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public function roles(): BelongsToMany
|
|
37
|
+
{
|
|
38
|
+
return $this->belongsToMany(Role::class)->withTimestamps();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
## Accessors & Mutators (Laravel 9+)
|
|
44
|
+
\`\`\`php
|
|
45
|
+
class User extends Model
|
|
46
|
+
{
|
|
47
|
+
// Accessor: $user->full_name
|
|
48
|
+
protected function fullName(): Attribute
|
|
49
|
+
{
|
|
50
|
+
return Attribute::make(
|
|
51
|
+
get: fn () => "{$this->first_name} {$this->last_name}",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mutator: $user->password = 'secret'
|
|
56
|
+
protected function password(): Attribute
|
|
57
|
+
{
|
|
58
|
+
return Attribute::make(
|
|
59
|
+
set: fn (string $value) => bcrypt($value),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Accessor with caching
|
|
64
|
+
protected function postsCount(): Attribute
|
|
65
|
+
{
|
|
66
|
+
return Attribute::make(
|
|
67
|
+
get: fn () => $this->posts()->count(),
|
|
68
|
+
)->shouldCache();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
## Query Scopes
|
|
74
|
+
\`\`\`php
|
|
75
|
+
class User extends Model
|
|
76
|
+
{
|
|
77
|
+
// Local scope: User::active()->get()
|
|
78
|
+
public function scopeActive(Builder $query): Builder
|
|
79
|
+
{
|
|
80
|
+
return $query->where('status', 'active');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Dynamic scope: User::ofType('admin')->get()
|
|
84
|
+
public function scopeOfType(Builder $query, string $type): Builder
|
|
85
|
+
{
|
|
86
|
+
return $query->where('type', $type);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Chainable: User::active()->ofType('admin')->recent()->get()
|
|
90
|
+
public function scopeRecent(Builder $query): Builder
|
|
91
|
+
{
|
|
92
|
+
return $query->orderBy('created_at', 'desc');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Global Scope (always applied)
|
|
97
|
+
class ActiveScope implements Scope
|
|
98
|
+
{
|
|
99
|
+
public function apply(Builder $builder, Model $model): void
|
|
100
|
+
{
|
|
101
|
+
$builder->where('deleted_at', null);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// In model boot method
|
|
106
|
+
protected static function booted(): void
|
|
107
|
+
{
|
|
108
|
+
static::addGlobalScope(new ActiveScope);
|
|
109
|
+
|
|
110
|
+
// Or inline
|
|
111
|
+
static::addGlobalScope('active', fn (Builder $builder) =>
|
|
112
|
+
$builder->where('is_active', true)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
## Query Patterns
|
|
118
|
+
\`\`\`php
|
|
119
|
+
// Eager loading (prevent N+1)
|
|
120
|
+
$users = User::with(['posts', 'team'])->get();
|
|
121
|
+
|
|
122
|
+
// Nested eager loading
|
|
123
|
+
$users = User::with(['posts.comments', 'posts.tags'])->get();
|
|
124
|
+
|
|
125
|
+
// Conditional eager loading
|
|
126
|
+
$users = User::with(['posts' => fn ($query) =>
|
|
127
|
+
$query->where('published', true)->latest()
|
|
128
|
+
])->get();
|
|
129
|
+
|
|
130
|
+
// Lazy eager loading
|
|
131
|
+
$users = User::all();
|
|
132
|
+
$users->load('posts'); // Load after initial query
|
|
133
|
+
|
|
134
|
+
// Chunk for large datasets
|
|
135
|
+
User::chunk(100, function ($users) {
|
|
136
|
+
foreach ($users as $user) {
|
|
137
|
+
// Process each user
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Cursor for memory efficiency
|
|
142
|
+
foreach (User::cursor() as $user) {
|
|
143
|
+
// One model at a time in memory
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Aggregations
|
|
147
|
+
$count = User::where('status', 'active')->count();
|
|
148
|
+
$avg = Order::where('user_id', $id)->avg('total');
|
|
149
|
+
$exists = User::where('email', $email)->exists();
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Eloquent Collections
|
|
153
|
+
\`\`\`php
|
|
154
|
+
$users = User::all();
|
|
155
|
+
|
|
156
|
+
// Filter
|
|
157
|
+
$active = $users->where('status', 'active');
|
|
158
|
+
$admins = $users->filter(fn ($user) => $user->isAdmin());
|
|
159
|
+
|
|
160
|
+
// Transform
|
|
161
|
+
$emails = $users->pluck('email');
|
|
162
|
+
$grouped = $users->groupBy('team_id');
|
|
163
|
+
$keyed = $users->keyBy('id');
|
|
164
|
+
|
|
165
|
+
// Aggregate
|
|
166
|
+
$first = $users->first();
|
|
167
|
+
$unique = $users->unique('email');
|
|
168
|
+
$sorted = $users->sortBy('name');
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
## Model Events & Observers
|
|
172
|
+
\`\`\`php
|
|
173
|
+
// Using closures in boot
|
|
174
|
+
protected static function booted(): void
|
|
175
|
+
{
|
|
176
|
+
static::creating(function (User $user) {
|
|
177
|
+
$user->uuid = Str::uuid();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
static::deleting(function (User $user) {
|
|
181
|
+
$user->posts()->delete();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Observer class (php artisan make:observer UserObserver --model=User)
|
|
186
|
+
class UserObserver
|
|
187
|
+
{
|
|
188
|
+
public function created(User $user): void
|
|
189
|
+
{
|
|
190
|
+
Mail::to($user)->send(new WelcomeEmail($user));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public function updating(User $user): void
|
|
194
|
+
{
|
|
195
|
+
if ($user->isDirty('email')) {
|
|
196
|
+
$user->email_verified_at = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Register in AppServiceProvider
|
|
202
|
+
User::observe(UserObserver::class);
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
## Factories & Seeders
|
|
206
|
+
\`\`\`php
|
|
207
|
+
// database/factories/UserFactory.php
|
|
208
|
+
class UserFactory extends Factory
|
|
209
|
+
{
|
|
210
|
+
public function definition(): array
|
|
211
|
+
{
|
|
212
|
+
return [
|
|
213
|
+
'name' => fake()->name(),
|
|
214
|
+
'email' => fake()->unique()->safeEmail(),
|
|
215
|
+
'password' => bcrypt('password'),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// States for variations
|
|
220
|
+
public function admin(): static
|
|
221
|
+
{
|
|
222
|
+
return $this->state(fn (array $attributes) => [
|
|
223
|
+
'is_admin' => true,
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public function withPosts(int $count = 3): static
|
|
228
|
+
{
|
|
229
|
+
return $this->has(Post::factory()->count($count));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage
|
|
234
|
+
$user = User::factory()->create();
|
|
235
|
+
$admin = User::factory()->admin()->create();
|
|
236
|
+
$userWithPosts = User::factory()->withPosts(5)->create();
|
|
237
|
+
User::factory()->count(50)->create();
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
## ❌ DON'T
|
|
241
|
+
- Use \`User::all()\` without limits on large tables
|
|
242
|
+
- Forget to eager load relations (N+1 problem)
|
|
243
|
+
- Put business logic in Eloquent models
|
|
244
|
+
- Use \`$model->save()\` without checking return value
|
|
245
|
+
- Store sensitive data without encryption
|
|
246
|
+
- Use raw queries without parameter binding
|
|
247
|
+
|
|
248
|
+
## ✅ DO
|
|
249
|
+
- Always eager load known relations
|
|
250
|
+
- Use chunks or cursors for large datasets
|
|
251
|
+
- Implement soft deletes for important data
|
|
252
|
+
- Use casts for automatic type conversion
|
|
253
|
+
- Define inverse relationships
|
|
254
|
+
- Use scopes for reusable query logic
|
|
255
|
+
- Use observers for side effects
|
|
256
|
+
- Use factories for testing
|