From 16b4459a1bf8a1424fe489710626f0a113fd52dd Mon Sep 17 00:00:00 2001 From: jjergus Date: Wed, 5 Aug 2020 11:07:51 -0700 Subject: [PATCH] rewrite XHP guides for namespace support (xhp-lib v4) --- guides/hack/21-XHP/01-introduction.md | 39 +---- guides/hack/21-XHP/04-setup.md | 65 ++++++++ guides/hack/21-XHP/07-basic-usage.md | 74 +++++++-- guides/hack/21-XHP/10-interfaces.md | 59 +++++--- .../13-methods-examples/list-builder.php | 33 ++-- .../list-builder.php.hhvm.expect | 22 +-- guides/hack/21-XHP/13-methods.md | 31 ++-- .../16-extending-examples/basic.inc.php | 8 +- .../16-extending-examples/children.inc.php | 8 +- guides/hack/21-XHP/16-extending.md | 141 +++++++++++------- guides/hack/21-XHP/19-migration.md | 25 ++-- guides/hack/21-XHP/22-guidelines.md | 18 +-- 12 files changed, 334 insertions(+), 189 deletions(-) create mode 100644 guides/hack/21-XHP/04-setup.md diff --git a/guides/hack/21-XHP/01-introduction.md b/guides/hack/21-XHP/01-introduction.md index 8878e47f5..e1e62b8b6 100644 --- a/guides/hack/21-XHP/01-introduction.md +++ b/guides/hack/21-XHP/01-introduction.md @@ -5,7 +5,6 @@ must contain ``. Using traditional interpolation, a simple page could look like this: ```Hack -<?hh // strict $user_name = 'Fred'; echo "<tt>Hello <strong>$user_name</strong></tt>"; ``` @@ -13,7 +12,6 @@ echo "<tt>Hello <strong>$user_name</strong></tt>"; However, with XHP, it looks like this: ``` -<?hh // strict $user_name = 'Fred'; echo <tt>Hello <strong>{$user_name}</strong></tt>; ``` @@ -26,39 +24,18 @@ fully understood by Hack—but this does not mean that all you need to do is - Remove all HTML/attribute escaping - e.g., you don't need to call `htmlspecialchars` before including a variable in your XHP output; and if you do, it will be double-escaped. -## About Namespaces - -XHP currently has several issues with namespaces; we recommend that: - - XHP classes are not declared in namespaces - - code that use XHP classes is not namespaced. - -We [plan to support namespaces](https://github.com/facebook/xhp-lib/issues/64) in the future. - -## The XHP-Lib Library - -While the XHP syntax is part of Hack, a large part of the implementation is in a library called XHP-Lib that needs to be installed via composer; -add `xhp-lib` to your `composer.json`, e.g: - -``` -$ composer require facebook/xhp-lib -``` - -This includes the base classes and interfaces, and definitions of standard HTML elements. - -Do not use HHVM to execute composer. - ## Why use XHP? The initial reason for most users is because it is *safe by default*: all variables are automatically escaped in a context-appropriate way (e.g., there are different rules for escaping attribute values vs. text nodes). In addition, XHP -is understood by the typechecker, making sure that you don't pass attribute values. A common example of this is `border="3"`, +is understood by the typechecker, making sure that you don't pass invalid attribute values. A common example of this is `border="3"`, but `border` is an on/off attribute, so a value of 3 doesn't make sense. For users experienced with XHP, the biggest advantage is that it is easy to add custom 'elements' with your own behavior, -which can then be used like plain HTML elements. For example, this site defines an `<a:post>` tag that has the same interface +which can then be used like plain HTML elements. For example, this site defines an `<a_post>` tag that has the same interface as a standard `<a>` tag, but makes a POST request instead of a GET request: -@@ some-basics-examples/a_post.inc.php @@ +@@ introduction-examples/a_post.inc.php @@ A little CSS is needed so that the `<form>` doesn't create a block element: @@ -70,20 +47,20 @@ form.postLink { At this point, the new element can be used like any built-in element: -@@ some-basics-examples/a_post_usage.php @@ +@@ introduction-examples/a_post_usage.php @@ ## Runtime Validation Since XHP objects are first-class and not just strings, a whole slew of validation can occur to ensure that your UI does not have subtle bugs: -@@ some-basics-examples/tag-matching-validation.php.type-errors @@ +@@ introduction-examples/tag-matching-validation.php.type-errors @@ The above code won't typecheck or run because the XHP validator will see that `<span>` and `<naps>` tags are mismatched; however, the following code will typecheck correctly but fail to run, because while the tags are matched, they are not nested correctly (according to the HTML specification), and nesting verification only happens at runtime: -@@ some-basics-examples/allowed-tag-validation.inc.php @@ -@@ some-basics-examples/allowed-tag-validation.php @@ +@@ introduction-examples/allowed-tag-validation.inc.php @@ +@@ introduction-examples/allowed-tag-validation.php @@ ## Security @@ -91,4 +68,4 @@ String-based entry and validation are prime candidates for cross-site scripting functions like [`htmlspecialchars`](http://php.net/manual/en/function.htmlspecialchars.php), but then you have to actually remember to use those functions. XHP automatically escapes reserved HTML characters to HTML entities before output. -@@ some-basics-examples/avoid-xss.php @@ +@@ introduction-examples/avoid-xss.php @@ diff --git a/guides/hack/21-XHP/04-setup.md b/guides/hack/21-XHP/04-setup.md new file mode 100644 index 000000000..d289c1d37 --- /dev/null +++ b/guides/hack/21-XHP/04-setup.md @@ -0,0 +1,65 @@ +## Namespace Support + +Support for namespaced XHP classes (elements) has only recently been added to +HHVM (as of HHVM 4.68). Depending on the exact HHVM version, it may not be +enabled by default, so make sure to add these flags to your `.hhconfig`: + +``` +enable_xhp_class_modifier=true +disable_xhp_element_mangling=true +disable_xhp_children_declarations=true +check_xhp_attribute=true +``` + +And to `hhvm.ini` (or provided via `-d` when executing `hhvm`): + +``` +hhvm.hack.lang.enable_xhp_class_modifier=true +hhvm.hack.lang.disable_xhp_element_mangling=true +``` + +Using a subset of these flags might work, but is not officially supported and +tested, so we currently recommend using either all or none of them. + +If these flags are disabled, or using an older version of HHVM: + +- XHP classes cannot be declared in namespaces (only in the global namespace) +- any code that uses XHP classes also cannot be in a namespace, as HHVM + previously didn't have any syntax to reference XHP classes across namespaces +- note that the above two rules are not consistently enforced by the + typechecker or the runtime, but violating them can result in various bugs +- it is, however, possible to use namespaced code from inside XHP class + declarations + +Make sure to also use the correct version of XHP-Lib (see below) based on +whether XHP namespace support is enabled in your HHVM version. + +## The XHP-Lib Library + +While the XHP syntax is part of Hack, a large part of the implementation is in a +library called [XHP-Lib](https://github.com/hhvm/xhp-lib/) that needs to be +installed via composer; add `xhp-lib` to your `composer.json`, e.g: + +``` +$ composer require facebook/xhp-lib +# note: make sure to use PHP, not HHVM, to execute composer +``` + +This includes the base classes and interfaces, and definitions of standard HTML elements. + +### XHP-Lib Version + +There are currently two major supported versions of XHP-Lib: + +- **XHP-Lib v4:** to be used when XHP namespace support is enabled. Declares all + base classes, interfaces and elements in namespaces (e.g. standard HTML + elements are in `Facebook\XHP\HTML`). It is also more strict (e.g. disallows + most mutations after an element is rendered) and deprecates some features + (e.g. attribute transfer). +- **XHP-Lib v3:** to be used in older HHVM versions or when XHP namespace + support is disabled. Declares everything in the global namespace, with the + exception of `Facebook\XHP\ChildValidation`. + +All the following guides are written with the assumption that XHP namespace +support is enabled and XHP-Lib v4 is used, but there are notes pointing out any +major differences—look for **Historical note** sections. diff --git a/guides/hack/21-XHP/07-basic-usage.md b/guides/hack/21-XHP/07-basic-usage.md index 28c2b0181..16006d85e 100644 --- a/guides/hack/21-XHP/07-basic-usage.md +++ b/guides/hack/21-XHP/07-basic-usage.md @@ -1,8 +1,5 @@ -First, make sure that you have the [XHP Library](some-basics.md#the-xhp-lib-library) installed a dependency of your project—this defines -the various core classes of XHP, and the standard HTML components. - XHP is a syntax to create actual Hack objects, called *XHP objects*. They are meant to be used as a tree, where children can either be -other XHP objects or text nodes. +other XHP objects or text nodes (or, rarely, other non-XHP objects). ## Creating a Simple XHP Object @@ -12,16 +9,69 @@ Instead of using the `new` operator, creating XHP looks very much like XML: $my_xhp_object = <p>Hello, world</p>; ``` -`$my_xhp_object` now contains an instance of the `:p` class; the initial `:` marks it as an XHP class, but is not needed when instantiating -it. It is a real object, meaning that `is_object` will return `true` and you can call methods on it. +`$my_xhp_object` now contains an instance of the `p` class. +It is a real object, meaning that `is_object` will return `true` and you can call methods on it. + +**Historical note:** Before XHP namespace support (in XHP-Lib v3), XHP classes +lived in a separate (but still global) namespace from regular classes, denoted +by a `:` prefix in the typechecker and an `xhp_` prefix at runtime. `<p>` would +therefore instantiate a class named `:p` in Hack code and `xhp_p` at runtime. It +would therefore not conflict with a global non-XHP class named `p`, but would +conflict with a class named `xhp_p`. -The following example utilizes three XHP classes: `:div`, `:strong`, `:i`. Whitespace is insignificant, so you can create a readable +The following example utilizes three XHP classes: `div`, `strong`, `i`. Whitespace is insignificant, so you can create a readable tree structure in your code. @@ basic-usage-examples/basic.php @@ -The `var_dump` shows that a tree of objects has been created, not an HTML/XML string. An HTML string can be produced either by simply -using `echo`/`print`, or by calling `$xhp_object->toString`. +The `var_dump` shows that a tree of objects has been created, not an HTML/XML string. An HTML string can be produced by calling `await $xhp_object->toStringAsync()`. + +## Namespace Syntax + +When instantiating an XHP class using the `<ClassName>` syntax, `:` must be used +instead of `\` as a namespace separator (this mirrors XML's namespace syntax). +These are all equivalent ways to instantiate a `Facebook\XHP\HTML\p` object: + +``` +use type Facebook\XHP\HTML\p; +$xhp = <p>Hello, world</p>; +``` + +``` +use namespace Facebook\XHP\HTML; +$xhp = <HTML:p>Hello, world</HTML:p>; +``` + +``` +use namespace Facebook\XHP\HTML as h; +$xhp = <h:p>Hello, world</h:p>; +``` + +``` +// from global namespace: +$xhp = <Facebook:XHP:HTML:p>Hello, world</Facebook:XHP:HTML:p>; +``` + +``` +namespace CustomNamespace; // from any namespace: +$xhp = <:Facebook:XHP:HTML:p>Hello, world</:Facebook:XHP:HTML:p>; +``` + +In all other contexts, `\` must be used, for example: + +``` +if ($obj is HTML\p) { ... } +h\p::staticMethod(); +$class_name = Facebook\XHP\HTML\p::class; +final xhp class my_element extends \Facebook\XHP\Core\element { ... } +``` + +**Historical note:** Before XHP namespace support (in XHP-Lib v3), `:` is +allowed as part of an XHP class name, but it is *not* a namespace separator. It +is simply translated to `__` at runtime (this is called "name mangling"). For +example, `<ui:table>` would instantiate a global class named `xhp_ui__table`. In +all other contexts, XHP classes must be referenced with the `:` prefix (e.g. +`if ($obj is :ui:table) { ... }`). ## Dynamic Content @@ -51,9 +101,11 @@ class defines what attributes are available to objects of that class: echo <input type="button" name="submit" value="OK" />; ``` -Here the `:input` class has the attributes `type`, `name` and `value` as part of its class properties. +Here the `input` class has the attributes `type`, `name` and `value` as part of its class properties. -Some attributes are required, and XHP will throw an error if you use an XHP object with a required attribute but without the attribute. +Some attributes are required, and XHP will throw an error if you use an XHP object with a required attribute but without the attribute. Depending on the exact HHVM +version and configuration, this may or may not be a typechecker error, but is +always a runtime error. ## HTML Character References diff --git a/guides/hack/21-XHP/10-interfaces.md b/guides/hack/21-XHP/10-interfaces.md index 14d24aad2..7552e30f6 100644 --- a/guides/hack/21-XHP/10-interfaces.md +++ b/guides/hack/21-XHP/10-interfaces.md @@ -1,22 +1,40 @@ -There are two important interfaces in XHP, `XHPRoot` and `XHPChild`, that you will need to use these when adding type annotations to functions. +There are two important XHP types, the `\XHPChild` interface (HHVM built-in) and +the `\Facebook\XHP\Core\node` base class (declared in XHP-Lib). You will most +commonly encounter these in functions' return type annotations. -## XHPRoot - -The `XHPRoot` interface is a implemented by all XHP objects; in practice, this means: - - `:x:element` subclasses -- these are classes that re-use (and combine) existing XHP classes - - `:x:primitive` subclasses -- these define basic elements, such as `:x:frag`, and all of the basic HTML elements - - implementations of the [`XHPUnsafeRenderable`](#xhpunsaferenderable) interface described below - -## XHPChild +## `\XHPChild` XHP presents a tree structure, and this interface defines what can be valid child nodes of the tree; it includes: - - All implementations of `XHPRoot` - - strings, integers, floats - - arrays of any of the above + +- all subclasses of `\Facebook\XHP\Core\node` and the advanced interfaces + described below +- strings, integers, floats +- arrays of any of the above Despite strings, integers, floats, and arrays not being objects, both the typechecker and HHVM consider them to implement this interface, for parameter/return types and for `is` checks. +## `\Facebook\XHP\Core\node` (`x\node`) + +The `\Facebook\XHP\Core\node` base class is implemented by all XHP objects, via +one of its two subclasses: + +- `\Facebook\XHP\Core\element` (`x\element`): most common; subclasses implement a + `renderAsync()` method that returns another `node`, and XHP-Lib automatically + takes care of recursively rendering nested XHP objects +- `\Facebook\XHP\Core\primitive` (`x\primitive`): for very low-level nodes that + need exact control of how the object is rendered to a string, or when the + automatic handling of nested XHP objects is insufficient; subclasses implement + a `stringifyAsync()` method that returns a `string` and must manually deal with + any children + +**Historical note:** Before XHP namespace support (in XHP-Lib v3), the names of +`node`, `element` and `primitive` are `\XHPRoot`, `:x:element` and +`:x:primitive` respectively. + +The `\Facebook\XHP\Core` namespace is conventionally aliased as `x` (`use Facebook\XHP\Core as x;`), so you might encounter these classes as `x\node`, +`x\element` and `x\primitive`, which also mirrors their historical names. + ## Advanced Interfaces While XHP's safe-by-default features are usually beneficial, occasionally they need to be bypassed; the most common cases are: @@ -27,11 +45,14 @@ XHP usually gets in the way of this by: - Escaping all variables, including your HTML code. - Enforcing child relationships - and XHP objects can not be marked as allowing HTML string children. -The `XHPUnsafeRenderable` and `XHPAlwaysValidChild` interfaces allow bypassing these safety mechanisms. +The `\Facebook\XHP\UnsafeRenderable` and `\Facebook\XHP\XHPAlwaysValidChild` interfaces allow bypassing these safety mechanisms. + +**Historical note:** Before XHP namespace support (in XHP-Lib v3), the names of +these interfaces are `\XHPUnsafeRenderable` and `\XHPAlwaysValidChild`. -## XHPUnsafeRenderable +### `\Facebook\XHP\UnsafeRenderable` -If you need to render raw HTML strings, wrap them in a class that implements this interface and provides a `toHTMLString: string` method: +If you need to render raw HTML strings, wrap them in a class that implements this interface and provides a `toHTMLStringAsync()` method: @@ interfaces-examples/xss-security-hole.inc.php @@ @@ interfaces-examples/xss-security-hole.php @@ @@ -42,13 +63,13 @@ implementations: @@ interfaces-examples/markdown-wrapper.inc.php @@ @@ interfaces-examples/markdown-wrapper.php @@ -## XHPAlwaysValidChild +### `\Facebook\XHP\AlwaysValidChild` XHP's child validation can be bypassed by implementing this interface. Most classes that implement this interface are also implementations of -`XHPUnsafeRenderable`, as the most common need is when a child is produced by another rendering or template system. +`UnsafeRenderable`, as the most common need is when a child is produced by another rendering or template system. -This can also be implemented by XHP objects, but this usually indicates that a child class specification should be replaced with a category. This -interface is intentionally breaking part of XHP's safety, so should be used as sparingly as possible. +This can also be implemented by XHP objects, but this usually indicates that some class in `getChildrenDeclaration()` should be replaced with a more generic interface. +`AlwaysValidChild` is intentionally breaking part of XHP's safety, so should be used as sparingly as possible. ## Example diff --git a/guides/hack/21-XHP/13-methods-examples/list-builder.php b/guides/hack/21-XHP/13-methods-examples/list-builder.php index 5b4cb63a7..1d39e028a 100644 --- a/guides/hack/21-XHP/13-methods-examples/list-builder.php +++ b/guides/hack/21-XHP/13-methods-examples/list-builder.php @@ -1,9 +1,11 @@ <?hh // partial +namespace Hack\UserDocumentation\XHP\Methods; + use namespace Facebook\XHP\Core as x; -use type Facebook\XHP\HTML\{li, ul}; +use type Facebook\XHP\HTML\{li, p, ul}; -function xhp_object_methods_build_list(Vector<string> $names): x\node { +function build_list(vec<string> $names): x\node { $list = <ul id="names" />; foreach ($names as $name) { $list->appendChild(<li>{$name}</li>); @@ -14,27 +16,22 @@ function xhp_object_methods_build_list(Vector<string> $names): x\node { <<__EntryPoint>> async function xhp_object_methods_run(): Awaitable<void> { \init_docs_autoloader(); - $names = Vector {'Sara', 'Fred', 'Josh', 'Scott', 'Paul', 'David', 'Matthew'}; + $names = vec['Sara', 'Fred', 'Josh', 'Scott', 'Paul', 'David', 'Matthew']; - $list = xhp_object_methods_build_list($names); - foreach ($list->getChildren() as $child) { - $xhp = <ul>{$child}</ul>; - echo await $xhp->toStringAsync()."\n"; + foreach (build_list($names)->getChildren() as $child) { + $child as x\node; + echo 'Child: '.await $child->toStringAsync()."\n"; } - $list = xhp_object_methods_build_list($names); - $xhp = <ul>{$list->getFirstChild()}</ul>; - echo await $xhp->toStringAsync()."\n"; + echo 'First child: '. + await (build_list($names)->getFirstChild() as x\node->toStringAsync())."\n"; - $list = xhp_object_methods_build_list($names); - $xhp = <ul>{$list->getLastChild()}</ul>; - echo await $xhp->toStringAsync()."\n"; + echo 'Last child: '. + await (build_list($names)->getLastChild() as x\node->toStringAsync())."\n"; - foreach ($list->getAttributes() as $attr) { - $xhp = <ul><li>{(string)$attr}</li></ul>; - echo await $xhp->toStringAsync()."\n"; + foreach (build_list($names)->getAttributes() as $name => $value) { + echo 'Attribute '.$name.' = '.$value as string."\n"; } - $xhp = <ul><li>{$list->getAttribute('id') as string}</li></ul>; - echo await $xhp->toStringAsync()."\n"; + echo 'ID: '.build_list($names)->getAttribute('id') as string."\n"; } diff --git a/guides/hack/21-XHP/13-methods-examples/list-builder.php.hhvm.expect b/guides/hack/21-XHP/13-methods-examples/list-builder.php.hhvm.expect index 517b6c119..a4454564e 100644 --- a/guides/hack/21-XHP/13-methods-examples/list-builder.php.hhvm.expect +++ b/guides/hack/21-XHP/13-methods-examples/list-builder.php.hhvm.expect @@ -1,11 +1,11 @@ -<ul><li>Sara</li></ul> -<ul><li>Fred</li></ul> -<ul><li>Josh</li></ul> -<ul><li>Scott</li></ul> -<ul><li>Paul</li></ul> -<ul><li>David</li></ul> -<ul><li>Matthew</li></ul> -<ul><li>Sara</li></ul> -<ul><li>Matthew</li></ul> -<ul><li>names</li></ul> -<ul><li>names</li></ul> +Child: <li>Sara</li> +Child: <li>Fred</li> +Child: <li>Josh</li> +Child: <li>Scott</li> +Child: <li>Paul</li> +Child: <li>David</li> +Child: <li>Matthew</li> +First child: <li>Sara</li> +Last child: <li>Matthew</li> +Attribute id = names +ID: names diff --git a/guides/hack/21-XHP/13-methods.md b/guides/hack/21-XHP/13-methods.md index 66d1e05f4..b20ace904 100644 --- a/guides/hack/21-XHP/13-methods.md +++ b/guides/hack/21-XHP/13-methods.md @@ -1,21 +1,24 @@ -Remember, all XHP Objects derive from the [`XHPRoot`](/hack/XHP/interfaces) interface and an object that implements `XHPRoot` -has some public methods that can be called. - -## XHP Object Methods +Remember, all XHP Objects derive from the [`\Facebook\XHP\Core\node`](/hack/XHP/interfaces) base class, which has some public methods that can be called. Method | Description --------|------------ +`toStringAsync(): Awaitable<string>` | Renders the element to a string for output. Mutating methods like `setAttribute` can no longer be called after this. `appendChild(mixed $child): this` | Adds `$child` to the end of the XHP object's array of children. If `$child` is an array, each item in the array will be appended. -`categoryOf(string $cat): bool` | Returns whether the XHP object belongs to the category named `$cat` -`getAttribute(string $name): mixed` | Returns the value of the XHP object's attribute named `$name`. If the attribute is not set, `null` is returned, unless the attribute is required, in which case `XHPAttributeRequiredException` is thrown. If the attribute is not declared or does not exist, then `XHPAttributeNotSupportedException` is thrown. If the attribute you are reading is statically known, use `$this->:name` style syntax instead for better typechecker coverage. -`getAttributes(): Map<string, mixed>` | Returns the XHP object's array of attributes, as a cloned copy. -`getChildren(?string $selector = null): Vector<XHPChild>` | Returns the XHP object's children. If `$selector` is `null`, all children are returned. If `$selector` starts with `%`, all children belonging to the category named by `$selector` are returned. Otherwise, all children that are an instance of the class named by `$selector` are returned. -`getFirstChild(?string $selector = null):): ?XHPChild` | Returns the XHP object's first child. If `$selector` is `null`, the true first child is returned. Otherwise, the first child that matches `$selector` (or `null`) is returned. -`getLastChild(?string $selector = null):): ?XHPChild` | Returns the XHP object's last child. If `$selector` is `null`, the true last child is returned. Otherwise, the last child that matches `$selector` (or `null`) is returned. +`getAttribute(string $name): mixed` | Returns the value of the XHP object's attribute named `$name`. If the attribute is not set, `null` is returned, unless the attribute is required, in which case `AttributeRequiredException` is thrown. If the attribute is not declared or does not exist, then `AttributeNotSupportedException` is thrown. If the attribute you are reading is statically known, use `$this->:name` style syntax instead for better typechecker coverage. +`getAttributes(): dict<string, mixed>` | Returns the XHP object's array of attributes. +`getChildren(): vec<XHPChild>` | Returns the XHP object's children. +`getChildrenOfType<T as XHPChild>(): vec<T>` | Returns the XHP object's children that are instances of the specified class/interface. +`getFirstChild(): ?XHPChild` | Returns the XHP object's first child or `null` if it has no children. +`getFirstChildx(): XHPChild` | Same but throws if the XHP object has no children. +`getFirstChildOfType<T as XHPChild>(): ?T` | Returns the first of XHP object's children that is an instance of the specified class/interface, or `null` if it has no such children. +`getFirstChildOfTypex<T as XHPChild>(): T` | Same but throws if the XHP object has no children of the specified type. +`getLastChild(): ?XHPChild` | Analogous to `getFirstChild`. +`getLastChildx(): XHPChild` | Analogous to `getFirstChildx`. +`getLastChildOfType<T as XHPChild>(): ?T` | Analogous to `getFirstChildOfType`. +`getLastChildOfType<T as XHPChild>(): T` | Analogous to `getFirstChildOfTypex`. `isAttributeSet(string $name): bool` | Returns whether the attribute with name `$name` is set. -`prependChild(mixed $child): this` | Adds $child to the beginning of the XHP object's array of children. If $child is an `array`, each item in the array will be appended. -`replaceChildren(...): this` | Replaces all the children of this XHP Object with the variable number of children passed to this method. -`setAttribute(string $name, mixed $val): this` | Sets the value of the XHP object's attribute named `$name`. The value will be checked against the attribute's type, and if they don't match, `XHPInvalidAttributeException` is thrown. If the attribute is not declared or does not exist, then `XHPAttributeNotSupportedException` is thrown. -`setAttributes(KeyedTraversable<string, mixed> $attrs): this` | Replaces the XHP object's array of attributes with `$attrs`. Same errors can apply as `setAttribute()`. +`replaceChildren(XHPChild ...$children): this` | Replaces all the children of this XHP Object with the variable number of children passed to this method. +`setAttribute(string $name, mixed $val): this` | Sets the value of the XHP object's attribute named `$name`. This does no validation, attributes are only validated when retrieved using `getAttribute` or during rendering. If the attribute you are setting is statically known, use `$this->:name` style syntax instead for better typechecker coverage. +`setAttributes(KeyedTraversable<string, mixed> $attrs): this` | Replaces the XHP object's array of attributes with `$attrs`. @@ methods-examples/list-builder.php @@ diff --git a/guides/hack/21-XHP/16-extending-examples/basic.inc.php b/guides/hack/21-XHP/16-extending-examples/basic.inc.php index bd11971aa..18febc53b 100644 --- a/guides/hack/21-XHP/16-extending-examples/basic.inc.php +++ b/guides/hack/21-XHP/16-extending-examples/basic.inc.php @@ -9,10 +9,8 @@ } } -final xhp class intro_plain_str extends x\element { - protected async function renderAsync(): Awaitable<x\node> { - // Since this function returns an XHPRoot, if you want to return a primitive - // like a string, wrap it around <x:frag> - return <x:frag>Hello!</x:frag>; +final xhp class intro_plain_str extends x\primitive { + protected async function stringifyAsync(): Awaitable<string> { + return 'Hello!'; } } diff --git a/guides/hack/21-XHP/16-extending-examples/children.inc.php b/guides/hack/21-XHP/16-extending-examples/children.inc.php index 0c10d314c..eb718ccb8 100644 --- a/guides/hack/21-XHP/16-extending-examples/children.inc.php +++ b/guides/hack/21-XHP/16-extending-examples/children.inc.php @@ -1,17 +1,19 @@ <?hh // partial +// Conventionally aliased to XHPChild, which makes the children declarations +// easier to read (more fluid). use namespace Facebook\XHP\{ChildValidation as XHPChild, Core as x}; use type Facebook\XHP\HTML\{body, head, html, li, ul}; -xhp class my_br extends x\element { +xhp class my_br extends x\primitive { use XHPChild\Validation; protected static function getChildrenDeclaration(): XHPChild\Constraint { return XHPChild\empty(); } - protected async function renderAsync(): Awaitable<x\node> { - return <x:frag>PHP_EOL</x:frag>; + protected async function stringifyAsync(): Awaitable<string> { + return "\n"; } } diff --git a/guides/hack/21-XHP/16-extending.md b/guides/hack/21-XHP/16-extending.md index 55886fdad..beb503bc3 100644 --- a/guides/hack/21-XHP/16-extending.md +++ b/guides/hack/21-XHP/16-extending.md @@ -3,16 +3,29 @@ items that are not in the standard HTML specification. ## Basics -All XHP class names start with a colon `:` and may include other `:` as well, as long as they are not adjacent. Note that this is an -exception to the Hack rule where you cannot have `:` in class names. - -A custom XHP class needs to do two things: -* extend `:x:element`. -* implement the method `render` to return an XHP Object via `XHPRoot`. +XHP class names must follow the same rules as any other Hack class names: +Letters, numbers and `_` are allowed and the name mustn't start with a number. + +**Historical note:** Before XHP namespace support (in XHP-Lib v3), XHP class +names could also contain `:` (now a namespace separator) and `-` (now disallowed +completely). These were translated to `__` and `_` respectively at runtime (this +is called "name mangling"). For example, `<ui:big-table>` would instantiate a +global class named `xhp_ui__big_table`. + +A custom XHP class needs to do three things: +* use the keywords `xhp class` instead of `class` +* extend `x\element` (`\Facebook\XHP\Core\element`) or, rarely, another + [base class](/hack/XHP/interfaces) +* implement the method `renderAsync` to return an XHP object (`x\node`) or the + respective method of the chosen base class @@ extending-examples/basic.inc.php @@ @@ extending-examples/basic.php @@ +**Historical note:** Before XHP namespace support (in XHP-Lib v3), use +`class :intro_plain_str` instead of `xhp class intro_plain_str` (no `xhp` +keyword, but requires a `:` prefix in the class name). + ## Attributes ### Syntax @@ -68,78 +81,67 @@ attribute string butIAmNullable; ``` -### Inheritance - -The easiest way to have attributes in your custom XHP class inherit attributes from an existing XHP object is to use the [`XHPHelpers` trait](#xhp-helpers). - ## Children -You should declare the types that your custom class is allowed to have as children. You use the `children` declaration: - -``` -children (:class1, :class2); -children empty; // no children allowed -``` +You can declare the types that your custom class is allowed to have as children +by using the `Facebook\XHP\ChildValidation\Validation` trait and implementing the +`getChildrenDeclaration()` method. -If you don't explicitly declare using the `children` attribute, then your class can have any child. If you try to add a child to an object -that doesn't allow one or doesn't exist in its declaration list, then an `XHPInvalidChildrenException` will be thrown. +**Historical note:** Before XHP namespace support (in XHP-Lib v3), a special +`children` keyword with a regex-like syntax could be used instead +([examples](https://github.com/hhvm/xhp-lib/blob/v3.x/tests/ChildRuleTest.php)). +However, XHP-Lib v3 also supports `Facebook\XHP\ChildValidation\Validation`, and +it is therefore recommended to use it everywhere. -You can use the standard regex operators `*` (zero or more), `+` (one or more) `|` (this or that) when declaring your children. +If you don't use the child validation trait, then your class can have any +children. If you try to add a child to an object +that doesn't allow one or it doesn't exist in its declaration list, then an `InvalidChildrenException` will be thrown during rendering. @@ extending-examples/children.inc.php @@ @@ extending-examples/children.php @@ -### Categories - -Categories in XHP are like interfaces in object-oriented languages. You can mark your custom class with any number of categories which then -can be referred to from your children. You use the `category` declaration. Each category is prefixed with a `%`. +### Interfaces (categories) -``` -category %name1, %name2,...., %$nameN; -``` +XHP classes are encouraged to implement one or more interfaces (usually empty), +conventionally called "categories". Some common ones taken from the HTML +specification are declared in the `Facebook\XHP\HTML\Category` namespace. -The categories are taken from the HTML specification (e.g., `%flow`, `%phrase`). +Using such interfaces makes it possible to implement `getChildrenDeclaration()` +in other elements without having to manually list all possible child types, some +of which may not even exist yet. @@ extending-examples/categories.inc.php @@ @@ extending-examples/categories.php @@ +**Historical note:** Before XHP namespace support (in XHP-Lib v3), a special +`category` keyword could be used instead of an interface +(`category %name1, %name2;`). + ## Async -XHP and [async](../asynchronous-operations/introduction.md) co-exist well together. An async XHP class must do two additional things: -* use the `XHPAsync` trait -* implement `asyncRender: Awaitable<XHPRoot>` instead of `render: XHPRoot` +XHP and [async](../asynchronous-operations/introduction.md) co-exist well together. +As you may have noticed, all rendering methods (`renderAsync`, `stringifyAsync`) +are declared to return an `Awaitable` and can therefore be implemented as async +functions and use `await`. @@ extending-examples/xhp-async.inc.php @@ @@ extending-examples/xhp-async.php @@ -## XHP Helpers +**Historical note:** In XHP-Lib v3, most rendering methods are not async, and +therefore a special `\XHPAsync` trait must be used in XHP classes that need to +`await` something during rendering. + +## HTML Helpers -The `XHPHelpers` trait implements three behaviors: -* Transferring attributes from one object to the object returned from its `render` method. +The `Facebook\XHP\HTML\XHPHTMLHelpers` trait implements two behaviors: * Giving each object a unique `id` attribute. * Managing the `class` attribute. -### Attribute Transfer - -Let's say you have a class that wants to inherit attributes from `:div`. You could do something like this: - -@@ extending-examples/bad-attribute-transfer.inc.php @@ -@@ extending-examples/bad-attribute-transfer.php @@ - -The issue above is that any attribute set on `:ui:my-custom` will be lost because the `<div>` returned from `render` will not automatically -get those attributes. - -Instead, you should use `XHPHelpers`. - -@@ extending-examples/attribute-transfer.inc.php @@ -@@ extending-examples/attribute-transfer.php @@ - -Now, when `:ui:my-custom` is rendered, each `:div` attribute will be transferred over, assuming that it was declared in the `render` -function. Also, an `:ui:my-custom` attribute value that is set will override the same `:div` attribute set in the `render` function. +**Historical note:** In XHP-Lib v3, this trait is called `\XHPHelpers`. ### Unique IDs -`XHPHelpers` has a method `getID` that you can call to give your rendered custom XHP object a unique ID that can be referred to in other +`XHPHTMLHelpers` has a method `getID` that you can call to give your rendered custom XHP object a unique ID that can be referred to in other parts of your code or UI framework (e.g., CSS). @@ extending-examples/get-id.inc.php @@ @@ -147,8 +149,39 @@ parts of your code or UI framework (e.g., CSS). ### Class Attribute Management -`XHPHelpers` has two methods to add a class name to the `class` attribute of an XHP object. This all assumes that your custom class -declares the `class` attribute directly or through inheritance. `addClass` takes a `string` and appends that `string` to the `class` -attribute; `conditionClass` takes a `string` and a `bool` appends that `string` to the `class` attribute if the `bool` is `true`. +`XHPHTMLHelpers` has two methods to add a class name to the `class` attribute of +an XHP object. `addClass` takes a `string` and appends that `string` to the +`class` attribute; `conditionClass` takes a `bool` and a `string`, and only +appends that `string` to the `class` attribute if the `bool` is `true`. + +This is best illustrated with a standard HTML element, all of which have a +`class` attribute and use the `XHPHTMLHelpers` trait, but it works with any +XHP class, as long as it uses the trait and declares the `class` attribute +directly or through inheritance. @@ extending-examples/add-class.php @@ + +## Attribute Transfer (deprecated) + +*This feature is deprecated! We recommend declaring all attributes explicitly in +new XHP classes.* + +Let's say you have a class that wants to inherit attributes from `<div>`. You could do something like this: + +@@ extending-examples/bad-attribute-transfer.inc.php @@ +@@ extending-examples/bad-attribute-transfer.php @@ + +`:Facebook:XHP:HTML:div` causes your class to inherit all `<div>` attributes, +however, any attribute set on `<ui:my-custom>` will be lost because the `<div>` returned from `render` will not automatically +get those attributes. + +This can be addressed by using the `XHPAttributeClobbering_DEPRECATED` trait. + +@@ extending-examples/attribute-transfer.inc.php @@ +@@ extending-examples/attribute-transfer.php @@ + +Now, when `<ui:my-custom>` is rendered, each `<div>` attribute will be transferred over. +Also, any explicitly set `<ui:my-custom>` attribute value will override the same `<div>` attribute set in the `render` function. + +**Historical note:** In XHP-Lib v3, this is provided by the `\XHPHelpers` trait +instead of a separate one. diff --git a/guides/hack/21-XHP/19-migration.md b/guides/hack/21-XHP/19-migration.md index 6c40ba43f..48be54b0e 100644 --- a/guides/hack/21-XHP/19-migration.md +++ b/guides/hack/21-XHP/19-migration.md @@ -1,6 +1,7 @@ You can incrementally port code to use XHP. -The basis for all these possibilities is this function: +Assume your output is currently handled by the following function, which might +be called from many places. ```Hack function render_component($text, $uri) { @@ -10,16 +11,15 @@ function render_component($text, $uri) { } ``` -This could be called from many places. - ## Convert Leaf Functions -You can simply use XHP in `render_component`: +You can start by simply using XHP in `render_component`: ```Hack -function render_component($text, $uri) { +async function render_component($text, $uri) { $link = <a href={$uri}>{$text}</a>; - return $link->toString(); + return await $link->toStringAsync(); + // or HH\Asio\join if converting all callers to async is hard } ``` @@ -31,11 +31,12 @@ strings around in the end. You could make `render_component` into a class: ```Hack -// Assume class Uri -class :ui:component-link extends :x:element { - attribute Uri $uri @required; +namespace ui; + +class link extends x\element { + attribute Uri $uri @required; // Assume class Uri attribute string $text @required; - protected function render(): XHPRoot { + protected async function renderAsync(): Awaitable<x\node> { return <a href={$this->:uri}>{$this->:text}</a>; } @@ -45,7 +46,7 @@ class :ui:component-link extends :x:element { Keep a legacy `render_component` around while you are converting the old code that uses `render_component` to use the class. ```Hack -function render_component(string $text, Uri $uri): string { - return (<ui:component-link uri={$uri} text={$text} />)->toString(); +async function render_component(string $text, Uri $uri): Awaitable<string> { + return await (<ui:link uri={$uri} text={$text} />)->toStringAsync(); } ``` diff --git a/guides/hack/21-XHP/22-guidelines.md b/guides/hack/21-XHP/22-guidelines.md index 8d060cc0d..30684f330 100644 --- a/guides/hack/21-XHP/22-guidelines.md +++ b/guides/hack/21-XHP/22-guidelines.md @@ -4,15 +4,10 @@ Here are some general guidelines to know and follow when using XHP. In addition ## Validation of Attributes and Children The constraints of XHP object children and attributes are done at various times: -* Children constraints are validated at render-time (when `toString` is called explicitly or implicitly). -* Attribute names and types are validated when the attributes are set in a tag or via `setAttribute`. -* `@required` is validated when the required attributes are read. - -This validation is on by default. You can turn it off by running the following code before using XHP: - -```Hack -:xhp::$ENABLE_VALIDATION=false -``` +* Children constraints are validated at render-time (when `toStringAsync` is called). +* Attributes provided directly (`<your_class attr="value">`) are validated by + the typechecker. +* Attribute names set by `setAttribute` are only validated at render-time. ## Use Contexts to Access Higher Level Information @@ -37,8 +32,9 @@ we haven't yet come across any situations where public methods are a better solu ## Use Inheritance Minimally -If you need an XHP object to act like another, but slightly modified, use composition. Categories and attribute cloning can -be used to provide a common interface. +If you need an XHP object to act like another, but slightly modified, use +composition (the would-be subclass can instead return an instance of the +original XHP class from its `renderAsync` method). ## Remember No Dollar Signs