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