blank()
,present()
(renamed tofilled()
), andtransform()
are now helper functions built into the Laravel 5.5 core.
Developing a RESTful API in Laravel, internal quirks require request handling customization to fill in the framework’s gaps. Before getting to the database layer, each codebase needs custom boilerplate for transforming and filtering inputs. Up to version 5.3, HTTP PATCH endpoints have been better implemented by using Request
macros for a reliable parameter “picker” method. Michael Dyrynda’s partial model updates post does a good job of covering some of those edge cases.
Starting in Laravel 5.4, the core framework has moved toward better standardization of validation rules through sanitized inputs. New TrimStrings and ConvertEmptyStringsToNull middleware allow databases to keep a clean state while also avoiding cluttered controllers and FormRequest
classes.
There are still some form handling and API endpoint problems popping up but there is a path going forward.
Beware of Request@intersect()
behavior
From laravel.com docs, Retrieving Input:
“If you would like to retrieve a portion of input data that is actually present on the request, you may use the
intersect
method:”
So about that… what if the string ‘0’ is a valid request parameter value that needs to be saved to an integer column? Or an Eloquent model is being updated to clear a nullable description field? intersect()
excludes those keys because it runs array_filter()
, which defaults to rejecting PHP falsy array values. The model will be partially updated missing those key/values pairs – not as intended.
Thankfully by the middle of 2017 this will no longer be a problem.
Forthcoming Laravel 5.5 Request
changes
The next major version of Laravel aims to allow devs to better handle these edge cases by making significant changes to the Illuminate\Http\Request
class. Project refactoring for this 5.5 upgrade will be of the find-and-replace variety.
only()
now behaves likeCollection@only()
andArr::only
, excluding keys not present. This becomes the new partial update parameter picker, removingintersect()
. (#18695)- The old
only($keys)
is replaced byall($keys)
(#18754) - …and there’s something about
has()
,exists()
, and the newfilled()
method. (#18715)
With middleware forcing null
on blank input, there is now better clarity about what is “present” on the request payload and what should be validated and then sent to the database.
The ‘present’ validation rule is just
Arr::has()
that confirms the parameter name appears in the request payload. Whether there’s a value “present” (non-null, non-empty array, etc.) doesn’t matter. What’s in that request parameter value requires other validation rules.
“present” in Laravel app codebases
While the Laravel request lifecycle will soon have less pain points, in general projects still have an issue with PHP’s mixed-typing requiring not-so-readable conditional expressions. These will rear their ugly head during control flow of business logic or the presentation layer. The latter case would be Blade views or decorator classes that sit on top of the view and model.
Reading PHP type comparison tables from the official docs cover what happens for different if ($x)
conditions. Typically expressions will be some variant of:
is_null()
, variable is already assumed as defined and is it null?isset()
, is the variable defined and is it not null?empty()
, is the variable defined and is it PHP falsy?- Avoid using this on Laravel
Collection
objects as it has no relation to itsisEmpty()
method.
- Avoid using this on Laravel
The expressions I’ve continuously seen devs get burned on are if ($var)
or if (!empty($var))
when $var
isn’t a boolean type and 0
is a valid value.
if (!empty($item) && !empty($x) && !empty($y)) {
$map->items->push($item->at($x, $y));
}
If $item
is just an object (or null
), that first clause is OK. But this expression doesn’t allow an origin point (0, 0) as location of that item.
As a practical example, $latitude = 0.0
is valid for the equator so there needs to be a way to assert value presence when it is API-consumed string value '0.0'
. Preferably this can be done without instantiating a new Validator
instance in arbitrary sections of the codebase.
Global helper functions blank()
and present()
These functions won’t help with undefined variables, but they better express meaning behind conditionals. They intentionally only check the first parameter passed to the function so they can be used by array and collection pipeline callbacks.
$stringsThatArePresent = collect($nonTrimmedStrings)->filter('present');
Inspired by similar Ruby on Rails methods, a colleague (and fellow Cape Bretoner) Jason Smith introduced me to these functions along with a presence()
helper. These have parallel uses in the JavaScript universe for variables without strict-typing that cause conditionals to get out of hand.
function blank($value)
{
if (is_null($value)) {
return true;
}
if (is_string($value)) {
return trim($value) === '';
}
if (is_numeric($value) || is_bool($value)) {
return false;
}
if ($value instanceof \Illuminate\Support\Collection) {
return $value->isEmpty();
}
return empty($value);
}
function present($value)
{
return ! blank($value);
}
It could also be used (not so efficiently) on iterators:
if ($value instanceof \Traversable) {
return empty(iterator_to_array($value));
}
Global helper function presence()
This idiom is for grabbing an alternate value when nothing is present. In the majority of cases, the ternary ?:
or PHP7 null coalesce ??
operators better cover such a statement. With Laravel requests also now handling blank strings inputs, this function is best for consuming APIs that may give junk data.
function presence($value, $default = null)
{
if (present($value)) {
return $value;
}
if (is_callable($default)) {
return call_user_func($default, $value);
}
return $default;
}
With a single parameter, it can just be used as a null transformer when the value is blank.
$latitude = 0;
presence($latitude); // 0
$latitude = ''
presence($latitude); // null
$middleName = "\t ";
$middleName = presence($middleName); // null
This function is actually what I used in Laravel projects before ConvertEmptyStringsToNull
middleware was introduced.
Since it accepts a callback for the fallback value, you can defer expensive operations for when a value isn’t present.
public function response()
{
return presence($this->response, function () {
return $this->response = SomeService::get();
});
}
It’s easy for beginners to go too far with these helpers so I try to instill that they should only be used on primitive values. Duck typing is best used on PHP objects, especially Eloquent relationships where you know an object property is a given Eloquent class object or null.
if ($user->phone)
will do in place ofif (present($user->phone))
orif ($user->phone instanceof Phone)
.
These are strong idioms that clean out the mess out of PHP’s truthiness that causes cognitive overload and thus unexpected bugs.
transform()
because I hate writing code
So this is another helper I added to compute another value from something else only when it’s present. It’s for those three or four lines that could be one or two lines. Similar to tap(), it declutters with subtlety.
One use case is an optional GET query parameter (when route model binding isn’t available) that’s used to grab an Eloquent model. In the below example, an email campaign can direct leads to the checkout page with query ‘?coupon=MYDISCOUNT’ in the URL.
return view('order.checkout')
->with('coupon', transform(request('coupon'), function ($code) {
return Coupon::ofCode($code)->first();
}));
The most typical use for me is model decorators that generate human readable state of the database. Consider this presenter class for a Product
model that will show its pricing and subscription length.
use Format;
public function ProductPresenter
{
protected $model;
public function __construct($model)
{
$this->model = $model;
}
/**
* Localized dollar amount of product purchase before tax.
*
* e.g., $1,499.99 for 'en' locale
* 1 499,99 $ for 'fr' locale
*
* @return string
*/
public function price()
{
if (! is_null($this->model->price) {
return Format::currency($this->model->price);
}
return '-';
}
/**
* Number of months the product's pricing covers.
*
* @return string|null
*/
public function term()
{
if ($this->model->term > 0) {
return trans_choice('product.subscription-term', $this->model->term, [
'term' => $this->model->term,
]);
});
}
}
I just see imperative stuff that can be made more blunt:
public function price()
{
return transform($this->model->price, ['Format', 'currency'], '-');
}
public function term()
{
return transform($this->model->term, function ($term) {
return trans_choice('product.subscription-term', $term, compact('term'));
});
}
If those conditional blocks were more than five lines, in code reviews I’d request to avoid using transform()
as closures that long tend to indicate a new class or named method is a better solution. It’s just one way I nudge PHP towards functional programming happiness.
transform()
implementation
This leverages the above helpers so you may need to refactor it if you don’t wish to use them too.
public function transform($value, callable $callback, $default = null)
{
if (present($value)) {
return $callback($value);
}
if (is_callable($default)) {
return call_user_func($default, $value);
}
return $default;
}