It's recommended to avoid complex conditional expression because they are difficult to read and understand. However, if the expression uses only one type of logical operators to connect the conditions then it can be easily understood.
For example:
if ($a && $b || $c && ! $d) {
echo("Yes!");
}
Can you say at the first glance when the code in the if
branch (echo()
) will execute? Most probably no. The expression contains a mix of all the logical operations and its final value is difficult to evaluate without drawing the table of all possible values of $a
, $b
, $c
and $d
.
On the other hand, the expression:
if ($a && $b && $c && $d) {
echo("Hooray!");
}
is much easier to grasp.
The echo()
statement executes when all of $a
, $b
, $c
and $d
are simultaneously TRUE
.
Refactoring
In your case the condition is more complex than the first one presented above. It contains all the logical operators and parentheses and the sub-expressions are quite long.
In order to keep it readable I would extract the complex condition into a (private or protected) method that returns a boolean value (a predicate).
private function isValidData(array $data)
{
if (array_key_exists(self::SUB_FORM_CONTACT, $data) &&
array_key_exists('company', $data[self::SUB_FORM_CONTACT]) &&
((array_key_exists('salesRegion', $data[self::SUB_FORM_CONTACT]['company']) && !empty($data[self::SUB_FORM_CONTACT]['company']['salesRegion'])) ||
(array_key_exists('serviceRegion', $data[self::SUB_FORM_CONTACT]['company']) && !empty($data[self::SUB_FORM_CONTACT]['company'][''])))
) {
return TRUE;
} else {
return FALSE;
}
}
and replace the expression in the if
statement:
if ($this->isValidData($data)) {
}
On the next steps I would split the complex conditional expression into smaller conditions, test them one by one and make the function isValidData()
return (FALSE
) early, when it's possible.
Step 1
Because the expression is complex and the pieces are relatively long it's difficult to work with it as is. This is why, on the first step I would extract the conditions into local variables having shorter names ($a
, $b
a.s.o. from above):
private function isValidData(array $data)
{
$a = array_key_exists(self::SUB_FORM_CONTACT, $data);
$b = array_key_exists('company', $data[self::SUB_FORM_CONTACT]);
$c = array_key_exists('salesRegion', $data[self::SUB_FORM_CONTACT]['company']);
$d = empty($data[self::SUB_FORM_CONTACT]['company']['salesRegion']);
$e = array_key_exists('serviceRegion', $data[self::SUB_FORM_CONTACT]['company']);
$f = empty($data[self::SUB_FORM_CONTACT]['company']['']);
// this should probably read 'serviceRegion' -------^
if ($a && $b && (($c && ! $d) || ($e && ! $f))) {
return TRUE;
} else {
return FALSE;
}
}
Everything looks much clear now and we even have found out a bug. Great!
Step 2
Let's try to untangle the (now easier to read) condition:
if ($a && $b && (($c && ! $d) || ($e && ! $f))) {
return TRUE;
} else {
return FALSE;
}
It may become even easier to understand if we do one test at a time and return (TRUE
or FALSE
) as soon as the outcome is obvious.
The function returns TRUE
when $a
and $b
and the parenthesis that contain the rest of the expression are all TRUE
on the same time.
The if
statement can be rewritten this way:
if ($a) {
if ($b) {
if (($c && ! $d) || ($e && ! $f))) {
return TRUE;
}
}
}
return FALSE;
By inverting the conditions on $a
and $b
we can return FALSE
early:
if (! $a) {
return FALSE;
}
if (! $b) {
return FALSE;
}
if (($c && ! $d) || ($e && ! $f))) {
return TRUE;
}
return FALSE;
The code now looks like this:
private function isValidData(array $data)
{
$a = array_key_exists(self::SUB_FORM_CONTACT, $data);
$form = $data[self::SUB_FORM_CONTACT];
$b = array_key_exists('company', $form);
$c = array_key_exists('salesRegion', $form['company']);
$d = empty($form['company']['salesRegion']);
$e = array_key_exists('serviceRegion', $form['company']);
$f = empty($form['company']['serviceRegion']);
if (! $a) {
return FALSE;
}
if (! $b) {
return FALSE;
}
if (($c && ! $d) || ($e && ! $f))) {
return TRUE;
}
}
Notice that because all the assignments starting with the second one contained the common sub-expression $data[self::SUB_FORM_CONTACT]
we have extracted it into a local variable. The same can be applied to the last 4 assignments (extracting the common sub-expression $form['company']
:
// ...
$b = array_key_exists('company', $form);
$company = $form['company'];
$c = array_key_exists('salesRegion', $company);
$d = empty($company['salesRegion']);
$e = array_key_exists('serviceRegion', $company);
$f = empty($company['serviceRegion']);
Step 3
Let's notice that in expression $c && ! $d
:
array_key_exists('salesRegion', $company) && ! empty($company['salesRegion'])
the call to array_key_exists()
is not really needed. I guess its purpose is to avoid triggering notices when $company['salesRegion']
is accessed and $company
does not contain the key 'salesRegion'
. The empty()
PHP construct will happily return TRUE
when the key does not exist in the array and it doesn't trigger notices.
The same happens with $e && ! $f
. This allows us to completely remove $c
and $e
. The condition nows look like this:
if (! $d || ! $f) {
return TRUE;
}
Or
if (! ($d && $f)) {
return TRUE;
}
By negating the condition:
if ($d && $f) {
return FALSE;
} else {
return TRUE;
}
The complete function now looks like this:
private function isValidData(array $data)
{
$a = array_key_exists(self::SUB_FORM_CONTACT, $data);
$form = $data[self::SUB_FORM_CONTACT];
$b = array_key_exists('company', $form);
$company = $form['company'];
$d = empty($company['salesRegion']);
$f = empty($company['serviceRegion']);
if (! $a) {
return FALSE;
}
if (! $b) {
return FALSE;
}
if ($d && $f) {
return FALSE;
} else {
return TRUE;
}
}
Final step
We can now eliminate the local variables $a
, $b
, $d
and $f
and move the initialization of $form
and $company
after we know they are not NULL
(each one after the corresponding call to array_key_exists()
):
The final form of the function is this:
/**
* Verify if the provided data is valid.
*
* @param array $data the data to validate
* @return boolean TRUE if the data is valid, FALSE if some field is missing or empty
*/
private function isValidData(array $data)
{
// $data must contain the contact form information
if (! (array_key_exists(self::SUB_FORM_CONTACT, $data))) {
return FALSE;
}
// The contact form information exists in $data
$form = $data[self::SUB_FORM_CONTACT];
// The form must contain company information
if (! (array_key_exists('company', $form))) {
return FALSE;
}
// The company information exists in the contact form
$company = $form['company'];
// The company must contains information about salesRegion or serviceRegion or both
if (empty($company['salesRegion']) && empty($company['serviceRegion'])) {
return FALSE;
}
// The data is valid
return TRUE;
}
Conclusion
The code is several lines longer than before but:
- the conditions are easier to read and understand because they are evaluated one after another and not in a big complex expression;
- the code is encapsulated in a function; the reader of the
if ($this->isValidData($data))
condition now understands at a glance what happens there, without the need to read and understand how it happens; if it works then don't worry about it;
- the validation code can be tested independent of what happens when the data is valid or not;
- the code can be reused, if needed and appropriate, by calling the method with different data (maybe submitted through a different form).