LLMS_REST_Controller
LLMS_REST_Controller class
Source Source
File: libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller.php
abstract class LLMS_REST_Controller extends LLMS_REST_Controller_Stubs { /** * Endpoint namespace. * * @var string */ protected $namespace = 'llms/v1'; /** * Schema properties available for ordering the collection. * * @var string[] */ protected $orderby_properties = array( 'id', ); /** * Whether search is allowed. * * @var boolean */ protected $is_searchable = false; /** * LLMS REST resource schema. * * @var array */ protected $schema; /** * Additional rest field names to skip (added via `register_rest_field()`). * * @var string[] */ protected $disallowed_additional_fields = array(); /** * Meta field names to skip (added via `register_meta()`). * * @var string[] */ protected $disallowed_meta_fields = array(); /** * Create an item. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.12 Call `object_inserted` and `object_completely_inserted` after an object is * respectively inserted in the DB and all its additional fields have been * updated as well (completely inserted). * @since 1.0.0-beta.27 Handle custom meta registered via `register_meta()` and custom rest fields registered via `register_rest_field()`. * * @param WP_REST_Request $request Request object. * @return WP_Error|WP_REST_Response */ public function create_item( $request ) { if ( ! empty( $request['id'] ) ) { return llms_rest_bad_request_error( __( 'Cannot create an existing resource.', 'lifterlms' ) ); } $schema = $this->get_item_schema(); $item = $this->prepare_item_for_database( $request ); // Exclude additional fields registered via `register_rest_field()`. $item = array_diff_key( $item, $this->get_additional_fields() ); $object = $this->create_object( $item, $request ); if ( is_wp_error( $object ) ) { return $object; } $this->object_inserted( $object, $request, $schema, true ); // Registered via `register_meta()`. $meta_update = $this->update_meta( $object, $request, $schema ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } // Registered via `register_rest_field()`. $fields_update = $this->update_additional_fields_for_object( $object, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $this->object_completely_inserted( $object, $request, $schema, true ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $object, $request ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $this->get_object_id( $object ) ) ) ); return $response; } /** * Called right after a resource is inserted (created/updated). * * @since 1.0.0-beta.12 * * @param object $object Inserted or updated object. * @param WP_REST_Request $request Request object. * @param array $schema The item schema. * @param bool $creating True when creating a post, false when updating. */ protected function object_inserted( $object, $request, $schema, $creating ) { $type = $this->get_object_type(); /** * Fires after a single llms resource is created or updated via the REST API. * * The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing. * * @since 1.0.0-beta.12 * * @param object $object Inserted or updated object. * @param WP_REST_Request $request Request object. * @param array $schema The item schema. * @param bool $creating True when creating a post, false when updating. */ do_action( "llms_rest_insert_{$type}", $object, $request, $schema, $creating ); } /** * Called right after a resource is completely inserted (created/updated). * * @since 1.0.0-beta.12 * * @param LLMS_Post $object Inserted or updated object. * @param WP_REST_Request $request Request object. * @param array $schema The item schema. * @param bool $creating True when creating a post, false when updating. */ protected function object_completely_inserted( $object, $request, $schema, $creating ) { $type = $this->get_object_type(); /** * Fires after a single llms resource is completely created or updated via the REST API. * * The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing. * * @since 1.0.0-beta.12 * * @param object $object Inserted or updated object. * @param WP_REST_Request $request Request object. * @param array $schema The item schema. * @param bool $creating True when creating a post, false when updating. */ do_action( "llms_rest_after_insert_{$type}", $object, $request, $schema, $creating ); } /** * Delete the item. * * @since 1.0.0-beta.1 * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $object = $this->get_object( $request['id'], false ); // We don't return 404s for items that are not found. if ( ! is_wp_error( $object ) ) { // If there was an error deleting the object return the error. If the error is that the object doesn't exist return 204 below! $del = $this->delete_object( $object, $request ); if ( is_wp_error( $del ) ) { return $del; } } $response = rest_ensure_response( null ); $response->set_status( 204 ); return $response; } /** * Retrieves the query params for the objects collection. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.12 Added `search_columns` collection param for searchable resources. * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; // We're not currently implementing searching for all of our controllers. if ( empty( $this->is_searchable ) ) { unset( $query_params['search'] ); } elseif ( ! empty( $this->search_columns_mapping ) ) { $search_columns = array_keys( $this->search_columns_mapping ); $query_params['search_columns'] = array( 'description' => __( 'Column names to be searched. Accepts a single column or a comma separated list of columns.', 'lifterlms' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => $search_columns, ), 'default' => $search_columns, ); } // page and per_page params are already specified in WP_Rest_Controller->get_collection_params(). $query_params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'lifterlms' ), 'type' => 'string', 'default' => 'asc', 'enum' => array( 'asc', 'desc' ), 'validate_callback' => 'rest_validate_request_arg', ); $query_params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'lifterlms' ), 'type' => 'string', 'default' => $this->orderby_properties[0], 'enum' => $this->orderby_properties, 'validate_callback' => 'rest_validate_request_arg', ); $query_params['include'] = array( 'description' => __( 'Limit results to a list of ids. Accepts a single id or a comma separated list of ids.', 'lifterlms' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'validate_callback' => 'rest_validate_request_arg', ); $query_params['exclude'] = array( 'description' => __( 'Exclude a list of ids from results. Accepts a single id or a comma separated list of ids.', 'lifterlms' ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'validate_callback' => 'rest_validate_request_arg', ); return $query_params; } /** * Get a single item. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.27 Don't call `rest_ensure_response()` twice, already called in `$this->prepare_item_for_response()`. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $object = $this->get_object( (int) $request['id'] ); if ( is_wp_error( $object ) ) { return $object; } $response = $this->prepare_item_for_response( $object, $request ); return $response; } /** * Retrieves all items. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.3 Fix an issue displaying a last page for lists with 0 possible results. * @since 1.0.0-beta.7 Broken into several methods so to improve abstraction. * @since 1.0.0-beta.12 Return early if `prepare_collection_query_args()` is a `WP_Error`. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { $prepared = $this->prepare_collection_query_args( $request ); if ( is_wp_error( $prepared ) ) { return $prepared; } $query = $this->get_objects_query( $prepared, $request ); $pagination = $this->get_pagination_data_from_query( $query, $prepared, $request ); // Out-of-bounds, run the query again on page one to get a proper total count. if ( $pagination['total_results'] < 1 ) { $prepared_for_total_count = $this->prepare_args_for_total_count_query( $prepared, $request ); $count_query = $this->get_objects_query( $prepared_for_total_count, $request ); $count_results = $this->get_pagination_data_from_query( $count_query, $prepared_for_total_count, $request ); $pagination['total_results'] = $count_results['total_results']; } if ( $pagination['current_page'] > $pagination['total_pages'] && $pagination['total_results'] > 0 ) { return llms_rest_bad_request_error( __( 'The page number requested is larger than the number of pages available.', 'lifterlms' ) ); } $objects = $this->get_objects_from_query( $query ); $items = $this->prepare_collection_items_for_response( $objects, $request ); $response = rest_ensure_response( $items ); $response = $this->add_header_pagination( $response, $pagination, $request ); return $response; } /** * Format query arguments to retrieve a collection of objects. * * @since 1.0.0-beta.7 * @since 1.0.0-beta.12 Prepare args for search and call collection params to query args map method. * * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error */ protected function prepare_collection_query_args( $request ) { // Prepare all set args. $registered = $this->get_collection_params(); $prepared = array(); foreach ( $registered as $key => $value ) { if ( isset( $request[ $key ] ) ) { $prepared[ $key ] = $request[ $key ]; } } $prepared = $this->prepare_collection_query_search_args( $prepared, $request ); if ( is_wp_error( $prepared ) ) { return $prepared; } $prepared = $this->map_params_to_query_args( $prepared, $registered, $request ); return $prepared; } /** * Map schema to query arguments to retrieve a collection of objects. * * @since 1.0.0-beta.12 * * @param array $prepared Array of collection arguments. * @param array $registered Registered collection params. * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error */ protected function map_params_to_query_args( $prepared, $registered, $request ) { return $prepared; } /** * Format search query arguments to retrieve a collection of objects. * * @since 1.0.0-beta.12 * @since 1.0.0-beta.21 Return an error if requesting a list ordered by 'relevance' without providing a search string. * * @param array $prepared Array of collection arguments. * @param WP_REST_Request $request Request object. * @return array|WP_Error */ protected function prepare_collection_query_search_args( $prepared, $request ) { // Search? if ( ! empty( $prepared['search'] ) ) { if ( ! empty( $this->search_columns_mapping ) ) { if ( empty( $prepared['search_columns'] ) ) { return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) ); } // Filter search columns by context. $search_columns = array_keys( $this->filter_response_by_context( array_flip( $prepared['search_columns'] ), $request['context'] ) ); // Check if one of more unallowed search columns have been provided as request query params (not merged with defaults). if ( ! empty( $request->get_query_params()['search_columns'] ) ) { $forbidden_columns = array_diff( $prepared['search_columns'], $search_columns ); if ( ! empty( $forbidden_columns ) ) { return llms_rest_authorization_required_error( sprintf( // Translators: %1$s comma separated list of search columns. __( 'You are not allowed to search into the provided column(s): %1$s', 'lifterlms' ), implode( ',', $forbidden_columns ) ) ); } } $prepared['search_columns'] = array(); // Map our search columns into query compatible ones. foreach ( $search_columns as $search_column ) { if ( isset( $this->search_columns_mapping[ $search_column ] ) ) { $prepared['search_columns'][] = $this->search_columns_mapping[ $search_column ]; } } if ( empty( $prepared['search_columns'] ) ) { return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) ); } } $prepared['search'] = '*' . $prepared['search'] . '*'; } else { // Ensure a search string is set in case the orderby is set to 'relevance'. if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] ) { return llms_rest_bad_request_error( __( 'You need to define a search term to order by relevance.', 'lifterlms' ) ); } } return $prepared; } /** * Prepare query args for total count query. * * @since 1.0.0-beta.7 * * @param array $args Array of query args. * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_args_for_total_count_query( $args, $request ) { // Run the query again without pagination to get a proper total count. unset( $args['paged'], $args['page'] ); return $args; } /** * Prepare collection items for response. * * @since 1.0.0-beta.7 * * @param array $objects Array of objects to be prepared for response. * @param WP_REST_Request $request Full details about the request. * @return array */ protected function prepare_collection_items_for_response( $objects, $request ) { $items = array(); foreach ( $objects as $object ) { $object = $this->get_object( $object, false ); if ( ! $this->check_read_object_permissions( $object ) ) { continue; } $item = $this->prepare_item_for_response( $object, $request ); if ( ! is_wp_error( $item ) ) { $items[] = $this->prepare_response_for_collection( $item ); } } return $items; } /** * Add pagination info and links to the response header. * * @since 1.0.0-beta.7 * * @param WP_REST_Response $response Current response being served. * @param array $pagination Pagination array. * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response */ protected function add_header_pagination( $response, $pagination, $request ) { $response->header( 'X-WP-Total', $pagination['total_results'] ); $response->header( 'X-WP-TotalPages', $pagination['total_pages'] ); $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $request->get_route() ) ); // First page link. if ( 1 !== $pagination['current_page'] ) { $first_link = add_query_arg( 'page', 1, $base ); $response->link_header( 'first', $first_link ); } // Previous page link. if ( $pagination['current_page'] > 1 ) { $prev_page = $pagination['current_page'] - 1; if ( $prev_page > $pagination['total_pages'] ) { $prev_page = $pagination['total_pages']; } $prev_link = add_query_arg( 'page', $prev_page, $base ); $response->link_header( 'prev', $prev_link ); } // Next page link. if ( $pagination['total_pages'] > $pagination['current_page'] ) { $next_link = add_query_arg( 'page', $pagination['current_page'] + 1, $base ); $response->link_header( 'next', $next_link ); } // Last page link. if ( $pagination['total_pages'] && $pagination['total_pages'] !== $pagination['current_page'] ) { $last_link = add_query_arg( 'page', $pagination['total_pages'], $base ); $response->link_header( 'last', $last_link ); } return $response; } /** * Retrieves the query params for retrieving a single resource. * * @since 1.0.0-beta.1 * * @return array */ public function get_get_item_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view', ) ), ); } /** * Retrieve arguments for deleting a resource. * * @since 1.0.0-beta.1 * * @return array */ public function get_delete_item_args() { return array(); } /** * Map request keys to database keys for insertion. * * Array keys are the request fields (as defined in the schema) and * array values are the database fields. * * @since 1.0.0-beta.1 * * @return array */ protected function map_schema_to_database() { $schema = $this->get_item_schema(); $keys = array_keys( $schema['properties'] ); return array_combine( $keys, $keys ); } /** * Get the LLMS REST resource schema, conforming to JSON Schema. * * @since 1.0.0-beta.27 * * @return array */ public function get_item_schema() { if ( isset( $this->schema ) ) { // Additional fields are not cached in the schema, @see https://core.trac.wordpress.org/ticket/47871#comment:5. return $this->add_additional_fields_schema( $this->schema ); } $schema = $this->get_item_schema_base(); // Add custom fields registered via `register_meta()`. $schema = $this->add_meta_fields_schema( $schema ); // Allow the schema to be filtered. $schema = $this->filter_item_schema( $schema ); /** * Set `$this->schema` here so that we can exclude additional fields * already covered by the base, filtered, schema. * * Additional fields are added through the call to add_additional_fields_schema() below, * which will call {@see LLMS_REST_Controller::get_additional_fields()}, which requires * `$this->schema` to be set. */ $this->schema = $schema; /** * Adds the schema from additional fields (registered via `register_rest_field()`) to the schema array. * Note: WordPress core doesn't cache the additional fields in the schema, see https://core.trac.wordpress.org/ticket/47871#comment:5 */ return $this->add_additional_fields_schema( $this->schema ); } /** * Add custom fields registered via `register_meta()`. * * @since 1.0.0-beta.27 * * @param array $schema The resource item schema. * @return array */ protected function add_meta_fields_schema( $schema ) { if ( ! empty( $this->meta ) ) { $schema['properties']['meta'] = $this->meta->get_field_schema(); $schema['properties']['meta']['properties'] = $this->exclude_disallowed_meta_fields( $schema['properties']['meta']['properties'], $schema ); } return $schema; } /** * Retrieves all of the registered additional fields for a given object-type. * * Overrides wp core `get_additional_fields()` to allow excluding fields. * * @since 1.0.0-beta.27 * * @param string $object_type The object type. * @return array Registered additional fields (if any), empty array if none or if the object type could * not be inferred. */ protected function get_additional_fields( $object_type = null ) { // We require the `$this->schema['properties']` to be set in order to exclude additional fields already covered by our schema definition. if ( ! isset( $this->schema['properties'] ) ) { return parent::get_additional_fields( $object_type ); } $additional_fields = parent::get_additional_fields( $object_type ); if ( ! empty( $additional_fields ) ) { /** * Filters the disallowed additional fields. * * The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing. * * @since 1.0.0-beta.27 * * @param string[] $disallowed_additional_fields Additional rest field names to skip (added via `register_rest_field()`). */ $disallowed_fields = apply_filters( "llms_rest_{$object_type}_disallowed_additional_fields", $this->disallowed_additional_fields ); /** * Exclude: * - disallowed fields defined in the instance's property `disallowed_additional_fields`. * - additional fields already covered in the schema. * * This is meant to run only once, because otherwise we have no way * to determine whether the property comes from the original schema * definition, or has been added via `register_rest_field()`. */ $additional_fields = array_diff_key( $additional_fields, array_flip( array_keys( $this->schema['properties'] ) ), array_flip( $disallowed_fields ) ); } return $additional_fields; } /** * Exclude disallowed meta fields. * * @since 1.0.0-beta.27 * * @param array $meta Array of meta fields. * @param array $schema The resource item schema. Falls back on the defined schema if not supplied. * @return array */ protected function exclude_disallowed_meta_fields( $meta, $schema = array() ) { $schema = empty( $schema ) ? $this->schema : $schema; if ( empty( $schema ) || empty( $meta ) ) { return $meta; } $object_type = $this->get_object_type( $schema ); /** * Filters the disallowed meta fields. * * The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing. * * @since 1.0.0-beta.27 * * @param string[] $disallowed_meta_fields Meta field names to skip (added via `register_meta()`). */ $disallowed_meta_fields = apply_filters( "llms_rest_{$object_type}_disallowed_meta_fields", $this->disallowed_meta_fields ); $meta = array_diff_key( $meta, array_flip( $disallowed_meta_fields ) ); return $meta; } /** * Get the LLMS REST resource schema base properties, conforming to JSON Schema. * * @since 1.0.0-beta.27 * * @return array */ protected function get_item_schema_base() { return array(); }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- add_header_pagination — Add pagination info and links to the response header.
- create_item — Create an item.
- delete_item — Delete the item.
- get_collection_params — Retrieves the query params for the objects collection.
- get_delete_item_args — Retrieve arguments for deleting a resource.
- get_get_item_params — Retrieves the query params for retrieving a single resource.
- get_item — Get a single item.
- get_items — Retrieves all items
- map_params_to_query_args — Map schema to query arguments to retrieve a collection of objects.
- map_schema_to_database — Map request keys to database keys for insertion.
- object_completely_inserted — Called right after a resource is completely inserted (created/updated).
- object_inserted — Called right after a resource is inserted (created/updated).
- prepare_args_for_total_count_query — Prepare query args for total count query.
- prepare_collection_items_for_response — Prepare collection items for response.
- prepare_collection_query_args — Format query arguments to retrieve a collection of objects.
- prepare_collection_query_search_args — Format search query arguments to retrieve a collection of objects.
- prepare_item_for_database — Prepare request arguments for a database insert/update.
- prepare_item_for_response — Prepares a single object for response.
- prepare_links — Prepare links for the request.
- register_routes — Register routes.
- update_item — Update item.
Changelog Changelog
Version | Description |
---|---|
1.0.0-beta.7 | Break get_items() method into prepare_collection_query_args() , prepare_args_for_total_count_query() , prepare_collection_items_for_response() and add_header_pagination() methods so to improve abstraction. prepare_objects_query() renamed to prepare_collection_query_args() . |
1.0.0-beta.3 | Fix an issue displaying a last page for lists with 0 possible results & handle error conditions early in responses. |
1.0.0-beta.14 | Update prepare_links() to accept a second parameter, WP_REST_Request . |
1.0.0-beta.12 | Added logic to perform a collection search. Added object_inserted() and object_completely_inserted() methods called after an object is respectively inserted in the DB and all its additional fields have been updated as well (completely inserted). |
1.0.0-beta.1 | Introduced. |