Elegant way of chaining checks in PHP

Viewed 324

Lets imagine you have a general validation function and you have to do several sub-checks to the input.Each of them also returns boolean. See this fictive one:

private function hasValidContent(string $content) : bool
{
    $isValid = false;
    $isValid = $this->hasMoreThanOneChar($content);
    $isValid = $this->hasValidCharacters($content);
    $isValid = $this->hasCapitalLetters($content);
    ...
    
    return $isValid;

}

Of course, this code above wont work since every next check will override the evaluation of the previous.

But how can it be achieved to stop further checks, when the first check leads to false? For example if its content has not more than one char, then it should stop after the hasMoreThanOneChar function and return the hasValidContent method immediately as false.

Yes of course, you could check after each method call if the expression turned to false, but that is awkward and is much overhead and repetition.

Also it feels really ugly to do it in a single if with thousands of || or && like in

return $this->checkA($content) 
   && $this->checkB($content)
   && $this->checkC($content)
  ...;

The readability will suffer after few more expressions.

A further often mentioned approach might be using exceptions

private function hasValidContent(string $content) : bool
{
    try {
        $this->hasMoreThanOneChar($content);
        $this->hasValidCharacters($content);
        $this->hasCapitalLetters($content);
        ...
    }
    catch {
        return false;
    }
    return true;

}

private function hasMoreThanOneChar(string $string) : void {
    if(count($string) < 2 ) {
      throw new HasNotMoreThanOneCharException(...)
    }
}

But i also see this not as an elegant solution since validating stuff is nothing exceptional.

So my question:

Is there any elegant pattern for this? Any keyword i can search for?

3 Answers

Create an array with all the validations you want to perform. Each element of the array must be a callable. Iterate and exit early. If all validators pass, you're good.

private function hasValidContent(string $content) : bool
{
    $validators = [
        // instance method
        [$this, 'hasMoreThanOneChar'],
        // callable class
        new class {
            public function __invoke(string $content)
            {
                return ...;
            }
        },
        // anonymous function
        function (string $content): bool {
            return ...;
        }
    ];

    foreach ($validators as $validate) {
        if (!$validate($content)) {
            return false;
        }
    }

    return true;
}

A suggestion can be to make the validation methods throw an Exception instead of returning true and false (return nothing when successful).

For example:

Class Validator {
    public static function assertString($string) {
        if (!is_string($string)) {
            throw new Exception('Type mismatch.');
        }
    }
    
    public static function assertStringLength($string, $min, $max) {
        if (strlen($string) < $min || strlen($string) > $max) {
            throw new Exception('String outside of length boundaries.');
        }
    }
}

Your code will look something like that:

try {
    Validator::assertString($string);
    Validator::assertStringLength($string, 4, 50);
} catch (Exception $e) {
    // handle invalid input
}

Note that my functions are static, it makes sense in this case, but not necessarily always.

I infer you are looking for the chain of responsibility pattern.

For example let's say you have three classes: Locks, Alarm and Lights which check the home's status.

abstract class HomeChecker {
    protected $successor;

    public abstract function check(Home $home);

    public function succeedWith($successor) {
        $this->successor = $successor;
    } 

    public function next(Home $home) {
        $this->successor->check($home);
    }
}

class HomeStatus {
     public $locked = true;
     public $alarmOn = true;
     public $lightsOff = true;
}

class Locks extends HomeChecker {
   public function check(Home $home) {
       if(!$home->locked) {
           throw new Exception('The doors are not locked'); // or basically whatever you want to do
       }
       $this->next($home);
   }
}

class Alarm extends HomeChecker {
   public function check(Home $home) {
       if(!$home->alarmOn) {
           throw new Exception('The Alarms are not on'); // or basically whatever you want to do
       }
       $this->next($home);
   }
}

class Lights extends HomeChecker {
   public function check(Home $home) {
       if(!$home->lightsOff) {
           throw new Exception('The lights are still on!'); // or basically whatever you want to do
       }
       $this->next($home);
   }
}

$locks = new Locks();
$alarm = new Alarm();
$lights = new Lights();
$locks->succeedWith(new Alarm); // set the order in which you want the chain to work
$alarms->succeedWith(new Lights);

$locks->check(new HomeStatus);

All the three classes: Locks, Lights and Alarm extend a class called HomeChecker which basically has an abstract check method to ensure that the children classes do implement that method and two methods called succeedWith and next. The succeedWith method is used to set a successor that would be called if the current method getting executed returns true (meaning no error). And the next method essentially calls the next method chained up.

With this line $locks->check(new HomeStatus);, we are beginning the chain, if the check in the check method of locks class fails then an error would be thrown and the execution would be stopped else it will call the next method of HomeChecker class which will ultimately call the check method of the next class which we had set

$locks->succeedWith(new Alarm); // set the order in which you want the chain to work
$alarms->succeedWith(new Lights);

in these lines.

So in this way we can very elegantly chain checks. I hope this will help you.

Related