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 inpackages/.
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_idcolumn, filtered by middleware or traits. - Convention over configuration: Naming follows Laravel standards (e.g.,
CreateProductRequestfor 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).
- Modular: Each feature is in its own package (e.g.,
Use of Routes File (How We Organize Route Creation)
Routes are defined per package inpackages/{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.teamshandles request context and permission-scope consistency. - Select a complete access group such as
exowpee.companyorexowpee.client. It includes authentication, guard selection, active-session and actor checks. - We use
apiResourcefor standard CRUD and custom routes for actions.
Use of Controllers (Simple and Special Cases)
Controllers are in each package’ssrc/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:
CategoryControllerfor 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::storevalidates request, callsProductService::createProduct, returns resource.
packages/Catalog/src/Http/Controllers/ProductController.php:
Use of Requests (…)
Requests are insrc/Http/Requests/ for validation. We use Laravel Form Requests for input sanitization and authorization.
- Organization: One per action (e.g.,
CreateProductRequest,UpdateVariantRequest).rules(): validation rulesauthorize(): check permissions (e.g.,$this->user()->can('create_product'))
Use of Policies (…)
Policies are insrc/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)
- Methods:
Gate::policy(Product::class, ProductPolicy::class);
Why this way : Centralizes authorization, integrated with Laravel Gates and middleware.
Use of Resources (Response Control)
Resources are insrc/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 insrc/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)
- Public: methods called from controllers (e.g.,
ProductService.php :
Use of Enums
Enums are insrc/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).
StatusEnum.php :
Use of Rules
Rules are insrc/Rules/ for custom validation rules.
- Naming:
UniqueSkuRule,ValidVariantOptionsRule, etc. - Usage: In FormRequest rules():
'sku' => [new UniqueSkuRule()]
UniqueSkuRule.php :
Use of Global Helpers (In Shared)
Global helpers are inpackages/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)
Helpers.php (loaded via autoload):
Communication Between Modules (Via Interface Named After the Module)
Inter-module communication is via interfaces (insrc/Contracts/ of each package).
- Naming: Interface is named after the module (e.g.,
IamInterfacefor IAM) - Usage: Dependency injection (DI) via interfaces for decoupling.
packages/Iam/src/Contracts/IamInterface.php:
packages/Iam/src/Services/IamService.php (implements the interface):
CatalogServiceProvider: