diff --git a/include/PhorgeAPI.php b/include/PhorgeAPI.php new file mode 100644 index 0000000..a097e27 --- /dev/null +++ b/include/PhorgeAPI.php @@ -0,0 +1,391 @@ +. + +/** + * Utility to run HTTP queries against Phabricator / Phorge + */ +class PhorgeAPI { + + /** + * Query a single Phabricator Project by its slug + * + * If this is not possible, it throws an exception. + * + * @param $slug string Example 'foo_bar' + * @return mixed + */ + public static function querySingleProjectBySlug( $slug ) { + return self::querySingle( 'project.search', [ + 'constraints' => [ + 'slugs' => [ $slug ], + ], + ] ); + } + + /** + * Query a single Phabricator Project by its PHID + * + * If this is not possible, it throws an exception. + * + * @param $phid string + * @return mixed + */ + public static function querySingleProjectByPHID( $phid ) { + return self::querySingle( 'project.search', [ + 'constraints' => [ + 'phids' => [ $phid ], + ], + ] ); + } + + /** + * Query multiple Phabricator projects by their PHIDs + * + * @param $phids array + * @return mixed + */ + public static function queryProjectsByPHIDs( $phids ) { + return self::query( 'project.search', [ + 'constraints' => [ + 'phids' => $phids, + ], + ] ); + } + + /** + * Query a single Phabricator Project's Column by its PHID + * + * If this is not possible, it throws an exception. + * + * @param $phid string + * @return mixed + */ + public static function querySingleProjectColumnByPHID( $phid ) { + return self::querySingle( 'project.column.search', [ + 'constraints' => [ + 'phids' => [ $phid ], + ], + ] ); + } + + /** + * Query the Parent Tasks PHIDs from a Phabricator Task PHID + * + * @param $phid string + * @return array + */ + public static function queryParentTaskPHIDsFromTaskPHID( $phid ) { + $parents = []; + $response = self::query( 'edge.search', [ + 'sourcePHIDs' => [ $phid ], + 'types' => [ 'task.parent' ], + ] ); + foreach( $response['data'] as $result ) { + $parents[] = $result['destinationPHID']; + } + return $parents; + } + + /** + * Try to guess a single Phabricator Project by its human name + * + * If this is not possible, NULL is returned. + * + * @param string $slug Example 'Foo Bar' + * @return mixed + */ + public static function guessPhabricatorTagFromHumanName( $name ) { + $name = str_replace( ' ', '_', $name ); + $name = strtolower( $name ); + $project = null; + + try { + $project = self::querySingleProjectBySlug( $name ); + } catch( Exception $e ) { + // do nothing + } + + return $project; + } + + /** + * Extract some Column objects from a Phabricator Task object + * + * @param $task mixed + * @return array + */ + public static function getColumnsFromTaskObject( $task ) { + $all_columns = []; + foreach( $task['attachments']['columns']['boards'] as $project_phid => $data ) { + foreach( $data['columns'] as $column ) { + // expose the Project PHID + $column['projectPHID'] = $project_phid; + + // expose the full column object + $all_columns[] = $column; + } + } + return $all_columns; + } + + /** + * Extract some Column PHIDs from a Phabricator Task object + * + * @param $task mixed + * @return array + */ + public static function getColumnPHIDsFromTaskObject( $task ) { + $column_phids = []; + foreach( self::getColumnsFromTaskObject( $task ) as $column ) { + $column_phids[] = $column['phid']; + } + return $column_phids; + } + + /** + * Extract Project PHIDs from a Phabricator Task object + * + * @param $task mixed + * @return array + */ + public static function getProjectPHIDsFromTaskObject( $task ) { + return $task['attachments']['projects']['projectPHIDs']; + } + + public static function query( $entry_point, $query = [] ) { + $client = new ConduitClient( PHABRICATOR_URL ); + $client->setConduitToken( PHABRICATOR_CONDUIT_API_TOKEN ); + return $client->callMethodSynchronous( $entry_point, $query ); + } + + /** + * Simplified query to the maniphest.edit Conduit Phabricator API + * + * https://sviluppo.erinformatica.it/conduit/method/maniphest.edit/ + * + * @param $transaction_values array Example: [ 'title' => 'ASD' ] + * @param $extra_query array Example: ['objectIdentifier' => 'PHID--...'] to edit + * @return mixed + */ + public static function createTask( $transaction_values = [], $query = [] ) { + + // build transactions + $query['transactions'] = []; + foreach( $transaction_values as $key => $value ) { + $query['transactions'][] = self::transaction( $key, $value ); + } + + return self::query( 'maniphest.edit', $query ); + } + + /** + * Simplified query to the maniphest.edit Conduit Phabricator API + * + * https://sviluppo.erinformatica.it/conduit/method/maniphest.edit/ + * + * @param $id string Task ID (e.g. 123 for T123) + * @param $transaction_values array Example: [ 'title' => 'ASD' ] + * @return mixed + */ + public static function editTask( $id, $transaction_values = [] ) { + return self::createTask( $transaction_values, [ + 'objectIdentifier' => $id, + ] ); + } + + /** + * Check if a Phabricator status is equivalent to "closed" + * + * @return self + */ + public static function isStatusClosed( $task_status ) { + // TODO: read from "maniphest.status.search" and set in cache and read from there + $closed_statuses = [ + 'resolved', + 'wontfix', + 'invalid', + 'duplicate', + 'spite', + ]; + return in_array( $task_status, $closed_statuses, true ); + } + + /** + * Get a Task by its ID + * + * It returns just one element. + * + * https://sviluppo.erinformatica.it/conduit/method/maniphest.search/ + * + * @param $task_id mixed + */ + public static function getTaskByID( $task_id ) { + + $task_id = self::sanitize_task_id( $task_id ); + + $query = [ + 'constraints' => [ + 'ids' => [ $task_id ], + ], + 'attachments' => [ + 'columns' => true, + 'projects' => true, + ], + ]; + + return self::searchSingleTask( $query ); + } + + public static function getUserByPHID( $phid ) { + $query = [ + 'constraints' => [ + 'phids' => [ $phid ], + ], + ]; + return self::querySingle( 'user.search', $query ); + } + + public static function getUserByUsername( $username ) { + $query = [ + 'constraints' => [ + 'usernames' => [ $username ], + ], + ]; + return self::querySingle( 'user.search', $query ); + } + + public static function getTaskByPHID( $phid ) { + $query = [ + 'constraints' => [ + 'phids' => [ $phid ], + ], + 'attachments' => [ + 'columns' => true, + 'projects' => true, + ], + ]; + return self::searchSingleTask( $query ); + } + + public static function searchSingleUserByPHID( $phid ) { + $query = [ + 'constraints' => [ + 'phids' => [ $phid ], + ], + ]; + return self::searchSingleUser( $query ); + } + + public static function searchSingleTask( $query ) { + return self::querySingle( 'maniphest.search', $query ); + } + + public static function searchSingleUser( $query ) { + return self::querySingle( 'user.search', $query ); + } + + /** + * Return a single element from Phabricator or throw an exception + * + * @param $method string + * @param $query array + * @return mixed + */ + public static function querySingle( $method, $query ) { + + $results = self::query( $method, $query ); + + // just the first one is OK + foreach( $results['data'] as $entry ) { + return $entry; + } + + throw new Exception( "Phabricator result not found from $method using: " . json_encode( $query ) ); + } + + public static function searchObjectTransactionsFromTransactions( $phab_object_id, $transactions ) { + $transaction_phids = []; + foreach( $transactions as $transaction ) { + $transaction_phids[] = $transaction['phid']; + } + return self::searchObjectTransactionsFromPHIDs( $phab_object_id, $transaction_phids ); + } + + public static function searchObjectTransactionsFromPHIDs( $phab_object_id, $transaction_phids ) { + + $query = [ + 'objectIdentifier' => $phab_object_id, + 'constraints' => [ + 'phids' => $transaction_phids, + ], + ]; + $results = self::query( 'transaction.search', $query ); + + return $results['data']; + } + + public static function extractStoryPointsInMillisecondsFromPhabTaskData( $task_data ) { + $points_raw = $task_data['fields']['points'] ?? null; + return self::storyPointsToMilliseconds( $points_raw ); + } + + /** + * Convert a Phabricator "Story Points" field to milliseconds (or zero) + * + * @param $story_points mixed + * @return int Seconds of time extimated + */ + public static function storyPointsToMilliseconds( $story_points ) { + + $ms = 0; + if( $story_points ) { + $points_float = (float) $story_points; + $ms_float = $points_float * 60 * 60 * 1000; + $ms = (int) $ms_float; + } + + return $ms; + } + + private static function transaction( $type, $value ) { + return [ + 'type' => $type, + 'value' => $value, + ]; + } + + /** + * Sanitize a Task ID + * + * @param $task_id mixed + * @return int + */ + public static function sanitize_task_id( $task_id ) { + + // strip the damn 'T' since the 'id' API only accepts numeric + $task_id = ltrim( $task_id, 'T' ); + + // no numeric no party + $task_id = (int)$task_id; + if( !$task_id ) { + throw new Exception( "invalid Task ID" ); + } + + return $task_id; + } + +}