WP_Interactivity_API::data_wp_bind_processorprivateWP 6.5.0

Processes the data-wp-bind directive.

It updates or removes the bound attributes based on the evaluation of its associated reference.

Method of the class: WP_Interactivity_API{}

No Hooks.

Returns

null. Nothing (null).

Usage

// private - for code of main (parent) class only
$result = $this->data_wp_bind_processor( $p, $mode ): void;
$p(WP_Interactivity_API_Directives_Processor) (required)
The directives processor instance.
$mode(string) (required)
Whether the processing is entering or exiting the tag.

Changelog

Since 6.5.0 Introduced.
Since 7.1.0 An object is resolved to whatever it serializes to for the client, a number is formatted by the JSON encoder, and a value which cannot be sent to the client is rejected rather than passed to WP_HTML_Tag_Processor::set_attribute().

WP_Interactivity_API::data_wp_bind_processor() code WP 7.1

private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ): void {
	if ( 'enter' === $mode ) {
		$entries = $this->get_directive_entries( $p, 'bind' );
		foreach ( $entries as $entry ) {
			if ( empty( $entry['suffix'] ) || null !== $entry['unique_id'] ) {
				continue;
			}

			// Skip if the suffix is an event handler.
			if ( str_starts_with( $entry['suffix'], 'on' ) ) {
				_doing_it_wrong(
					__METHOD__,
					sprintf(
						/* translators: %s: The directive, e.g. data-wp-on--click. */
						__( 'Binding event handler attributes is not supported. Please use "%s" instead.' ),
						esc_attr( 'data-wp-on--' . substr( $entry['suffix'], 2 ) )
					),
					'6.9.2'
				);
				continue;
			}

			$result = $this->evaluate( $entry );

			/*
			 * An object is resolved to whatever it serializes to. When the reference points to a value stored
			 * in state or context, that is the value the client receives for it when the store is hydrated.
			 * A derived state closure is never serialized, so there the client value comes from the derived
			 * state's client-side implementation instead; the resolution is still applied so that both origins
			 * behave the same. Round-tripping through the JSON encoder rather than calling
			 * JsonSerializable::jsonSerialize() directly keeps this resolution identical to the client's,
			 * including for an object which serializes to another serializable object. When the encoding fails
			 * the object is left in place, to be reported as a usage error below. Note that it rarely does
			 * fail: wp_json_encode() retries through _wp_json_sanity_check(), which rebuilds the object from
			 * its public properties and so ignores jsonSerialize() altogether. An object whose serialized form
			 * JSON cannot represent therefore resolves to whatever that rebuild encodes to, which is what the
			 * client is sent for it as well.
			 *
			 * A throwing JsonSerializable::jsonSerialize() is caught for the same reason the value is checked
			 * at all: a binding must not be able to abort the render. An exception escaping here would leave
			 * `$context_stack` and `$namespace_stack` unrestored for every later `process_directives()` call
			 * on this instance, so the object is treated as one which failed to encode.
			 */
			if ( is_object( $result ) ) {
				try {
					$encoded = wp_json_encode( $result );
				} catch ( Throwable $e ) {
					$encoded = false;
				}
				if ( false !== $encoded ) {
					$result = json_decode( $encoded );
				}
			}

			/*
			 * Only a value which can be sent to the client may be stored in an attribute value. Strings and
			 * booleans are passed in as-is, numbers are formatted, and everything else is rejected as a usage
			 * error.
			 *
			 * An object which does not serialize to a scalar is rejected even when it defines `__toString()`,
			 * which PHP would otherwise coerce for the string parameters of the escaping functions. Its string
			 * representation is not what the client evaluates this reference to, whether that is the form
			 * serialized into the store or the return value of a derived state's client-side implementation,
			 * so the two could disagree once the directive is evaluated during hydration.
			 */
			if ( null !== $result ) {
				if ( ! is_scalar( $result ) ) {
					_doing_it_wrong(
						__METHOD__,
						sprintf(
							/* translators: %s: The attribute name. */
							__( 'Attempted to bind a non-scalar value to the "%s" attribute. Ensure the state/context property or the derived state closure resolves to a string, number, or boolean.' ),
							esc_html( $entry['suffix'] )
						),
						'7.1.0'
					);
					$result = null;
				} elseif ( is_int( $result ) || is_float( $result ) ) {
					/*
					 * A number is formatted by the JSON encoder rather than cast to string, so that the
					 * attribute value matches the number the client receives for this same reference. Casting
					 * a float is locale-dependent before PHP 8.0, and rounds to `precision` rather than to the
					 * encoder's `serialize_precision`.
					 *
					 * This closes the cases which differ in practice, not every one. A float written in
					 * exponent notation still disagrees, since PHP encodes 1e25 as `1.0e+25` where JavaScript
					 * renders it as `1e+25`, as does negative zero, and an integer above the range JavaScript
					 * can represent exactly is rounded once it reaches the client. Casting diverged on all
					 * three as well, so none is a regression.
					 */
					$encoded = wp_json_encode( $result );
					if ( JSON_ERROR_INF_OR_NAN === json_last_error() ) {
						/*
						 * The encoder only rejects INF and NAN, of which JSON can represent neither. When such
						 * a value is stored in state, the store itself also fails to encode in its entirety,
						 * and the client is sent an empty script tag in place of all of its state; only
						 * removing the value from the state resolves that. A derived state closure returning
						 * one never reaches the store, so there only the binding itself is affected.
						 */
						_doing_it_wrong(
							__METHOD__,
							sprintf(
								/* translators: %s: The attribute name. */
								__( 'Attempted to bind a non-finite number to the "%s" attribute. Ensure the state/context property or the derived state closure resolves to a finite number or a string.' ),
								esc_html( $entry['suffix'] )
							),
							'7.1.0'
						);
						$result = null;
					} else {
						$result = $encoded;
					}
				}
			}

			if (
				null !== $result &&
				(
					false !== $result ||
					( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
				)
			) {
				/*
				 * If the result of the evaluation is a boolean and the attribute is
				 * `aria-` or `data-, convert it to a string "true" or "false". It
				 * follows the exact same logic as Preact because it needs to
				 * replicate what Preact will later do in the client:
				 * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
				 */
				if (
					is_bool( $result ) &&
					( strlen( $entry['suffix'] ) > 5 && '-' === $entry['suffix'][4] )
				) {
					$result = $result ? 'true' : 'false';
				}
				$p->set_attribute( $entry['suffix'], $result );
			} else {
				$p->remove_attribute( $entry['suffix'] );
			}
		}
	}
}