Skip to content

Commit

Permalink
Merge pull request #84 from sectsect/feature/tag-update-programmatically
Browse files Browse the repository at this point in the history
feat: add support for programmatic tag order management
  • Loading branch information
sectsect authored Nov 26, 2024
2 parents 6ca1270 + 370b01b commit 5d668b3
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 8 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- 🔍 Works with default WordPress tag and custom taxonomy systems
- 🔢 Drag-and-Drop interface for easy tag reordering
- 📊 Supports multiple post types and taxonomies
- 🔧 **NEW: Programmatic Tag Order Management**
- 🌐 **NEW: REST API Support**
- Retrieve ordered tags via GET endpoint
- Update tag order programmatically
Expand Down Expand Up @@ -126,6 +127,35 @@ if ( $terms && ! is_wp_error( $terms ) ) :
<?php the_terms_ordered( $post->ID, 'post_tag' ); ?>
```

## Programmatic Tag Order Management

The plugin provides a `Tag_Updater` class for developers to programmatically manage tag order:

```php
$tag_updater = new \WP_Tag_Order\Tag_Updater();

try {
// Update tag order using an array or comma-separated string
$result = $tag_updater->update_tag_order(
get_the_ID(), // Post ID
'post_tag', // Taxonomy that enabled in plugin settings
[1, 2, 3] // Tag IDs in desired order
);
} catch ( \InvalidArgumentException $e ) {
// Error handling
error_log( $e->getMessage() );
}
```

This class allows flexible tag order updates directly from your theme or custom plugin code, supporting both array and string inputs with robust validation.

#### Return Value

`update_tag_order()` returns: `int|bool`
- `int`: Meta ID if the tag order meta key didn't previously exist
- `true`: Successful update of existing meta
- `false`: Update failed or the new tag order is identical to the existing order

## REST API

The WP Tag Order plugin provides two REST API endpoints for managing tag order:
Expand Down
89 changes: 89 additions & 0 deletions includes/class-tag-updater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
/**
* WP Tag Order Tag Updater Class
*
* Provides a method to update tag order for a specific post and taxonomy.
*
* @package WP_Tag_Order
* @since 3.11.0
*/

declare(strict_types=1);

namespace WP_Tag_Order;

/**
* Class responsible for updating tag order for a specific post.
*/
class Tag_Updater {
/**
* Update tag order for a specific post and taxonomy.
*
* @param int $post_id The post ID to update tags for.
* @param string $taxonomy The taxonomy name.
* @param array<int>|string $tag_ids Array of tag IDs or comma-separated tag IDs.
*
* @return int|bool True if meta update was successful, false otherwise.
* @throws \InvalidArgumentException If input validation fails.
*/
public function update_tag_order( int $post_id, string $taxonomy, array|string $tag_ids ): int|bool {
// Validate inputs.
if ( $post_id <= 0 ) {
throw new \InvalidArgumentException( 'Invalid post ID' );
}

if ( empty( $taxonomy ) ) {
throw new \InvalidArgumentException( 'Taxonomy cannot be empty' );
}

if ( ! taxonomy_exists( $taxonomy ) ) {
throw new \InvalidArgumentException( sprintf( 'Invalid taxonomy: %s', esc_html( $taxonomy ) ) );
}

if ( is_taxonomy_hierarchical( $taxonomy ) ) {
throw new \InvalidArgumentException( sprintf( 'Taxonomy %s is hierarchical and not supported', esc_html( $taxonomy ) ) );
}

// Convert string to array if needed.
if ( is_string( $tag_ids ) ) {
$tag_ids = explode( ',', wp_unslash( $tag_ids ) );
}

if ( empty( $tag_ids ) ) {
throw new \InvalidArgumentException( 'Tag IDs cannot be empty' );
}

if ( ! wto_is_enabled_taxonomy( $taxonomy ) ) {
throw new \InvalidArgumentException( sprintf( 'Taxonomy %s is not enabled for tag ordering', esc_html( $taxonomy ) ) );
}

// Sanitize tag IDs.
$sanitized_tag_ids = array_map(
function ( $tag_id ): int {
// Ensure each tag ID is a positive integer.
$sanitized_id = filter_var(
$tag_id,
FILTER_VALIDATE_INT,
array(
'options' => array(
'min_range' => 1,
),
)
);

if ( false === $sanitized_id ) {
throw new \InvalidArgumentException( 'Invalid tag ID: ' . esc_html( is_string( $tag_id ) ? $tag_id : (string) $tag_id ) );
}

return $sanitized_id;
},
$tag_ids
);

// Serialize tags for storage.
$meta_box_tags_value = serialize( $sanitized_tag_ids );

// Update post meta.
return update_post_meta( $post_id, 'wp-tag-order-' . $taxonomy, $meta_box_tags_value );
}
}
16 changes: 11 additions & 5 deletions includes/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,18 @@ function ajax_wto_update_tags(): void {
exit;
}

$newordertags = array_map( 'sanitize_text_field', explode( ',', wp_unslash( $tags ) ) );
$meta_box_tags_value = serialize( $newordertags );
$result = update_post_meta( intval( $id ), 'wp-tag-order-' . $taxonomy, $meta_box_tags_value );
try {
$tag_updater = new \WP_Tag_Order\Tag_Updater();
$result = $tag_updater->update_tag_order(
intval( $id ),
$taxonomy,
$tags
);

echo wp_json_encode( $result );
exit;
wp_send_json_success( $result );
} catch ( \InvalidArgumentException $e ) {
wp_send_json_error( $e->getMessage(), 400 );
}
}
add_action( 'wp_ajax_wto_update_tags', 'ajax_wto_update_tags' );
add_action( 'wp_ajax_nopriv_wto_update_tags', 'ajax_wto_update_tags' );
Expand Down
6 changes: 3 additions & 3 deletions includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,14 +327,14 @@ function ( $tag_id ) use ( $taxonomy ) {
}

// Prepare and update metadata.
$meta_box_tags_value = serialize( $tags );
$meta_result = update_post_meta( $post_id, 'wp-tag-order-' . $taxonomy, $meta_box_tags_value );
$tag_updater = new \WP_Tag_Order\Tag_Updater();
$result = $tag_updater->update_tag_order( $post_id, $taxonomy, $tags );

// Update post terms.
$term_taxonomy_ids = wp_set_object_terms( $post_id, $tags, $taxonomy );

// Handle metadata update failure.
if ( false === $meta_result ) {
if ( false === $result ) {
return rest_ensure_response(
array(
'success' => false,
Expand Down
1 change: 1 addition & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
*/
function _manually_load_plugin() {
require dirname( __DIR__, 1 ) . '/wp-tag-order.php';
require_once dirname( __DIR__, 1 ) . '/includes/class-tag-updater.php';
}

tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
Expand Down
196 changes: 196 additions & 0 deletions tests/class-test-tag-updater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php
/**
* Test Tag_Updater Class
*
* @package WP_Tag_Order
* @since 3.11.0
*/

declare(strict_types=1);

namespace WP_Tag_Order\Tests;

use WP_Tag_Order\Tag_Updater;
use WP_UnitTestCase;

/**
* Test cases for Tag_Updater class
*/
class Test_Tag_Updater extends WP_UnitTestCase {
/**
* Tag_Updater instance
*
* @var Tag_Updater
*/
private $tag_updater;

/**
* Test taxonomy name
*
* @var string
*/
private $test_taxonomy;

/**
* Setup before each test.
* Enables specific taxonomy for the plugin.
*/
protected function setUp(): void {
parent::setUp();
$this->tag_updater = new Tag_Updater();
$this->test_taxonomy = 'post_tag';

// Ensure the test taxonomy is enabled for the plugin.
add_filter( 'wto_is_enabled_taxonomy', array( $this, 'enable_test_taxonomy' ), 10, 1 );

// Register test taxonomy if not already registered.
if ( ! taxonomy_exists( $this->test_taxonomy ) ) {
register_taxonomy( $this->test_taxonomy, 'post' );
}

// Enable specific taxonomy for the test.
$enabled_taxonomies = array( 'post_tag' );
update_option( 'wpto_enabled_taxonomies', $enabled_taxonomies );
}

/**
* Teardown after each test.
* Resets the enabled taxonomies option.
*/
protected function tearDown(): void {
// Remove the filter after the test.
remove_filter( 'wto_is_enabled_taxonomy', array( $this, 'enable_test_taxonomy' ) );

// Reset the enabled taxonomies option.
delete_option( 'wpto_enabled_taxonomies' );

parent::tearDown();
}

/**
* Mock method to enable test taxonomy for the plugin
*
* @param string $taxonomy Taxonomy name.
* @return bool
*/
public function enable_test_taxonomy( $taxonomy ): bool {
return $taxonomy === $this->test_taxonomy;
}

/**
* Test successful tag order update
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_array(): void {
// Create a test post.
$post_id = $this->factory()->post->create();

// Create test tags.
$tag_ids = array(
$this->factory()->term->create( array( 'taxonomy' => $this->test_taxonomy ) ),
$this->factory()->term->create( array( 'taxonomy' => $this->test_taxonomy ) ),
$this->factory()->term->create( array( 'taxonomy' => $this->test_taxonomy ) ),
);

// Update tag order.
$result = $this->tag_updater->update_tag_order( $post_id, $this->test_taxonomy, $tag_ids );

$this->assertNotFalse( $result, 'Failed to update metadata for tag order' );

// Verify meta was saved correctly.
$saved_tags = unserialize( get_post_meta( $post_id, 'wp-tag-order-' . $this->test_taxonomy, true ) );
$this->assertSame( $tag_ids, $saved_tags );
}

/**
* Test tag order update with comma-separated string
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_string(): void {
// Create a test post.
$post_id = $this->factory()->post->create();

// Create test tags.
$tag_ids = array(
$this->factory()->term->create( array( 'taxonomy' => $this->test_taxonomy ) ),
$this->factory()->term->create( array( 'taxonomy' => $this->test_taxonomy ) ),
);

$tag_ids_string = implode( ',', $tag_ids );

// Update tag order.
$result = $this->tag_updater->update_tag_order( $post_id, $this->test_taxonomy, $tag_ids_string );

$this->assertNotFalse( $result, 'Failed to update metadata for tag order' );

// Verify meta was saved correctly.
$saved_tags = unserialize( get_post_meta( $post_id, 'wp-tag-order-' . $this->test_taxonomy, true ) );
$this->assertSame( $tag_ids, $saved_tags );
}

/**
* Test invalid post ID throws exception
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_invalid_post_id(): void {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Invalid post ID' );

$this->tag_updater->update_tag_order( 0, $this->test_taxonomy, array( 1 ) );
}

/**
* Test empty taxonomy throws exception
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_empty_taxonomy(): void {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Taxonomy cannot be empty' );

$post_id = $this->factory()->post->create();
$this->tag_updater->update_tag_order( $post_id, '', array( 1 ) );
}

/**
* Test non-existent taxonomy throws exception
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_non_existent_taxonomy(): void {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Invalid taxonomy: non_existent_taxonomy' );

$post_id = $this->factory()->post->create();
$this->tag_updater->update_tag_order( $post_id, 'non_existent_taxonomy', array( 1 ) );
}

/**
* Test empty tag IDs throws exception
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_empty_tag_ids(): void {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Tag IDs cannot be empty' );

$post_id = $this->factory()->post->create();
$this->tag_updater->update_tag_order( $post_id, $this->test_taxonomy, array() );
}

/**
* Test invalid tag ID throws exception
*
* @covers Tag_Updater::update_tag_order
*/
public function test_update_tag_order_with_invalid_tag_id(): void {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'Invalid tag ID: invalid' );

$post_id = $this->factory()->post->create();
$this->tag_updater->update_tag_order( $post_id, $this->test_taxonomy, array( 'invalid' ) );
}
}
1 change: 1 addition & 0 deletions wp-tag-order.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function my_plugin_row_meta( array $plugin_meta, string $plugin_file, array $plu

if ( wptagorder_phpversioncheck() ) {
require_once plugin_dir_path( __FILE__ ) . 'includes/functions.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/class-tag-updater.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/category-template.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/index.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/rest-api.php';
Expand Down

0 comments on commit 5d668b3

Please sign in to comment.