'text', 'category' => 'key', 'stock_status' => 'key', 'type' => 'key', 'status' => 'key', 'variations' => 'bool', ); } /** * Product statuses the grid can set. * * @return array */ public static function statuses() { return array( 'publish' => __( 'Published', 'gridsmith' ), 'draft' => __( 'Draft', 'gridsmith' ), 'pending' => __( 'Pending review', 'gridsmith' ), 'private' => __( 'Private', 'gridsmith' ), ); } /** * Whether prices can be edited directly on this product. * * @param WC_Product $product Product. * @return bool */ public static function has_own_price( $product ) { return ! $product->is_type( 'variable' ) && ! $product->is_type( 'grouped' ); } /** * Field definitions. * * @return array */ protected function build_fields() { $types = wc_get_product_types(); return array( 'thumb' => array( 'label' => __( 'Image', 'gridsmith' ), 'type' => 'image', 'width' => 52, 'get' => function ( $p ) { $image = $p->get_image_id(); if ( ! $image && $p->get_parent_id() ) { $image = get_post_thumbnail_id( $p->get_parent_id() ); } $url = $image ? wp_get_attachment_image_url( $image, 'thumbnail' ) : ''; return $url ? $url : ''; }, ), 'id' => array( 'label' => __( 'ID', 'gridsmith' ), 'type' => 'readonly', 'width' => 64, 'sortable' => 'id', 'get' => function ( $p ) { return (string) $p->get_id(); }, ), 'name' => array( 'label' => __( 'Name', 'gridsmith' ), 'type' => 'text', 'width' => 260, 'sortable' => 'name', 'get' => function ( $p ) { return $p->get_name( 'edit' ); }, 'set' => function ( $p, $value ) { if ( '' === $value ) { return new WP_Error( 'bdbe_name', __( 'The product name cannot be empty.', 'gridsmith' ) ); } $p->set_name( $value ); return null; }, 'editable' => function ( $p ) { return ! $p->is_type( 'variation' ); }, 'display' => function ( $p ) { if ( ! $p->is_type( 'variation' ) ) { return ''; } $attributes = wc_get_formatted_variation( $p, true, false ); return '' !== $attributes ? $attributes : '#' . $p->get_id(); }, ), 'sku' => array( 'label' => __( 'SKU', 'gridsmith' ), 'type' => 'text', 'width' => 120, 'sortable' => 'sku', 'get' => function ( $p ) { return $p->get_sku( 'edit' ); }, 'set' => function ( $p, $value ) { if ( '' !== $value && ! wc_product_has_unique_sku( $p->get_id(), $value ) ) { return new WP_Error( 'bdbe_sku', __( 'This SKU is already used by another product.', 'gridsmith' ) ); } $p->set_sku( $value ); return null; }, ), 'type' => array( 'label' => __( 'Type', 'gridsmith' ), 'type' => 'readonly', 'width' => 90, 'get' => function ( $p ) use ( $types ) { if ( $p->is_type( 'variation' ) ) { return __( 'Variation', 'gridsmith' ); } $type = $p->get_type(); return isset( $types[ $type ] ) ? $types[ $type ] : ucfirst( $type ); }, ), 'regular_price' => array( 'label' => __( 'Regular price', 'gridsmith' ), 'type' => 'price', 'width' => 110, 'sortable' => 'price', 'get' => function ( $p ) { return self::has_own_price( $p ) ? $p->get_regular_price( 'edit' ) : ''; }, 'set' => function ( $p, $value ) { $p->set_regular_price( $value ); return null; }, 'editable' => array( __CLASS__, 'has_own_price' ), 'display' => array( __CLASS__, 'price_range' ), ), 'sale_price' => array( 'label' => __( 'Sale price', 'gridsmith' ), 'type' => 'price', 'width' => 100, 'get' => function ( $p ) { return self::has_own_price( $p ) ? $p->get_sale_price( 'edit' ) : ''; }, 'set' => function ( $p, $value ) { $p->set_sale_price( $value ); return null; }, 'editable' => array( __CLASS__, 'has_own_price' ), 'display' => function ( $p ) { return self::has_own_price( $p ) ? '' : '—'; }, ), 'stock_quantity' => array( 'label' => __( 'Stock', 'gridsmith' ), 'type' => 'int', 'width' => 80, 'hint' => __( 'Leave empty to stop tracking stock for this product.', 'gridsmith' ), 'get' => function ( $p ) { if ( true !== $p->get_manage_stock( 'edit' ) ) { return ''; } $qty = $p->get_stock_quantity( 'edit' ); return null === $qty ? '' : (string) wc_stock_amount( $qty ); }, 'set' => function ( $p, $value ) { if ( '' === $value ) { $p->set_manage_stock( false ); return null; } $p->set_manage_stock( true ); $p->set_stock_quantity( wc_stock_amount( $value ) ); return null; }, 'editable' => function ( $p ) { return ! $p->is_type( 'grouped' ) && ! $p->is_type( 'external' ); }, ), 'stock_status' => array( 'label' => __( 'Stock status', 'gridsmith' ), 'type' => 'select', 'width' => 120, 'options' => wc_get_product_stock_status_options(), 'get' => function ( $p ) { return $p->get_stock_status( 'edit' ); }, 'set' => function ( $p, $value ) { $p->set_stock_status( $value ); return null; }, 'editable' => function ( $p ) { return true !== $p->get_manage_stock( 'edit' ) && ! $p->is_type( 'variable' ) && ! $p->is_type( 'grouped' ); }, ), 'status' => array( 'label' => __( 'Status', 'gridsmith' ), 'type' => 'select', 'width' => 120, 'options' => self::statuses(), 'get' => function ( $p ) { return $p->get_status( 'edit' ); }, 'set' => function ( $p, $value ) { $p->set_status( $value ); return null; }, 'editable' => function ( $p ) { return ! $p->is_type( 'variation' ); }, 'display' => function ( $p ) { if ( ! $p->is_type( 'variation' ) ) { return ''; } return 'publish' === $p->get_status( 'edit' ) ? __( 'Enabled', 'gridsmith' ) : __( 'Disabled', 'gridsmith' ); }, ), ); } /** * Read-only price text for variable / grouped parents. * * @param WC_Product $p Product. * @return string */ public static function price_range( $p ) { if ( self::has_own_price( $p ) ) { return ''; } if ( $p->is_type( 'variable' ) ) { $min = $p->get_variation_regular_price( 'min' ); $max = $p->get_variation_regular_price( 'max' ); if ( '' === $min ) { return '—'; } return $min === $max ? (string) $min : $min . ' – ' . $max; } return '—'; } /** * Build wc_get_products args from filters. * * @param array $filters Sanitised filters. * @return array|null Args, or null when nothing can match. */ protected function query_args( array $filters ) { $statuses = array_keys( self::statuses() ); $args = array( 'status' => ! empty( $filters['status'] ) && in_array( $filters['status'], $statuses, true ) ? array( $filters['status'] ) : $statuses, ); if ( ! empty( $filters['category'] ) ) { $args['category'] = array( $filters['category'] ); } if ( ! empty( $filters['stock_status'] ) && array_key_exists( $filters['stock_status'], wc_get_product_stock_status_options() ) ) { $args['stock_status'] = $filters['stock_status']; } if ( ! empty( $filters['type'] ) && array_key_exists( $filters['type'], wc_get_product_types() ) ) { $args['type'] = array( $filters['type'] ); } if ( isset( $filters['search'] ) && '' !== $filters['search'] ) { $store = WC_Data_Store::load( 'product' ); $ids = array_filter( array_map( 'absint', (array) $store->search_products( wc_clean( $filters['search'] ), '', false, true ) ) ); if ( ! $ids ) { return null; } $args['include'] = array_values( $ids ); } return $args; } /** * One page of products. * * @param array $args Query args. * @return array */ public function query( array $args ) { $base = $this->query_args( $args['filters'] ); if ( null === $base ) { return array( 'objects' => array(), 'total' => 0, ); } $order = 'desc' === $args['order'] ? 'DESC' : 'ASC'; $base = array_merge( $base, array( 'limit' => $args['per_page'], 'page' => $args['page'], 'paginate' => true, 'return' => 'ids', 'order' => $order, ) ); switch ( $args['orderby'] ) { case 'name': $base['orderby'] = 'title'; break; case 'date': $base['orderby'] = 'date'; break; case 'sku': $base['orderby'] = 'meta_value'; $base['meta_key'] = '_sku'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- user-chosen sort, one page at a time. break; case 'price': $base['orderby'] = 'meta_value_num'; $base['meta_key'] = '_price'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- user-chosen sort, one page at a time. break; default: $base['orderby'] = 'ID'; } $result = wc_get_products( $base ); $ids = array_map( 'absint', (array) $result->products ); if ( $ids ) { _prime_post_caches( $ids, true, true ); } $objects = array_values( array_filter( array_map( 'wc_get_product', $ids ) ) ); return array( 'objects' => $objects, 'total' => (int) $result->total, ); } /** * IDs matching filters, for bulk actions. * * @param array $filters Filters. * @return int[] */ public function matching_ids( array $filters ) { $base = $this->query_args( $filters ); if ( null === $base ) { return array(); } $base = array_merge( $base, array( 'limit' => -1, 'return' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', ) ); return array_map( 'absint', (array) wc_get_products( $base ) ); } /** * Load a product or variation. * * @param int $id ID. * @return WC_Product|null */ public function load( $id ) { $product = wc_get_product( absint( $id ) ); return $product instanceof WC_Product ? $product : null; } /** * Capability check per product. * * @param WC_Product $product Product. * @return bool */ public function can_edit( $product ) { $id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); return current_user_can( 'manage_woocommerce' ) && current_user_can( 'edit_post', $id ); } /** * Sale price must stay below the regular price. * * @param WC_Product $product Product. * @param array $touched Changed fields. * @return array */ public function validate( $product, array $touched ) { $errors = array(); if ( ! array_intersect( array( 'regular_price', 'sale_price' ), $touched ) || ! self::has_own_price( $product ) ) { return $errors; } $regular = $product->get_regular_price( 'edit' ); $sale = $product->get_sale_price( 'edit' ); $field = in_array( 'sale_price', $touched, true ) ? 'sale_price' : 'regular_price'; if ( '' !== $sale && '' === $regular ) { $errors[ $field ] = __( 'Set a regular price before a sale price.', 'gridsmith' ); } elseif ( '' !== $sale && (float) $sale >= (float) $regular ) { $errors[ $field ] = __( 'The sale price must be lower than the regular price.', 'gridsmith' ); } return $errors; } /** * Save through the WooCommerce CRUD so caches and lookup tables update. * * @param WC_Product $product Product. */ public function save( $product ) { $product->save(); } /** * Re-sync variable parents after their variations changed. * * @param WC_Product[] $objects Saved products. */ public function after_save( array $objects ) { $parents = array(); foreach ( $objects as $product ) { if ( $product->get_parent_id() && $product->is_type( 'variation' ) ) { $parents[ $product->get_parent_id() ] = true; } } foreach ( array_keys( $parents ) as $parent_id ) { WC_Product_Variable::sync( $parent_id ); wc_delete_product_transients( $parent_id ); } } /** * Row identity. * * @param WC_Product $p Product. * @return array */ protected function row_meta( $p ) { $is_var = $p->is_type( 'variation' ); $owner = $is_var ? $p->get_parent_id() : $p->get_id(); return array( 'id' => $p->get_id(), 'kind' => $is_var ? 'variation' : 'product', 'parent' => $is_var ? $p->get_parent_id() : 0, 'title' => $p->get_name( 'edit' ), 'edit' => (string) get_edit_post_link( $owner, 'raw' ), 'view' => (string) get_permalink( $owner ), 'ptype' => $p->get_type(), ); } }