From bc8dc98578f779a25ff3e94a35d07fce2cc8ced3 Mon Sep 17 00:00:00 2001 From: Andre Ruegger Date: Thu, 5 Mar 2026 14:25:13 -0300 Subject: [PATCH 1/2] First code version. --- README.md => apiki-favorites/README.md | 0 apiki-favorites/apiki-favorites.php | 27 +++ apiki-favorites/includes/class-activator.php | 28 +++ .../includes/class-favorites-service.php | 122 +++++++++++ .../includes/class-rest-controller.php | 193 ++++++++++++++++++ apiki-favorites/uninstall.php | 13 ++ 6 files changed, 383 insertions(+) rename README.md => apiki-favorites/README.md (100%) create mode 100644 apiki-favorites/apiki-favorites.php create mode 100644 apiki-favorites/includes/class-activator.php create mode 100644 apiki-favorites/includes/class-favorites-service.php create mode 100644 apiki-favorites/includes/class-rest-controller.php create mode 100644 apiki-favorites/uninstall.php diff --git a/README.md b/apiki-favorites/README.md similarity index 100% rename from README.md rename to apiki-favorites/README.md diff --git a/apiki-favorites/apiki-favorites.php b/apiki-favorites/apiki-favorites.php new file mode 100644 index 00000000..eb9c144c --- /dev/null +++ b/apiki-favorites/apiki-favorites.php @@ -0,0 +1,27 @@ +register_routes(); +} ); \ No newline at end of file diff --git a/apiki-favorites/includes/class-activator.php b/apiki-favorites/includes/class-activator.php new file mode 100644 index 00000000..b1b793f0 --- /dev/null +++ b/apiki-favorites/includes/class-activator.php @@ -0,0 +1,28 @@ +prefix . APIKI_FAV_TABLE_NAME; + $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, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_post (user_id, post_id), + KEY post_id (post_id) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $sql ); + } +} \ No newline at end of file diff --git a/apiki-favorites/includes/class-favorites-service.php b/apiki-favorites/includes/class-favorites-service.php new file mode 100644 index 00000000..8ec2ae88 --- /dev/null +++ b/apiki-favorites/includes/class-favorites-service.php @@ -0,0 +1,122 @@ +prefix . APIKI_FAV_TABLE_NAME; + } + + /** + * Adiciona um post aos favoritos do usuário. + * + * @param int $user_id + * @param int $post_id + * + * @return string status: 'favorited' ou 'already_favorited' + */ + public function add_favorite( $user_id, $post_id ) { + global $wpdb; + + $table_name = $this->get_table_name(); + + $inserted = $wpdb->insert( + $table_name, + [ + 'user_id' => $user_id, + 'post_id' => $post_id, + ], + [ '%d', '%d' ] + ); + + if ( false === $inserted ) { + return 'already_favorited'; + } + + return 'favorited'; + } + + /** + * Remove um post dos favoritos do usuário. + * + * @param int $user_id + * @param int $post_id + * + * @return string status: 'unfavorited' ou 'not_found' + */ + public function remove_favorite( $user_id, $post_id ) { + global $wpdb; + + $table_name = $this->get_table_name(); + + $deleted = $wpdb->delete( + $table_name, + [ + 'user_id' => $user_id, + 'post_id' => $post_id, + ], + [ '%d', '%d' ] + ); + + if ( $deleted ) { + return 'unfavorited'; + } + + return 'not_found'; + } + + /** + * Retorna IDs de posts favoritados pelo usuário. + * + * @param int $user_id + * + * @return int[] + */ + public function get_favorites_by_user( $user_id ) { + global $wpdb; + + $table_name = $this->get_table_name(); + + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT post_id FROM {$table_name} WHERE user_id = %d", + $user_id + ) + ); + + return array_map( 'intval', $post_ids ); + } + + /** + * Verifica se um post está favoritado pelo usuário. + * + * @param int $user_id + * @param int $post_id + * + * @return bool + */ + public function is_favorited( $user_id, $post_id ) { + global $wpdb; + + $table_name = $this->get_table_name(); + + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$table_name} WHERE user_id = %d AND post_id = %d", + $user_id, + $post_id + ) + ); + + return ( $exists > 0 ); + } +} \ No newline at end of file diff --git a/apiki-favorites/includes/class-rest-controller.php b/apiki-favorites/includes/class-rest-controller.php new file mode 100644 index 00000000..90ac161c --- /dev/null +++ b/apiki-favorites/includes/class-rest-controller.php @@ -0,0 +1,193 @@ +service = $service; + } + + /** + * Registra as rotas da REST API. + */ + public function register_routes() { + register_rest_route( + APIKI_FAV_REST_NAMESPACE, + '/favorites', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_favorites' ], + 'permission_callback' => [ $this, 'permissions_check' ], + ], + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'add_favorite' ], + 'permission_callback' => [ $this, 'permissions_check' ], + 'args' => [ + 'post_id' => [ + 'required' => true, + 'validate_callback' => [ $this, 'validate_post_id' ], + 'sanitize_callback' => 'absint', + ], + ], + ], + [ + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => [ $this, 'remove_favorite' ], + 'permission_callback' => [ $this, 'permissions_check' ], + 'args' => [ + 'post_id' => [ + 'required' => true, + 'validate_callback' => [ $this, 'validate_post_id' ], + 'sanitize_callback' => 'absint', + ], + ], + ], + ] + ); + + // Opcional: rota para checar se um post específico está favoritado + register_rest_route( + APIKI_FAV_REST_NAMESPACE, + '/favorites/check', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'check_favorite' ], + 'permission_callback' => [ $this, 'permissions_check' ], + 'args' => [ + 'post_id' => [ + 'required' => true, + 'validate_callback' => [ $this, 'validate_post_id' ], + 'sanitize_callback' => 'absint', + ], + ], + ] + ); + } + + /** + * Garante que o usuário esteja logado. + */ + public function permissions_check( WP_REST_Request $request ) { + if ( ! is_user_logged_in() ) { + return new WP_Error( + 'rest_forbidden', + __( 'Você precisa estar logado para usar favoritos.', 'apiki-favorites' ), + [ 'status' => 401 ] + ); + } + + return true; + } + + /** + * Valida se o post existe. + */ + public function validate_post_id( $param, WP_REST_Request $request, $key ) { + $post = get_post( (int) $param ); + return ( $post instanceof WP_Post ); + } + + /** + * GET /favorites + * Lista os posts favoritados do usuário logado. + */ + public function get_favorites( WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $post_ids = $this->service->get_favorites_by_user( $user_id ); + + $favorites = []; + + foreach ( $post_ids as $post_id ) { + $post = get_post( $post_id ); + if ( ! $post ) { + continue; + } + + $favorites[] = [ + 'id' => $post->ID, + 'title' => get_the_title( $post ), + 'link' => get_permalink( $post ), + ]; + } + + return rest_ensure_response( + [ + 'user_id' => $user_id, + 'favorites' => $favorites, + ] + ); + } + + /** + * POST /favorites + * Adiciona um post aos favoritos. + */ + public function add_favorite( WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $post_id = (int) $request->get_param( 'post_id' ); + + $status = $this->service->add_favorite( $user_id, $post_id ); + + $http_status = ( 'favorited' === $status ) ? 201 : 200; + + return new WP_REST_Response( + [ + 'status' => $status, + 'user_id' => $user_id, + 'post_id' => $post_id, + ], + $http_status + ); + } + + /** + * DELETE /favorites + * Remove um post dos favoritos. + */ + public function remove_favorite( WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $post_id = (int) $request->get_param( 'post_id' ); + + $status = $this->service->remove_favorite( $user_id, $post_id ); + + $http_status = ( 'unfavorited' === $status ) ? 200 : 404; + + return new WP_REST_Response( + [ + 'status' => $status, + 'user_id' => $user_id, + 'post_id' => $post_id, + ], + $http_status + ); + } + + /** + * GET /favorites/check?post_id=123 + * Verifica se um post está favoritado. + */ + public function check_favorite( WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $post_id = (int) $request->get_param( 'post_id' ); + + $is_favorited = $this->service->is_favorited( $user_id, $post_id ); + + return rest_ensure_response( + [ + 'user_id' => $user_id, + 'post_id' => $post_id, + 'is_favorited' => $is_favorited, + ] + ); + } +} \ No newline at end of file diff --git a/apiki-favorites/uninstall.php b/apiki-favorites/uninstall.php new file mode 100644 index 00000000..68d2738e --- /dev/null +++ b/apiki-favorites/uninstall.php @@ -0,0 +1,13 @@ +prefix . 'apiki_favorites'; + +// Cuidado: em produção, às vezes não se remove dados. +// Mas para o desafio, faz sentido limpar. +$wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); \ No newline at end of file From ab8f2e158ab0c47277c7c317b2e91956183e2dfb Mon Sep 17 00:00:00 2001 From: Andre Ruegger Date: Thu, 5 Mar 2026 16:32:24 -0300 Subject: [PATCH 2/2] Updated comments to english. --- apiki-favorites/apiki-favorites.php | 4 ++-- .../includes/class-favorites-service.php | 14 +++++++------- .../includes/class-rest-controller.php | 16 ++++++++-------- apiki-favorites/uninstall.php | 3 +-- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apiki-favorites/apiki-favorites.php b/apiki-favorites/apiki-favorites.php index eb9c144c..71278e87 100644 --- a/apiki-favorites/apiki-favorites.php +++ b/apiki-favorites/apiki-favorites.php @@ -1,9 +1,9 @@ prefix . 'apiki_favorites'; -// Cuidado: em produção, às vezes não se remove dados. -// Mas para o desafio, faz sentido limpar. +// Don't do this in production. $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); \ No newline at end of file