DDD Layer Details
The plugin's domain is large — roughly 48 entities, 37 repositories and
~27 application services span commerce plus the full ERP suite
(Invoicing/myDATA, Inventory/WMS, Purchasing, Accounting/GL, CRM, HR). The layers
below use Product as the running example, but the same entity → repository →
service → controller stack repeats across every domain.
Domain Layer
The innermost layer — business entities with identity and lifecycle. They live in
WPECommerce\Core\Domain\Entities (the Domain/ layer currently contains the
Entities/ folder; there is no separate value-object or repository-interface
folder today).
Entities
namespace WPECommerce\Core\Domain\Entities;
class Product
{
private int $id;
private string $title;
private int $stockQuantity;
public function getTitle(): string { return $this->title; }
public function getStockQuantity(): int { return $this->stockQuantity; }
}
Always use getter methods to access entity properties. Never use direct property access or array syntax.
// Correct
$product->getTitle();
$product->getFeaturedImageId();
// Wrong — will cause errors
$product['title'];
$product->title; // properties are private
Application Layer
Orchestrates use cases by coordinating domain objects. Services live in
WPECommerce\Core\Application\Services.
namespace WPECommerce\Core\Application\Services;
class CartService
{
public function addItem(
int $productId,
?int $variantId, // variant BEFORE quantity!
int $quantity
): Cart {
// Business logic here
}
}
CartService::addItem() signature: addItem($product_id, $variant_id, $quantity)
— variant before quantity. This is a common source of bugs.
Infrastructure Layer
Implements data access with real database operations. Repositories live in
WPECommerce\Core\Infrastructure\Repositories — note the namespace is
Repositories, not Persistence.
namespace WPECommerce\Core\Infrastructure\Repositories;
use WPECommerce\Core\Domain\Entities\Product;
class ProductRepository
{
public function find(int $id): ?Product
{
// Uses find($id), NOT findById()
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ec_products WHERE id = %d",
$id
));
return $row ? $this->hydrate($row) : null;
}
public function save(Product $product): bool
{
// Returns bool — the ID is set on the entity internally
}
}
Repositories are currently concrete classes in Infrastructure\Repositories
— the codebase does not (yet) define a separate RepositoryInterface layer under
Domain. Depend on the concrete repository class, and resolve it through the
container (see Dependency Injection).
Presentation Layer
REST controllers handle HTTP and delegate to services. They live in
WPECommerce\Core\Presentation\Api and register into the plugin's REST namespace
via the WPEC_REST_NAMESPACE constant (= ec/v1), not a hardcoded string.
Product routes, for example, are registered by RestApiController:
namespace WPECommerce\Core\Presentation\Api;
class RestApiController
{
public function registerRoutes(): void // camelCase
{
register_rest_route(WPEC_REST_NAMESPACE, '/products', [ /* … */ ]);
register_rest_route(WPEC_REST_NAMESPACE, '/products/(?P<id>\d+)', [ /* … */ ]);
}
}
Grepping for the literal 'ec/v1' under-counts the API surface — controllers use
the WPEC_REST_NAMESPACE constant. There are 37 REST controllers under
Presentation/Api/ (commerce + ERP + HR + CRM + managers/RBAC + email + marketplace).