Overview
  • Namespace
  • Class

Namespaces

  • None
  • WC_POS
    • Admin
      • Settings
        • Receipt
    • API
    • Gateways
    • Integrations
    • Products

Classes

  • WC_POS\Activator
  • WC_POS\Admin
  • WC_POS\Admin\Gateways
  • WC_POS\Admin\Menu
  • WC_POS\Admin\Notices
  • WC_POS\Admin\Orders
  • WC_POS\Admin\Page
  • WC_POS\Admin\Permalink
  • WC_POS\Admin\Plugins
  • WC_POS\Admin\Products
  • WC_POS\Admin\Settings
  • WC_POS\Admin\Settings\Access
  • WC_POS\Admin\Settings\Checkout
  • WC_POS\Admin\Settings\Customers
  • WC_POS\Admin\Settings\Gateways
  • WC_POS\Admin\Settings\General
  • WC_POS\Admin\Settings\HotKeys
  • WC_POS\Admin\Settings\Page
  • WC_POS\Admin\Settings\Receipt\Options
  • WC_POS\Admin\Settings\Receipt\Template
  • WC_POS\Admin\Settings\Receipts
  • WC_POS\Admin\Settings\Status
  • WC_POS\Admin\Settings\Tools
  • WC_POS\Admin\Status
  • WC_POS\Ajax
  • WC_POS\API
  • WC_POS\API\Coupons
  • WC_POS\API\Customers
  • WC_POS\API\Gateways
  • WC_POS\API\i18n
  • WC_POS\API\Orders
  • WC_POS\API\Params
  • WC_POS\API\Payload
  • WC_POS\API\Products
  • WC_POS\API\Settings
  • WC_POS\API\Support
  • WC_POS\API\Templates
  • WC_POS\Customers
  • WC_POS\Deactivator
  • WC_POS\Gateways
  • WC_POS\Gateways\Card
  • WC_POS\Gateways\Cash
  • WC_POS\i18n
  • WC_POS\Integrations\Bookings
  • WC_POS\Products
  • WC_POS\Products\Visibility
  • WC_POS\Setup
  • WC_POS\Status
  • WC_POS\Tax
  • WC_POS\Template

Functions

  • is_pos
  • is_pos_admin
  • wc_pos_get_option
  • wc_pos_json_encode
  • wc_pos_locate_template
  • wc_pos_trim_html_string
  • wc_pos_update_option
  • wc_pos_url
  1 <?php
  2 
  3 /**
  4  * POS Product Class
  5  * duck punches the WC REST API
  6  *
  7  * @class    WC_POS_API_Products
  8  * @package  WooCommerce POS
  9  * @author   Paul Kilmurray <paul@kilbot.com.au>
 10  * @link     http://www.woopos.com.au
 11  */
 12 
 13 namespace WC_POS\API;
 14 
 15 use WC_API_Resource;
 16 use WC_API_Server;
 17 
 18 class Products extends WC_API_Resource {
 19 
 20   /** @var string $base the route base */
 21   protected $base = '/products';
 22 
 23   /* @var string Barcode postmeta */
 24   public $barcode_meta_key;
 25 
 26   /**
 27    * Product fields used by the POS
 28    * @var array
 29    */
 30   private $whitelist = array(
 31     'title',
 32     'id',
 33     'created_at',
 34     'updated_at',
 35     'type',
 36     'status',
 37     'downloadable',
 38     'virtual',
 39 //    'permalink',
 40     'sku',
 41     'price',
 42     'regular_price',
 43     'sale_price',
 44     'price_html',
 45     'taxable',
 46     'tax_status',
 47     'tax_class',
 48     'managing_stock',
 49     'stock_quantity',
 50     'in_stock',
 51     'backorders_allowed',
 52     'backordered',
 53     'sold_individually',
 54     'purchaseable',
 55     'featured',
 56     'visible',
 57 //    'catalog_visibility',
 58     'on_sale',
 59 //    'weight',
 60 //    'dimensions',
 61     'shipping_required',
 62     'shipping_taxable',
 63     'shipping_class',
 64     'shipping_class_id',
 65 //    'description',
 66 //    'short_description',
 67 //    'reviews_allowed',
 68 //    'average_rating',
 69 //    'rating_count',
 70 //    'related_ids',
 71 //    'upsell_ids',
 72 //    'cross_sell_ids',
 73     'parent_id',
 74     'categories',
 75     'tags',
 76 //    'images',
 77 //    'featured_src', // replaced by thumbnail
 78     'attributes',
 79 //    'downloads',
 80 //    'download_limit',
 81 //    'download_expiry',
 82 //    'download_type',
 83     'purchase_note',
 84     'total_sales',
 85     'variations',
 86 //    'parent',
 87 
 88     /**
 89      * Fields add by POS
 90      * - product thumbnail
 91      * - barcode
 92      */
 93     'featured_src',
 94     'barcode'
 95   );
 96 
 97 
 98   /**
 99    * @param WC_API_Server $server
100    */
101   public function __construct( WC_API_Server $server ) {
102     parent::__construct( $server );
103 
104     // allow third party plugins to change the barcode postmeta field
105     $this->barcode_meta_key = apply_filters( 'woocommerce_pos_barcode_meta_key', '_sku' );
106     add_filter( 'woocommerce_api_product_response', array( $this, 'product_response' ), 10, 4 );
107 
108     if( $server->path === $this->base ){
109       add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
110       add_filter( 'posts_search', array( $this, 'posts_search'), 10, 2 );
111     }
112   }
113 
114 
115   /**
116    * Register routes for POS Products
117    *
118    * @param array $routes
119    * @return array
120    */
121   public function register_routes( $routes ) {
122 
123     # GET /products/ids
124     $routes[ $this->base . '/ids'] = array(
125       array( array( $this, 'get_all_ids' ), WC_API_Server::READABLE ),
126     );
127 
128     return $routes;
129   }
130 
131 
132   /**
133    * Filter each product response from WC REST API for easier handling by the POS
134    * - use the thumbnails rather than fullsize
135    * - add barcode field
136    * - unset unnecessary data
137    *
138    * @param  array $data
139    * @param $product
140    *
141    * @return array modified data array $product_data
142    */
143   public function product_response( $data, $product, $fields, $server ) {
144     $type = isset( $data['type'] ) ? $data['type'] : '';
145 
146     // variable products
147     if( $type == 'variable' ) :
148       // nested variations
149       foreach( $data['variations'] as &$variation ) :
150         $_product = wc_get_product( $variation['id'] );
151         $variation = $this->filter_response_data( $variation, $_product );
152         $variation['attributes'] = $this->patch_variation_attributes( $_product );
153       endforeach;
154     endif;
155 
156     // variation
157     if( $type == 'variation' ) :
158       $data['attributes'] = $this->patch_variation_attributes( $product );
159     endif;
160 
161     return $this->filter_response_data( $data, $product );
162   }
163 
164 
165   /**
166    * https://github.com/woothemes/woocommerce/issues/8457
167    * patches WC_Product_Variable->get_variation_attributes()
168    * @param $product
169    * @return array
170    */
171   private function patch_variation_attributes( $product ){
172     $patched_attributes = array();
173     $attributes = $product->get_attributes();
174     $variation_attributes = $product->get_variation_attributes();
175 
176     // patch for corrupted data, depreciate asap
177     if( empty( $attributes ) ){
178       $attributes = $product->parent->product_attributes;
179       delete_post_meta( $product->variation_id, '_product_attributes' );
180     }
181 
182     foreach( $variation_attributes as $slug => $option ){
183       $slug = str_replace( 'attribute_', '', $slug );
184 
185       if( isset( $attributes[$slug] ) ){
186         $patched_attributes[] = array(
187           'name'    => $this->get_variation_name( $attributes[$slug] ),
188           'option'  => $this->get_variation_option( $product, $attributes[$slug], $option )
189         );
190       }
191 
192     }
193 
194     return $patched_attributes;
195   }
196 
197 
198   /**
199    * @param $attribute
200    * @return null|string
201    */
202   private function get_variation_name( $attribute ){
203     if( $attribute['is_taxonomy'] ){
204       global $wpdb;
205       $name = str_replace( 'pa_', '', $attribute['name'] );
206 
207       $label = $wpdb->get_var(
208         $wpdb->prepare("
209           SELECT attribute_label
210           FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
211           WHERE attribute_name = %s;
212         ", $name ) );
213 
214       return $label ? $label : $name;
215     }
216 
217     return $attribute['name'];
218   }
219 
220 
221   /**
222    * @param $product
223    * @param $option
224    * @param $attribute
225    * @return mixed
226    */
227   private function get_variation_option( $product, $attribute, $option ){
228     $name = $option;
229 
230     // taxonomy attributes
231     if ( $attribute['is_taxonomy'] ) {
232       $terms = wp_get_post_terms( $product->parent->id, $attribute['name'] );
233       if( !is_wp_error($terms) ) : foreach( $terms as $term ) :
234         if( $option === $term->slug ) $name = $term->name;
235       endforeach; endif;
236 
237     // piped attributes
238     } else {
239       $values = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) );
240       $options = array_combine( array_map( 'sanitize_title', $values) , $values );
241       if( $options && isset( $options[$option] ) ){
242         $name = $options[$option];
243       }
244     }
245 
246     return $name;
247   }
248 
249 
250   /**
251    * Filter product response data
252    * - add featured_src
253    * - add special key for barcode, defaults to sku
254    * @param array $data
255    * @param $product
256    * @return array
257    */
258   private function filter_response_data( array $data, $product ){
259     $barcode = get_post_meta( $product->id, $this->barcode_meta_key, true );
260 
261     $data['featured_src'] = $this->get_thumbnail( $product->id );
262     $data['barcode'] = apply_filters( 'woocommerce_pos_product_barcode', $barcode, $product->id );
263 
264     // allow decimal stock quantities, fixed in WC 2.4
265     if( version_compare( WC()->version, '2.4', '<' ) ){
266       $data['stock_quantity'] = $product->get_stock_quantity();
267     }
268 
269     // filter by whitelist
270     // - note, this uses the same method as WC REST API fields parameter
271     // - this doesn't speed up queries as it should
272     // - when WC REST API properly filters requests POS should use fields param
273     return array_intersect_key( $data, array_flip( $this->whitelist ) );
274   }
275 
276 
277   /**
278    * Returns thumbnail if it exists, if not, returns the WC placeholder image
279    * @param int $id
280    * @return string
281    */
282   private function get_thumbnail($id){
283     $image = false;
284     $thumb_id = get_post_thumbnail_id( $id );
285 
286     if( $thumb_id )
287       $image = wp_get_attachment_image_src( $thumb_id, 'shop_thumbnail' );
288 
289     if( is_array($image) )
290       return $image[0];
291 
292     return wc_placeholder_img_src();
293   }
294 
295   /**
296    * @param \WP_Query $wp_query
297    */
298   public function pre_get_posts(\WP_Query $wp_query){
299     $query_array = isset($wp_query->query['s']) ? $wp_query->query['s'] : '';
300 
301     if(!is_array($query_array)){
302       return;
303     }
304 
305     foreach( $query_array as $query ){
306       $this->parse_query_array($query, $wp_query);
307     }
308   }
309 
310   /**
311    * @param array $query
312    * @param \WP_Query $wp_query
313    */
314   private function parse_query_array( array $query, \WP_Query $wp_query){
315     $type = isset($query['type']) ? $query['type'] : 'string';
316 
317     if($type == 'string' && isset($query)){
318       $term = isset($query['query']) ? $query['query'] : '';
319       $this->string_query($term, $wp_query);
320     }
321 
322     if($type == 'prefix' && isset($query['prefix']) && isset($query['query'])){
323       $prefix = isset($query['prefix']) ? $query['prefix'] : '';
324       $term = isset($query['query']) ? $query['query'] : '';
325       $this->prefix_query($prefix, $term, $wp_query);
326     }
327 
328   }
329 
330   /**
331    * @param $term
332    * @param \WP_Query $wp_query
333    */
334   private function string_query( $term, \WP_Query $wp_query ){
335     global $wpdb;
336     $search_ids = $wpdb->get_col(
337       $wpdb->prepare("
338         SELECT ID
339         FROM $wpdb->posts
340         WHERE post_type = 'product'
341         AND post_status = 'publish'
342         AND post_title LIKE %s
343       ", '%' . $term . '%')
344     );
345     $post__in = $wp_query->get('post__in');
346     $include = empty($post__in) ? $search_ids : array_intersect($post__in, $search_ids);
347     if(empty($include)){
348       $include = array(0);
349     }
350     $wp_query->set( 'post__in', $include );
351   }
352 
353   /**
354    * @param $prefix
355    * @param $term
356    * @param \WP_Query $wp_query
357    */
358   private function prefix_query( $prefix, $term, \WP_Query $wp_query ){
359     // store original meta_query
360     $meta_query = $wp_query->get( 'meta_query' );
361     $tax_query = $wp_query->get( 'tax_query' );
362 
363     // id
364     if($prefix == 'id'){
365       $term = (int) $term;
366       $post__in = $wp_query->get('post__in');
367       $include = empty($post__in) ? array($term) : array_intersect($post__in, array($term));
368       if(empty($include)){
369         $include = array(0);
370       }
371       $wp_query->set( 'post__in', $include );
372     }
373 
374     // featured
375     if($prefix == 'featured'){
376       $meta_query[] = array(
377         'key'     => '_featured',
378         'value'   => $term == 'true' ? 'yes' : 'no',
379         'compare' => '='
380       );
381     }
382 
383     // sku
384     if($prefix == 'sku'){
385       $meta_query[] = array(
386         'key'     => '_sku',
387         'value'   => $term,
388         'compare' => 'LIKE'
389       );
390     }
391 
392     // barcode
393     if($prefix == 'barcode'){
394       $meta_query[] = array(
395         'key'     => $this->barcode_meta_key,
396         'value'   => $term,
397         'compare' => 'LIKE'
398       );
399     }
400 
401     // on_sale
402     if($prefix == 'on_sale'){
403       $sale_ids = array_filter( wc_get_product_ids_on_sale() );
404       if($term == 'true'){
405         $post__in = $wp_query->get('post__in');
406         $include = empty($post__in) ? $sale_ids : array_intersect($post__in, $sale_ids);
407         if(empty($include)){
408           $include = array(0);
409         }
410         $wp_query->set( 'post__in', $include );
411       } else {
412         $exclude = $wp_query->get('post__not_in');
413         $wp_query->set( 'post__not_in', array_merge($exclude, $sale_ids) );
414       }
415     }
416 
417     // categories and cat (abbr)
418     if($prefix == 'categories' || $prefix == 'cat'){
419       $tax_query[] = array(
420         'taxonomy' => 'product_cat',
421         'field'    => 'name',
422         'terms'    => array( $term )
423       );
424     }
425 
426     // tags and tag
427     if($prefix == 'tags' || $prefix == 'tag'){
428       $tax_query[] = array(
429         'taxonomy' => 'product_tag',
430         'field'    => 'name',
431         'terms'    => array( $term )
432       );
433     }
434 
435     $wp_query->set('meta_query', $meta_query);
436     $wp_query->set('tax_query', $tax_query);
437 
438   }
439 
440   /**
441    * @param $search
442    * @param \WP_Query $wp_query
443    * @return string
444    */
445   public function posts_search( $search, \WP_Query $wp_query ){
446     global $wpdb;
447     if(!empty($search)){
448       $term = isset($wp_query->query['s']) ? $wp_query->query['s'] : '';
449       $search = " AND ($wpdb->posts.post_title LIKE '%$term%')";
450     }
451     return $search;
452   }
453 
454   /**
455    * Returns array of all product ids
456    *
457    * @param array $filter
458    * @return array|void
459    */
460   public function get_all_ids( $filter = array() ){
461     $args = array(
462       'post_type'     => array('product'),
463       'post_status'   => array('publish'),
464       'posts_per_page'=> -1,
465       'fields'        => 'ids'
466     );
467 
468     if( isset( $filter['updated_at_min'] ) ){
469       $args['date_query'][] = array(
470         'column'    => 'post_modified_gmt',
471         'after'     => $filter['updated_at_min'],
472         'inclusive' => false
473       );
474     }
475 
476     $query = new \WP_Query( $args );
477     $this->server->add_pagination_headers($query);
478     return array( 'products' => array_map( array( $this, 'format_id' ), $query->posts ) );
479   }
480 
481   /**
482    * @param $id
483    * @return array
484    */
485   private function format_id( $id ){
486     return array( 'id' => (int) $id );
487   }
488 
489 }
API documentation generated by ApiGen