Page 1 of 1

REST API PHP Library

Posted: Thu Apr 13, 2017 12:01 pm
by mavieo
Through days of trial and error, working with support, debugging, etc., I finally have a pretty simple PHP library for authenticating and handling candidate entities (including raw resume upload) through the REST API.

Hoping this will assist someone new to the API with a practical implementation.

Library

Code: Select all

<?php

class BullhornAPI {
	const API_USERNAME = '[created via support ticket]';
	const API_PASSWORD = '[set in admin]';
	const CLIENT_ID = '[created via support ticket]';
	const CLIENT_SECRET = '[created via support ticket]';
	const ENDPOINT_AUTH_CODE = 'https://auth.bullhornstaffing.com/oauth/authorize?%s';
	const ENDPOINT_ACCESS_TOKEN = 'https://rest-west.bullhornstaffing.com/oauth/token?%s';
	const ENDPOINT_REST_TOKEN = 'https://rest.bullhornstaffing.com/rest-services/login?version=*&access_token=%s';
	const PUBLIC_ERROR = 'We\'re unable to process your application at this time. Contact a site administrator to learn more.';
	const RESUME_FILE_TYPE = 'Resume'; # Must exist in BH account
	const REWIND_CREATE_ON_RESUME_FAIL = TRUE;
	const SHOW_LOG = FALSE;

	private $access_token;
	private $auth_code;
	private $comm_response;
	private $comm_response_info;
	private $errors = array();
	private $is_dev = FALSE;
	private $refresh_token;
	private $rest_token;
	private $rest_endpoint_url;

	public function __construct() {
		if(function_exists('is_dev')) :
			$this->is_dev = (bool) is_dev();
		endif;
	}

	public function candidateAttachResume($candidate_id, $resume_path=NULL, $resume_filename=NULL) {
		if(TRUE !== $this->oAuth()) : 
			return FALSE;
		endif;

		if(in_array(self::RESUME_FILE_TYPE, array(NULL, ''))) : 
			$this->errorsSet('Empty resume file type.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		if(NULL === $resume_path) : 
			$this->errorsSet('Empty resume path.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		if(!is_readable($resume_path)) : 
			$this->errorsSet('Unable to locate resume.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		if(function_exists('finfo_open')) : 
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
			$content_type = finfo_file($finfo, $resume_path);
			finfo_close($finfo);
		else : 
			$content_type = '';
		endif;

		$comm_args = array(
			'externalID' => 'PORTFOLIO',
			'fileType' => self::RESUME_FILE_TYPE,
			'description' => 'Candidate resume',
			'type' => 'resume',
			'content_type' => $content_type,
			'file' => array(
				'filename' => empty($resume_filename) || !is_string($resume_filename) ? 'resume.txt' : trim($resume_filename),
				'name' => 'Resume',
				'path' => $resume_path,
			),
		);

		if(FALSE === $this->comm(sprintf('file/Candidate/%d/raw', $candidate_id), 'FILE', $comm_args)) : 
			return FALSE;
		endif;

		if(!isset($this->comm_response['fileId'])) : 
			$this->errorsSet('fileId not present in response', __LINE__, __METHOD__);
			return FALSE;
		endif;

		return TRUE;
	}

	public function candidateCreate($args=array(), $resume_path=NULL, $resume_filename=NULL) {
		if(TRUE !== $this->oAuth()) : 
			return FALSE;
		endif;

		if(FALSE === ($comm_args = self::candidatePrepData($args))) : 
			return FALSE;
		endif;

		if(FALSE === $this->comm('entity/Candidate', 'PUT', $comm_args)) : 
			return FALSE;
		endif;

		if(NULL !== $resume_path) : 
			$candidate_id = $this->comm_response['changedEntityId'];

			if(FALSE === $this->candidateAttachResume($candidate_id, $resume_path, $resume_filename)) : 
				if(TRUE === self::REWIND_CREATE_ON_RESUME_FAIL) : 
					$this->candidateDelete($candidate_id);
				endif;

				return FALSE;
			endif;
		endif;

		return TRUE;
	}

	public function candidateDelete($candidate_ids=NULL) {
		if(TRUE !== $this->oAuth()) : 
			return FALSE;
		endif;

		$candidate_ids = !is_array($candidate_ids) ? explode(',', $candidate_ids) : $candidate_ids;
		$candidate_ids = array_map('trim', $candidate_ids);
		$candidate_ids = array_filter($candidate_ids, 'strlen');
		$candidate_ids = array_unique($candidate_ids);

		foreach($candidate_ids as $candidate_id) : 
			if(!is_numeric($candidate_id)) : 
				ob_start(); var_dump($candidate_id);

				$this->errorsSet(array(
					'Delete aborted: Invalid candidate id',
					'Candidate id: '.ob_get_clean(),
				), __METHOD__, __LINE__);

				continue;
			endif;

			$this->log('Deleting candidate '.$candidate_id);

			$comm_args = array(
				'isDeleted' => '1',
			);

			if(FALSE === $this->comm('entity/Candidate/'.$candidate_id, 'POST', $comm_args)) : 
				$this->log('Failed to delete candidate '.$candidate_id);
			endif;
		endforeach;

		return 0 === sizeof($this->errors);
	}

	public function candidateFind($args=array(), $return_ids=FALSE) {
		if(TRUE !== $this->oAuth()) : 
			return FALSE;

		endif;

		$args = array_merge(array(
			'first_name' => '',
			'last_name' => '',
			'email' => '',
			'is_deleted' => '0',
		), $args);

		$select_args = array(
			'and' => $args,
		);

		if(NULL === ($query = $this->searchBuildQuery($select_args, array('id', 'first_name', 'last_name', 'email')))) : 
			$this->errorsSet(array(
				'Failed to build search query',
			), __LINE__, __METHOD__);

			return FALSE;
		endif;

		if(FALSE === $this->comm(sprintf('search/Candidate?%s', $query), 'GET')) : 
			return FALSE;
		endif;

		if(!is_array($this->comm_response) || !isset($this->comm_response['total']) || 0 === $this->comm_response['total'] || !isset($this->comm_response['data'][0])) : 
			return NULL;
		endif;

		$perfect_matches = array();
		foreach($this->comm_response['data'] as $match) : 
			if(1 !== (int) $match['_score']) : 
				continue;
			endif;

			$perfect_matches[] = $match;
		endforeach;

		if(empty($perfect_matches)) : 
			return NULL;
		endif;

		if(FALSE === $return_ids) : 
			return $perfect_matches;
		endif;

		$return = array();
		foreach($perfect_matches as $match) : 
			$return[] = $match['id'];
		endforeach;

		return $return;
	}

	private static function candidatePrepData($data=array()) {
		if(!is_array($data) || empty($data)) : 
			$this->errorsSet('Invalid candidate data', __LINE__, __METHOD__);
			return FALSE;
		endif;

		if(isset($data['gender'])) : 
			$data['gender'] = substr($data['gender'], 0, 1);
		endif;

		if(isset($data['veteran'])) : 
			$data['veteran'] = substr($data['veteran'], 0, 1);
		endif;

		if(isset($data['occupation']) && 50 < strlen($data['occupation'])) : 
			$data['occupation'] = substr($data['occupation'], 0, 47).'...';
		endif;

		if(isset($data['referredBy']) && 50 < strlen($data['referredBy'])) : 
			$data['referredBy'] = substr($data['referredBy'], 0, 47).'...';
		endif;

		foreach(array('dateAvailable') as $date_field) : 
			if(isset($data[$date_field]) && !empty($data[$date_field])) : 
				if(FALSE === ($date_field_ts = strtotime($data[$date_field]))) : 
					unset($data[$date_field]);
				else : 
					# Add 12 hours so BH admin doesn't use the incorrect day...
					# 01/01/2018 was being inserted as 12/31/2017
					$date_field_ts += 3600*12;
				
					# Convert it to milliseconds so BH API knows what to do with it
					# Convert it to an int so BH API doesn't complain about invalid scalar
					$date_field_ts = (int) $date_field_ts*1000;
				
					$data[$date_field] = $date_field_ts;
				endif;
			endif;
		endforeach;
		
		return $data;
	}

	public function candidateUpdate($candidate_id=NULL, $args=array(), $resume_path=NULL, $resume_filename=NULL) {
		if(TRUE !== $this->oAuth()) : 
			return FALSE;
		endif;

		if(empty($candidate_id) || !is_numeric($candidate_id)) : 
			ob_start(); var_dump($candidate_id);

			$this->errorsSet(array(
				'Delete aborted: Invalid candidate id',
				'Candidate id: '.ob_get_clean(),
			), __METHOD__, __LINE__);
			
			return FALSE;
		endif;

		if(FALSE === ($comm_args = self::candidatePrepData($args))) : 
			return FALSE;
		endif;

		if(FALSE === $this->comm('entity/Candidate/'.$candidate_id, 'POST', $comm_args)) : 
			return FALSE;
		endif;

		if(NULL !== $resume_path) : 
			if(FALSE === $this->candidateAttachResume($candidate_id, $resume_path, $resume_filename)) : 
				return FALSE;
			endif;
		endif;

		return TRUE;
	}

	private function comm($endpoint_script=NULL, $endpoint_method=NULL, $endpoint_args=array(), $endpoint_url=NULL, $verify_response=TRUE) {
		$endpoint_args = !is_array($endpoint_args) ? array() : $endpoint_args;

		$this->comm_response = NULL;
		$this->comm_response_info = NULL;

		if(NULL === $endpoint_url) : 
			$endpoint_url = $this->rest_endpoint_url.ltrim($endpoint_script, '/');
		endif;

		if(!empty($this->rest_token)) : 
			$bh_token_param = '?BhRestToken='.urlencode($this->rest_token).'&';

			if(strstr($endpoint_url, '?')) : 
				$endpoint_url = str_replace('?', $bh_token_param, $endpoint_url);
			else : 
				$endpoint_url = $endpoint_url.$bh_token_param;
			endif;
		elseif(strstr($endpoint_url, '?')) : 
			$endpoint_url = rtrim($endpoint_url, '&').'&';
		else : 
			$endpoint_url = $endpoint_url.'?';
		endif;

		switch($endpoint_method) : 
			case 'FILE' : 
				if(!isset($endpoint_args['file'])) : 
					$this->errorsSet('missing argument "file"', __LINE__, __METHOD__);
					return FALSE;
				endif;

				$file = $endpoint_args['file'];
				unset($endpoint_args['file']);

				if(FALSE === is_readable($file['path'])) : 
					$this->errorsSet('Failed to locate path', __LINE__, __METHOD__);
					return FALSE;
				else :
					$file['contents'] = file_get_contents($file['path']);
				endif;

				$multipart_name = !isset($file['name']) || empty($file['name']) ? 'Unknown' : $file['name'];
				$multipart_filename = !isset($file['filename']) || empty($file['filename']) ? 'unknown' : $file['filename'];

				$multipart_new_line = "\r\n";
				$multipart_boundary = md5(time());
				$multipart_body  = '--'.$multipart_boundary.$multipart_new_line;
				$multipart_body .= 'Content-Disposition: form-data; name="'.$multipart_name.'"; filename="'.$multipart_filename.'"'.$multipart_new_line;
				$multipart_body .= 'Content-Length: '.strlen($file['contents']).$multipart_new_line;
				$multipart_body .= 'Content-Type: application/octet-stream'.$multipart_new_line;
				$multipart_body .= 'Content-Transfer-Encoding: binary'.$multipart_new_line.$multipart_new_line;
				$multipart_body .= $file['contents'].$multipart_new_line;
				$multipart_body .= '--'.$multipart_boundary.'--'.$multipart_new_line.$multipart_new_line;

				$endpoint_url_qs = empty($endpoint_args) ? '' : http_build_query($endpoint_args);

				$ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $endpoint_url.$endpoint_url_qs);
				curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::ENDPOINT_TIMEOUT);
				curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); # Don't use CURLOPT_PUT (results in a "read timed out" error)
				curl_setopt($ch, CURLOPT_BINARYTRANSFER, TRUE);
				curl_setopt($ch, CURLOPT_POSTFIELDS, $multipart_body);
				curl_setopt($ch, CURLOPT_HTTPHEADER, array(
					'Content-Type: multipart/form-data; boundary='.$multipart_boundary,
				));
			break;
			case 'GET' : 
				$endpoint_url_qs = empty($endpoint_args) ? '' : http_build_query($endpoint_args);

				$ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $endpoint_url.$endpoint_url_qs);
				curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::ENDPOINT_TIMEOUT);
				curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
			break;
			case 'POST' : 
				$ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $endpoint_url);
				curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::ENDPOINT_TIMEOUT);
				curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($ch, CURLOPT_POST, TRUE);

				if(!empty($endpoint_args)) : 
					$endpoint_args_string = json_encode($endpoint_args);

					curl_setopt($ch, CURLOPT_POSTFIELDS, $endpoint_args_string);
					curl_setopt($ch, CURLOPT_HTTPHEADER, array(
						'Content-Type: application/json', 
						'Content-Length: '.strlen($endpoint_args_string),
					));
				endif;
			break;
			case 'PUT' : 
				$ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $endpoint_url);
				curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::ENDPOINT_TIMEOUT);
				curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); # Don't use CURLOPT_PUT (results in a "read timed out")

				if(!empty($endpoint_args)) : 
					$endpoint_args_string = json_encode($endpoint_args);

					curl_setopt($ch, CURLOPT_POSTFIELDS, $endpoint_args_string);
					curl_setopt($ch, CURLOPT_HTTPHEADER, array(
						'Content-Type: application/json', 
						'Content-Length: '.strlen($endpoint_args_string),
					));
				endif;
			break;
			default : 
				ob_start();
				var_dump($method);
	
				$this->errorsSet(array(
					'Comm error (method)',
					'Invalid endpoint method ('.ob_get_clean().')',
				), __LINE__, __METHOD__);
	
				return FALSE;
			break;
		endswitch;

		$this->comm_response = curl_exec($ch);
		$this->comm_response_info = curl_getinfo($ch);
		curl_close($ch);

		if(FALSE === $this->comm_response) : 
			$this->errorsSet(array(
				'Comm error (cURL)',
				'Endpoint: '.$endpoint_url,
				'REST method: '.$endpoint_method,
				'REST args: '.((empty($endpoint_args)) ? 'NULL' : json_encode($endpoint_args)),
				'Error: '.curl_error($ch),
			), __LINE__, __METHOD__);

			return FALSE;
		endif;

		if(TRUE === $verify_response) : 
			if(NULL === ($this->comm_response = json_decode($this->comm_response, TRUE))) : 
				$this->errorsSet(array(
					'Comm error (Failed to decode response)',
					'Endpoint: '.$endpoint_url,
					'REST method: '.$endpoint_method,
					'REST args: '.((empty($endpoint_args)) ? 'NULL' : json_encode($endpoint_args)),
					'Response: '.$this->comm_response,
					'Response info: '.json_encode($this->comm_response_info),
				), __LINE__, __METHOD__);

				return FALSE;
			endif;

			if(!is_array($this->comm_response)) : 
				$this->errorsSet(array(
					'Comm error (Response is not an array)',
					'Endpoint: '.$endpoint_url,
					'REST method: '.$endpoint_method,
					'REST args: '.((empty($endpoint_args)) ? 'NULL' : json_encode($endpoint_args)),
					'Response: '.$this->comm_response,
					'Response info: '.json_encode($this->comm_response_info),
				), __LINE__, __METHOD__);

				return FALSE;
			endif;
		endif;

		if(is_array($this->comm_response) && isset($this->comm_response['error']) || isset($this->comm_response['errorCode'])) : 
			$this->errorsSet(array(
				'Comm error (Endpoint error)',
				'Endpoint: '.$endpoint_url,
				'REST method: '.$endpoint_method,
				'REST args: '.((empty($endpoint_args)) ? 'NULL' : json_encode($endpoint_args)),
				'Response: '.json_encode($this->comm_response),
				'Response info: '.json_encode($this->comm_response_info),
			), __LINE__, __METHOD__);

			return FALSE;
		endif;

		return $this->comm_response;
	}

	public function errorsGetAll() {
		return $this->errors;
	}

	public function errorsGetLast() {
		return empty($this->errors) ? NULL : end($this->errors);
	}

	private function errorsSet($message=NULL, $line=NULL, $method=NULL) {
		if(TRUE !== $this->is_dev) : 
			$message = self::PUBLIC_ERROR;
		endif;

		$message = !is_array($message) ? array($message) : $message;
		$prefix = '';
		$suffix = '';

		if(TRUE !== $this->is_dev) : 
			$prefix = empty($line) ? '' : '[Err'.$line.']';
		else : 
			array_unshift($message, 'Method: '.$method, 'Line: '.$line);
		endif;

		$message = implode('<br>', $message);
		$message = trim($prefix.' '.$message.' '.$suffix);

		$this->errors[] = array('method' => $method, 'line' => $line, 'message' => $message);
		return TRUE;
	}

	private function log($msg) {
		if(FALSE === $this->is_dev || FALSE === self::SHOW_LOG) : 
			return;
		endif;

		echo $msg.'<br>';
	}

	private function oAuth() {
		if('' === self::CLIENT_ID || '' === self::CLIENT_SECRET || '' === self::API_USERNAME || '' === self::API_PASSWORD) : 
			$this->errorsSet(array(
				'Empty client id, client secret, api username, or api password.', 
				'Follow instructions located at: http://developer.bullhorn.com/articles/getting_started',
				'Hint: Request API access via support ticket',
			), __LINE__, __METHOD__);

			return FALSE;
		endif;

		if(TRUE !== $this->oAuthSetRestToken()) : 
			return FALSE;
		endif;

		if(TRUE !== $this->oAuthLogin()) :
			return FALSE;
		endif;

		return TRUE;
	}

	private function oAuthLogin() {
		if(FALSE === $this->comm(NULL, 'POST', $args, sprintf(self::ENDPOINT_REST_TOKEN, urlencode($this->access_token)))) : 
			return FALSE;				
		endif;

		$this->rest_endpoint_url = $this->comm_response['restUrl'];
		$this->rest_token = $this->comm_response['BhRestToken'];

		return TRUE;
	}

	private function oAuthSetAuthCode($force=FALSE) {
		if(FALSE === $force && !empty($this->auth_code)) : 
			return TRUE;
		endif;

		$comm_args = array(
			'client_id' => self::CLIENT_ID,
			'response_type' => 'code',
			'username' => self::API_USERNAME,
			'password' => self::API_PASSWORD,
			'action' => 'Login',
		);

		$comm_url = sprintf(self::ENDPOINT_AUTH_CODE, http_build_query($comm_args));

		if(FALSE === $this->comm(NULL, 'GET', NULL, $comm_url, FALSE)) : 
			return FALSE;
		endif;

		if(!empty($this->comm_response_info) && preg_match('@\?code=(.*)&@i', $this->comm_response_info['url'], $auth_code)) : 
			$this->auth_code = urldecode($auth_code[1]);
			$this->log('Setting auth code: "'.$this->auth_code.'"');

			return TRUE;
		endif;

		$this->errorsSet(array(
			'Failed to retreive auth code',
			'Endpoint: '.$comm_url,
			'REST method: GET',
			'REST args: '.((empty($comm_args)) ? 'NULL' : json_encode($comm_args)),
			'Response: '.$this->comm_response,
			'Response info: '.json_encode($this->comm_response_info),
		), __LINE__, __METHOD__);

		return FALSE;
	}

	private function oAuthSetRestToken() {
		if(!empty($this->refresh_token)) : 
			$comm_args = array(
				'grant_type' => 'refresh_token',
				'refresh_token' => $this->refresh_token,
				'client_id' => self::CLIENT_ID,
				'client_secret' => self::CLIENT_SECRET,
			);

			if(FALSE === $this->comm(NULL, 'POST', NULL, sprintf(self::ENDPOINT_ACCESS_TOKEN, http_build_query($comm_args)))) : 
				return FALSE;
			endif;

			$this->log('Setting access token (via refresh): "'.$this->comm_response['access_token'].'"');
			$this->log('Setting refresh token (via refresh): "'.$this->comm_response['refresh_token'].'"');

			$this->access_token = $this->comm_response['access_token'];
			$this->refresh_token = $this->comm_response['refresh_token'];
			
			return TRUE;
		endif;

		if(FALSE === $this->oAuthSetAuthCode()) : 
			return FALSE;
		endif;

		# Fetch access token
		$comm_args = array(
			'grant_type' => 'authorization_code',
			'code' => $this->auth_code,
			'client_id' => self::CLIENT_ID,
			'client_secret' => self::CLIENT_SECRET,
		);

		if(FALSE === $this->comm(NULL, 'POST', NULL, sprintf(self::ENDPOINT_ACCESS_TOKEN, http_build_query($comm_args)))) : 
			return FALSE;
		endif;

		$this->log('Setting access token: "'.$this->comm_response['access_token'].'"');
		$this->log('Setting refresh token: "'.$this->comm_response['refresh_token'].'"');
			
		$this->access_token = $this->comm_response['access_token'];
		$this->refresh_token = $this->comm_response['refresh_token'];

		return TRUE;
	}

	private function searchBuildQuery($query_fields=array(), $select_args=array()) {
		$operator_groups = array('and', 'or', 'custom');

		# internal_field => bullhorn_field
		# This list could be greatly expanded...
		$possible_fields = array(
			'id' => 'id', 
			'ID' => 'id',
			'first_name' => 'firstName', 
			'firstName' => 'firstName', 
			'last_name' => 'lastName', 
			'email' => 'email', 
			'is_deleted' => 'isDeleted',
			'isDeleted' => 'isDeleted',
		);

		if(empty($query_fields)) : 
			$this->errorsSet('Query fields cannot be empty.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		$valid_operator = FALSE;
		foreach($operator_groups as $operator) : 
			if(!isset($query_fields[$operator]) || empty($query_fields[$operator]) || !is_array($query_fields[$operator])) : 
				continue;
			endif;

			$valid_operator = TRUE;
			break;
		endforeach;

		if(FALSE === $valid_operator) : 
			$this->errorsSet('Invalid query fields.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		# Translate $select_args into BH field names
		foreach($select_args as $k => $v) : 
			if(!isset($possible_fields[$v])) : 
				unset($select_args[$operator][$k]);
				continue;
			endif;

			$select_args[$k] = $possible_fields[$v];
		endforeach;

		if(empty($select_args)) : 
			$this->errorsSet('Select cannot be empty.', __LINE__, __METHOD__);
			return FALSE;
		endif;

		$return = array(
			'query' => '',
			'fields' => implode(',', $select_args),
		);

		foreach($query_fields as $operator_group => $operator_group_fields) : 
			$return['query'] .= empty($return['query']) ? '' : ' AND ';

			if('custom' === $operator_group) : 
				$return['query'] .= $operator_group_fields;
				continue;
			endif;
 
			$operator_fields = array();

			foreach($operator_group_fields as $operator_group_field => $operator_group_field_value) : 
				if(!isset($possible_fields[$operator_group_field]) || in_array(trim($operator_group_field_value), array(NULL, ''))) : 
					continue;
				endif;

				$operator_fields[] = $possible_fields[$operator_group_field].':'.$operator_group_field_value;
			endforeach;

			$return['query'] .= '('.implode(' '.strtoupper($operator_group).' ', $operator_fields).')';
		endforeach;

		if(empty($return['query'])) : 
			$this->errorsSet('Invalid query fields', __LINE__, __METHOD__);
			return NULL;
		endif;

		return http_build_query($return);
	}
}

// End BullhornAPI class
Usage

Code: Select all

<?php 
	require_once 'BullhornAPI.php';

	# ...

	if($_POST) : 
		$bh_obj = new BullhornAPI();
		$candidate_data = $_POST;
		$form_status = NULL;

		# Validate resume upload
		if(NULL === $form_status) : 
			if($_FILES) : 
				if(FALSE === ($resume = validateUpload('resume'))) : 
					$form_status = array('status' => 'error', 'message' => uploadError());
					unset($resume);
				endif;
			endif;
		endif;

		# Look for duplicate applicant
		if(NULL === $form_status) : 
			$duplicate_candidates = $bh_obj->candidateFind(array('first_name' => $candidate_data['firstName'], 'last_name' => $candidate_data['lastName'], 'email' => $candidate_data['email']), TRUE);

			if(FALSE === $duplicate_candidates) : 
				if(NULL === ($bh_error = $bh_obj->errorsGetLast())) : 
					$bh_error = '('.__LINE__.') An unknown error occurred';
				endif;

				$form_status = array('status' => 'error', 'message' => $bh_error['message']);
			elseif(NULL !== $duplicate_candidates) : 
				$duplicate_comment = 'DUP:'.implode(',', $duplicate_candidates);

				# "occupation" is the field name, Quick Notes is the field label...
				if(isset($candidate_data['occupation'])) : 
					$candidate_data['occupation'] = trim($candidate_data['occupation'])."\n\n".$duplicate_comment;
				else : 
					$candidate_data['occupation'] = $duplicate_comment;
				endif;
			endif;
		endif;

		# Create the candidate
		if(NULL === $form_status) : 
			$candidate_data = array_merge($candidate_data, array(
				'firstName' => $candidate_data['firstName'],
				'lastName' => $candidate_data['lastName'],
				'name' => trim($candidate_data['firstName'].' '.$candidate_data['lastName']),
				'description' => 'Website applicant',
				'email' => $candidate_data['email'],
				'address' => array(
					'address1' => $candidate_data['address1'],
					'address2' => $candidate_data['address2'],
					'city' => $candidate_data['city'],
					'state' => $candidate_data['state'],
					'zip' => $candidate_data['zip'],
					'countryID' => $candidate_data['countryID'],
				),
			));

			# Ditch keys Bullhorn will complain about
			unset($candidate_data['address1']);
			unset($candidate_data['address2']);
			unset($candidate_data['city']);
			unset($candidate_data['state']);
			unset($candidate_data['zip']);
			unset($candidate_data['countryID']);

			if($has_resume) : 
				$resume_filename = basename($resume['data']['tmp_name']);
				$resume_path = $resume['data']['tmp_name'];
			else : 
				$resume_filename = $resume_path = NULL;
			endif;

			if(FALSE === ($candidate_id = $bh_obj->candidateCreate($candidate_data, $resume_path, $resume_filename))) : 
				if(NULL === ($bh_error = $bh_obj->errorsGetLast())) : 
					$bh_error = '('.__LINE__.') An unknown error occurred';
				endif;

				$form_status = array('status' => 'error', 'message' => $bh_error['message']);
			endif;
		endif;

		if(NULL === $form_status) : 
			redirect('to/success/page');
		endif;

		# Do something with $form_status
	endif;

	# ...