Search code examples
phplaraveldesign-patternscontainers

Why use service container instead of new class


In what scenario should we use a service container instead of calling a new class directly? I read from Laravel official docs that it is mainly used for managing class dependency and performing dependency injection, however I am not sure if I understand it correctly.

For example payroll applications, we would have a Payroll class, Employee Class, Leave Class, Timesheet Class. So to process a monthly payroll, Payroll class would depends on

  • Employee Class (getBasicSalary)
  • Leave Class (getUnpaidLeaveCount)
  • Timesheet Class (getLateHoursCount)

Below is what I normally writes (without any design pattern)

Class PayrollProcessController 
{
    public function index(){
         $payrollObj = new Payroll();
         $payroll->setPayrollMonth('202106');
         $payroll->process();
    }
}

Class Payroll 
{
    private $payrollMonth;

    public function process()
    {
        // loop employee from database
        foreach ($employees as $employee_id){
             $salary = $this->calculateNetSalary($employee_id); 
             // save into db
        }
    }

    public function calculateNetSalary($employee_id)
    {
         $employee = Employee::find($employee_id);
         $basicSalary = $employee->getBasicSalary();
         $basicSalaryPerDay = $basicSalary / date('t');         
         $basicSalaryPerHour = $basicSalaryPerDay / 8;
         $leaveTakenDay = Leave::getUnpaidLeaveCount( $employee->getId(), $this->payrollMonth);
         $leaveDeductionAmount = $basicSalaryPerDay * $leaveTakenDay;
         $timesheetLateHours = Timesheet::getLateHoursCount($employee->getId(), $this->payrollMonth);
         $lateDeductionAmount = $basicSalaryPerHour * $timesheetLateHours;
         $netSalary = $basicSalary - $leaveDeductionAmount - $lateDeductionAmount;
         return $netSalary;
    }
}

If apply with service container concept,

Class PayrollProcessController 
{
    public function index(Payroll $payroll){  
          $payroll->setPayrollMonth('202106');        
          $payroll->process();
    }
}

Class Payroll 
{
    public function process()
    {
        // loop employee from database
        foreach ($employees as $employee_id){

            $employee = app(Employee::class);
            $employee = $employee->find($employee_id);
            $leave = app(Leave::class);
            $timesheet = app(Timesheet::class);

            $salary = $this->calculateNetSalary($employee, $leave, $timesheet); 
             // save into db
        }
    }

    public function calculateNetSalary(Employee $employee, Leave $leave, Timesheet $timesheet)
    {
         $basicSalary = $employee->getBasicSalary();
         $basicSalaryPerDay = $basicSalary / date('t');         
         $basicSalaryPerHour = $basicSalaryPerDay / 8;
         $leaveTakenDay = $leave->getUnpaidLeaveCount( $employee->getId(), $this->payrollMonth);
         $leaveDeductionAmount = $basicSalaryPerDay * $leaveTakenDay;
         $timesheetLateHours = $timesheet->getLateHoursCount($employee->getId(), $this->payrollMonth);
         $lateDeductionAmount = $basicSalaryPerHour * $timesheetLateHours;
         $netSalary = $basicSalary - $leaveDeductionAmount - $lateDeductionAmount;
         return $netSalary;
    }
}

Questions:

  1. Is the above concept correct? I can see with service container it allows me to move the payroll, employee, timesheet, leave class into a laravel package so I can install in another laravel application (and the main benefits is I can re-use the code and also overrides the class if needed by specify the class path in service provider)

  2. I checked again on the service container documentation https://laravel.com/docs/8.x/container#when-to-use-the-container, it is written:

    First, if you write a class that implements an interface and you wish to type-hint that interface on a route or class constructor, you must tell the container how to resolve that interface. Secondly, if you are writing a Laravel package that you plan to share with other Laravel developers, you may need to bind your package's services into the container.

    So can I say that unless we are publishing the payroll modules as a laravel package for others to use or to write unit testing, there is no point in using a service container?

  3. If we look at opencart's registry class, is that is similar to Laravel's service container? https://github.com/opencart/opencart/blob/e22ddfb060752cd8134abbfb104202172ab45c86/upload/system/framework.php#L9

  4. Take an example from opencart, so Language class is singleton design pattern, while registry is container design pattern?

    $language = new \Opencart\System\Library\Language($config->get('language_code'));
    $language->addPath(DIR_LANGUAGE);
    $language->load($config->get('language_code'));
    $registry->set('language', $language);
    

Solution

  • The purpose of Dependency Injection, which is achieved via Laravels IoC container (service container), is to separate the creation and use of objects. So I would argue that having app(Employee::class); is no better than new Employee or Employee::create(). IoC is more than just registering objects with the container and pulling them out when you need them.

    A practical example for this might be where you have a class which transmits SMS messages.

    class Sms
    {
        private $smsProvider;
    
        public function send()
        {
            $this->smsProvider->send();
        }
    }
    

    So your SmsSender class requires a smsProvider to function. Rather than creating a new instance of the SMS provider in Sms class, you would inject the provider into the class (usually the constructor in such examples) and make use of it that way. If you wanted to make it even more flexible, you would define an interface for SMS providers and then allow the service container to inject the correct concrete class from your IoC mappings.

    class Sms
    {
        private $smsProvider;
    
        public __construct(ISmsProvider $smsProvider)
        {
            $this->smsProvider = $smsProvider;
        }
    
        public function send()
        {
            $this->smsProvider->send();
        }
    }
    
    class SmsTwillio implements ISmsProvider
    {
    }
    
    class SmsNexmo implements ISmsProvider
    {
    }
    

    You would then define mappings in the service container to bind references to ISmsProvider to a concrete implementation such as SmsTwillio or SmsNexmo.

    Using your example, you might define a PayrollService:

    class PayrollService
    {
        public function process($employees, $start, $end = null)
        {
            foreach ($employees as $employee) {
                $salary = $this->calculateNetSalary($employee);
            }
        }
    
        private function calculateNetSalary(Employee $employee)
        {
        }
    }
    
    class ProcessPayrollController extends ProcessPayrollController
    {
        private $payrollService;
    
        public __constructor(PayrollService $PayrollService)
        {
            $this->payrollService = $payrollService;
        }
    
        public function __invoke()
        {
            $result = $this->payrollService->process(Employee::all(), '2021-06-01');
        }
    }
    

    I don't think your Payroll class needs Leave or Timesheet. I would define functions or scopes on your Employee class which returns that data:

    $employee->daysUnpaidLeave('2021-06-01');
    
    $employee->hoursLate('2021-06-01');
    

    Where the dates are the start date to calculate from until today, or provide an optional end date as a second argument.

    You could go even deeper down the rabbit hole and look at other creational design patterns for creating objects at runtime, however, design patterns can be quickly abused and add complexity rather than reduce it, so keep it simple.

    The Registry in OpenCart is a similar principal. In that example $registry->get('language'); will always return the same instance of Language, so yes it would be a singleton.

    Your Payroll could be a singleton as you don't need multiple instances of it as it is simply processing data you provide to it.