Applying SOLID Principles to WordPress Development
Chigozie Orunta
Senior WordPress Engineer @ X-Team | Enterprise WP, Plugin Development.
SOLID Principles in modern software development refers to the design approach intended in making software code more easily readable, extendable and maintainable.
It was first introduced by Uncle Bob (Robert C. Martin) in his 2000 paper titled Design Principles and Design Patterns, but became popular in 2004 when Michael Feathers coined the term SOLID.
In this article, my goal is to try and show how SOLID principles can be applied to WordPress development.
Single Responsibility Principle
This principle simply states that functions or methods used in classes should perform only one function or task at a time.
This simply means if a function or method is performing more than one function, then it is in violation of this principle or rule and should be modified to perform one task only.
Lets see an example below:
<?php
class Student {
/**
* Save user input to Student custom post type.
*
* @return void
*/
public function save(): void {
// Authenticate user.
if ( ! is_user_logged_in() ) {
return;
}
// Validate input.
$name = $this->validate( $_POST['name'], 'string' );
$age = $this->validate( $_POST['age'], 'int' );
$phone = $this->validate( $_POST['phone'], 'string' );
// Save student data.
$post_id = wp_insert_post(
array(
'post_type' => 'student',
'post_status' => 'publish',
'post_title' => $name
)
);
add_post_meta( $post_id, 'age', $age );
add_post_meta( $post_id, 'phone', $phone );
}
/**
* Validate user input based on type.
*
* @param string $input
* @param string $type
* @return string
*/
public function validate( $input, $type ): string {
// Validation logic here.
}
}
Let's examine closely the save method, shall we. From the above code, it is doing 3 things as follows:
1st thing
// Authenticate user.
if ( ! is_user_logged_in() ) {
return;
}
2nd thing
// Validate input.
$name = $this->validate( $_POST['name'], 'string' );
$age = $this->validate( $_POST['age'], 'int' );
$phone = $this->validate( $_POST['phone'], 'string' );
3rd thing
// Save student data.
$post_id = wp_insert_post(
array(
'post_type' => 'student',
'post_status' => 'publish',
'post_title' => $name
)
);
add_post_meta( $post_id, 'age', $age );
add_post_meta( $post_id, 'phone', $phone );
There are so many reasons why this set up is wrong.
Firstly, this violates the Single Responsibility Principle because a method is supposed to do one thing and one thing only. The save method in this case is authenticating, validating and then saving the user data to the Student custom post type.
Secondly, the Student class in this context acts like a model and is basically concerned with CRUD operations for the Student custom post type. This means it is not supposed to know or have implementation details for authentication and validation. Those pieces of logic should belong to the Authentication and Validation classes separately.
When refactored, our Student class would look like so:
<?php
class Student {
/**
* Save user input to Student custom post type.
*
* @param array $request POST/GET input.
* @return void
*/
public function save( $request ): void {
// Save student data.
$post_id = wp_insert_post(
array(
'post_type' => 'student',
'post_status' => 'publish',
'post_title' => $request['name'],
)
);
add_post_meta( $post_id, 'age', $request['age'] );
add_post_meta( $post_id, 'phone', $request['phone'] );
}
}
We can also have a validation class like so:
<?php
class Validation {
/**
* Validate user input based on type.
*
* @param array $input POST input.
* @return array
*/
public function validate( $input ): array {
// Validation logic here.
}
}
And an authentication class like so:
<?php
class Authentication {
/**
* Authenticate user.
*
* @return void
*/
public function authenticate(): void {
// Authenticate user.
if ( ! is_user_logged_in() ) {
return;
}
}
}
These classes can now be injected (Dependency Injection) into a Student Service class that does the actual job of creating the data. The authentication class can be called in an earlier init hook or middleware and does not need be injected for this service.
<?php
class StudentService {
/**
* Service for creating Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
$student->save( $validation->validate( $_REQUEST ) );
}
}
These separation of concerns helps us achieve the following:
Open for Extension, Closed for Modification
Confusing right?
Well actually, this principle simply suggests that our classes should be designed in such a way that they are open for extension so that users and developers can add custom logic to them but closed for modification so they don't end up modifying our original lines of code which may end up breaking things in production.
And now I know you're wondering, is this even possible? Is there a way we can allow other developers extend our logic without modifying our own lines of code? Well, the answer is Yes! It turns out that Matt Mullenweg and the Automattic team (Maintainers of WordPress) created this tiny little thing called a Hook that makes this possible.
领英推荐
Hooks are WordPress' intelligent way of allowing developers add logic to an existing WP plugin or application for the purpose of extending it so they don't have to modify the original codebase.
There are many advantages to this:
Firstly, it is strongly frowned upon by the WordPress community to modify an existing codebase as written by an author since this could break an existing feature or function in production.
Secondly, any modifications you make to an existing codebase would disappear once the user upgrades their plugin and then your changes would be lost completely!
When building WordPress applications and plugins, it is important we provide ways for other developers to add custom logic or functionality to our piece of software with Hooks. In this way, Add-ons can be built to cater for several other needs that our WordPress plugin or application may not currently address.
Lets a take a look at the StudentService example we used earlier:
<?php
class StudentService {
/**
* Service for creating Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
$student->save( $validation->validate( $_REQUEST ) );
}
}
Now, let's assume one of our plugin users needed to perform an action on the POST data before saving, the question is, how would they able to do that?
If they modified our plugin, their changes would be lost anytime they performed a plugin update! The best way to do this would be to plug in their changes using a hook! In this way, they could add custom logic to our plugin without worrying about if their changes would be lost when they updated their plugin.
Let's take a look at it again, shall we:
<?php
class StudentService {
/**
* Service for creating Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
// Plug in user's custom logic before saving.
do_action( 'before_save_student_data' );
// Plug in custom user $_POST array.
$student->save( $validation->validate( apply_filters( 'student_post_data', $_REQUEST ) ) );
// Plug in user's custom logic after saving.
do_action( 'after_save_student_data' );
}
}
Interesting... There are 3 things happening in the above scenario namely:
In this way, we have made the StudentService class very extensible and now it can be made to do a lot much more without loosing changes during plugin updates.
Liskov Substitution Principle
When I first heard about this principle I was pretty confused but, its actually easy than it sounds. As the name implies, the principle simply suggests that Parents and Child Classes should be interchangeable.
The way we achieve this is by type-hinting parameters and return types for all class methods.
Let's take a look at the StudentService class we used earlier:
<?php
class StudentService {
/**
* Service for creating Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
// Plug in user's custom logic before saving.
do_action( 'before_save_student_data' );
// Plug in custom user $_POST array.
$student->save( $validation->validate( apply_filters( 'student_post_data', $_REQUEST ) ) );
// Plug in user's custom logic after saving.
do_action( 'after_save_student_data' );
}
}
It's create method contains validation and student instances which contain type declarations and a return type:
public function create( Validation $validation, Student $student ): void
Now let's say we wanted to handle the creation of data for Part-time Students and Full-time Students separately, how would we do that?
We could extend the Student Service like so:
<?php
class PartTimeStudentService extends StudentService {
/**
* Service for creating Part-time Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
// Plug in user's custom logic before saving.
do_action( 'before_save_student_data' );
// Plug in custom user $_POST array.
$student->save( $validation->validate( apply_filters( 'student_post_data', $_REQUEST ) ) );
// Plug in user's custom logic after saving.
do_action( 'after_save_student_data' );
}
}
And for the Full Time Students like so:
<?php
class FullTimeStudentService extends StudentService {
/**
* Service for creating Full-time Student Data.
*
* @param \Validation $validation Validation instance.
* @param \Student $student Student instance.
* @return void
*/
public function create( Validation $validation, Student $student ): void {
// Plug in user's custom logic before saving.
do_action( 'before_save_student_data' );
// Plug in custom user $_POST array.
$student->save( $validation->validate( apply_filters( 'student_post_data', $_REQUEST ) ) );
// Plug in user's custom logic after saving.
do_action( 'after_save_student_data' );
}
}
The above child classes can now be interchanged with the parent class because they contain the exact parameter type definitions and return types specified in the parent class.
Interface Segregation Principle
Interfaces in PHP are well known programming construct that helps us define abstractions for use by both high-level code and low-level concretion, implementation classes.
This principle simply suggests that interfaces should not be modified to accommodate new method definitions but instead new interfaces should be created to accommodate those changes.
Let's take a look at an example that will make this clearer:
Multiple Business Owner | Solution Driven | Development Focused | Co-Founder and Chief Operating Officer at HIP Creative.
2 年Great post Chigozie Orunta