'product', 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ), 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_term_cache' => false, // Only the products that have a recipe. 'meta_key' => self::META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key ) ); $recipes = array(); foreach ( $query->posts as $product_id ) { $recipe = $this->get( (int) $product_id ); if ( null !== $recipe ) { $recipes[ (string) $product_id ] = $recipe; } } return $recipes; } /** * The recipe of one product. * * @param int $product_id Product id. * @return array|null */ public function get( int $product_id ): ?array { $raw = get_post_meta( $product_id, self::META_KEY, true ); if ( ! is_string( $raw ) || '' === $raw ) { return null; } $recipe = json_decode( $raw, true ); if ( ! is_array( $recipe ) || ! isset( $recipe['lines'] ) ) { return null; } $recipe['productId'] = (string) $product_id; return $recipe; } /** * Save the recipe of a product (replaces the previous one). * * @param int $product_id Product id. * @param mixed $input Decoded JSON. * @return array Validator-style result; 'value' is the saved recipe. 'status' carries an HTTP code when not 422. */ public function save( int $product_id, $input ): array { $product = wc_get_product( $product_id ); if ( ! $product || ! in_array( $product->get_type(), Catalog::SUPPORTED_TYPES, true ) || 'trash' === $product->get_status() ) { return array( 'ok' => false, 'errors' => array( 'productId' => __( 'This is not a product MadeCost can handle (simple or variable products only).', 'madecost' ) ), ); } if ( ! current_user_can( 'edit_post', $product_id ) ) { return array( 'ok' => false, 'errors' => array( 'form' => __( 'You are not allowed to edit this product.', 'madecost' ) ), 'status' => 403, ); } $result = Validator::recipe( $input ); if ( ! $result['ok'] ) { return $result; } $recipe = $result['value']; $known = array(); foreach ( ( new Materials() )->all() as $material ) { $known[ $material['id'] ] = true; } foreach ( $recipe['lines'] as $line ) { if ( ! isset( $known[ $line['materialId'] ] ) ) { return array( 'ok' => false, 'errors' => array( 'lines' => __( 'One of the materials no longer exists. Reload the page.', 'madecost' ) ), ); } } $recipe['productTitle'] = $product->get_name(); $recipe['updatedAt'] = gmdate( 'c' ); // update_post_meta() strips slashes, so the JSON is slashed first. update_post_meta( $product_id, self::META_KEY, wp_slash( wp_json_encode( $recipe ) ) ); $recipe['productId'] = (string) $product_id; return array( 'ok' => true, 'value' => $recipe, ); } /** * Delete the recipe of a product. * * @param int $product_id Product id. * @return bool False when there was no recipe. */ public function delete( int $product_id ): bool { if ( null === $this->get( $product_id ) ) { return false; } delete_post_meta( $product_id, self::META_KEY ); return true; } /** * In how many recipes each material is used. * * @param array[] $recipes Recipes, as returned by all(). * @return int[] Material id => number of recipes. */ public static function usage( array $recipes ): array { $usage = array(); foreach ( $recipes as $recipe ) { foreach ( $recipe['lines'] as $line ) { $usage[ $line['materialId'] ] = ( $usage[ $line['materialId'] ] ?? 0 ) + 1; } } return $usage; } }