Skip to content

Code Standards

PHP Conventions

Constructors

Use PHP 8 constructor property promotion. No empty __construct().

// Correct
public function __construct(
    public readonly CartManager $cartManager,
    private readonly OrderManager $orderManager,
) {}

// Wrong
public function __construct(CartManager $cartManager) {
    $this->cartManager = $cartManager;
}

Return Types

Always declare explicit return type on methods and functions.

public function apply(LotteryDTO $dto): Lottery
public function findActive(): ?Product
protected function rules(): array

Control Structures

Always use curly braces, even for one-liners.

if ($condition) {
    return null;
}

Comments

PHPDoc blocks over inline comments. Never narrate what code does — only document the non-obvious why.

/**
 * @param array<int, CartItemDTO> $items
 * @return Collection<int, CartItem>
 */
public function addItems(array $items): Collection

Naming

  • Variables and methods: descriptive. isRegisteredForDiscounts not discount()
  • Enums: TitleCase keys — FavoritePerson, Normal, Lottery
  • Actions: {Verb}{Noun}ActionStoreOrderAction, ApplyLotteryAction
  • Managers: {Domain}ManagerOrderManager, LotteryManager
  • Form Requests: {Verb}{Resource}RequestStoreOrderRequest, CartItemRequest

Architecture Patterns

Actions (CQRS-style)

Single-responsibility. Accept a DTO, perform exactly one operation.

final class StoreOrderAction
{
    public function __construct(private readonly OrderManager $manager) {}

    public function execute(OrderDTO $dto): Order
    {
        // ...
    }
}

DTOs (spatie/laravel-data)

All data crossing module boundaries goes through a DTO.

class CartItemDTO extends Data
{
    public function __construct(
        public readonly int $productVariationId,
        public readonly int $quantity,
        #[Rule(['required', 'string'])]
        public readonly ?string $ckcShotType,
    ) {}
}

Managers

Coordinate multi-step flows. Injectable via interface. Not stateless — do not use as Octane singletons without verifying they carry no per-request state.

QueryBuilders

Scope logic goes in the custom QueryBuilder, not in the model or controller.

class ProductQueryBuilder extends Builder
{
    public function publishedAndActive(): static
    {
        return $this->where('publish_type', PublishTypeEnum::Public)
                    ->where('status', 'active');
    }
}

Interfaces + Providers

Module-level contracts are defined in Interfaces/ and bound in the module's Providers/ServiceProvider.php. Use the interface as the type hint; the concrete is resolved by IoC.

Validation

  • Form Request for every mutating route — no inline $request->validate() in controllers
  • Custom Rule classes for business logic — not in Form Requests
  • Check app/Rules/ for existing rules before writing a new one
  • Purchase limits: use existing PurchaseLimitRule / BuyableRule

Database

  • Eloquent models and relationships before raw DB:: queries
  • Model::query() not DB::table()
  • Eager-load to prevent N+1: $orders->load('orderDetails.productVariation')
  • Migrations in database/migrations/; data-only migrations in database/migrations/data_migration/
  • New models: create factory + seeder alongside (php artisan make:model -mfs {Name})

Enums

enum SaleTypeEnum: string
{
    case Normal = 'normal';
    case Lottery = 'lottery';
}

Enum keys: TitleCase. Backed by string or int.

Frontend (Vue 3 + Inertia)

  • Components in resources/js/Pages/ (Inertia pages) or resources/js/Components/
  • Single root element per component
  • Navigation: <Link href="../..."> or router.visit() — no <a href>
  • Forms: router.post('/path', formData) — no standard <form action>
  • Named routes in JS: route('route.name') via Ziggy

Testing

PHPUnit (not Pest). Create with php artisan make:test --phpunit {Name}.

  • Feature tests cover happy path, failure path, and edge cases
  • Use factories for model creation — check for existing factory states first
  • Run the minimal filter before considering a test done: php artisan test --filter=testName
  • Current state: Zero real tests exist. Priority for new code: Payment, Order, Lottery.

Formatting

Run vendor/bin/pint --dirty before finalising any change. Do not run --test — run it and fix.

Security

  • Never call env() outside config files — use config('section.key')
  • Validate at system boundaries: user input + external API responses
  • Use $this->authorize() / policies for resource-level auth
  • Parameterised queries via Eloquent — no string interpolation in SQL
  • Presigned S3 URLs with expiry for private files (5-minute TTL)