Back to Course |
PHP for Laravel Developers

Callback Functions or Closures

So-called callback functions, "callables" or "closures" are used in Laravel very often, so we need to understand all the details behind their syntax.

Take a look at these Laravel examples.

You can provide the route functionality in a callback function without any Controller:

Route::get('/greeting', function () {
return 'Hello World';
});

You can use Collections to map through data with your custom logic defined in a callback function:

$socialLinks = collect([
'Twitter' => $user->link_twitter,
'Facebook' => $user->link_facebook,
'Instagram' => $user->link_instagram,
])
->filter()
->map(fn ($link, $network) => '<a href="' . $link . '">' . $network . '</a>')
->implode(' | ');

In Eloquent, you may use the chunk() method with a closure, too:

use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
 
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});

So, what are the main things we need to know about them?


When Can We Use Them As Parameters?

When the method is defined with the parameters types as callable.

So, if you take a look at the Laravel framework core, you will see these examples:

src/Illuminate/Database/Concerns/BuildsQueries.php

public function chunk($count, callable $callback)
{
// ...

Another one:

src/Illuminate/Collections/Arr.php

public static function map(array $array, callable $callback)
{
// ...

Also, remember that callable may be only one of the accepted parameter types. There can be others:

src/Illuminate/Collections/Arr.php

/**
* Sort the array using the given callback or "dot" notation.
*
* @param array $array
* @param callable|array|string|null $callback
* @return array
*/
public static function sort($array, $callback = null)
{
return Collection::make($array)->sortBy($callback)->all();
}

Parameters VS "use" Variables from Outside

Let me show you the most typical mistake developers make with closures.

When using some variable in a closure, many starting developers think that variable is accessible in a closure by default, but it isn't.

To access the variable in a closure, it must be added in a use.

Here is a typical example from open-source financial freedom project using database transactions.

app/Actions/Fortify/CreateNewUser.php:

public function create(array $input)
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
])->validate();
 
return DB::transaction(function () use ($input) {
return tap(User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]), function (User $user) {
$this->createTeam($user);
});
});
}

If you forget to add a variable in a use, you will receive a typical error message:

Undefined variable $input

Also, your IDE should inform you about the undefined variable.


Short Syntax

When using functions, you can use a shorter version called the arrow function.

use Illuminate\Database\Eloquent\Builder;
 
public function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes());
}

But sometimes, using short closure can be hard to read, and it isn't that short.

Forms\Components\TextInput::make('name')
->afterStateUpdated(fn (string $operation, $state, Forms\Set $set) => $operation === 'create' ? $set('slug', Str::slug($state)) : null),

Writing it as a normal closure would be more readable in this case.

Forms\Components\TextInput::make('name')
->afterStateUpdated(function(string $operation, $state, Forms\Set $set) {
if ($operation === 'create') {
$set('slug', Str::slug($state));
}
})

Assign Function to a Variable

There might be places where you need to use some variable in more than one place, but that variable could have a value based on some condition.

In this case, assigning a variable to a closure is called an anonymous function.

In this example from Filament, a variable is assigned based on condition.

packages/support/resources/views/components/grid/column.blade.php:

$getSpanValue = function ($span): string {
if ($span === 'full') {
return '1 / -1';
}
 
return "span {$span} / span {$span}";
};

Then, this variable is used in more than one place.

packages/support/resources/views/components/grid/column.blade.php:

<div
{{
$attributes
// ...
->style([
"--col-span-default: {$getSpanValue($default)}" => $default,
"--col-span-sm: {$getSpanValue($sm)}" => $sm,
"--col-span-md: {$getSpanValue($md)}" => $md,
"--col-span-lg: {$getSpanValue($lg)}" => $lg,
"--col-span-xl: {$getSpanValue($xl)}" => $xl,
"--col-span-2xl: {$getSpanValue($twoXl)}" => $twoXl,
"--col-start-default: {$defaultStart}" => $defaultStart,
"--col-start-sm: {$smStart}" => $smStart,
"--col-start-md: {$mdStart}" => $mdStart,
"--col-start-lg: {$lgStart}" => $lgStart,
"--col-start-xl: {$xlStart}" => $xlStart,
"--col-start-2xl: {$twoXlStart}" => $twoXlStart,
])
}}
>
{{ $slot }}
</div>