What’s coming to Nephtali 3.3?

What’s coming to Nephtali 3.3? Lots of stuff!

Basic updates

First, let’s get to the basic updates.

  1. Pipes can be set to be optionally displayed if the markup for the pipe is present (just set is_optional to true in $opts array for n\pipe\register().)
  2. Ports can now be registered in bulk through n\port\register_bulk().
  3. Regexes updated for url and US phone.
  4. Many micro-optimizations implemented.
  5. Many improvements to doc-blocks within the API.

Now, before we move on to discuss the big new features, let me give you some background.

Functional programming languages light my fire

I love functional programming languages. Nephtali’s design principles clearly trace their roots to my experience with functional languages such as LISP, Haskell, Erlang, and others. While PHP is obviously not a functional programming language, through the Nephtali framework I’ve sought to embrace general functional programming principles such as immutability, laziness through use of a registration model, and using arrays for everything under the sun.

One of my favorite aspects of functional languages is the value placed in keeping functions “pure.” You can read what Wikipedia says about pure functions if you’re not familiar with them, but in short, a pure function will produce the same output for a given set of arguments every time the function is called because no other state is brought into or altered by the function. Pure functions are easier to reason about in terms of correctness, easier to test, and probably even increase life expectancy ūüôā

Some of you who are coming from functional languages are now saying to yourselves, “Yes, these principles of functional programming languages are great, but what could this possibly have to do with the¬†syntactically challenged language¬†PHP?”

I built a framework for PHP because of practical reasons. I develop websites for a living and my clients don’t care what language I’m using, they just want an effective, reasonably priced, maintainable website. PHP is a very practical language choice in light of these considerations. And, through Nephtali, I’ve been able to simulate some of the functional programming features I’ve missed the most. Now, back to the discussion of pure functions in Nephtali.

Nephtali 3.3 will promote pure pipe functions

While I can’t force particular functions to be pure in PHP, I can try and promote pure functions through the features and conventions in Nephtali, and Nephali 3.3 provides you with some handy features that facilitate keeping a lexically-pure pipe function. Let’s work through an example contrasting the code of an old-style Nephtali action pipe (I must stress Nephtali 3.3 is fully backwards compatible with this style) with the newer, pure-promoting style.

Old-style action pipe

n\port\register(
    $name = 'id',
    $opts = array(
        'max_length' => 10
    )
);
n\port\register(
    $name = 'company_name',
    $opts = array(
        'max_length' => 50,
        'filter' => n\constant\FILTER_TEXT
    )
);
n\port\register(
    $name = 'description',
    $opts = array(
        'max_length' => 500,
        'filter' => n\constant\FILTER_TEXT_MULTILINE
    )
);
n\port\register(
    $name = 'url',
    $opts = array(
        'max_length' => 200,
        'filter' => n\constant\FILTER_URL
    )
);
n\port\register(
    $name = 'did',
    $opts = array(
        'max_length' => 10,
    )
);
n\val($name = 'update_host', $value = array('id','company_name','description','url'));
n\val($name = 'insert_host', $value = array('company_name','description','url'));
n\val($name = 'delete_host', $value = array('did'));
n\pipe\register_action(
    $name = 'host',
    $actions = array(
        n\port\signature(n\val('update_host')) => function($markup)
        {
            if ($rows = n\port\validate(n\val('update_host')))
            {
                return n\view\render($view = 'feedback', $markup, $rows);
            }
 
            n\sql\action\update($table_name = 'hosts', $inputs = n\port\get(n\val('update_host')));
            return n\view\render($view = 'default', $markup, $rows = array(array('message' => "We've updated the host")));
         },
        n\port\signature(n\val('insert_host')) => function($markup)
        {
            if ($rows = n\port\validate(n\val('insert_host')))
            {
                return n\view\render($view = 'feedback', $markup, $rows);
            }
 
            n\sql\action\insert($table_name = 'hosts', $inputs = n\port\get(n\val('insert_host')));
            return n\view\render($view = 'default', $markup, $rows = array(array('message' => "We've added the host")));
         },
        n\port\signature(n\val('delete_host')) => function($markup)
        {
            if ($rows = n\port\validate(n\val('delete_host')))
            {
                return n\view\render($view = 'feedback', $markup, $rows);
            }
 
            n\sql\action\delete($table_name = 'hosts', $id = n\port\get('did'));
            return n\view\render($view = 'default', $markup, $rows = array(array('message' => "We've deleted the host")));
         }
    )
);

I won’t talk much about this example as most Nephtali users are familiar with this approach. However, let me just point out the amount of work it takes to either mentally work through correctness of the code OR write unit tests for it.

New pure-promoting pipe

n\port\register_bulk(
	$ports = array(
		'id' => array(
			'max_length' => 10
		),
		'company_name' => array(
			'max_length' => 50,
			'filter' => n\constant\FILTER_TEXT
		),
		'description' => array(
			'max_length' => 500,
			'filter' => n\constant\FILTER_TEXT_MULTILINE
		),
		'url' => array(
			'max_length' => 200,
			'filter' => n\constant\FILTER_URL
		),
		'did' => array(
			'max_length' => 10,
		)
	)
);
n\pipe\register(
    $name = 'host',
    $function = function($markup, $args, $signatures)
	{
		if ($signatures['update']['is_valid']) {
			if ($signatures['update']['errors']) {
				return n\view\render($view = 'feedback', $markup, $signatures['update']['errors']);
			} else {
				if ($args['update']) {
					return n\view\render($view = 'default', $markup, $rows = array('message' => "We've updated the host"));
				} else {
					return n\view\render($view = 'error', $markup);
				}
			}
		} else
		if ($signatures['insert']['is_valid']) {
			if ($signatures['insert']['errors']) {
				return n\view\render($view = 'feedback', $markup, $signatures['insert']['errors']);
			} else {
				if ($args['insert']) {
					return n\view\render($view = 'default', $markup, $rows = array('message' => "We've inserted the host"));
				} else {
					return n\view\render($view = 'error', $markup);
				}
			}
		} else
		if ($signatures['delete']['is_valid']) {
			if ($signatures['delete']['errors']) {
				return n\view\render($view = 'feedback', $markup, $signatures['delete']['errors']);
			} else {
				if ($args['delete']) {
					return n\view\render($view = 'default', $markup, $rows = array('message' => "We've deleted the host"));
				} else {
					return n\view\render($view = 'error', $markup);
				}
			}
		} else {
			return '';
		}
    },
	$opts = array(
		'signatures' => array(
			'update' => array('id','company_name','description','url'),
			'insert' => array('company_name','description','url'),
			'delete' => array('did')
		)
	),
	$args = array(
		'update' => function($signatures) {
			try {
				return n\sql\action\update($table_name = 'hosts', $inputs = $signatures['update']['values']);
			} catch (Exception $e) {
				return false;
			}
		},
		'insert' => function($signatures) {
			try {
				return n\sql\action\insert($table_name = 'hosts', $inputs = $signatures['insert']['values']);
			} catch (Exception $e) {
				return false;
			}
		},
		'delete' => function($signatures) {
			try {
				return n\sql\action\delete($table_name = 'hosts', $id = $signatures['delete']['values']['did']);
			} catch (Exception $e) {
				return false;
			}
		}
	)
);

In the new style, the pipe function is structured so that you can quite easily reason about the flow AND write unit tests for it. No contrived mock objects, no IO gotchas, just pass in the arguments representing the states you want to test and¬†voila, you’ve got a test (mind you, if you haven’t carefully reasoned the correctness of your code, your test will suck, but I know you like seeing pretty green lights, so…)

The “how” behind the above code owes much to the magic of the $args argument. While it looks like an array, it’s actually an object that implements the appropriate interfaces so it can be accessed like an array, keeping consistent with Nephtali’s M.O. in that regard. It stores the functions and only processes a function once you try to access the value. However, any further access to the variable makes use of a cached value result, so you don’t have to worry about calling an insert function more than once by accessing the value later on in your code, for example.

The beauty in the $args argument is that it encourages you to keep as much logic as possible in the easily tested pipe function. Only pull out the basic state-accessing/changing operations into the $args functions, and you’ll have code that’s easier to reason about and to test.

Coming soon, to a server near you…

P.S. – You can obviously refactor the new pure-promoting style so it is much shorter, as I’ve done below (and, indeed, it could even be shorter, but this gives you just a quick idea), but I didn’t include this in the body of the article out of fear that it’s harder to conceptually work through what’s going on in terms of the new features.

$function = function($markup, $args, $signatures)
	{
		$valid_sig_processor = function($signature, $arg, $message) {
			if ($signature['errors']) {
				return n\view\render($view = 'feedback', $markup, $signature['errors']);
			} else {
				if ($arg) {
					return n\view\render($view = 'default', $markup, $rows = array('message' => $message));
				} else {
					return n\view\render($view = 'error', $markup);
				}
			}
		};
 
		if ($signatures['update']['is_valid']) {
			return $valid_sig_processor($signatures['update'], $args['update'], "We've updated the host.");
		} else
		if ($signatures['insert']['is_valid']) {
			return $valid_sig_processor($signatures['insert'], $args['insert'], "We've inserted the host.");
		} else
		if ($signatures['delete']['is_valid']) {
			return $valid_sig_processor($signatures['delete'], $args['delete'], "We've deleted the host.");
		} else {
			return '';
		}
    }