thumbnail

Understanding SOLID Principles

Rawa Hamid
Rawa Hamid 3 min read
2 months ago July 30, 2024

In the world of software development, creating high-quality, scalable, and maintainable code is essential for the success of any project. The SOLID principles are a set of five design principles that provide a roadmap for writing clean, modular, and extensible code. These principles were introduced by Robert C. Martin, and they serve as a valuable guideline for developers seeking to improve the architecture and design of their applications.

In this blog post, we will dive into each SOLID principle and explore practical PHP examples to better understand their implementation and benefits.

SOLID Stands for

  • S Single-responsibility principle
  • O Open-closed principle
  • L Liskov substitution principle
  • I Interface segregation principle
  • D 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.

Conclusion:

The SOLID principles provide valuable guidance to developers for creating robust, flexible, and maintainable code. By following these principles, we can design more scalable and less error-prone applications. Embracing SOLID principles, along with other best practices, sets a solid foundation for building successful PHP projects, saving time and resources in the long run. Remember, well-structured code is easier to understand, modify, and extend, benefiting both developers and end-users alike.