PHP Developer Agent. Laravel 기반 백엔드 개발을 담당합니다. Eloquent ORM, Artisan, Queue, Sanctum 전문.
Install with Tessl CLI
npx tessl i github:shaul1991/shaul-agents-plugin --skill backend-php60
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
Laravel 기반 백엔드 개발을 담당합니다. RESTful API 설계, 데이터베이스 모델링, 인증/인가 시스템 구현 등 전반적인 백엔드 개발을 수행합니다.
php, laravel, eloquent, artisan, composer, blade, migration, seeder, factory, sanctum, 라라벨
app/
├── Console/ # Artisan 커맨드
├── Exceptions/ # 예외 핸들러
├── Http/
│ ├── Controllers/ # 컨트롤러
│ ├── Middleware/ # 미들웨어
│ ├── Requests/ # Form Request (유효성 검증)
│ └── Resources/ # API Resource (응답 변환)
├── Models/ # Eloquent 모델
├── Providers/ # 서비스 프로바이더
├── Repositories/ # Repository 패턴 (선택)
└── Services/ # 비즈니스 로직| 타입 | 규칙 | 예시 |
|---|---|---|
| 클래스 | PascalCase | UserController |
| 메서드 | camelCase | getUserById() |
| 변수 | camelCase | $userName |
| 상수 | UPPER_SNAKE | MAX_ATTEMPTS |
| 테이블 | snake_case (복수형) | user_profiles |
| 모델 | PascalCase (단수형) | UserProfile |
// Controller - 얇게 유지
class UserController extends Controller
{
public function __construct(
private readonly UserService $userService
) {}
public function store(StoreUserRequest $request): JsonResponse
{
$user = $this->userService->create($request->validated());
return response()->json(
new UserResource($user),
Response::HTTP_CREATED
);
}
}
// Form Request - 유효성 검증 분리
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
];
}
}
// Service - 비즈니스 로직
class UserService
{
public function __construct(
private readonly UserRepository $userRepository
) {}
public function create(array $data): User
{
$data['password'] = Hash::make($data['password']);
return $this->userRepository->create($data);
}
}
// Repository - 데이터 접근 추상화
class UserRepository
{
public function __construct(
private readonly User $model
) {}
public function create(array $data): User
{
return $this->model->create($data);
}
public function findByEmail(string $email): ?User
{
return $this->model->where('email', $email)->first();
}
}
// API Resource - 응답 변환
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toISOString(),
];
}
}use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use SoftDeletes; // Soft Delete 활성화
// Mass Assignment 보호
protected $fillable = ['name', 'email', 'password'];
// 숨김 필드
protected $hidden = ['password', 'remember_token'];
// 타입 캐스팅
protected $casts = [
'email_verified_at' => 'datetime',
'settings' => 'array',
'is_active' => 'boolean',
];
// 관계 정의
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
// 스코프
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
// 접근자 (Accessor)
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
}// 마이그레이션에 deleted_at 컬럼 추가
Schema::table('users', function (Blueprint $table) {
$table->softDeletes(); // deleted_at 컬럼 추가
});
// Soft Delete 실행 (deleted_at에 타임스탬프 설정)
$user->delete();
// 기본 쿼리 - soft deleted 레코드 제외
$users = User::all(); // deleted_at이 null인 레코드만
// Soft deleted 레코드 포함 조회
$allUsers = User::withTrashed()->get();
// Soft deleted 레코드만 조회
$deletedUsers = User::onlyTrashed()->get();
// 복원 (deleted_at을 null로)
$user->restore();
// 영구 삭제 (DB에서 완전 삭제)
$user->forceDelete();
// 관계에서 Soft Delete 적용
public function posts(): HasMany
{
return $this->hasMany(Post::class)->withTrashed(); // 삭제된 것도 포함
}
// Soft deleted 여부 확인
if ($user->trashed()) {
// 삭제된 상태
}// Migration
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index(['created_at', 'is_active']);
});
// Seeder with Factory
User::factory()
->count(50)
->has(Post::factory()->count(3))
->create();테스트 메서드명은 한글로 상세히 작성하여 테스트 의도를 명확히 합니다.
권장 이유:
it('사용자가 로그인할 수 있다') 형태로 문장처럼 작성expect()->toBe() 체이닝으로 가독성 향상uses(RefreshDatabase::class);
describe('UserService', function () {
it('유효한 데이터로 사용자를 생성할 수 있다', function () {
// Arrange
$data = [
'name' => '홍길동',
'email' => 'hong@example.com',
'password' => 'password123',
];
// Act
$user = app(UserService::class)->create($data);
// Assert
expect($user)->toBeInstanceOf(User::class);
$this->assertDatabaseHas('users', [
'email' => 'hong@example.com',
]);
expect(Hash::check('password123', $user->password))->toBeTrue();
});
it('이메일이 중복되면 사용자 생성에 실패한다', function () {
// Arrange
User::factory()->create(['email' => 'hong@example.com']);
// Act & Assert
expect(fn () => app(UserService::class)->create([
'name' => '김철수',
'email' => 'hong@example.com',
'password' => 'password123',
]))->toThrow(QueryException::class);
});
it('소프트 삭제된 사용자는 기본 조회에서 제외된다', function () {
// Arrange
$user = User::factory()->create();
$user->delete();
// Act & Assert
expect(User::find($user->id))->toBeNull();
expect(User::withTrashed()->find($user->id))->not->toBeNull();
});
it('소프트 삭제된 사용자를 복원할 수 있다', function () {
// Arrange
$user = User::factory()->create();
$user->delete();
// Act
$user->restore();
// Assert
expect($user->trashed())->toBeFalse();
expect(User::find($user->id))->not->toBeNull();
});
});class UserServiceTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function 유효한_데이터로_사용자를_생성할_수_있다(): void
{
// Arrange
$data = [
'name' => '홍길동',
'email' => 'hong@example.com',
'password' => 'password123',
];
// Act
$user = app(UserService::class)->create($data);
// Assert
$this->assertDatabaseHas('users', [
'email' => 'hong@example.com',
]);
$this->assertTrue(Hash::check('password123', $user->password));
}
/** @test */
public function 이메일이_중복되면_사용자_생성에_실패한다(): void
{
// Arrange
User::factory()->create(['email' => 'hong@example.com']);
// Assert
$this->expectException(QueryException::class);
// Act
app(UserService::class)->create([
'name' => '김철수',
'email' => 'hong@example.com',
'password' => 'password123',
]);
}
/** @test */
public function 소프트_삭제된_사용자는_기본_조회에서_제외된다(): void
{
// Arrange
$user = User::factory()->create();
// Act
$user->delete();
// Assert
$this->assertNull(User::find($user->id));
$this->assertNotNull(User::withTrashed()->find($user->id));
}
/** @test */
public function 소프트_삭제된_사용자를_복원할_수_있다(): void
{
// Arrange
$user = User::factory()->create();
$user->delete();
// Act
$user->restore();
// Assert
$this->assertFalse($user->trashed());
$this->assertNotNull(User::find($user->id));
}
}{{ }} 이스케이프 사용@csrf 디렉티브 사용$fillable 또는 $guarded 정의// Policy 기반 인가
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
// Controller에서 사용
$this->authorize('update', $post);// Bad - N+1 쿼리 발생
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // 각 반복마다 쿼리 실행
}
// Good - Eager Loading
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // 추가 쿼리 없음
}// 쿼리 결과 캐싱
$users = Cache::remember('users', 3600, function () {
return User::active()->get();
});
// 캐시 무효화
Cache::forget('users');php artisan make:migrationphp artisan make:modelphp artisan make:requestphp artisan make:controllerphp artisan make:resourcephp artisan make:testroutes/api.php 또는 routes/web.php# 개발
php artisan serve # 개발 서버 시작
php artisan tinker # REPL 실행
php artisan route:list # 라우트 목록
# 데이터베이스
php artisan migrate # 마이그레이션 실행
php artisan migrate:fresh --seed # DB 초기화 및 시딩
php artisan db:seed # 시더 실행
# 캐시
php artisan config:cache # 설정 캐싱
php artisan route:cache # 라우트 캐싱
php artisan view:cache # 뷰 캐싱
php artisan cache:clear # 캐시 클리어
# 큐
php artisan queue:work # 큐 워커 시작
php artisan queue:failed # 실패한 작업 목록
# 테스트
php artisan test # 전체 테스트 실행
php artisan test --filter=UserTest # 특정 테스트 실행9242c58
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.