Maîtrisez les règles et principes pour un code propre
Ensemble de pratiques pour écrire un code clair, lisible et maintenable
public function a($r) {
$u = $this->em->find(User::class, $r->get('id'));
$d = ['n' => $u->getName()];
return $this->json($d);
}
public function getUserDetails(Request $request): JsonResponse {
$user = $this->entityManager->find(User::class, $request->get('id'));
$userData = ['name' => $user->getName()];
return $this->json($userData);
}
public function canEdit(User $user, Article $article): bool {
if ($user->isAdmin() ||
($user->getId() === $article->getAuthor()->getId() &&
$article->getStatus() === 'draft' &&
$article->getCreatedAt() > new DateTime('-24 hours'))) {
return true;
}
return false;
}
public function canEdit(User $user, Article $article): bool {
$isAdmin = $user->isAdmin();
$isAuthor = $user->getId() === $article->getAuthor()->getId();
$isDraft = $article->getStatus() === 'draft';
$isRecent = $article->getCreatedAt() > new DateTime('-24 hours');
$isEditable = $isAuthor && $isDraft && $isRecent;
return $isAdmin || $isEditable;
}
class OrderService
{
public function processOrder(Order $order): void
{
// Validation
if (!$order->getItems()) {
throw new Exception('No items');
}
// Calcul du prix
$total = 0;
foreach ($order->getItems() as $item) {
$total += $item->getPrice() * $item->getQuantity();
}
// Envoi d'email
$mailer = new Mailer();
$mailer->send($order->getCustomer()->getEmail(), 'Order confirmed');
// Sauvegarde
$this->em->persist($order);
$this->em->flush();
}
}
class OrderService
{
public function __construct(
private OrderValidator $validator,
private PriceCalculator $calculator,
private NotificationService $notifier,
private EntityManagerInterface $em
) {}
public function processOrder(Order $order): void
{
$this->validator->validate($order);
$order->setTotal($this->calculator->calculate($order));
$this->notifier->sendOrderConfirmation($order);
$this->em->persist($order);
$this->em->flush();
}
}
✅ Chaque classe a une seule responsabilité : validation, calcul, notification, persistance
class ProductRepository
{
public function findActiveProducts(): array
{
return $this->createQueryBuilder('p')
->where('p.active = :active')
->andWhere('p.deletedAt IS NULL')
->setParameter('active', true)
->getQuery()
->getResult();
}
public function findActiveProductsByCategory($categoryId): array
{
// Répétition de la même logique !
return $this->createQueryBuilder('p')
->where('p.active = :active')
->andWhere('p.deletedAt IS NULL')
->andWhere('p.category = :category')
->setParameter('active', true)
->setParameter('category', $categoryId)
->getQuery()
->getResult();
}
}
class ProductRepository
{
private function createActiveProductsQuery(): QueryBuilder
{
return $this->createQueryBuilder('p')
->where('p.active = :active')
->andWhere('p.deletedAt IS NULL')
->setParameter('active', true);
}
public function findActiveProducts(): array
{
return $this->createActiveProductsQuery()
->getQuery()
->getResult();
}
public function findActiveProductsByCategory($categoryId): array
{
return $this->createActiveProductsQuery()
->andWhere('p.category = :category')
->setParameter('category', $categoryId)
->getQuery()
->getResult();
}
}
✅ La logique commune est extraite et réutilisée
public function getUserDiscount(User $user)
{
if ($user->isPremium()) {
return 20; // Return 1 - Quel return a été exécuté ?
}
if ($user->getOrderCount() > 10) {
return 15; // Return 2
}
if ($user->isFirstOrder()) {
return 10; // Return 3
}
return 0; // Return 4 - Difficile à déboguer !
}
// ❌ Problèmes : Difficile à déboguer, complexe à tester
public function getUserDiscount(User $user): int
{
$discount = 0;
if ($user->isPremium()) {
$discount = 20;
} elseif ($user->getOrderCount() > 10) {
$discount = 15;
} elseif ($user->isFirstOrder()) {
$discount = 10;
}
return $discount; // Un seul point de sortie !
}
// ✅ Plus facile à déboguer et à tester
// ✅ Exception : Early returns pour les garde-fous
public function processOrder(Order $order): bool
{
// Garde-fous en début de fonction ✅
if (!$order->hasItems()) {
return false;
}
if (!$order->getCustomer()) {
return false;
}
if ($order->getTotal() <= 0) {
return false;
}
// Logique métier principale
$this->calculateTotal($order);
$this->notifyCustomer($order);
$this->saveOrder($order);
return true; // Un seul return à la fin de la logique
}
// ✅ Évite l'imbrication excessive
class ProductRepository
{
public function findById($id)
{
return $this->find($id); // Product ou null ?
}
public function findActive()
{
return $this->findBy(['active' => true]); // Array ? Collection ?
}
public function count()
{
return $this->createQueryBuilder('p')
->select('COUNT(p.id)')
->getQuery()
->getSingleScalarResult(); // String ? Int ?
}
}
class ProductRepository
{
public function findById(int $id): ?Product
{
return $this->find($id); // Clairement Product ou null
}
/** @return Product[] */
public function findActive(): array
{
return $this->findBy(['active' => true]); // Tableau de Product
}
public function count(): int
{
return (int) $this->createQueryBuilder('p')
->select('COUNT(p.id)')
->getQuery()
->getSingleScalarResult(); // Clairement un entier
}
}
public function register(Request $request): Response
{
$user = new User();
$user->setEmail($request->get('email'));
$user->setFirstName($request->get('firstName'));
$user->setLastName($request->get('lastName'));
$user->setPhone($request->get('phone'));
$user->setAddress($request->get('address'));
$user->setCity($request->get('city'));
$user->setPostalCode($request->get('postalCode'));
$user->setCountry($request->get('country'));
$user->setPassword(
$this->hasher->hash($request->get('password'))
);
$this->em->persist($user);
$this->em->flush();
return $this->json($user);
}
// Solution 1 : DTO (Data Transfer Object)
class RegisterUserDTO
{
public function __construct(
public readonly string $email,
public readonly string $firstName,
public readonly string $lastName,
public readonly string $phone,
public readonly string $address,
public readonly string $city,
public readonly string $postalCode,
public readonly string $country,
public readonly string $password,
) {}
}
class UserFactory
{
public function createFromDTO(RegisterUserDTO $dto): User
{
$user = new User();
$user->setEmail($dto->email);
$user->setFirstName($dto->firstName);
$user->setLastName($dto->lastName);
// ... autres setters
$user->setPassword($this->hasher->hash($dto->password));
return $user;
}
}
public function register(RegisterUserDTO $dto): Response
{
$user = $this->userFactory->createFromDTO($dto);
$this->em->persist($user);
$this->em->flush();
return $this->json($user);
}
// Solution 2 : Constructeur avec paramètres nommés (PHP 8+)
class User
{
public function __construct(
private string $email,
private string $firstName,
private string $lastName,
private string $phone,
private string $address,
private string $city,
private string $postalCode,
private string $country,
private string $hashedPassword,
) {}
}
public function register(Request $request): Response
{
$user = new User(
email: $request->get('email'),
firstName: $request->get('firstName'),
lastName: $request->get('lastName'),
phone: $request->get('phone'),
address: $request->get('address'),
city: $request->get('city'),
postalCode: $request->get('postalCode'),
country: $request->get('country'),
hashedPassword: $this->hasher->hash($request->get('password'))
);
$this->em->persist($user);
$this->em->flush();
return $this->json($user);
}
Une classe = une responsabilité
Ouvert à l'extension, fermé à la modification
Une classe dérivée remplace sa parente
Interfaces spécifiques > interface générale
Dépendre d'abstractions, pas de concret
class PaymentService
{
private StripePayment $stripe;
public function __construct()
{
$this->stripe = new StripePayment(); // Couplage fort !
}
public function processPayment(float $amount): void
{
$this->stripe->charge($amount);
}
}
// ❌ Impossible de changer de provider sans modifier le code
interface PaymentGatewayInterface
{
public function charge(float $amount): bool;
}
class StripePayment implements PaymentGatewayInterface
{
public function charge(float $amount): bool
{
// Logique Stripe
return true;
}
}
class PaypalPayment implements PaymentGatewayInterface
{
public function charge(float $amount): bool
{
// Logique Paypal
return true;
}
}
class PaymentService
{
public function __construct(
private PaymentGatewayInterface $gateway // Dépendance abstraite !
) {}
public function processPayment(float $amount): void
{
$this->gateway->charge($amount);
}
}
// ✅ Facile de changer de provider sans modifier PaymentService
Évitez la duplication avec l'abstraction
public function isEditable(
RegistrationTab $registrationTab,
Registration $registration,
?int $actorType
): bool {
try {
$this->checkRegistrationExists($registration);
$this->checkRegistrationTabVisibility($registrationTab, $actorType);
$configEdition = $this->getConfigEditionForActorType($registrationTab, $actorType);
$this->checkRegistrationTabConfigEditionExists($configEdition);
$currentStep = $this->registrationStepService->getCurrentRegistrationStep($registration);
$this->checkCurrentTabInferiorNextValidationTab($registrationTab, $currentStep);
return $this->isEditableAtAllSteps($configEdition)
|| $this->isEditableUntilStep($configEdition, $registration);
} catch (UnauthorizedEditionException) {
return false;
}
}
// ✅ Lecture fluide, testable, maintenable, réutilisable
$user = $this->getUser();
if (!$user) {
throw new AccessDeniedException();
}
// $user pourrait être false, null, 0, '', []...
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException('User must be authenticated');
}
// ✅ Type-safety garantie + auto-complétion IDE
class Order {
public const STATUS_PENDING = 'pending';
public const STATUS_PAID = 'paid';
public const STATUS_SHIPPED = 'shipped';
public const STATUS_DELIVERED = 'delivered';
public const STATUS_CANCELLED = 'cancelled';
private string $status;
public function setStatus(string $status): void {
$this->status = $status; // Aucune validation !
}
}
// ❌ Entité surchargée de constantes
enum OrderStatus: string {
case PENDING = 'pending';
case PAID = 'paid';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
}
class Order {
private OrderStatus $status;
public function setStatus(OrderStatus $status): void {
$this->status = $status; // Type-safe !
}
}
// Utilisation
$order->setStatus(OrderStatus::PAID); // Auto-complétion IDE
// ✅ Entité légère, type-safe, réutilisable