Documentation Best Practices
Keep documentation minimal and meaningful. Well-written code with descriptive names often eliminates the need for comments. Document the "why" not the "what", and focus on complex business logic, not obvious code.
When NOT to Document
// BAD: Redundant comments that add no value class UserController { // This is the constructor public function __construct( // Inject user repository private UserRepository $repository ) { // Set the repository $this->repository = $repository; }
// Get all users
public function index()
{
// Return all users
return $this->repository->all();
}
}
// BAD: Obvious comments $user->age = 25; // Set age to 25 $total = $price * $quantity; // Calculate total if ($user->isActive()) { // Check if user is active // Send email $this->sendEmail($user); }
When TO Document
- Complex Business Logic
// GOOD: Explain complex business rules class PricingCalculator { /** * Calculate the final price with tiered discounts. * * Discount tiers: * - 10+ items: 5% discount * - 50+ items: 10% discount * - 100+ items: 15% discount * - VIP customers get additional 5% on top * * Note: Discounts don't apply to items already on sale */ public function calculateTotal(Order $order): float { $subtotal = $order->items ->reject(fn($item) => $item->is_on_sale) ->sum(fn($item) => $item->price * $item->quantity);
$regularItems = $order->items
->filter(fn($item) => !$item->is_on_sale);
$discount = match(true) {
$regularItems->sum('quantity') >= 100 => 0.15,
$regularItems->sum('quantity') >= 50 => 0.10,
$regularItems->sum('quantity') >= 10 => 0.05,
default => 0
};
if ($order->customer->is_vip) {
$discount += 0.05;
}
return $subtotal * (1 - $discount) + $this->calculateSaleItemsTotal($order);
}
}
- Non-Obvious Solutions
class QueryOptimizer { /** * Using a subquery here instead of a join because it performs * 10x faster on large datasets (tested with 1M+ records). * The MySQL optimizer handles this pattern better with our indexes. */ public function getActiveUsersWithRecentOrders() { return User::whereIn('id', function ($query) { $query->select('user_id') ->from('orders') ->where('created_at', '>', now()->subDays(30)) ->groupBy('user_id'); })->get(); }
/**
* We're intentionally NOT eager loading relationships here.
* The polymorphic relation combined with the large dataset
* causes N+1 to actually be faster than the massive join.
* Benchmarked: N+1 = 1.2s, Eager = 8.3s for 10k records.
*/
public function getPolymorphicItems()
{
return Item::where('active', true)->get();
}
}
- Workarounds and Hacks
class PaymentGateway { /** * WORKAROUND: Stripe's API has a bug where amounts over $999,999 * cause a timeout. We split large transactions into multiple charges. * Remove this when Stripe fixes the issue (tracked in STRIPE-12345). */ public function chargeLargeAmount(int $amountInCents): array { if ($amountInCents <= 99999900) { return [$this->charge($amountInCents)]; }
$charges = [];
$remaining = $amountInCents;
while ($remaining > 0) {
$chargeAmount = min($remaining, 99999900);
$charges[] = $this->charge($chargeAmount);
$remaining -= $chargeAmount;
}
return $charges;
}
}
- External Dependencies and Integration Points
class ThirdPartyApiClient { /** * Rate limit: 100 requests per minute (resets at minute boundary) * Docs: https://api.example.com/docs/rate-limits * * The API returns 429 with Retry-After header when limited. * We respect this header and queue retries accordingly. */ public function makeRequest(string $endpoint, array $data = []): array { // Implementation }
/**
* The API expects dates in EST timezone regardless of server location.
* All DateTime objects are converted to EST before sending.
*
* Known issue: DST transitions can cause 1-hour discrepancies.
* The API team is aware but considers it low priority.
*/
public function sendScheduledEvent(DateTime $scheduledAt, array $event): void
{
$scheduledAt->setTimezone(new DateTimeZone('America/New_York'));
// ...
}
}
Self-Documenting Code Techniques
- Descriptive Naming
// BAD: Cryptic names require comments public function calc($u, $i) // Calculate discount for user and items { $d = 0; // discount if ($u->vip) { // if user is VIP $d = 0.1; // 10% discount } return $i * (1 - $d); // Apply discount }
// GOOD: Self-explanatory names public function calculateDiscountedPrice(User $customer, float $originalPrice): float { $discountPercentage = $customer->is_vip ? 0.1 : 0; return $originalPrice * (1 - $discountPercentage); }
- Extract Methods for Clarity
// BAD: Complex condition needs explanation if ($user->created_at > now()->subDays(7) && $user->orders()->count() == 0 && !$user->hasVerifiedEmail()) { // New unverified user without orders $this->sendWelcomeReminder($user); }
// GOOD: Method name explains the condition if ($this->isNewUnengagedUser($user)) { $this->sendWelcomeReminder($user); }
private function isNewUnengagedUser(User $user): bool { return $user->created_at > now()->subDays(7) && $user->orders()->count() == 0 && !$user->hasVerifiedEmail(); }
- Type Declarations and Return Types
// BAD: Unclear what the function accepts and returns function process($data) { // What is $data? What does this return? }
// GOOD: Types make it self-documenting function processOrderItems(Collection $items): OrderSummary { // Clear input and output types }
- Value Objects for Domain Concepts
// BAD: What does this string represent? public function setPrice(string $price) { $this->price = $price; }
// GOOD: Type clarifies the domain concept public function setPrice(Money $price) { $this->price = $price; }
// The Money class documents the concept class Money { public function __construct( private int $cents, private string $currency = 'USD' ) { if ($cents < 0) { throw new InvalidArgumentException('Amount cannot be negative'); } }
public function formatted(): string
{
return number_format($this->cents / 100, 2);
}
}
PHPDoc Best Practices
When to Use PHPDoc
/**
- Process a refund for an order.
- @param Order $order The order to refund
- @param float $amount Amount to refund (null for full refund)
- @param string $reason Reason for the refund (for audit log)
- @throws PaymentGatewayException When payment gateway is unreachable
- @throws InsufficientFundsException When refund amount exceeds paid amount
- @throws RefundWindowExpiredException When refund window (90 days) has passed
- @return Refund The created refund record */ public function processRefund( Order $order, ?float $amount = null, string $reason = 'Customer request' ): Refund { // Complex refund logic }
IDE Helper Annotations
class UserRepository { /** * @return Collection<int, User> */ public function getActiveUsers(): Collection { return User::where('active', true)->get(); }
/**
* @param array<string, mixed> $filters
* @return Builder<User>
*/
public function applyFilters(array $filters): Builder
{
return User::query()->where($filters);
}
}
Deprecation Notices
class PaymentService { /** * @deprecated Since v2.0, use processPaymentWithStripe() instead * @see processPaymentWithStripe() */ public function processPayment($amount) { trigger_error('Method ' . METHOD . ' is deprecated', E_USER_DEPRECATED); return $this->processPaymentWithStripe($amount); } }
API Documentation
- Controller Method Documentation
class ApiController extends Controller { /** * List all products with optional filtering. * * @group Products * @queryParam category string Filter by category slug. Example: electronics * @queryParam min_price number Minimum price filter. Example: 10.00 * @queryParam max_price number Maximum price filter. Example: 100.00 * @queryParam sort string Sort field (price, name, created_at). Default: name * * @response 200 { * "data": [ * { * "id": 1, * "name": "Product Name", * "price": "29.99", * "category": "electronics" * } * ], * "meta": { * "total": 100, * "per_page": 20, * "current_page": 1 * } * } */ public function index(Request $request) { // Implementation } }
- API Resource Documentation
/**
- @property-read int $id
- @property-read string $name
- @property-read Money $price
- @property-read Carbon $created_at
- @property-read Category $category
- @property-read Collection<int, Review> $reviews */ class ProductResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, 'price' => $this->price->formatted(), 'category' => new CategoryResource($this->whenLoaded('category')), 'reviews' => ReviewResource::collection($this->whenLoaded('reviews')), 'created_at' => $this->created_at->toIso8601String(), ]; } }
README Documentation
Project README Template
Project Name
Brief description of what this project does.
Requirements
- PHP 8.2+
- MySQL 8.0+
- Redis 6.0+
- Node.js 18+
Installation
```bash
Clone repository
git clone https://github.com/username/project.git cd project
Install dependencies
composer install npm install
Environment setup
cp .env.example .env php artisan key:generate
Database setup
php artisan migrate --seed
Start development server
php artisan serve ```
Key Features
- Feature 1: Brief description
- Feature 2: Brief description
- Feature 3: Brief description
Architecture Decisions
Why We Use X Instead of Y
Brief explanation of important technical decisions.
Database Design
Key points about the database structure.
Testing
```bash
Run all tests
php artisan test
Run specific test suite
php artisan test --testsuite=Feature
With coverage
php artisan test --coverage ```
Deployment
Instructions for deploying to production.
Troubleshooting
Common Issue 1
Solution to common issue 1.
Common Issue 2
Solution to common issue 2.
Configuration Documentation
// config/custom.php return [ /* |-------------------------------------------------------------------------- | Cache TTL Settings |-------------------------------------------------------------------------- | | These values determine how long various types of data are cached. | The values are in seconds. Shorter values mean fresher data but | more database queries. Adjust based on your needs. | */ 'cache_ttl' => [ 'short' => env('CACHE_TTL_SHORT', 60), // User-specific data 'medium' => env('CACHE_TTL_MEDIUM', 300), // Frequently changing 'long' => env('CACHE_TTL_LONG', 3600), // Rarely changing 'forever' => env('CACHE_TTL_FOREVER', 86400), // Static data ],
/*
|--------------------------------------------------------------------------
| External API Configuration
|--------------------------------------------------------------------------
|
| Configuration for third-party API integrations. Each service has
| its own timeout and retry settings. Credentials are stored in .env
|
*/
'external_apis' => [
'weather' => [
'base_url' => env('WEATHER_API_URL', 'https://api.weather.com'),
'timeout' => 5, // seconds
'retries' => 3,
// Rate limit: 100 requests per minute
],
],
];
Migration Documentation
class CreateOrdersTable extends Migration { public function up(): void { Schema::create('orders', function (Blueprint $table) { $table->id();
// Customer reference - soft delete cascade handled in model
$table->foreignId('user_id')->constrained();
// Status uses enum for type safety (see App\Enums\OrderStatus)
$table->string('status')->default('pending')->index();
// Monetary values stored as integers (cents) to avoid float precision issues
$table->unsignedInteger('subtotal');
$table->unsignedInteger('tax');
$table->unsignedInteger('total');
// Snapshot shipping address as JSON for historical accuracy
// even if customer updates their address later
$table->json('shipping_address');
// Soft deletes for audit trail
$table->softDeletes();
$table->timestamps();
// Composite index for common query pattern
$table->index(['user_id', 'status', 'created_at']);
});
}
}
Testing Documentation
test('document complex test scenarios', function () { /** * Scenario: Test that expired discount codes are rejected * * Given: A discount code that expired yesterday * When: User attempts to apply it to their cart * Then: The code should be rejected with appropriate message * And: The cart total should remain unchanged */
$expiredCode = DiscountCode::factory()->expired()->create();
$cart = Cart::factory()->withItems(3)->create();
$originalTotal = $cart->total;
$response = $this->postJson("/api/cart/{$cart->id}/discount", [
'code' => $expiredCode->code,
]);
$response->assertUnprocessable()
->assertJsonPath('errors.code.0', 'This discount code has expired.');
expect($cart->fresh()->total)->toBe($originalTotal);
});
Best Practices Summary
-
Code should be self-documenting through good naming
-
Document WHY, not WHAT
-
Keep comments close to the code they describe
-
Update documentation when code changes
-
Use tools to generate API documentation
-
Document complex business rules thoroughly
-
Include examples in documentation
-
Document breaking changes clearly
-
Keep README files up to date
-
Document environmental dependencies
Remember: The best documentation is code that doesn't need documentation. Strive for clarity in your code first, then document what remains complex or non-obvious.