New Features - In Depth
XHP-Lib 2.x has included major changes and new features; this post covers how to use them, and explains the the rationale behind some of the decisions.
Hack Conversion
XHP-Lib 2.x is written entirely in Hack, primarily so that we can take advantage of the async features - though 1.x is still supported for PHP5 users. The Hack migration also means that you can (and should) use the Hack typechecker with your XHP-based projects; this has led to several bugs being discovered in both XHP-Lib and XHP-Bootstrap, and many more have been prevented.
Additionally, features such as trait requirements and traits being able to implement interfaces have both reduced the amount of boilerplate and resulted in clearer errors for users.
Attribute Types
Attribute types are now consistent with the Hack type system with the exception
of the enum { "foo", "bar" }
syntax, which is just a string
as far as the
typechecker is concerned; for most cases,
hack enums are likely
to be a better choice.
This change was mostly done just to increase consistency, which reduces the amount of rules that people need to learn; it also allows the typechecker to verify that attributes are used correctly:
class :foo {
attribute
int myattr @required;
protected function render(): XHPRoot {
// In all versions, the typechecker does not know the type of $untyped
$untyped = $this->getAttribute('myattr');
// ...but it understands that $attr is an integer (HHVM 3.6+ required)
$attr = $this->:myattr;
return <div />;
}
}
function main() {
// Correct
$x = <foo myattr={123} />;
/* Incorrect as this passes a string.
* - XHP-Lib v2.0: E_USER_DEPRECATED
* - XHP-Lib v2.1: exception
* - HHVM 3.7+: typechecker error
*/
$x = <foo myattr="123" />;
}
A few points are worth highlighting:
- Like function parameters,
string
does not allow a Stringable object. If you want to allow these,Stringish
is an HHVM-specific interface supported by both scalar strings and byStringable
objects. callable
has been removed, as it is not a valid type in Hack.
Most of the HTML attributes have been converted to use Stringish
; some
example uses include:
$xhp = <a href={$my_uri_object} />;
$xhp = <div class={myFancyMinifyingStaticResourcesSystem('className')} />;
Asynchronous XHP Rendering
At Facebook, we tend to think of XHP as a component system rather than merely a template system; this ideally means strict encapsulation, with call-sites not aware of data-fetching requirements. For example:
class :mysite:typeahead extends :x:element {
attribute
int edgetype @required;
protected function render(): XHPRoot {
$user = User::fetchFromID($this->getContext('userid'));
$targets = $user->fetchRelatedObjects($this->:edgetype);
// ...
return <div>{ /* ... */ }</div>;
}
}
Unfortunately, this code is a performance nightmare:
- rendering this involves blocking IO - potentially 3 database queries
- it’s not obvious at call-sites that this is a very expensive component to render
- the entire tree is rendered sequentially - so, every extra typeahead you ahead adds the time taken for 3 more database queries
Async allows us to fix this, without significantly changing the way we structure code:
class :mysite:typeahead extends :x:element {
use XHPAsync;
attribute
int edgetype @required;
protected async function asyncRender(): Awaitable<XHPRoot> {
$user = await User::asyncFetchFromID($this->getContext('userid'));
$targets = await User::asyncFetchRelatedObjects($this->:edgetype);
return <div>{ /* ... */ }</div>;
}
}
As XHP-Lib automatically parallelizes any asyncRender()
calls in the tree, the
performance is no longer a major concern: the total IO time is the time required
for the longest data dependency chain, not the sum of all of them.
Embedding Non-XHP Content
Naively, all your markup should be XHP - however this isn’t always practical
(e.g. when migrating to XHP) or desirable (e.g. if you prefer to keep static
content as markdown files). XHP-Lib now supports this natively via the
XHPUnsafeRenderable
interface:
class OldTemplateXHP implements XHPUnsafeRenderable {
public function __construct(
private OldTemplate $template,
) {
}
public function toHTMLString(): string {
return $this->template->render();
}
}
public function XHPIZE(OldTemplate $template): XHPChild {
return new OldTemplateXHP($template);
}
class :my:xhp-class extends :x:element {
protected function render(): XHPRoot {
// ...
$template = new MyTemplate($a, $b, $c);
return <div>{XHPIZE($template)}</div>;
}
}
There is a problem: XHP supports validating child elements, and you might need
to embed non-XHP markup in an element that validates its children. Avoid this
if at all possible, but sometimes it’s not. In that case, add the
XHPAlwaysValidChild
interface:
class OldTemplateXHP implements XHPUnsafeRenderable, XHPAlwaysValidChild {
Please be extremely cautious about using either of these interfaces: their entire purpose is to bypass the safety that XHP usually provides.
XHPHelpers
We’ve added an additional trait, XHPHelpers
, which solves three common
problems:
- requiring a unique ID for an element, but not overwriting an ID if one is already specified - and not requiring the creator of a component to build support for an ID
- transfering attributes from composite elements to the underlying HTML elements
class
should be appended, instead of transferred verbatim
For example:
class :example:with-id extends :x:element {
use XHPHelpers;
attribute :xhp:html-element;
protected function render(): XHPRoot {
return <div id={$this->getID()} />;
}
}
class :example:with-transferred {
use XHPHelpers;
attribute :xhp:html-element;
protected function render(): XHPRoot {
return <div class="foo" />;
}
}
function main() {
// <div id="something_random"></div>
var_dump((<example:with-id />)->toString());
// <div id="herp"></div>
var_dump((<example:with-id id="herp" />)->toString());
// <div id="herp" class="foo"></div>
var_dump((<example:with-transferred id="herp" />)->toString());
// <div class="foo bar"></div>
var_dump((<example:with-transferred class="bar" />)->toString());
}
The Future
We feel this is a great start; we’re looking forward to bringing you namespace support, and tools to implement the ‘component’ concept more thoroughly.
If there are things you’d like to see, please file an issue on GitHub.
Comments