laravel:documentation-best-practices

Documentation Best Practices

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "laravel:documentation-best-practices" with this command: npx skills add jpcaparas/superpowers-laravel/jpcaparas-superpowers-laravel-laravel-documentation-best-practices

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

  1. 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);
}

}

  1. 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();
}

}

  1. 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;
}

}

  1. 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

  1. 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); }

  1. 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(); }

  1. 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 }

  1. 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&#x3C;string, mixed> $filters
 * @return Builder&#x3C;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

  1. 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 } }

  1. 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

laravel:routes-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

laravel:blade-components-and-layouts

No summary provided by upstream source.

Repository SourceNeeds Review
General

laravel:queues-and-horizon

No summary provided by upstream source.

Repository SourceNeeds Review
General

laravel:quality-checks

No summary provided by upstream source.

Repository SourceNeeds Review