Solid Principles in PHP – Single Responsibility

The S in Solid stands for single responsibility and it means that a class should have one, and only one, reason to change. Meaning that a class should only have one job. It means that if our class assumes more than one responsibility we will have a high coupling. The cause is that our code will be fragile at any changes. Let dig straight into the code.

class SalesReporter
{
    public function between($startDate, $endDate)
    {
        //perform Authentication
        if (!Auth::check()) throw new Exception('Authentication required for reporting');

        $sales = $this->queryDBForSalesBetween($startDate, $endDate);

        return $this->format($sales);
    }

    protected function queryDBForSalesBetween($startDate, $endDate)
    {
        return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charge') / 100;
    }
    
    protected function format($sales)
    {
        return "<h1>Sales: $sales</h1>h";
    }
}

Looking at the code above we can see that the SalesReporter class has too many responsibilities. The between function performs an auth check. Now why should the SalesReporter class care about authentication checking, this is application logic and it should not belong here, it should belong in the controller.

The queryDBForSalesBetween function also violates the principle because it has too many resons to change. If we wanted to change the persistence layer and use another DAO, then we’ll need to modify the code.

Also with the format function, if we wanted to change the way output is formatted, once again, the code needs to change. Those two alone violate the single responsibility principle. It’s not the SalesReporter class responsibility to care which persistence class is used, or how we format the output.

Instead, lets inject that through a constructor the SalesRepository class which will be responsible for the database specific interaction. The below constructor function is entered into the SalesReporter class.

private $repo;
    
public function __construct(SalesRepository $repo)
{
    $this->report = $repo;
}

Next, lets create the SalesRepository class and extract the database query function. We have changed the name of the function to between.

class SalesRepository
{
    public function between($startDate, $endDate)
    {
        return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charge') / 100;
    }
}

Back to our SalesReporter class, it has one less responsibility which makes it easier to test and maintain.

class SalesReporter
{
    private $repo;

    public function __construct(SalesRepository $repo)
    {
        $this->report = $repo;
    }

    public function between($startDate, $endDate)
    {
        $sales = $this->repo->between($startDate, $endDate);

        return $this->format($sales);
    }

    protected function format($sales)
    {
        return "<h1>Sales: $sales</h1>h";
    }
}

Let’s deal with the format function. It’s not the responsibility of the SalesReporter class to care how the output of the data is done. Also what happens down the track if you want to output the data to json format or anything else. So anytime you want to change the format, the SalesReporter class will need to be updated.

We’ll need to extract the format function to it’s own class. First what we’ll do is create an interface that the output class has to adhere to.

interface SalesOutputInterface
{
    public function output();
}

We’ll need to create a class HtmlOutput which implements the SalesOutputInterface

class HtmlOutput implements SalesOutputInterface
{
    public function output($sales)
    {
        return "<h1>Sales: $sales</h1>";
    }
}

Back to our SalesReporter class, we just need to work out a way to call the output to display our sales data. In the between function in SalesReporter we’ll pass through the SalesOutputInterface and from there it will know which output to render. Our SalesReporter class will be as follows:

class SalesReporter
{
    private $repo;

    public function __construct(SalesRepository $repo)
    {
        $this->report = $repo;
    }

    public function between($startDate, $endDate, SalesOutputInterface $formatter)
    {
        $sales = $this->repo->between($startDate, $endDate);

        $formatter->output($sales);
    }
}

Now we can execute the function as follows:

$report = new SalesReporter(new SalesRepository);

$begin = Carbon\Carbon::now()->subDays(10);
$end = Carbon\Carbon::now();

$report->between($begin, $end, new HtmlOutput)

Leave a Reply

Your email address will not be published.