I saw that the pipe operator RFC was accepted.
And I was wondering what it would mean for PHP codebases.
Introduction
Now if you need to execute multiple functions you have to create a functions onion, $uppercaseWords = str_word_count(strtoupper('test test'), 1);
.
This gets confusing very fast if the function calls have additional parameters.
When you use the pipe operator the code becomes more readable.
$uppercaseWords = 'test test'
|> strtoupper(...)
|> fn($value) => str_word_count($value, 1);
For the lazy typists amongst the developers, yes it means more typing.
Less fluent classes?
Now it is a common practice to wrap PHP functions in a class method to get similar readability.
class String {
public function __construct(private string $value)
{}
public function uppercase() : object
{
$this->value = strtoupper($this->value);
return $this;
}
public function getWords() : array
{
return str_word_count($this->value, 1);
}
}
// in the application
$uppercaseWords = new String('test test')
->uppercase()
->getWords();
As you can see the class methods are a mix of returning the instance, to make the method calls fluent, and function output. You have to be very aware of the method naming to convey the difference between the two outputs.
Because some of the methods return the instance you might need to have a mechanism to stop the fluency of the methods. It can be done by a method that returns the private value or with a fluency guard parameter for the methods that return the instance.
With the pipe operator you don't have to worry about naming methods or breaking the fluency.
More custom functions?
Fluent classes are one type of classes that could disappear. What about invokable classes? The benefit of an invokable class is that you can separate dependencies from the method specific arguments.
class HomeController
{
public function __construct(private ProductModel $product)
{}
public function __invoke(string $country)
{
$productTeasers = $this->product->getWhere('country', $country)->limit(3);
return view('home.html', compact($productTeasers));
}
}
// With the pipe operator this could be
function homeController(
string $country,
callable $productTeasersForCountry,
) {
return $country
|> fn($x) => $productTeasersForCountry($x, 3)
|> fn($productTeasers) => view('home.html', compact($productTeasers));
}
With the pipe operator the flow is more explicit, but this wil not work when exceptions are bubbling up.
To make the function as testable as the invokable class I exposed the database query function as an argument. I'm not sure if this is the best way forward.
Less use for traits?
In Laravel it is not possible for a model to have a reusable local scope, so a lot of people are using traits to solve that problem.
The builder design pattern is used for the scopes to work. With the pipe operator this would be more explicit.
class User extends Model
{
/**
* Scope a query to only include popular users.
*/
#[Scope]
protected function popular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* Scope a query to only include active users.
*/
#[Scope]
protected function active(Builder $query): void
{
$query->where('active', 1);
}
}
// in the application
$users = User::popular()->active()->get();
The pipe operator way to make the scopes reusable.
function popular(Builder $query): Builder
{
$query->where('votes', '>', 100);
return $query;
}
function active(Builder $query): Builder
{
$query->where('active', 1);
return $query;
}
// in the application
$query = User::query()
|> fn($x) => popular($x)
|> fn($x) => active($x);
$users = $query->get();
This is going to trouble the people that like the fluent methods. I think it is a good thing, because it separates the building of the query from the execution. Not needing traits is just a bonus in this case.
Conclusion
I think for the people that go for the pure object oriented way of programming the pipe operator isn't going to change much.
For the people that use multiple paradigms in their codebases it opens up more ways to get to a solution. And I am curious what creative solutions will come of it.
Top comments (2)
Frankly... it's another "let's add a thing to do the same things we can already do in a few other ways and don't really need" feature...
Sorta pointless.
I mentioned in the introduction I think it makes the code more readable, and in the post I mention that you have to be more explicit. For me that is enough to think of it if there is a situation that fits the operator case.
When you introduce more functional programming code like and Either or a Result type, that is where the pipe operator is going to shine.
The pipe operator is just one tool in the toolbox. But the beauty of multi paradigm languages like PHP is that you can mix and match paradigm behaviours. Like I showed in the Eloquent reusable local scopes example.
With all features in PHP it is up to you to use it or not.