Overview

This page provides a detailed look at the code organization and conventions used in the Exowpee Laravel backend project. It’s designed for developers, contributors, or anyone maintaining the code. We follow Laravel best practices with a modular approach using packages for each feature (IAM, Catalog, Customer, Order, Ledger, Team, etc.). The project is a monorepo with the core Laravel app and packages in packages/. The code emphasizes readability, scalability, and multi-tenant isolation. Key conventions include RBAC (roles-based access control) via policies, cursor pagination for large datasets, enums for constants, and interfaces for module communication to avoid tight coupling. All modules are scoped to the user’s current company. For beginners: The code is organized to make it easy to add new features or fix bugs in one module without affecting others. Start by exploring packages/ for the business logic.
  • Key Principles:
    • Modular: Each feature is in its own package (e.g., packages/Iam).
    • Multi-tenant: Every model has a company_id column, filtered by middleware or traits.
    • Convention over configuration: Naming follows Laravel standards (e.g., CreateProductRequest for validation).
    • Testing: Each package has its own tests in tests/.
    • Dependencies: Packages depend on each other via composer.json (e.g., Catalog depends on Iam for permissions).
The project uses Laravel 10+ with PHP 8.1+, and tools like Pest for testing, Laravel Pint for style, and GitLab CI for pipelines.

Use of Routes File (How We Organize Route Creation)

Routes are defined per package in packages/{package}/src/Http/routes.php.
  • All routes are API-only (no web routes in packages).
  • Grouped by prefix (e.g. /v1/catalog/ for Catalog).
  • The parent exowpee.teams handles request context and permission-scope consistency.
  • Select a complete access group such as exowpee.company or exowpee.client. It includes authentication, guard selection, active-session and actor checks.
  • We use apiResource for standard CRUD and custom routes for actions.
Company-scoped route example:

Use of Controllers (Simple and Special Cases)

Controllers are in each package’s src/Http/Controllers/. We use single-action controllers for complex logic, or standard resource controllers for CRUD.
  • Simple cases: Standard resource controllers (extend Controller, use methods index, show, store, update, destroy). Example: CategoryController for basic CRUD.
  • Special cases: For complex operations (e.g., product creation with variants), we inject services (e.g., ProductService) to handle business logic, keeping controllers thin. Example: ProductController::store validates request, calls ProductService::createProduct, returns resource.
Example from packages/Catalog/src/Http/Controllers/ProductController.php:
Why this way : Controllers focus on HTTP (request/response), services on business rules. Makes testing easier (mock services).

Use of Requests (…)

Requests are in src/Http/Requests/ for validation. We use Laravel Form Requests for input sanitization and authorization.
  • Organization: One per action (e.g., CreateProductRequest, UpdateVariantRequest).
    • rules(): validation rules
    • authorize(): check permissions (e.g., $this->user()->can('create_product'))
Example:
Why this way: Keeps validation logic separate from controllers, automatic error responses (422 with messages).

Use of Policies (…)

Policies are in src/Policies/ for authorization (RBAC).
  • Organization: One per model (e.g., ProductPolicy).
    • Methods: viewAny, view, create, update, delete
    • Use $user->can('permission') or custom logic (e.g., check company_id)
Example:
Registration: In the ServiceProvider of the package, Gate::policy(Product::class, ProductPolicy::class); Why this way : Centralizes authorization, integrated with Laravel Gates and middleware.

Use of Resources (Response Control)

Resources are in src/Http/Resources/. Migrated Resources extend Exowpee\Shared\Http\Response\ApiResource, declare a manifest() (fields with the columns, relations and batch loaders they read; includes) and build toArray() from $this->field() / $this->relation(). The controller declares ResponseMiddleware and makes one ApiResponse call, which fixes the JSON shape:
ApiResponse::paginated() renders a cursor page as {data, meta: {per_page, next_cursor, prev_cursor}}, so no dedicated {X}Collection class is needed. Clients pick relations with ?include= and root keys with ?fields=. Why this way : Cursor pagination is efficient on large datasets (no offset), and the manifest lets the response loader select only the columns and relations the client asked for, without N+1 queries. See docs/developer-guide/response-control-migration.md for the migration procedure.

Use of Services (Naming, Public Methods Above Private)

Services are in src/Services/ for business logic.
  • Naming: ProductService, VariantService, etc.
  • Convention: Public methods above private.
    • Public: methods called from controllers (e.g., createProduct)
    • Private: internal helpers (e.g., validateVariants)
Example ProductService.php :

Use of Enums

Enums are in src/Enums/ for constants (PHP 8.1+ enums).
  • Naming: StatusEnum, TypeEnum, etc.
  • Usage: For statuses (e.g., “published” / “draft” for products), types (“simple” / “variable” for products).
Example StatusEnum.php :
Usage in model :
Why this way : Enums are type-safe, IDE-friendly, and avoid magic strings.

Use of Rules

Rules are in src/Rules/ for custom validation rules.
  • Naming: UniqueSkuRule, ValidVariantOptionsRule, etc.
  • Usage: In FormRequest rules(): 'sku' => [new UniqueSkuRule()]
Example UniqueSkuRule.php :

Use of Global Helpers (In Shared)

Global helpers are in packages/Shared/src/Helpers/ (the Shared package for common code).
  • Naming: ksuid(), company_id(), has_permission(string $permission)
  • Usage: Called directly (e.g., ksuid() to generate IDs)
Example Helpers.php (loaded via autoload):

Communication Between Modules (Via Interface Named After the Module)

Inter-module communication is via interfaces (in src/Contracts/ of each package).
  • Naming: Interface is named after the module (e.g., IamInterface for IAM)
  • Usage: Dependency injection (DI) via interfaces for decoupling.
Example: Catalog uses IAM for permissions. In packages/Iam/src/Contracts/IamInterface.php:
In packages/Iam/src/Services/IamService.php (implements the interface):
In Catalog, in a controller :
Registration: In CatalogServiceProvider: