Skip to main content

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; }
}
caution

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
}
}
danger

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
}
}
note

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+)', [ /* … */ ]);
}
}
note

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).