Laravel – Code structure with UnitTest

Giả sử ta có một code structure như sau

Controller

  • Gọi Services để thực thi và respond

Services

  • Gọi Repository khi cần tương tác database
  • Trigger Event
  • Gọi các Services khác hoặc các code logic khác.

Vậy UnitTest sẽ cần những gì. Trong quan điểm mình là đi từ thấp nhất lên cao nhất

  • Repositories : Đảm bảo database được thực thi, assert các data như mong đợi.
    • Assert events đã dispatched.
    • Events : Dispatch events và assert data như mong đợi
  • Services
    • Nếu services có các events ẢNH HƯỞNG TRỰC TIẾP đến kết quả thì mình sẽ không fake event mà để event thực thi và assert kết quả
    • Nếu các events đó dùng cho 3rd thì mình assert để make sure events dispatched như mong đợi

Tất nhiên nếu service đó thuần túy gọi tới 3rd party thì mình chỉ việc mock và test respond từ 3rd party thôi.

Và sau cùng là assert controller trả về data như mong đợi

Laravel – Different between Model trait boot & initialization

Trace back to Model we have

    protected static function bootTraits()
    {
        $class = static::class;

        $booted = [];

        static::$traitInitializers[$class] = [];

        foreach (class_uses_recursive($class) as $trait) {
            $method = 'boot'.class_basename($trait);

            if (method_exists($class, $method) && ! in_array($method, $booted)) {
                forward_static_call([$class, $method]);

                $booted[] = $method;
            }

            if (method_exists($class, $method = 'initialize'.class_basename($trait))) {
                static::$traitInitializers[$class][] = $method;

                static::$traitInitializers[$class] = array_unique(
                    static::$traitInitializers[$class]
                );
            }
        }
    }

Theo như code trên có thể thấy

  • boot sẽ call đến static method. Hay nói cách khác sẽ tác động tới mọi instances của Model này
  • trong khi đó `initialize thì ngược lại. Chỉ tác động tới instance hiện tại.

1 ví dụ thực tế

trait HasSlug
{
    public function initializeHasSystem()
    {
        $this->mergeCasts([
            'slug' => 'string'
        ]);

        $this->mergeFillable([
            'slug'
        ]);
    }

    public static function bootHasSlug()
    {
        static::creating(function ($model) {
            $model->{$model->getSlugName()} = Str::slug($model->name);
        });

        static::updating(function ($model) {
            $model->{$model->getSlugName()} = Str::slug($model->name);
        });
    }

    public function scopeSlug($query, $uuid)
    {
        return $query->where($this->getSlugName(), $uuid);
    }

    public function getSlugName()
    {
        return property_exists($this, 'slug') ? $this->slug : 'slug';
    }
}

Laravel – Queue’ parameter

Redis

In your config/queue.php configuration file, each queue connection defines a retry_after option. This option specifies how many seconds the queue connection should wait before retrying a job that is being processed. For example, if the value of retry_after is set to 90, the job will be released back onto the queue if it has been processing for 90 seconds without being released or deleted. Typically, you should set the retry_after value to the maximum number of seconds your jobs should reasonably take to complete processing.

retry_after

Aspire

During my time at Aspire, I had the opportunity to fully immerse myself in the work and expand my knowledge in areas related to banking and security. As a developer, I was tasked with automating the client’s application review process based on reports from third parties. This involved using Laravel, a powerful PHP framework, to build custom tools and workflows that could streamline the review process and make it more efficient.

One of the key challenges in this project was ensuring that the application review process was secure and met all the necessary compliance requirements. To this end, I had to stay up to date with the latest security standards and best practices, as well as work closely with the Aspire team to identify and address any potential vulnerabilities.

In addition to my technical work, I also had the opportunity to collaborate with other team members and share my knowledge and experience with them. As a result, I was able to help mentor and train junior developers, which was a rewarding experience in its own right.

Overall, my time at Aspire was a valuable learning experience that allowed me to expand my skills and knowledge in new areas. By developing custom tools and workflows, automating the application review process, and staying up to date with the latest security standards, I was able to contribute to the success of the project and help Aspire deliver high-quality services to its clients.

SOLID

SOLID là một nguyên tắc lập trình hướng đối tượng được đặt tên theo chữ cái đầu của các từ trong các nguyên tắc này: Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), và Dependency Inversion Principle (DIP).

Các nguyên tắc SOLID đề cao việc thiết kế các lớp và đối tượng trong hệ thống phần mềm, nhằm tăng tính linh hoạt, dễ bảo trì, mở rộng và tái sử dụng của hệ thống.

Dưới đây là một số định nghĩa cho từng nguyên tắc SOLID:

  1. Single Responsibility Principle (SRP): Một lớp nên chỉ có một trách nhiệm duy nhất và không nên bị phân tán với nhiều trách nhiệm khác.
  2. Open/Closed Principle (OCP): Một lớp nên được thiết kế để dễ dàng mở rộng mà không cần sửa đổi mã nguồn hiện tại.
  3. Liskov Substitution Principle (LSP): Các lớp con phải có thể thay thế được cho các lớp cha của chúng mà không làm thay đổi tính năng của hệ thống.
  4. Interface Segregation Principle (ISP): Các giao diện nên được thiết kế theo những chức năng cụ thể và không nên yêu cầu các phương thức không cần thiết.
  5. Dependency Inversion Principle (DIP): Mã nguồn nên phụ thuộc vào các giao diện và không phải các lớp cụ thể, để tạo tính linh hoạt và dễ bảo trì.

1 chút mở rộng với Interface Segregation Principle

interface PaymentGatewayInterface {
    public function processPayment(float $amount, string $paymentMethod): bool;
    public function refundPayment(float $amount, string $paymentMethod): bool;
    public function savePaymentInfo(array $paymentInfo): bool;
}

Trong ví dụ này, PaymentGatewayInterface định nghĩa các phương thức không cần thiết như refundPayment() và savePaymentInfo(). Nếu PaymentGateway không sử dụng chúng, nó vẫn phải triển khai các phương thức này và sẽ phụ thuộc vào chúng. Việc này có thể dẫn đến các vấn đề về độ phức tạp và hiệu suất của ứng dụng, đồng thời cũng làm cho việc thay đổi PaymentGateway khó khăn hơn nếu chúng ta muốn loại bỏ các phương thức không cần thi

Còn đối với Dependency Inversion Principle (DIP) là một trong những nguyên tắc trong SOLID và nhấn mạnh rằng “Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào abstraction”. Nguyên tắc này khuyến khích chúng ta thiết kế các module để chúng phụ thuộc vào abstraction chung hơn là phụ thuộc vào các chi tiết cụ thể.

DIP có thể được áp dụng bằng cách sử dụng các design pattern như Dependency Injection (DI), Inversion of Control (IoC) và Service Locator.

Ví dụ, giả sử bạn đang phát triển một ứng dụng quản lý đơn hàng. Trong ứng dụng của bạn, bạn có một lớp OrderService để xử lý các thao tác liên quan đến đơn hàng, ví dụ như tạo đơn hàng, cập nhật đơn hàng, hoàn tất đơn hàng, v.v.

Trong trường hợp này, nếu OrderService phụ thuộc vào các lớp cấp thấp như OrderRepository, ProductRepository và CustomerRepository, nó sẽ phải biết chi tiết cụ thể của các lớp này, gây ra sự phụ thuộc chặt chẽ và làm cho việc bảo trì, mở rộng và thay đổi khó khăn hơn.

Thay vào đó, nếu chúng ta áp dụng DIP, chúng ta sẽ tạo ra các abstraction (interface hoặc abstract class) để mô tả các chức năng của các lớp cấp thấp như OrderRepository, ProductRepository và CustomerRepository. Sau đó, chúng ta sẽ thiết kế OrderService để phụ thuộc vào các abstraction này thay vì các lớp cụ thể. Ví dụ:

interface OrderRepositoryInterface {
    public function createOrder(array $orderData): int;
    public function updateOrder(int $orderId, array $orderData): bool;
    public function completeOrder(int $orderId): bool;
}

interface ProductRepositoryInterface {
    public function getProductById(int $productId): array;
}

interface CustomerRepositoryInterface {
    public function getCustomerById(int $customerId): array;
}

class OrderService {
    private $orderRepository;
    private $productRepository;
    private $customerRepository;

    public function __construct(OrderRepositoryInterface $orderRepository, ProductRepositoryInterface $productRepository, CustomerRepositoryInterface $customerRepository) {
        $this->orderRepository = $orderRepository;
        $this->productRepository = $productRepository;
        $this->customerRepository = $customerRepository;
    }

    public function createOrder(array $orderData): int {
        // Use the injected order repository to create the order
        $orderId = $this->orderRepository->createOrder($orderData);

        // Use the injected product repository to get the product information
        $product = $this->productRepository->getProductById($orderData['product_id']);

        // Use the injected customer repository to get the customer information
        $customer = $this->customerRepository->getCustomer

Facade vs Helper trong Laravel

Facade

  • Là một tập hợp các static methods cung cấp cách gọi dễ dàng đến các đối tượng không phải là tĩnh (như các class của Service Container).
  • Facade cung cấp một cách tiếp cận rất thuận tiện cho các đối tượng Laravel, vì nó cho phép bạn gọi các phương thức như bạn đang gọi tới một static methods, mặc dù thực tế là các phương thức được đóng gói trong Service Container.
  • Ví dụ, khi bạn gọi phương thức DB::table('table_name')->get(), thực tế bạn đang gọi đến phương thức get() của đối tượng DB, được quản lý bởi Service Container.

Helper

  • Là một tập hợp các hàm toàn cục (global) được định nghĩa để thực hiện các tác vụ phổ biến, và được sử dụng trong toàn bộ ứng dụng Laravel.
  • Helper không liên quan đến Service Container, và không cung cấp các phương thức cho các đối tượng không phải là tĩnh.
  • Ví dụ, route() là một helper để tạo URL của một route được định nghĩa trong ứng dụng Laravel.

Bất cập của Facade

Giả sử bạn có một ứng dụng Laravel đơn giản, nơi bạn cần thực hiện một số thao tác với đối tượng Request của Laravel. Bạn muốn lấy giá trị của một tham số trong URL và hiển thị nó trên trang web.

Một cách để thực hiện điều này là sử dụng Facade của Laravel, đó là Illuminate\Support\Facades\Request. Bằng cách sử dụng Facade này, bạn có thể lấy giá trị của một tham số bằng cách gọi phương thức input() trên Facade, như sau:

$value = Request::input('parameter');

Tuy nhiên, điều gì sẽ xảy ra nếu Request không được đăng ký trong Service Container của Laravel hoặc không có đối tượng Request nào được trả về từ Service Container? Trong trường hợp này, việc gọi Request::input('parameter') sẽ dẫn đến lỗi runtime, vì Facade không thể truy cập đối tượng Request và sử dụng phương thức input().

Điều này có thể xảy ra nếu bạn đang thực hiện một chức năng đơn giản như hiển thị trang web, nhưng vấn đề sẽ trở nên nghiêm trọng hơn nếu bạn đang phát triển một ứng dụng lớn hơn với nhiều phương thức và đối tượng phức tạp. Việc sử dụng quá nhiều Facade trong trường hợp này có thể dẫn đến mã khó bảo trì và khó hiểu, đặc biệt là khi bạn phải theo dõi nhiều phương thức và đối tượng phức tạp.