How to improve this code that adds custom route to wordpress rest api

Recently I completed an assignment and the task was to add a custom route on top of the wordpress rest API. I have had some experience with rest API in Laravel but not in WordPress.

My code is posted below. I thought I did a good job but it didn't meet the mark.

Any expert on API, better on WordPress rest API, please help me to improve this bit of code. I will be really grateful to you all. Thanks.

/*-------------------------------------------*/
    /* Adjust the following code */
    /*-------------------------------------------*/

    /**
     * Class My_Rest_API_Thing
     *
     * @package My_Rest_API
     */

    /**
     * Class My_Rest_API_Thing
     *
     * Objective: Familiarity with WP_REST_X
     * Code has to work in PHP 7.3 and WP 5.7.2 as environment
     *
     * API Properties
     * Comes from WP option, named "my_thing_option", use get_option and update_option
     * to manage
     * "enum": has to be string, with accepted values: "good", "bad" OR "" ( empty string )
     * "string": has to be Alphanumeric case insensitive, no spaces, no other symbols
     * characters, with maximum 64 characters
     * "list": has to be array of integer with maximum 5 items, and must be unique, with each
     * item has to be between 1-10
     * - On view / GET, "list" has to be ordered / sorted
     * - On Update, "list" parameter order is optional, but response should still be ordered /
     * sorted, regardless of request parameter's order
     *
     * Authentication and Authorization
     * - Anybody, either logged in or not, can view / GET
     * - Only user that is logged in AND ( has "administrator" role OR user that has
     * `update_my_thing` WP capability ) can update / PUT, otherwise 401 AND 403 should be
     * thrown respectively. With error code `rest_forbidden`
     *
     * Validation preferably using default WP core REST built in functions.
     * rest_invalid_param, with 400 status code
     * Expected response would be either error or a JSON object, like this
     * {
     * "enum": "good",
     * "string": "mySt3eRing",
     * "list": [2, 4, 5],
     * }
     *
     * Tricky cases :
     * When update / PUT requested with only one property attached, other properties
     * should not be updated
     * Example :
     * - Initial State :
     * {
     * "enum": "good",
     * "string": "mySt3eRing",
     * "list": [2, 4, 5],
     * }
     * - Update request :
     * {
     * "enum": "bad"
     * }
     * - Expected result state :
     * {
     * "enum": "bad",
     * "string": "mySt3eRing",
     * "list": [2, 4, 5],
     * }
     *
     * When update / PUT requested passing unrecognized properties, accept the request
     * but dont update or add it into MY_Thing object
     * - Initial State :
     * {
     * "enum": "good",
     * "string": "mySt3Ring",
     * "list": [2, 4, 5],
     * }
     * - Update request :
     * {
     * "unknown": "stuff"
     * }
     * - Expected result state :
     * {
     * "enum": "good",
     * "string": "mySt3eRing",
     * "list": [2, 4, 5],
     * }
     *
     * _fields request should also be taken care of.
     * https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/#_fields
     *
     * First time GET has to return full object properties with default empty values based on
     * type respectively. Not an empty array / object
     * {
     * "enum": "",
     * "string": "",
     * "list": [],
     * }
     *
     * A BIG plus if codes following this:
     * https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/
     */
    class My_Rest_API_Thing extends WP_REST_Controller {
        /**
         * My_Rest_API_Thing constructor.
         */
        public function __construct() {
            $this->namespace = 'my-api/v1';
            $this->rest_base = 'my-thing';
            add_action( 'rest_api_init', array( $this, 'register_routes' ) );
        }

        /**
         * Register routes
         */
        public function register_routes() {
            register_rest_route(
                $this->namespace,
                '/' . $this->rest_base,
                array(
                    array(
                        'methods'             => WP_REST_Server::READABLE,
                        'callback'            => array( $this, 'get_item' ),
                        'permission_callback' => array( $this, 'get_item_permissions_check' ),
                    ),
                    array(
                        'methods'             => WP_REST_Server::EDITABLE,
                        'callback'            => array( $this, 'update_item' ),
                        'permission_callback' => array( $this, 'update_item_permissions_check' )
                    ),
                    'schema' => array( $this, 'get_public_item_schema' ),
                )
            );
        }

        /**
         * Permission check for GET
         *
         * @param WP_REST_Request $request request object.
         *
         * @return true|WP_Error
         */
        public function get_item_permissions_check( $request ) {
            // everyone can read
            return true;
        }

        /**
         * Permissions check for PUT
         *
         * @param WP_REST_Request $request request object.
         *
         * @return true|WP_Error
         */
        public function update_item_permissions_check( $request ) {
            $user = wp_get_current_user();

            return in_array( 'administrator', $user->roles ) || current_user_can( 'update_my_thing' );
        }

        /**
         * Get my-thing
         *
         * @param WP_REST_Request $request request object.
         *
         * @return WP_Error|WP_REST_Response
         */
        public function get_item( $request ) {
            $data = get_option( 'my_thing_option', $this->get_default_data() );

            return $this->prepare_item_for_response( $data, $request );
        }

        /**
         * Formatting the response
         *
         * @param array $item my-thing.
         * @param WP_REST_Request $request request object.
         *
         * @return WP_Error|WP_REST_Response
         */
        public function prepare_item_for_response( $item, $request ) {
            $allowed_keys = array_keys( $this->get_item_schema()['properties'] );

            // if _fields is present in request, then we will use it to filter data
            if ( isset( $request['_fields'] ) ) {
                // fields can be in form: _fields=enum,string & _fields[]=enum&_fields[]=string
                // so make sure it's always in array form
                if ( ! is_array( $request['_fields'] ) ) {
                    $request['_fields'] = explode( ',', $request['_fields'] );
                }

                // only _fields that are in allowed keys are valid
                $allowed_keys = array_filter( $request['_fields'], function ( $field ) use ( $allowed_keys ) {
                    return in_array( $field, $allowed_keys );
                } );
            }

            // Actually, in this case, this whole filter thing is not necessary as
            // it is pretty simple and WordPress handles it automatically
            return rest_ensure_response( $this->filter_array_by_keys( $item, $allowed_keys ) );
        }

        /**
         * Get schema structure
         *
         * @return array
         */
        public function get_item_schema() {
            if ( $this->schema ) {
                return $this->add_additional_fields_schema( $this->schema );
            }

            $this->schema = array(
                '$schema'    => 'https://json-schema.org/draft-04/schema#',
                'title'      => 'my_thing_option',
                'type'       => 'object',
                'properties' => array(
                    'enum'   => array(
                        'type'    => 'string',
                        'enum'    => array( '', 'good', 'bad' ),
                        'default' => ''
                    ),
                    'string' => array(
                        'type'      => 'string',
                        'maxLength' => 64,
                        'pattern'   => '^[A-Za-z0-9]*$',
                        'default'   => ''

                    ),
                    'list'   => array(
                        'type'        => 'array',
                        'items'       => array(
                            'type'    => 'integer',
                            'minimum' => 1,
                            'maximum' => 10
                        ),
                        'maxItems'    => 5,
                        'uniqueItems' => true,
                        'default'     => array()
                    ),
                ),
            );

            return $this->add_additional_fields_schema( $this->schema );
        }

        /**
         * Filter array by keys
         *
         * Works on first level keys only
         *
         * @param $array
         * @param $keys
         *
         * @return array filtered array
         */
        protected function filter_array_by_keys( $array, $keys ) {
            // $keys = ['enum', 'string', 'list'];
            $filtered_array = array();
            foreach ( $array as $key => $value ) {
                if ( in_array( $key, $keys ) ) {
                    $filtered_array[ $key ] = $value;
                }
            }

            return $filtered_array;
        }

        /**
         * Update my-thing
         *
         * @param WP_REST_Request $request request-object.
         *
         * @return WP_Error|WP_REST_Response
         */
        public function update_item( $request ) {
            // validate
            $validation_response = $this->validate_data( $request );
            if ( $validation_response instanceof WP_Error ) {
                return $validation_response;
            }

            // prepare
            $prepared_data = $this->prepare_item_for_database( $request );

            // sanitize
            $sanitization_response = $this->sanitize_data( $prepared_data );
            if ( $sanitization_response instanceof WP_Error ) {
                return $sanitization_response;
            }

            // update
            update_option( 'my_thing_option', $sanitization_response );

            // return updated data
            return $this->prepare_item_for_response( get_option( 'my_thing_option' ), $request );
        }

        /**
         * Schema for public
         *
         * This is the place where you want to unset items from schema that are of no use to
         * a third party application using this api
         *
         * @return array
         */
        public function get_public_item_schema() {
            $schema = $this->get_item_schema();

            if ( ! empty( $schema['properties'] ) ) {
                foreach ( $schema['properties'] as &$property ) {
                    unset( $property['arg_options'] );
                }
            }

            return $schema;
        }

        /**
         * Validate post data against defined schema
         *
         * @param WP_REST_Request $request Request Object
         *
         * @return bool|WP_Error true if valid, else return error
         */
        protected function validate_data( $request ) {
            //initialize
            $received_data = $request->get_json_params() ? $request->get_json_params() : $request->get_body_params();
            $properties    = $this->get_item_schema()['properties'];

            // validate data
            foreach ( $received_data as $key => $value ) {
                if ( isset( $properties[ $key ] ) ) {
                    $res = rest_validate_value_from_schema( $value, $properties[ $key ], $key );

                    // if invalid, return error
                    if ( $res instanceof WP_Error ) {
                        return new WP_Error(
                            'rest_invalid_param',
                            $res->get_error_message(),
                            array( 'status' => 400 )
                        );
                    }
                }
            }

            // if it arrives here, then the data is valid
            return true;
        }

        /**
         * Prepares one item for create or update operation.
         *
         * @param WP_REST_Request $request Request object.
         *
         * @return array|WP_Error The prepared item, or WP_Error object on failure.
         */
        protected function prepare_item_for_database( $request ) {
            // initialize
            $allowed_keys = array_keys( $this->get_item_schema()['properties'] );
            $data         = get_option( 'my_thing_option', $this->get_default_data() );

            // filter data
            $received_data = $request->get_json_params() ? $request->get_json_params() : $request->get_body_params();
            $filtered_data = $this->filter_array_by_keys( $received_data, $allowed_keys );

            // sort items in list
            if ( isset( $filtered_data['list'] ) ) {
                sort( $filtered_data['list'], 1 );
            }

            // update new values
            foreach ( $filtered_data as $key => $value ) {
                $data[ $key ] = $value;
            }

            // return prepared data
            return $data;
        }

        /**
         * Sanitize data against defined schema
         *
         * @param $data array of data to be sanitized
         *
         * @return array|WP_Error sanitized data or error
         */
        protected function sanitize_data( $data ) {
            // initialize
            $sanitized_data = array();
            $properties     = $this->get_item_schema()['properties'];

            // sanitize
            foreach ( $data as $key => $value ) {
                if ( isset( $properties[ $key ] ) ) {
                    $res = rest_sanitize_value_from_schema( $value, $properties[ $key ], $key );

                    // if unable to sanitize safely, return error
                    if ( $res instanceof WP_Error ) {
                        return new WP_Error(
                            'rest_invalid_param',
                            $res->get_error_message(),
                            array( 'status' => 400 )
                        );
                    }

                    $sanitized_data[ $key ] = $res;
                }
            }

            // return sanitized data
            return $sanitized_data;
        }

        /**
         * Generate default data
         *
         * @return array
         */
        protected function get_default_data() {
            $default_data = array();
            $properties   = $this->get_item_schema()['properties'];

            foreach ( $properties as $key => $value ) {
                $default_data[ $key ] = $value['default'] ?? null;
            }

            return $default_data;
        }

    }

    /**
     * Register our custom routes
     */
    add_action( 'rest_api_init', function () {
        $controller = new My_Rest_API_Thing();
        $controller->register_routes();
    } );
How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum