CursorPool
← 返回首页

CodeIgniter

CodeIgniter 4.x rules and best practices for Cursor

cursor.directory·15
规则

codeigniter

CodeIgniter 4.x rules and best practices for Cursor

# CodeIgniter 4 — Clean Code Skill

> Expert guidance for writing readable, maintainable, and testable CodeIgniter 4 / PHP 8.1+ code.

---

## Core Philosophy

- **Readable first**: Code is read far more than it is written. Optimize for the next developer.
- **Small, focused units**: Every class, method, and function does one thing only (SRP).
- **Explicit over implicit**: Avoid magic. Make intent visible through naming and structure.
- **No premature optimization**: Write clear code first; optimize only when profiling justifies it.
- **Fail loudly**: Prefer exceptions over silent failures. Never swallow errors.

---

## PHP Standards

- Always declare `declare(strict_types=1);` at the top of every file.
- Use PHP 8.1+ features: `readonly` properties, enums, named arguments, fibers, intersection types.
- Follow PSR-12 for code style.
- Use typed properties, typed parameters, and explicit return types on **every** method — including `void`.
- Never use `mixed` unless absolutely unavoidable; document why when you do.
- Prefer `readonly` classes and properties for value objects and DTOs.
- Use named arguments when calling functions with multiple parameters to improve clarity.

```php
// Bad
$user = new User('John', 28, true);

// Good
$user = new User(name: 'John', age: 28, isActive: true);
```

---

## Naming Rules

> The most important clean code rule.

- **Classes**: PascalCase, noun or noun phrase — `UserRepository`, `CreateOrderService`, `PaymentGatewayInterface`.
- **Methods**: camelCase, verb or verb phrase — `findById()`, `createOrder()`, `isEmailTaken()`.
- **Variables**: camelCase, meaningful nouns — `$activeUsers`, `$orderTotal`. Never `$data`, `$arr`, `$temp`, `$x`.
- **Booleans**: prefix with `is`, `has`, `can`, `should` — `$isActive`, `$hasPermission`, `$canRefund`.
- **Constants / Enums**: UPPER_SNAKE for constants, PascalCase for enum cases.
- **No abbreviations**: write `$userIdentifier`, not `$uid`. Write `getUserByEmail()`, not `getUsrByEml()`.
- **Length rule**: if a name needs a comment to explain it, rename it instead.

```php
// Bad
$d = $u->getData(); // get user data

// Good
$userProfile = $user->getProfile();
```

---

## Clean Code Rules

### Functions and Methods

- **Maximum 20 lines** per method. If it's longer, extract.
- **Maximum 3 parameters**. If you need more, introduce a DTO or value object.
- **One level of abstraction per method**: don't mix high-level logic with low-level implementation.
- **No boolean flag parameters** — they indicate a method does two things.
- **No output arguments** — functions should return values, not modify passed references.
- **Command Query Separation (CQS)**: a method either changes state (command) or returns data (query), never both.

```php
// Bad — boolean flag, does two things
public function getUsers(bool $includeInactive): array { ... }

// Good — two explicit, clear methods
public function getActiveUsers(): array { ... }
public function getAllUsers(): array { ... }
```

### Classes

- **Single Responsibility**: one reason to change per class.
- Mark all classes `final` unless designed for extension.
- Use `readonly` for value objects and DTOs.
- Keep classes under **200 lines**. Longer classes are doing too much.
- Declare properties explicitly with types; never rely on dynamic property assignment.
- No public mutable properties — expose state through methods.

### Comments

- **Never comment what the code does** — write code that explains itself.
- Use comments only to explain **why**, not **what**.
- Prefer descriptive method extraction over inline comments.
- PHPDoc blocks for public API methods; skip them for obvious private methods.

```php
// Bad
// Loop through users and send email
foreach ($users as $user) { ... }

// Good — the method name IS the comment
$this->notifyUsersOfUpcomingExpiry($users);
```

---

## Architecture

### Directory Structure

```
app/
├── Controllers/
│   └── Api/V1/
├── DTO/               ← Input/output data shapes
├── Enums/             ← PHP 8.1 enums
├── Exceptions/        ← Domain-specific exceptions
├── Filters/           ← Middleware equivalents
├── Interfaces/        ← Contracts for services and repos
├── Models/            ← CI4 Models (DB layer only)
├── Repositories/      ← Data access abstraction
├── Services/          ← Business logic
└── ValueObjects/      ← Immutable domain primitives
```

### Layers and Responsibilities

#### Controllers — HTTP only. No business logic.

- Validate the HTTP request.
- Call one service method.
- Return the HTTP response.
- Must be `final`.

```php
final class UsersController extends ResourceController
{
    public function __construct(
        private readonly UserService $userService
    ) {}

    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->findById($id);
        return $this->respond($user->toArray());
    }
}
```

#### Services — Business logic only. No HTTP, no SQL.

- Orchestrate domain operations.
- Call repositories, not models directly.
- Must be `final` and `readonly`.
- Throw domain exceptions on failure.

```php
final class UserService
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
        private readonly MailerInterface $mailer
    ) {}

    public function register(RegisterUserDTO $dto): User
    {
        if ($this->userRepository->existsByEmail($dto->email)) {
            throw new EmailAlreadyTakenException($dto->email);
        }

        $user = $this->userRepository->save(User::fromDTO($dto));
        $this->mailer->sendWelcome($user);

        return $user;
    }
}
```

#### Repositories — Data access only. No business logic.

- Always program to an interface.
- Return domain objects or collections, never raw arrays from DB.
- Encapsulate all query logic; no Query Builder outside the repository.

```php
interface UserRepositoryInterface
{
    public function findById(int $id): User;
    public function existsByEmail(string $email): bool;
    public function save(User $user): User;
}
```

#### DTOs — Data transfer only. Immutable.

```php
readonly class RegisterUserDTO
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}

    public static function fromRequest(IncomingRequest $request): self
    {
        return new self(
            name: $request->getVar('name'),
            email: $request->getVar('email'),
            password: $request->getVar('password'),
        );
    }
}
```

#### Value Objects — Immutable domain primitives with self-validation.

```php
readonly class Email
{
    public function __construct(public readonly string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException($value);
        }
    }

    public function equals(Email $other): bool
    {
        return $this->value === $other->value;
    }
}
```

#### Enums — Replace magic strings and integers.

```php
enum OrderStatus: string
{
    case Pending   = 'pending';
    case Confirmed = 'confirmed';
    case Shipped   = 'shipped';
    case Cancelled = 'cancelled';

    public function canTransitionTo(self $next): bool
    {
        return match($this) {
            self::Pending   => $next === self::Confirmed || $next === self::Cancelled,
            self::Confirmed => $next === self::Shipped   || $next === self::Cancelled,
            default         => false,
        };
    }
}
```

---

## Error Handling

- **Never return null to signal failure** — throw a named exception.
- Create domain-specific exceptions under `app/Exceptions/`.
- Name exceptions after what happened: `UserNotFoundException`, `InsufficientStockException`.
- Never catch `\Exception` generically unless at the top-level handler.
- Log at the boundary (controller / exception handler), not deep in services.

```php
// Bad
public function findById(int $id): ?User
{
    return $this->db->find($id); // null means "not found" — ambiguous
}

// Good
public function findById(int $id): User
{
    $record = $this->db->find($id);

    if ($record === null) {
        throw new UserNotFoundException($id);
    }

    return User::fromRecord($record);
}
```

---

## Validation

- Always validate at the HTTP boundary — never deep inside services.
- Use CodeIgniter's Validation service or a dedicated `FormRequest`-style class.
- Keep validation rules on the DTO or a dedicated `*Rules` class, not inline in the controller.

```php
final class RegisterUserRules
{
    public static function rules(): array
    {
        return [
            'name'     => 'required|min_length[2]|max_length[100]',
            'email'    => 'required|valid_email|is_unique[users.email]',
            'password' => 'required|min_length[8]',
        ];
    }

    public static function messages(): array
    {
        return [
            'email' => ['is_unique' => 'This email is already registered.'],
        ];
    }
}
```

---

## CodeIgniter 4 Specifics

- Register services in `app/Config/Services.php` with explicit type hints.
- Use Filters (`app/Filters/`) for auth, rate limiting, CORS — never put this logic in controllers.
- Use `$db->transStart()` / `$db->transComplete()` wrapped in try-catch for all multi-step writes.
- Use CodeIgniter Shield for authentication; never hand-roll auth.
- Use `cache()` helper with explicit TTL and tagged cache keys for invalidation.
- Prefix all CLI commands with `spark`; keep them in `app/Commands/`.
- Never call `model()` global helper inside services — inject the repository.

---

## Testing Rules

- Every service class must have a unit test.
- Every controller endpoint must have a feature test.
- Test method names: `test_it_throws_when_email_is_already_taken()`.
- Prefer fakes and stubs over mocks when possible.
- No test should touch the real database — use transactions or an in-memory SQLite DB.
- Keep tests under 30 lines; extract helpers to `setUp()` or factory methods.

---

## What to Always Avoid

| Avoid | Use instead |
|---|---|
| `$data`, `$result`, `$arr` | Descriptive names |
| `mixed` return types | Explicit types or generics |
| Raw SQL in controllers | Repository methods |
| `null` as error signal | Named exceptions |
| God classes (500+ lines) | Split by responsibility |
| Boolean flag parameters | Two separate methods |
| Comments explaining *what* | Self-documenting method names |
| Magic numbers/strings | Enums or named constants |
| `model()` global in services | Constructor injection |
| Silent `catch` blocks | Log and rethrow or handle explicitly |

---

## Dependencies

- PHP 8.1+
- CodeIgniter 4.x
- Composer for dependency management
- `codeigniter4/shield` for authentication
- PHPUnit for testing