Pentest d’Application Web : Méthodologie et Outils
Introduction : L'Art du Hacking Éthique Le pentest (test d'intrusion) est une évaluation de sécurité…
WordPress alimente plus de 43% du web en 2025, mais son architecture héritée pose des défis significatifs pour les applications d’entreprise modernes. Cet article explore comment appliquer les principes SOLID, l’injection de dépendances (DI) et les design patterns avancés pour transformer WordPress en une plateforme robuste, maintenable et testable.
L’architecture traditionnelle de WordPress – avec ses hooks globaux, ses fonctions procédurales et son couplage fort – nécessite une refonte conceptuelle pour répondre aux standards de développement modernes. En 2025, les équipes de développement WordPress matures adoptent des approches orientées objet qui facilitent la testabilité, la scalabilité et la maintenance à long terme.
Le principe de responsabilité unique stipule qu’une classe ne devrait avoir qu’une seule raison de changer. Dans WordPress, cela signifie séparer la logique métier de l’infrastructure WordPress.
Anti-pattern classique :
construct() {
addaction('init', [$this, 'registerposttype']);
addaction('savepost', [$this, 'saveproduct']);
addfilter('thecontent', [$this, 'modifycontent']);
}
public function registerposttype() {
// Enregistrement du CPT
}
public function saveproduct($postid) {
// Validation
// Sanitization
// Sauvegarde en base
// Envoi d'email
// Appel API externe
// Génération de cache
}
}
Pattern recommandé avec SRP :
Value Object représentant un produit
/
class Product {
private string $id;
private string $name;
private Money $price;
private ProductStatus $status;
public function construct(
string $id,
string $name,
Money $price,
ProductStatus $status
) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->status = $status;
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getPrice(): Money {
return $this->price;
}
public function isPublished(): bool {
return $this->status->equals(ProductStatus::PUBLISHED());
}
public function publish(): self {
return new self(
$this->id,
$this->name,
$this->price,
ProductStatus::PUBLISHED()
);
}
}
/
Repository pour la persistance des produits
/
class ProductRepository {
private wpdb $wpdb;
public function construct(wpdb $wpdb) {
$this->wpdb = $wpdb;
}
public function save(Product $product): void {
$data = [
'posttitle' => $product->getName(),
'posttype' => 'product',
'poststatus' => $product->isPublished() ? 'publish' : 'draft',
'metainput' => [
'price' => $product->getPrice()->getAmount(),
'currency' => $product->getPrice()->getCurrency(),
],
];
if ($product->getId()) {
$data['ID'] = $product->getId();
wpupdatepost($data);
} else {
wpinsertpost($data);
}
}
public function findById(string $id): ?Product {
$post = getpost($id);
if (!$post || $post->posttype !== 'product') {
return null;
}
return $this->hydrateFromPost($post);
}
private function hydrateFromPost(WPPost $post): Product {
$price = new Money(
(float) getpostmeta($post->ID, 'price', true),
getpostmeta($post->ID, 'currency', true) ?: 'EUR'
);
return new Product(
(string) $post->ID,
$post->posttitle,
$price,
ProductStatus::fromWordPressStatus($post->poststatus)
);
}
}
/
Service applicatif orchestrant la logique métier
/
class ProductService {
private ProductRepository $repository;
private ProductValidator $validator;
private ProductNotifier $notifier;
public function construct(
ProductRepository $repository,
ProductValidator $validator,
ProductNotifier $notifier
) {
$this->repository = $repository;
$this->validator = $validator;
$this->notifier = $notifier;
}
public function createProduct(CreateProductCommand $command): Product {
// Validation
$errors = $this->validator->validate($command);
if (!empty($errors)) {
throw new ValidationException($errors);
}
// Création du domaine
$product = new Product(
'',
$command->getName(),
new Money($command->getPrice(), $command->getCurrency()),
ProductStatus::DRAFT()
);
// Persistance
$this->repository->save($product);
// Notification
$this->notifier->notifyCreation($product);
return $product;
}
public function publishProduct(string $productId): Product {
$product = $this->repository->findById($productId);
if (!$product) {
throw new ProductNotFoundException($productId);
}
$publishedProduct = $product->publish();
$this->repository->save($publishedProduct);
$this->notifier->notifyPublication($publishedProduct);
return $publishedProduct;
}
}
Les entités doivent être ouvertes à l’extension mais fermées à la modification. Dans WordPress, utilisez des interfaces et des stratégies plutôt que de modifier le code existant.
Interface pour les stratégies de tarification
/
interface PricingStrategyInterface {
public function calculatePrice(Product $product, Customer $customer): Money;
}
/
Stratégie de tarification standard
/
class StandardPricingStrategy implements PricingStrategyInterface {
public function calculatePrice(Product $product, Customer $customer): Money {
return $product->getPrice();
}
}
/
Stratégie de tarification avec remise volume
/
class VolumePricingStrategy implements PricingStrategyInterface {
private array $volumeDiscounts;
public function construct(array $volumeDiscounts) {
$this->volumeDiscounts = $volumeDiscounts;
}
public function calculatePrice(Product $product, Customer $customer): Money {
$basePrice = $product->getPrice();
$quantity = $customer->getCartQuantity($product);
foreach ($this->volumeDiscounts as $threshold => $discountPercent) {
if ($quantity >= $threshold) {
return $basePrice->multiply(1 - ($discountPercent / 100));
}
}
return $basePrice;
}
}
/
Stratégie de tarification B2B
/
class B2BPricingStrategy implements PricingStrategyInterface {
private float $b2bDiscount;
public function construct(float $b2bDiscount = 0.15) {
$this->b2bDiscount = $b2bDiscount;
}
public function calculatePrice(Product $product, Customer $customer): Money {
if (!$customer->isB2B()) {
return $product->getPrice();
}
return $product->getPrice()->multiply(1 - $this->b2bDiscount);
}
}
/
Calculateur de prix utilisant une stratégie
/
class PriceCalculator {
private PricingStrategyInterface $strategy;
public function construct(PricingStrategyInterface $strategy) {
$this->strategy = $strategy;
}
public function setStrategy(PricingStrategyInterface $strategy): void {
$this->strategy = $strategy;
}
public function calculate(Product $product, Customer $customer): Money {
return $this->strategy->calculatePrice($product, $customer);
}
}
// Utilisation
$calculator = new PriceCalculator(new StandardPricingStrategy());
$price = $calculator->calculate($product, $customer);
// Changement dynamique de stratégie
if ($customer->isB2B()) {
$calculator->setStrategy(new B2BPricingStrategy(0.20));
}
Les objets d’une classe dérivée doivent pouvoir remplacer les objets de la classe de base sans altérer le comportement du programme.
Interface de cache
/
interface CacheInterface {
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): bool;
public function delete(string $key): bool;
public function flush(): bool;
}
/
Implémentation avec transients WordPress
/
class WordPressTransientCache implements CacheInterface {
private string $prefix;
public function construct(string $prefix = 'app') {
$this->prefix = $prefix;
}
public function get(string $key): mixed {
return gettransient($this->prefix . $key);
}
public function set(string $key, mixed $value, int $ttl = 3600): bool {
return settransient($this->prefix . $key, $value, $ttl);
}
public function delete(string $key): bool {
return deletetransient($this->prefix . $key);
}
public function flush(): bool {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE optionname LIKE %s
OR optionname LIKE %s",
$wpdb->esclike('transient' . $this->prefix) . '%',
$wpdb->esclike('transienttimeout' . $this->prefix) . '%'
)
);
return true;
}
}
/
Implémentation avec Redis
/
class RedisCache implements CacheInterface {
private Redis $redis;
private string $prefix;
public function construct(Redis $redis, string $prefix = 'app:') {
$this->redis = $redis;
$this->prefix = $prefix;
}
public function get(string $key): mixed {
$value = $this->redis->get($this->prefix . $key);
return $value !== false ? unserialize($value) : false;
}
public function set(string $key, mixed $value, int $ttl = 3600): bool {
return $this->redis->setex(
$this->prefix . $key,
$ttl,
serialize($value)
);
}
public function delete(string $key): bool {
return (bool) $this->redis->del($this->prefix . $key);
}
public function flush(): bool {
$keys = $this->redis->keys($this->prefix . '');
if (empty($keys)) {
return true;
}
return (bool) $this->redis->del($keys);
}
}
/
Service utilisant le cache (LSP en action)
/
class ProductCacheService {
private CacheInterface $cache;
private ProductRepository $repository;
public function construct(
CacheInterface $cache,
ProductRepository $repository
) {
$this->cache = $cache;
$this->repository = $repository;
}
public function getProduct(string $id): ?Product {
$cacheKey = "product{$id}";
// Tentative de récupération depuis le cache
$cached = $this->cache->get($cacheKey);
if ($cached !== false) {
return $cached;
}
// Récupération depuis la base de données
$product = $this->repository->findById($id);
if ($product) {
$this->cache->set($cacheKey, $product, 3600);
}
return $product;
}
}
Les clients ne doivent pas dépendre d’interfaces qu’ils n’utilisent pas. Créez des interfaces spécifiques plutôt qu’une interface monolithique.
Interface minimale pour la lecture
/
interface ProductReaderInterface {
public function findById(string $id): ?Product;
public function findAll(array $criteria = []): array;
}
/
Interface pour l'écriture
/
interface ProductWriterInterface {
public function save(Product $product): void;
public function delete(string $id): void;
}
/
Interface pour les opérations batch
/
interface ProductBulkOperationsInterface {
public function saveMultiple(array $products): void;
public function deleteMultiple(array $ids): void;
}
/
Interface pour les statistiques
/
interface ProductStatisticsInterface {
public function countByStatus(ProductStatus $status): int;
public function getAveragePrice(): Money;
public function getMostExpensive(int $limit = 10): array;
}
/
Repository complet implémentant toutes les interfaces
/
class ProductRepository implements
ProductReaderInterface,
ProductWriterInterface,
ProductBulkOperationsInterface,
ProductStatisticsInterface
{
// Implémentation complète
}
/
Service de lecture seule
/
class ProductDisplayService {
private ProductReaderInterface $reader;
public function construct(ProductReaderInterface $reader) {
$this->reader = $reader;
}
public function displayProduct(string $id): void {
$product = $this->reader->findById($id);
// Affichage
}
}
/
Service d'administration
/
class ProductAdminService {
private ProductReaderInterface $reader;
private ProductWriterInterface $writer;
public function construct(
ProductReaderInterface $reader,
ProductWriterInterface $writer
) {
$this->reader = $reader;
$this->writer = $writer;
}
public function updateProduct(string $id, array $data): void {
$product = $this->reader->findById($id);
// Mise à jour
$this->writer->save($product);
}
}
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.
Interface d'abstraction pour l'envoi d'emails
/
interface EmailSenderInterface {
public function send(Email $email): bool;
}
/
Value Object représentant un email
/
class Email {
private string $to;
private string $subject;
private string $body;
private array $headers;
public function construct(
string $to,
string $subject,
string $body,
array $headers = []
) {
$this->to = $to;
$this->subject = $subject;
$this->body = $body;
$this->headers = $headers;
}
public function getTo(): string { return $this->to; }
public function getSubject(): string { return $this->subject; }
public function getBody(): string { return $this->body; }
public function getHeaders(): array { return $this->headers; }
}
/
Implémentation avec wpmail
/
class WordPressEmailSender implements EmailSenderInterface {
public function send(Email $email): bool {
return wpmail(
$email->getTo(),
$email->getSubject(),
$email->getBody(),
$email->getHeaders()
);
}
}
/
Implémentation avec SendGrid
/
class SendGridEmailSender implements EmailSenderInterface {
private SendGrid $sendgrid;
public function construct(string $apiKey) {
$this->sendgrid = new SendGrid($apiKey);
}
public function send(Email $email): bool {
$sendGridEmail = new SendGridMailMail();
$sendGridEmail->setFrom("noreply@example.com");
$sendGridEmail->setSubject($email->getSubject());
$sendGridEmail->addTo($email->getTo());
$sendGridEmail->addContent("text/html", $email->getBody());
try {
$response = $this->sendgrid->send($sendGridEmail);
return $response->statusCode() >= 200 && $response->statusCode() < 300;
} catch (Exception $e) {
return false;
}
}
}
/
Service de notification (module haut niveau)
/
class ProductNotifier {
private EmailSenderInterface $emailSender;
private TemplateEngineInterface $templateEngine;
public function construct(
EmailSenderInterface $emailSender,
TemplateEngineInterface $templateEngine
) {
$this->emailSender = $emailSender;
$this->templateEngine = $templateEngine;
}
public function notifyCreation(Product $product): void {
$body = $this->templateEngine->render('emails/product-created.php', [
'product' => $product,
]);
$email = new Email(
getoption('adminemail'),
"Nouveau produit créé : {$product->getName()}",
$body,
['Content-Type: text/html; charset=UTF-8']
);
$this->emailSender->send($email);
}
}
L’injection de dépendances est essentielle pour découpler les composants et faciliter les tests. Implémentons un conteneur DI conforme au PSR-11.
Exception pour les services non trouvés
/
class NotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface {}
/
Exception pour les erreurs de conteneur
/
class ContainerException extends RuntimeException implements ContainerExceptionInterface {}
/
Conteneur d'injection de dépendances PSR-11
/
class Container implements ContainerInterface {
private array $services = [];
private array $factories = [];
private array $parameters = [];
private array $singletons = [];
/
Enregistre un service avec une factory
/
public function set(string $id, callable $factory): void {
$this->factories[$id] = $factory;
}
/
Enregistre un singleton
/
public function singleton(string $id, callable $factory): void {
$this->factories[$id] = $factory;
$this->singletons[$id] = true;
}
/
Enregistre une instance existante
/
public function instance(string $id, object $instance): void {
$this->services[$id] = $instance;
}
/
Enregistre un paramètre
/
public function parameter(string $key, mixed $value): void {
$this->parameters[$key] = $value;
}
/
Récupère un paramètre
/
public function getParameter(string $key): mixed {
if (!isset($this->parameters[$key])) {
throw new NotFoundException("Parameter {$key} not found");
}
return $this->parameters[$key];
}
/
Récupère un service
/
public function get(string $id): mixed {
// Service déjà instancié
if (isset($this->services[$id])) {
return $this->services[$id];
}
// Factory disponible
if (isset($this->factories[$id])) {
$service = $this->factories$id;
// Stockage pour les singletons
if (isset($this->singletons[$id])) {
$this->services[$id] = $service;
}
return $service;
}
// Auto-wiring via réflexion
if (classexists($id)) {
return $this->autowire($id);
}
throw new NotFoundException("Service {$id} not found");
}
/
Vérifie si un service existe
/
public function has(string $id): bool {
return isset($this->services[$id])
|| isset($this->factories[$id])
|| classexists($id);
}
/
Auto-wiring automatique via réflexion
/
private function autowire(string $className): object {
try {
$reflector = new ReflectionClass($className);
if (!$reflector->isInstantiable()) {
throw new ContainerException(
"Class {$className} is not instantiable"
);
}
$constructor = $reflector->getConstructor();
if (!$constructor) {
return new $className();
}
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if (!$type || $type->isBuiltin()) {
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new ContainerException(
"Cannot resolve parameter ${$parameter->getName()} in {$className}"
);
}
} else {
$dependencies[] = $this->get($type->getName());
}
}
return $reflector->newInstanceArgs($dependencies);
} catch (ReflectionException $e) {
throw new ContainerException(
"Error while autowiring {$className}: {$e->getMessage()}",
0,
$e
);
}
}
}
Service Provider pour l'application
/
class ApplicationServiceProvider {
private Container $container;
public function construct(Container $container) {
$this->container = $container;
}
public function register(): void {
$this->registerParameters();
$this->registerInfrastructure();
$this->registerRepositories();
$this->registerServices();
}
private function registerParameters(): void {
$this->container->parameter('app.env', wpgetenvironmenttype());
$this->container->parameter('app.debug', WPDEBUG);
$this->container->parameter('sendgrid.apikey', getenv('SENDGRIDAPIKEY'));
$this->container->parameter('redis.host', getenv('REDISHOST') ?: '127.0.0.1');
$this->container->parameter('redis.port', (int)(getenv('REDISPORT') ?: 6379));
}
private function registerInfrastructure(): void {
// Base de données WordPress
$this->container->singleton(wpdb::class, function() {
global $wpdb;
return $wpdb;
});
// Cache
$this->container->singleton(CacheInterface::class, function(Container $c) {
if (classexists('Redis') && $c->getParameter('app.env') === 'production') {
$redis = new Redis();
$redis->connect(
$c->getParameter('redis.host'),
$c->getParameter('redis.port')
);
return new RedisCache($redis, 'monapp:');
}
return new WordPressTransientCache('monapp');
});
// Email
$this->container->singleton(EmailSenderInterface::class, function(Container $c) {
if ($c->getParameter('sendgrid.apikey')) {
return new SendGridEmailSender($c->getParameter('sendgrid.apikey'));
}
return new WordPressEmailSender();
});
}
private function registerRepositories(): void {
$this->container->singleton(ProductRepository::class, function(Container $c) {
return new ProductRepository($c->get(wpdb::class));
});
}
private function registerServices(): void {
// Les services sont autowirés automatiquement
$this->container->singleton(ProductService::class);
}
}
/
Point d'entrée de l'application
/
class Application {
private static ?Application $instance = null;
private Container $container;
private function construct() {
$this->container = new Container();
$this->bootstrap();
}
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function bootstrap(): void {
$provider = new ApplicationServiceProvider($this->container);
$provider->register();
}
public function get(string $id): mixed {
return $this->container->get($id);
}
public function container(): Container {
return $this->container;
}
}
// Fonction helper globale
function app(string $service = null): mixed {
$app = Application::getInstance();
if ($service === null) {
return $app;
}
return $app->get($service);
}
// Utilisation dans un hook WordPress
addaction('init', function() {
$productService = app(ProductService::class);
// Utilisation du service
});
Query Builder fluide pour WordPress
/
class QueryBuilder {
private string $postType;
private array $metaQuery = [];
private array $taxQuery = [];
private array $orderBy = [];
private int $perPage = -1;
private int $page = 1;
private string $postStatus = 'publish';
public function construct(string $postType) {
$this->postType = $postType;
}
public function whereMeta(string $key, mixed $value, string $compare = '='): self {
$this->metaQuery[] = [
'key' => $key,
'value' => $value,
'compare' => $compare,
];
return $this;
}
public function whereTaxonomy(string $taxonomy, array $terms, string $field = 'termid'): self {
$this->taxQuery[] = [
'taxonomy' => $taxonomy,
'field' => $field,
'terms' => $terms,
];
return $this;
}
public function orderBy(string $field, string $order = 'ASC'): self {
$this->orderBy[$field] = $order;
return $this;
}
public function paginate(int $perPage, int $page = 1): self {
$this->perPage = $perPage;
$this->page = $page;
return $this;
}
public function status(string $status): self {
$this->postStatus = $status;
return $this;
}
public function get(): array {
$args = [
'posttype' => $this->postType,
'poststatus' => $this->postStatus,
'postsperpage' => $this->perPage,
'paged' => $this->page,
'metaquery' => $this->metaQuery,
'taxquery' => $this->taxQuery,
];
if (!empty($this->orderBy)) {
$args['orderby'] = $this->orderBy;
}
$query = new WPQuery($args);
return $query->posts;
}
public function first(): ?WPPost {
$posts = $this->paginate(1)->get();
return $posts[0] ?? null;
}
public function count(): int {
$args = [
'posttype' => $this->postType,
'poststatus' => $this->postStatus,
'metaquery' => $this->metaQuery,
'taxquery' => $this->taxQuery,
'fields' => 'ids',
'postsperpage' => -1,
];
$query = new WPQuery($args);
return $query->foundposts;
}
}
/
Repository abstrait avec Query Builder
/
abstract class AbstractRepository {
protected string $postType;
protected function query(): QueryBuilder {
return new QueryBuilder($this->postType);
}
abstract protected function hydrateFromPost(WPPost $post): object;
public function findById(string $id): ?object {
$post = getpost($id);
if (!$post || $post->posttype !== $this->postType) {
return null;
}
return $this->hydrateFromPost($post);
}
public function findAll(array $criteria = []): array {
$query = $this->query();
foreach ($criteria as $key => $value) {
$query->whereMeta($key, $value);
}
$posts = $query->get();
return arraymap(
fn(WPPost $post) => $this->hydrateFromPost($post),
$posts
);
}
}
Interface pour les événements
/
interface EventInterface {
public function getName(): string;
}
/
Classe abstraite pour les événements
/
abstract class AbstractEvent implements EventInterface {
private float $timestamp;
public function construct() {
$this->timestamp = microtime(true);
}
public function getTimestamp(): float {
return $this->timestamp;
}
public function getName(): string {
return static::class;
}
}
/
Événement de création de produit
/
class ProductCreatedEvent extends AbstractEvent {
private Product $product;
public function construct(Product $product) {
parent::construct();
$this->product = $product;
}
public function getProduct(): Product {
return $this->product;
}
}
/
Interface pour les listeners
/
interface EventListenerInterface {
public function handle(EventInterface $event): void;
}
/
Listener pour l'indexation ElasticSearch
/
class IndexProductListener implements EventListenerInterface {
private ElasticSearchClient $elasticsearch;
public function construct(ElasticSearchClient $elasticsearch) {
$this->elasticsearch = $elasticsearch;
}
public function handle(EventInterface $event): void {
if (!$event instanceof ProductCreatedEvent) {
return;
}
$this->elasticsearch->index('products', [
'id' => $event->getProduct()->getId(),
'name' => $event->getProduct()->getName(),
'price' => $event->getProduct()->getPrice()->getAmount(),
]);
}
}
/
Event Dispatcher
/
class EventDispatcher {
private array $listeners = [];
public function addListener(string $eventName, EventListenerInterface $listener): void {
if (!isset($this->listeners[$eventName])) {
$this->listeners[$eventName] = [];
}
$this->listeners[$eventName][] = $listener;
}
public function dispatch(EventInterface $event): void {
$eventName = $event->getName();
if (!isset($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $listener) {
$listener->handle($event);
}
// Intégration avec les hooks WordPress
doaction('appeventdispatched', $event);
doaction("appevent{$eventName}", $event);
}
}
// Configuration
$dispatcher = app(EventDispatcher::class);
$dispatcher->addListener(
ProductCreatedEvent::class,
new IndexProductListener(app(ElasticSearchClient::class))
);
// Dispatch dans le service
class ProductService {
private EventDispatcher $dispatcher;
public function createProduct(CreateProductCommand $command): Product {
// Création...
$this->dispatcher->dispatch(new ProductCreatedEvent($product));
return $product;
}
}
repository = $this->createMock(ProductRepository::class);
// Mock du validator
$this->validator = $this->createMock(ProductValidator::class);
$this->validator->method('validate')->willReturn([]);
// Mock du notifier
$this->notifier = $this->createMock(ProductNotifier::class);
// Service avec dépendances mockées
$this->service = new ProductService(
$this->repository,
$this->validator,
$this->notifier
);
}
public function testCreateProductSavesToRepository(): void {
$command = new CreateProductCommand(
'Test Product',
99.99,
'EUR'
);
// Assertion : le repository doit être appelé
$this->repository
->expects($this->once())
->method('save')
->with($this->isInstanceOf(Product::class));
$product = $this->service->createProduct($command);
$this->assertInstanceOf(Product::class, $product);
$this->assertEquals('Test Product', $product->getName());
}
public function testCreateProductSendsNotification(): void {
$command = new CreateProductCommand(
'Test Product',
99.99,
'EUR'
);
// Assertion : le notifier doit être appelé
$this->notifier
->expects($this->once())
->method('notifyCreation')
->with($this->isInstanceOf(Product::class));
$this->service->createProduct($command);
}
public function testCreateProductWithInvalidDataThrowsException(): void {
$this->validator
->method('validate')
->willReturn(['name' => 'Name is required']);
$command = new CreateProductCommand('', 99.99, 'EUR');
$this->expectException(ValidationException::class);
$this->service->createProduct($command);
}
}
L’application des principes SOLID et des design patterns avancés transforme WordPress d’une plateforme procédurale en une architecture moderne et maintenable. Les bénéfices incluent :
Cette approche nécessite un investissement initial supérieur mais garantit une base de code pérenne pour les projets WordPress de grande envergure en 2025 et au-delà.
Mots-clés : WordPress architecture, SOLID principles PHP, dependency injection WordPress, design patterns WordPress, enterprise WordPress development, PSR-11 container, repository pattern, WordPress testing, modern PHP development, WordPress DDD
Meta Description* : Guide complet d’architecture WordPress moderne avec principes SOLID, injection de dépendances PSR-11, design patterns avancés et code production-ready pour développeurs experts.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.