PHP Formatting
Creative, simple and structured code to read and write. Some of the following guides were inspired by Jason McCreary's talk during Laracon 2019.
- Reformatting
- Dead Code
- Guard Clauses
- Nested Code
- Using Objects
- Naming & Removing Comments
- Rule of Three
Reformatting:
Once code has been completed that is complex or that is a part of a long process, it may be useful to go back and refactor it.
Why reformat?
- More readable for other Developers (PSR-2- including yourself)
- Better representation of how the code is structured
- Able to remove any unnecessary code (code that is not used/Dead Code)
- Code may be reusable into separate functions
Dead Code
Similarly like Reformatting, once a task/method/event has been completed, go back. You may find that there is unnecessary 'Dead Code' that is not being used within the event.
What is Dead Code?
- Unreachable/Not executed code
- Unnecessary code that is commented out or empty if statements (Empty logic)
- Logic that performs the same tasks/checks
Guard Clauses
Guard clauses are when preconditions are met to avoid both errors during the execution period and nested conditions entirely.
There is much debate on how if statements can be used for better code clarity and readability. However, when used appropriately both complexity and memory load can be reduced.
/**
* Traditional If/Else
* -- In this example $total is doing nothing.
*/
function calculations(int $a, int $b)
{
$total = 0;
if ($b) {
$total = $a;
} else {
$total = $a + $b;
}
return $total;
}
/**
* If/Else and Early Return
* -- $total has now been removed, yet we now have a pointless else statement.
*/
function calculations(int $a, int $b = null)
{
if ($b) {
return $a;
} else {
return $a + $b;
}
}
/**
* Without Else
* -- By removing the else statement, we now get the expected result of this
* -- method at the end and a catch for user error.
*/
function calculations(int $a, int $b = null) : int
{
if ($b) {
return $a;
}
return $a + $b;
}
/**
* By class
* -- Even better, by thinking further ahead this method can be used multiple
* -- times; why not refactor into it's own class.
*/
class Calculator
{
public $total = 0;
public function __construct(int $total = null)
{
$this->total = $total;
}
public function add(...$numbers) : Calculator
{
foreach ($numbers as $number) {
$this->sum = $this->sum + $number;
}
return $this;
}
public function subtract(...$numbers) : Calculator;
public function multiply(...$numbers) : Calculator;
public function divide(...$numbers) : Calculator;
}
Nested Code
If possible, remove any nested code into it's own block. This makes code more readable for future developers to take over legacy code (Reformatted 1x).
Using Objects
Something big to take away is the use of objects and storing methods, these may be reusable in the build/project again. For example, making use of implicit Route Model Binding to get a specific row and therefore removing code such as $record = Model::findOrFail($id)
.
Naming & Removing Comments
Conventions of naming are very important and relate to comments. Sometimes commenting is unnecessary where methods have specific, relatable names. If this is applied you'll find comments not necessary as the/a task is obvious by the method name. For example:
public function store()
{
// Get all items that are active
$items = Item::where('active', 1)->get();
// or simply create a scope
$items = Item::active(true)->get();
}
Rule of Three
Within a method it's important to keep to a structure because to make clear of what the outcome is.
- With more data points we can predict what will happen next. For example, step 1: check this, step 2: check that.
- "Don't Repeat Yourself" - if a similar method is used elsewhere use it or make a helper
- The end point should be the final outcome.
Again this rule can be applied during the refactoring stage, please see some examples below.
True/False (boolean) statements:
// This piece of code is fine and there is nothing wrong with it.
// It does make sense
public function isEmpty()
{
if( $this->size === 0 ) {
return true;
} else {
return false;
}
}
// However it can easily be reformatted to return the
// raw Boolean value directly this because if $size
// is empty when returned then we should see false anyways
public function isEmpty()
{
return $this->size === 0;
}
Primary actions should appear at the end of a method
By the end of this section, you should understand why actions should follow at the end of a method and that nested code should be avoided if possible
// There are many things that can be done about this code.
public function add($item)
{
if ($item === null) {
// code...
} else {
if (!$this->contains($item)) {
return $this->items[] = $item;
}
}
}
// Reformatted 1x
// The first argument is that the first Boolean returns nothing,
// so lets remove and invert that logic.
// The nested if statement is more readable...
public function add($item)
{
if ($item !== null) {
if (!$this->contains($item)) {
return $this->items[] = $item;
}
}
}
// Reformatted 2x
// Furthermore these nested if statement can be on the same line
public function add($item)
{
if ( $item !== null && ! $this->contains($item) ) {
return $this->items[] = $item;
}
}
// Reformatted 3x
// Now there was nothing wrong with the previous code, however
// it is not very readable. Instead we should really see the final
// action at the end of this method
public function add($item)
{
if ( $item === null && $this->contains($item) ) {
return false;
}
return $this->items[] = $item;
}
Controller actions
By the end of this section, you should understand why code blocks are separated into different files to handle particular methods
class EnrollmentController extends Controller
{
// Globally if the Request were to contain these
// Validation rules, they can be stored into
// an object and therefore simplify the Controller.
public function store(Request $request, $id)
{
// Rules
Validator::make($request->all(), [
'first_name' => 'request|string'
'last_name' => 'request|string',
'email' => 'request|email',
'password' => 'request|confirmed|min:8',
])->validate();
}
// Similarly although this is only one line of code,
// Route Model Binding will perform the same task as the method
// findOrFail(id)
public function store(EnrollUser $request, $id)
{
$course = Course::findOrFail($id);
}
// Now that the Controller method is simplified further, we can may use
// of another method to separate further lines of code that may be global
// to other methods of the same Controller
public function redirectIfAuthenticatedToCourse($course)
{
if( ! Auth::check() ) {
return redirect($course->path());
}
}
// Similarly to Validation, we can store this particular instance of
// creating a new User from this Request into it's own method of
// the User object
public function store(EnrollUser $request, Course $course)
{
$this->redirectIfAuthenticatedToCourse($course);
$user = User::create([
'first_name' => request('first_name')
'last_name' => request('last_name'),
'email' => request('email'),
'password' => bcrypt(request('password')),
])
}
// Final Result
public function store(EnrollUser $request, Course $course)
{
$this->redirectIfAuthenticatedToCourse($course);
$user = User::createFromEnrollmentRequest($request, $course);
return redirect($course->path())->with('success', 'message');
}
}