<?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartItemSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; use Automattic\WooCommerce\Blocks\StoreApi\Formatters; use Throwable; use Exception; /** * Service class to provide utility functions to extend REST API. */ final class ExtendRestApi { /** * Holds the Package instance * * @var Package */ private $package; /** * Holds the formatters class instance. * * @var Formatters */ private $formatters; /** * Constructor * * @param Package $package An instance of the package class. * @param Formatters $formatters An instance of the formatters class. */ public function __construct( Package $package, Formatters $formatters ) { $this->package = $package; $this->formatters = $formatters; } /** * Returns a formatter instance. * * @param string $name Formatter name. * @return FormatterInterface */ public function get_formatter( $name ) { return $this->formatters->$name; } /** * Valid endpoints to extend * * @var array */ private $endpoints = [ CartItemSchema::IDENTIFIER, CartSchema::IDENTIFIER ]; /** * Data to be extended * * @var array */ private $extend_data = []; /** * Array of payment requirements * * @var array */ private $payment_requirements = []; /** * An endpoint that validates registration method call * * @param array $args { * An array of elements that make up a post to update or insert. * * @type string $endpoint The endpoint to extend. * @type string $namespace Plugin namespace. * @type callable $schema_callback Callback executed to add schema data. * @type callable $data_callback Callback executed to add endpoint data. * @type string $data_type The type of data, object or array. * } * * @throws Exception On failure to register. * @return boolean True on success. */ public function register_endpoint_data( $args ) { if ( ! is_string( $args['namespace'] ) ) { $this->throw_exception( 'You must provide a plugin namespace when extending a Store REST endpoint.' ); } if ( ! is_string( $args['endpoint'] ) || ! in_array( $args['endpoint'], $this->endpoints, true ) ) { $this->throw_exception( sprintf( 'You must provide a valid Store REST endpoint to extend, valid endpoints are: %1$s. You provided %2$s.', implode( ', ', $this->endpoints ), $args['endpoint'] ) ); } if ( ! is_callable( $args['schema_callback'] ) ) { $this->throw_exception( '$schema_callback must be a callable function.' ); } if ( ! is_callable( $args['data_callback'] ) ) { $this->throw_exception( '$data_callback must be a callable function.' ); } if ( isset( $args['data_type'] ) && ! in_array( $args['data_type'], [ ARRAY_N, ARRAY_A ], true ) ) { $this->throw_exception( sprintf( 'Data type must be either ARRAY_N for a numeric array or ARRAY_A for an object like array. You provided %1$s.', $args['data_type'] ) ); } $this->extend_data[ $args['endpoint'] ][ $args['namespace'] ] = [ 'schema_callback' => $args['schema_callback'], 'data_callback' => $args['data_callback'], 'data_type' => isset( $args['data_type'] ) ? $args['data_type'] : ARRAY_A, ]; return true; } /** * Returns the registered endpoint data * * @param string $endpoint A valid identifier. * @param array $passed_args Passed arguments from the Schema class. * @return object Returns an casted object with registered endpoint data. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_endpoint_data( $endpoint, array $passed_args = [] ) { $registered_data = []; if ( ! isset( $this->extend_data[ $endpoint ] ) ) { return (object) $registered_data; } foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { $data = []; try { $data = $callbacks['data_callback']( ...$passed_args ); if ( ! is_array( $data ) ) { throw new Exception( '$data_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $registered_data[ $namespace ] = $data; } return (object) $registered_data; } /** * Returns the registered endpoint schema * * @param string $endpoint A valid identifier. * @param array $passed_args Passed arguments from the Schema class. * @return array Returns an array with registered schema data. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_endpoint_schema( $endpoint, array $passed_args = [] ) { $registered_schema = []; if ( ! isset( $this->extend_data[ $endpoint ] ) ) { return (object) $registered_schema; } foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { $schema = []; try { $schema = $callbacks['schema_callback']( ...$passed_args ); if ( ! is_array( $schema ) ) { throw new Exception( '$schema_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $schema = $this->format_extensions_properties( $namespace, $schema, $callbacks['data_type'] ); $registered_schema[ $namespace ] = $schema; } return (object) $registered_schema; } /** * Registers and validates payment requirements callbacks. * * @param array $args { * Array of registration data. * * @type callable $data_callback Callback executed to add payment requirements data. * } * * @throws Exception On failure to register. * @return boolean True on success. */ public function register_payment_requirements( $args ) { if ( ! is_callable( $args['data_callback'] ) ) { $this->throw_exception( '$data_callback must be a callable function.' ); } $this->payment_requirements[] = $args['data_callback']; return true; } /** * Returns the additional payment requirements. * * @param array $initial_requirements list of requirements that should be added to the collected requirements. * @return array Returns a list of payment requirements. * @throws Exception If a registered callback throws an error, or silently logs it. */ public function get_payment_requirements( array $initial_requirements = [ 'products' ] ) { $requirements = $initial_requirements; if ( empty( $this->payment_requirements ) ) { return $initial_requirements; } foreach ( $this->payment_requirements as $callback ) { $data = []; try { $data = $callback(); if ( ! is_array( $data ) ) { throw new Exception( '$data_callback must return an array.' ); } } catch ( Throwable $e ) { $this->throw_exception( $e ); continue; } $requirements = array_merge( $requirements, $data ); } return array_unique( $requirements ); } /** * Throws error and/or silently logs it. * * @param string|Throwable $exception_or_error Error message or Exception. * @throws Exception An error to throw if we have debug enabled and user is admin. */ private function throw_exception( $exception_or_error ) { if ( is_string( $exception_or_error ) ) { $exception = new Exception( $exception_or_error ); } else { $exception = $exception_or_error; } // Always log an error. wc_caught_exception( $exception ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'manage_woocommerce' ) ) { throw $exception; } } /** * Format schema for an extension. * * @param string $namespace Error message or Exception. * @param array $schema An error to throw if we have debug enabled and user is admin. * @param string $data_type How should data be shaped. * * @return array Formatted schema. */ private function format_extensions_properties( $namespace, $schema, $data_type ) { if ( ARRAY_N === $data_type ) { return [ /* translators: %s: extension namespace */ 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), 'type' => [ 'array', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => $schema, ]; } return [ /* translators: %s: extension namespace */ 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), 'type' => [ 'object', 'null' ], 'context' => [ 'view', 'edit' ], 'readonly' => true, 'properties' => $schema, ]; } }