Closure Pattern to Reduce Boilerplate

Published: 29 June, 2014

A closure is a function instance that takes variables from the current scope.

    $a = 3;
    $b = 4;
    $f = function() use ($a, $b) {
        return $a + $b;
    }
    $c = $f(); // now c = 7
    

These higher order functions can be used to abstract your code at a higher level, especially where you have some common wrapper code around some more specific code. Two good examples where closures can be used to cut boilerplate code are caching and database transactions.

Database Transaction Closure

To execute a set of queries in a transaction, you typically have to wrap your in some transaction code to explicitly commit the transaction on success or rollback the transaction on failure.

    $database->beginTransaction();
    try {
        $user1->cash -= 100;
        $user2->cash += 100;
        $userRepository->persist($user1);
        $userRepository->persist($user2);
        $database->commit();
    } catch (Exception $e) {
        $database->rollback();
    }

You can abstract out all the transaction related code into a higher order function like so:

    class Database
    {
        // higher order function decorates some other function with database transaction code
        public function transactional(callable $function)
        {
            $this->beginTransaction();
            try {
                $function();
                $this->commit();
            } catch (Exception $e) {
                $this->rollback();
            }
        }
    }

    // give the higher order function a closure to execute inside transaction
    $database->transactional(function () use ($userRepository, $user1, $user2) {
        $user1->cash -= 100;
        $user2->cash += 100;
        $userRepository->persist($user1);
        $userRepository->persist($user2);
    });
    

Now you have all the transaction code separate from the query code (no longer repeated across your code base).

Cache Closure

Typical cache handling code for getting a User object from a UserRepository by user ID looks like the following:

    // get value from cache if it exists, otherwise create it and save in cache for later
    $cacheKey = 'user';
    if ($cache->contains($cacheKey)) {
        $user = $cache->get($cacheKey);
    } else {
        $user = $userRepository->get($userId); // important code
        $this->set($cacheKey, $user);
    }
    

Like the transactional code example above, this caching code can be abstracted into a higher order function that takes a closure for generating the value to be cached.

    class ClosureCache extends ArrayCache
    {
        /**
         * Gets the value from the cache for key, otherwise creates and stores the value
         *
         * @param string $key cache key
         * @param callable $createFunction function to generate and store value for if not in cache
         */
        public function getOrElse($key, callable $createFunction)
        {
            if ($this->contains($key)) {
                return $this->get($key);
            } else {
                $value = $createFunction();
                $this->set($key, $value);
                return $value;
            }
        }
    }

    // get the user from cache by giving closure to compute value
    $user = $cache->getOrElse('user', function () use ($userRepository, $userId) {
        return $userRepository->get($userId);
    });
    

Binding Closure Params

Closures were added to PHP in version 5.3 and still have a few limitations. Variables in the current scope have to be explicitly passed to closures with the use directive. To compare that with a language that implicitly binds all variables in the current scope to closures, you can do the following in Scala:

    val user = cache.getOrElse("user", userRepository.get(userId))
    

As a result, the Scala code has essentially no boilerplate hiding the intent of your code.

Summary

Closures are a nice new addition to PHP and can greatly simplify repetitive boilerplate code as the two real-world examples demonstrate. Hopefully the next generation of libraries will make good use of this feature.

Comments