AbstractSchema.php
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
/**
* AbstractSchema class.
*
* For REST Route Schemas
*/
abstract class AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'Schema';
/**
* Rest extend instance.
*
* @var ExtendSchema
*/
protected $extend;
/**
* Schema Controller instance.
*
* @var SchemaController
*/
protected $controller;
/**
* Extending key that gets added to endpoint.
*
* @var string
*/
const EXTENDING_KEY = 'extensions';
/**
* Constructor.
*
* @param ExtendSchema $extend Rest Extending instance.
* @param SchemaController $controller Schema Controller instance.
*/
public function __construct( ExtendSchema $extend, SchemaController $controller ) {
$this->extend = $extend;
$this->controller = $controller;
}
/**
* Returns the full item schema.
*
* @return array
*/
public function get_item_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->title,
'type' => 'object',
'properties' => $this->get_properties(),
);
}
/**
* Returns the full item response.
*
* @param mixed $item Item to get response for.
* @return array|stdClass
*/
public function get_item_response( $item ) {
return [];
}
/**
* Return schema properties.
*
* @return array
*/
abstract public function get_properties();
/**
* Recursive removal of arg_options.
*
* @param array $properties Schema properties.
*/
protected function remove_arg_options( $properties ) {
return array_map(
function( $property ) {
if ( isset( $property['properties'] ) ) {
$property['properties'] = $this->remove_arg_options( $property['properties'] );
} elseif ( isset( $property['items']['properties'] ) ) {
$property['items']['properties'] = $this->remove_arg_options( $property['items']['properties'] );
}
unset( $property['arg_options'] );
return $property;
},
(array) $properties
);
}
/**
* Returns the public schema.
*
* @return array
*/
public function get_public_item_schema() {
$schema = $this->get_item_schema();
if ( isset( $schema['properties'] ) ) {
$schema['properties'] = $this->remove_arg_options( $schema['properties'] );
}
return $schema;
}
/**
* Returns extended data for a specific endpoint.
*
* @param string $endpoint The endpoint identifier.
* @param array ...$passed_args An array of arguments to be passed to callbacks.
* @return object the data that will get added.
*/
protected function get_extended_data( $endpoint, ...$passed_args ) {
return $this->extend->get_endpoint_data( $endpoint, $passed_args );
}
/**
* Gets an array of schema defaults recursively.
*
* @param array $properties Schema property data.
* @return array Array of defaults, pulled from arg_options
*/
protected function get_recursive_schema_property_defaults( $properties ) {
$defaults = [];
foreach ( $properties as $property_key => $property_value ) {
if ( isset( $property_value['arg_options']['default'] ) ) {
$defaults[ $property_key ] = $property_value['arg_options']['default'];
} elseif ( isset( $property_value['properties'] ) ) {
$defaults[ $property_key ] = $this->get_recursive_schema_property_defaults( $property_value['properties'] );
}
}
return $defaults;
}
/**
* Gets a function that validates recursively.
*
* @param array $properties Schema property data.
* @return function Anonymous validation callback.
*/
protected function get_recursive_validate_callback( $properties ) {
/**
* Validate a request argument based on details registered to the route.
*
* @param mixed $values
* @param \WP_REST_Request $request
* @param string $param
* @return true|\WP_Error
*/
return function ( $values, $request, $param ) use ( $properties ) {
foreach ( $properties as $property_key => $property_value ) {
$current_value = isset( $values[ $property_key ] ) ? $values[ $property_key ] : null;
$property_type = is_array( $property_value['type'] ) ? $property_value['type'] : [ $property_value['type'] ];
if ( empty( $current_value ) && in_array( 'null', $property_type, true ) ) {
// If the value is null and the schema allows null, we can skip validation for children.
continue;
}
if ( isset( $property_value['arg_options']['validate_callback'] ) ) {
$callback = $property_value['arg_options']['validate_callback'];
$result = is_callable( $callback ) ? $callback( $current_value, $request, $param ) : false;
} else {
$result = rest_validate_value_from_schema( $current_value, $property_value, $param . ' > ' . $property_key );
}
if ( ! $result || is_wp_error( $result ) ) {
// If schema validation fails, we return here as we don't need to validate any deeper.
return $result;
}
if ( isset( $property_value['properties'] ) ) {
$validate_callback = $this->get_recursive_validate_callback( $property_value['properties'] );
$result = $validate_callback( $current_value, $request, $param . ' > ' . $property_key );
if ( ! $result || is_wp_error( $result ) ) {
// If schema validation fails, we return here as we don't need to validate any deeper.
return $result;
}
}
}
return true;
};
}
/**
* Gets a function that sanitizes recursively.
*
* @param array $properties Schema property data.
* @return function Anonymous validation callback.
*/
protected function get_recursive_sanitize_callback( $properties ) {
/**
* Validate a request argument based on details registered to the route.
*
* @param mixed $values
* @param \WP_REST_Request $request
* @param string $param
* @return true|\WP_Error
*/
return function ( $values, $request, $param ) use ( $properties ) {
$sanitized_values = [];
foreach ( $properties as $property_key => $property_value ) {
$current_value = isset( $values[ $property_key ] ) ? $values[ $property_key ] : null;
if ( isset( $property_value['arg_options']['sanitize_callback'] ) ) {
$callback = $property_value['arg_options']['sanitize_callback'];
$current_value = is_callable( $callback ) ? $callback( $current_value, $request, $param ) : $current_value;
} else {
$current_value = rest_sanitize_value_from_schema( $current_value, $property_value, $param . ' > ' . $property_key );
}
// If sanitization failed, return the WP_Error object straight away.
if ( is_wp_error( $current_value ) ) {
return $current_value;
}
if ( isset( $property_value['properties'] ) ) {
$sanitize_callback = $this->get_recursive_sanitize_callback( $property_value['properties'] );
$sanitized_values[ $property_key ] = $sanitize_callback( $current_value, $request, $param . ' > ' . $property_key );
} else {
$sanitized_values[ $property_key ] = $current_value;
}
}
return $sanitized_values;
};
}
/**
* Returns extended schema for a specific endpoint.
*
* @param string $endpoint The endpoint identifer.
* @param array ...$passed_args An array of arguments to be passed to callbacks.
* @return array the data that will get added.
*/
protected function get_extended_schema( $endpoint, ...$passed_args ) {
$extended_schema = $this->extend->get_endpoint_schema( $endpoint, $passed_args );
$defaults = $this->get_recursive_schema_property_defaults( $extended_schema );
return [
'type' => 'object',
'context' => [ 'view', 'edit' ],
'arg_options' => [
'default' => $defaults,
'validate_callback' => $this->get_recursive_validate_callback( $extended_schema ),
'sanitize_callback' => $this->get_recursive_sanitize_callback( $extended_schema ),
],
'properties' => $extended_schema,
];
}
/**
* Apply a schema get_item_response callback to an array of items and return the result.
*
* @param AbstractSchema $schema Schema class instance.
* @param array $items Array of items.
* @return array Array of values from the callback function.
*/
protected function get_item_responses_from_schema( AbstractSchema $schema, $items ) {
$items = array_filter( $items );
if ( empty( $items ) ) {
return [];
}
return array_values( array_map( [ $schema, 'get_item_response' ], $items ) );
}
/**
* Retrieves an array of endpoint arguments from the item schema for the controller.
*
* @uses rest_get_endpoint_args_for_schema()
* @param string $method Optional. HTTP method of the request.
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = \WP_REST_Server::CREATABLE ) {
$schema = $this->get_item_schema();
$endpoint_args = rest_get_endpoint_args_for_schema( $schema, $method );
$endpoint_args = $this->remove_arg_options( $endpoint_args );
return $endpoint_args;
}
/**
* Force all schema properties to be readonly.
*
* @param array $properties Schema.
* @return array Updated schema.
*/
protected function force_schema_readonly( $properties ) {
return array_map(
function( $property ) {
$property['readonly'] = true;
if ( isset( $property['items']['properties'] ) ) {
$property['items']['properties'] = $this->force_schema_readonly( $property['items']['properties'] );
}
return $property;
},
(array) $properties
);
}
/**
* Returns consistent currency schema used across endpoints for prices.
*
* @return array
*/
protected function get_store_currency_properties() {
return [
'currency_code' => [
'description' => __( 'Currency code (in ISO format) for returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'currency_symbol' => [
'description' => __( 'Currency symbol for the currency which can be used to format returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'currency_minor_unit' => [
'description' => __( 'Currency minor unit (number of digits after the decimal separator) for returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'currency_decimal_separator' => array(
'description' => __( 'Decimal separator for the currency which can be used to format returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'currency_thousand_separator' => array(
'description' => __( 'Thousand separator for the currency which can be used to format returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'currency_prefix' => array(
'description' => __( 'Price prefix for the currency which can be used to format returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'currency_suffix' => array(
'description' => __( 'Price prefix for the currency which can be used to format returned prices.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
];
}
/**
* Adds currency data to an array of monetary values.
*
* @param array $values Monetary amounts.
* @return array Monetary amounts with currency data appended.
*/
protected function prepare_currency_response( $values ) {
return $this->extend->get_formatter( 'currency' )->format( $values );
}
/**
* Convert monetary values from WooCommerce to string based integers, using
* the smallest unit of a currency.
*
* @param string|float $amount Monetary amount with decimals.
* @param int $decimals Number of decimals the amount is formatted with.
* @param int $rounding_mode Defaults to the PHP_ROUND_HALF_UP constant.
* @return string The new amount.
*/
protected function prepare_money_response( $amount, $decimals = 2, $rounding_mode = PHP_ROUND_HALF_UP ) {
return $this->extend->get_formatter( 'money' )->format(
$amount,
[
'decimals' => $decimals,
'rounding_mode' => $rounding_mode,
]
);
}
/**
* Prepares HTML based content, such as post titles and content, for the API response.
*
* @param string|array $response Data to format.
* @return string|array Formatted data.
*/
protected function prepare_html_response( $response ) {
return $this->extend->get_formatter( 'html' )->format( $response );
}
}