laravel:documentation-best-practices

安装量: 43
排名: #17059

安装

npx skills add https://github.com/jpcaparas/superpowers-laravel --skill laravel: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);
    }
}

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

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

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

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

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

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

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

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

返回排行榜