Intended Audience
This article is aimed at developers who are new to PHP or programming in general. We'll be covering the basics of what closures and arrow functions are, how they work, and when you might want to use them in your code. So if you're already familiar with either of these features, this article might not be for you; but it might be a good chance to recap some PHP fundamentals.
Introduction
As a PHP web developer, it's almost inevitable that you'll come across closures and arrow functions at some point. They are both handy features that can be used to write pretty powerful code when used correctly.
But I'll admit, when I first started learning PHP (and to code, in general), I didn't have much of an idea of what closures were. I knew they were a thing, but I didn't really understand how they worked, when to use them, or what they achieved.
So in this blog post, I'm going to cover the basics of closures and arrow functions in PHP. We'll discuss what each of them does, where you might want to use them, and then we'll explore the differences between them.
Closures in PHP
Closures are anonymous functions that can be stored in variables, passed as arguments to other functions, or returned as values from other functions.
For any of my Laravel readers, you might have already used closures when adding database transactions.
Closures are defined using the function
keyword followed by a list of parameters, and a body enclosed in curly braces. Let's take a look at a very simple example. We'll create a closure that adds two numbers together and then returns it:
1$addition = function($a, $b) {2 return $a + $b;3};
In the code above, we've created a closure and stored it in a variable called $addition
. However, we haven't actually called the function yet, so nothing has happened. To call the function, we can do the following:
1$result = $addition(2, 3);2 3echo $result; // Output: 5
As we can see, the function has been called and the result has been stored in the $result
variable. We can then echo the result to the screen.
Similar to how you might want to type hint a typical function parameter, you can also type hint closure parameters. We can also add return types to the closures to ensure the function returns the correct type of value. Let's add some type hints and return types to our closure:
1$addition = function(int $a, int $b): int {2 return $a + $b;3};
Now, we've added type hints to the parameters and a return type to the function. This means that if we pass anything other than an integer to the function, PHP will throw an error. Similarly, if the function doesn't return an integer, PHP will also throw an error.
Since closures aren't run until they are called, they are perfect for using as callbacks. This is great if you're using a function like array_map
or array_filter
where you can define how each item in the array should be processed. Let's take a look at an example:
1$names = ['john', 'jane', 'joe']; 2 3$greeting = 'Hello, '; 4 5$sentences = array_map( 6 callback: function (string $name) use ($greeting): string { 7 return $greeting.$name; 8 }, 9 array: $names,10);11 12// $sentences would be equal to:13// [14// 'Hello, John',15// 'Hello, Jane',16// 'Hello, Joe',17// ]
Let's take a quick look at what we're doing with the above code. We're creating an array of names (we could assume that this array was coming from a database or some other source). We then create a variable called $greeting
and assign it a string value of Hello,
Again, we could assume that this is coming from a database or other source. We then create a new array called $sentences
and use the array_map
function to map over the $names
array.
The array_map
function then iterates through each of the names in the array we passed to it and passes the name to the closure. So the closure will be run once for each item in the array.
You may have noticed that we're using use
in the closure declaration. But what is this for? Closures can only access variables that are passed as function parameters or that are passed from the surrounding scope using the use
keyword. If we didn't use the use
keyword, the $greeting
variable would not be accessible inside the closure and an error would be thrown when trying to run the code.
Arrow Functions in PHP
Arrow functions are a similar feature to closures, but they provide a more concise syntax for writing anonymous functions with shorter and simpler expressions.
Rather than defining them using the function
keyword and wrapping the body in curly braces like with closures, arrow functions use the fn
keyword and a single expression. It's also important to note that arrow functions must return a value. So they can't be used for functions that don't return a value, whereas closures can.
Let's take a look at how we could write our previous example closure that adds two numbers together as an arrow function:
1$addition = fn($a, $b) => $a + $b;2 3$result = $addition(2, 3);4 5echo $result; // Output: 5
We can also add type hints and return types to arrow functions in the same way as we did with closures:
1$addition = fn(int $a, int $b): int => $a + $b;2 3$result = $addition(2, 3);4 5echo $result; // Output: 5
As we can see, the arrow function syntax is shorter than the closure syntax. This can make them much easier to write, especially with shorter functions.
Only Allows Single Expressions
Arrow functions do have a downside in comparison to closures. They're limited to a single statement, whereas closures can contain multiple statements.
For example, let's imagine we had a basic closure that takes a name, uppercases the first letter of the name passed to it, and then returns the name with a greeting. But if the name passed to the closure is Ashley
, we'll return the string Hello, Ash Allen
instead. The closure could look something like this:
1$greeting = 'Hello, '; 2 3$greet = function (string $name) use ($greeting): string { 4 if ($name === 'Ashley') { 5 return 'Hello, Ash Allen'; 6 } 7 8 $upperCasedName = ucfirst($name); 9 10 return $greeting.$upperCasedName;11};12 13echo $greet('Ashley'); // Output: Hello, Ash Allen14echo $greet('john'); // Output: Hello, John
Since the closure contains two statements, we can't simply switch out the function
for fn
to convert it to an arrow function. It would require some refactoring to make it work. We could do something like this:
1$greeting = 'Hello, ';2 3$greet = fn (string $name): string =>4 $name === 'Ashley'5 ? 'Hello, Ash Allen'6 : $greeting.ucfirst($name);7 8echo $greet('Ashley'); // Output: Hello, Ash Allen9echo $greet('john'); // Output: Hello, John
As you can see in the code above, we've had to change the way the code inside the function is written so that it can be executed in a single statement. Although I personally think it looks much cleaner and easier to read how we've rewritten it, I'm hoping it's highlighted the fact that the arrow functions can sometimes be more restrictive than closures.
As the code inside your arrow functions becomes more complex, you may find that you need to consider using closures instead. Or, you may decide to extract the closure's code into another function that can be called.
After all, it's more important to write code that is easy to understand and maintain than it is to write code that is unreadable just for the sake of trying to fit it on one line.
However, if you're writing simple functions that only contain a single statement, then arrow functions are a great way to write them. For instance, I love using them to add constraints to my database queries in my Laravel apps. For example, let's say I want to write a query to grab all the authors (and some statistics about them) from my database that have published books. I could write the query using closures to write something like this:
1$authors = Author::query() 2 ->whereHas( 3 'books', 4 function (Builder $query): Builder { 5 return $query->where('is_published', true); 6 } 7 ) 8 ->withCount([ 9 'reviews' => function (Builder $query): Builder {10 return $query->where('rating', 5);11 },12 'sales' => function (Builder $query): Builder {13 return $query->where('purchased_at', '>=', now()->subDays(7));14 }15 ])16 ->get();
Alternatively, I could write the same query using arrow functions like so:
1$authors = Author::query() 2 ->whereHas( 3 'books', 4 fn (Builder $query): Builder => $query->where('is_published', true) 5 ) 6 ->withCount([ 7 'reviews' => fn (Builder $query): Builder => $query->where('rating', 5), 8 'sales' => fn (Builder $query): Builder => $query->where('purchased_at', '>=', now()->subDays(7)), 9 ])10 ->get();
Automatic Variable Capturing
One of the biggest differences between arrow functions and closures you should be aware of is that arrow functions automatically capture variables from the parent scope. This means that you don't need to use the use
keyword to capture variables like you would with a closure. You may have noticed this in the examples above where we didn't need to use the use
keyword to capture the $greeting
variable inside the arrow function.
When to Use Closures or Arrow Functions
You should always choose the most appropriate tool for the job. So there's not really a right or wrong answer when it comes to choosing between closures and arrow functions. It's more about choosing the one that is most appropriate for the task at hand.
I'd strongly encourage you to use the one that allows you to write the most readable and maintainable code. For example, if you find that you're trying to cram a lot of complex logic inside your arrow functions, then this might be a sign that you should switch to closures instead. However, if you're writing simple functions that only contain a single statement, then arrow functions may be a more suitable approach.
Here are a few pointers to help you decide which to use...
Use closures when:
- Your anonymous function requires multiple statements or complex logic.
- You need finer control over variable scoping and don't want the parent scope to be automatically captured.
Use arrow functions when:
- You want to write short, simple, and concise anonymous functions.
- The function consists of a single expression.
- You want automatic variable capturing from the parent scope.
Conclusion
Hopefully, this blog post should have given you a helpful introduction to closure and arrow functions in PHP. You should now know enough to be able to start trying to use them in your own projects.
If you enjoyed reading this post, I'd love to hear about it. Likewise, if you have any feedback to improve the future ones, I'd also love to hear that too.
You might also be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.
If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter below.
Keep on building awesome stuff! ๐