As modern PHP and Laravel applications grow in complexity, maintaining code consistency and quality becomes increasingly challenging. Cursor Rules provide an elegant solution to this problem by guiding AI-assisted development with precise, project-specific patterns and constraints. This article explores how to create effective Cursor Rules for Laravel/PHP projects that improve code quality, enforce architectural patterns, and enhance developer productivity.
Cursor Rules are a set of instructions that help AI development assistants understand the patterns, best practices, and constraints specific to your project. They act as guardrails to ensure AI-generated code follows your team's standards and architectural decisions.
A well-crafted Cursor Rules file dramatically improves the quality and consistency of AI-generated code, reducing the need for manual corrections and ensuring adherence to project-specific patterns.
Laravel's flexibility is both a strength and a challenge. While it allows developers to implement various architectural patterns, this flexibility can lead to inconsistency across a codebase without proper guidelines. Cursor Rules help by:
Begin with a clear, concise description of your project and its architectural approach. This provides crucial context for AI to understand your system's purpose and constraints.
## Project Overview
This is a multi-establishment hospital scheduling and leave management system built with Laravel 12. The system handles:
- Role-based access control via Spatie Permissions
- Specialized handling for different medical professions
- Automated leave allocation
- Dynamic schedule management
- Calendar interface with room reservation logic
## High-Level Guidelines
- Use PHP 8.3+ features where appropriate
- Follow Laravel 12 conventions and best practices
- Create code with excellent developer experience (DX)
- Ensure type safety with comprehensive docblocks
- Write maintainable, testable code
Don't just describe your architecture—show it. Provide concrete, implementable examples of each pattern you use.
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Leave;
use App\DataTransferObjects\LeaveRequestDto;
use Illuminate\Support\Collection;
final class LeaveManagementService
{
/**
* Get all pending leave requests for a specific department.
*
* @param int $departmentId
* @return Collection<int, Leave>
*/
public function getPendingLeaveRequests(int $departmentId): Collection
{
return Leave::query()
->where('department_id', $departmentId)
->where('status', 'pending')
->get();
}
/**
* Calculate remaining leave days for a user.
*
* @param int $userId
* @return int
*/
public function calculateRemainingLeaveDays(int $userId): int
{
// Implementation
}
}
Illustrate common mistakes with contrasting "AVOID" vs "PREFERRED" examples. This is particularly effective for teaching AI to avoid anti-patterns specific to your codebase.
// AVOID - Missing proper casts
class Schedule extends Model
{
protected $fillable = [
'user_id',
'start_date',
'end_date',
];
}
// PREFERRED - With proper casts
class Schedule extends Model
{
protected $fillable = [
'user_id',
'start_date',
'end_date',
];
protected $casts = [
'user_id' => 'integer',
'start_date' => 'datetime',
'end_date' => 'datetime',
];
}
Testing is critical for Laravel applications. Include examples of your preferred testing approach.
<?php
use App\Models\User;
use App\Services\LeaveManagementService;
beforeEach(function () {
$this->service = app(LeaveManagementService::class);
$this->user = User::factory()->create();
});
test('it calculates remaining leave days correctly', function () {
// Act
$remainingDays = $this->service->calculateRemainingLeaveDays($this->user->id);
// Assert
expect($remainingDays)->toBeInt();
expect($remainingDays)->toBeGreaterThanOrEqual(0);
});
test('it returns pending leave requests for department', function () {
// Arrange
$departmentId = 1;
// Act
$pendingRequests = $this->service->getPendingLeaveRequests($departmentId);
// Assert
expect($pendingRequests)->toBeInstanceOf(Collection::class);
});
Include a checklist that AI can follow to verify its generated code meets your standards.
## Verification Steps
Before completing any implementation, ensure that:
1. All classes follow their respective architectural patterns (Service, Action, Strategy, DTO)
2. All method parameters and return types are properly type-hinted
3. Models have proper casts for integer and date fields
4. Unit and feature tests are written for both happy and sad paths
5. Policies are implemented for all controller methods
6. Code is properly documented with PHPDoc comments
Clearly define the responsibilities of different architectural components. In Laravel applications, this often means specifying the boundaries between:
Controllers should:
- Validate input data
- Call appropriate services or actions
- Return responses
Controllers should NOT:
- Contain business logic
- Access the database directly
- Handle file operations
PHP 8.x's improved type system is powerful but requires consistent usage. Show examples of proper type declarations:
// PREFERRED
public function findAvailableAppointments(
int $doctorId,
\DateTimeInterface $startDate,
\DateTimeInterface $endDate
): Collection {
// Implementation
}
// AVOID
public function findAvailableAppointments($doctorId, $startDate, $endDate) {
// Implementation
}
Establish clear patterns for exception handling in your Laravel application:
// PREFERRED
public function createAppointment(AppointmentDto $dto): Appointment
{
try {
// Implementation
} catch (ValidationException $e) {
report($e);
throw $e;
} catch (\Exception $e) {
report($e);
throw new AppointmentCreationException('Failed to create appointment', 0, $e);
}
}
Encourage performance-conscious database access:
// PREFERRED - Using Laravel Query Builder with eager loading
public function getActiveAppointments(int $doctorId): Collection
{
return Appointment::query()
->where('doctor_id', $doctorId)
->where('status', 'active')
->with(['patient', 'room'])
->get();
}
// AVOID - N+1 query problem
public function getActiveAppointments(int $doctorId): Collection
{
$appointments = Appointment::where('doctor_id', $doctorId)
->where('status', 'active')
->get();
// Inefficient: This creates N+1 queries
foreach ($appointments as $appointment) {
$patient = $appointment->patient;
$room = $appointment->room;
}
return $appointments;
}
For complex design patterns like Strategy or Command, provide complete examples:
interface ScheduleStrategyInterface
{
public function generateSchedule(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array;
}
abstract class ScheduleBaseStrategy implements ScheduleStrategyInterface
{
protected function validateConstraints(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): bool
{
// Common validation logic
return true;
}
}
class DoctorScheduleStrategy extends ScheduleBaseStrategy
{
public function generateSchedule(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$this->validateConstraints($userId, $startDate, $endDate);
// Doctor-specific schedule generation logic
return [];
}
}
class ScheduleStrategyContext
{
private array $strategies = [];
public function registerStrategy(string $role, ScheduleStrategyInterface $strategy): void
{
$this->strategies[$role] = $strategy;
}
public function executeStrategy(string $role, int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$strategy = $this->getStrategy($role);
return $strategy->generateSchedule($userId, $startDate, $endDate);
}
}
Document how to use Laravel-specific features in your application:
// PREFERRED - Using Laravel's Query Scopes
class Appointment extends Model
{
public function scopeUpcoming($query)
{
return $query->where('start_time', '>', now());
}
public function scopeForDoctor($query, int $doctorId)
{
return $query->where('doctor_id', $doctorId);
}
}
// Usage
$upcomingAppointments = Appointment::upcoming()->forDoctor($doctorId)->get();
Show how validation should work in your application:
// PREFERRED - Using Laravel's Form Requests
class AppointmentRequest extends FormRequest
{
public function rules(): array
{
return [
'doctor_id' => ['required', 'integer', 'exists:users,id'],
'patient_id' => ['required', 'integer', 'exists:users,id'],
'start_time' => ['required', 'date', 'after:now'],
'end_time' => ['required', 'date', 'after:start_time'],
'room_id' => ['nullable', 'integer', 'exists:rooms,id'],
'notes' => ['nullable', 'string', 'max:500'],
];
}
}
Show how authorization should work with your role-based access control:
// PREFERRED - Using Policies
class AppointmentPolicy
{
public function view(User $user, Appointment $appointment): bool
{
return $user->id === $appointment->doctor_id
|| $user->id === $appointment->patient_id
|| $user->hasRole('admin')
|| $user->hasPermissionTo('view appointments');
}
}
Establish clear standards for DocBlocks and comments:
/**
* Calculate available time slots for a doctor on a specific date.
*
* This method considers the doctor's working hours, existing appointments,
* and any approved leave requests to determine available slots.
*
* @param int $doctorId The ID of the doctor
* @param \DateTimeInterface $date The date to check availability for
* @return \Illuminate\Support\Collection<int, array> Collection of available time slots
* with 'start' and 'end' keys as DateTime objects
* @throws \App\Exceptions\DoctorNotFoundException If doctor does not exist
*/
public function getAvailableTimeSlots(int $doctorId, \DateTimeInterface $date): Collection
Include performance best practices in your rules:
Performance Considerations
- Always eager load relationships to avoid N+1 queries
- Use chunking for processing large datasets
- Implement caching for expensive queries
- Consider using database transactions for multiple related operations
- Use indexed columns for frequently queried data
Creating effective Cursor Rules for Laravel/PHP projects is about much more than just coding standards—it's about encoding your team's architectural vision and best practices into a format that AI can understand and apply consistently. By clearly defining your patterns, expectations, and constraints, you empower AI to generate code that not only works but also aligns with your project's architectural principles.
The investment in creating these rules pays dividends through:
As PHP and Laravel continue to evolve, your Cursor Rules should evolve too. Regularly updating them with new patterns, best practices, and lessons learned ensures they remain relevant and valuable for your development process.
By embracing this approach, you can harness the power of AI-assisted development while maintaining the high standards and architectural vision that make your Laravel application robust, maintainable, and a joy to work with.