CodeIgniter

17

CodeIgniter 4.x rules and best practices for Cursor

1 rule

# 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