├── .gitignore ├── README.md ├── boxybird-wp-query-endpoint.php ├── composer.json ├── index.php └── src └── Query.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP_Query as a REST API Endpoint 2 | 3 | Query anything you want from the WordPress database using a single REST API endpoint. 4 | 5 | **Simply put, this plugin allows to pass GET params from a url into the `WP_Query` class as `$args`** 6 | 7 | > `WP_Query` Reference: https://developer.wordpress.org/reference/classes/wp_query/ 8 | 9 | ![wp_query in javascript](https://user-images.githubusercontent.com/18317878/100775094-a5e1c300-33b7-11eb-99a7-4c14c16d21e1.png) 10 | 11 | ## Installation 12 | 13 | Clone or Download plugin and run `composer install` before activating in WordPress Admin. 14 | 15 | ## Usage Examples 16 | 17 | ### PHP 18 | 19 | Normally in a WordPress theme or plugin you would create an array of $args and pass them into the `WP_Query($args)` constructor. Like this: 20 | 21 | ```php 22 | 'post', 26 | 'orderby' => 'title', 27 | 'posts_per_page' => 12, 28 | 'category__in' => [31, 12, 4], 29 | //... any other WP_Query args you want. 30 | ]; 31 | 32 | $query = new WP_Query($args); 33 | 34 | // loop through results in PHP file. 35 | ``` 36 | 37 | Using this plugin, you can pass those same args from your front-end Javascript: 38 | 39 | ### Vanilla JS 40 | 41 | ```js 42 | const url = 'https://your-site.com/wp-json/boxybird/wp-query/v1/args?post_type=post&orderby=title&posts_per_page=12&category__in[]=31&category__in[]=12&category__in[]=4' 43 | 44 | fetch(url) 45 | .then(res => res.json()) 46 | .then(data => { 47 | console.log(data) 48 | }) 49 | 50 | ``` 51 | 52 | ### jQuery 53 | 54 | ```js 55 | const params = jQuery.param({ 56 | post_type: 'post', 57 | orderby: 'title', 58 | posts_per_page: 12, 59 | category__in: [31, 12, 4], 60 | //... any other WP_Query args you want. 61 | }); 62 | 63 | jQuery.get(`https:/your-site.com/wp-json/boxybird/wp-query/v1/args?${params}`).done((data) => { 64 | console.log(data); 65 | }); 66 | ``` 67 | 68 | ## Hooks 69 | 70 | > The examples below will use a hypothetical site with a 'movie' post_type. 71 | > 72 | 73 | ### Formatting the JSON response 74 | 75 | Out of the box, `WP_Query` will return the raw rows from the `wp_posts` table. Like this: 76 | 77 | ```json 78 | { 79 | "success": true, 80 | "data": [ 81 | { 82 | "ID": 569, 83 | "post_author": "1", 84 | "post_date": "2020-11-23 00:17:16", 85 | "post_date_gmt": "2020-11-23 00:17:16", 86 | "post_content": "An eclectic foursome of aspiring teenage witches get more than they bargained for as they lean into their newfound powers.", 87 | "post_title": "The Craft: Legacy", 88 | "post_excerpt": "", 89 | "post_status": "publish", 90 | "comment_status": "closed", 91 | "ping_status": "closed", 92 | "post_password": "", 93 | "post_name": "the-craft-legacy", 94 | "to_ping": "", 95 | "pinged": "", 96 | "post_modified": "2020-11-23 00:17:16", 97 | "post_modified_gmt": "2020-11-23 00:17:16", 98 | "post_content_filtered": "", 99 | "post_parent": 0, 100 | "guid": "https://wp-query.andrewrhyand.com/movies/the-craft-legacy", 101 | "menu_order": 0, 102 | "post_type": "movie", 103 | "post_mime_type": "", 104 | "comment_count": "0", 105 | "filter": "raw" 106 | }, 107 | { 108 | "ID": 567, 109 | "post_author": "1", 110 | "post_date": "2020-11-23 00:17:16", 111 | "...and so on" 112 | } 113 | ] 114 | } 115 | ``` 116 | 117 | The above may be useful in some situations, but more often than not you'll likely want to format the JSON response. This is the filter to do it: 118 | 119 | ```php 120 | add_filter('boxybird/query/format-response', function (WP_Query $query) { 121 | // do something with $query and return. 122 | }); 123 | ``` 124 | 125 | Here's an example of how you could use the filter: 126 | 127 | ```php 128 | add_filter('boxybird/query/format-response', function (WP_Query $query) { 129 | // Assign queried 'post_type' 130 | $post_type = strtolower($query->query_vars['post_type']); 131 | 132 | // If it's a 'movie' post_type, format like this: 133 | if ($post_type === 'movie') { 134 | return array_map(function ($movie) { 135 | return [ 136 | 'id' => $movie->ID, 137 | 'title' => get_the_title($movie->ID), 138 | 'description' => get_the_content(null, false, $movie->ID), 139 | 'link' => get_the_permalink($movie->ID), 140 | 'genres' => array_map(function ($term) { 141 | return $term->name; 142 | }, get_the_terms($movie->ID, 'genre')), 143 | 'details' => array_map(function ($detail) { 144 | return $detail[0]; 145 | }, get_post_meta($movie->ID)), 146 | 'description' => [ 147 | 'short' => wp_trim_words(get_the_content(null, false, $movie->ID), 10), 148 | 'long' => wp_trim_words(get_the_content(null, false, $movie->ID), 75), 149 | ], 150 | 'images' => [ 151 | 'full' => get_the_post_thumbnail_url($movie->ID, 'full'), 152 | 'medium' => get_the_post_thumbnail_url($movie->ID, 'medium'), 153 | 'thumbnail' => get_the_post_thumbnail_url($movie->ID, 'thumbnail'), 154 | ], 155 | ]; 156 | }, $query->posts); 157 | } 158 | 159 | // If it's a 'post' post_type, format like this: 160 | if ($post_type === 'post') { 161 | return array_map(function ($post) { 162 | return [ 163 | 'id' => $post->ID, 164 | 'title' => get_the_title($post->ID), 165 | 'content' => get_the_content(null, false, $post->ID), 166 | 'link' => get_the_permalink($post->ID), 167 | ]; 168 | }, $query->posts); 169 | } 170 | 171 | // If it's any other post_type, format like this: 172 | return array_map(function ($post) { 173 | return [ 174 | 'id' => $post->ID, 175 | 'title' => get_the_title($post->ID), 176 | ]; 177 | }, $query->posts); 178 | }); 179 | ``` 180 | 181 | Focusing on the 'movie' post_type above, this would be the custom formatted response: 182 | 183 | ```json 184 | { 185 | "success": true, 186 | "data": [ 187 | { 188 | "id": 553, 189 | "title": "Dark Phoenix", 190 | "description": "The X-Men face their most formidable and powerful foe when one of their own, Jean Grey, starts to spiral out of control. During a rescue mission in outer space, Jean is nearly killed when she's hit by a mysterious cosmic force. Once she returns home, this force not only makes her infinitely more powerful, but far more unstable. The X-Men must now band together to save her soul and battle aliens that want to use Grey's new abilities to rule the galaxy.", 191 | "link": "https://wp-query.andrewrhyand.com/movies/dark-phoenix", 192 | "genres": [ 193 | "Drama", 194 | "Horror", 195 | "Thriller" 196 | ], 197 | "details": { 198 | "budget": "200000000", 199 | "status": "Released", 200 | "tmdb_id": "320288", 201 | "imdb_id": "tt6565702", 202 | "revenue": "252442974", 203 | "runtime": "114", 204 | "tagline": "X-Men Dark Phoenix", 205 | "homepage": "http://darkphoenix.com", 206 | "popularity": "122.285", 207 | "vote_count": "4063", 208 | "vote_average": "61%", 209 | "release_date": "Jun 05, 2019", 210 | "_thumbnail_id": "554" 211 | }, 212 | "short_description": { 213 | "short": "The X-Men face their most formidable and powerful foe when…", 214 | "long": "The X-Men face their most formidable and powerful foe when one of their own, Jean Grey, starts to spiral out of control. During a rescue mission in outer space, Jean is nearly killed when she's hit by a mysterious cosmic force. Once she returns home, this force not only makes her infinitely more powerful, but far more unstable. The X-Men must now band together to save her soul and battle aliens that want to use…" 215 | }, 216 | "images": { 217 | "full": "https://wp-query.andrewrhyand.com/wp-content/uploads/2020/11/cCTJPelKGLhALq3r51A9uMonxKj.jpg", 218 | "medium": "https://wp-query.andrewrhyand.com/wp-content/uploads/2020/11/cCTJPelKGLhALq3r51A9uMonxKj-200x300.jpg", 219 | "thumbnail": "https://wp-query.andrewrhyand.com/wp-content/uploads/2020/11/cCTJPelKGLhALq3r51A9uMonxKj-150x150.jpg" 220 | } 221 | }, 222 | { 223 | "id": 551, 224 | "title": "Enemy Lines", 225 | "description": "In the frozen, war torn landscape of occupied Poland during World War II, a crack team of allied commandos are sent on a deadly mission behind enemy lines to extract a rocket scientist from the hands of the Nazis.", 226 | "...and so on" 227 | } 228 | ] 229 | } 230 | ``` 231 | 232 | ### Default/Overriding WP_Query Args 233 | 234 | #### Default Args 235 | 236 | If you would like to add default `WP_Query $args` **BEFORE** the request params are applied, this is the filter to do it. 237 | 238 | ```php 239 | // Example 240 | add_filter('boxybird/query/default-args', function ($args) { 241 | $args['posts_per_page'] = 12; 242 | 243 | return $args; 244 | }); 245 | ``` 246 | 247 | > Note: The above are defaults only. Meaning, if the incoming request specifies `posts_per_page`, it will override the `boxybird/query/default-args` filter defaults. 248 | 249 | #### Override Args 250 | 251 | If you would like to modify/remove incoming request params **BEFORE** running the `WP_Query`, this is the filter to do it. 252 | 253 | ```php 254 | // Example. 255 | add_filter('boxybird/query/override-args', function ($args) { 256 | // Don't allow more than 20 'posts_per_page'. 257 | if (isset($args['posts_per_page']) && $args['posts_per_page'] > 20) { 258 | $args['posts_per_page'] = 20; 259 | } 260 | 261 | // Always override 'post_status' 262 | $args['post_status'] = 'publish'; 263 | 264 | return $args; 265 | }); 266 | ``` 267 | 268 | > Note: The above filter can be thought of as a security layer. If you never want an `$arg` to be passed to `WP_Query`, do it here! 269 | 270 | ### Permissions Callback 271 | 272 | If you would like to protected who has access to the `/wp-json/boxybird/wp-query/v1/args` endpoint, this is the filter. 273 | 274 | > Reference: https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/#permissions-callback 275 | 276 | ```php 277 | // Basic Example. 278 | add_filter('boxybird/query/permission', function () { 279 | // Only logged in users have access. 280 | return is_user_logged_in(); 281 | }); 282 | 283 | // Example taken from the WordPress docs. 284 | add_filter('boxybird/query/permission', function () { 285 | // Restrict endpoint to only users who have the edit_posts capability. 286 | if (!current_user_can('edit_posts')) { 287 | return new WP_Error('rest_forbidden', esc_html__('OMG you can not view private data.', 'my-text-domain'), ['status' => 401]); 288 | } 289 | 290 | // This is a black-listing approach. You could alternatively do this via white-listing, by returning false here and changing the permissions check. 291 | return true; 292 | }); 293 | ``` 294 | -------------------------------------------------------------------------------- /boxybird-wp-query-endpoint.php: -------------------------------------------------------------------------------- 1 | 'GET', 17 | 'callback' => [Query::class, 'callback'], 18 | 'permission_callback' => [Query::class, 'permissionCallback'], 19 | ]); 20 | }); 21 | } 22 | 23 | public static function callback(WP_REST_Request $request) 24 | { 25 | static::handleArgs($request); 26 | static::handleResponse(); 27 | } 28 | 29 | public static function permissionCallback() 30 | { 31 | return apply_filters('boxybird/query/permission', true); 32 | } 33 | 34 | protected static function handleArgs(WP_REST_Request $request) 35 | { 36 | $defaults = apply_filters('boxybird/query/default-args', []); 37 | $args = apply_filters('boxybird/query/override-args', $request->get_params()); 38 | 39 | static::$args = array_merge($defaults, $args); 40 | } 41 | 42 | protected static function handleFormatResponse(WP_Query $query) 43 | { 44 | if (!has_filter('boxybird/query/format-response')) { 45 | return $query->post_count ? $query->posts : []; 46 | } 47 | 48 | return apply_filters('boxybird/query/format-response', $query); 49 | } 50 | 51 | protected static function handleResponse() 52 | { 53 | $query = new WP_Query(static::$args); 54 | 55 | $data = static::handleFormatResponse($query); 56 | 57 | return wp_send_json_success($data); 58 | } 59 | } 60 | --------------------------------------------------------------------------------