<?php
namespace Checkdomain\Comodo;

use Zend\Mail\Exception\ExceptionInterface;
use Zend\Mail\Storage\Message;
use Zend\Mail\Storage\Part;

/**
 * Class ImapHelper
 */
class ImapHelper
{
    const PROCESSED_FLAG = 'checkdomain_comodo_processed';

    public static $subjects = [
        'order_received'       => 'Your order has been received',
        'confirmation'         => 'CONFIRMATION',
        'information_required' => 'Information Required',
        '1_expiry'             => 'Customer certificate expiry warning (1 days)',
        '30_expiry'            => 'Customer certificate expiry warning (30 days)',
        '60_expiry'            => 'Customer certificate expiry warning (60 days)',
    ];

    public static $bodies = [
        'issued' => '/Your [a-zA-Z ]* Certificate for [\*a-zA-Z0-9\_\-öäü\.]* is attached!/',
    ];

    /**
     *  Fetches the mail recursively, through the folders.
     *
     * @param ImapAdapter $imap             imap helper class
     * @param string      $search           imap-searchterm
     * @param bool        $markProcessed    Sets the flag as processed
     * @param bool        $assume           Assumes domainName / order-Number in the mail
     * @param \Closure    $callbackFunction callback
     *
     * @return array
     */
    public function fetchMails(ImapAdapter $imap, $search, $markProcessed = true, $assume = false, \Closure $callbackFunction = null)
    {
        $folder = 'INBOX';

        $imap->selectFolder($folder);
        $result = $imap->search(array($search));

        $messages = [];

        foreach ($result as $id) {
            $message = null;

            try {
                $message = $imap->getMessage($id);

                $messages[$id]['id']     = $id;
                $messages[$id]['folder'] = $folder;

                // Zend-mail sometimes got problems, with incorrect e-mails
                try {
                    $messages[$id]['subject'] = utf8_decode($message->getHeader('subject', 'string'));
                } catch (\Exception $e) {
                    $messages[$id]['subject'] = '-No subject-';
                }

                try {
                    $messages[$id]['received'] = strtotime($message->getHeader('date', 'string'));
                } catch (\Exception $e) {
                    $messages[$id]['received'] = '-No date-';
                }

                $messages[$id]['plainText']   = $this->getPlainText($message);
                $messages[$id]['attachments'] = $this->getAttachments($message);
                $messages[$id]['type']        = $this->getTypeOfMail($messages[$id]);

                if ($assume) {
                    $messages[$id]['orderNumber'] = $this->assumeOrderNumber($messages[$id]);
                    $messages[$id]['domainName']  = $this->assumeDomainName($messages[$id]);
                }

                $success = true;
                if (is_callable($callbackFunction)) {
                    $success = $callbackFunction($id, $messages[$id]);
                }
            } catch (ExceptionInterface $e) {
                // General decoding error -> removeMessage
                unset($messages[$id]);

                // Always mark as processed
                $success = true;
            }

            if ($markProcessed && $success) {
                $this->markProcessed($imap, $id, $message);
            }
        }

        return $messages;
    }

    /**
     * Marks the mail with the processed flag
     *
     * @param ImapAdapter $imap
     * @param integer     $id
     * @param Message     $message
     */
    protected function markProcessed(ImapAdapter $imap, $id, Message $message = null)
    {
        $flags   = $message ? $message->getFlags() : [];
        $flags[] = self::PROCESSED_FLAG;

        try {
            $imap->setFlags($id, $flags);
        } catch (ExceptionInterface $e) {
            // Nothing to do
        }
    }

    /**
     * Tries to find out, what Comodo wanna tell us...
     *
     * @param array $mail
     *
     * @return null|string
     */
    protected function getTypeOfMail(array $mail)
    {
        foreach (self::$subjects as $key => $subject) {
            if (stristr($mail['subject'], $subject) !== false) {
                return $key;
            }
        }

        foreach (self::$bodies as $key => $body) {
            if (preg_match($body, $mail['plainText'])) {
                return $key;
            }
        }

        return null;
    }

    /**
     * @param Part $part
     *
     * @return string|null
     */
    public function getPlainText(Part $part)
    {
        $text = null;

        if ($part->isMultipart()) {
            $partCount = $part->countParts();

            // Parse all parts
            for ($i = 0; $i < $partCount; $i++) {
                $subPart = $part->getPart($i + 1);

                // Check for headers, to avoid exceptions
                if ($subPart->getHeaders() !== null) {
                    if ($subPart->isMultipart()) {
                        // Part is multipart as well
                        $tmp = $this->getPlainText($subPart);
                        $text = ($tmp !== null) ? $tmp : $text;
                    } else {
                        // Try to get plain/text of this content
                        $tmp = $this->getPlainTextOfPart($subPart);
                        $text = ($tmp !== null) ? $tmp : $text;
                    }
                }
            }
        } else {
            // Its not multipart, so try to get its content
            $text = $this->getPlainTextOfPart($part);
        }

        return $text;
    }

    /**
     * @param Part $part
     *
     * @return null|string
     */
    private function getPlainTextOfPart(Part $part)
    {
        try {
            if ($part->getHeaderField('Content-Disposition') == 'attachment') {
                return null;
            }
        } catch (\Exception $e) {
            // Zend throws an Exception, if headerField does not exist
        }

        try {
            // If content-type is text/plain, return its content
            if ($part->getHeaderField('Content-Type') == 'text/plain') {
                return $this->getStringFromPart($part);
            }
        } catch (\Exception $e) {
            // If content-type is not available (exception thrown), return its content
            return $this->getStringFromPart($part);
        }

        return null;
    }

    /**
     * @param $part
     *
     * @return null|string
     */
    public function getStringFromPart($part)
    {
        if ($part === null) {
            return null;
        }

        if ($part instanceof Part) {
            $text = $part->getContent();
        } else {
            $text = $part;
        }

        return strip_tags($text);
    }

    /**
     * @param Message $message
     *
     * @return array|null
     */
    public function getAttachments(Message $message)
    {
        $attachments = null;

        // Only multi-part will have attachments
        if ($message->isMultipart()) {
            if ($message->countParts() > 1) {

                $partCount = $message->countParts();
                for ($i = 0; $i < $partCount; $i++) {
                    $part = $message->getPart($i + 1);

                    // Check for headers, to avoid exceptions
                    if ($part->getHeaders() !== null) {
                        // Getting disposition
                        $disposition = null;
                        try {
                            $disposition = $part->getHeaderField('Content-Disposition');
                        } catch (\Exception $e) {
                            // Zend throws an Exception, if headerField does not exist
                        }

                        if ($disposition == 'attachment') {
                            $content = quoted_printable_decode($part->getContent());

                            // Encoding?
                            try {
                                $transferEncoding = $part->getHeaderField('Content-Transfer-Encoding');

                                if ($transferEncoding == 'base64') {
                                    $content = base64_decode($content);
                                }
                            } catch (\Exception $e) {
                                // Zend throws an Exception, if headerField does not exist
                            }

                            $attachments[] = array(
                                'mime'     => $part->getHeaderField('Content-Type'),
                                'filename' => $part->getHeaderField('Content-Disposition', 'filename'),
                                'content'  => $content
                            );
                        }
                    }
                }
            }
        }

        return $attachments;
    }

    /**
     * @param string $message
     *
     * @return integer|null
     */
    protected function assumeOrderNumber($message)
    {
        $pattern = '/[order|\#][^0-9]+([0-9]{7,8})/i';

        return $this->searchSubjectAndPlainText($pattern, $message);
    }

    /**
     * @param string $message
     *
     * @return string|null
     */
    protected function assumeDomainName($message)
    {
        $pattern = '/(?:certificate for|domain|domain name){1}[\s\t\n\r\:\-]*((?:[\*a-z0-9äöüß-]+\.)+[a-z0-9äöüß-]+)/i';

        return $this->searchSubjectAndPlainText($pattern, $message);
    }

    /**
     * @param $pattern
     * @param $message
     *
     * @return null
     */
    protected function searchSubjectAndPlainText($pattern, $message)
    {
        $matchesText = array();
        preg_match($pattern, $message['subject'], $matchesText);

        if (!empty($matchesText[1])) {
            return $matchesText[1];
        }

        $matchesSubject = array();
        preg_match($pattern, $message['plainText'], $matchesSubject);

        if (!empty($matchesSubject[1])) {
            return $matchesSubject[1];
        }

        return null;
    }
}


<?php
namespace Checkdomain\Comodo;

use Checkdomain\Comodo\Model\Exception\AccountException;
use Checkdomain\Comodo\Model\Exception\ArgumentException;
use Checkdomain\Comodo\Model\Exception\CSRException;
use Checkdomain\Comodo\Model\Exception\RequestException;
use Checkdomain\Comodo\Model\Exception\UnknownApiException;
use Checkdomain\Comodo\Model\Exception\UnknownException;

use Checkdomain\Comodo\Model\Result\AutoApplyResult;
use Checkdomain\Comodo\Model\Result\AutoReplaceResult;
use Checkdomain\Comodo\Model\Result\CollectSslResult;
use Checkdomain\Comodo\Model\Result\GetDCVEMailAddressListResult;
use Checkdomain\Comodo\Model\Result\GetMDCDomainDetailsResult;
use Checkdomain\Comodo\Model\Result\UpdateUserEvClickThroughResult;
use Checkdomain\Comodo\Model\Result\SslCheckerResult;
use Checkdomain\Comodo\Model\Result\WebHostReportResult;

/**
 * Class Util
 * Provides functions to communicate with the Comodo API,requires an Account object, given to the Communication-adapter
 */
class Util
{
    const AUTO_REFUND_REASON_VALIDATION = 1;
    const AUTO_REFUND_REASON_INACTIVE = 2;
    const AUTO_REFUND_REASON_TYPE = 3;
    const AUTO_REFUND_REASON_BRAND = 4;
    const AUTO_REFUND_REASON_REJECTED = 5;
    const AUTO_REFUND_REASON_MALWARE = 6;
    const AUTO_REFUND_REASON_PHISHING = 7;
    const AUTO_REFUND_REASON_SAFE_BROWSING = 8;
    const AUTO_REFUND_REASON_AUTHORITY = 9;
    const AUTO_REFUND_REASON_PRICE = 10;
    const AUTO_REFUND_REASON_OTHER = 11;

    const COMODO_AUTO_REFUND_URL                = 'https://secure.comodo.net/products/!AutoRefund';
    const COMODO_AUTO_APPLY_URL                 = 'https://secure.comodo.net/products/!AutoApplySSL';
    const COMODO_AUTO_REVOKE_URL                = 'https://secure.comodo.net/products/!AutoRevokeSSL';
    const COMODO_DCV_MAIL_URL                   = 'https://secure.comodo.net/products/!GetDCVEmailAddressList';
    const COMODO_DCV_RESEND_URL                 = 'https://secure.comodo.net/products/!ResendDCVEmail';
    const COMODO_AUTO_UPDATE_DCV_URL            = 'https://secure.comodo.net/products/!AutoUpdateDCV';
    const COMODO_PROVIDE_EV_DETAILS_URL         = 'https://secure.comodo.net/products/!ProvideEVDetails';
    const COMODO_MDC_DOMAIN_DETAILS_URL         = 'https://secure.comodo.net/products/!GetMDCDomainDetails';
    const COMODO_AUTO_REPLACE_URL               = 'https://secure.comodo.net/products/!AutoReplaceSSL';
    const COMODO_COLLECT_SSL_URL                = 'https://secure.comodo.net/products/download/CollectSSL';
    const COMODO_UPDATE_USER_EV_CLICK_THROUGH   = 'https://secure.comodo.net/products/!UpdateUserEvClickThrough';
    const COMODO_WEB_HOST_REPORT                = 'https://secure.comodo.net/products/!WebHostReport';
    const COMODO_SSLCHECKER                     = 'https://secure.comodo.com/sslchecker';

    const COMODO_DCV_CODE_URL                   = 'https://secure.comodo.net/products/EnterDCVCode2';

    /**
     * @var CommunicationAdapter
     */
    protected $communicationAdapter;

    /**
     * @var ImapAdapter
     */
    protected $imapAdapter;

    /**
     * @var ImapHelper
     */
    protected $imapHelper;

    /**
     * Constructs the Util with a communicationAdapter
     *
     * @param CommunicationAdapter|null $communicationAdapter
     * @param ImapAdapter|null          $imapAdapter
     * @param ImapHelper|null           $imapHelper
     */
    public function __construct(CommunicationAdapter $communicationAdapter, ImapAdapter $imapAdapter, ImapHelper $imapHelper)
    {
        $this->communicationAdapter = $communicationAdapter;
        $this->imapAdapter          = $imapAdapter;
        $this->imapHelper           = $imapHelper;
    }

    /**
     * @return CommunicationAdapter
     */
    public function getCommunicationAdapter()
    {
        return $this->communicationAdapter;
    }

    /**
     * @param CommunicationAdapter $communicationAdapter
     *
     * @return Util
     */
    public function setCommunicationAdapter(CommunicationAdapter $communicationAdapter)
    {
        $this->communicationAdapter = $communicationAdapter;

        return $this;
    }

    /**
     * @return ImapHelper
     */
    public function getImapHelper()
    {
        return $this->imapHelper;
    }

    /**
     * @param ImapHelper $imapHelper
     *
     * @return Util
     */
    public function setImapHelper(ImapHelper $imapHelper)
    {
        $this->imapHelper = $imapHelper;

        return $this;
    }

    /**
     * @return ImapAdapter
     */
    public function getImapAdapter()
    {
        return $this->imapAdapter;
    }

    /**
     * @param ImapAdapter $imapAdapter
     *
     * @return Util
     */
    public function setImapAdapter(ImapAdapter $imapAdapter)
    {
        $this->imapAdapter = $imapAdapter;

        return $this;
    }

    /**
     * Refunds a ordered certificate
     *
     * @param array $params
     *
     * @return bool
     *
     * @throws AccountException
     * @throws ArgumentException
     * @throws CSRException
     * @throws RequestException
     * @throws UnknownApiException
     * @throws UnknownException
     */
    public function autoRefund(array $params)
    {
        if (false === isset($params['refundReasonCode'])) {
            $params['refundReasonCode'] = self::AUTO_REFUND_REASON_OTHER;
        }

        $response = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_AUTO_REFUND_URL,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );

        if ($response['errorCode'] == 0) {
            return true;
        } else {
            throw $this->createException($response);
        }
    }

    /**
     * Function apply for a certificate
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return AutoApplyResult
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function autoApplySSL(array $params)
    {
        // Two choices, we want url-encoded
        $params['responseFormat']    = CommunicationAdapter::RESPONSE_URL_ENCODED;
        $params['showCertificateID'] = 'Y';

        // Send request
        $arr = $this
            ->communicationAdapter
            ->sendToApi(self::COMODO_AUTO_APPLY_URL, $params, CommunicationAdapter::RESPONSE_URL_ENCODED);

        // Successful
        if ($arr['errorCode'] == 1 || $arr['errorCode'] == 0) {
            $result = new AutoApplyResult();

            if ($arr['errorCode'] == 0) {
                $paid = true;
            } else {
                $paid = false;
            }

            $result
                ->setPaid($paid)
                ->setCertificateID($arr['certificateID'])
                ->setExpectedDeliveryTime($arr['expectedDeliveryTime'])
                ->setOrderNumber($arr['orderNumber'])
                ->setTotalCost($arr['totalCost'])
                ->setRequestQuery($arr['requestQuery']);

            if (isset($arr['uniqueValue'])) {
                $result->setUniqueValue($arr['uniqueValue']);
            }

            return $result;
        } else {
            throw $this->createException($arr);
        }
    }

    /**
     * @param array $params
     *
     * @return UpdateUserEvClickThroughResult
     * @throws AccountException
     * @throws ArgumentException
     * @throws CSRException
     * @throws RequestException
     * @throws UnknownApiException
     * @throws UnknownException
     */
    public function updateUserEvClickThrough(array $params)
    {
        // Send request
        $arr = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_UPDATE_USER_EV_CLICK_THROUGH,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );

        // Successful
        if ($arr['errorCode'] == 0) {
            $result = new UpdateUserEvClickThroughResult();

            $result
                ->setStatus($arr['status']);

            return $result;
        } else {
            throw $this->createException($arr);
        }
    }

    /**
     * Function update for a certificate
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return AutoReplaceResult
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function autoReplaceSSL(array $params)
    {
        // Two choices, we want url-encoded
        $params['responseFormat'] = CommunicationAdapter::RESPONSE_URL_ENCODED;

        // Send request
        $arr = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_AUTO_REPLACE_URL,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );

        // Successful
        if ($arr['errorCode'] == 0) {
            $result = new AutoReplaceResult();

            $result
                ->setCertificateID($arr['certificateID'])
                ->setExpectedDeliveryTime($arr['expectedDeliveryTime']);

            if (isset($arr['uniqueValue'])) {
                $result->setUniqueValue($arr['uniqueValue']);
            }

            return $result;
        } else {
            throw $this->createException($arr);
        }
    }

    /**
     * Function to revoke order
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return bool
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function autoRevokeSSL(array $params)
    {
        // Two choices, we want url-encoded
        $params['responseFormat'] = CommunicationAdapter::RESPONSE_URL_ENCODED;

        return $this->sendBooleanRequest(
            self::COMODO_AUTO_REVOKE_URL,
            $params,
            CommunicationAdapter::RESPONSE_URL_ENCODED
        );
    }

    /**
     * Function to auto update dcv
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return bool
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function autoUpdateDCV(array $params)
    {
        return $this->sendBooleanRequest(
            self::COMODO_AUTO_UPDATE_DCV_URL,
            $params,
            CommunicationAdapter::RESPONSE_URL_ENCODED
        );
    }

    /**
     * Function to get details of a certificate
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return CollectSslResult
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function collectSsl(array $params)
    {
        // Not decode the following indexes
        $notDecode = array('caCertificate', 'certificate', 'netscapeCertificateSequence', 'zipFile');

        // Force threating as array
        $forceArray = array('caCertificate');

        // Two choices, we want url-encoded
        $params['responseFormat'] = CommunicationAdapter::RESPONSE_URL_ENCODED;

        // Send request
        $arr = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_COLLECT_SSL_URL,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED,
                $notDecode,
                $forceArray
            );

        // Successful
        if ($arr['errorCode'] >= 0) {
            $result = new CollectSslResult();

            $this->fill($result, $arr, array('notBefore', 'notAfter'));

            /*
             * Comodo does not provide these data, when not a EV certificate (Bug?). So we fill this manually to "not required".
             *
             * https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/CollectSSL%20v1.17.pdf
             */
            if (!isset($arr['evClickThroughStatus'])) {
                $result->setEvClickThroughStatus(-1);
            }

            if (!isset($arr['brandValStatus'])) {
                $result->setBrandValStatus(-1);
            }

            return $result;
        } else {
            throw $this->createException($arr);
        }
    }

    /**
     * Function to resend the DCV Email
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return bool
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function resendDCVEMail(array $params)
    {
        return $this
            ->sendBooleanRequest(
                self::COMODO_DCV_RESEND_URL,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );
    }

    /**
     * @param array $params
     *
     * @deprecated Comodo support told this function doesn't have any effect anymore
     *
     * @return bool
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function provideEVDetails(array $params)
    {
        return $this->sendBooleanRequest(
            self::COMODO_PROVIDE_EV_DETAILS_URL,
            $params,
            CommunicationAdapter::RESPONSE_URL_ENCODED
        );
    }

    /**
     * Function to get the DCV e-mail address-list
     *
     * See documentation of params at https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/
     *
     * @param array $params
     *
     * @return GetDCVEMailAddressListResult
     * @throws AccountException
     * @throws ArgumentException
     * @throws CSRException
     * @throws RequestException
     * @throws UnknownApiException
     * @throws UnknownException
     */
    public function getDCVEMailAddressList(array $params)
    {
        // Force threating as array
        $forceArray = array('whois_email', 'level2_email', 'level3_email');

        // Response is always new line encoded
        $responseArray = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_DCV_MAIL_URL,
                $params,
                CommunicationAdapter::RESPONSE_NEW_LINE,
                null,
                $forceArray
            );

        if ($responseArray['errorCode'] == 0) {
            $result = new GetDCVEMailAddressListResult();

            if (isset($responseArray['whois_email'][0]) && $responseArray['whois_email'][0] == 'none') {
                unset($responseArray['whois_email'][0]);
            }

            $result
                ->setDomainName($responseArray['domain_name'])
                ->setWhoisEmail($responseArray['whois_email'])
                ->setLevel2Emails($responseArray['level2_email'])
                ->setRequestQuery($responseArray['requestQuery']);

            if (isset($responseArray['level3_email'])) {
                $result->setLevel3Emails($responseArray['level3_email']);
            }

            return $result;
        } else {
            throw $this->createException($responseArray);
        }
    }

    /**
     * Function to get details of a order-number (this API support just one domain)
     *
     * https://secure.comodo.net/api/pdf/webhostreseller/sslcertificates/GetMDCDomainDetails%20v1.00.pdf
     *
     * @param array $params
     *
     * @return GetMDCDomainDetailsResult
     *
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function getMDCDomainDetails(array $params)
    {
        // Response is always new line encoded
        $responseArray = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_MDC_DOMAIN_DETAILS_URL,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );

        if ($responseArray['errorCode'] == 0) {
            $result = new GetMDCDomainDetailsResult();

            $result
                ->setDomainName($responseArray['1_domainName'])
                ->setDcvMethod($responseArray['1_dcvMethod'])
                ->setDcvStatus($responseArray['1_dcvStatus']);

            return $result;
        } else {
            throw $this->createException($responseArray);
        }
    }

    /**
     * Function to do a sslcheck
     *
     * https://secure.comodo.net/api/pdf/latest/SSLChecker.pdf
     *
     * @param array $params
     *
     * @return GetMDCDomainDetailsResult
     *
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function sslChecker(array $params)
    {
        // Response is always new line encoded
        $responseArray = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_SSLCHECKER,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );

        if ($responseArray['error_code'] == 0) {
            $result = new SslCheckerResult();

            $result
                ->setServerUrl($responseArray['server_url'])
                ->setServerDomainIsIDN($responseArray['server_domain_isIDN'])
                ->setServerDomainUtf8($responseArray['server_domain_utf8'])
                ->setServerDomainAce($responseArray['server_domain_ace'])
                ->setServerIp($responseArray['server_ip'])
                ->setServerPort($responseArray['server_port'])
                ->setServerSoftware($responseArray['server_software'])
                ->setCertNotBeforeFromUnixTimestamp($responseArray['cert_notBefore'])
                ->setCertNotAfterFromUnixTimestamp($responseArray['cert_notAfter'])
                ->setCertValidityNotBefore($responseArray['cert_validity_notBefore'])
                ->setCertValidityNotAfter($responseArray['cert_validity_notAfter'])
                ->setCertKeyAlgorithm($responseArray['cert_key_algorithm'])
                ->setCertKeySize($responseArray['cert_key_size'])
                ->setCertSubjectCN($responseArray['cert_subject_DN'])
                ->setCertSubjectCN($responseArray['cert_subject_CN'])
                ->setCertSubjectOU($responseArray['cert_subject_OU'])
                ->setCertSubjectOrganization($responseArray['cert_subject_O'])
                ->setCertSubjectStreetAddress1($responseArray['cert_subject_streetAddress_1'])
                ->setCertSubjectStreetAddress2($responseArray['cert_subject_streetAddress_2'])
                ->setCertSubjectStreetAddress3($responseArray['cert_subject_streetAddress_3'])
                ->setCertSubjectLocality($responseArray['cert_subject_L'])
                ->setCertSubjectState($responseArray['cert_subject_S'])
                ->setCertSubjectPostalCode($responseArray['cert_subject_postalCode'])
                ->setCertSubjectCountry($responseArray['cert_subject_C'])
                ->setCertIsMultiDomain($responseArray['cert_isMultiDomain'])
                ->setCertIsWildcard($responseArray['cert_isWildcard'])
                ->setCertIssuerDN($responseArray['cert_issuer_DN'])
                ->setCertIssuerCN($responseArray['cert_issuer_CN'])
                ->setCertIssuerOrganization($responseArray['cert_issuer_O'])
                ->setCertIssuerOrganization($responseArray['cert_issuer_C'])
                ->setCertIssuerBrand($responseArray['cert_issuer_brand'])
                ->setCertPolicyOID($responseArray['cert_policyOID'])
                ->setCertValidation($responseArray['cert_validation']);

            return $result;
        } else {
            throw $this->createException($responseArray);
        }
    }

    /**
     * Function to call WebHostReport api
     *
     * https://secure.comodo.net/products/!WebHostReport
     *
     * Details for params usage:
     * https://secure.comodo.net/api/pdf/latest/WebHostReport.pdf
     *
     * @param array $params
     *
     * @return WebHostReportResult
     *
     * @throws Model\Exception\AccountException
     * @throws Model\Exception\ArgumentException
     * @throws Model\Exception\CSRException
     * @throws Model\Exception\RequestException
     * @throws Model\Exception\UnknownApiException
     * @throws Model\Exception\UnknownException
     */
    public function webHostReport(array $params) {

        if (empty($params['lastResultNo'])) {
            $params['lastResultNo'] = 10;
        }

        // Response is always new line encoded
        $responseArray = $this
            ->communicationAdapter
            ->sendToApi(
                self::COMODO_WEB_HOST_REPORT,
                $params,
                CommunicationAdapter::RESPONSE_URL_ENCODED
            );
        if ($responseArray['error_code'] == 0) {
            $result = new WebHostReportResult();
            $result->importEntries($responseArray);
            return $result;
        } else {
            throw $this->createException($responseArray);
        }
    }

    /**
     * Function to enter the DCV code, coming from DCV E-Mail
     *
     * @param array $params
     *
     * @return bool
     * @throws Model\Exception\UnknownException
     * @throws Model\Exception\ArgumentException
     */
    public function enterDCVCode(array $params)
    {
        // Check parameters
        if (!isset($params['dcvCode'])) {
            throw new ArgumentException(-3, 'Please provide an order-number', 'dcvCode', '');
        }

        if (!isset($params['orderNumber'])) {
            throw new ArgumentException(-3, 'Please provide an order-number', 'orderNumber', '');
        }

        // this is not a official request, so we need to use the website
        $responseString = $this
            ->communicationAdapter
            ->sendToWebsite(self::COMODO_DCV_CODE_URL, $params);

        // Decode answer from website
        if (stristr($responseString, 'You have entered the correct Domain Control Validation code') !== false) {
            return true;
        } elseif (stristr($responseString, 'the certificate has already been issued') !== false) {
            throw new ArgumentException(-104, 'The certificate has already been issued', 'certificate', $responseString);
        } elseif (stristr($responseString, 'Invalid Validation Code!') !== false) {
            throw new ArgumentException(-103, 'Invalid Validation Code', 'validation-code', $responseString);
        } else {
            throw new UnknownException(99, 'UnknownException', $responseString);
        }
    }

    /**
     * @param string   $domainName
     * @param null     $orderNumbers
     * @param \Closure $callbackFunction
     *
     * @return array
     */
    public function getMails($domainName, $orderNumbers = null, \Closure $callbackFunction = null)
    {
        $orList    = ' OR ';
        $whereList = ' BODY "'.$domainName.'"';
        $whereList .= ' SUBJECT "'.$domainName.'"';

        if (is_array($orderNumbers)) {
            foreach ($orderNumbers as $orderNumber) {
                $orList .= ' OR OR ';
                $whereList .= ' BODY "'.$orderNumber.'"';
                $whereList .= ' SUBJECT "'.$orderNumber.'"';
            }
        }

        $search = $orList.' '.$whereList;

        return $this
            ->imapHelper
            ->fetchMails($this->imapAdapter, $search, false, false, $callbackFunction);
    }

    /**
     * @param bool     $markProcessed
     * @param \Closure $callbackFunction
     *
     * @return array
     */
    public function getUnprocessedMails($markProcessed = true, \Closure $callbackFunction = null)
    {
        $search = ' NOT KEYWORD "'.ImapHelper::PROCESSED_FLAG.'"';

        return $this
            ->imapHelper
            ->fetchMails(
                $this->imapAdapter,
                $search,
                $markProcessed,
                true,
                $callbackFunction
            );
    }

    /**
     * Function to create an exception for API errorcodes
     *
     * @param array|mixed $responseArray
     *
     * @return AccountException|ArgumentException|CSRException|RequestException|UnknownApiException|UnknownException
     *
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    protected function createException($responseArray)
    {
        if (is_array($responseArray) === false) {
            return new UnknownException(
                0,
                'Internal error',
                $responseArray['responseString']
            );
        }

        switch ($responseArray['errorCode']) {
            case -1: // Not using https:
            case -17: // Wrong HTTP-method
                return new RequestException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    $responseArray['responseString']
                );

            case -2: // unrecognized argument
            case -3: // missing argument
            case -4: // invalid argument
            case -7: // invalid ISO Country
            case -18: // Name = Fully-Qualified Domain Name
            case -35: // Name = = IP
            case -19: // Name = = Accessible IP
                return new ArgumentException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    ((isset($responseArray['errorItem'])) ? $responseArray['errorItem'] : null),
                    $responseArray['responseString']
                );

            case -16: // Permission denied
            case -15: // insufficient credits
                return new AccountException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    $responseArray['responseString']
                );

            case -5: // contains wildcard
            case -6: // no wildcard, but must have
            case -8: // missing field
            case -9: // base64 decode exception
            case -10: // decode exception
            case -11: // unsupported algorithm
            case -12: // invalid signature
            case -13: // unsupported key size
            case -20: // Already rejected / Order relevated
            case -21: // Already revoked
            case -26: // current being issued
            case -40: // key compromised
                return new CSRException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    $responseArray['responseString']
                );

            case -14:
                return new UnknownApiException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    $responseArray['responseString']
                );

            default:
                return new UnknownException(
                    $responseArray['errorCode'],
                    $responseArray['errorMessage'],
                    $responseArray['responseString']
                );
        }
    }

    /**
     * @param CollectSslResult $object
     * @param array            $arr
     * @param array            $timestampFields
     *
     * @return $this
     */
    protected function fill(CollectSslResult $object, array $arr, array $timestampFields = array())
    {
        foreach ($arr as $key => $value) {
            if (in_array($key, $timestampFields)) {
                $value = new \DateTime('@'.$value);
            }

            $function = 'set'.ucfirst($key);

            // For example setErrorCode does not exists, so check before
            if (method_exists($object, $function)) {
                call_user_func(array($object, $function), $value);
            }
        }

        return $this;
    }

    /**
     * @param string $url
     * @param array  $params
     * @param int    $type
     *
     * @return bool
     * @throws AccountException
     * @throws ArgumentException
     * @throws CSRException
     * @throws RequestException
     * @throws UnknownApiException
     * @throws UnknownException
     */
    protected function sendBooleanRequest($url, array $params, $type)
    {
        // Response is always url encoded
        $responseArray = $this
            ->communicationAdapter
            ->sendToApi(
                $url,
                $params,
                $type
            );

        if ($responseArray['errorCode'] == 0) {
            return true;
        } else {
            throw $this->createException($responseArray);
        }
    }
}


<?php namespace Calotype\SEO\Generators;

use Calotype\SEO\Contracts\MetaAware;

class MetaGenerator
{
    /**
     * The meta title.
     *
     * @var string
     */
    protected $title;

    /**
     * The meta description.
     *
     * @var string
     */
    protected $description;

    /**
     * The meta keywords.
     *
     * @var string
     */
    protected $keywords;

    /**
     * The maximal length of the meta description.
     *
     * @var integer
     */
    protected $max_description_length = 160;

    /*
     * The default configurations.
     *
     * @var array
     */
    protected $defaults = array(
        'title' => false,
        'description' => false,
        'separator' => ' | ',
        'keywords' => false
    );

    /**
     * The canonical url.
     *
     * @var array
     */
    protected $canonical;

    /**
     * Create a new MetaGenerator instance.
     *
     * @param array $defaults
     */
    public function __construct(array $defaults = array())
    {
        foreach ($defaults as $key => $value) {
            $this->defaults[$key] = $value;
        }
    }

    /**
     * Render the meta tags.
     *
     * @return string
     */
    public function generate()
    {
        $title = $this->getTitle();
        $description = $this->getDescription();
        $keywords = $this->getKeywords();
        $canonical = $this->getCanonical();

        $html[] = "<title>$title</title>";

        if (! empty($description)) {
            $html[] = "<meta name='description' content='$description' />";
        }

        if (! empty($keywords)) {
            $html[] = "<meta name='keywords' content='$keywords' />";
        }

        if (! empty($canonical)) {
            $html[] = "<link rel='canonical' href='$canonical' />";
        }

        return implode(PHP_EOL, $html);
    }

    /**
     * Use the meta data of a MetaAware object.
     *
     * @param MetaAware $object
     */
    public function fromObject(MetaAware $object)
    {
        $data = $object->getMetaData();

        if (array_key_exists('title', $data)) {
            $this->setTitle($data['title']);
        }

        if (array_key_exists('description', $data)) {
            $this->setDescription($data['description']);
        }

        if (array_key_exists('keywords', $data)) {
            $this->setKeywords($data['keywords']);
        }

        if (array_key_exists('canonical', $data)) {
            $this->setCanonical($data['canonical']);
        }
    }

    /**
     * Set the Meta title.
     *
     * @param string $title
     */
    public function setTitle($title)
    {
        $title = strip_tags($title);

        $this->title = $title . $this->getDefault('separator') . $this->getDefault('title');
    }

    /**
     * Set the Meta description.
     *
     * @param string $description
     */
    public function setDescription($description)
    {
        $description = strip_tags($description);

        if (strlen($description) > $this->max_description_length) {
            $description = substr($description, 0, $this->max_description_length);
        }

        $this->description = $description;
    }

    /**
     * Set the Meta keywords.
     *
     * @param array|string $keywords
     */
    public function setKeywords($keywords)
    {
        $this->keywords = implode(',', (array) $keywords);
    }

    /**
     * Get the Meta title.
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title ? : $this->getDefault('title');
    }

    /**
     * Get the Meta keywords.
     *
     * @return string
     */
    public function getKeywords()
    {
        return $this->keywords ? : $this->getDefault('keywords');
    }

    /**
     * Get the Meta description.
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description ? : $this->getDefault('description');
    }

    /**
     * Set the canonical url.
     *
     * @param string $url
     */
    public function setCanonical($url)
    {
        $this->canonical = $url;
    }

    /**
     * Get the canonical url.
     *
     * @return string
     */
    public function getCanonical()
    {
        return $this->canonical;
    }

    /**
     * Reset the title and description fields.
     *
     * @return void
     */
    public function reset()
    {
        $this->title = null;
        $this->description = null;
        $this->keywords = null;
        $this->canonical = null;
    }

    /**
     * Get a default configuration.
     *
     * @param string $default
     *
     * @return mixed
     */
    public function getDefault($default)
    {
        if (array_key_exists($default, $this->defaults)) {
            return $this->defaults[$default];
        }

        $class = get_class($this);
        throw new \InvalidArgumentException("{$class}: default configuration $default does not exist.");
    }
}


<?php namespace Calotype\SEO\Providers;

use Illuminate\Http\Response;
use Illuminate\Support\ServiceProvider;

use Calotype\SEO\Generators\MetaGenerator;
use Calotype\SEO\Generators\RobotsGenerator;
use Calotype\SEO\Generators\SitemapGenerator;
use Calotype\SEO\Generators\OpenGraphGenerator;

class SEOServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app['config']->package('calotype/seo', __DIR__ . '/../../../config', 'calotype-seo');

        $this->registerBindings();
    }

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        // If the user does not want us to create default routes, we won't
        $should_generate_routes = $this->app['config']->get('calotype-seo::generate_routes');

        if ($should_generate_routes) {
            $this->generateDefaultRoutes();
        }
    }

    /**
     * Generate default routes for /sitemap.xml and /robots.txt
     *
     * @return void
     */
    public function generateDefaultRoutes()
    {
        $app = $this->app;

        // Create the default robots.txt content
        $app['calotype.seo.generators.robots']->addUserAgent('*');
        $app['calotype.seo.generators.robots']->addDisallow('');
        $app['calotype.seo.generators.robots']->addSpacer();
        $app['calotype.seo.generators.robots']->addSitemap($app['request']->root() . '/sitemap.xml');

        // Generate sitemap.xml route
        $app['router']->get('sitemap.xml', function () use ($app) {
            $response = new Response($app['calotype.seo.generators.sitemap']->generate(), 200);
            $response->header('Content-Type', 'application/xml');

            return $response;
        });

        // Generate robots.txt route
        $app['router']->get('robots.txt', function () use ($app) {
            $response = new Response($app['calotype.seo.generators.robots']->generate(), 200);
            $response->header('Content-Type', 'text/plain');

            return $response;
        });
    }

    /**
     * Register the bindings.
     *
     * @return void
     */
    public function registerBindings()
    {
        // Register the sitemap.xml generator
        $this->app->singleton('calotype.seo.generators.sitemap', function ($app) {
            return new SitemapGenerator();
        });

        // Register the meta tags generator
        $this->app->singleton('calotype.seo.generators.meta', function ($app) {
            return new MetaGenerator($app['config']->get('calotype-seo::defaults'));
        });

        // Register the robots.txt generator
        $this->app->singleton('calotype.seo.generators.robots', function ($app) {
            return new RobotsGenerator();
        });

        // Register the open graph properties generator
        $this->app->singleton('calotype.seo.generators.opengraph', function ($app) {
            return new OpenGraphGenerator();
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return array(
            'calotype.seo.generators.meta',
            'calotype.seo.generators.opengraph',
            'calotype.seo.generators.sitemap',
            'calotype.seo.generators.robots',
        );
    }
}


<?php namespace Calotype\SEO\Generators;

use Calotype\SEO\Contracts\OpenGraphAware;

class OpenGraphGenerator
{
    /**
     * The prefix used by the open graph protocol.
     *
     * @const OPENGRAPH_PREFIX
     */
    const OPENGRAPH_PREFIX = 'og:';

    /**
     * The tag used by the open graph protocol.
     *
     * @const OPENGRAPH_TAG
     */
    const OPENGRAPH_TAG = '<meta property="[property]" content="[value]" />';

    /**
     * The properties that we are going to generate.
     *
     * @var array
     */
    protected $properties = array();

    /**
     * The properties that are required.
     *
     * @var array
     */
    protected $required = array(
        'title', 'type', 'image', 'url', 'site_name'
    );

    /**
     * Render the open graph tags.
     *
     * @return string
     */
    public function generate()
    {
        $html = array();

        foreach ($this->properties as $property => $value) {
            $html[] = strtr(static::OPENGRAPH_TAG, array(
                '[property]' => static::OPENGRAPH_PREFIX . $property,
                '[value]' => $value
            ));
        }

        return implode(PHP_EOL, $html);
    }

    /**
     * Set the open graph properties from a raw array.
     *
     * @param array $properties
     */
    public function fromRaw($properties)
    {
        $this->validateProperties($properties);

        foreach ($properties as $property => $value) {
            $this->properties[$property] = $value;
        }
    }

    /**
     * Use the open graph data of a open graph aware object.
     *
     * @param OpenGraphAware $object
     */
    public function fromObject(OpenGraphAware $object)
    {
        $properties = $object->getOpenGraphData();

        $this->validateProperties($properties);

        foreach ($properties as $property => $value) {
            $this->properties[$property] = $value;
        }
    }

    /**
     * Reset all the properties.
     *
     * @return void
     */
    public function reset()
    {
        $this->properties = array();
    }

    /**
     * Validate to make sure the properties contain all required ones.
     *
     * @param array $properties
     */
    protected function validateProperties($properties)
    {
        foreach ($this->required as $required) {
            if (! array_key_exists($required, $properties)) {
                throw new \InvalidArgumentException("Required open graph property [$required] is not present.");
            }
        }
    }
}


<?php namespace Calotype\SEO\Generators;

use XMLWriter;
use Traversable;
use Calotype\SEO\Contracts\SitemapAware;

class SitemapGenerator
{
    /**
     * All the entries of the sitemap.
     *
     * @var array
     */
    protected $entries = array();

    /**
     * Closures used for lazy loading.
     *
     * @var array
     */
    protected $closures = array();

    /**
     * The required fields of a sitemap entry.
     *
     * @var array
     */
    protected $required = array(
        'loc'
    );

    /**
     * The attributes that should be replaced with
     * their valid counterpart for readability.
     *
     * @var array
     */
    protected $replacements = array(
        'location' => 'loc',
        'last_modified' => 'lastmod',
        'change_frequency' => 'changefreq'
    );

    /**
     * The allowed values for the change frequency.
     *
     * @var array
     */
    protected $frequencies = array(
        'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'
    );

    /**
     * Add a SitemapAware object to the sitemap.
     *
     * @param mixed $object
     */
    public function add($object)
    {
        if (is_a($object, 'Closure')) {
            return $this->closures[] = $object;
        }

        $this->validateObject($object);

        $data = $object->getSitemapData();
        $this->addRaw($data);
    }

    /**
     * Add multiple SitemapAware objects to the sitemap.
     *
     * @param array|Traversable $objects
     */
    public function addAll($objects)
    {
        if (is_a($objects, 'Closure')) {
            return $this->closures[] = $objects;
        }

        foreach ($objects as $object) {
            $this->add($object);
        }
    }

    /**
     * Add a raw entry to the sitemap.
     *
     * @param array $data
     */
    public function addRaw(array $data)
    {
        $this->validateData($data);

        $data = $this->prepareData($data);

        $this->entries[] = $data;
    }

    /**
     * Prepare the data.
     *
     * @param  array $data
     *
     * @return array
     */
    public function prepareData(array $data)
    {
        $data = $this->replaceAttributes($data);

        // Remove trailing slashes from the location
        if (array_key_exists('loc', $data)) {
            $data['loc'] = trim($data['loc'], '/');
        }

        return $data;
    }

    /**
     * Check if the sitemap contains an url.
     *
     * @param string $url
     *
     * @return boolean
     */
    public function contains($url)
    {
        $url = trim($url, '/');

        foreach ($this->entries as $entry) {
            if ($entry['loc'] == $url) {
                return true;
            }
        }

        return false;
    }

    /**
     * Generate the xml for the sitemap.
     *
     * @return string
     */
    public function generate()
    {
        $this->loadClosures();

        $xml = new XMLWriter();
        $xml->openMemory();

        $xml->writeRaw('<?xml version="1.0" encoding="UTF-8"?>');
        $xml->writeRaw('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');

        foreach ($this->entries as $data) {
            $xml->startElement('url');

            foreach ($data as $attribute => $value) {
                $xml->writeElement($attribute, $value);
            }

            $xml->endElement();
        }

        $xml->writeRaw('</urlset>');

        return $xml->outputMemory();
    }

    /**
     * Clean all entries from the sitemap.
     *
     * @return void
     */
    public function reset()
    {
        $this->entries = array();
    }

    /**
     * Validate the data for a sitemap entry.
     *
     * @param array $data
     */
    protected function validateData($data)
    {
        $data = $this->replaceAttributes($data);

        foreach ($this->required as $required) {
            if (! array_key_exists($required, $data)) {
                $replacement = array_search($required, $this->replacements);

                if ($replacement !== false) {
                    $required = $replacement;
                }

                throw new \InvalidArgumentException("Required sitemap property [$required] is not present.");
            }
        }
    }

    /**
     * Validate an element.
     *
     * @param mixed $element
     */
    protected function validateObject($element)
    {
        if (! $element instanceof SitemapAware) {
            throw new \InvalidArgumentException("Element should implement Calotype\SEO\Contracts\SitemapAware");
        }
    }

    /**
     * Replace the attribute names with their replacements.
     *
     * @param array $data
     *
     * @return array
     */
    protected function replaceAttributes($data)
    {
        foreach ($data as $attribute => $value) {
            $replacement = $this->replaceAttribute($attribute);
            unset($data[$attribute]);
            $data[$replacement] = $value;
        }

        return $data;
    }

    /**
     * Replace an attribute with it's replacement if available.
     *
     * @param string $attribute
     *
     * @return string
     */
    protected function replaceAttribute($attribute)
    {
        if (array_key_exists($attribute, $this->replacements)) {
            return $this->replacements[$attribute];
        }

        return $attribute;
    }

    /**
     * Load the lazy loaded elements.
     *
     * @return void
     */
    protected function loadClosures()
    {
        foreach ($this->closures as $closure) {
            $instance = $closure();

            if (is_array($instance) || $instance instanceof Traversable) {
                $this->addAll($instance);
            } else {
                $this->add($instance);
            }
        }
    }
}


<?php
/**
 * User: zach
 * Date: 3/22/13
 * Time: 7:20 AM
 */

namespace Sherlock\requests;

/**
 * Class BatchCommand
 * @package Sherlock\requests
 */
class BatchCommand implements BatchCommandInterface
{

    /**
     * @var
     */
    protected $commands = array();

    /**
     * @param null $commands
     */
    public function __construct($commands = null)
    {
        if ($commands !== null) {
            $this->commands = $commands;
        }

    }

    /**
     * @param \Sherlock\requests\Command $command
     *
     * @return $this
     */
    public function addCommand($command)
    {
        $this->commands[] = $command;

        return $this;
    }

    /**
     * @return $this
     */
    public function clearCommands()
    {
        $this->commands = array();

        return $this;
    }

    /**
     * Fill all commands that don't have an index set
     *
     * @param $index
     *
     * @return $this
     */
    public function fillIndex($index)
    {
        /** @param Command $value */
        $map = function ($value) use ($index) {
            if ($value->getIndex() === null) {
                $value->index($index);
            }
        };

        array_map($map, $this->commands);

        return $this;
    }

    /**
     * Fill all commands that don't have a type set
     *
     * @param $type
     *
     * @return $this
     */
    public function fillType($type)
    {

        /** @param Command $value */
        $map = function ($value) use ($type) {
            if ($value->getType() === null) {
                $value->type($type);
            }
        };

        array_map($map, $this->commands);

        return $this;
    }

    /**
     *
     */
    public function rewind()
    {
        reset($this->commands);
    }

    /**
     * @return Command
     */
    public function current()
    {
        return current($this->commands);
    }

    /**
     * @return mixed
     */
    public function key()
    {
        return key($this->commands);
    }

    /**
     * @return Command|void
     */
    public function next()
    {
        return next($this->commands);
    }

    /**
     * @return bool
     */
    public function valid()
    {
        return false !== current($this->commands);
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2/12/13
 * Time: 7:37 PM
 * @package Sherlock\requests
 */

namespace Sherlock\requests;

use Sherlock\common\exceptions;
use Sherlock\wrappers;
use Sherlock\responses\IndexResponse;

/**
 * IndexRequest manages index-specific operations
 *
 * Index operations include actions like getting or updating mappings,
 * creating new indices, deleting old indices, state, stats, etc
 *
 * Note, this is distinct from the IndexDocument class, which is solely responsible
 * for indexing documents
 */
class IndexRequest extends Request
{

    protected $dispatcher;

    /**
     * @var array
     */
    protected $params;

    /**
     * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
     * @param                                                    $index
     *
     * @throws \Sherlock\common\exceptions\BadMethodCallException
     * @internal param $node
     */
    public function __construct($dispatcher, $index)
    {
        if (!isset($dispatcher))
                {
                    throw new \Sherlock\common\exceptions\BadMethodCallException("Dispatcher argument required for IndexRequest");
                }
        if (!isset($index))
            throw new \Sherlock\common\exceptions\BadMethodCallException("Index argument required for IndexRequest");

        $this->dispatcher = $dispatcher;

        if (!is_array($index))
            $this->params['index'][] = $index;
        else
            $this->params['index'] = $index;

        $this->params['indexSettings'] = array();
        $this->params['indexMappings'] = array();

        parent::__construct($dispatcher);
    }

    /**
     * @param $name
     * @param $args
     *
     * @return IndexRequest
     */
    public function __call($name, $args)
    {
        $this->params[$name] = $args[0];

        return $this;
    }

    /**
     * ---- Settings / Parameters ----
     * Various settings and parameters to be set before invoking an action
     * Returns $this
     *
     */

    /**
     * Set the index to operate on
     *
     * @param  string       $index     indices to operate on
     * @param  string       $index,... indices to operate on
     *
     * @return IndexRequest
     */
    public function index($index)
    {
        $this->params['index'] = array();
        $args                  = func_get_args();
        foreach ($args as $arg) {
            $this->params['index'][] = $arg;
        }

        return $this;
    }

    /**
     * Set the type to operate on
     *
     * @param  string       $type     indices to operate on
     * @param  string       $type,... indices to operate on
     *
     * @return IndexRequest
     */
    public function type($type)
    {
        $this->params['type'] = array();
        $args                 = func_get_args();
        foreach ($args as $arg) {
            $this->params['type'][] = $arg;
        }

        return $this;
    }

    /**
     * Set the mappings that are used for various operations (set mappings, index creation, etc)
     *
     * @todo fix array-only input
     * @todo add json input
     *
     * @param  array|\sherlock\components\MappingInterface|bool   $mapping,...
     *
     * @throws \Sherlock\common\exceptions\BadMethodCallException
     * @return \Sherlock\requests\IndexRequest
     */
    public function mappings($mapping)
    {
        $args = func_get_args();

        //@todo this is all horrible, burn it and rewrite

        foreach ($args as $arg) {

            if ($arg instanceof \Sherlock\components\MappingInterface) {

                //is this a core type?  Wrap in 'properties'
                if (!($arg instanceof \Sherlock\components\mappings\Analyzer)) {
                    $mappingValue = array("properties" => $arg->toArray());
                } else {
                    $mappingValue = $arg->toArray();
                }

                if (isset($this->params['indexMappings'][$arg->getType()])) {
                    $this->params['indexMappings'][$arg->getType()] = array_merge_recursive(
                        $this->params['indexMappings'][$arg->getType()],
                        $mappingValue
                    );
                } else {
                    $this->params['indexMappings'][$arg->getType()] = $mappingValue;
                }
            } elseif (is_array($arg)) {
                foreach ($arg as $argMapping) {

                    //is this a core type?  Wrap in 'properties'
                    if (!($arg instanceof \Sherlock\components\mappings\Analyzer)) {
                        $mappingValue = array("properties" => $argMapping->toArray());
                    } else {
                        $mappingValue = $argMapping->toArray();
                    }

                    if (isset($this->params['indexMappings'][$argMapping->getType()])) {
                        $this->params['indexMappings'][$argMapping->getType()] = array_merge_recursive(
                            $this->params['indexMappings'][$argMapping->getType()],
                            $mappingValue
                        );
                    } else {
                        $this->params['indexMappings'][$argMapping->getType()] = $mappingValue;
                    }
                }
            } else {
                throw new \Sherlock\common\exceptions\BadMethodCallException("Arguments must be an array or a Mapping Property.");
            }

        }

        return $this;
    }

    /**
     * Set the index settings, used predominantly for index creation
     *
     * @param  array|\sherlock\wrappers\IndexSettingsWrapper      $settings
     * @param  bool                                               $merge
     *
     * @throws \Sherlock\common\exceptions\BadMethodCallException
     * @return IndexRequest
     */
    public function settings($settings, $merge = true)
    {
        if ($settings instanceof \Sherlock\wrappers\IndexSettingsWrapper)
            $newSettings = $settings->toArray();
        else if (is_array($settings))
            $newSettings = $settings;
        else
            throw new \Sherlock\common\exceptions\BadMethodCallException("Unknown parameter provided to settings(). Must be array of settings or IndexSettingsWrapper.");

        if ($merge)
            $this->params['indexSettings'] = array_merge($this->params['indexSettings'], $newSettings);
        else
            $this->params['indexSettings'] = $newSettings;

        return $this;
    }

    /*
     * ---- Actions -----
     * Actions are applied to the index through an HTTP request, and return a response
     *
     */

    /**
     * Delete an index
     *
     * @return \Sherlock\responses\IndexResponse
     * @throws exceptions\RuntimeException
     */
    public function delete()
    {

        if (!isset($this->params['index']))
            throw new exceptions\RuntimeException("Index cannot be empty.");

        $index = implode(',', $this->params['index']);

        $command = new Command();
        $command->index($index)
            ->action('delete');

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        $ret = parent::execute();

        return $ret[0];
    }

    /**
     * Create an index
     *
     * @return \Sherlock\responses\IndexResponse
     * @throws exceptions\RuntimeException
     */
    public function create()
    {

        if (!isset($this->params['index']))
            throw new exceptions\RuntimeException("Index cannot be empty.");

        $index = implode(',', $this->params['index']);

        //Final JSON should be object properties, not an array.  So we need to iterate
        //through the array members and merge into an associative array.
        $mappings = array();
        foreach ($this->params['indexMappings'] as $type => $mapping) {
            $mappings = array_merge($mappings, array($type => $mapping));
        }
        $body = array(
            "settings" => $this->params['indexSettings'],
            "mappings" => $mappings
        );

        $command = new Command();
        $command->index($index)
            ->action('put')
            ->data(json_encode($body, JSON_FORCE_OBJECT));

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        /**
         * @var \Sherlock\responses\IndexResponse
         */
        $ret = parent::execute();

        //clear out mappings, settings
        $this->resetIndex();

        return $ret[0];
    }

    /**
     * Update the settings of an index
     *
     * @todo allow updating settings of all indices
     *
     * @return \Sherlock\responses\IndexResponse
     * @throws exceptions\RuntimeException
     */
    public function updateSettings()
    {

        if (!isset($this->params['index'])) {
                        throw new exceptions\RuntimeException("Index cannot be empty.");
        }

        $index = implode(',', $this->params['index']);

        $body = array("index" => $this->params['indexSettings']);

        $command = new Command();
        $command->index($index)
            ->id('_settings')
            ->action('put')
            ->data(json_encode($body, JSON_FORCE_OBJECT));

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        $ret = parent::execute();

        //clear out mappings, settings
        //$this->resetIndex();

        return $ret[0];

    }

    /**
     * Update/add the Mapping of an index
     *
     * @return \Sherlock\responses\IndexResponse
     * @throws exceptions\RuntimeException
     */
    public function updateMapping()
    {

        if (!isset($this->params['index'])) {
                        throw new exceptions\RuntimeException("Index cannot be empty.");
        }

        if (count($this->params['indexMappings']) > 1) {
                        throw new exceptions\RuntimeException("May only update one mapping at a time.");
        }

        if (!isset($this->params['type'])) {
                        throw new exceptions\RuntimeException("Type must be specified.");
        }

        if (count($this->params['type']) > 1) {
                        throw new exceptions\RuntimeException("Only one type may be updated at a time.");
        }

        $index = implode(',', $this->params['index']);
        $body  = $this->params['indexMappings'];

        $command = new Command();
        $command->index($index)
            ->type($this->params['type'][0])
            ->id('_mapping')
            ->action('put')
            ->data(json_encode($body, JSON_FORCE_OBJECT));

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        $ret = parent::execute();

        //clear out mappings, settings
        //$this->resetIndex();

        return $ret[0];
    }

    /**
     *
     */
    private function resetIndex()
    {
        $this->params['indexMappings'] = array();
        $this->params['indexSettings'] = array();
    }

    /**
     * @param $response
     * @return \Sherlock\responses\IndexResponse|\Sherlock\responses\Response
     */
    protected function getReturnResponse($response)
    {
        return new IndexResponse($response);
    }
}


<?php
/**
 * User: Zachary Tong
 * Date: 4/30/13
 * Time: 1:20 PM
 * @package Sherlock\requests
 */
namespace Sherlock\requests;

use Sherlock\common\exceptions;
use Sherlock\components;
use Sherlock\components\queries;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
 * RawRequest allows arbitrary requests on an ES index
 *
 * @method \Sherlock\requests\RawRequest uri() uri(\string $value)
 * @method \Sherlock\requests\RawRequest method() method(\int $value)
 */
class RawRequest extends Request
{
    /**
     * @var array
     */
    protected $params;

    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcher
     */
    protected $dispatcher;

    /**
     * @param  \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
     *
     * @throws exceptions\BadMethodCallException
     */
    public function __construct($dispatcher)
    {
        if (!isset($dispatcher)) {
            throw new exceptions\BadMethodCallException("Dispatcher argument required for RawRequest");
        }

        $this->dispatcher = $dispatcher;

        parent::__construct($dispatcher);
    }

    /**
     * @param $name
     * @param $args
     *
     * @return RawRequest
     */
    public function __call($name, $args)
    {
        $this->params[$name] = $args[0];

        return $this;
    }

    /**
     * @param $body
     *
     * @return $this
     */
    public function body($body)
    {
        if (is_array(($body)) && count($body) > 0) {
            //Raw array hash map provided
            //put it right into the params
            $this->params['body'] = $body;
        } elseif (is_string($body)) {
            //Raw JSON has been provided
            //Decode from JSON into array
            $this->params['body'] = json_decode($body, true);
        }
        return $this;
    }

    /**
     * Execute the RawRequest
     *
     * @throws \Sherlock\common\exceptions\RuntimeException
     * @return \Sherlock\responses\QueryResponse
     */
    public function execute()
    {

        if (!isset($this->params['uri'])) {
                        throw new exceptions\RuntimeException("URI is required for RawRequest");
        }

        if (!isset($this->params['method'])) {
                        throw new exceptions\RuntimeException("Method is required for RawRequest");
        }

        $command = new Command();
        $command->index($this->params['uri'])
            ->action($this->params['method']);

        if (isset($this->params['body'])) {
            $command->data($this->params['body']);
        }

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        $ret = parent::execute();

        return $ret[0];
    }

    /**
     * Return a JSON representation of the final search request
     *
     * @return string
     */
    public function toJSON()
    {
        $finalQuery = "";

        if (isset($this->params['body'])) {
            $finalQuery = json_encode($this->params['body']);
        }

        return $finalQuery;
    }
}


<?php
/**
 * User: Zachary Tong
 * Date: 2013-02-16
 * Time: 09:24 PM
 * Auto-generated by "generate.php"
 * @package Sherlock\components\queries
 */
namespace Sherlock\components\queries;

use Sherlock\components;
use Sherlock\components\QueryInterface;

/**
 * Class Indices
 * @package Sherlock\components\queries
 */
class Indices extends components\BaseComponent implements QueryInterface
{
    /**
     * @param QueryInterface $value
     *
     * @return $this
     */
    public function no_match_query(QueryInterface $value)
    {
        $this->params['no_match_query'] = $value->toArray();
        return $this;
    }

    /**
     * @param QueryInterface $value
     *
     * @return $this
     */
    public function query(QueryInterface $value)
    {
        $this->params['query'] = $value->toArray();
        return $this;
    }

    /**
     * @param  array | string $indices,...
     *
     * @return Indices
     */
    public function indices($indices)
    {
        $args = $this->normalizeFuncArgs(func_get_args());

        foreach ($args as $arg) {
            if (is_string($arg)) {
                $this->params['indices'][] = $arg;
            }
        }

        return $this;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        $params = $this->convertParams(
            array(
                'indices',
                'query',
                'no_match_query',
            )
        );
        $ret = array('indices' => $params);

        return $ret;
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2/10/13
 * Time: 12:10 PM
 * @package Sherlock\requests
 */
namespace Sherlock\requests;

use Sherlock\common\exceptions;
use Sherlock\components;
use Sherlock\components\queries;
use Symfony\Component\EventDispatcher\EventDispatcher;
use sherlock\components\FacetInterface;
use Sherlock\responses\QueryResponse;

/**
 * SearchRequest facilitates searching an ES index using the ES query DSL
 *
 * @method \Sherlock\requests\SearchRequest timeout() timeout(\int $value)
 * @method \Sherlock\requests\SearchRequest from() from(\int $value)
 * @method \Sherlock\requests\SearchRequest size() size(\int $value)
 * @method \Sherlock\requests\SearchRequest search_type() search_type(\int $value)
 * @method \Sherlock\requests\SearchRequest routing() routing(mixed $value)
 */
class SearchRequest extends Request
{
    /**
     * @var array
     */
    protected $params;

    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcher
     */
    protected $dispatcher;

    /**
     * @param  \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
     *
     * @throws \Sherlock\common\exceptions\BadMethodCallException
     */
    public function __construct($dispatcher)
    {
        if (!isset($dispatcher)) {
            throw new \Sherlock\common\exceptions\BadMethodCallException("Dispatcher argument required for IndexRequest");
        }

        $this->params['filter'] = array();
        $this->dispatcher       = $dispatcher;

        parent::__construct($dispatcher);
    }

    /**
     * @param $name
     * @param $args
     *
     * @return SearchRequest
     */
    public function __call($name, $args)
    {
        $this->params[$name] = $args[0];

        return $this;
    }

    /**
     * Sets the index to operate on
     *
     * @param  string        $index     indices to query
     * @param  string        $index,... indices to query
     *
     * @return SearchRequest
     */
    public function index($index)
    {
        $this->params['index'] = array();
        $args                  = func_get_args();
        foreach ($args as $arg) {
            $this->params['index'][] = $arg;
        }

        return $this;
    }

    /**
     * Sets the type to operate on
     *
     * @param  string        $type     types to query
     * @param  string        $type,... types to query
     *
     * @return SearchRequest
     */
    public function type($type)
    {
        $this->params['type'] = array();
        $args                 = func_get_args();
        foreach ($args as $arg) {
            $this->params['type'][] = $arg;
        }

        return $this;
    }

    /**
     * Sets the query that will be executed
     *
     * @param $query
     *
     * @return SearchRequest
     */
    public function query($query)
    {
        $this->params['query'] = $query;

        return $this;
    }

    /**
     * Sets the query or queries that will be executed
     *
     * @param  \Sherlock\components\SortInterface|array,... $value
     *
     * @return SearchRequest
     */
    public function sort($value)
    {
        $args = func_get_args();

        //single param, array of sorts
        if (count($args) == 1 && is_array($args[0])) {
            $args = $args[0];
        }

        foreach ($args as $arg) {
            if ($arg instanceof \Sherlock\components\SortInterface) {
                $this->params['sort'][] = $arg->toArray();
            }
        }

        return $this;
    }

    /**
     * Sets the fields that will be returned
     *
     * @param $fields
     *
     * @return SearchRequest
     */
    public function fields($fields)
    {
        $this->params['fields'] = $fields;

        return $this;
    }

    /**
     * Sets the filter that will be executed
     *
     * @param $filter
     *
     * @return SearchRequest
     */
    public function filter($filter)
    {
        $this->params['filter'] = $filter;

        return $this;
    }

    /**
     * Sets the facets to operate on
     *
     * @param  FacetInterface $facets     types to query
     * @param  FacetInterface $facets,... types to query
     *
     * @return SearchRequest
     */
    public function facets($facets)
    {
        $this->params['facets'] = array();
        if (is_array($facets)){
            $args = $facets;
        }else{
            $args = func_get_args();
        }
        foreach ($args as $arg) {
            if ($arg instanceof FacetInterface) {
                $this->params['facets'][] = $arg;
            }
        }

        return $this;
    }

    /**
     * @param HighlightInterface $highlight
     *
     * @return SearchRequest
     */
    public function highlight($highlight)
    {
        $this->params['highlight'] = $highlight;

        return $this;
    }

    /**
     * Set the min_score value
     * Documents with a score below this value will be discarded
     * @param Integer $score minimal score
     */
    public function min_score($score)
    {
        $this->params['min_score'] =  $score;
        return $this;
    }

    /**
     * Execute the search request on the ES cluster
     *
     * @throws \Sherlock\common\exceptions\RuntimeException
     * @return \Sherlock\responses\QueryResponse
     */
    public function execute()
    {

        $finalQuery = $this->composeFinalQuery();

        if (isset($this->params['index'])) {
            $index = implode(',', $this->params['index']);
        } else {
            $index = '';
        }

        if (isset($this->params['type'])) {
            $type = implode(',', $this->params['type']);
        } else {
            $type = '';
        }

        if (isset($this->params['search_type'])) {
            $queryParams[] = $this->params['search_type'];
        }

        if (isset($this->params['routing'])) {
            $queryParams[] = $this->params['routing'];
        }

        if (isset($queryParams)) {
            $queryParams = '?' . implode("&", $queryParams);
        } else {
            $queryParams = '';
        }

        $command = new Command();
        $command->index($index)
            ->type($type)
            ->id('_search' . $queryParams)
            ->action('post')
            ->data($finalQuery);

        $this->batch->clearCommands();
        $this->batch->addCommand($command);

        $ret = parent::execute();

        return $ret[0];
    }

    /**
     * Return a JSON representation of the final search request
     *
     * @return string
     */
    public function toJSON()
    {
        $finalQuery = $this->composeFinalQuery();

        return $finalQuery;
    }

    /**
     * Composes the final query, aggregating together the queries, filters, facets and associated parameters
     *
     * @return string
     * @throws \Sherlock\common\exceptions\RuntimeException
     */
    private function composeFinalQuery()
    {
        $finalQuery = array();

        if (isset($this->params['fields'])) {
            $finalQuery['fields'] = $this->params['fields'];
        }

        if (isset($this->params['query']) && $this->params['query'] instanceof components\QueryInterface) {
            $finalQuery['query'] = $this->params['query']->toArray();
        }

        if (isset($this->params['filter']) && $this->params['filter'] instanceof components\FilterInterface) {
            $finalQuery['filter'] = $this->params['filter']->toArray();
        }

        if (isset($this->params['facets'])) {
            $tFacets = array();
            foreach ($this->params['facets'] as $facet) {
                //@todo Investigate a better way of doing this
                //array_merge is supposedly slow when merging arrays of arrays
                if ($facet instanceof FacetInterface) {
                    $tFacets = array_merge($tFacets, $facet->toArray());
                }
            }
            $finalQuery['facets'] = $tFacets;
            unset($tFacets);
        }

        if (isset($this->params['highlight']) && $this->params['highlight'] instanceof components\HighlightInterface) {
            $finalQuery['highlight'] = $this->params['highlight']->toArray();
        }

        foreach (array('from', 'size', 'timeout', 'sort', 'min_score') as $key) {
            if (isset($this->params[$key])) {
                $finalQuery[$key] = $this->params[$key];
            }
        }

        $finalQuery = json_encode($finalQuery, true);

        return $finalQuery;
    }

    /**
     * @param $response
     * @return \Sherlock\responses\QueryResponse|\Sherlock\responses\Response
     */
    protected function getReturnResponse($response)
    {
        return new QueryResponse($response);
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2013-02-19
 * Time: 08:26 PM
 * Auto-generated by "generate.filters.php"
 * @package Sherlock\components\filters
 */
namespace Sherlock\components\filters;

use Sherlock\components;

/**
 * @method \Sherlock\components\filters\Terms field() field(\string $value)
 * @method \Sherlock\components\filters\Terms execution() execution(\string $value) Default: "plain"
 * @method \Sherlock\components\filters\Terms _cache() _cache(\bool $value) Default: true

 */
class Terms extends \Sherlock\components\BaseComponent implements \Sherlock\components\FilterInterface
{
    public function __construct($hashMap = null)
    {
        $this->params['execution'] = "plain";
        $this->params['_cache']    = true;

        parent::__construct($hashMap);
    }

    /**
     * @param  \string | array $terms,...
     *
     * @return Terms
     */
    public function terms($terms)
    {

        $args = func_get_args();

        //single param, array of filters
        if (count($args) == 1 && is_array($args[0])) {
            $args = $args[0];
        }

        foreach ($args as $arg) {
            if (is_string($arg)) {
                $this->params['terms'][] = $arg;
            }
        }

        return $this;
    }

    public function toArray()
    {
        $ret = array(
            'terms' =>
            array(
                $this->params["field"] => $this->params["terms"],
                'execution'            => $this->params["execution"],
                '_cache'               => $this->params["_cache"],
            ),
        );

        return $ret;
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2013-02-16
 * Time: 09:24 PM
 * Auto-generated by "generate.php"
 * @package Sherlock\components\queries
 */
namespace Sherlock\components\queries;

use Sherlock\components;
use Sherlock\components\QueryInterface;

/**
 * Class DisMax
 * @package Sherlock\components\queries
 */
class DisMax extends \Sherlock\components\BaseComponent implements \Sherlock\components\QueryInterface
{
    /**
     * @param float $value
     *
     * @return $this
     */
    public function tie_breaker($value)
    {
        $this->params['tie_breaker'] = $value;
        return $this;
    }

    /**
     * @param float $value
     *
     * @return $this
     */
    public function boost($value)
    {
        $this->params['boost'] = $value;
        return $this;
    }

    /**
     * @param \Sherlock\components\QueryInterface | array $queries,... - one or more Queries can be specified individually, or an array of filters
     *
     * @return $this
     */
    public function queries($queries)
    {

        $args = $this->normalizeFuncArgs(func_get_args());

        foreach ($args as $arg) {
            if ($arg instanceof QueryInterface) {
                $this->params['queries'][] = $arg->toArray();
            }
        }

        return $this;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        $params = $this->convertParams(
            array(
                'tie_breaker',
                'boost',
                'queries',
            )
        );

        $ret = array('dis_max' => $params);

        return $ret;
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2/6/13
 * Time: 8:54 AM
 * @package Sherlock\requests
 */

namespace Sherlock\requests;

use Sherlock\common\events\Events;
use Sherlock\common\events\RequestEvent;
use Sherlock\common\exceptions;
use Sherlock\common\tmp\RollingCurl;
use Sherlock\responses\IndexResponse;

use Sherlock\responses\Response;

/**
 * Base class for various requests.
 *
 * Handles generic functionality such as transport.
 */
class Request
{
    protected $dispatcher;

    public $node;

    //required since PHP doesn't allow argument differences between
    //parent and children under Strict

    /**
     * @var BatchCommandInterface
     */
    protected $batch;

    /**
     * @var array
     */
    protected $options;

    /**
     * @param  \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
     *
     * @throws \Sherlock\common\exceptions\BadMethodCallException
     */
    public function __construct($dispatcher)
    {
        if (!isset($dispatcher)) {
                        throw new exceptions\BadMethodCallException("An Event Dispatcher must be injected into all Request objects");
        }

        $this->dispatcher = $dispatcher;
        $this->batch      = new BatchCommand();
    }

    /**
     * Execute the Request, performs on the actual transport layer
     *
     * @throws exceptions\RuntimeException
     * @throws \Sherlock\common\exceptions\BadResponseException
     * @throws \Sherlock\common\exceptions\ClientErrorResponseException
     * @return \Sherlock\responses\Response
     */
    public function execute()
    {
        //construct a requestEvent and dispatch it with the "request.preexecute" event
        //This will, among potentially other things, populate the $node variable with
        //values from Cluster
        $event = new RequestEvent($this);
        $this->dispatcher->dispatch(Events::REQUEST_PREEXECUTE, $event);

        //Make sure the node variable is set correctly after the event
        if (!isset($this->node)) {
                        throw new exceptions\RuntimeException("Request requires a valid, non-empty node");
        }

        if (!isset($this->node['host'])) {
                        throw new exceptions\RuntimeException("Request requires a host to connect to");
        }

        if (!isset($this->node['port'])) {
                        throw new exceptions\RuntimeException("Request requires a port to connect to");
        }

        $path = 'http://' . $this->node['host'] . ':' . $this->node['port'];

        $rolling = new RollingCurl\RollingCurl();
        $rolling->setHeaders(array('Content-Type: application/json'));

        if ( ! is_null($this->options)) {
            $rolling->setOptions($this->options);
        }

        $window  = 10;
        $counter = 0;

        /** @var BatchCommandInterface $batch */
        $batch = $this->batch;

        //prefill our buffer with a full window
        //the rest will be streamed by our callback closure
        foreach ($batch as $request) {

            /** @var CommandInterface $req */
            $req    = $request;
            $action = $req->getAction();

            if ($action == 'put' || $action == 'post') {
                $rolling->$action(
                    $path . $req->getURI(),
                    json_encode($req->getData()),
                    array('Content-Type: application/json')
                );
            } else {
                $rolling->$action($path . $req->getURI());
            }

            if ($counter > $window) {
                break;
            }
        }

        /**
         * @param RollingCurl\Request     $request
         * @param RollingCurl\RollingCurl $rolling
         */
        $callback = function (RollingCurl\Request $request, RollingCurl\RollingCurl $rolling) use ($batch, $path) {

            //a curl handle just finished, advance the iterator one and add to the queue
            //First check to see if there are any left to process (aka valid)
            if ($batch->valid()) {

                //advance
                $batch->next();

                //make sure we haven't hit the end
                if ($batch->valid()) {

                    $data = $batch->current();

                    $action = $data->getAction();

                    if ($action == 'put' || $action == 'post') {
                        $rolling->$action($path . $data->getURI(), json_encode($data->getData()));
                    } else {
                        $rolling->$action($path . $data->getURI());
                    }
                }
            }
        };

        $rolling->setSimultaneousLimit($window);
        $rolling->setCallback($callback);

        $rolling->execute();
        $ret = $rolling->getCompletedRequests();

        $this->batch = new BatchCommand();

        $finalResponse = array();
        foreach ($ret as $response) {
            $finalResponse[] = $this->getReturnResponse($response);
        }

        return $finalResponse;
    }

    /**
     * @param $response
     * @return \Sherlock\responses\Response
     */
    protected function getReturnResponse($response)
    {
        return new Response($response);
    }

    /**
     * Set the options that should be set on the cURL request.
     *
     * @param  $options
     * @return \Sherlock\requests\Request
     */
    public function setOptions(array $options)
    {
        $this->options = $options;

        return $this;
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2013-02-16
 * Time: 09:24 PM
 * Auto-generated by "generate.php"
 * @package Sherlock\components\queries
 */
namespace Sherlock\components\queries;

use Sherlock\common\exceptions\InvalidArgumentException;
use Sherlock\components;
use Sherlock\components\QueryInterface;

/**
 * Class CustomFiltersScore
 * @package Sherlock\components\queries
 */
class CustomFiltersScore extends components\BaseComponent implements QueryInterface
{
    /**
     * @param QueryInterface $value
     *
     * @return $this
     */
    public function query(QueryInterface $value)
    {
        $this->params['query'] = $value->toArray();
        return $this;
    }

    /**
     * @param string $value
     *
     * @return $this
     */
    public function score_mode($value)
    {
        $this->params['score_mode'] = $value;
        return $this;
    }

    /**
     * @param float $value
     *
     * @return $this
     */
    public function max_boost($value)
    {
        $this->params['max_boost'] = $value;
        return $this;
    }

    /**
     * @param \Sherlock\components\FilterInterface[]
     *
     * @return CustomFiltersScore
     */
    public function filters($filter)
    {
        $args = func_get_args();

        foreach ($args as $arg) {
            $this->parseFiltersArray($arg);
        }

        return $this;
    }

    /**
     * @param $arg
     */
    private function parseFiltersArray($arg)
    {
        if (isset($arg['filter']) === true && isset($arg['boost']) === true) {
            $this->filter($arg['filter'], $arg['boost']);
        } elseif (is_array($arg) === true) {
            foreach ($arg as $a) {
                $this->parseFiltersArray($a);
            }
        }
    }

    /**
     * @param components\FilterInterface $filter
     * @param                            $boost
     * @return $this
     */
    public function filter(components\FilterInterface $filter, $boost)
    {
        $this->params['filters'][] = array(
            "filter" => $filter->toArray(),
            'boost' => $boost
        );
        return $this;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        $params = $this->convertParams(
            array(
                'query',
                'filters',
                'score_mode',
                'max_boost',
            )
        );

        $ret = array('custom_filters_score' => $params);

        return $ret;
    }

}


<?php
/**
 * User: Zachary Tong
 * Date: 2/7/13
 * Time: 5:20 PM
 * @package Sherlock\components
 */

namespace sherlock\components;

/**
 * BaseComponent is an abstract class for various components (filters, queries, mappings, etc)
 *
 * BaseComponent is used to enforce the toJSON() and toArray() functions in children classes,
 * as well as provide a unified logger for all components.  It also manages hashmap initialization
 * on construction.  Lastly, it provides a convenient type when you know a variable will be a
 * query/filter/mapping...but not sure which until runtime
 */
abstract class BaseComponent
{
    /**
     * @var array Parameters of the component, varies depending on the child class
     */
    protected $params = array();

    /**
     * @param array $hashMap Optional hashmap parameter, accepts an associative array to set parameters manually
     */
    public function __construct($hashMap = null)
    {
        if (is_array(($hashMap)) && count($hashMap) > 0) {
            //merge the provided values with our param array, overwriting defaults where necessary
            $this->params = array_merge($this->params, $hashMap);
        }

    }

    /**
     * Magic method, primary setter of the components.
     *
     * Most components in Sherlock use magic methods to set values, returning itself
     * so that methods can be chained.  In most cases, the method simply sets a parameter
     * of it's own name, but sometimes is overridden by the child class.  toJSON is a special
     * method call that is only found in BaseComponent
     *
     * @param $name
     * @param $arguments
     *
     * @return BaseComponent|string
     */
    public function __call($name, $arguments)
    {
        if ($name == 'toJSON') {
            return $this->toJSON();
        }

        $this->params[$name] = $arguments[0];

        return $this;
    }

    /**
     * Return a JSOn representation of this component
     *
     * @return string
     */
    public function toJSON()
    {
        return json_encode($this->toArray());
    }

    /**
     * Convert a parameter list into a composed parameter request array
     *
     * @param array $params array of parameters to convert into assoc array
     *
     * @return array
     */
    protected function convertParams($params)
    {
        $paramArray = array();
        foreach ($params as $param) {
            if (isset($this->params[$param]) === true) {
                $paramArray[$param] = $this->params[$param];
            }
        }

        return $paramArray;
    }

    /**
     * Normalize func_get_args to a consistent format.
     * This allows for single, array or inline syntax
     *
     * @param QueryInterface|array $args Singular QueryInterface, array of QueryInterfaces, or inline array
     *
     * @return mixed
     */
    protected function normalizeFuncArgs($args)
    {
        if (count($args) === 1 && is_array($args[0])) {
            $args = $args[0];
        }

        return $args;
    }

    /**
     * Return an associative array representation of this component
     *
     * @return array
     */
    abstract public function toArray();

}