Building a Better PHP — Part 4: More Hack
Carrying on from Part 3 of our series, we are going to take a look at some of the more complex (and in my opinion, cooler) parts of Hack.
Custom Types
While hack supports the creation of custom types, it’s important to understand that these are only used by the static analysis — at runtime, they are seen as their original underlying types.
Hack supports two different ways of defining new types, aliases, and opaque types. These are defined using the type
and newtype
keywords respectively.
Type aliases are just that — aliases. They simply allow you to give a more appropriate name for an existing type, or complex type — the static analyzer will permit you to use values of those types in the same way as the underlying type.
Opaque types on the other hand hide their underlying implementation outside of the file in which they are defined — this means that you cannot use them as standard types, and effectively they become immutable except when calling code defined within the file in which the type itself is defined. This restriction is not enforced at runtime.
<?hh // strict
type HTTPStatusCode = int;
class HTTPStatus {
const HTTPStatusCode OK = 200;
const HTTPStatusCode FOUND = 302;
const HTTPStatusCode NOT_FOUND = 404;
protected Map<HTTPStatusCode, string> $status = Map {
self::OK => "200 OK",
self::FOUND => "302 Found",
self::NOT_FOUND => "404 Not Found",
};
public function send(HTTPStatusCode $code): bool
{
if (isset($this->status[$code])) {
header('HTTP/1.1 ' .$this->status[$code]);
return true;
}
return false;
}
}
Here we have a class to handle HTTP Status codes. We have defined a type alias HTTPStatusCode
which is a simple int
.
Within our class, we have defined constants for each status code, type hinted using our type alias.
We then define a property, $status
that is a Map
with HTTPStatusCode
keys, and string
values.
Finally, we have our send()
method that accepts a HTTPStatusCode
argument.
We then call this code using:
<?hh
function notFound() {
$status = new HTTPStatus();
$status->send(404);
}
Because we use a type alias, we are allowed to pass any int
to the send()
method. This means that it’s possible to pass in an unknown status code, which requires that we validate it before trying to access it by checking isset()
.
We can make this code simpler by using an opaque type:
<?hh // strict
newtype HTTPStatusCode = int;
class HTTPStatus {
const HTTPStatusCode OK = 200;
const HTTPStatusCode FOUND = 302;
const HTTPStatusCode NOT_FOUND = 404;
protected Map<HTTPStatusCode, string> $status = Map {
self::OK => "200 OK",
self::FOUND => "302 Found",
self::NOT_FOUND => "404 Not Found",
};
public function send(HTTPStatusCode $code): bool
{
header('HTTP/1.1 ' .$this->status[$code]);
return true;
}
}
By using newtype
instead of type
, we can no longer pass standard int
values in to the send()
method. Doing so will cause hack to show the following error:
<file>:6:16,18: Invalid argument
<file>:15:23,36: This is an object of type HTTPStatusCode
<file>:6:16,18: It is incompatible with an int
Instead, we must use the constants we defined (or some other variable declared as a HTTPStatusCode
).
<?hh
function notFound() {
$status = new HTTPStatus();
$status->send(HTTPStatus::NOT_FOUND);
}
Now we can be relatively sure that we’re being passed a valid HTTPStatusCode
that will be in our Map
, and no longer need the isset()
validation.
This is the power of type hinted code — it reduces your workload, clarifies your code, and hopefully minimizes bugs.
Shapes
Shapes are an array-like construct, similar to tuples, but allowing us to pre-define the keys for our array and their value types.
We can use this as a form of validation — for example, if we want to represent an HTTP response we might create a shape that looks like:
<?hh // strict
newtype HTTPRequest = shape(
'status' => HTTPStatusCode,
'headers' => Map<string, string>,
'data' => shape (
'GET' => ?Map<string, mixed>,
'POST' => ?Map<string, mixed>,
'COOKIE' => ?Map<string, mixed>,
'SERVER' => Map<string, mixed>
),
'body' => ?string
);
At this point, any place a HTTPRequest
type hint is used, we can guarantee that all of these keys will exist, and have data of certain types.
XHP: XML fragments as expressions
One of the bigger changes to syntax is the introduction of XHP — and yet, it was originally released as an extension for PHP (also by Facebook), which you can still install and use with PHP today to get much of the same functionality.
Essentially, what XHP does is allow for XML tags to be top-level syntax, for example, note the lack of quotes as the markup is not a string.
<?hh
echo <p>Hello World</p>;
Now, as this is XML, it must be valid — this means all tags must be closed (or empty e.g. <br/>
).
Each tag is mapped to a class, whose name starts with a colon, for example the <p>
class would be named :p
. As this is XML, namespaces are also supported, for a tag named <hal:resource>
it would be the :hal:resource
class.
Each of these classes extends the :x:element
base class, which in turn supports a number of DOM-like methods:
appendChild()
prependChild()
replaceChildren()
getChildren()
getFirstChild()
getLastChild()
getAttribute()
,getAttributes()
setAttribute()
,setAttributes()
isAttributeSet()
removeAttribute()
Whenever you use an XHP element it instantiates an instance of the associated class.
Traditionally, we might do something like this to output a list of items:
<?php
echo '<ul>';
foreach ($items as $item) {
echo '<li>';
echo htmlentities($item, ENT_QUOTES, 'UTF-8');
echo '</li>';
}
echo '</ul>';
?>
With XHP, we can’t do this as we have incomplete XML fragments with our opening and closing <ul>
tags.
With XHP, we can take advantage of the object nature to create this XML structure dynamically:
<?hh
$list = <ul/>;
foreach ($items as $item) {
$list->appendChild(<li>{$item}</li>);
}
One other thing you might notice here is we have removed the call to htmlentities()
. Because our markup is no longer just part of a larger string, the engine is able to distinguish between markup and content — allowing it to automatically escape content.
Installing XHP
While the syntax is supported out of the box, the tags are not defined. To “install” XHP, you will need to grab the hack
branch of the php-lib from the xhp repository.
Then simply include init.hh
in any application that wants to use XHP.
Anonymous Functions
While Hack supports PHP-style closures, it also supports anonymous functions otherwise known as lambda expressions.
There are two reasons the Hack team decided to add this new form of anonymous functions:
- Anonymous functions have a much more compact syntax, which is also helped by the fact that:
- Scope is inherited by the anonymous function — this means that it inherits variables from its defining scope (by value)
Lambda expressions at their simplest use the syntax:
<?hh
$fn = $args ==> expression;
?>
However, there is an expanded syntax that allows for multiple expressions within the function, and optionally parenthesis to distinguish arguments:
<?hh
$fn = ($arg1, $arg2) ==> { expression; return ...; };
?>
Even with the expanded syntax, especially with scope capture, these are much more concise.
<?hh
$list = <ul/>;
array_walk($items, $item ==> $list->appendChild(<li>{$item}</li>);
echo $list;
Coming up next…
In our final installment on HHVM and Hack, we will look into using the static analyzer, as well as the HHVM command line tools, and how to convert your existing codebase to Hack.
It has been several months since HHVM 3.0 (with Hack) has been released, has it become part of your workflow? Are you using it in production? Let us know in the comments.
Share your thoughts with @engineyard on Twitter