SOLID principles in Laravel

Image
Categories
laravel
Content

SOLID Principles

First, let's discuss what SOLID stands for:

  • Single-responsibility principle
  • Open-closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Single-Responsibility Principle

Each class should have only one reason to change

a bad one

This is a modern example of mixing the data and the representation layer in one class

class UserResource extends JsonResource
{
	publicfunctiontoArray($request)
	{
		$mostPopularPosts = $user->posts()
			->where('like_count', '>', 50)
			->where('share_count', '>', 25)
			->orderByDesc('visits')
			->take(10)
			->get();
		
		return [
			'id' => $this->id,
			'full_name' => $this->full_name, 
			'most_popular_posts' => $mostPopularPosts,
		];
	}
}

a good one

class UserResource extends JsonResource
{
	publicfunctiontoArray($request)
	{
		return [
			'id' => $this->id,
			'full_name' => $this->full_name, 
			'most_popular_posts' => $this->when(
					$request->most_popular_posts,
					GetMostPopularPosts::execute($user), 
			),
		];
	}
}

class GetMostPopularPosts 
{
	/**
	* @return Collection<Post> 
	*/
	public static function execute(User $user): Collection {
		return $user->posts()
			->where('like_count', '>', 50)
			->where('share_count', '>', 25)
			->orderByDesc('visits')
			->take(10)
			->get();
	} 
}

Now we have two well-defined classes:

  • UserResource is responsible only for the representation and it has one reason to change.
  • GetMostPopularPosts is responsible only for the query and it has one reason to change.

Open-Closed Principle

A class should be open for extension but closed for modification

example:

trait Likeable 
{
	public function like(): void 
	{

	}
	
	public function dislike(): void 
	{

	}
}

class Post extends Model 
{
	use Likeable; 
}

class Comment extends Model 
{
	use Likeable; 
}

This is pretty standard, right? But think about what happened here.

We just added new functionality to multiple classes without changing them! We extended our classes instead of modifying them

Liskov Substitution Principle

Each base class can be replaced by its subclasses

example

abstract class EmailProvider 
{
		abstract public function addSubscriber(User $user): array;
}
	
class MailChimp extends EmailProvider 
{
	public function addSubscriber(User $user): array 
	{ 
		// Using MailChimp API
	}
}

class ConvertKit extends EmailProvider 
{
		public function addSubscriber(User $user): array 
		{
			// Using ConvertKit API
		}
}

We have an abstract EmailProvider and we use both MailChimp and ConvertKit for some reason. These classes should behave exactly the same way, no matter what.

class AuthController 
{
	public function register( RegisterRequest $request, MailChimp $emailProvider){}
}

class AuthController 
{
	public function register( RegisterRequest $request, ConvertKit $emailProvider){}
}

Interface Segregation Principle

You should have many small interfaces instead of a few huge ones

example

class TextInput extends Field implements CanHaveNumericState, Contracts\CanBeLengthConstrained, Contracts\HasAffixActions
{
    use Concerns\CanBeAutocapitalized;
    use Concerns\CanBeAutocompleted;
    use Concerns\CanBeLengthConstrained;
    use Concerns\CanBeReadOnly;
    use Concerns\HasAffixes;
    use Concerns\HasDatalistOptions;
    use Concerns\HasExtraInputAttributes;
    use Concerns\HasInputMode;
    use Concerns\HasPlaceholder;	
}

Each of those traits has a pretty small and well-defined interface and it adds a small chunk of functionality to the class

Dependency Inversion Principle

Depend upon abstraction, not concretions.

example

interface MarketDataProvider 
{
	public function getPrice(string $ticker): float; 
}

class IexCloud implements MarketDataProvider 
{
	public function getPrice(string $ticker): float 
	{
		// Using IEX API
	} 
}

class Finnhub implements MarketDataProvider 
{
	public function getPrice(string $ticker): float 
	{
		// Using Finnhub API
	} 
}

class CompanyController 
{
		public function show(Company $company, MarketDataProvider $marketDataProvider)
		{
			$price = $marketDataProvider->getPrice();
			return view('company.show', compact('company', 'price')); 
		}
}

So every class should depend on the abstract MarketDataProvider not on the concrete implementation.