kilbot/WooCommerce-POS

View on GitHub
includes/api/legacy/products.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

/**
 * POS Product Class
 * duck punches the WC REST API
 *
 * @package    WCPOS\API_Products
 * @author   Paul Kilmurray <paul@kilbot.com.au>
 * @link     http://www.wcpos.com
 */

namespace WCPOS\API;

use WC_API_Resource;
use WC_API_Server;

class Products extends WC_API_Resource {

    /** @var string $base the route base */
    protected $base = '/products';

    /* @var string Barcode postmeta */
    public $barcode_meta_key;

    /**
     * Product fields used by the POS
     * @var array
     */
    private $whitelist = array(
        'title',
        'id',
        'created_at',
        'updated_at',
        'type',
        'status',
        'downloadable',
        'virtual',
//    'permalink',
        'sku',
        'price',
        'regular_price',
        'sale_price',
//    'price_html',
        'taxable',
        'tax_status',
        'tax_class',
        'managing_stock',
        'stock_quantity',
        'in_stock',
        'backorders_allowed',
        'backordered',
        'sold_individually',
        'purchaseable',
        'featured',
        'visible',
//    'catalog_visibility',
        'on_sale',
//    'weight',
//    'dimensions',
        'shipping_required',
        'shipping_taxable',
        'shipping_class',
        'shipping_class_id',
//    'description',
//    'short_description',
//    'reviews_allowed',
//    'average_rating',
//    'rating_count',
//    'related_ids',
//    'upsell_ids',
//    'cross_sell_ids',
        'parent_id',
        'categories',
        'tags',
//    'images',
//    'featured_src', // replaced by thumbnail
        'attributes',
//    'downloads',
//    'download_limit',
//    'download_expiry',
//    'download_type',
        'purchase_note',
        'total_sales',
        'variations',
//    'parent',

        /**
         * Fields add by POS
         * - product thumbnail
         * - barcode
         */
        'featured_src',
        'barcode'
    );


    /**
     * @param WC_API_Server $server
     */
    public function __construct( WC_API_Server $server ) {
        parent::__construct( $server );

        // allow third party plugins to change the barcode postmeta field
        $this->barcode_meta_key = apply_filters( 'woocommerce_pos_barcode_meta_key', '_sku' );
        add_filter( 'woocommerce_api_product_response', array( $this, 'product_response' ), 10, 4 );

        if ( $server->path === $this->base ) {
            add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
            add_filter( 'posts_search', array( $this, 'posts_search' ), 10, 2 );
        }
    }


    /**
     * Register routes for POS Products
     *
     * @param array $routes
     * @return array
     */
    public function register_routes( $routes ) {

        # GET /products/ids
        $routes[ $this->base . '/ids' ] = array(
            array( array( $this, 'get_all_ids' ), WC_API_Server::READABLE ),
        );

        return $routes;
    }


    /**
     * Filter each product response from WC REST API for easier handling by the POS
     * - use the thumbnails rather than fullsize
     * - add barcode field
     * - unset unnecessary data
     *
     * @param  array $data
     * @param $product
     *
     * @return array modified data array $product_data
     */
    public function product_response( $data, $product, $fields, $server ) {
        $type = isset( $data['type'] ) ? $data['type'] : '';

        // variable products
        if ( $type == 'variable' ) :
            // nested variations
            foreach ( $data['variations'] as &$variation ) :
                $_product                = wc_get_product( $variation['id'] );
                $variation               = $this->filter_response_data( $variation, $_product );
                $variation['attributes'] = $this->patch_variation_attributes( $_product );
            endforeach;
        endif;

        // variation
        if ( $type == 'variation' ) :
            $data['attributes'] = $this->patch_variation_attributes( $product );
        endif;

        return $this->filter_response_data( $data, $product );
    }


    /**
     * https://github.com/woothemes/woocommerce/issues/8457
     * patches WC_Product_Variable->get_variation_attributes()
     * @param $product
     * @return array
     */
    private function patch_variation_attributes( $product ) {
        $patched_attributes   = array();
        $attributes           = $product->get_attributes();
        $variation_attributes = $product->get_variation_attributes();

        // patch for corrupted data, depreciate asap
        if ( empty( $attributes ) ) {
            $attributes = $product->parent->product_attributes;
            delete_post_meta( $product->variation_id, '_product_attributes' );
        }

        foreach ( $variation_attributes as $slug => $option ) {
            $slug = str_replace( 'attribute_', '', $slug );

            if ( isset( $attributes[ $slug ] ) ) {
                $patched_attributes[] = array(
                    'name'   => $this->get_variation_name( $attributes[ $slug ] ),
                    'option' => $this->get_variation_option( $product, $attributes[ $slug ], $option )
                );
            }

        }

        return $patched_attributes;
    }


    /**
     * @param $attribute
     * @return null|string
     */
    private function get_variation_name( $attribute ) {
        if ( $attribute['is_taxonomy'] ) {
            global $wpdb;
            $name = str_replace( 'pa_', '', $attribute['name'] );

            $label = $wpdb->get_var(
                $wpdb->prepare( "
          SELECT attribute_label
          FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
          WHERE attribute_name = %s;
        ", $name ) );

            return $label ? $label : $name;
        }

        return $attribute['name'];
    }


    /**
     * @param $product
     * @param $option
     * @param $attribute
     * @return mixed
     */
    private function get_variation_option( $product, $attribute, $option ) {
        $name = $option;

        // taxonomy attributes
        if ( $attribute['is_taxonomy'] ) {
            $terms = wp_get_post_terms( $product->parent->id, $attribute['name'] );
            if ( ! is_wp_error( $terms ) ) : foreach ( $terms as $term ) :
                if ( $option === $term->slug ) {
                    $name = $term->name;
                }
            endforeach; endif;

            // piped attributes
        } else {
            $values  = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) );
            $options = array_combine( array_map( 'sanitize_title', $values ), $values );
            if ( $options && isset( $options[ $option ] ) ) {
                $name = $options[ $option ];
            }
        }

        return $name;
    }


    /**
     * Filter product response data
     * - add featured_src
     * - add special key for barcode, defaults to sku
     * @param array $data
     * @param $product
     * @return array
     */
    private function filter_response_data( array $data, $product ) {
        $id      = isset( $data['id'] ) ? $data['id'] : '';
        $barcode = get_post_meta( $id, $this->barcode_meta_key, true );

        $data['featured_src'] = $this->get_thumbnail( $id );
        $data['barcode']      = apply_filters( 'woocommerce_pos_product_barcode', $barcode, $id );

        // allow decimal stock quantities, fixed in WC 2.4
        if ( version_compare( WC()->version, '2.4', '<' ) ) {
            $data['stock_quantity'] = $product->get_stock_quantity();
        }

        // filter by whitelist
        // - note, this uses the same method as WC REST API fields parameter
        // - this doesn't speed up queries as it should
        // - when WC REST API properly filters requests POS should use fields param
        return array_intersect_key( $data, array_flip( $this->whitelist ) );
    }


    /**
     * Returns thumbnail if it exists, if not, returns the WC placeholder image
     * @param int $id
     * @return string
     */
    private function get_thumbnail( $id ) {
        $image    = false;
        $thumb_id = get_post_thumbnail_id( $id );

        if ( $thumb_id ) {
            $image = wp_get_attachment_image_src( $thumb_id, 'shop_thumbnail' );
        }

        if ( is_array( $image ) ) {
            return $image[0];
        }

        return wc_placeholder_img_src();
    }

    /**
     * @param \WP_Query $wp_query
     */
    public function pre_get_posts( \WP_Query $wp_query ) {
        $query_array = isset( $wp_query->query['s'] ) ? $wp_query->query['s'] : '';

        if ( ! is_array( $query_array ) ) {
            return;
        }

        foreach ( $query_array as $query ) {
            $this->parse_query_array( $query, $wp_query );
        }
    }

    /**
     * @param array $query
     * @param \WP_Query $wp_query
     */
    private function parse_query_array( array $query, \WP_Query $wp_query ) {
        $type = isset( $query['type'] ) ? $query['type'] : 'string';

        if ( $type == 'string' && isset( $query ) ) {
            $term = isset( $query['query'] ) ? $query['query'] : '';
            $this->string_query( $term, $wp_query );
        }

        if ( $type == 'prefix' && isset( $query['prefix'] ) && isset( $query['query'] ) ) {
            $prefix = isset( $query['prefix'] ) ? $query['prefix'] : '';
            $term   = isset( $query['query'] ) ? $query['query'] : '';
            $this->prefix_query( $prefix, $term, $wp_query );
        }

    }

    /**
     * @param $term
     * @param \WP_Query $wp_query
     */
    private function string_query( $term, \WP_Query $wp_query ) {
        global $wpdb;
        $search_ids = $wpdb->get_col(
            $wpdb->prepare( "
        SELECT ID
        FROM $wpdb->posts
        WHERE post_type = 'product'
        AND post_status = 'publish'
        AND post_title LIKE %s
      ", '%' . $term . '%' )
        );
        $post__in   = $wp_query->get( 'post__in' );
        $include    = empty( $post__in ) ? $search_ids : array_intersect( $post__in, $search_ids );

        // query can't have post__in & post__not_in
        $post__not_in = $wp_query->get( 'post__not_in' );
        $include      = array_diff( $include, $post__not_in );

        if ( empty( $include ) ) {
            $include = array( 0 );
        }

        $wp_query->set( 'post__in', $include );
    }

    /**
     * @param $prefix
     * @param $term
     * @param \WP_Query $wp_query
     */
    private function prefix_query( $prefix, $term, \WP_Query $wp_query ) {
        // store original meta_query
        $meta_query = $wp_query->get( 'meta_query' );
        $tax_query  = $wp_query->get( 'tax_query' );

        // id
        if ( $prefix == 'id' ) {
            $term         = (int) $term;
            $post__in     = $wp_query->get( 'post__in' );
            $post__not_in = $wp_query->get( 'post__not_in' );

            $include = empty( $post__in ) ? array( $term ) : array_intersect( $post__in, array( $term ) );
            $include = array_diff( $include, $post__not_in );

            if ( empty( $include ) ) {
                $include = array( 0 );
            }
            $wp_query->set( 'post__in', $include );
        }

        // featured
        if ( in_array( $prefix, array( 'featured', 'downloadable' ) ) ) {
            $meta_query[] = array(
                'key'     => '_' . $prefix,
                'value'   => $term == 'true' ? 'yes' : 'no',
                'compare' => '='
            );
        }

        // sku
        if ( $prefix == 'sku' ) {
            $meta_query[] = array(
                'key'     => '_sku',
                'value'   => $term,
                'compare' => 'LIKE'
            );
        }

        // barcode
        // @todo refactor idb storage and product display to accommodate product_variation
        if ( $prefix == 'barcode' ) {
            global $wpdb;

            $ids = $wpdb->get_col(
                $wpdb->prepare( "
          SELECT DISTINCT p.ID
          FROM $wpdb->posts as p 
          LEFT JOIN $wpdb->postmeta as pm
          ON p.ID = pm.post_id
          WHERE p.post_type = 'product'
          AND p.post_status = 'publish'
          AND pm.meta_key = '$this->barcode_meta_key'
          AND pm.meta_value LIKE %s
        ", '%' . $term . '%' )
            );

            // special case, we need to search variations also
            $parent_ids = $wpdb->get_col(
                $wpdb->prepare( "
          SELECT DISTINCT p.post_parent
          FROM $wpdb->posts as p 
          LEFT JOIN $wpdb->postmeta as pm
          ON p.ID = pm.post_id
          WHERE p.post_type = 'product_variation'
          AND p.post_status = 'publish'
          AND pm.meta_key = '$this->barcode_meta_key'
          AND pm.meta_value LIKE %s
        ", '%' . $term . '%' )
            );

            $in     = $wp_query->get( 'post__in' );
            $not_in = $wp_query->get( 'post__not_in' );
            $in     = array_unique( array_merge( $in, $ids, $parent_ids ) );
            $in     = array_diff( $in, $not_in );

            if ( empty( $in ) ) {
                $in = array( 0 );
            }
            $wp_query->set( 'post__in', $in );
        }

        // on_sale
        if ( $prefix == 'on_sale' ) {
            $sale_ids = array_filter( wc_get_product_ids_on_sale() );
            $in       = $wp_query->get( 'post__in' );
            $not_in   = $wp_query->get( 'post__not_in' );

            if ( $term == 'true' ) {
                $post__in = array_diff( $sale_ids, $not_in );
                if ( ! empty( $in ) ) {
                    $post__in = array_intersect( $in, $post__in );
                }
                if ( empty( $post__in ) ) {
                    $post__in = array( 0 ); // no posts
                }
                $wp_query->set( 'post__in', $post__in );
            } else {
                $post__not_in = array_unique( array_merge( $sale_ids, $not_in ) );
                $wp_query->set( 'post__not_in', $post__not_in );
            }
        }

        // categories and cat (abbr)
        if ( $prefix == 'categories' || $prefix == 'cat' ) {
            $tax_query[] = array(
                'taxonomy' => 'product_cat',
                'field'    => 'name',
                'terms'    => array( $term )
            );
        }

        // tags and tag
        if ( $prefix == 'tags' || $prefix == 'tag' ) {
            $tax_query[] = array(
                'taxonomy' => 'product_tag',
                'field'    => 'name',
                'terms'    => array( $term )
            );
        }

        $wp_query->set( 'meta_query', $meta_query );
        $wp_query->set( 'tax_query', $tax_query );

    }

    /**
     * @param $search
     * @param \WP_Query $wp_query
     * @return string
     */
    public function posts_search( $search, \WP_Query $wp_query ) {
        global $wpdb;
        if ( ! empty( $search ) ) {
            $term   = isset( $wp_query->query['s'] ) ? $wp_query->query['s'] : '';
            $search = " AND ($wpdb->posts.post_title LIKE '%$term%')";
        }

        return $search;
    }

    /**
     * Returns array of all product ids
     *
     * @param array $filter
     * @return array|void
     */
    public function get_all_ids( $filter = array() ) {
        $args = array(
            'post_type'      => array( 'product' ),
            'post_status'    => array( 'publish' ),
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'order'          => isset( $filter['order'] ) ? $filter['order'] : 'ASC',
            'orderby'        => isset( $filter['orderby'] ) ? $filter['orderby'] : 'title'
        );

        if ( isset( $filter['updated_at_min'] ) ) {
            $args['date_query'][] = array(
                'column'    => 'post_modified_gmt',
                'after'     => $filter['updated_at_min'],
                'inclusive' => false
            );
        }

        $query = new \WP_Query( $args );
        $this->server->add_pagination_headers( $query );

        return array( 'products' => array_map( array( $this, 'format_id' ), $query->posts ) );
    }

    /**
     * @param $id
     * @return array
     */
    private function format_id( $id ) {
        return array( 'id' => (int) $id );
    }

}