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.
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.
isRegisteredForDiscountsnotdiscount() - Enums:
TitleCasekeys —FavoritePerson,Normal,Lottery - Actions:
{Verb}{Noun}Action—StoreOrderAction,ApplyLotteryAction - Managers:
{Domain}Manager—OrderManager,LotteryManager - Form Requests:
{Verb}{Resource}Request—StoreOrderRequest,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()notDB::table()- Eager-load to prevent N+1:
$orders->load('orderDetails.productVariation') - Migrations in
database/migrations/; data-only migrations indatabase/migrations/data_migration/ - New models: create factory + seeder alongside (
php artisan make:model -mfs {Name})
Enums¶
Enum keys: TitleCase. Backed by string or int.
Frontend (Vue 3 + Inertia)¶
- Components in
resources/js/Pages/(Inertia pages) orresources/js/Components/ - Single root element per component
- Navigation:
<Link href="../...">orrouter.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 — useconfig('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)