initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,195 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\RestApi\Routes;
/**
* AddressSchema class.
*
* Provides a generic address schema for composition in other schemas.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 4.1.0
*/
abstract class AbstractAddressSchema extends AbstractSchema {
/**
* Term properties.
*
* @internal Note that required properties don't require values, just that they are included in the request.
* @return array
*/
public function get_properties() {
return [
'first_name' => [
'description' => __( 'First name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'last_name' => [
'description' => __( 'Last name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'company' => [
'description' => __( 'Company', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'address_1' => [
'description' => __( 'Address', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'address_2' => [
'description' => __( 'Apartment, suite, etc.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'city' => [
'description' => __( 'City', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'state' => [
'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'postcode' => [
'description' => __( 'Postal code', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'country' => [
'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
'phone' => [
'description' => __( 'Phone', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
];
}
/**
* Sanitize and format the given address object.
*
* @param array $address Value being sanitized.
* @param \WP_REST_Request $request The Request.
* @param string $param The param being sanitized.
* @return array
*/
public function sanitize_callback( $address, $request, $param ) {
$address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address );
$address['country'] = wc_strtoupper( wc_clean( wp_unslash( $address['country'] ) ) );
$address['first_name'] = wc_clean( wp_unslash( $address['first_name'] ) );
$address['last_name'] = wc_clean( wp_unslash( $address['last_name'] ) );
$address['company'] = wc_clean( wp_unslash( $address['company'] ) );
$address['address_1'] = wc_clean( wp_unslash( $address['address_1'] ) );
$address['address_2'] = wc_clean( wp_unslash( $address['address_2'] ) );
$address['city'] = wc_clean( wp_unslash( $address['city'] ) );
$address['state'] = $this->format_state( wc_clean( wp_unslash( $address['state'] ) ), $address['country'] );
$address['postcode'] = $address['postcode'] ? wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
$address['phone'] = wc_clean( wp_unslash( $address['phone'] ) );
return $address;
}
/**
* Format a state based on the country. If country has defined states, will return an upper case state code.
*
* @param string $state State name or code (sanitized).
* @param string $country Country code.
* @return string
*/
protected function format_state( $state, $country ) {
$states = $country ? array_filter( (array) wc()->countries->get_states( $country ) ) : [];
if ( count( $states ) ) {
$state = wc_strtoupper( $state );
$state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $states ) ) );
if ( isset( $state_values[ $state ] ) ) {
// Convert to state code if a state name was provided.
return $state_values[ $state ];
}
}
return $state;
}
/**
* Validate the given address object.
*
* @see rest_validate_value_from_schema
*
* @param array $address Value being sanitized.
* @param \WP_REST_Request $request The Request.
* @param string $param The param being sanitized.
* @return true|\WP_Error
*/
public function validate_callback( $address, $request, $param ) {
$errors = new \WP_Error();
$address = $this->sanitize_callback( $address, $request, $param );
if ( empty( $address['country'] ) ) {
$errors->add(
'missing_country',
__( 'Country is required', 'woocommerce' )
);
return $errors;
}
if ( ! in_array( $address['country'], array_keys( wc()->countries->get_countries() ), true ) ) {
$errors->add(
'invalid_country',
sprintf(
/* translators: %s valid country codes */
__( 'Invalid country code provided. Must be one of: %s', 'woocommerce' ),
implode( ', ', array_keys( wc()->countries->get_countries() ) )
)
);
return $errors;
}
$states = array_filter( array_keys( (array) wc()->countries->get_states( $address['country'] ) ) );
if ( ! empty( $address['state'] ) && count( $states ) && ! in_array( $address['state'], $states, true ) ) {
$errors->add(
'invalid_state',
sprintf(
/* translators: %s valid states */
__( 'The provided state is not valid. Must be one of: %s', 'woocommerce' ),
implode( ', ', $states )
)
);
}
if ( ! empty( $address['postcode'] ) && ! \WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) {
$errors->add(
'invalid_postcode',
__( 'The provided postcode / ZIP is not valid', 'woocommerce' )
);
}
if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) {
$errors->add(
'invalid_phone',
__( 'The provided phone number is not valid', 'woocommerce' )
);
}
return $errors->has_errors( $errors ) ? $errors : true;
}
}

View File

@ -0,0 +1,369 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* AbstractSchema class.
*
* For REST Route Schemas
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
abstract class AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'Schema';
/**
* Rest extend instance
*
* @var ExtendRestApi
*/
protected $extend;
/**
* Extending key that gets added to endpoint.
*
* @var string
*/
const EXTENDING_KEY = 'extensions';
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
*/
public function __construct( ExtendRestApi $extend ) {
$this->extend = $extend;
}
/**
* 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(),
);
}
/**
* 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 identifer.
* @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;
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 ) ) {
return $result;
}
if ( isset( $property_value['properties'] ) ) {
$validate_callback = $this->get_recursive_validate_callback( $property_value['properties'] );
return $validate_callback( $current_value, $request, $param . ' > ' . $property_key );
}
}
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 ) {
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 ( is_wp_error( $current_value ) ) {
return $current_value;
}
if ( isset( $property_value['properties'] ) ) {
$sanitize_callback = $this->get_recursive_sanitize_callback( $property_value['properties'] );
return $sanitize_callback( $current_value, $request, $param . ' > ' . $property_key );
}
}
return true;
};
}
/**
* 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 );
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\RestApi\Routes;
/**
* BillingAddressSchema class.
*
* Provides a generic billing address schema for composition in other schemas.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class BillingAddressSchema extends AbstractAddressSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'billing_address';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'billing-address';
/**
* Term properties.
*
* @return array
*/
public function get_properties() {
$properties = parent::get_properties();
return array_merge(
$properties,
[
'email' => [
'description' => __( 'Email', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'required' => true,
],
]
);
}
/**
* Sanitize and format the given address object.
*
* @param array $address Value being sanitized.
* @param \WP_REST_Request $request The Request.
* @param string $param The param being sanitized.
* @return array
*/
public function sanitize_callback( $address, $request, $param ) {
$address = parent::sanitize_callback( $address, $request, $param );
$address['email'] = wc_clean( wp_unslash( $address['email'] ) );
return $address;
}
/**
* Validate the given address object.
*
* @param array $address Value being sanitized.
* @param \WP_REST_Request $request The Request.
* @param string $param The param being sanitized.
* @return true|\WP_Error
*/
public function validate_callback( $address, $request, $param ) {
$errors = parent::validate_callback( $address, $request, $param );
$address = $this->sanitize_callback( $address, $request, $param );
$errors = is_wp_error( $errors ) ? $errors : new \WP_Error();
if ( ! empty( $address['email'] ) && ! is_email( $address['email'] ) ) {
$errors->add(
'invalid_email',
__( 'The provided email address is not valid', 'woocommerce' )
);
}
return $errors->has_errors( $errors ) ? $errors : true;
}
/**
* Convert a term object into an object suitable for the response.
*
* @param \WC_Order|\WC_Customer $address An object with billing address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
*/
public function get_item_response( $address ) {
if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) {
return (object) $this->prepare_html_response(
[
'first_name' => $address->get_billing_first_name(),
'last_name' => $address->get_billing_last_name(),
'company' => $address->get_billing_company(),
'address_1' => $address->get_billing_address_1(),
'address_2' => $address->get_billing_address_2(),
'city' => $address->get_billing_city(),
'state' => $address->get_billing_state(),
'postcode' => $address->get_billing_postcode(),
'country' => $address->get_billing_country(),
'email' => $address->get_billing_email(),
'phone' => $address->get_billing_phone(),
]
);
}
throw new RouteException(
'invalid_object_type',
sprintf(
/* translators: Placeholders are class and method names */
__( '%1$s requires an instance of %2$s or %3$s for the address', 'woocommerce' ),
'BillingAddressSchema::get_item_response',
'WC_Customer',
'WC_Order'
),
500
);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController;
/**
* CartCouponSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
* @since 3.9.0 Coupon type (`discount_type`) added.
*/
class CartCouponSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart_coupon';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart-coupon';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'code' => [
'description' => __( 'The coupon\'s unique code.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'arg_options' => [
'sanitize_callback' => 'wc_format_coupon_code',
'validate_callback' => [ $this, 'coupon_exists' ],
],
],
'discount_type' => [
'description' => __( 'The discount type for the coupon (e.g. percentage or fixed amount)', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'arg_options' => [
'validate_callback' => [ $this, 'coupon_exists' ],
],
],
'totals' => [
'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total_discount' => [
'description' => __( 'Total discount applied by this coupon.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount applied by this coupon.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Check given coupon exists.
*
* @param string $coupon_code Coupon code.
* @return bool
*/
public function coupon_exists( $coupon_code ) {
$coupon = new \WC_Coupon( $coupon_code );
return (bool) $coupon->get_id() || $coupon->get_virtual();
}
/**
* Generate a response from passed coupon code.
*
* @param string $coupon_code Coupon code from the cart.
* @return array
*/
public function get_item_response( $coupon_code ) {
$controller = new CartController();
$cart = $controller->get_cart_instance();
$total_discounts = $cart->get_coupon_discount_totals();
$total_discount_taxes = $cart->get_coupon_discount_tax_totals();
$coupon = new \WC_Coupon( $coupon_code );
return [
'code' => $coupon_code,
'discount_type' => $coupon->get_discount_type(),
'totals' => (object) $this->prepare_currency_response(
[
'total_discount' => $this->prepare_money_response( isset( $total_discounts[ $coupon_code ] ) ? $total_discounts[ $coupon_code ] : 0, wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( isset( $total_discount_taxes[ $coupon_code ] ) ? $total_discount_taxes[ $coupon_code ] : 0, wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use AutomateWoo\Exception;
use Automattic\WooCommerce\Blocks\StoreApi\Routes\RouteException;
/**
* Class CartExtensionsSchema
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class CartExtensionsSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart-extensions';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart-extensions';
/**
* Cart extensions schema properties.
*
* @return array
*/
public function get_properties() {
return [];
}
/**
* Handle the request and return a valid response for this endpoint.
*
* @param \WP_REST_Request $request Request containing data for the extension callback.
* @throws RouteException When callback is not callable or parameters are incorrect.
*
* @return array
*/
public function get_item_response( $request = null ) {
try {
$callback = $this->extend->get_update_callback( $request['namespace'] );
} catch ( RouteException $e ) {
throw $e;
}
if ( is_callable( $callback ) ) {
$callback( $request['data'] );
}
return rest_ensure_response( wc()->api->get_endpoint_data( '/wc/store/cart' ) );
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* CartFeeSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class CartFeeSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart_fee';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart-fee';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the fee within the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Fee name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Fee total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total' => [
'description' => __( 'Total amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Convert a WooCommerce cart fee to an object suitable for the response.
*
* @param array $fee Cart fee data.
* @return array
*/
public function get_item_response( $fee ) {
return [
'key' => $fee->id,
'name' => $this->prepare_html_response( $fee->name ),
'totals' => (object) $this->prepare_currency_response(
[
'total' => $this->prepare_money_response( $fee->total, wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $fee->tax, wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@ -0,0 +1,447 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
/**
* CartItemSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class CartItemSchema extends ProductSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart_item';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart-item';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The cart item product or variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item in the cart.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limit' => [
'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the cart item', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/**
* Convert a WooCommerce cart item to an object suitable for the response.
*
* @param array $cart_item Cart item array.
* @return array
*/
public function get_item_response( $cart_item ) {
$product = $cart_item['data'];
return [
'key' => $cart_item['key'],
'id' => $product->get_id(),
'quantity' => wc_stock_amount( $cart_item['quantity'] ),
'quantity_limit' => $this->get_product_quantity_limit( $product ),
'name' => $this->prepare_html_response( $product->get_title() ),
'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'low_stock_remaining' => $this->get_low_stock_remaining( $product ),
'backorders_allowed' => (bool) $product->backorders_allowed(),
'show_backorder_badge' => (bool) $product->backorders_require_notification() && $product->is_on_backorder( $cart_item['quantity'] ),
'sold_individually' => $product->is_sold_individually(),
'permalink' => $product->get_permalink(),
'images' => $this->get_images( $product ),
'variation' => $this->format_variation_data( $cart_item['variation'], $product ),
'item_data' => $this->get_item_data( $cart_item ),
'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
'totals' => (object) $this->prepare_currency_response(
[
'line_subtotal' => $this->prepare_money_response( $cart_item['line_subtotal'], wc_get_price_decimals() ),
'line_subtotal_tax' => $this->prepare_money_response( $cart_item['line_subtotal_tax'], wc_get_price_decimals() ),
'line_total' => $this->prepare_money_response( $cart_item['line_total'], wc_get_price_decimals() ),
'line_total_tax' => $this->prepare_money_response( $cart_item['line_tax'], wc_get_price_decimals() ),
]
),
'catalog_visibility' => $product->get_catalog_visibility(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER, $cart_item ),
];
}
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
$prices = parent::prepare_product_price_response( $product, $tax_display_mode );
// Add raw prices (prices with greater precision).
$prices['raw_prices'] = [
'precision' => wc_get_rounding_precision(),
'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ),
'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ),
];
return $prices;
}
/**
* Returns the remaining stock for a product if it has stock.
*
* This also factors in draft orders.
*
* @param \WC_Product $product Product instance.
* @return integer|null
*/
protected function get_remaining_stock( \WC_Product $product ) {
if ( is_null( $product->get_stock_quantity() ) ) {
return null;
}
$draft_order = wc()->session->get( 'store_api_draft_order', 0 );
$reserve_stock = new ReserveStock();
$reserved_stock = $reserve_stock->get_reserved_stock( $product, $draft_order );
return $product->get_stock_quantity() - $reserved_stock;
}
/**
* Format variation data, for example convert slugs such as attribute_pa_size to Size.
*
* @param array $variation_data Array of data from the cart.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( $variation_data, $product ) {
$return = [];
if ( ! is_iterable( $variation_data ) ) {
return $return;
}
foreach ( $variation_data as $key => $value ) {
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $key ) ) );
if ( taxonomy_exists( $taxonomy ) ) {
// If this is a term slug, get the term's nice name.
$term = get_term_by( 'slug', $value, $taxonomy );
if ( ! is_wp_error( $term ) && $term && $term->name ) {
$value = $term->name;
}
$label = wc_attribute_label( $taxonomy );
} else {
// If this is a custom option slug, get the options name.
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product );
}
$return[] = [
'attribute' => $this->prepare_html_response( $label ),
'value' => $this->prepare_html_response( $value ),
];
}
return $return;
}
/**
* Format cart item data removing any HTML tag.
*
* @param array $cart_item Cart item array.
* @return array
*/
protected function get_item_data( $cart_item ) {
$item_data = apply_filters( 'woocommerce_get_item_data', array(), $cart_item );
return array_map( [ $this, 'format_item_data_element' ], $item_data );
}
/**
* Remove HTML tags from cart item data and set the `hidden` property to
* `__experimental_woocommerce_blocks_hidden`.
*
* @param array $item_data_element Individual element of a cart item data.
* @return array
*/
protected function format_item_data_element( $item_data_element ) {
if ( array_key_exists( '__experimental_woocommerce_blocks_hidden', $item_data_element ) ) {
$item_data_element['hidden'] = $item_data_element['__experimental_woocommerce_blocks_hidden'];
}
return array_map( 'wp_strip_all_tags', $item_data_element );
}
}

View File

@ -0,0 +1,424 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
use WC_Tax;
use WP_Error;
/**
* CartSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class CartSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart';
/**
* Item schema instance.
*
* @var CartItemSchema
*/
public $item_schema;
/**
* Coupon schema instance.
*
* @var CartCouponSchema
*/
public $coupon_schema;
/**
* Fee schema instance.
*
* @var CartFeeSchema
*/
public $fee_schema;
/**
* Shipping rates schema instance.
*
* @var CartShippingRateSchema
*/
public $shipping_rate_schema;
/**
* Shipping address schema instance.
*
* @var ShippingAddressSchema
*/
public $shipping_address_schema;
/**
* Billing address schema instance.
*
* @var BillingAddressSchema
*/
public $billing_address_schema;
/**
* Error schema instance.
*
* @var ErrorSchema
*/
public $error_schema;
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
* @param CartItemSchema $item_schema Item schema instance.
* @param CartCouponSchema $coupon_schema Coupon schema instance.
* @param CartFeeSchema $fee_schema Fee schema instance.
* @param CartShippingRateSchema $shipping_rate_schema Shipping rates schema instance.
* @param ShippingAddressSchema $shipping_address_schema Shipping address schema instance.
* @param BillingAddressSchema $billing_address_schema Billing address schema instance.
* @param ErrorSchema $error_schema Error schema instance.
*/
public function __construct(
ExtendRestApi $extend,
CartItemSchema $item_schema,
CartCouponSchema $coupon_schema,
CartFeeSchema $fee_schema,
CartShippingRateSchema $shipping_rate_schema,
ShippingAddressSchema $shipping_address_schema,
BillingAddressSchema $billing_address_schema,
ErrorSchema $error_schema
) {
$this->item_schema = $item_schema;
$this->coupon_schema = $coupon_schema;
$this->fee_schema = $fee_schema;
$this->shipping_rate_schema = $shipping_rate_schema;
$this->shipping_address_schema = $shipping_address_schema;
$this->billing_address_schema = $billing_address_schema;
$this->error_schema = $error_schema;
parent::__construct( $extend );
}
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'coupons' => [
'description' => __( 'List of applied cart coupons.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->coupon_schema->get_properties() ),
],
],
'shipping_rates' => [
'description' => __( 'List of available shipping rates for the cart.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->shipping_rate_schema->get_properties() ),
],
],
'shipping_address' => [
'description' => __( 'Current set shipping address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->shipping_address_schema->get_properties() ),
],
'billing_address' => [
'description' => __( 'Current set billing address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->billing_address_schema->get_properties() ),
],
'items' => [
'description' => __( 'List of cart items.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->item_schema->get_properties() ),
],
],
'items_count' => [
'description' => __( 'Number of items in the cart.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'items_weight' => [
'description' => __( 'Total weight (in grams) of all products in the cart.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_payment' => [
'description' => __( 'True if the cart needs payment. False for carts with only free products and no shipping costs.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_shipping' => [
'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'has_calculated_shipping' => [
'description' => __( 'True if the cart meets the criteria for showing shipping costs, and rates have been calculated and included in the totals.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'fees' => [
'description' => __( 'List of cart fees.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->fee_schema->get_properties() ),
],
],
'totals' => [
'description' => __( 'Cart total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total_items' => [
'description' => __( 'Total price of items in the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items_tax' => [
'description' => __( 'Total tax on items in the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees' => [
'description' => __( 'Total price of any applied fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees_tax' => [
'description' => __( 'Total tax on fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount' => [
'description' => __( 'Total discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping' => [
'description' => __( 'Total price of shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping_tax' => [
'description' => __( 'Total tax on shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_price' => [
'description' => __( 'Total price the customer will pay.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax applied to items and shipping.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'tax_lines' => [
'description' => __( 'Lines of taxes applied to items and shipping.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'The name of the tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'The amount of tax charged.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'rate' => [
'description' => __( 'The rate at which tax is applied.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
]
),
],
'errors' => [
'description' => __( 'List of cart item errors, for example, items in the cart which are out of stock.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ),
],
],
'payment_requirements' => [
'description' => __( 'List of required payment gateway features to process the order.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'generated_timestamp' => [
'description' => __( 'The time at which this cart data was prepared', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/**
* Convert a woo cart into an object suitable for the response.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
public function get_item_response( $cart ) {
$controller = new CartController();
// Get cart errors first so if recalculations are performed, it's reflected in the response.
$cart_errors = $this->get_cart_errors( $cart );
// The core cart class will not include shipping in the cart totals if `show_shipping()` returns false. This can
// happen if an address is required, or through the use of hooks. This tracks if shipping has actually been
// calculated so we can avoid returning costs and rates prematurely.
$has_calculated_shipping = $cart->show_shipping();
// Get shipping packages to return in the response from the cart.
$shipping_packages = $has_calculated_shipping ? $controller->get_shipping_packages() : [];
return [
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'shipping_address' => $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => $this->billing_address_schema->get_item_response( wc()->customer ),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ),
'items_count' => $cart->get_cart_contents_count(),
'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'has_calculated_shipping' => $has_calculated_shipping,
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response(
[
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $cart ),
]
),
'errors' => $cart_errors,
'payment_requirements' => $this->extend->get_payment_requirements(),
'generated_timestamp' => time(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
];
}
/**
* Get tax lines from the cart and format to match schema.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
protected function get_tax_lines( $cart ) {
$cart_tax_totals = $cart->get_tax_totals();
$tax_lines = [];
foreach ( $cart_tax_totals as $cart_tax_total ) {
$tax_lines[] = array(
'name' => $cart_tax_total->label,
'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ),
'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ),
);
}
return $tax_lines;
}
/**
* Get cart validation errors.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
protected function get_cart_errors( $cart ) {
$controller = new CartController();
$item_errors = array_filter(
$controller->get_cart_item_errors(),
function ( WP_Error $error ) {
return $error->has_errors();
}
);
$coupon_errors = $controller->get_cart_coupon_errors();
return array_values( array_map( [ $this->error_schema, 'get_item_response' ], array_merge( $item_errors, $coupon_errors ) ) );
}
}

View File

@ -0,0 +1,354 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use WC_Shipping_Rate as ShippingRate;
/**
* CartShippingRateSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class CartShippingRateSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'cart-shipping-rate';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'cart-shipping-rate';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'package_id' => [
'description' => __( 'The ID of the package the shipping rates belong to.', 'woocommerce' ),
'type' => [ 'integer', 'string' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Name of the package.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'destination' => [
'description' => __( 'Shipping destination address.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'address_1' => [
'description' => __( 'First line of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'address_2' => [
'description' => __( 'Second line of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'city' => [
'description' => __( 'City of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'state' => [
'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'postcode' => [
'description' => __( 'Zip or Postcode of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'country' => [
'description' => __( 'ISO code for the country of the address being shipped to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'items' => [
'description' => __( 'List of cart items the returned shipping rates apply to.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Name of the item.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of the item in the current package.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'shipping_rates' => [
'description' => __( 'List of shipping rates.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->get_rate_properties(),
],
],
];
}
/**
* Schema for a single rate.
*
* @return array
*/
protected function get_rate_properties() {
return array_merge(
[
'rate_id' => [
'description' => __( 'ID of the shipping rate.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Name of the shipping rate, e.g. Express shipping.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Description of the shipping rate, e.g. Dispatched via USPS.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'delivery_time' => [
'description' => __( 'Delivery time estimate text, e.g. 3-5 business days.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Price of this shipping rate using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'taxes' => [
'description' => __( 'Taxes applied to this shipping rate using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'method_id' => [
'description' => __( 'ID of the shipping method that provided the rate.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'instance_id' => [
'description' => __( 'Instance ID of the shipping method that provided the rate.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'meta_data' => [
'description' => __( 'Meta data attached to the shipping rate.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'selected' => [
'description' => __( 'True if this is the rate currently selected by the customer for the cart.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
$this->get_store_currency_properties()
);
}
/**
* Convert a shipping rate from WooCommerce into a valid response.
*
* @param array $package Shipping package complete with rates from WooCommerce.
* @return array
*/
public function get_item_response( $package ) {
return [
'package_id' => $package['package_id'],
'name' => $package['package_name'],
'destination' => $this->prepare_package_destination_response( $package ),
'items' => $this->prepare_package_items_response( $package ),
'shipping_rates' => $this->prepare_package_shipping_rates_response( $package ),
];
}
/**
* Gets and formats the destination address of a package.
*
* @param array $package Shipping package complete with rates from WooCommerce.
* @return object
*/
protected function prepare_package_destination_response( $package ) {
return (object) $this->prepare_html_response(
[
'address_1' => $package['destination']['address_1'],
'address_2' => $package['destination']['address_2'],
'city' => $package['destination']['city'],
'state' => $package['destination']['state'],
'postcode' => $package['destination']['postcode'],
'country' => $package['destination']['country'],
]
);
}
/**
* Gets items from a package and creates an array of strings containing product names and quantities.
*
* @param array $package Shipping package complete with rates from WooCommerce.
* @return array
*/
protected function prepare_package_items_response( $package ) {
$items = array();
foreach ( $package['contents'] as $item_id => $values ) {
$items[] = [
'key' => $item_id,
'name' => $values['data']->get_name(),
'quantity' => $values['quantity'],
];
}
return $items;
}
/**
* Prepare an array of rates from a package for the response.
*
* @param array $package Shipping package complete with rates from WooCommerce.
* @return array
*/
protected function prepare_package_shipping_rates_response( $package ) {
$rates = $package['rates'];
$selected_rates = wc()->session->get( 'chosen_shipping_methods', array() );
$selected_rate = isset( $selected_rates[ $package['package_id'] ] ) ? $selected_rates[ $package['package_id'] ] : '';
if ( empty( $selected_rate ) && ! empty( $package['rates'] ) ) {
$selected_rate = wc_get_chosen_shipping_method_for_package( $package['package_id'], $package );
}
$response = [];
foreach ( $package['rates'] as $rate ) {
$response[] = $this->get_rate_response( $rate, $selected_rate );
}
return $response;
}
/**
* Response for a single rate.
*
* @param WC_Shipping_Rate $rate Rate object.
* @param string $selected_rate Selected rate.
* @return array
*/
protected function get_rate_response( $rate, $selected_rate = '' ) {
return $this->prepare_currency_response(
[
'rate_id' => $this->get_rate_prop( $rate, 'id' ),
'name' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'label' ) ),
'description' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'description' ) ),
'delivery_time' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'delivery_time' ) ),
'price' => $this->prepare_money_response( $this->get_rate_prop( $rate, 'cost' ), wc_get_price_decimals() ),
'taxes' => $this->prepare_money_response( array_sum( $this->get_rate_prop( $rate, 'taxes' ) ), wc_get_price_decimals() ),
'instance_id' => $this->get_rate_prop( $rate, 'instance_id' ),
'method_id' => $this->get_rate_prop( $rate, 'method_id' ),
'meta_data' => $this->get_rate_meta_data( $rate ),
'selected' => $selected_rate === $this->get_rate_prop( $rate, 'id' ),
]
);
}
/**
* Gets a prop of the rate object, if callable.
*
* @param WC_Shipping_Rate $rate Rate object.
* @param string $prop Prop name.
* @return string
*/
protected function get_rate_prop( $rate, $prop ) {
$getter = 'get_' . $prop;
return \is_callable( array( $rate, $getter ) ) ? $rate->$getter() : '';
}
/**
* Converts rate meta data into a suitable response object.
*
* @param WC_Shipping_Rate $rate Rate object.
* @return array
*/
protected function get_rate_meta_data( $rate ) {
$meta_data = $rate->get_meta_data();
return array_reduce(
array_keys( $meta_data ),
function( $return, $key ) use ( $meta_data ) {
$return[] = [
'key' => $key,
'value' => $meta_data[ $key ],
];
return $return;
},
[]
);
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Payments\PaymentResult;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* CheckoutSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class CheckoutSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'checkout';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'checkout';
/**
* Billing address schema instance.
*
* @var BillingAddressSchema
*/
protected $billing_address_schema;
/**
* Shipping address schema instance.
*
* @var ShippingAddressSchema
*/
protected $shipping_address_schema;
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
* @param BillingAddressSchema $billing_address_schema Billing address schema instance.
* @param ShippingAddressSchema $shipping_address_schema Shipping address schema instance.
*/
public function __construct( ExtendRestApi $extend, BillingAddressSchema $billing_address_schema, ShippingAddressSchema $shipping_address_schema ) {
$this->billing_address_schema = $billing_address_schema;
$this->shipping_address_schema = $shipping_address_schema;
parent::__construct( $extend );
}
/**
* Checkout schema properties.
*
* @return array
*/
public function get_properties() {
return [
'order_id' => [
'description' => __( 'The order ID to process during checkout.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'status' => [
'description' => __( 'Order status. Payment providers will update this value after payment.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'order_key' => [
'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'customer_note' => [
'description' => __( 'Note added to the order by the customer during checkout.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'customer_id' => [
'description' => __( 'Customer ID if registered. Will return 0 for guests.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'billing_address' => [
'description' => __( 'Billing address.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'properties' => $this->billing_address_schema->get_properties(),
'arg_options' => [
'sanitize_callback' => [ $this->billing_address_schema, 'sanitize_callback' ],
'validate_callback' => [ $this->billing_address_schema, 'validate_callback' ],
],
'required' => true,
],
'shipping_address' => [
'description' => __( 'Shipping address.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'properties' => $this->shipping_address_schema->get_properties(),
'arg_options' => [
'sanitize_callback' => [ $this->shipping_address_schema, 'sanitize_callback' ],
'validate_callback' => [ $this->shipping_address_schema, 'validate_callback' ],
],
'required' => true,
],
'payment_method' => [
'description' => __( 'The ID of the payment method being used to process the payment.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'enum' => wc()->payment_gateways->get_payment_gateway_ids(),
],
'create_account' => [
'description' => __( 'Whether to create a new user account as part of order processing.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
],
'payment_result' => [
'description' => __( 'Result of payment processing, or false if not yet processed.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'payment_status' => [
'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woocommerce' ),
'readonly' => true,
'type' => 'string',
],
'payment_details' => [
'description' => __( 'An array of data being returned from the payment gateway.', 'woocommerce' ),
'readonly' => true,
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'type' => 'string',
],
'value' => [
'type' => 'string',
],
],
],
],
'redirect_url' => [
'description' => __( 'A URL to redirect the customer after checkout. This could be, for example, a link to the payment processors website.', 'woocommerce' ),
'readonly' => true,
'type' => 'string',
],
],
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/**
* Return the response for checkout.
*
* @param object $item Results from checkout action.
* @return array
*/
public function get_item_response( $item ) {
return $this->get_checkout_response( $item->order, $item->payment_result );
}
/**
* Get the checkout response based on the current order and any payments.
*
* @param \WC_Order $order Order object.
* @param PaymentResult $payment_result Payment result object.
* @return array
*/
protected function get_checkout_response( \WC_Order $order, PaymentResult $payment_result = null ) {
return [
'order_id' => $order->get_id(),
'status' => $order->get_status(),
'order_key' => $order->get_order_key(),
'customer_note' => $order->get_customer_note(),
'customer_id' => $order->get_customer_id(),
'billing_address' => $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => $this->shipping_address_schema->get_item_response( $order ),
'payment_method' => $order->get_payment_method(),
'payment_result' => [
'payment_status' => $payment_result->status,
'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ),
'redirect_url' => $payment_result->redirect_url,
],
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
];
}
/**
* This prepares the payment details for the response so it's following the
* schema where it's an array of objects.
*
* @param array $payment_details An array of payment details from the processed payment.
*
* @return array An array of objects where each object has the key and value
* as distinct properties.
*/
protected function prepare_payment_details_for_response( array $payment_details ) {
return array_map(
function( $key, $value ) {
return (object) [
'key' => $key,
'value' => $value,
];
},
array_keys( $payment_details ),
$payment_details
);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* ErrorSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class ErrorSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'error';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'error';
/**
* Product schema properties.
*
* @return array
*/
public function get_properties() {
return [
'code' => [
'description' => __( 'Error code', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'message' => [
'description' => __( 'Error message', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Convert a WP_Error into an object suitable for the response.
*
* @param \WP_Error $error Error object.
* @return array
*/
public function get_item_response( \WP_Error $error ) {
return [
'code' => $this->prepare_html_response( $error->get_error_code() ),
'message' => $this->prepare_html_response( $error->get_error_message() ),
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* ImageAttachmentSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class ImageAttachmentSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'image';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'image';
/**
* Product schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
],
'src' => [
'description' => __( 'Full size image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
],
'thumbnail' => [
'description' => __( 'Thumbnail URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
],
'srcset' => [
'description' => __( 'Thumbnail srcset for responsive images.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'sizes' => [
'description' => __( 'Thumbnail sizes for responsive images.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'name' => [
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'alt' => [
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
];
}
/**
* Convert a WooCommerce product into an object suitable for the response.
*
* @param int $attachment_id Image attachment ID.
* @return array|null
*/
public function get_item_response( $attachment_id ) {
if ( ! $attachment_id ) {
return null;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
return [];
}
$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );
return [
'id' => (int) $attachment_id,
'src' => current( $attachment ),
'thumbnail' => current( $thumbnail ),
'srcset' => (string) wp_get_attachment_image_srcset( $attachment_id, 'full' ),
'sizes' => (string) wp_get_attachment_image_sizes( $attachment_id, 'full' ),
'name' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
];
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* OrderCouponSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class OrderCouponSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_coupon';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-coupon';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'code' => [
'description' => __( 'The coupons unique code.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total_discount' => [
'description' => __( 'Total discount applied by this coupon.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount applied by this coupon.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Convert an order coupon to an object suitable for the response.
*
* @param \WC_Order_Item_Coupon $coupon Order coupon array.
* @return array
*/
public function get_item_response( \WC_Order_Item_Coupon $coupon ) {
return [
'code' => $coupon->get_code(),
'totals' => (object) $this->prepare_currency_response(
[
'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* ProductAttributeSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class ProductAttributeSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product_attribute';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'product-attribute';
/**
* Term properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxonomy' => array(
'description' => __( 'The attribute taxonomy name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Attribute type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order' => array(
'description' => __( 'How terms in this attribute are sorted by default.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'has_archives' => array(
'description' => __( 'If this attribute has term archive pages.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of terms in the attribute taxonomy.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
];
}
/**
* Convert an attribute object into an object suitable for the response.
*
* @param object $attribute Attribute object.
* @return array
*/
public function get_item_response( $attribute ) {
return [
'id' => (int) $attribute->id,
'name' => $this->prepare_html_response( $attribute->name ),
'taxonomy' => $attribute->slug,
'type' => $attribute->type,
'order' => $attribute->order_by,
'has_archives' => $attribute->has_archives,
'count' => (int) \wp_count_terms( $attribute->slug ),
];
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* ProductCategorySchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class ProductCategorySchema extends TermSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product-category';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'product-category';
/**
* Image attachment schema instance.
*
* @var ImageAttachmentSchema
*/
protected $image_attachment_schema;
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
* @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance.
*/
public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) {
$this->image_attachment_schema = $image_attachment_schema;
parent::__construct( $extend );
}
/**
* Term properties.
*
* @return array
*/
public function get_properties() {
$schema = parent::get_properties();
$schema['image'] = [
'description' => __( 'Category image.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
'properties' => $this->image_attachment_schema->get_properties(),
];
$schema['review_count'] = [
'description' => __( 'Number of reviews for products in this category.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
];
$schema['permalink'] = [
'description' => __( 'Category URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
];
return $schema;
}
/**
* Convert a term object into an object suitable for the response.
*
* @param \WP_Term $term Term object.
* @return array
*/
public function get_item_response( $term ) {
$response = parent::get_item_response( $term );
$count = get_term_meta( $term->term_id, 'product_count_product_cat', true );
if ( $count ) {
$response['count'] = (int) $count;
}
$response['image'] = $this->image_attachment_schema->get_item_response( get_term_meta( $term->term_id, 'thumbnail_id', true ) );
$response['review_count'] = $this->get_category_review_count( $term );
$response['permalink'] = get_term_link( $term->term_id, 'product_cat' );
return $response;
}
/**
* Get total number of reviews for products in a category.
*
* @param \WP_Term $term Term object.
* @return int
*/
protected function get_category_review_count( $term ) {
global $wpdb;
$children = get_term_children( $term->term_id, 'product_cat' );
if ( ! $children || is_wp_error( $children ) ) {
$terms_to_count_str = absint( $term->term_id );
} else {
$terms_to_count = array_unique( array_map( 'absint', array_merge( array( $term->term_id ), $children ) ) );
$terms_to_count_str = implode( ',', $terms_to_count );
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$products_of_category_sql = $wpdb->prepare(
"SELECT SUM(comment_count) as review_count
FROM {$wpdb->posts} AS posts
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id
WHERE term_relationships.term_taxonomy_id IN ({$terms_to_count_str})"
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$review_count = $wpdb->get_var( $products_of_category_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return (int) $review_count;
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* ProductCollectionDataSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class ProductCollectionDataSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product-collection-data';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'product-collection-data';
/**
* Product collection data schema properties.
*
* @return array
*/
public function get_properties() {
return [
'price_range' => [
'description' => __( 'Min and max prices found in collection of products, provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'min_price' => [
'description' => __( 'Min price found in collection of products.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_price' => [
'description' => __( 'Max price found in collection of products.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'attribute_counts' => [
'description' => __( 'Returns number of products within attribute terms.', 'woocommerce' ),
'type' => [ 'array', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'term' => [
'description' => __( 'Term ID', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'count' => [
'description' => __( 'Number of products.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'rating_counts' => [
'description' => __( 'Returns number of products with each average rating.', 'woocommerce' ),
'type' => [ 'array', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'rating' => [
'description' => __( 'Average rating', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'count' => [
'description' => __( 'Number of products.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'stock_status_counts' => [
'description' => __( 'Returns number of products with each stock status.', 'woocommerce' ),
'type' => [ 'array', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'status' => [
'description' => __( 'Status', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'count' => [
'description' => __( 'Number of products.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
];
}
/**
* Format data.
*
* @param array $data Collection data to format and return.
* @return array
*/
public function get_item_response( $data ) {
return [
'price_range' => ! is_null( $data['min_price'] ) && ! is_null( $data['max_price'] ) ? (object) $this->prepare_currency_response(
[
'min_price' => $this->prepare_money_response( $data['min_price'], wc_get_price_decimals() ),
'max_price' => $this->prepare_money_response( $data['max_price'], wc_get_price_decimals() ),
]
) : null,
'attribute_counts' => $data['attribute_counts'],
'rating_counts' => $data['rating_counts'],
'stock_status_counts' => $data['stock_status_counts'],
];
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* ProductReviewSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
*/
class ProductReviewSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product_review';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'product-review';
/**
* Image attachment schema instance.
*
* @var ImageAttachmentSchema
*/
protected $image_attachment_schema;
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
* @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance.
*/
public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) {
$this->image_attachment_schema = $image_attachment_schema;
parent::__construct( $extend );
}
/**
* Product review schema properties.
*
* @return array
*/
public function get_properties() {
$properties = [
'id' => [
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_created' => [
'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'formatted_date_created' => [
'description' => __( "The date the review was created, in the site's timezone in human-readable format.", 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_created_gmt' => [
'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'product_id' => [
'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'product_name' => [
'description' => __( 'Name of the product that the review belongs to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'product_permalink' => [
'description' => __( 'Permalink of the product that the review belongs to.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'product_image' => [
'description' => __( 'Image of the product that the review belongs to.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->image_attachment_schema->get_properties(),
],
'reviewer' => [
'description' => __( 'Reviewer name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'review' => [
'description' => __( 'The content of the review.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'arg_options' => [
'sanitize_callback' => 'wp_filter_post_kses',
],
'readonly' => true,
],
'rating' => [
'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'verified' => [
'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
if ( get_option( 'show_avatars' ) ) {
$avatar_properties = array();
$avatar_sizes = rest_get_avatar_sizes();
foreach ( $avatar_sizes as $size ) {
$avatar_properties[ $size ] = array(
/* translators: %d: avatar image size in pixels */
'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$properties['reviewer_avatar_urls'] = array(
'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $avatar_properties,
);
}
return $properties;
}
/**
* Convert a WooCommerce product into an object suitable for the response.
*
* @param \WP_Comment $review Product review object.
* @return array
*/
public function get_item_response( \WP_Comment $review ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true );
$data = [
'id' => (int) $review->comment_ID,
'date_created' => wc_rest_prepare_date_response( $review->comment_date ),
'formatted_date_created' => get_comment_date( 'F j, Y', $review->comment_ID ),
'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ),
'product_id' => (int) $review->comment_post_ID,
'product_name' => get_the_title( (int) $review->comment_post_ID ),
'product_permalink' => get_permalink( (int) $review->comment_post_ID ),
'product_image' => $this->image_attachment_schema->get_item_response( get_post_thumbnail_id( (int) $review->comment_post_ID ) ),
'reviewer' => $review->comment_author,
'review' => $review->comment_content,
'rating' => $rating,
'verified' => wc_review_is_from_verified_owner( $review->comment_ID ),
'reviewer_avatar_urls' => rest_get_avatar_urls( $review->comment_author_email ),
];
if ( 'view' === $context ) {
$data['review'] = wpautop( $data['review'] );
}
return $data;
}
}

View File

@ -0,0 +1,824 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
/**
* ProductSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class ProductSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'product';
/**
* Image attachment schema instance.
*
* @var ImageAttachmentSchema
*/
protected $image_attachment_schema;
/**
* Constructor.
*
* @param ExtendRestApi $extend Rest Extending instance.
* @param ImageAttachmentSchema $image_attachment_schema Image attachment schema instance.
*/
public function __construct( ExtendRestApi $extend, ImageAttachmentSchema $image_attachment_schema ) {
$this->image_attachment_schema = $image_attachment_schema;
parent::__construct( $extend );
}
/**
* Product schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'parent' => [
'description' => __( 'ID of the parent product, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'type' => [
'description' => __( 'Product type.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'variation' => [
'description' => __( 'Product variation attributes, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'on_sale' => [
'description' => __( 'Is the product on sale?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'prices' => [
'description' => __( 'Price data provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'price_html' => array(
'description' => __( 'Price string formatted as HTML.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'average_rating' => [
'description' => __( 'Reviews average rating.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'review_count' => [
'description' => __( 'Amount of reviews that the product has.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'categories' => [
'description' => __( 'List of categories, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Category ID', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Category name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'slug' => [
'description' => __( 'Category slug', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'link' => [
'description' => __( 'Category link', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'tags' => [
'description' => __( 'List of tags, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Tag ID', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Tag name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'slug' => [
'description' => __( 'Tag slug', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'link' => [
'description' => __( 'Tag link', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'attributes' => [
'description' => __( 'List of attributes assigned to the product/variation that are visible or used for variations.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'The attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'taxonomy' => [
'description' => __( 'The attribute taxonomy, or null if the attribute is not taxonomy based.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'has_variations' => [
'description' => __( 'True if this attribute is used by product variations.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'terms' => [
'description' => __( 'List of assigned attribute terms.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'The term ID, or 0 if the attribute is not a global attribute.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'The term name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'slug' => [
'description' => __( 'The term slug.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'default' => [
'description' => __( 'If this is a default attribute', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
],
],
],
'variations' => [
'description' => __( 'List of variation IDs, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'attributes' => [
'description' => __( 'List of variation attributes.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'The attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'The assigned attribute.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
],
],
],
'has_options' => [
'description' => __( 'Does the product have additional options before it can be added to the cart?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'is_purchasable' => [
'description' => __( 'Is the product purchasable?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'is_in_stock' => [
'description' => __( 'Is the product in stock?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'is_on_backorder' => [
'description' => __( 'Is the product stock backordered? This will also return false if backorder notifications are turned off.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limit' => [
'description' => __( 'The maximum quantity than can be added to the cart at once.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'add_to_cart' => [
'description' => __( 'Add to cart button parameters.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'text' => [
'description' => __( 'Button text.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Button description.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'url' => [
'description' => __( 'Add to cart URL.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
];
}
/**
* Convert a WooCommerce product into an object suitable for the response.
*
* @param \WC_Product $product Product instance.
* @return array
*/
public function get_item_response( $product ) {
return [
'id' => $product->get_id(),
'name' => $this->prepare_html_response( $product->get_title() ),
'parent' => $product->get_parent_id(),
'type' => $product->get_type(),
'variation' => $this->prepare_html_response( $product->is_type( 'variation' ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ),
'permalink' => $product->get_permalink(),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
'on_sale' => $product->is_on_sale(),
'prices' => (object) $this->prepare_product_price_response( $product ),
'price_html' => $this->prepare_html_response( $product->get_price_html() ),
'average_rating' => (string) $product->get_average_rating(),
'review_count' => $product->get_review_count(),
'images' => $this->get_images( $product ),
'categories' => $this->get_term_list( $product, 'product_cat' ),
'tags' => $this->get_term_list( $product, 'product_tag' ),
'attributes' => $this->get_attributes( $product ),
'variations' => $this->get_variations( $product ),
'has_options' => $product->has_options(),
'is_purchasable' => $product->is_purchasable(),
'is_in_stock' => $product->is_in_stock(),
'is_on_backorder' => 'onbackorder' === $product->get_stock_status(),
'low_stock_remaining' => $this->get_low_stock_remaining( $product ),
'sold_individually' => $product->is_sold_individually(),
'quantity_limit' => $this->get_product_quantity_limit( $product ),
'add_to_cart' => (object) $this->prepare_html_response(
[
'text' => $product->add_to_cart_text(),
'description' => $product->add_to_cart_description(),
'url' => $product->add_to_cart_url(),
]
),
];
}
/**
* Get list of product images.
*
* @param \WC_Product $product Product instance.
* @return array
*/
protected function get_images( \WC_Product $product ) {
$attachment_ids = array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() );
return array_filter( array_map( [ $this->image_attachment_schema, 'get_item_response' ], $attachment_ids ) );
}
/**
* Gets remaining stock amount for a product.
*
* @param \WC_Product $product Product instance.
* @return integer|null
*/
protected function get_remaining_stock( \WC_Product $product ) {
if ( is_null( $product->get_stock_quantity() ) ) {
return null;
}
return $product->get_stock_quantity();
}
/**
* If a product has low stock, return the remaining stock amount for display.
*
* @param \WC_Product $product Product instance.
* @return integer|null
*/
protected function get_low_stock_remaining( \WC_Product $product ) {
$remaining_stock = $this->get_remaining_stock( $product );
if ( ! is_null( $remaining_stock ) && $remaining_stock <= wc_get_low_stock_amount( $product ) ) {
return max( $remaining_stock, 0 );
}
return null;
}
/**
* Get the quantity limit for an item in the cart.
*
* @param \WC_Product $product Product instance.
* @return int
*/
protected function get_product_quantity_limit( \WC_Product $product ) {
$limits = [ 99 ];
if ( $product->is_sold_individually() ) {
$limits[] = 1;
} elseif ( ! $product->backorders_allowed() ) {
$limits[] = $this->get_remaining_stock( $product );
}
return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product );
}
/**
* Returns true if the given attribute is valid.
*
* @param mixed $attribute Object or variable to check.
* @return boolean
*/
protected function filter_valid_attribute( $attribute ) {
return is_a( $attribute, '\WC_Product_Attribute' );
}
/**
* Returns true if the given attribute is valid and used for variations.
*
* @param mixed $attribute Object or variable to check.
* @return boolean
*/
protected function filter_variation_attribute( $attribute ) {
return $this->filter_valid_attribute( $attribute ) && $attribute->get_variation();
}
/**
* Get variation IDs and attributes from the DB.
*
* @param \WC_Product $product Product instance.
* @returns array
*/
protected function get_variations( \WC_Product $product ) {
if ( ! $product->is_type( 'variable' ) ) {
return [];
}
global $wpdb;
$variation_ids = $product->get_visible_children();
if ( ! count( $variation_ids ) ) {
return [];
}
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_variation_attribute' ] );
$default_variation_meta_data = array_reduce(
$attributes,
function( $defaults, $attribute ) use ( $product ) {
$meta_key = wc_variation_attribute_name( $attribute->get_name() );
$defaults[ $meta_key ] = [
'name' => wc_attribute_label( $attribute->get_name(), $product ),
'value' => null,
];
return $defaults;
},
[]
);
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$variation_meta_data = $wpdb->get_results(
"
SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value
FROM {$wpdb->postmeta}
WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $variation_ids ) ) . ")
AND meta_key IN ('" . implode( "','", array_map( 'esc_sql', array_keys( $default_variation_meta_data ) ) ) . "')
"
);
// phpcs:enable
$attributes_by_variation = array_reduce(
$variation_meta_data,
function( $values, $data ) {
$values[ $data->variation_id ][ $data->attribute_key ] = $data->attribute_value;
return $values;
},
array_fill_keys( $variation_ids, [] )
);
$variations = [];
foreach ( $variation_ids as $variation_id ) {
$attribute_data = $default_variation_meta_data;
foreach ( $attributes_by_variation[ $variation_id ] as $meta_key => $meta_value ) {
if ( '' !== $meta_value ) {
$attribute_data[ $meta_key ]['value'] = $meta_value;
}
}
$variations[] = (object) [
'id' => $variation_id,
'attributes' => array_values( $attribute_data ),
];
}
return $variations;
}
/**
* Get list of product attributes and attribute terms.
*
* @param \WC_Product $product Product instance.
* @return array
*/
protected function get_attributes( \WC_Product $product ) {
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_valid_attribute' ] );
$default_attributes = $product->get_default_attributes();
$return = [];
foreach ( $attributes as $attribute_slug => $attribute ) {
// Only visible and variation attributes will be exposed by this API.
if ( ! $attribute->get_visible() || ! $attribute->get_variation() ) {
continue;
}
$terms = $attribute->is_taxonomy() ? array_map( [ $this, 'prepare_product_attribute_taxonomy_value' ], $attribute->get_terms() ) : array_map( [ $this, 'prepare_product_attribute_value' ], $attribute->get_options() );
// Custom attribute names are sanitized to be the array keys.
// So when we do the array_key_exists check below we also need to sanitize the attribute names.
$sanitized_attribute_name = sanitize_key( $attribute->get_name() );
if ( array_key_exists( $sanitized_attribute_name, $default_attributes ) ) {
foreach ( $terms as $term ) {
$term->default = $term->slug === $default_attributes[ $sanitized_attribute_name ];
}
}
$return[] = (object) [
'id' => $attribute->get_id(),
'name' => wc_attribute_label( $attribute->get_name(), $product ),
'taxonomy' => $attribute->is_taxonomy() ? $attribute->get_name() : null,
'has_variations' => true === $attribute->get_variation(),
'terms' => $terms,
];
}
return $return;
}
/**
* Prepare an attribute term for the response.
*
* @param \WP_Term $term Term object.
* @return object
*/
protected function prepare_product_attribute_taxonomy_value( \WP_Term $term ) {
return $this->prepare_product_attribute_value( $term->name, $term->term_id, $term->slug );
}
/**
* Prepare an attribute term for the response.
*
* @param string $name Attribute term name.
* @param int $id Attribute term ID.
* @param string $slug Attribute term slug.
* @return object
*/
protected function prepare_product_attribute_value( $name, $id = 0, $slug = '' ) {
return (object) [
'id' => (int) $id,
'name' => $name,
'slug' => $slug ? $slug : $name,
];
}
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$prices = [];
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
// If we have a variable product, get the price from the variations (this will use the min value).
if ( $product->is_type( 'variable' ) ) {
$regular_price = $product->get_variation_regular_price();
$sale_price = $product->get_variation_sale_price();
} else {
$regular_price = $product->get_regular_price();
$sale_price = $product->get_sale_price();
}
$prices['price'] = $this->prepare_money_response( $price_function( $product ), wc_get_price_decimals() );
$prices['regular_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $regular_price ] ), wc_get_price_decimals() );
$prices['sale_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $sale_price ] ), wc_get_price_decimals() );
$prices['price_range'] = $this->get_price_range( $product, $tax_display_mode );
return $this->prepare_currency_response( $prices );
}
/**
* WooCommerce can return prices including or excluding tax; choose the correct method based on tax display mode.
*
* @param string $tax_display_mode Provided tax display mode.
* @return string Valid tax display mode.
*/
protected function get_tax_display_mode( $tax_display_mode = '' ) {
return in_array( $tax_display_mode, [ 'incl', 'excl' ], true ) ? $tax_display_mode : get_option( 'woocommerce_tax_display_shop' );
}
/**
* WooCommerce can return prices including or excluding tax; choose the correct method based on tax display mode.
*
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return string Function name.
*/
protected function get_price_function_from_tax_display_mode( $tax_display_mode ) {
return 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax';
}
/**
* Get price range from certain product types.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return object|null
*/
protected function get_price_range( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
if ( $product->is_type( 'variable' ) ) {
$prices = $product->get_variation_prices( true );
if ( ! empty( $prices['price'] ) && ( min( $prices['price'] ) !== max( $prices['price'] ) ) ) {
return (object) [
'min_amount' => $this->prepare_money_response( min( $prices['price'] ), wc_get_price_decimals() ),
'max_amount' => $this->prepare_money_response( max( $prices['price'] ), wc_get_price_decimals() ),
];
}
}
if ( $product->is_type( 'grouped' ) ) {
$children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );
$price_function = 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax';
foreach ( $children as $child ) {
if ( '' !== $child->get_price() ) {
$child_prices[] = $price_function( $child );
}
}
if ( ! empty( $child_prices ) ) {
return (object) [
'min_amount' => $this->prepare_money_response( min( $child_prices ), wc_get_price_decimals() ),
'max_amount' => $this->prepare_money_response( max( $child_prices ), wc_get_price_decimals() ),
];
}
}
return null;
}
/**
* Returns a list of terms assigned to the product.
*
* @param \WC_Product $product Product object.
* @param string $taxonomy Taxonomy name.
* @return array Array of terms (id, name, slug).
*/
protected function get_term_list( \WC_Product $product, $taxonomy = '' ) {
if ( ! $taxonomy ) {
return [];
}
$terms = get_the_terms( $product->get_id(), $taxonomy );
if ( ! $terms || is_wp_error( $terms ) ) {
return [];
}
$return = [];
$default_category = (int) get_option( 'default_product_cat', 0 );
foreach ( $terms as $term ) {
$link = get_term_link( $term, $taxonomy );
if ( is_wp_error( $link ) ) {
continue;
}
if ( $term->term_id === $default_category ) {
continue;
}
$return[] = (object) [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'link' => $link,
];
}
return $return;
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
use Automattic\WooCommerce\Blocks\RestApi\Routes;
/**
* ShippingAddressSchema class.
*
* Provides a generic shipping address schema for composition in other schemas.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class ShippingAddressSchema extends AbstractAddressSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'shipping_address';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'shipping-address';
/**
* Convert a term object into an object suitable for the response.
*
* @param \WC_Order|\WC_Customer $address An object with shipping address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
*/
public function get_item_response( $address ) {
if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) {
if ( is_callable( [ $address, 'get_shipping_phone' ] ) ) {
$shipping_phone = $address->get_shipping_phone();
} else {
$shipping_phone = $address->get_meta( $address instanceof \WC_Customer ? 'shipping_phone' : '_shipping_phone', true );
}
return (object) $this->prepare_html_response(
[
'first_name' => $address->get_shipping_first_name(),
'last_name' => $address->get_shipping_last_name(),
'company' => $address->get_shipping_company(),
'address_1' => $address->get_shipping_address_1(),
'address_2' => $address->get_shipping_address_2(),
'city' => $address->get_shipping_city(),
'state' => $address->get_shipping_state(),
'postcode' => $address->get_shipping_postcode(),
'country' => $address->get_shipping_country(),
'phone' => $shipping_phone,
]
);
}
throw new RouteException(
'invalid_object_type',
sprintf(
/* translators: Placeholders are class and method names */
__( '%1$s requires an instance of %2$s or %3$s for the address', 'woocommerce' ),
'ShippingAddressSchema::get_item_response',
'WC_Customer',
'WC_Order'
),
500
);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Automattic\WooCommerce\Blocks\StoreApi\Schemas;
/**
* TermSchema class.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @since 2.5.0
*/
class TermSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'term';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'term';
/**
* Term properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'String based identifier for the term.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Term description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'parent' => array(
'description' => __( 'Parent term ID, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of objects (posts of any type) assigned to the term.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
];
}
/**
* Convert a term object into an object suitable for the response.
*
* @param \WP_Term $term Term object.
* @return array
*/
public function get_item_response( $term ) {
return [
'id' => (int) $term->term_id,
'name' => $this->prepare_html_response( $term->name ),
'slug' => $term->slug,
'description' => $this->prepare_html_response( $term->description ),
'parent' => (int) $term->parent,
'count' => (int) $term->count,
];
}
}