<?php

namespace Greenhouse\GreenhouseToolsPhp\Clients;

use Greenhouse\GreenhouseToolsPhp\Clients\ApiClientInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7;
use Greenhouse\GreenhouseToolsPhp\Clients\Exceptions\GreenhouseAPIClientException;
use Greenhouse\GreenhouseToolsPhp\Clients\Exceptions\GreenhouseAPIResponseException;

/**
 * Client to wrap the Guzzle package.
 */
class GuzzleClient implements ApiClientInterface
{
    public $guzzleResponse;
    private $_client;
    private $_nextLink;
    private $_prevLink;
    private $_lastLink;

    /**
     * Constructor should receive an array that would be understood by the Guzzle
     * client constructor.  Constructor hands off an unmodified array to the Guzzle
     * constructor.
     *
     * @params  $options    Array
     */
    public function __construct($options)
    {
        $this->_client = new Client($options);
    }

    /**
     * Fetch the URL. As this is guzzle, this can take a relative URL.  See the Guzzle
     * docs more info.
     *
     * @params  string  $url        A relative URL off the base URL. A full URL
     *                                  should work, too
     * @return  string  The Raw JSON response from Greenhouse
     * @throws  GreenhouseAPIResponseException  if the get request fails
     */
    public function get($url="")
    {
        try {
            $this->guzzleResponse = $this->_client->request('GET', $url);
            $this->_setLinks();
        } catch (RequestException $e) {
            throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e);
        }

        /**
         * Just return the response cast as a string.  The rest of the universe need
         * not be aware of Guzzle's details.
         */
        return (string) $this->guzzleResponse->getBody();
    }

    /**
     * Post to the application endpoint.
     *
     * @params  Array   $postVars       A guzzle compliant multipart array of post parameters.
     * @params  Array   $headers        This should only contain the Authorization header.
     * @params  string  $url            This can be left blank.  Url is set in the constructor.
     * @return  string
     * @throws  GreenhouseAPIResponseException  for non-200 responses
     */
    public function post(Array $postVars, Array $headers, $url=null)
    {
        try {
            $this->guzzleResponse = $this->_client->request(
                'POST',
                $url,
                array('multipart' => $postVars, 'headers' => $headers)
            );
        } catch (RequestException $e) {
            throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e);
        }

        return (string) $this->guzzleResponse->getBody();
    }

    public function send($method, $url, Array $options=array())
    {
        try {
            $this->guzzleResponse = $this->_client->request($method, $url, $options);
            $this->_setLinks();
        } catch (RequestException $e) {
            throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e);
        }

        return (string) $this->guzzleResponse->getBody();
    }

    /**
     * Return a Guzzle post parameter array that can be entered in to the 'multipart'
     * argument of a post request.  For details on this, see the Guzzle
     * documentation here: http://docs.guzzlephp.org/en/latest/request-options.html#multipart
     *
     * @params  Array   $postParameters
     * @return  Array
     */
    public function formatPostParameters(Array $postParameters=array())
    {
        $guzzleParams = array();
        /**
         * There are three possibile inputs for $postParameter values.  Strings, Curlfiles, and arrays.
         * This look should process them in to something that Guzzle understands.
         */
        foreach ($postParameters as $key => $value) {
            if ($value instanceof \CURLFile) {
                $guzzleParams[] = array(
                    'name' => $key,
                    'contents' => fopen($value->getFilename(), 'r'),
                    'filename' => $value->getPostFilename()
                );
            } else if (is_array($value)) {
                foreach ($value as $val) $guzzleParams[] = array('name' => $key . '[]', 'contents' => "$val");
            } else {
                $guzzleParams[] = array('name' => $key, 'contents' => "$value");
            }
        }

        return $guzzleParams;
    }

    public function getClient()
    {
        return $this->_client;
    }

    /**
     * Set the next/prev/last links using the current response object.
     */
    private function _setLinks()
    {
        $links = Psr7\parse_header($this->guzzleResponse->getHeader('Link'));
        foreach ($links as $link) {
            if ($link['rel'] == 'last') {
                $this->_lastLink = str_replace(['<', '>'], '', $link[0]);
            } elseif ($link['rel'] == 'next') {
                $this->_nextLink = str_replace(['<', '>'], '', $link[0]);
            } elseif ($link['rel'] == 'prev') {
                $this->_prevLink = str_replace(['<', '>'], '', $link[0]);
            }
        }
    }

    public function getNextLink()
    {
        return $this->_nextLink;
    }

    public function getPrevLink()
    {
        return $this->_prevLink;
    }

    public function getLastLink()
    {
        return $this->_lastLink;
    }

    public function getResponse()
    {
        return $this->guzzleResponse;
    }
}


<?php

namespace Greenhouse\GreenhouseToolsPhp\Services;

use Greenhouse\GreenhouseToolsPhp\Services\JobApiService;
use Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient;
use Greenhouse\GreenhouseToolsPhp\Tools\JsonHelper;
use Greenhouse\GreenhouseToolsPhp\Services\Exceptions\GreenhouseApplicationException;

/**
 * This class interacts with Greenhouse's job board API.  This class interacts with the GET
 * methods and does not require any API keys.  The constructor takes your job board
 * token, which can be found on the job board configuration page, located
 * here: https://app.greenhouse.io/configure/dev_center/config
 *
 *
 */
class ApplicationService extends ApiService
{
    private $_jobApiService;

    /**
     * The client token is your job board token.  For instance in the following
     * API get URL: https://api.greenhouse.io/v1/boards/example_co/embed/
     * The board token is 'example_co'
     *
     * @param   string  $clientToken    As above
     */
    public function __construct($apiKey, $clientToken)
    {
        $this->_apiKey = $apiKey;
        $this->_clientToken = $clientToken;
        $this->_authorizationHeader = $this->getAuthorizationHeader($apiKey);

        $client = new GuzzleClient(array('base_uri' => self::APPLICATION_URL));
        $this->setClient($client);
        $jobService = new JobApiService($this->_clientToken);
        $this->_jobApiService = $jobService;
    }

    /**
     * Allow the job service to be overwritten by a new JobService.  Mostly for testing.
     *
     * @params  JobApiService   $jobService     A new job service.
     */
    public function setJobApiService(JobApiService $jobService)
    {
        $this->_jobApiService = $jobService;
    }

    /**
     * Post an application to Greenhouse.  The post parameters should be defined in an
     * array similar to the way you would send the item using libcurl.  The difference is
     * that instead of sending multiselect with indexed arrays; ie:
     *
     * 'var[0]' => 'foo',
     * 'var[1]' => 'bar'
     *
     * You will instead send them as an array
     *
     * 'var' => array('foo', 'bar')
     *
     * We'll use the client interface to transform this in to whatever works for the
     * given client and then post the application.
     *
     * Document attachments may also be defined with CurlFile.
     *
     * This method will also verify that required fields are not empty.  As this currently
     * requires verification on the client-side, we will run that verification before we
     * submit anything to the API.
     *
     * @params  Array   $postVars       An array of questions to be posted to the
     *                                      Applications endpoint.
     * @return  boolean
     * @throws  GreenhouseApplicationException  if required fields are not included.
     * @throws  GreenhouseAPIResponseException  if a non-200 response is returned.
     */
    public function postApplication(Array $postVars=array())
    {
        $this->validateRequiredFields($postVars);
        $postParams = $this->_apiClient->formatPostParameters($postVars);
        $headers    = array('Authorization' => $this->_authorizationHeader);

        return $this->_apiClient->post($postParams, $headers);
    }

    /**
     * Method ensures that a non-empty response is contained in all fields marked as required.  If not,
     * an Exception is thrown.
     *
     * @params  Array   $postVars   The Greenhouse-formatted post parameters.
     * @return  boolean
     * @throws  GreenhouseApplicationException
     */
    public function validateRequiredFields($postVars)
    {
        $requiredFields = $this->getRequiredFields($postVars['id']);
        $missingKeys = array();

        foreach ($requiredFields as $human => $keys) {
            if (!$this->hasRequiredValue($postVars, $keys)) {
                $missingKeys[] = $human;
            }
        }

        if (!empty($missingKeys)) {
            throw new GreenhouseApplicationException('Submission missing required answers for: ' . implode(', ', $missingKeys));
        }

        return true;
    }

    /**
     * Since a required field can have multiple possible inputs, this method just checks the
     * field has a value and returns true if it does.  If it finds no values in any of the inputs
     * it returns false.
     *
     * @params  Array   $postVars   Greenhouse post parameters.
     * @params  Array   $keys       The keys to check for in $postVars
     * @return  boolean
     */
    public function hasRequiredValue($postVars, $keys)
    {
        foreach ($keys as $key) {
            $requiredKey = $this->findKey($key, $postVars);
            if (array_key_exists($requiredKey, $postVars) && $postVars[$requiredKey] !== '') return true;
        }

        return false;
    }

    /**
     * This fixes an issue where the Greenhouse API returns required multiselects as <field[]> but we require
     * the user to submit it as <field>, thus making the system think the required field is not set.  This
     * checks that either <field> or <field[]> is set, in order to work correctly but also be backward
     * compatable in case external systems have already corrected for it.
     *
     * @params  Array   $key        The key to check for in $postVars
     * @params  Array   $postVars   Greenhouse post parameters.
     * @return  Mixed   string/null
     */
    public function findKey($key, $postVars)
    {
        $multiselectKey = str_replace('[]', '', $key);
        if (array_key_exists($key, $postVars)) {
            return $key;
        } else if (array_key_exists($multiselectKey, $postVars)) {
            return $multiselectKey;
        } else {
            return null;
        }
    }

    /**
     * Given a job id, make a requrest to the Greenhouse API for those questions and build an associative
     * array indexed on the human-readable name containing an array of the indices that must be set.  The
     * array is due to the fact that one  required question can have one of two things required.  For example,
     * if first name, last name, and resume are required, your response would look like this:
     *
     * <code>
     *      array(
     *          'First Name' => array('first_name'),
     *          'Last Name'  => array('last_name'),
     *          'Resume'     => array('resume', 'resume_text')
     *      );
     * </code>
     *
     * Where either resume or resume_text must have a value.
     *
     * @params  number  $jobId      A Greenhouse job id.
     * @returns Array
     * @throws  GreenhouseAPIResponseException if getJob returns a non-200 response.
     */
    public function getRequiredFields($jobId)
    {
        $job = $this->_jobApiService->getJob($jobId, true);
        $jobHash = JsonHelper::decodeToHash($job);
        $requiredFields = array();

        foreach ($jobHash['questions'] as $question) {
            if ($question['required']) {
                $questionFields = array();
                foreach ($question['fields'] as $field) {
                    $questionFields[] = $field['name'];
                }
                $requiredFields[$question['label']] = $questionFields;
            }
        }

        return $requiredFields;
    }
}


<?php

namespace Greenhouse\GreenhouseToolsPhp\Services;

use Greenhouse\GreenhouseToolsPhp\Services\ApiService;
use Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient;
use Greenhouse\GreenhouseToolsPhp\Services\Exceptions\GreenhouseServiceException;
use Greenhouse\GreenhouseToolsPhp\Tools\HarvestHelper;

/**
 * This class interacts with Greenhouse's Harvest API.
 */
class HarvestService extends ApiService
{
    private $_harvestHelper;
    private $_harvest;

    public function __construct($apiKey)
    {
        $this->_apiKey = $apiKey;
        $client = new GuzzleClient(array('base_uri' => self::HARVEST_V1_URL));
        $this->setClient($client);
        $this->_authorizationHeader = $this->getAuthorizationHeader($apiKey);
        $this->_harvestHelper = new HarvestHelper();
    }

    public function getHarvest()
    {
        return $this->_harvest;
    }

    public function sendRequest()
    {
        $authHeader = array('Authorization' => $this->_authorizationHeader);
        $allHeaders = array_merge($this->_harvest['headers'], $authHeader);
        $requestUrl = $this->_harvestHelper->addQueryString($this->_harvest['url'], $this->_harvest['parameters']);
        $options = array(
            'headers' => $allHeaders,
            'body' => $this->_harvest['body'],
        );

        return $this->_apiClient->send($this->_harvest['method'], $requestUrl, $options);
    }

    /**
     * The following 3 methods are for Harvest Paging. They return paging info from the Link header (if it
     * exists). Paging is complete if 'nextLink' returns nothing.
     */
    public function nextLink()
    {
        return $this->_apiClient->getNextLink();
    }

    public function prevLink()
    {
        return $this->_apiClient->getPrevLink();
    }

    public function lastLink()
    {
        return $this->_apiClient->getLastLink();
    }

    /**
     * In order to keep up to date with changes to the Harvest api and not trigger a re-release of this
     * package each time a new method is created, the magic Call method is used to construct URLs to the
     * Harvest API.  This will use the called method and the arguments provided to create the proper URL
     * to request the service.  This should return the response from the API on success and raise an
     * exception on failure.  In most cases, this should be straightforward parsing.
     *
     * 1) getApplications() will transform in to a Get request to "applications"
     * 2) getApplications(array('id' => 12345)) will translate to a GET request to "applications/12345"
     * 3) getScorecardsForApplications(array('id' => 12345)) will translate to
     *      "applications/12345/scorecards
     */
    public function __call($name, $arguments)
    {
        $args = sizeof($arguments) > 0 ? $arguments[0] : array();
        $this->_harvest = $this->_harvestHelper->parse($name, $args);
        return $this->sendRequest();
    }

    /**
     * All methods below this point are methods that don't fit in the standard url format.  Either the
     * words are not pluralized (application/12345/move instead of moves) or there is an additional word
     * at the end of the URL (application/123/offers/current_offer) which we can't handle in the magic method
     * above.  Standard URLs that fit the common Harvest format will work automatically going forward.  Any
     * exceptions should go below this line.
     */

    public function getActivityFeedForCandidate($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getActivityFeedForCandidate', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function postNoteForCandidate($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postActivityFeedForCandidate', $parameters);
        $this->_harvest['url'] = 'candidates/' . $parameters['id'] . '/activity_feed/notes';
        $this->sendRequest();
    }

    public function putAnonymizeCandidate($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('putAnonymizeForCandidate', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function getJobPostsForJob($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getJobPostForJob', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function getJobStagesForJob($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getStagesForJob', $parameters);
        return $this->sendRequest();
    }

    public function getCurrentOfferForApplication($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getOffersForApplication', $parameters);
        $this->_harvest['url'] = $this->_harvest['url'] . '/current_offer';
        return $this->sendRequest();
    }

    public function postAdvanceApplication($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postAdvanceForApplication', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function postMoveApplication($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postMoveForApplication', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function postTransferApplicationToJob($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postTransferToJobForApplication', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function postRejectApplication($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postRejectForApplication', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function postUnrejectApplication($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postUnrejectForApplication', $parameters);
        return $this->_trimUrlAndSendRequest();
    }

    public function putMergeCandidates($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('putMergeCandidate', $parameters);
        $this->_harvest['url'] = 'candidates/merge';
        $this->sendRequest();
    }

    /**
     * It is explicitely required to do custom field here because we require the trailing slash, because
     * in this case Sinatra didn't give us an option not to.
     */
    public function getCustomFields($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getCustomFields', $parameters);
        if (!array_key_exists('id', $parameters)) $this->_harvest['url'] = $this->_harvest['url'] . '/';
        $this->sendRequest();
    }

    public function getCustomField($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getCustomFields', $parameters);
        $this->_harvest['url'] = 'custom_field/' . $parameters['id'];
        $this->sendRequest();
    }

    public function getCustomFieldOptionsForCustomField($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getCustomFieldOptionsForCustomField', $parameters);
        $this->_harvest['url'] = 'custom_field/' . $parameters['id'] . '/custom_field_options';
        $this->sendRequest();
    }

    public function postCustomFieldOptionsForCustomField($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('postCustomFieldOptionsForCustomField', $parameters);
        $this->_harvest['url'] = 'custom_field/' . $parameters['id'] . '/custom_field_options';
        $this->sendRequest();
    }

    public function deleteCustomFieldOptionsForCustomField($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('deleteCustomFieldOptionsForCustomField', $parameters);
        $this->_harvest['url'] = 'custom_field/' . $parameters['id'] . '/custom_field_options';
        $this->sendRequest();
    }

    public function patchCustomFieldOptionsForCustomField($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('patchCustomFieldOptionsForCustomField', $parameters);
        $this->_harvest['url'] = 'custom_field/' . $parameters['id'] . '/custom_field_options';
        $this->sendRequest();
    }

    public function getEeoc($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getEeoc', $parameters);
        $this->_harvest['url'] = array_key_exists('id', $parameters) ? 'eeoc/' . $parameters['id'] : 'eeoc';
        $this->sendRequest();
    }

    public function putHiringTeamForJob($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('putHiringTeamForJob', $parameters);
        $this->_harvest['url'] = 'jobs/' . $parameters['id'] . '/hiring_team';
        $this->sendRequest();
    }

    public function getCandidateTags($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('getCandidateTags', $parameters);
        $this->_harvest['url'] = 'tags/candidate';
        $this->sendRequest();
    }

    public function patchEnableUser($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('patchEnableUser', $parameters);
        $this->_harvest['url'] = 'users/' . $parameters['id'] . '/enable';
        $this->sendRequest();
    }

    public function patchDisableUser($parameters=array())
    {
        $this->_harvest = $this->_harvestHelper->parse('patchDisableUser', $parameters);
        $this->_harvest['url'] = 'users/' . $parameters['id'] . '/disable';
        $this->sendRequest();
    }

    private function _trimUrlAndSendRequest()
    {
        $this->_harvest['url'] = substr($this->_harvest['url'], 0, -1);
        return $this->sendRequest();
    }
}


<?php

namespace Greenhouse\GreenhouseToolsPhp\Services;

use Greenhouse\GreenhouseToolsPhp\Services\ApiService;
use Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient;
use Greenhouse\GreenhouseToolsPhp\Services\Exceptions\GreenhouseServiceException;

/**
 * This class interacts with Greenhouse's job board API.  This class interacts with the GET
 * methods and does not require any API keys.  The constructor takes your job board
 * token, which can be found on the job board configuration page, located
 * here: https://app.greenhouse.io/configure/dev_center/config
 */
class JobApiService extends ApiService
{
    /**
     * The client token is your job board token.  For instance in the following
     * API get URL: https://api.greenhouse.io/v1/boards/example_co/embed/
     * The board token is 'example_co'
     *
     * @param   string  $clientToken    As above
     */
    public function __construct($clientToken)
    {
        $this->_clientToken = $clientToken;
        $client = new GuzzleClient(array('base_uri' => self::jobBoardBaseUrl($clientToken)));
        $this->setClient($client);
    }

    /**
     * GET $baseUrl/offices
     *
     * @return string   JSON response string from Greenhouse API.
     * @throws GreenhouseAPIResponseException for non-200 responses
     */
    public function getOffices()
    {
        return $this->_apiClient->get('offices');
    }

    /**
     * GET $baseUrl/office?id=$id
     *
     * @param   $id     number      The id of the office to retrieve
     * @return  string  JSON response string from Greenhouse API.
     * @throws  GreenhouseAPIResponseException for non-200 responses
     */
    public function getOffice($id)
    {
        return $this->_apiClient->get("office?id=$id");
    }

    /**
     * GET $baseUrl/departments
     *
     * @return string   JSON response string from Greenhouse API.
     * @throws GreenhouseAPIResponseException for non-200 responses
     */
    public function getDepartments()
    {
        return $this->_apiClient->get('departments');
    }

    /**
     * GET $baseUrl/office?id=$id
     *
     * @param   $id     number      The id of the department to retrieve
     * @return  string  JSON response string from Greenhouse API.
     * @throws  GreenhouseAPIResponseException for non-200 responses
     */
    public function getDepartment($id)
    {
        return $this->_apiClient->get("department?id=$id");
    }

    /**
     * GET $baseUrl     (The Job board name and intro)
     *
     * @return  string  JSON response string from Greenhouse API.
     * @throws  GreenhouseAPIResponseException for non-200 responses
     */
    public function getBoard()
    {
        return $this->_apiClient->get();
    }

    /**
     * GET $baseUrl/jobs(?content=true)
     *
     * @param   boolean     $content    Append the content paramenter to get the
     *                                      job post content, department, and office.
     * @return  string      JSON response string from Greenhouse API.
     * @throws  GreenhouseAPIResponseException for non-200 responses
     */
    public function getJobs($content=false)
    {
        $queryString = $this->getContentQuery('jobs', $content);
        return $this->_apiClient->get($queryString);
    }

    /**
     * GET $baseUrl/job?id=$id(?questions=true)
     *
     * @param   $id         number      The id of the job to retrieve
     * @param   $question   boolean     Append the question paramenter to get the
     *                                      question info in the job response.
     * @return  string      JSON response string from Greenhouse API.
     * @throws  GreenhouseAPIResponseException for non-200 responses
     */
    public function getJob($id, $questions=false)
    {
        $queryString = $this->getQuestionsQuery("job?id=$id", $questions);
        return $this->_apiClient->get($queryString);
    }

    /**
     * Method appends the content parameter to the URL if content is true, returns
     * just the uriString if it's false.
     *
     * @param   string  $uriString      A base string.
     * @param   boolean $showConent     If true, appends ?content=true to $uriString
     * @return  string
     */
    public function getContentQuery($uriString, $showContent=false)
    {
        $queryString = $showContent ? '?content=true' : '';
        return $uriString . $queryString;
    }

    /**
     * Shortcut method appends questions=true to the query string for a single
     */
    public function getQuestionsQuery($uriString, $showQuestions=false)
    {
        $queryString = $showQuestions ? '&questions=true' : '';
        return $uriString . $queryString;
    }
}


<?php

namespace Greenhouse\GreenhouseToolsPhp\Services;

use Greenhouse\GreenhouseToolsPhp\Services\Exceptions\GreenhouseServiceException;

class ApiService
{
    protected $_apiClient;
    protected $_clientToken;
    protected $_apiKey;
    protected $_authorizationHeader;

    const APPLICATION_URL = 'https://api.greenhouse.io/v1/applications/';
    const API_V1_URL = 'https://api.greenhouse.io/v1/';
    const HARVEST_V1_URL = 'https://harvest.greenhouse.io/v1/';

    public function setClient($apiClient)
    {
        $this->_apiClient = $apiClient;
    }

    /**
     * This is in a static method instead of a constant because it has a variable in it.
     * If I wanted to be crafty I could assemble a constant with a variable here but
     * since it's still in one place, who cares?
     *
     * @param   string  $clientToken    Your company's URL token.
     * @return  string
     */
    public static function jobBoardBaseUrl($clientToken)
    {
        return self::API_V1_URL . "boards/{$clientToken}/embed/";
    }

    /**
     * This wraps the above static method so you don't have to call it statically from
     * within the package.
     */
    public function getJobBoardBaseUrl()
    {
        if (empty($this->_clientToken)) {
            throw new GreenhouseServiceException('A client token must be defined to get the base URL.');
        }
        return self::jobBoardBaseUrl($this->_clientToken);
    }

    /**
     * Return whatever client we're using.  This should be something that implements the
     * GreenhouseClientInterface
     */
    public function getClient()
    {
        return $this->_apiClient;
    }

    /**
     * Base64 Encode an API key.  Greenhouse's encoding treats the key as the username with
     * a blank password.  Here we append the : for convenience.  We also handle the case
     * where the user appends the : themselves.  If no key is provided, we will attempt
     * to encode the private api key property.
     *
     * @params  string  $apiKey     A greenhouse job board API key.
     * @return  string  The other side of an Authorization header (Basic: <encoded key>)
     */
    public function getAuthorizationHeader($apiKey="")
    {
        if (empty($apiKey)) {
            if (empty($this->_apiKey)) throw new GreenhouseServiceException('No key provided to encode.');
            $apiKey = $this->_apiKey;
        }
        $key = rtrim($apiKey, ':') . ':';

        return 'Basic ' . base64_encode($key);
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Endpoint\POST;

use SugarAPI\SDK\Endpoint\Abstracts\POST\AbstractPostFileEndpoint;
use SugarAPI\SDK\Exception\Endpoint\RequiredOptionsException;

class ModuleRecordFileField extends AbstractPostFileEndpoint
{
    /**
     * @inheritdoc
     */
    protected $_URL = '$module/$record/file/$field';

    /**
     * @inheritdoc
     */
    protected $_DATA_TYPE = 'array';

    /**
     * @inheritdoc
     */
    protected $_REQUIRED_DATA = array(
        'format' => 'sugar-html-json',
        'delete_if_fails' => false
    );

    /**
     * Allow for shorthand calling of attachFile method.
     * Users can simply submit the File in via string, or pass the filename => path
     * @param $data
     * @throws RequiredOptionsException
     */
    protected function configureData($data)
    {
        if (is_string($data)) {
            if (!empty($this->Options)) {
                $fileField = end($this->Options);
                $data = array(
                    $fileField => $data
                );
            } else {
                throw new RequiredOptionsException(get_called_class(), "Options are required, when passing String for data.");
            }
        }
        if (is_array($data)) {
            foreach ($data as $key => $value) {
                if (!array_key_exists($key, $this->_REQUIRED_DATA)) {
                    $data[$key] = $this->setFileFieldValue($value);
                }
            }
        }
        parent::configureData($data);
    }

    /**
     * Configure the filepath to have an @ symbol in front if one is not found
     * @param string $value
     * @return string
     */
    protected function setFileFieldValue($value)
    {
        if (version_compare(PHP_VERSION, '5.5.0') >= 0){
            if (!($value instanceof \CURLFile)){
                $value = ltrim($value,"@");
                $value = new \CURLFile($value);
            }
        } else {
            if (strpos($value, '@') !== 0) {
                $value = '@'.$value;
            }
        }
        return $value;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Endpoint\Abstracts;

use SugarAPI\SDK\Endpoint\Interfaces\EPInterface;
use SugarAPI\SDK\Exception\Endpoint\InvalidRequestException;
use SugarAPI\SDK\Exception\Endpoint\InvalidURLException;
use SugarAPI\SDK\Exception\Endpoint\RequiredDataException;
use SugarAPI\SDK\Exception\Endpoint\RequiredOptionsException;
use SugarAPI\SDK\Response\Interfaces\ResponseInterface;
use SugarAPI\SDK\Request\Interfaces\RequestInterface;

/**
 * Class AbstractEndpoint
 * @package SugarAPI\SDK\Endpoint\Abstracts
 */
abstract class AbstractEndpoint implements EPInterface
{
    /**
     * Whether or not Authentication is Required
     * @var bool
     */
    protected $_AUTH_REQUIRED = true;

    /**
     * The URL for the Endpoint
     * - When configuring URL you define URL Parameters with $variables
     *      Examples:
     *          - Forecasts/$record_id
     * - $module Variable is a keyword to place the Module property into the URL
     *      Examples:
     *          - $module/$record
     * - Options property is used to replace variables in the order in which they are passed
     *
     * @var string
     */
    protected $_URL;

    /**
     * An array of Required Data properties that should be passed in the Request
     * @var array
     */
    protected $_REQUIRED_DATA = array();

    /**
     * The required type of Data to be given to the Endpoint. If none, different types can be passed in.
     * @var string
     */
    protected $_DATA_TYPE;

    /**
     * The configured URL for the Endpoint
     * @var string
     */
    protected $Url;

    /**
     * The initial URL passed into the Endpoint
     * @var
     */
    protected $baseUrl;

    /**
     * The passed in Options for the Endpoint
     * - If $module variable is used in $URL static property, then 1st option will be used as Module
     * @var array
     */
    protected $Options = array();

    /**
     * The data being passed to the API Endpoint.
     * Defaults to Array, but can be mixed based on how you want to use Endpoint. Defaults only work for Array type
     * @var mixed - array||stdClass
     */
    protected $Data = array();

    /**
     * The Request Object, used by the Endpoint to submit the data
     * @var RequestInterface
     */
    protected $Request;

    /**
     * The Response Object, returned by the Request Object
     * @var ResponseInterface
     */
    protected $Response;

    /**
     * Access Token for authentication
     * @var string
     */
    protected $accessToken;

    public function __construct($baseUrl, array $options = array())
    {
        $this->baseUrl = $baseUrl;

        if (!empty($options)) {
            $this->setOptions($options);
        }
        $this->configureURL();
    }

    /**
     * @inheritdoc
     */
    public function setOptions(array $options)
    {
        $this->Options = $options;
        $this->configureURL();
        return $this;
    }

    /**
     * @inheritdoc
     * @throws RequiredDataException - When passed in data contains issues
     */
    public function setData($data)
    {
        $this->Data = $data;
        return $this;
    }

    /**
     * @inheritdoc
     * @param string
     */
    public function setAuth($accessToken)
    {
        $this->accessToken = $accessToken;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setUrl($url)
    {
        $this->Url = $url;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setRequest(RequestInterface $Request)
    {
        $this->Request = $Request;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setResponse(ResponseInterface $Response)
    {
        $this->Response = $Response;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getOptions()
    {
        return $this->Options;
    }

    /**
     * @inheritdoc
     */
    public function getData()
    {
        return $this->Data;
    }

    /**
     * @inheritdoc
     */
    public function getUrl()
    {
        return $this->Url;
    }

    /**
     * @inheritdoc
     */
    public function getResponse()
    {
        return $this->Response;
    }

    /**
     * @inheritdoc
     */
    public function getRequest()
    {
        return $this->Request;
    }

    /**
     * @inheritdoc
     * @param null $data - short form data for Endpoint, which is configure by configureData method
     * @return $this
     * @throws InvalidRequestException
     * @throws InvalidURLException
     */
    public function execute($data = null)
    {
        $data =  ($data === null ? $this->Data : $data);
        $this->configureData($data);
        if (is_object($this->Request)) {
            $this->configureRequest();
            $this->Request->send();
            $this->configureResponse();
        } else {
            throw new InvalidRequestException(get_called_class(), "Request property not configured");
        }
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function authRequired()
    {
        return $this->_AUTH_REQUIRED;
    }

    /**
     * Verifies URL and Data are setup, then sets them on the Request Object
     * @throws InvalidURLException
     * @throws RequiredDataException
     */
    protected function configureRequest()
    {
        if ($this->verifyUrl()) {
            $this->Request->setURL($this->Url);
        }
        if ($this->verifyData() && !empty($this->Data)) {
            $this->Request->setBody($this->Data);
        }
        $this->configureAuth();
    }

    /**
     * Verifies URL and Data are setup, then sets them on the Request Object
     * @throws InvalidURLException
     * @throws RequiredDataException
     */
    protected function configureResponse()
    {
        if (is_object($this->Response)) {
            $this->Response->setCurlResponse($this->Request->getCurlResponse());
        }
    }

    /**
     * Configures the authentication header on the Request object
     */
    protected function configureAuth()
    {
        if ($this->authRequired()) {
            $this->Request->addHeader('OAuth-Token', $this->accessToken);
        }
    }

    /**
     * Configures Data for the Endpoint. Used mainly as an override function on implemented Endpoints.
     * @var $data
     */
    protected function configureData($data)
    {
        if (!empty($this->_REQUIRED_DATA)&&is_array($data)) {
            $data = $this->configureDefaultData($data);
        }
        $this->setData($data);
    }

    /**
     * Configure Defaults on a Data array based on the Required Data property
     * @param array $data
     * @return array
     */
    protected function configureDefaultData(array $data)
    {
        foreach ($this->_REQUIRED_DATA as $property => $value) {
            if (!isset($data[$property]) && $value !== null) {
                $data[$property] = $value;
            }
        }
        return $data;
    }

    /**
     * Configures the URL, by updating any variable placeholders in the URL property on the Endpoint
     * - Replaces $module with $this->Module
     * - Replaces all other variables starting with $, with options in the order they were given
     */
    protected function configureURL()
    {
        $url = $this->_URL;
        if ($this->requiresOptions()) {
            foreach ($this->Options as $key => $option) {
                $url = preg_replace('/(\$.*?[^\/]*)/', $option, $url, 1);
            }
        }
        $url = $this->baseUrl.$url;
        $this->setUrl($url);
    }

    /**
     * Verify if URL is configured properly
     * @return bool
     * @throws InvalidURLException
     */
    protected function verifyUrl()
    {
        $UrlArray = explode("?", $this->Url);
        if (strpos($UrlArray[0], "$") !== false) {
            throw new InvalidURLException(get_called_class(), "Configured URL is ".$this->Url);
        }
        return true;
    }

    /**
     * Validate the Data property on the Endpoint
     * @return bool
     * @throws RequiredDataException
     */
    protected function verifyData()
    {
        if (isset($this->_DATA_TYPE) || !empty($this->_DATA_TYPE)) {
            $this->verifyDataType();
        }
        if (!empty($this->_REQUIRED_DATA)) {
            $this->verifyRequiredData();
        }
        return true;
    }

    /**
     * Validate DataType on the Endpoint object
     * @return bool
     * @throws RequiredDataException
     */
    protected function verifyDataType()
    {
        if (gettype($this->Data) !== $this->_DATA_TYPE) {
            throw new RequiredDataException(get_called_class(), "Valid DataType is {$this->_DATA_TYPE}");
        }
        return true;
    }

    /**
     * Validate Required Data for the Endpoint
     * @return bool
     * @throws RequiredDataException
     */
    protected function verifyRequiredData()
    {
        $errors = array();
        foreach ($this->_REQUIRED_DATA as $property => $defaultValue) {
            if ((!isset($this->Data[$property])) && empty($defaultValue)) {
                $errors[] = $property;
            }
        }
        if (count($errors) > 0) {
            throw new RequiredDataException(get_called_class(), "Missing data for ".implode(",", $errors));
        }
        return true;
    }

    /**
     * Checks if Endpoint URL contains requires Options
     * @return bool
     */
    protected function requiresOptions()
    {
        return strpos($this->_URL, "$") === false ? false : true;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Request;

use SugarAPI\SDK\Request\Abstracts\AbstractRequest;

class GET extends AbstractRequest
{
    /**
     * @inheritdoc
     */
    protected static $_TYPE = 'GET';

    /**
     * @inheritdoc
     */
    protected static $_DEFAULT_HEADERS = array(
        "Content-Type: application/json"
    );

    /**
     * @inheritdoc
     *
     * Convert Body to Query String
     */
    public function setBody($body)
    {
        if (is_array($body) || is_object($body)) {
            $body = http_build_query($body);
        }
        $this->body = $body;
        return $this;
    }

    /**
     * @inheritdoc
     *
     * Configure the URL with Body since Payload is sent via Query String
     */
    public function send()
    {
        $body = '';
        if (!empty($this->body)) {
            if (strpos($this->url, "?") === false) {
                $body = "?".$this->body;
            } else {
                $body = "&".$this->body;
            }
        }
        $this->setURL($this->url.$body);
        return parent::send();
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Response\Abstracts;

use SugarAPI\SDK\Response\Interfaces\ResponseInterface;

abstract class AbstractResponse implements ResponseInterface
{
    /**
     * The Curl Request Resource that was used when curl_exec was called
     * @var cURL resource handle
     */
    protected $CurlRequest;

    /**
     * Extracted headers from Curl Response
     * @var string
     */
    protected $headers;

    /**
     * Extracted body from Curl Response
     * @var mixed
     */
    protected $body;

    /**
     * The HTTP Status Code of Request
     * @var string
     */
    protected $status;

    /**
     * The last Curl Error that occurred
     * @var string|boolean - False when no Curl Error = 0
     */
    protected $error;

    /**
     * The cURL Resource information returned via curl_getinfo
     * @var array
     */
    protected $info;

    public function __construct($curlRequest, $curlResponse = null)
    {
        $this->CurlRequest = $curlRequest;
        if ($curlResponse !== null) {
            $this->setCurlResponse($curlResponse);
        }
    }

    public function setCurlResponse($curlResponse)
    {
        $this->extractInfo();
        if ($this->error === false) {
            $this->extractResponse($curlResponse);
        }
    }

    /**
     * Extract the information from the Curl Request via curl_getinfo
     * Setup the Status property to be equal to the http_code
     */
    protected function extractInfo()
    {
        $this->info = curl_getinfo($this->CurlRequest);
        $this->status = $this->info['http_code'];
        if (curl_errno($this->CurlRequest)!== CURLE_OK) {
            $this->error = curl_error($this->CurlRequest);
        } else {
            $this->error = false;
        }
    }

    /**
     * Seperate the Headers and Body from the CurlResponse, and set the object properties
     * @param string $curlResponse
     */
    protected function extractResponse($curlResponse)
    {
        $this->headers = substr($curlResponse, 0, $this->info['header_size']);
        $this->body = substr($curlResponse, $this->info['header_size']);
    }

    /**
     * @inheritdoc
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @inheritdoc
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * @inheritdoc
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * @inheritdoc
     */
    public function getError()
    {
        return $this->error;
    }

    /**
     * @inheritdoc
     */
    public function getInfo()
    {
        return $this->info;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Helpers;

class Helpers
{
    const API_VERSION = 10;
    const API_URL = '/rest/v%s/';

    /**
     * Given a sugarcrm server/instance generate the Rest/v10 API Url
     * @param $instance
     * @param int $version
     * @return string
     */
    public static function configureAPIURL($instance, $version = null)
    {
        $url = 0;
        $version = ($version === null ? self::API_VERSION : $version);
        $instance = preg_replace('/\/rest\/v.+/', '', $instance);
        $url = rtrim($instance,"/").sprintf(self::API_URL, $version);
        if (preg_match('/^(http|https):\/\//i', $url) === 0) {
            $url = "http://".$url;
        }
        return $url;
    }

    /**
     * Return the list of Endpoints that come with the SDK
     * @return array
     */
    public static function getSDKEndpointRegistry()
    {
        $entryPoints = array();
        require __DIR__.DIRECTORY_SEPARATOR.'registry.php';
        foreach ($entryPoints as $funcName => $className) {
            $className = "SugarAPI\\SDK\\Endpoint\\" . $className;
            $entryPoints[$funcName] = $className;
        }
        return $entryPoints;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Response;

use SugarAPI\SDK\Response\Abstracts\AbstractResponse;

class File extends AbstractResponse
{
    /**
     * The name of the File from Response
     * @var string
     */
    protected $fileName;

    /**
     * File Path for response
     * @var string
     */
    protected $destinationPath;

    public function __construct($curlRequest, $curlResponse = null, $destination = null)
    {
        parent::__construct($curlRequest, $curlResponse);
        $this->setDestinationPath($destination);
    }

    /**
     * @inheritdoc
     * Extract Filename from Headers
     * @param mixed $curlResponse
     */
    public function setCurlResponse($curlResponse)
    {
        parent::setCurlResponse($curlResponse);
        if (!$this->error) {
            if (empty($this->fileName)) {
                $this->extractFileName();
            }
            $this->writeFile();
        }
    }

    /**
     * Configure the Destination path to store the File response
     * @param null $destination
     * @return self
     */
    public function setDestinationPath($destination = null)
    {
        if (empty($destination)) {
            $destination = sys_get_temp_dir().'/SugarAPI';
        }
        $this->destinationPath = $destination;
        return $this;
    }

    /**
     * Extract the filename from the Headers, and store it in filename property
     */
    protected function extractFileName(){
        foreach (explode("\r\n", $this->headers) as $header)
        {
            if (strpos($header, 'filename') !== false && strpos($header, 'Content-Disposition') !== false)
            {
                $fileName = substr($header, (strpos($header, "=")+1));
                $this->setFileName($fileName);
                break;
            }
        }
    }

    /**
     * Set the Filename for response to be saved to
     * @param $fileName
     * @return self
     */
    public function setFileName($fileName)
    {
        $fileName = preg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $fileName);
        $fileName = preg_replace("([\.]{2,})", '', $fileName);
        $this->fileName = $fileName;
        return $this;
    }

    /**
     * Return the filename found in response
     * @return mixed
     */
    public function getFileName()
    {
        return $this->fileName;
    }

    /**
     * Write the downloaded file
     * @return string|boolean - False if not written
     */
    public function writeFile()
    {
        if (!empty($this->fileName)) {
            if (!file_exists($this->destinationPath)) {
                mkdir($this->destinationPath, 0777);
            }
            $file = $this->file();
            $fileHandle = fopen($file, 'w+');
            fwrite($fileHandle, $this->body);
            fclose($fileHandle);
            return $file;
        } else {
            return false;
        }
    }

    /**
     * Return the full File path, where Response was stored
     * @return string
     */
    public function file()
    {
        return rtrim($this->destinationPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$this->fileName;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Client\Abstracts;

use SugarAPI\SDK\Exception\Authentication\AuthenticationException;
use SugarAPI\SDK\Exception\SDKException;
use SugarAPI\SDK\Helpers\Helpers;
use SugarAPI\SDK\Endpoint\Interfaces\EPInterface;

/**
 * The Abstract Client implementation for Sugar
 * @package SugarAPI\SDK\Client\Abstracts\AbstractClient
 * @method EPInterface ping()
 * @method EPInterface getRecord(string $module = '',string $record_id = '')
 * @method EPInterface getAttachment(string $module = '',string $record_id = '')
 * @method EPInterface getChangeLog(string $module = '',string $record_id = '')
 * @method EPInterface filterRelated(string $module = '')
 * @method EPInterface getRelated(string $module = '',string $record_id = '',string $relationship = '',string $related_id = '')
 * @method EPInterface me()
 * @method EPInterface search()
 * @method EPInterface oauth2Token()
 * @method EPInterface oauth2Refresh()
 * @method EPInterface createRecord(string $module = '')
 * @method EPInterface filterRecords(string $module = '')
 * @method EPInterface attachFile(string $module = '',string $record_id = '')
 * @method EPInterface oauth2Logout()
 * @method EPInterface createRelated(string $module = '',string $record_id = '',string $relationship = '')
 * @method @deprecated EPInterface linkRecords(string $module = '',string $record_id = '',string $relationship = '',string $related_id = '')
 * @method EPInterface relateRecord(string $module = '',string $record_id = '',string $relationship = '',string $related_id = '')
 * @method EPInterface massRelate(string $module = '',string $record_id = '')
 * @method EPInterface bulk()
 * @method EPInterface updateRecord(string $module = '',string $record_id = '')
 * @method EPInterface updateRelated(string $module = '',string $record_id = '',string $relationship = '',string $related_id = '')
 * @method EPInterface favorite(string $module = '',string $record_id = '')
 * @method EPInterface deleteRecord(string $module = '',string $record_id = '')
 * @method EPInterface unfavorite(string $module = '',string $record_id = '')
 * @method EPInterface deleteFile(string $module = '',string $record_id = '')
 * @method EPInterface unlinkRecords(string $module = '',string $record_id = '',string $relationship = '',string $related_id = '')
 * @method EPInterface duplicateCheck(string $module = '')
 * @method EPInterface count(string $module = '')
 */
abstract class AbstractSugarClient extends AbstractClient
{
    /**
     * @inheritdoc
     * @var array
     */
    protected $credentials = array(
        'username' => '',
        'password' => '',
        'client_id' => '',
        'client_secret' => '',
        'platform' => ''
    );

    /**
     * The API Version to be used.
     * Defaults to 10 (for v10), but can be any number above 10,
     * since customizing API allows for additional versioning to allow for duplicate entrypoints
     * @var
     */
    protected $apiVersion = 10;

    public function __construct($server = '', array $credentials = array())
    {
        $server = (empty($server) ? $this->server : $server);
        $this->setServer($server);
        $credentials = (empty($credentials) ? $this->credentials : $credentials);
        $this->setCredentials($credentials);
        $this->registerSDKEndpoints();
    }

    /**
     * @inheritdoc
     */
    protected function setAPIUrl()
    {
        $this->apiURL = Helpers::configureAPIURL($this->server, $this->apiVersion);
    }

    /**
     * @param $version
     * @return $this
     */
    public function setVersion($version)
    {
        $this->apiVersion = $version;
        $this->setAPIUrl();
        return $this;
    }

    /**
     * Get the Version of API being used by the Client
     * @return int
     */
    public function getVersion()
    {
        return $this->apiVersion;
    }

    /**
     * @inheritdoc
     * Overrides only the credentials properties passed in, instead of entire credentials array
     * Retrieves stored token based on passed in Credentials
     */
    public function setCredentials(array $credentials)
    {
        foreach ($this->credentials as $key => $value) {
            if (isset($credentials[$key])) {
                $this->credentials[$key] = $credentials[$key];
            }
        }
        parent::setCredentials($this->credentials);
        $token = static::getStoredToken($this->credentials);
        if (!empty($token)) {
            $this->setToken($token);
        }
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setToken($token)
    {
        if ($token instanceof \stdClass) {
            if (!isset($token->expiration)) {
                $token->expiration = time() + $token->expires_in;
            }
            if (!isset($token->refresh_expiration)) {
                $token->refresh_expiration = time() + $token->refresh_expires_in;
            }
            parent::setToken($token);
            return $this;
        } else {
            throw new SDKException('Sugar API Client requires Token to be of type \stdClass');
        }
    }

    /**
     * @inheritdoc
     */
    public function authenticated()
    {
        if (parent::authenticated()) {
            if (!$this->expiredToken()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if token is expired based on Access Token Expiration
     * @return bool
     */
    protected function expiredToken()
    {
        return time() >= $this->token->expiration;
    }

    /**
     * Register the defined Endpoints in SDK, located in src/Endpoint/registry.php file
     * @throws EndpointException
     */
    protected function registerSDKEndpoints()
    {
        $entryPoints = Helpers::getSDKEndpointRegistry();
        foreach ($entryPoints as $funcName => $className) {
            $this->registerEndpoint($funcName, $className);
        }
    }

    /**
     * @inheritdoc
     * Adds Auth token to EntryPoint if Auth is required
     */
    public function __call($name, $params)
    {
        $Endpoint = parent::__call($name, $params);

        if ($Endpoint->authRequired()) {
            $Endpoint->setAuth($this->token->access_token);
        }
        return $Endpoint;
    }

    /**
     * @inheritdoc
     * @throws AuthenticationException - When Login request fails
     */
    public function login()
    {
        if (!(empty($this->credentials['username']) ||
            empty($this->credentials['password']) ||
            empty($this->credentials['client_id']) ||
            !isset($this->credentials['client_secret']))) {
            $response = $this->oauth2Token()->execute($this->credentials)->getResponse();
            if ($response->getStatus() == '200') {
                $this->setToken($response->getBody(false));
                static::storeToken($this->token, $this->credentials);
                return true;
            } else {
                if ($response->getError() === false) {
                    $error = $response->getBody();
                    $error = $error['error'] . " - " . $error['error_message'];
                } else {
                    $error = $response->getError();
                }
                throw new AuthenticationException("Login Response [" .$response->getStatus() ."] - " .$error);
            }
        }
        return false;
    }

    /**
     * @inheritdoc
     * @throws AuthenticationException - When Refresh Request fails
     */
    public function refreshToken()
    {
        if (isset($this->credentials['client_id']) &&
            isset($this->credentials['client_secret']) &&
            isset($this->token)) {
            if (time() < $this->token->refresh_expiration) {
                $refreshOptions = array(
                    'client_id' => $this->credentials['client_id'],
                    'client_secret' => $this->credentials['client_secret'],
                    'refresh_token' => $this->token->refresh_token
                );
                $response = $this->oauth2Refresh()->execute($refreshOptions)->getResponse();
                if ($response->getStatus() == '200') {
                    $this->setToken($response->getBody(false));
                    static::storeToken($this->token, $this->credentials['client_id']);
                    return true;
                } else {
                    if ($response->getError() === false) {
                        $error = $response->getBody();
                        $error = $error['error'] . " - " . $error['error_message'];
                    } else {
                        $error = $response->getError();
                    }
                    throw new AuthenticationException("Refresh Response [" .$response->getStatus() ."] - " .$error);
                }
            }
        }
        return false;
    }

    /**
     * @inheritdoc
     * @throws AuthenticationException - When logout request fails
     */
    public function logout()
    {
        if ($this->authenticated()) {
            $response = $this->oauth2Logout()->execute()->getResponse();
            if ($response->getStatus()=='200') {
                static::removeStoredToken($this->credentials);
                return parent::logout();
            } else {
                if ($response->getError() === false) {
                    $error = $response->getBody();
                    $error = $error['error'] . " - " . $error['error_message'];
                } else {
                    $error = $response->getError();
                }
                throw new AuthenticationException("Logout Response [" .$response->getStatus() ."] - " .$error);
            }
        }
        return false;
    }

    /**
     * @param \stdClass $token
     * @param array $credentials
     * @inheritdoc
     */
    public static function storeToken($token, $credentials)
    {
        if (is_array($credentials)) {
            if (isset($credentials['client_id'])&&isset($credentials['platform'])&&isset($credentials['username'])) {
                $client_id = $credentials['client_id'];
                $platform = $credentials['platform'];
                $username = $credentials['username'];
                if (!isset(static::$_STORED_TOKENS[$client_id])) {
                    static::$_STORED_TOKENS[$client_id] = array();
                    if (!isset(static::$_STORED_TOKENS[$client_id][$platform])) {
                        static::$_STORED_TOKENS[$client_id][$platform] = array();
                    }
                }
                static::$_STORED_TOKENS[$client_id][$platform][$username] = $token;
                return true;
            }
        }
        return false;
    }

    /**
     * @param array $credentials
     * @inheritdoc
     */
    public static function getStoredToken($credentials)
    {
        $storeData = parent::getStoredToken($credentials['client_id']);
        if (!empty($storeData)) {
            $platform = $credentials['platform'];
            if (isset($storeData[$platform])) {
                $username = $credentials['username'];
                if (isset($storeData[$platform][$username])) {
                    return $storeData[$platform][$username];
                }
            }
        }
        return false;
    }

    /**
     * @param array $credentials
     * @inheritdoc
     */
    public static function removeStoredToken($credentials)
    {
        if (!is_array($credentials)||!isset($credentials['client_id'])) {
            return false;
        }
        $client_id = $credentials['client_id'];
        $platform = isset($credentials['platform']) ? $credentials['platform'] : null;
        $username = isset($credentials['username']) ? $credentials['username'] : null;
        if (empty($platform) && empty($username)) {
            return parent::removeStoredToken($client_id);
        } else {
            if (empty($username)) {
                unset(static::$_STORED_TOKENS[$client_id][$platform]);
            } else {
                unset(static::$_STORED_TOKENS[$client_id][$platform][$username]);
            }
        }
        return true;
    }
}


<?php
/**
 * ©[2016] SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
 */

namespace SugarAPI\SDK\Request\Abstracts;

use SugarAPI\SDK\Request\Interfaces\RequestInterface;

abstract class AbstractRequest implements RequestInterface
{
    const STATUS_INIT = 'initialized';
    const STATUS_SENT = 'sent';
    const STATUS_CLOSED = 'closed';

    /**
     * The HTTP Request Type
     * @var string
     */
    protected static $_TYPE = '';

    /**
     * The Default Curl Options
     * @var array
     */
    protected static $_DEFAULT_OPTIONS = array(
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
        CURLOPT_HEADER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERAGENT => 'Sugar-REST-PHP-Client'
    );

    /**
     * The default HTTP Headers to be added to Curl Request
     * @var array
     */
    protected static $_DEFAULT_HEADERS = array();

    /**
     * The Curl Resource used to actually send data to Sugar API
     * @var - Curl Resource
     */
    protected $CurlResponse;

    /**
     * The raw response from curl_exec
     * @var - Curl Response
     */
    protected $CurlRequest;

    /**
     * List of Headers for Request
     * @var array
     */
    protected $headers = array();

    /**
     * The body of the request or payload. JSON Encoded
     * @var string
     */
    protected $body = '';

    /**
     * The URL the Request is sent to
     * @var string
     */
    protected $url = '';

    /**
     * @var string
     */
    protected $status = '';

    /**
     * The Request Type
     * @var
     */
    protected $type;

    /**
     * The options configured on the Curl Resource object
     * @var array
     */
    protected $options = array();

    public function __construct($url = null)
    {
        $this->start();
        if (!empty($url)) {
            $this->setURL($url);
        }
        $this->setType(static::$_TYPE);
        $this->setHeaders(static::$_DEFAULT_HEADERS);
        foreach (static::$_DEFAULT_OPTIONS as $option => $value) {
            $this->setOption($option, $value);
        }
    }

    /**
     * Always make sure to destroy Curl Resource
     */
    public function __destruct()
    {
        if ($this->status !== self::STATUS_CLOSED) {
            curl_close($this->CurlRequest);
        }
    }

    /**
     * @inheritdoc
     */
    public function setURL($url)
    {
        $this->url = $url;
        $this->setOption(CURLOPT_URL, $this->url);
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getURL()
    {
        return $this->url;
    }

    /**
     * @inheritdoc
     */
    public function addHeader($name, $value)
    {
        $token = $name.": ".$value;
        $this->headers[] = $token;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setHeaders(array $array = array())
    {
        if (count($array) > 0) {
            foreach ($array as $key => $value) {
                if (is_numeric($key)) {
                    $this->headers[] = $value;
                } else {
                    $this->addHeader($key, $value);
                }
            }
        }
        $this->setOption(CURLOPT_HTTPHEADER, $this->headers);
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * @inheritdoc
     */
    public function setBody($body)
    {
        $this->body = $body;
        $this->setOption(CURLOPT_POSTFIELDS, $this->body);
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * @inheritdoc
     */
    public function getCurlObject()
    {
        return $this->CurlRequest;
    }

    /**
     * @inheritdoc
     */
    public function setOption($option, $value)
    {
        curl_setopt($this->CurlRequest, $option, $value);
        $this->options[$option] = $value;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * @inheritdoc
     */
    public function send()
    {
        $this->setHeaders();
        $this->CurlResponse = curl_exec($this->CurlRequest);
        $this->status = self::STATUS_SENT;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getCurlResponse()
    {
        return $this->CurlResponse;
    }

    /**
     * @inheritdoc
     */
    public function setType($type)
    {
        $this->type = strtoupper($type);
        $this->configureType();
        return $this;
    }

    /**
     * Configure the Curl Options based on Request Type
     */
    protected function configureType()
    {
        switch ($this->type) {
            case 'POST':
                $this->setOption(CURLOPT_POST, true);
                break;
            case 'DELETE':
            case 'PUT':
                $this->setOption(CURLOPT_CUSTOMREQUEST, $this->type);
                break;
        }
    }

    /**
     * @inheritdoc
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @inheritdoc
     */
    public function reset()
    {
        if (gettype($this->CurlRequest) == 'resource') {
            $this->close();
        }
        $this->start();
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function start()
    {
        $this->CurlRequest = curl_init();
        $this->status = self::STATUS_INIT;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function close()
    {
        curl_close($this->CurlRequest);
        unset($this->CurlRequest);
        $this->status = self::STATUS_CLOSED;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getCurlStatus()
    {
        return $this->status;
    }
}


<?php

namespace TwbBundle\Form\View\Helper;

use Zend\Form\Element\Collection as CollectionElement;
use Zend\Form\View\Helper\FormCollection;
use Zend\Form\ElementInterface;

class TwbBundleFormCollection extends FormCollection
{
    /**
     * @var string
     */
    protected static $legendFormat = '<legend%s>%s</legend>';

    /**
     * @var string
     */
    protected static $fieldsetFormat = '<fieldset%s>%s</fieldset>';

    /**
     * Attributes valid for the tag represented by this helper
     * @var array
     */
    protected $validTagAttributes = array(
        'disabled' => true
    );

    /**
     * Render a collection by iterating through all fieldsets and elements
     * @param \Zend\Form\ElementInterface $oElement
     * @return string
     */
    public function render(ElementInterface $oElement)
    {
        $oRenderer = $this->getView();
        if (!method_exists($oRenderer, 'plugin')) {
            return '';
        }

        $bShouldWrap = $this->shouldWrap;

        $sMarkup = '';
        $sElementLayout = $oElement->getOption('twb-layout');
        if ($oElement instanceof \IteratorAggregate) {
            $oElementHelper = $this->getElementHelper();
            $oFieldsetHelper = $this->getFieldsetHelper();

            foreach ($oElement->getIterator() as $oElementOrFieldset) {
                $aOptions = $oElementOrFieldset->getOptions();
                if ($sElementLayout && empty($aOptions['twb-layout'])) {
                    $aOptions['twb-layout'] = $sElementLayout;
                    $oElementOrFieldset->setOptions($aOptions);
                }

                if ($oElementOrFieldset instanceof \Zend\Form\FieldsetInterface) {
                    $sMarkup .= $oFieldsetHelper($oElementOrFieldset);
                } elseif ($oElementOrFieldset instanceof \Zend\Form\ElementInterface) {
                	if ($oElementOrFieldset->getOption('twb-row-open')) {
						$sMarkup .= '
' . "\n";
					}

					$sMarkup .= $oElementHelper($oElementOrFieldset);

					if ($oElementOrFieldset->getOption('twb-row-close')) {
						$sMarkup .= '
' . "\n";
					}
                }
            }
            if ($oElement instanceof \Zend\Form\Element\Collection && $oElement->shouldCreateTemplate()) {
                $sMarkup .= $this->renderTemplate($oElement);
            }
        }

        if ($bShouldWrap) {
            if (false != ($sLabel = $oElement->getLabel())) {
                if (null !== ($oTranslator = $this->getTranslator())) {
                    $sLabel = $oTranslator->translate($sLabel, $this->getTranslatorTextDomain());
                }

                $sMarkup = sprintf(
                        static::$legendFormat, ($sAttributes = $this->createAttributesString($oElement->getLabelAttributes()? : array())) ? ' ' . $sAttributes : '', $this->getEscapeHtmlHelper()->__invoke($sLabel)
                ) . $sMarkup;
            }

            //Set form layout class
            if ($sElementLayout) {
                $sLayoutClass = 'form-' . $sElementLayout;
                if (false != ($sElementClass = $oElement->getAttribute('class'))) {
                    if (!preg_match('/(\s|^)' . preg_quote($sLayoutClass, '/') . '(\s|$)/', $sElementClass)) {
                        $oElement->setAttribute('class', trim($sElementClass . ' ' . $sLayoutClass));
                    }
                } else {
                    $oElement->setAttribute('class', $sLayoutClass);
                }
            }

            $sMarkup = sprintf(
                    static::$fieldsetFormat, ($sAttributes = $this->createAttributesString($oElement->getAttributes())) ? ' ' . $sAttributes : '', $sMarkup
            );
        }
        return $sMarkup;
    }

    /**
     * Only render a template
     *
     * @param CollectionElement $collection
     * @return string
     */
    public function renderTemplate(CollectionElement $collection)
    {
        if (false != ($sElementLayout = $collection->getOption('twb-layout'))) {
            $elementOrFieldset = $collection->getTemplateElement();
            $elementOrFieldset->setOption('twb-layout', $sElementLayout);
        }

        return parent::renderTemplate($collection);
    }
}


<?php

namespace TwbBundle\View\Helper;

use Zend\Form\View\Helper\AbstractHelper;
use InvalidArgumentException;

class TwbBundleBadge extends AbstractHelper
{
    /**
     * @var string
     */
    protected static $badgeFormat = '<span %s>%s</span>';

    /**
     * Invoke helper as functor, proxies to {@link render()}.
     * @param string $sBadgeMessage
     * @param array $aBadgeAttributes : [optionnal]
     * @return string|TwbBundleBadge
     */
    public function __invoke($sBadgeMessage = null, array $aBadgeAttributes = null)
    {
        if ($sBadgeMessage === null) {
            return $this;
        }
        return $this->render($sBadgeMessage, $aBadgeAttributes);
    }

    /**
     * Retrieve badge markup
     * @param string $sBadgeMessage
     * @param  array $aBadgeAttributes : [optionnal]
     * @throws InvalidArgumentException
     * @return string
     */
    public function render($sBadgeMessage, array $aBadgeAttributes = null)
    {
        if (!is_scalar($sBadgeMessage)) {
            throw new InvalidArgumentException(sprintf(
                'Badge message expects a scalar value, "%s" given',
                is_object($sBadgeMessage) ? get_class($sBadgeMessage) : gettype($sBadgeMessage)
            ));
        }

        if (empty($aBadgeAttributes)) {
            $aBadgeAttributes = array('class' => 'badge');
        } else {
            if (empty($aBadgeAttributes['class'])) {
                $aBadgeAttributes['class'] = 'badge';
            } elseif (!preg_match('/(\s|^)badge(\s|$)/', $aBadgeAttributes['class'])) {
                $aBadgeAttributes['class'] .= ' badge';
            }
        }

        if (null !== ($oTranslator = $this->getTranslator())) {
            $sBadgeMessage = $oTranslator->translate($sBadgeMessage, $this->getTranslatorTextDomain());
        }

        return sprintf(
            static::$badgeFormat,
            $this->createAttributesString($aBadgeAttributes),
            $sBadgeMessage
        );
    }
}


<?php

namespace TwbBundle\Form\View\Helper;

use Zend\Form\View\Helper\Form;
use Zend\Form\FormInterface;
use Zend\Form\FieldsetInterface;

class TwbBundleForm extends Form
{

    /**
     * @var string
     */
    const LAYOUT_HORIZONTAL = 'horizontal';

    /**
     * @var string
     */
    const LAYOUT_INLINE = 'inline';

    /**
     * @var string
     */
    protected static $formRowFormat = '
%s
';

    /**
     * Form layout (see LAYOUT_* consts)
     *
     * @var string
     */
    protected $formLayout = null;

    /**
     * @see Form::__invoke()
     * @param FormInterface $oForm
     * @param string $sFormLayout
     * @return TwbBundleForm|string
     */
    public function __invoke(FormInterface $oForm = null, $sFormLayout = self::LAYOUT_HORIZONTAL)
    {
        if ($oForm) {
            return $this->render($oForm, $sFormLayout);
        }
        $this->formLayout = $sFormLayout;
        return $this;
    }

    /**
     * Render a form from the provided $oForm,
     * @see Form::render()
     * @param FormInterface $oForm
     * @param string $sFormLayout
     * @return string
     */
    public function render(FormInterface $oForm, $sFormLayout = self::LAYOUT_HORIZONTAL)
    {
        //Prepare form if needed
        if (method_exists($oForm, 'prepare')) {
            $oForm->prepare();
        }

        $this->setFormClass($oForm, $sFormLayout);

        //Set form role
        if (!$oForm->getAttribute('role')) {
            $oForm->setAttribute('role', 'form');
        }

        return $this->openTag($oForm) . "\n" . $this->renderElements($oForm, $sFormLayout) . $this->closeTag();
    }

    /**
     * @param FormInterface $oForm
     * @param string|null $sFormLayout
     * @return string
     */
    protected function renderElements(FormInterface $oForm, $sFormLayout = self::LAYOUT_HORIZONTAL)
    {
        // Store button groups
        $aButtonGroups = array();

        // Store button groups column-size from buttons
        $aButtonGroupsColumnSize = array();

        // Store elements rendering
        $aElementsRendering = array();

        // Retrieve view helper plugin manager
        $oHelperPluginManager = $this->getView()->getHelperPluginManager();

        // Retrieve form row helper
        $oFormRowHelper = $oHelperPluginManager->get('formRow');

        // Retrieve form collection helper
        $oFormCollectionHelper = $oHelperPluginManager->get('formCollection');

        // Retrieve button group helper
        $oButtonGroupHelper = $oHelperPluginManager->get('buttonGroup');

        // Store column size option
        $bHasColumnSize = false;

        // Prepare options
        foreach ($oForm as $iKey => $oElement) {
            $aOptions = $oElement->getOptions();
            if (!$bHasColumnSize && !empty($aOptions['column-size'])) {
                $bHasColumnSize = true;
            }
            // Define layout option to form elements if not already defined
            if ($sFormLayout && empty($aOptions['twb-layout'])) {
                $oElement->setOption('twb-layout', $sFormLayout);
            }

            // Manage button group option
            if (array_key_exists('button-group', $aOptions)) {
                $sButtonGroupKey = $aOptions['button-group'];
                if (isset($aButtonGroups[$sButtonGroupKey])) {
                    $aButtonGroups[$sButtonGroupKey][] = $oElement;
                } else {
                    $aButtonGroups[$sButtonGroupKey] = array($oElement);
                    $aElementsRendering[$iKey] = $sButtonGroupKey;
                }
                if (!empty($aOptions['column-size']) && !isset($aButtonGroupsColumnSize[$sButtonGroupKey])) {
                    // Only the first occured column-size will be set, other are ignored.
                    $aButtonGroupsColumnSize[$sButtonGroupKey] = $aOptions['column-size'];
                }
            } elseif ($oElement instanceof FieldsetInterface) {
                $aElementsRendering[$iKey] = $oFormCollectionHelper->__invoke($oElement);
            } else {
                $aElementsRendering[$iKey] = $oFormRowHelper->__invoke($oElement);
            }
        }

        // Assemble elements rendering
        $sFormContent = '';
        foreach ($aElementsRendering as $sElementRendering) {
            // Check if element rendering is a button group key
            if (isset($aButtonGroups[$sElementRendering])) {
                $aButtons = $aButtonGroups[$sElementRendering];

                // Render button group content
                $options = (isset($aButtonGroupsColumnSize[$sElementRendering])) ? array('attributes' => array('class' => 'col-' . $aButtonGroupsColumnSize[$sElementRendering])) : null;
                $sFormContent .= $oFormRowHelper->renderElementFormGroup($oButtonGroupHelper($aButtons, $options), $oFormRowHelper->getRowClassFromElement(current($aButtons)));
            } else {
                $sFormContent .= $sElementRendering;
            }
        }

        if ($bHasColumnSize && $sFormLayout !== self::LAYOUT_HORIZONTAL) {
            $sFormContent = sprintf(static::$formRowFormat, $sFormContent);
        }
        return $sFormContent;
    }

    /**
     * Sets form layout class
     * @param FormInterface $oForm
     * @param string $sFormLayout
     * @return \TwbBundle\Form\View\Helper\TwbBundleForm
     */
    protected function setFormClass(FormInterface $oForm, $sFormLayout = self::LAYOUT_HORIZONTAL)
    {
        if (is_string($sFormLayout)) {
            $sLayoutClass = 'form-' . $sFormLayout;
            if ($sFormClass = $oForm->getAttribute('class')) {
                if (!preg_match('/(\s|^)' . preg_quote($sLayoutClass, '/') . '(\s|$)/', $sFormClass)) {
                    $oForm->setAttribute('class', trim($sFormClass . ' ' . $sLayoutClass));
                }
            } else {
                $oForm->setAttribute('class', $sLayoutClass);
            }
        }
        return $this;
    }

    /**
     * Generate an opening form tag
     * @param null|FormInterface $form
     * @return string
     */
    public function openTag(FormInterface $form = null)
    {
        $this->setFormClass($form, $this->formLayout);
        return parent::openTag($form);
    }
}


<?php

namespace TwbBundle\View\Helper;

use InvalidArgumentException;
use Zend\Form\View\Helper\AbstractHelper;

/**
 * Render labels (http://getbootstrap.com/components/#labels)
 **/
class TwbBundleLabel extends AbstractHelper
{
    /**
     * @var string
     */
    protected static $labelFormat = '<%s %s>%s</%1$s>';

    /**
     * @var string
     */
    protected $tagName = 'span';

    /**
     * @var array
     */
    protected $validTagAttributes = array(
        'href' => true,
    );

    /**
     * Invoke helper as functor, proxies to {@link render()}.
     * @param string $sLabelMessage
     * @param string|array $aLabelAttributes : [optionnal] if string, label class
     * @return string|\TwbBundle\View\Helper\TwbBundleAlert
     */
    public function __invoke($sLabelMessage = null, $aLabelAttributes = 'label-default')
    {
        if (!$sLabelMessage) {
            return $this;
        }

        return $this->render($sLabelMessage, $aLabelAttributes);
    }

    /**
     * Retrieve label markup
     * @param string $sLabelMessage
     * @param  string|array $aLabelAttributes : [optionnal] if string, label class
     * @throws InvalidArgumentException
     * @return string
     */
    public function render($sLabelMessage, $aLabelAttributes = 'label-default')
    {
        if (!is_scalar($sLabelMessage)) {
            throw new InvalidArgumentException('Label message expects a scalar value, "' . gettype($sLabelMessage) . '" given');
        }

        if (empty($aLabelAttributes)) {
            throw new InvalidArgumentException('Label attributes are empty');
        }

        if (is_string($aLabelAttributes)) {
            $aLabelAttributes = array('class' => $aLabelAttributes);
        } elseif (!is_array($aLabelAttributes)) {
            throw new InvalidArgumentException('Label attributes expects a string or an array, "' . gettype($aLabelAttributes) . '" given');
        } elseif (empty($aLabelAttributes['class'])) {
            throw new \InvalidArgumentException('Label "class" attribute is empty');
        } elseif (!is_string($aLabelAttributes['class'])) {
            throw new InvalidArgumentException('Label "class" attribute expects string, "' . gettype($aLabelAttributes) . '" given');
        }

        if (!preg_match('/(\s|^)label(\s|$)/', $aLabelAttributes['class'])) {
            $aLabelAttributes['class'] .= ' label';
        }

        if (null !== ($oTranslator = $this->getTranslator())) {
            $sLabelMessage = $oTranslator->translate($sLabelMessage, $this->getTranslatorTextDomain());
        }

        return sprintf(
            static::$labelFormat,
            isset($aLabelAttributes['tagName']) ? $aLabelAttributes['tagName'] : $this->tagName,
            $this->createAttributesString($aLabelAttributes),
            $sLabelMessage
        );
    }
}


<?php

namespace TwbBundle\Form\View\Helper;

use DomainException;
use Zend\Form\View\Helper\FormRow;
use Zend\Form\ElementInterface;
use Zend\Form\LabelAwareInterface;
use Zend\Form\Element\Button;
use Zend\Form\Element\Submit;

class TwbBundleFormRow extends FormRow
{

    /**
     * @var string
     */
    protected static $formGroupFormat = '
%s
';

    /**
     * @var string
     */
    protected static $horizontalLayoutFormat = '
%s
';

    /**
     * @var string
     */
    protected static $checkboxFormat = '
%s
';

    /**
     * @var string
     */
    protected static $helpBlockFormat = '
%s
';

    /**
     * The class that is added to element that have errors
     * @var string
     */
    protected $inputErrorClass = '';

    /**
     * @var string
     */
    protected $requiredFormat = null;

    /**
     * @see FormRow::render()
     * @param ElementInterface $oElement
     * @return string
     */
    public function render(ElementInterface $oElement, $sLabelPosition = null)
    {
        // Retrieve element type
        $sElementType = $oElement->getAttribute('type');

        // Nothing to do for hidden elements which have no messages
        if ($sElementType === 'hidden' && !$oElement->getMessages()) {
            return parent::render($oElement, $sLabelPosition);
        }

        // Retrieve expected layout
        $sLayout = $oElement->getOption('twb-layout');

        // Define label position
        if ($sLabelPosition === null) {
            $sLabelPosition = $this->getLabelPosition();
        }

        // Partial rendering
        if ($this->partial) {
            return $this->view->render($this->partial, array(
                        'element' => $oElement,
                        'label' => $this->renderLabel($oElement),
                        'labelAttributes' => $this->labelAttributes,
                        'labelPosition' => $sLabelPosition,
                        'renderErrors' => $this->renderErrors,
            ));
        }

        // "has-error" validation state case
        if ($oElement->getMessages()) {
            //Element have errors
            if ($sInputErrorClass = $this->getInputErrorClass()) {
                if ($sElementClass = $oElement->getAttribute('class')) {
                    if (!preg_match('/(\s|^)' . preg_quote($sInputErrorClass, '/') . '(\s|$)/', $sElementClass)) {
                        $oElement->setAttribute('class', trim($sElementClass . ' ' . $sInputErrorClass));
                    }
                } else {
                    $oElement->setAttribute('class', $sInputErrorClass);
                }
            }
        }

        // Render element
        $sElementContent = $this->renderElement($oElement, $sLabelPosition);

        // Render form row
        switch (true) {
            // Checkbox element not in horizontal form
            case $sElementType === 'checkbox' && $sLayout !== TwbBundleForm::LAYOUT_HORIZONTAL && !$oElement->getOption('form-group'):
            // All "button" elements in inline form
            case in_array($sElementType, array('submit', 'button', 'reset'), true) && $sLayout === TwbBundleForm::LAYOUT_INLINE:
                return $sElementContent . "\n";
            default:
                // Render element into form group
                return $this->renderElementFormGroup($sElementContent, $this->getRowClassFromElement($oElement), $oElement->getOption('feedback'));
        }
    }

    /**
     * @param ElementInterface $oElement
     * @return string
     */
    public function getRowClassFromElement(\Zend\Form\ElementInterface $oElement)
    {
        $sRowClass = '';
        if ($sFormGroupSize = $oElement->getOption('twb-form-group-size')) {
            $sRowClass = $sFormGroupSize;
        }

        // Validation state
        if (($sValidationState = $oElement->getOption('validation-state'))) {
            $sRowClass .= ' has-' . $sValidationState;
        }
        if ($oElement->getMessages()) {
            $sRowClass .= ' has-error';
        }
        if( $oElement->getOption('feedback')) {
            $sRowClass .= ' has-feedback';
        }

        // Column size
        if (($sColumSize = $oElement->getOption('column-size')) && $oElement->getOption('twb-layout') !== TwbBundleForm::LAYOUT_HORIZONTAL
        ) {
            $sColumSize = (is_array($sColumSize)) ? $sColumSize : array($sColumSize);
            $sRowClass .= implode('', array_map(function($item) { return ' col-' . $item; }, $sColumSize));
        }

        //Additional row class
        if ($sAddRowClass = $oElement->getOption('twb-row-class')) {
            $sRowClass .= ' ' . $sAddRowClass;
        }
        return $sRowClass;
    }

    /**
     * @param string $sElementContent
     * @param string $sRowClass
     * @param string $sFeedbackElement A feedback element that should be rendered within the element, optional
     *
     * @return string
     * @throws \InvalidArgumentException
     */
    public function renderElementFormGroup($sElementContent, $sRowClass, $sFeedbackElement = '' )
    {
        if (!is_string($sElementContent)) {
            throw new \InvalidArgumentException('Argument "$sElementContent" expects a string, "' . (is_object($sElementContent) ? get_class($sElementContent) : gettype($sElementContent)) . '" given');
        }
        if (!is_string($sRowClass)) {
            throw new \InvalidArgumentException('Argument "$sRowClass" expects a string, "' . (is_object($sRowClass) ? get_class($sRowClass) : gettype($sRowClass)) . '" given');
        }
        if ($sFeedbackElement && !is_string($sFeedbackElement)) {
            throw new \InvalidArgumentException('Argument "$sFeedbackElement" expects a string, "' . (is_object($sFeedbackElement) ? get_class($sFeedbackElement) : gettype($sFeedbackElement)) . '" given');
        }
        if( $sFeedbackElement ){
            $sElementContent .= '';
        }
        return sprintf(static::$formGroupFormat, $sRowClass, $sElementContent) . "\n";
    }

    /**
     * Render element's label
     * @param ElementInterface $oElement
     * @return string
     */
    protected function renderLabel(ElementInterface $oElement)
    {
        if (($sLabel = $oElement->getLabel()) && ($oTranslator = $this->getTranslator())) {
            $sLabel = $oTranslator->translate($sLabel, $this->getTranslatorTextDomain());
        }
        return $sLabel;
    }

    /**
     * Render element
     * @param ElementInterface $oElement
     * @param string $sLabelPosition
     * @return type
     * @throws DomainException
     */
    protected function renderElement(ElementInterface $oElement, $sLabelPosition = null)
    {
        //Retrieve expected layout
        $sLayout = $oElement->getOption('twb-layout');

        // Define label position
        if ($sLabelPosition === null) {
            $sLabelPosition = $this->getLabelPosition();
        }

        //Render label
        $sLabelOpen = $sLabelClose = $sLabelContent = $sElementType = '';
        if ($sLabelContent = $this->renderLabel($oElement)) {
            /*
             * Multicheckbox elements have to be handled differently
             * as the HTML standard does not allow nested labels.
             * The semantic way is to group them inside a fieldset
             */
            $sElementType = $oElement->getAttribute('type');

            //Button element is a special case, because label is always rendered inside it
            if (($oElement instanceof Button) or ( $oElement instanceof Submit)) {
                $sLabelContent = '';
            } else {
                $aLabelAttributes = $oElement->getLabelAttributes() ? : $this->labelAttributes;

                //Validation state
                if ($oElement->getOption('validation-state') || $oElement->getMessages()) {
                    if (empty($aLabelAttributes['class'])) {
                        $aLabelAttributes['class'] = 'control-label';
                    } elseif (!preg_match('/(\s|^)control-label(\s|$)/', $aLabelAttributes['class']) && $sElementType !== 'checkbox') {
                        $aLabelAttributes['class'] = trim($aLabelAttributes['class'] . ' control-label');
                    }
                }

                $oLabelHelper = $this->getLabelHelper();
                switch ($sLayout) {
                    //Hide label for "inline" layout
                    case TwbBundleForm::LAYOUT_INLINE:
                        if ($sElementType !== 'checkbox') {
                            if ($sElementType !== 'checkbox') {
                                if (empty($aLabelAttributes['class']) && empty($oElement->getOption('showLabel'))) {
                                    $aLabelAttributes['class'] = 'sr-only';
                                } elseif (empty($oElement->getOption('showLabel')) && !preg_match('/(\s|^)sr-only(\s|$)/', $aLabelAttributes['class'])) {
                                    $aLabelAttributes['class'] = trim($aLabelAttributes['class'] . ' sr-only');
                                }
                            }
                        }
                        break;

                    case TwbBundleForm::LAYOUT_HORIZONTAL:
                        if ($sElementType !== 'checkbox') {
                            if (empty($aLabelAttributes['class'])) {
                                $aLabelAttributes['class'] = 'control-label';
                            } else {
                                if (!preg_match('/(\s|^)control-label(\s|$)/', $aLabelAttributes['class'])) {
                                    $aLabelAttributes['class'] = trim($aLabelAttributes['class'] . ' control-label');
                                }
                            }
                        }
                        break;
                }
                if ($aLabelAttributes) {
                    $oElement->setLabelAttributes($aLabelAttributes);
                }

                $sLabelOpen = $oLabelHelper->openTag($oElement->getAttribute('id') ? $oElement : $aLabelAttributes);
                $sLabelClose = $oLabelHelper->closeTag();

                // Allow label html escape desable
                //$sLabelContent = $this->getEscapeHtmlHelper()->__invoke($sLabelContent);

                if (!$oElement instanceof LabelAwareInterface || !$oElement->getLabelOption('disable_html_escape')) {
                    $sLabelContent = $this->getEscapeHtmlHelper()->__invoke($sLabelContent);
                }
            }
        }

        //Add required string if element is required
        if ($this->requiredFormat &&
                $oElement->getAttribute('required') &&
                strpos($this->requiredFormat, $sLabelContent) === false
        ) {
            $sLabelContent .= $this->requiredFormat;
        }

        switch ($sLayout) {
            case null:
            case TwbBundleForm::LAYOUT_INLINE:

                $sElementContent = $this->getElementHelper()->render($oElement);

                // Checkbox elements are a special case, element is already rendered into label
                if ($sElementType === 'checkbox') {
                    $sElementContent = sprintf(static::$checkboxFormat, $sElementContent);
                } else {
                    if ($sLabelPosition === self::LABEL_PREPEND) {
                        $sElementContent = $sLabelOpen . $sLabelContent . $sLabelClose . $sElementContent;
                    } else {
                        $sElementContent = $sElementContent . $sLabelOpen . $sLabelContent . $sLabelClose;
                    }
                }

                //Render help block
                $sElementContent .= $this->renderHelpBlock($oElement);

                //Render errors
                if ($this->renderErrors) {
                    $sElementContent .= $this->getElementErrorsHelper()->render($oElement);
                }

                return $sElementContent;

            case TwbBundleForm::LAYOUT_HORIZONTAL:
                $sElementContent = $this->getElementHelper()->render($oElement) . $this->renderHelpBlock($oElement);

                //Render errors
                if ($this->renderErrors) {
                    $sElementContent .= $this->getElementErrorsHelper()->render($oElement);
                }

                $sClass = '';

                //Column size
                if ($sColumSize = $oElement->getOption('column-size')) {
                    $sClass .= ' col-' . $sColumSize;
                }

                // Checkbox elements are a special case, element is rendered into label
                if ($sElementType === 'checkbox') {
                    return sprintf(
                            static::$horizontalLayoutFormat, $sClass, sprintf(static::$checkboxFormat, $sElementContent)
                    );
                }

                if ($sLabelPosition === self::LABEL_PREPEND) {
                    return $sLabelOpen . $sLabelContent . $sLabelClose . sprintf(
                                    static::$horizontalLayoutFormat, $sClass, $sElementContent
                    );
                } else {
                    return sprintf(
                                    static::$horizontalLayoutFormat, $sClass, $sElementContent
                            ) . $sLabelOpen . $sLabelContent . $sLabelClose;
                }
        }
        throw new DomainException('Layout "' . $sLayout . '" is not valid');
    }

    /**
     * Render element's help block
     * @param ElementInterface $oElement
     * @return string
     */
    protected function renderHelpBlock(ElementInterface $oElement)
    {
        if ($sHelpBlock = $oElement->getOption('help-block')) {
            if ($oTranslator = $this->getTranslator()) {
                $sHelpBlock = $oTranslator->translate($sHelpBlock, $this->getTranslatorTextDomain());
            }
            $sHelpBlockString = strip_tags($sHelpBlock);
            if ($sHelpBlock === $sHelpBlockString) {
                $sHelpBlock = $this->getEscapeHtmlHelper()->__invoke($sHelpBlock);
            }
            return sprintf(static::$helpBlockFormat, $sHelpBlock);
        } else {
            return '';
        }
    }
}