Skip to content
advanced Phase 99 · Architecture Principles

Refactoring Techniques for Magento

Safe refactoring techniques including Martin Fowler patterns, refactoring in Magento, and test-driven refactoring

45m
0 problems
Topic Progress 0%

Safe Refactoring Principles

Refactoring Rules

1. Refactor in small steps
2. Run tests after each change
3. Don't change functionality
4. Use version control
5. Refactor before adding features
6. Don't refactor and add features simultaneously

Refactoring Workflow

1. Ensure tests exist (write if needed)
2. Run tests - they should pass
3. Make one small change
4. Run tests - they should still pass
5. Commit
6. Repeat

Preparation Checklist

## Before Refactoring

- [ ] Tests cover current behavior
- [ ] Version control is clean
- [ ] No pending changes
- [ ] CI pipeline is green
- [ ] Understand the code
- [ ] Plan the refactoring steps

Refactoring with Tests

// 1. Write test for current behavior
public function testCalculateTotal()
{
    $order = new Order();
    $order->addItem(new OrderItem(1000));  // $10.00
    $order->addItem(new OrderItem(2000));  // $20.00
    
    $this->assertEquals(3000, $order->getTotal());
}

// 2. Verify test passes
vendor/bin/phpunit --filter=CalculateTotal

// 3. Refactor
public function getTotal(): int
{
    return array_reduce(
        $this->items,
        fn($carry, $item) => $carry + $item->getSubtotal(),
        0
    );
}

// 4. Verify test still passes
vendor/bin/phpunit --filter=CalculateTotal

Key Takeaway

Refactor in small steps with tests. Never change functionality during refactoring. Commit after each successful change.

Extract Method Pattern

Extract Method

// Before: Long method
class OrderProcessor
{
    public function process($orderId)
    {
        // Load order (10 lines)
        $connection = $this->resource->getConnection();
        $select = $connection->select()->from('sales_order')->where('entity_id = ?', $orderId);
        $order = $connection->fetchRow($select);
        
        // Validate order (15 lines)
        if (!$order) {
            throw new \Exception('Order not found');
        }
        if ($order['status'] == 'cancelled') {
            throw new \Exception('Cannot process cancelled order');
        }
        
        // Process payment (20 lines)
        $payment = $this->paymentFactory->create();
        $payment->setOrderId($orderId);
        $payment->setAmount($order['total']);
        $payment->process();
        
        // Send email (10 lines)
        $email = $this->emailFactory->create();
        $email->setTo($order['customer_email']);
        $email->send();
        
        return $order;
    }
}

// After: Extracted methods
class OrderProcessor
{
    public function process($orderId)
    {
        $order = $this->loadOrder($orderId);
        $this->validateOrder($order);
        $this->processPayment($order);
        $this->sendConfirmation($order);
        return $order;
    }
    
    private function loadOrder($orderId)
    {
        $connection = $this->resource->getConnection();
        $select = $connection->select()->from('sales_order')->where('entity_id = ?', $orderId);
        $order = $connection->fetchRow($select);
        
        if (!$order) {
            throw new \Exception('Order not found');
        }
        return $order;
    }
    
    private function validateOrder($order)
    {
        if ($order['status'] == 'cancelled') {
            throw new \Exception('Cannot process cancelled order');
        }
    }
    
    private function processPayment($order)
    {
        $payment = $this->paymentFactory->create();
        $payment->setOrderId($order['entity_id']);
        $payment->setAmount($order['total']);
        $payment->process();
    }
    
    private function sendConfirmation($order)
    {
        $email = $this->emailFactory->create();
        $email->setTo($order['customer_email']);
        $email->send();
    }
}

Key Takeaway

Extract Method moves code blocks into named methods. Each method has a single responsibility and clear name.

Replace Temp with Query

Replace Temp with Query

// Before: Temporary variable
class OrderTotalCalculator
{
    public function calculate($order)
    {
        $basePrice = $order->getItemsTotal();
        $discount = $basePrice * 0.1;
        $tax = ($basePrice - $discount) * 0.08;
        return $basePrice - $discount + $tax;
    }
}

// After: Extract to query method
class OrderTotalCalculator
{
    public function calculate($order)
    {
        return $this->getBasePrice($order)
            - $this->getDiscount($order)
            + $this->getTax($order);
    }
    
    private function getBasePrice($order): int
    {
        return $order->getItemsTotal();
    }
    
    private function getDiscount($order): int
    {
        return $this->getBasePrice($order) * 0.1;
    }
    
    private function getTax($order): int
    {
        return ($this->getBasePrice($order) - $this->getDiscount($order)) * 0.08;
    }
}

Inline Temp

// Before: Unnecessary variable
$discount = $this->calculateDiscount($order);
return $order->getTotal() - $discount;

// After: Inline
return $order->getTotal() - $this->calculateDiscount($order);

Replace Magic Numbers

// Before: Magic number
if ($order->getTotal() > 1000) {
    $order->setFreeShipping(true);
}

// After: Named constant
const FREE_SHIPPING_THRESHOLD = 1000;

if ($order->getTotal() > self::FREE_SHIPPING_THRESHOLD) {
    $order->setFreeShipping(true);
}

Key Takeaway

Replace temporary variables with query methods. Inline unnecessary variables. Replace magic numbers with named constants.

Magento-Specific Refactoring

Refactoring to Service Contracts

// Before: Direct model usage
class ProductController
{
    public function execute()
    {
        $product = Mage::getModel('catalog/product')->load($id);
        return $product->getName();
    }
}

// After: Service contract
class ProductController
{
    private ProductRepositoryInterface $productRepository;
    
    public function __construct(ProductRepositoryInterface $productRepository)
    {
        $this->productRepository = $productRepository;
    }
    
    public function execute()
    {
        $product = $this->productRepository->get($id);
        return $product->getName();
    }
}

Refactoring Observers

// Before: Heavy logic in observer
class OrderPlacedObserver implements ObserverInterface
{
    public function execute(EventObserver $observer)
    {
        $order = $observer->getEvent()->getOrder();
        // 100+ lines of processing
    }
}

// After: Delegate to service
class OrderPlacedObserver implements ObserverInterface
{
    private OrderPostProcessor $postProcessor;
    
    public function execute(EventObserver $observer)
    {
        $order = $observer->getEvent()->getOrder();
        $this->postProcessor->process($order);
    }
}

Refactoring Plugins

// Before: Plugin too broad
class ProductPlugin
{
    public function aroundGetPrice($subject, $proceed)
    {
        // Modifies ALL product prices
    }
}

// After: Specific plugin with condition
class ProductPlugin
{
    public function aroundGetPrice($subject, $proceed)
    {
        if ($this->isSpecialPromotion($subject)) {
            return $this->calculateSpecialPrice($proceed());
        }
        return $proceed();
    }
}

Refactoring to Event-Driven

// Before: Direct dependencies
class OrderService
{
    public function placeOrder($order)
    {
        $this->inventoryService->decrement($order);
        $this->emailService->send($order);
        $this->analyticsService->track($order);
    }
}

// After: Event-driven
class OrderService
{
    private EventDispatcher $eventDispatcher;
    
    public function placeOrder($order)
    {
        $this->orderRepository->save($order);
        $this->eventDispatcher->dispatch('order_placed', ['order' => $order]);
    }
}

Key Takeaway

Refactor Magento code to use service contracts, thin observers, specific plugins, and event-driven architecture for better maintainability.

Quiz

1. What is the first rule of refactoring?

Question 1 options

2. What is Extract Method?

Question 2 options

3. Why replace magic numbers?

Question 3 options

4. How to refactor Magento observers?

Question 4 options

5. When should you refactor?

Question 5 options

Flashcards

Question

What is the first rule of refactoring?

Answer

Ensure tests exist and pass before making changes

Question

What is Extract Method?

Answer

Move code block into named method with single responsibility

Question

Why replace magic numbers?

Answer

Named constants improve readability and maintainability

Question

How to refactor observers?

Answer

Keep observers thin, delegate logic to service classes

Question

When to refactor?

Answer

Before adding features, not during bug fixes or under pressure

Question

What is Replace Temp with Query?

Answer

Replace temporary variables with query methods

Revision Notes

Key Takeaways

  • 1. Refactor in small steps with tests running after each change
  • 2. Extract Method: move code blocks into named methods
  • 3. Replace magic numbers with named constants
  • 4. Refactor Magento to use service contracts and thin observers
  • 5. Never refactor and add features simultaneously

Interview Tips

  • Explain safe refactoring principles
  • Describe common refactoring patterns
  • Discuss test-driven refactoring approach
  • Explain Magento-specific refactoring techniques

Cheat Sheet

Refactoring

Principles:

  1. Small steps
  2. Tests after each change
  3. Don't change functionality
  4. Commit frequently

Patterns:
Extract Method
Replace Temp with Query
Replace Magic Numbers
Inline Temp

Magento:
Refactor to service contracts
Thin observers
Specific plugins
Event-driven architecture