From 930db01c903c83d9179d91a30b0b151057347497 Mon Sep 17 00:00:00 2001 From: elopes-sv Date: Fri, 3 Apr 2026 22:54:12 -0300 Subject: [PATCH 1/2] feat(favorites): implementa plugin de favoritos via REST API --- apiki-favorites.php | 30 +++++ includes/class-activator.php | 26 ++++ includes/class-favorites-repository.php | 63 +++++++++ includes/class-favorites-rest-controller.php | 129 +++++++++++++++++++ includes/class-plugin.php | 21 +++ 5 files changed, 269 insertions(+) create mode 100644 apiki-favorites.php create mode 100644 includes/class-activator.php create mode 100644 includes/class-favorites-repository.php create mode 100644 includes/class-favorites-rest-controller.php create mode 100644 includes/class-plugin.php diff --git a/apiki-favorites.php b/apiki-favorites.php new file mode 100644 index 00000000..e8b24e21 --- /dev/null +++ b/apiki-favorites.php @@ -0,0 +1,30 @@ +run(); +} + +apiki_favorites_run_plugin(); diff --git a/includes/class-activator.php b/includes/class-activator.php new file mode 100644 index 00000000..ca79ba55 --- /dev/null +++ b/includes/class-activator.php @@ -0,0 +1,26 @@ +prefix . 'apiki_favorites'; + $charset_collate = $wpdb->get_charset_collate(); + $sql = "CREATE TABLE {$table_name} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + user_id bigint(20) unsigned NOT NULL, + post_id bigint(20) unsigned NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY user_post (user_id, post_id) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + dbDelta( $sql ); + } +} diff --git a/includes/class-favorites-repository.php b/includes/class-favorites-repository.php new file mode 100644 index 00000000..82a9e710 --- /dev/null +++ b/includes/class-favorites-repository.php @@ -0,0 +1,63 @@ +wpdb = $database ? $database : $wpdb; + $this->table_name = $this->wpdb->prefix . 'apiki_favorites'; + } + + public function exists( $user_id, $post_id ) { + $favorite_id = $this->wpdb->get_var( + $this->wpdb->prepare( + "SELECT id FROM {$this->table_name} WHERE user_id = %d AND post_id = %d LIMIT 1", + $user_id, + $post_id + ) + ); + + return ! empty( $favorite_id ); + } + + public function insert( $user_id, $post_id ) { + $result = $this->wpdb->insert( + $this->table_name, + array( + 'user_id' => $user_id, + 'post_id' => $post_id, + ), + array( + '%d', + '%d', + ) + ); + + return false !== $result; + } + + public function delete( $user_id, $post_id ) { + $result = $this->wpdb->delete( + $this->table_name, + array( + 'user_id' => $user_id, + 'post_id' => $post_id, + ), + array( + '%d', + '%d', + ) + ); + + return false !== $result && $result > 0; + } +} diff --git a/includes/class-favorites-rest-controller.php b/includes/class-favorites-rest-controller.php new file mode 100644 index 00000000..3f1707ce --- /dev/null +++ b/includes/class-favorites-rest-controller.php @@ -0,0 +1,129 @@ +repository = $repository; + } + + public function register_routes() { + register_rest_route( + $this->namespace, + '/posts/(?P\d+)/favorite', + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'favorite' ), + 'permission_callback' => array( $this, 'permission_check' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'unfavorite' ), + 'permission_callback' => array( $this, 'permission_check' ), + ), + ) + ); + } + + public function permission_check( $request ) { + if ( ! is_user_logged_in() ) { + return new WP_Error( + 'apiki_favorites_forbidden', + 'Authentication required.', + array( 'status' => 401 ) + ); + } + + return true; + } + + public function favorite( WP_REST_Request $request ) { + $post_id = (int) $request['post_id']; + $post = $this->get_valid_post( $post_id ); + $user_id = get_current_user_id(); + + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( $this->repository->exists( $user_id, $post_id ) ) { + return new WP_Error( + 'apiki_favorites_conflict', + 'Post already favorited.', + array( 'status' => 409 ) + ); + } + + if ( ! $this->repository->insert( $user_id, $post_id ) ) { + return new WP_Error( + 'apiki_favorites_insert_error', + 'Could not favorite post.', + array( 'status' => 500 ) + ); + } + + return new WP_REST_Response( + array( + 'post_id' => $post_id, + 'favorited' => true, + ), + 201 + ); + } + + public function unfavorite( WP_REST_Request $request ) { + $post_id = (int) $request['post_id']; + $post = $this->get_valid_post( $post_id ); + $user_id = get_current_user_id(); + + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( ! $this->repository->exists( $user_id, $post_id ) ) { + return new WP_Error( + 'apiki_favorites_not_found', + 'Favorite not found.', + array( 'status' => 404 ) + ); + } + + if ( ! $this->repository->delete( $user_id, $post_id ) ) { + return new WP_Error( + 'apiki_favorites_delete_error', + 'Could not unfavorite post.', + array( 'status' => 500 ) + ); + } + + return new WP_REST_Response( + array( + 'post_id' => $post_id, + 'favorited' => false, + ), + 200 + ); + } + + protected function get_valid_post( $post_id ) { + $post = get_post( $post_id ); + + if ( ! $post || 'post' !== $post->post_type ) { + return new WP_Error( + 'apiki_favorites_post_not_found', + 'Post not found.', + array( 'status' => 404 ) + ); + } + + return $post; + } +} diff --git a/includes/class-plugin.php b/includes/class-plugin.php new file mode 100644 index 00000000..3a5148ec --- /dev/null +++ b/includes/class-plugin.php @@ -0,0 +1,21 @@ +repository = new Apiki_Favorites_Repository(); + $this->rest_controller = new Apiki_Favorites_REST_Controller( $this->repository ); + } + + public function run() { + add_action( 'rest_api_init', array( $this->rest_controller, 'register_routes' ) ); + } +} From 2211e4784942c2a300c03507053d023f9b3c6e22 Mon Sep 17 00:00:00 2001 From: elopes-sv Date: Fri, 3 Apr 2026 23:23:42 -0300 Subject: [PATCH 2/2] fix: ajusta respostas em cenarios de concorrencia --- includes/class-favorites-repository.php | 8 ++----- includes/class-favorites-rest-controller.php | 24 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/includes/class-favorites-repository.php b/includes/class-favorites-repository.php index 82a9e710..93561933 100644 --- a/includes/class-favorites-repository.php +++ b/includes/class-favorites-repository.php @@ -30,7 +30,7 @@ public function exists( $user_id, $post_id ) { } public function insert( $user_id, $post_id ) { - $result = $this->wpdb->insert( + return $this->wpdb->insert( $this->table_name, array( 'user_id' => $user_id, @@ -41,12 +41,10 @@ public function insert( $user_id, $post_id ) { '%d', ) ); - - return false !== $result; } public function delete( $user_id, $post_id ) { - $result = $this->wpdb->delete( + return $this->wpdb->delete( $this->table_name, array( 'user_id' => $user_id, @@ -57,7 +55,5 @@ public function delete( $user_id, $post_id ) { '%d', ) ); - - return false !== $result && $result > 0; } } diff --git a/includes/class-favorites-rest-controller.php b/includes/class-favorites-rest-controller.php index 3f1707ce..bc7260ce 100644 --- a/includes/class-favorites-rest-controller.php +++ b/includes/class-favorites-rest-controller.php @@ -62,7 +62,17 @@ public function favorite( WP_REST_Request $request ) { ); } - if ( ! $this->repository->insert( $user_id, $post_id ) ) { + $insert_result = $this->repository->insert( $user_id, $post_id ); + + if ( false === $insert_result ) { + if ( $this->repository->exists( $user_id, $post_id ) ) { + return new WP_Error( + 'apiki_favorites_conflict', + 'Post already favorited.', + array( 'status' => 409 ) + ); + } + return new WP_Error( 'apiki_favorites_insert_error', 'Could not favorite post.', @@ -96,7 +106,17 @@ public function unfavorite( WP_REST_Request $request ) { ); } - if ( ! $this->repository->delete( $user_id, $post_id ) ) { + $delete_result = $this->repository->delete( $user_id, $post_id ); + + if ( 0 === $delete_result ) { + return new WP_Error( + 'apiki_favorites_not_found', + 'Favorite not found.', + array( 'status' => 404 ) + ); + } + + if ( false === $delete_result ) { return new WP_Error( 'apiki_favorites_delete_error', 'Could not unfavorite post.',