<?php

/**
 * @author Anton Terekhov <anton@netmonsters.ru>
 * @copyright Copyright Anton Terekhov, NetMonsters LLC, 2011
 * @license https://github.com/AntonTerekhov/OrientDB-PHP/blob/master/LICENSE
 * @link https://github.com/AntonTerekhov/OrientDB-PHP
 * @package OrientDB-PHP
 */

/**
 * Main class for OrientDB-PHP commands
 *
 * @author Anton Terekhov <anton@netmonsters.ru>
 * @package OrientDB-PHP
 * @subpackage Command
 */
abstract class OrientDBCommandAbstract
{

    const SHUTDOWN = 1;

    const CONNECT = 2;

    const DB_OPEN = 3;

    const DB_CREATE = 4;

    const DB_CLOSE = 5;

    const DB_EXIST = 6;

    const DB_DELETE = 7;

    // const DB_SIZE = 8;

    // const DB_COUNTRECORDS = 9;

    const DATACLUSTER_ADD = 10;

    const DATACLUSTER_REMOVE = 11;

    const DATACLUSTER_COUNT = 12;

    const DATACLUSTER_DATARANGE = 13;

    const DATASEGMENT_ADD = 20;

    const DATASEGMENT_REMOVE = 21;

    const RECORD_LOAD = 30;

    const RECORD_CREATE = 31;

    const RECORD_UPDATE = 32;

    const RECORD_DELETE = 33;

    const COUNT = 40;

    const COMMAND = 41;

    const TX_COMMIT = 60;

    const CONFIG_GET = 70;

    const CONFIG_SET = 71;

    const CONFIG_LIST = 72;

    // const DB_RELOAD = 73;

    const DB_LIST = 74;

    const STATUS_SUCCESS = 0x00;

    const STATUS_ERROR = 0x01;

    /**
     * Instance of OrientDBSocket
     * @var OrientDBSocket
     */
    private $socket;

    /**
     * Command type
     * @var integer
     */
    public $opType;

    /**
     * Attributes list
     * @var array
     */
    protected $attribs;

    /**
     * TransactionID to identify DB_OPEN and CONNECT queries
     * @var integer
     */
    protected static $transactionID = 0;

    /**
     * Current transaction id
     * @var integer
     */
    protected $currentTransactionID;

    /**
     * Request Status, OrientDBCommandAbstract::STATUS_SUCCESS or OrientDBCommandAbstract::STATUS_ERROR
     * @var string
     */
    protected $requestStatus;

    /**
     * Request bytes transferred to server
     * @var string
     */
    protected $requestBytes;

    /**
     * Print debug messages
     * @var boolean
     */
    protected $debug;

    /**
     * Link to OrientDB instance
     * @var OrientDB
     */
    protected $parent;

    /**
     * Construct new instance
     * @param OrientDB $parent
     */
    public function __construct($parent)
    {
        $this->socket = $parent->socket;
        $this->debug = $parent->isDebug();
        $this->parent = $parent;
    }

    /**
     * Prepare command execution
     * @throws OrientDBWrongCommandException
     */
    public function prepare()
    {
        $this->addByte(chr($this->opType));
        if ($this->opType === self::DB_OPEN || $this->opType === self::CONNECT) {
            $this->currentTransactionID = -(++self::$transactionID);
        } else {
            if (in_array($this->opType, $this->parent->getCommandsRequiresConnect())) {
                $this->currentTransactionID = $this->parent->getSessionIDServer();
            } elseif (in_array($this->opType, $this->parent->getCommandsRequiresDBOpen())) {
                $this->currentTransactionID = $this->parent->getSessionIDDB();
            } else {
                throw new OrientDBWrongCommandException('Unknown command');
            }
        }
        $this->addInt($this->currentTransactionID);
    }

    /**
     * Execute command by sending data to server, receive initial reply
     * @throws null|OrientDBException
     * @throws OrientDBException
     * @return mixed
     */
    public function execute()
    {
        $this->socket->debug = $this->debug;
        $this->socket->send($this->requestBytes);

        if (is_null($this->parent->protocolVersion)) {
            $this->debugCommand('protocol_version');
            $serverProtocolVersion = $this->readShort();
            $this->parent->setProtocolVersion($serverProtocolVersion);
        }
        if ($this->opType == self::DB_CLOSE) {
            // No incoming bytes
            return null;
        }

        $this->debugCommand('request_status');
        $this->requestStatus = $this->readByte();

        $this->debugCommand('TransactionID');
        $requestTransactionID = $this->readInt();
        if ($requestTransactionID !== $this->currentTransactionID) {
            throw new OrientDBException('Transaction ID mismatch');
        }

        if ($this->requestStatus === chr(OrientDBCommandAbstract::STATUS_SUCCESS)) {
            $data = $this->parseResponse();
            if ($this->opType === self::DB_OPEN) {
                $this->parent->setSessionIDDB($this->sessionID);
            } elseif ($this->opType === self::CONNECT) {
                $this->parent->setSessionIDServer($this->sessionID);
            }
            return $data;
        } elseif ($this->requestStatus === chr(OrientDBCommandAbstract::STATUS_ERROR)) {
            $exception = null;
            while ($this->readByte() === chr(OrientDBCommandAbstract::STATUS_ERROR)) {
                $this->debugCommand('exception_javaclass');
                $javaException = $this->readString();
                $this->debugCommand('exception_message');
                $javaExceptionDescr = $this->readString();
                $exception = new OrientDBException($javaException . ': ' . $javaExceptionDescr, 0, is_null($exception) ? null : $exception);
            }
            throw $exception;
        } else {
            throw new OrientDBException('Unknown error');
        }
    }

    /**
     * Parse server reply
     * @return mixed
     */
    protected abstract function parseResponse();

    /**
     * Get command attributes
     */
    public function setAttribs()
    {
        $this->attribs = func_get_args();
    }

    /**
     * Read raw data from socket
     * @param int $length
     * @return string
     */
    protected function readRaw($length)
    {
        $data = '';
        $dataLeft = $length;
        do {
            $data .= $this->socket->read($dataLeft);
            $dataLeft = $length - strlen($data);
        } while ($dataLeft > 0);
        return $data;
    }

    /**
     * Read 1 byte from socket
     * @return string
     */
    protected function readByte()
    {
        return $this->readRaw(1);
    }

    /**
     * Read short from server
     * @return int
     */
    protected function readShort()
    {
        $data = unpack('n', $this->readRaw(2));
        return self::convertComplementShort(reset($data));
    }

    /**
     * Read int from socket. Using custom function to convert twos-complement integer
     * @return int
     */
    protected function readInt()
    {
        $data = unpack('N', $this->readRaw(4));
        return self::convertComplementInt(reset($data));
    }

    /**
     * Read long from socket. Returns int
     * @throws OrientDBException
     * @return int
     */
    protected function readLong()
    {
        // First of all, read 8 bytes, divided into hi and low parts
        $hi = unpack('N', $this->readRaw(4));
        $hi = reset($hi);
        $low = unpack('N', $this->readRaw(4));
        $low = reset($low);
        // Unpack 64-bit signed long
        return self::unpackI64($hi, $low);
    }

    /**
     * Read string from socket, including its length
     * @return string
     */
    protected function readString()
    {
        $size = $this->readInt();
        if ($size === -1) {
            return null;
        }
        if ($size === 0) {
            return '';
        }
        return $this->readRaw($size);
    }

    /**
     * Read bytes stream from socket, including int length
     * @return string
     */
    protected function readBytes()
    {
        $size = $this->readInt();
        if ($size === -1) {
            return null;
        }
        if ($size === 0) {
            return '';
        }
        return $this->readRaw($size);
    }

    /**
     * Read entire record from socket
     * @return boolean|OrientDBTypeLink|OrientDBRecord
     */
    protected function readRecord()
    {

        $this->debugCommand('record_marker');
        $marker = $this->readShort();
        /**
         * @see enterprise/src/main/java/com/orientechnologies/orient/enterprise/channel/binary/OChannelBinaryProtocol.java
         */

        // -2=no record
        if ($marker == -2) {
            // no record
            return false;
        }

        // -3=Only recordID
        if ($marker == -3) {
            // only recordID
            $this->debugCommand('record_clusterID');
            $clusterID = $this->readShort();
            $this->debugCommand('record_position');
            $recordPos = $this->readLong();
            return new OrientDBTypeLink($clusterID, $recordPos);
        }

        $record = new OrientDBRecord();
        $this->debugCommand('record_type');
        $record->type = $this->readByte();
        $this->debugCommand('record_clusterID');
        $record->clusterID = $this->readShort();
        $this->debugCommand('record_position');
        $record->recordPos = $this->readLong();
        $this->debugCommand('record_version');
        $record->version = $this->readInt();
        $this->debugCommand('record_content');
        $record->content = $this->readBytes();

        return $record;
    }

    /**
     * Add byte to outgoing data
     * @param string $byte
     */
    protected function addByte($byte)
    {
        $this->requestBytes .= $byte;
    }

    /**
     * Add short to outgoing data. Short is represented by int
     * @param int $short
     */
    protected function addShort($short)
    {
        $this->requestBytes .= pack('n', $short);
    }

    /**
     * Add int to outgoing data
     * @param int $int
     */
    protected function addInt($int)
    {
        $this->requestBytes .= pack('N', $int);
    }

    /**
     * Add long to outgoing data. Long is represented by int
     * @param int $long
     */
    protected function addLong($long)
    {
        /**
         * @TODO support 64-bit
         */
        $this->requestBytes .= str_repeat(chr(0), 4) . pack('N', $long);
    }

    /**
     * Add string to outgoing data. Length is calculated automatically
     * @param string $string
     */
    protected function addString($string)
    {
        $this->addInt(strlen($string));
        $this->requestBytes .= $string;
    }

    /**
     * Add byte stream to outgoing data. Length is calculated automatically
     * @param string $string
     */
    protected function addBytes($string)
    {
        if ($string instanceof OrientDBRecord) {
            $string = (string) $string;
        }
        $this->addInt(strlen($string));
        $this->requestBytes .= $string;
    }

    /**
     * If debug is enabled, output $commandName to STDOUT
     * @param string $commandName
     */
    protected function debugCommand($commandName)
    {
        if ($this->debug) {
            echo '>' . $commandName . PHP_EOL;
        }
    }

    /**
     * Convert twos-complement integer after unpack() on x64 systems
     * @static
     * @param int $int
     * @return int
     */
    public static function convertComplementInt($int)
    {
        /*
         *  Valid 32-bit signed integer is -2147483648 <= x <= 2147483647
         *  -2^(n-1) < x < 2^(n-1) -1 where n = 32
         */
        if ($int > 2147483647) {
            return -(($int ^ 0xFFFFFFFF) + 1);
        }
        return $int;
    }

    /**
     * Convert twos-complement short after unpack() on x64 systems
     * @static
     * @param $short
     * @return int
     */
    public static function convertComplementShort($short)
    {
        /*
         *  Valid 16-bit signed integer is -32768 <= x <= 32767
         *  -2^(n-1) < x < 2^(n-1) -1 where n = 16
         */
        if ($short > 32767) {
            return -(($short ^ 0xFFFF) + 1);
        }
        return $short;
    }

    /**
     * Unpacks 64 bits signed long
     * @static
     * @param $hi int Hi bytes of long
     * @param $low int Low bytes of long
     * @throws OrientDBException
     * @return int|string
     */
    public static function unpackI64($hi, $low)
    {
        // Packing is:
        // OrientDBHelpers::hexDump(pack('NN', $int >> 32, $int & 0xFFFFFFFF));

        // If x64 system, just shift hi bytes to the left, add low bytes. Piece of cake.
        if (PHP_INT_SIZE === 8) {
            return ($hi << 32) + $low;
        }

        // x32
        // Check if long could fit into int
        $hiComplement = self::convertComplementInt($hi);
        if ($hiComplement === 0) {
            // Hi part is 0, low will fit in x32 int
            return $low;
        } elseif ($hiComplement === -1) {
            // Hi part is negative, so we just can convert low part
            if ($low >= 0x80000000) {
                // Check if low part is lesser than minimum 32 bit signed integer
                return self::convertComplementInt($low);
            }
        }

        // x32 string with bc_match
        // Check if module is available
        if (!extension_loaded('bcmath')) {
            throw new OrientDBException('Required bcmath module to continue');
        }

        // Sign char
        $sign = '';
        $lastBit = 0;
        // This is negative number
        if ($hiComplement < 0) {
            $hi = ~$hi;
            $low = ~$low;
            $lastBit = 1;
            $sign = '-';
        }

        // Format bytes properly
        $hi = sprintf('%u', $hi);
        $low = sprintf('%u', $low);

        // Do math
        $temp = bcmul($hi, '4294967296');
        $temp = bcadd($low, $temp);
        $temp = bcadd($temp, $lastBit);
        return $sign . $temp;
    }
}


<?php

/**
 * @author Anton Terekhov <anton@netmonsters.ru>
 * @copyright Copyright Anton Terekhov, NetMonsters LLC, 2011-2013
 * @license https://github.com/AntonTerekhov/OrientDB-PHP/blob/master/LICENSE
 * @link https://github.com/AntonTerekhov/OrientDB-PHP
 * @package OrientDB-PHP
 */

/**
 * Class representing OrientDB Record
 * @author Anton Terekhov <anton@netmonsters.ru>
 * @package OrientDB-PHP
 * @subpackage Datatypes
 * @property int clusterID ClusterID of record
 * @property int|string recordPos Record position in cluster
 * @property string className Class name of record
 * @property string content Document source as delivered from OrientDB
 * @property-read string recordID Fully-qualified recordID
 *
 */
class OrientDBRecord
{
    /**
     * Flag, if current $this->content was already decoded to $this->data
     * @var bool
     */
    private $isParsed = false;

    /**
     * ClassName as parsed from document
     * @var string
     */
    private $className;

    /**
     * Document type
     * @example OrientDB::RECORD_TYPE_DOCUMENT
     * @see OrientDB::$recordTypes
     * @var string
     */
    public $type = OrientDB::RECORD_TYPE_DOCUMENT;

    /**
     * ClusterID
     * @var int
     */
    private $clusterID;

    /**
     * Record position in cluster
     * @var int
     */
    private $recordPos;

    /**
     * Full qualified record ID
     * @example 1:1
     * @var string
     */
    private $recordID;

    /**
     * Document version
     * @var int
     */
    public $version;

    /**
     * Document source as delivered from OrientDB
     * @var string
     */
    private $content;

    /**
     * A placeholder for document data
     * @var OrientDBData|string
     */
    public $data;

    /**
     * Construct new instance
     */
    public function __construct()
    {
        $this->data = new OrientDBData($this);
        $this->isParsed = true;
    }

    /**
     * Parses $this->content and populates $this->data
     * @return void
     */
    public function parse()
    {
        // Check, if we already decoded current $this->content
        if (!$this->isParsed) {
            // Parse record content
            if ($this->type == OrientDB::RECORD_TYPE_DOCUMENT) {
                $parser = new OrientDBRecordDecoder(rtrim($this->content));

                $this->className = $parser->className;
                foreach ($parser->data as $key => $value) {
                    $this->data->$key = $value;
                }
            } else {
                $this->data = $this->content;
            }
            $this->isParsed = true;
        }
    }

    /**
     * Forces that record was already parsed
     * @return void
     */
    public function setParsed()
    {
        $this->isParsed = true;
    }

    /**
     * Fully resets class, equals to new()
     * @return void
     */
    public function reset()
    {
        $this->data = new OrientDBData($this);
        $this->className = null;
        $this->content = null;
        $this->isParsed = true;
        $this->clusterID = null;
        $this->recordPos = null;
        $this->recordID = null;
        $this->version = null;
    }

    /**
     * Resets Record data, keeping className and clusterID intact
     * @return void
     */
    public function resetData()
    {
        $this->data = new OrientDBData($this);
        $this->content = null;
        $this->isParsed = true;
        $this->recordPos = null;
        $this->recordID = null;
        $this->version = null;
    }

    /**
     * Magic method
     * @return string
     */
    public function __toString()
    {
        $encoder = new OrientDBRecordEncoder($this->data, $this->className);
        return $encoder->buffer;
    }

    /**
     * Parses recordID from $this->clusterID and $this->recordPos. Populates $this->recordID
     * @return void
     */
    private function parseRecordID()
    {
        if ((int) $this->clusterID !== $this->clusterID) {
            return;
        }
        if ($this->clusterID >= 0 && is_numeric($this->recordPos)) {
            if ($this->recordPos >= 0) {
                $this->recordID = $this->clusterID . ':' . $this->recordPos;
            }
        }
    }

    public function __get($name)
    {
        if ($name === 'className') {
            $this->parse();
        }
        if ($name === 'recordPos' || $name === 'clusterID' || $name === 'recordID' || $name === 'className' || $name == 'content') {
            return $this->$name;
        }
        $trace = debug_backtrace();
        trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
        return null;
    }

    public function __set($name, $value)
    {
        if ($name === 'recordPos' || $name === 'clusterID') {
            if (is_numeric($value) || is_null($value)) {
                $this->$name = $value;
            }
            $this->parseRecordID();
        } elseif ($name === 'content') {
            $this->content = $value;
            $this->isParsed = false;
            $this->data = new OrientDBData($this);
        } elseif ($name === 'className') {
            $this->className = $value;
        } elseif ($name === 'recordID') {
            $trace = debug_backtrace();
            trigger_error('Can\'t directly set property ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
        } else {
            $trace = debug_backtrace();
            trigger_error('Can\'t set property ' . $name . ' via __set() in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_NOTICE);
        }
    }
}

/**
 * Class representing OrientDB Record Data. Mainly used for parsing record "on demand" while any function for getting data is called
 * @author Anton Terekhov <anton@netmonsters.ru>
 * @package OrientDB-PHP
 * @subpackage Datatypes
 *
 */
class OrientDBData implements Countable, Iterator
{
    /**
     * Data holder
     * @var array
     */
    private $data = array();

    /**
     * Link to parent record, for calling it's ->parse()
     * @var OrientDBRecord|null
     */
    private $record;

    /**
     * Link to parent record
     * @param $record OrientDBRecord|null
     * @throws OrientDBException
     */
    public function __construct($record = null)
    {
        if (!is_null($record)) {
            if ($record instanceof OrientDBRecord) {
                $this->record = $record;
            } else {
                throw new OrientDBException('Only OrientDBRecord instance can be used in __construct()');
            }
        }
    }

    public function &__get($name)
    {
        $this->isParsed();
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        trigger_error('Undefined index: ' . $name, E_USER_NOTICE);
    }

    public function __set($name, $value)
    {
        $this->data[$name] = $value;
    }

    /**
     * Magic function for isset() call
     * @param $name
     * @return bool
     */
    public function __isset($name)
    {
        $this->isParsed();
        return isset($this->data[$name]);
    }

    /**
     * Magic function for unset() calls
     * @param $name
     * @return void
     */
    public function __unset($name)
    {
        unset($this->data[$name]);
    }

    /**
     * Count elements of an object. The return value is cast to an integer.
     * @link http://php.net/manual/en/countable.count.php
     * @return int The custom count as an integer.
     */
    public function count()
    {
        $this->isParsed();
        return count($this->data);
    }

    /**
     * Return the current element
     * @link http://php.net/manual/en/iterator.current.php
     * @return mixed Can return any type.
     */
    public function current()
    {
        $this->isParsed();
        return current($this->data);
    }

    /**
     * Move forward to next element
     * @link http://php.net/manual/en/iterator.next.php
     * @return void Any returned value is ignored.
     */
    public function next()
    {
        $this->isParsed();
        next($this->data);
    }

    /**
     * Return the key of the current element
     * @link http://php.net/manual/en/iterator.key.php
     * @return int scalar on success
     * 0 on failure.
     */
    public function key()
    {
        $this->isParsed();
        return key($this->data);
    }

    /**
     * Checks if current position is valid
     * @link http://php.net/manual/en/iterator.valid.php
     * @return boolean The return value will be casted to boolean and then evaluated.
     * Returns true on success or false on failure.
     */
    public function valid()
    {
        $this->isParsed();
        return key($this->data) !== null;
    }

    /**
     * Rewind the Iterator to the first element
     * @link http://php.net/manual/en/iterator.rewind.php
     * @return void Any returned value is ignored.
     */
    public function rewind()
    {
        $this->isParsed();
        reset($this->data);
    }

    /**
     * Check, if current dataset was already de-serialized
     * @return void
     */
    private function isParsed()
    {
        if (!is_null($this->record)) {
            // If we have link to parent record
            $this->record->parse();
        }
    }

    /**
     * Get all keys for data
     * @return array
     */
    public function getKeys()
    {
        return array_keys($this->data);
    }
}


<?php
namespace PackageFactory\AtomicFusion\FusionObjects;

/**
 * This file is part of the PackageFactory.AtomicFusion package
 *
 * (c) 2016
 * Wilhelm Behncke <wilhelm.behncke@googlemail.com>
 * Martin Ficzel <martin.ficzel@gmx.de>
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

use Neos\Flow\Annotations as Flow;
use Neos\Fusion\FusionObjects\ArrayImplementation;

/**
 * A Fusion Component-Object
 *
 * All properties except ``renderer`` are pushed into a context variable ``props``
 * afterwards the ``renderer`` is evaluated
 *
 * //fusionPath renderer The variable to display a dump of.
 * //fusionPath * generic Fusion values that will be added to the ``props`` object in the context
 * @api
 */
class ComponentImplementation extends ArrayImplementation
{
    /**
     * Properties that are ignored and not included into the ``props`` context
     *
     * @var array
     */
    protected $ignoreProperties = ['__meta', 'renderer'];

    /**
     * Get the component props as an associative array
     *
     * @return array
     */
    public function getProps()
    {
        $sortedChildFusionKeys = $this->sortNestedFusionKeys();

        $props = [];
        foreach ($sortedChildFusionKeys as $key) {
            try {
                $props[$key] = $this->fusionValue($key);
            } catch (\Exception $e) {
                $props[$key] = $this->runtime->handleRenderingException($this->path . '/' . $key, $e);
            }
        }

        return $props;
    }

    /**
     * Render the component with the given props
     *
     * @param array $props
     * @return void|string
     */
    public function renderComponent(array $props)
    {
        $context = $this->runtime->getCurrentContext();
        $context['props'] = $props;
        $this->runtime->pushContextArray($context);
        $result = $this->runtime->render($this->path . '/renderer');
        $this->runtime->popContext();

        return $result;
    }

    /**
     * Evaluate the fusion-keys and transfer the result into the context as ``props``
     * afterwards evaluate the ``renderer`` with this context
     *
     * @return void|string
     */
    public function evaluate()
    {
        $props = $this->getProps();
        return $this->renderComponent($props);
    }
}


<?php
namespace PackageFactory\AtomicFusion\Eel;

/**
 * This file is part of the PackageFactory.AtomicFusion package
 *
 * (c) 2016
 * Wilhelm Behncke <wilhelm.behncke@googlemail.com>
 * Martin Ficzel <martin.ficzel@gmx.de>
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

use Neos\Flow\Annotations as Flow;
use Neos\Eel\ProtectedContextAwareInterface;

/**
 * AtomicFusion EEl-Helper
 *
 * @api
 */
class AtomicFusionHelper implements ProtectedContextAwareInterface
{
    /**
     * Render the arguments as class-names after applying some rules
     *
     * @param mixed $arguments [optional] class-name rules
     * argument rules:
     * - falsy: (null, '', [], {}) -> not rendered
     * - array: all items that are scalar and truthy are rendered as class-name
     * - object: keys that have a truthy values are rendered as class-name
     * - scalar: is cast to string and rendered as class-name
     */
    public function classNames(...$arguments)
    {
        $classNames = [];
        foreach ($arguments as $argument) {
            if ((bool)$argument === true) {
                if (is_array($argument)) {
                    $keys = array_keys($argument);
                    $isAssoc = array_keys($keys) !== $keys;
                    if ($isAssoc) {
                        foreach ($argument as $className => $condition) {
                            if ((bool)$condition === true) {
                                $classNames[] = (string)$className;
                            }
                        }
                    } else {
                        foreach ($argument as $className) {
                            if (is_scalar($className) && !!is_bool($condition) && (bool)$className === true) {
                                $classNames[] = (string)$className;
                            }
                        }
                    }
                } elseif (is_scalar($argument) && !is_bool($argument)) {
                    $classNames[] = (string)$argument;
                }
            }
        }
        return implode(' ', $classNames);
    }

    public function allowsCallOfMethod($methodName)
    {
        return true;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;

/**
 * The PaymentRequestBodyBuilder builds a representation of a payment request body
 */
class PaymentRequestBodyBuilder
{
    protected $intent;
    protected $payer;
    protected $urls;
    protected $transactions;

    /**
     * Constructor
     *
     * @param $payerBuilder PayPalRestApiClient\Builder\PayerBuilder or null
     * @param $urlsBuilder PayPalRestApiClient\Builder\UrlsBuilder or null
     * @param $transactionsBuilder PayPalRestApiClient\Builder\TransactionsBuilder or null
     */
    public function __construct(
        PayerBuilder $payerBuilder = null,
        UrlsBuilder $urlsBuilder = null,
        TransactionsBuilder $transactionsBuilder = null
    ) {
        $this->payerBuilder = is_null($payerBuilder) ? new PayerBuilder() : $payerBuilder;
        $this->urlsBuilder = is_null($urlsBuilder) ? new UrlsBuilder() : $urlsBuilder;
        $this->transactionsBuilder = is_null($transactionsBuilder) ? new TransactionsBuilder() : $transactionsBuilder;
    }

    /**
     * Build an array or a json string that represents the body of a payment request
     *
     * @param string $intent not null and should be one of the following values: sale, authorize, order
     * @param array|\ArrayAccess|PayPalRestApiClient\Model\PayerInterface $payer checkout the PayerBuilder documentation
     * @param array|\ArrayAccess $urls checkout the UrlsBuilder documentation
     * @param array|\ArrayAccess|PayPalRestApiClient\Model\TransactionInterface $transactions
     * @param $returnArray boolean default false
     *
     * @return array|json return by default a json string, if $returnArray is set to true it will return an array
     *
     * @throws PayPalRestApiClient\Exception\BuilderException if intent is not valid or the other arrays will not pass the validation on the other builders
     *
     * @see https://developer.paypal.com/docs/api/#payments
     */
    public function build($intent, $payer, $urls, $transactions, $returnArray = false)
    {
        $this->assertIntent($intent);
        $this->intent = $intent;

        $requestBody = array();
        $requestBody['intent'] = $this->intent;
        $requestBody['payer'] = $this->payerBuilder->buildArray($payer);
        $requestBody['transactions'] = $this->transactionsBuilder->buildArray($transactions);
        $requestBody['redirect_urls'] = $this->urlsBuilder->buildArray($urls);

        if ($returnArray) {
            return $requestBody;
        }

        return json_encode($requestBody);
    }

    private function assertIntent($intent)
    {
        if ( ! in_array($intent, array('sale', 'authorize', 'order'))) {

            throw new BuilderException("intent is not valid: allowed value are 'sale', 'authorize', 'order'");
        }
    }

}


<?php

namespace PayPalRestApiClient\Traits;

use PayPalRestApiClient\Model\LinkInterface;

/**
 * LinkFinder trait defines how to search in a list of PayPalRestApiClient\Model\LinkInterface instances
 */
trait LinkFinder
{
    /**
     * @return array of PayPalRestApiClient\Model\LinkInterface instances
     */
    abstract public function getLinks();

    /**
     * Searches and returns the first link object with attribute "rel" equal to $rel input variable
     *
     * @param string $rel
     *
     * @return PayPalRestApiClient\Model\LinkInterface
     */
    protected function findLink($rel)
    {
        $result = array_filter($this->getLinks(), function($link) use ($rel) {
            if ( ! $link instanceof LinkInterface) {
                throw new \RuntimeException("All element of links array should implement PayPalRestApiClient\Model\LinkInterface");
            }

            if ($rel === $link->getRel()) {
                return true;
            }
        });

        if (count($result) > 0) {
            $link = array_shift($result);

            return $link;
        }

        throw new \RuntimeException("Cannot find a link corresponding to rel '".$rel."'");
    }
}


<?php

namespace PayPalRestApiClient\Service;

use Guzzle\Http\Client;
use PayPalRestApiClient\Model\AccessToken;
use PayPalRestApiClient\Model\Payment;
use PayPalRestApiClient\Model\PaymentAuthorizationInterface;
use PayPalRestApiClient\Builder\PaymentRequestBodyBuilder;
use PayPalRestApiClient\Builder\PaymentAuthorizationBuilder;
use PayPalRestApiClient\Builder\CaptureBuilder;
use Guzzle\Http\Exception\ClientErrorResponseException;
use PayPalRestApiClient\Builder\PaymentBuilder;
use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Validator\PayPalJsonSchemaValidator;

/**
 * The PaymentService class is the interface for who wants to make payment calls
 */
class PaymentService
{
    use \PayPalRestApiClient\Traits\RequestSender;

    protected $client;
    protected $baseUrl;
    protected $paymentRequestBodyBuilder;
    protected $debug;

    /**
     * Construct
     *
     * @param Guzzle\Http\Client $client not null
     * @param PayPalRestApiClient\Builder\PaymentRequestBodyBuilder $paymentRequestBodyBuilder not null
     * @param string $baseUrl not null
     * @param boolean $debug default false
     */
    public function __construct(
        Client $client,
        PaymentRequestBodyBuilder $paymentRequestBodyBuilder,
        $baseUrl,
        $debug = false
    ) {
        $this->client = $client;
        $this->baseUrl = $baseUrl;
        $this->paymentRequestBodyBuilder = $paymentRequestBodyBuilder;
        $this->debug = $debug;
    }

    /**
     * @return Guzzle\Http\Client
     */
    protected function getClient()
    {
        return $this->client;
    }

    /**
     * Execute a payment or an authorization that was previously approved by the client.
     *
     * @param PayPalRestApiClient\Model\AccessToken $accessToken
     * @param PayPalRestApiClient\Model\Payment $payment payment previously created with the "PaymentService::create" method
     * @param string $payerId not null, parameter "payerId" that is passed in the success url when using paypal payment method
     *
     * @return PayPalRestApiClient\Model\Payment
     */
    public function execute(
        AccessToken $accessToken,
        Payment $payment,
        $payerId
    ) {
        $request = $this->client->createRequest(
            'POST',
            $payment->getExecuteUrl(),
            array(
                'Accept' => 'application/json',
                'Accept-Language' => 'en_US',
                'Authorization' => $accessToken->getTokenType().' '.$accessToken->getAccessToken(),
                'Content-Type' => 'application/json'
            ),
            '{"payer_id":"'.$payerId.'"}',
            array(
                'debug' => $this->debug
            )
        );

        $response = $this->send($request, 200, "Payment error:");
        $data = json_decode($response->getBody(), true);

        if ('authorize' === $data['intent']) {

            $authorizationBuilder = new PaymentAuthorizationBuilder();
            $authorization = $authorizationBuilder->build($data);

            return $authorization;
        }

        $paymentBuilder = new PaymentBuilder();
        $payment = $paymentBuilder->build($data);

        return $payment;
    }

    /**
     * Capture payment that was previously authorized by the client or witha credit card.
     *
     * @param PayPalRestApiClient\Model\AccessToken $accessToken
     * @param PayPalRestApiClient\Model\Payment $payment payment previously authorized with the "PaymentService::authorize" method
     * @param boolean $isFinalCapture default true
     *
     * @return PayPalRestApiClient\Model\Capture
     */
    public function capture(AccessToken $accessToken, PaymentAuthorizationInterface $paymentAuthorization, $isFinalCapture = true)
    {
        $amount = $paymentAuthorization->getAmount();
        $data = array(
            'amount' => array(
                'total' => $amount->getTotal(),
                'currency' => $amount->getCurrency()
            ),
            'is_final_capture' => $isFinalCapture
        );

        $request = $this->client->createRequest(
            'POST',
            $paymentAuthorization->getCaptureUrl(),
            array(
                'Accept' => 'application/json',
                'Accept-Language' => 'en_US',
                'Authorization' => $accessToken->getTokenType().' '.$accessToken->getAccessToken(),
                'Content-Type' => 'application/json'
            ),
            json_encode($data),
            array(
                'debug' => $this->debug
            )
        );

        $response = $this->send($request, 200, "Payment error:");
        $data = json_decode($response->getBody(), true);

        $captureBuilder = new CaptureBuilder();
        $capture = $captureBuilder->build($data);

        return $capture;
    }

    /**
     * Authorize a payment
     *
     * @param PayPalRestApiClient\Model\AccessToken $accessToken
     * @param mixed $payer array or object that represents a paypal payer
     * @param array $urls array of the cancel and redirection url
     * @param array $transactions array of transaction objects
     *
     * @return PayPalRestApiClient\Model\Payment|PayPalRestApiClient\Model\PaymentAuthorization
     */
    public function authorize(
        AccessToken $accessToken,
        $payer,
        $urls,
        $transactions
    ) {
        $requestBody = $this->paymentRequestBodyBuilder->build(
            'authorize',
            $payer,
            $urls,
            $transactions
        );

        $this->assertReqeustJsonSchema($requestBody);

        $request = $this->buildRequest($accessToken, $requestBody);

        $response = $this->send($request, 201, "Payment error:");

        $data = json_decode($response->getBody(), true);

        $authorizationBuilder = new PaymentAuthorizationBuilder();
        $authorization = $authorizationBuilder->build($data);

        return $authorization;
    }

    /**
     * Create a payment
     *
     * @param PayPalRestApiClient\Model\AccessToken $accessToken
     * @param mixed $payer array or object that represents a paypal payer
     * @param array $urls array of the cancel and redirection url
     * @param array $transactions array of transaction objects
     *
     * @return PayPalRestApiClient\Model\Payment
     */
    public function create(
        AccessToken $accessToken,
        $payer,
        $urls,
        $transactions
    ) {
        $requestBody = $this->paymentRequestBodyBuilder->build(
            'sale',
            $payer,
            $urls,
            $transactions
        );

        $this->assertReqeustJsonSchema($requestBody);

        $request = $this->buildRequest($accessToken, $requestBody);

        $response = $this->send($request, 201, "Payment error:");

        $data = json_decode($response->getBody(), true);

        $paymentBuilder = new PaymentBuilder();
        $payment = $paymentBuilder->build($data);

        return $payment;
    }

    protected function assertReqeustJsonSchema($requestBody)
    {
        $validator = new PayPalJsonSchemaValidator('payment_request.json', json_decode($requestBody));
        if ( ! $validator->isValid()) {
            $errorString = '';
            foreach ($validator->getErrors() as $error) {
                $errorString .= sprintf("[%s] %s - ", $error['property'], $error['message']);
            }

            throw new BuilderException("Request body is not valid: $errorString");
        }
    }

    protected function buildRequest(AccessToken $accessToken, $requestBody)
    {
        $request = $this->client->createRequest(
            'POST',
            $this->baseUrl.'/v1/payments/payment',
            array(
                'Accept' => 'application/json',
                'Accept-Language' => 'en_US',
                'Authorization' => $accessToken->getTokenType().' '.$accessToken->getAccessToken(),
                'Content-Type' => 'application/json'
            ),
            $requestBody,
            array(
                'debug' => $this->debug
            )
        );

        return $request;
    }
}


<?php

namespace PayPalRestApiClient\Repository;

use Guzzle\Http\Client;
use PayPalRestApiClient\Builder\AccessTokenBuilder;

/**
 * The AccessTokenRepository class has the responsability of retriving an access token given a pair of cliendId and clientSecret
 *
 * @see https://developer.paypal.com/docs/api/#authentication--headers
 */
class AccessTokenRepository
{
    use \PayPalRestApiClient\Traits\RequestSender;

    protected $client;
    protected $baseUrl;
    protected $debug = false;

    /**
     * Construct
     *
     * @param Guzzle\Http\Client $client not null
     * @param string $baseUrl not null
     * @param boolean $debug default false
     */
    public function __construct(Client $client, $baseUrl, $debug = false)
    {
        $this->client = $client;
        $this->baseUrl = $baseUrl;
        $this->debug = $debug;
    }

    /**
     * @return Guzzle\Http\Client
     */
    protected function getClient()
    {
        return $this->client;
    }

    /**
     * Returns an access token object if paypal accepts the given credential
     *
     * For the time being, expires_in is not taken into consideration.
     * Calling twice the getAccessToken method with the same credential will give you two different token.
     *
     * @param string $clientId not null
     * @param string $secret not null
     *
     * @return PayPalRestApiClient\Model\AccessToken
     */
    public function getAccessToken($clientId, $secret)
    {
        $request = $this->client->createRequest(
            'POST',
            $this->baseUrl.'/v1/oauth2/token',
            array(
                'Accept' => 'application/json',
                'Accept-Language' => 'en_US',
                'Content-Type' => 'application/x-www-form-urlencoded'
            ),
            'grant_type=client_credentials',
            array(
                'auth' => array($clientId, $secret),
                'debug' => $this->debug
            )
        );

        $response = $this->send($request, $acceptedStatusCode = 200, 'Error requesting token:');
        $data = json_decode($response->getBody(), true);

        $builder = new AccessTokenBuilder();

        return $builder->build($data);
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Link;

/**
 * The LinkBuilder builds instances of PayPalRestApiClient\Model\Link
 */
class LinkBuilder extends AbstractBuilder
{
    /**
     * Build an instance of PayPalRestApiClient\Model\Link given an array
     *
     * @param array $data The array should contains the following keys:
     * href, rel, method.
     *
     * @return PayPalRestApiClient\Model\Link
     *
     * @throws PayPalRestApiClient\Exception\BuilderException
     *
     * @see https://developer.paypal.com/docs/api/#hateoas-links
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('href', 'rel', 'method'),
            $data
        );

        $link = new Link(
            $data['href'],
            $data['rel'],
            $data['method']
        );

        return $link;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;

/**
 * The AbstractBuilder class contains common methods for model objects builders
 */
abstract class AbstractBuilder
{
    abstract public function build(array $data);

    /**
     * Given an array of values $mandatoryKeys checks if the $data array contains all the keys specified in $mandatoryKeys
     *
     * @param array $mandatoryKeys
     * @param array $data
     * @param string $label
     *
     * @return void
     * @throws PayPalRestApiClient\Exception\BuilderException
     */
    protected function validateArrayKeys(array $mandatoryKeys, $data, $label = null)
    {
        $label = get_class($this);

        $diff = array_diff($mandatoryKeys, array_keys($data));
        if (count($diff) > 0) {
            throw new BuilderException('Mandatory keys missing for '.$label.': '.implode(', ', $diff));
        }
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Capture;
use PayPalRestApiClient\Model\Amount;

/**
 * The CaptureBuilder builds instances of PayPalRestApiClient\Model\Capture
 *
 * CaptureBuilder depends on two other builders: AmountBuilder and LinkBuilder.
 */
class CaptureBuilder extends AbstractBuilder
{
    protected $amountBuilder;
    protected $linkBuilder;

    public function __construct()
    {
        $this->amountBuilder = new AmountBuilder();
        $this->linkBuilder = new LinkBuilder();
    }

    public function setAmountBuilder($amountBuilder)
    {
        $this->amountBuilder = $amountBuilder;
    }

    public function setLinksBuilder($linkBuilder)
    {
        $this->linkBuilder = $linkBuilder;
    }

    /**
     * Build an instance of PayPalRestApiClient\Model\Capture given an array
     *
     * @param array $data The array should contains the following keys:
     * id, create_time, update_time, amount, is_final_capture, state, parent_payment, links.
     *
     * @return PayPalRestApiClient\Model\Capture
     *
     * @throws PayPalRestApiClient\Exception\BuilderException If not all keys are set
     *
     * @see https://developer.paypal.com/docs/api/#capture-object
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('id', 'create_time', 'update_time', 'amount', 'is_final_capture', 'state', 'parent_payment', 'links'),
            $data
        );

        $links = array();
        foreach ($data['links'] as $link) {
            $links[] = $this->linkBuilder->build($link);
        }

        $capture = new Capture(
            $data['id'],
            $data['create_time'],
            $data['update_time'],
            $this->amountBuilder->build($data['amount']),
            $data['is_final_capture'],
            $data['state'],
            $data['parent_payment'],
            $links
        );
        $capture->setPaypalData($data);

        return $capture;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Amount;

/**
 * The AmountBuilder builds instances of PayPalRestApiClient\Model\Amount
 */
class AmountBuilder extends AbstractBuilder
{
    /**
     * Build an instance of PayPalRestApiClient\Model\Amount given an array
     *
     * @param array $data
     *
     * @return PayPalRestApiClient\Model\Amount
     *
     * @throws PayPalRestApiClient\Exception\BuilderException
     *
     * @see https://developer.paypal.com/docs/api/#authentication--headers
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('currency', 'total'),
            $data
        );

        $details = array();
        if (isset($data['details'])) {
            $details = $data['details'];
        }

        $amount = new Amount(
            $data['currency'],
            $data['total'],
            $details
        );

        return $amount;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;

/**
 * The UrlsBuilder builds arrays that represents paypal redirection urls
 */
class UrlsBuilder
{
    /**
     * Build an array that represents paypal redirection urls
     *
     * @param array|\ArrayAccess $urls an array that should have the following keys: return_url and cancel_url
     *
     * @return array represents paypal redirection urls
     *
     * @throws PayPalRestApiClient\Exception\BuilderException if the input parameter is not an array or an object instance of \ArrayAccess, or the array keys "return_url" and "cancel_url" are not set
     *
     * @see https://developer.paypal.com/docs/api/#redirecturls-object
     */
    public function buildArray($urls)
    {
        if (
            ($urls instanceof \ArrayAccess || is_array($urls)) &&
            (isset($urls['return_url']) && isset($urls['cancel_url']))
        ) {

            return array(
                'return_url' => $urls['return_url'],
                'cancel_url' => $urls['cancel_url']
            );
        }

        throw new BuilderException("urls array is not valid: return_url and cancel_url are mandatory");
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Model\AccessToken;
use PayPalRestApiClient\Exception\BuilderException;

/**
 * The AccessTokenBuilder builds instances of PayPalRestApiClient\Model\AccessToken
 */
class AccessTokenBuilder extends AbstractBuilder
{
    /**
     * Build an instance of PayPalRestApiClient\Model\AccessToken given an array
     *
     * @param array $data The array should contains the following keys:
     * scope, access_token, token_type, app_id, expires_in.
     * The "access_token" key value should not be empty
     *
     * @return PayPalRestApiClient\Model\AccessToken
     *
     * @throws PayPalRestApiClient\Exception\BuilderException If not all keys are set or when "access_token" is empty
     *
     * @see https://developer.paypal.com/docs/api/#authentication--headers
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('scope', 'access_token', 'token_type', 'app_id', 'expires_in'),
            $data
        );

        if (empty($data['access_token'])) {
            throw new BuilderException('access_token is mandatory and should not be empty');
        }

        $accessToken = new AccessToken(
            $data['access_token'],
            $data['token_type'],
            $data['app_id'],
            $data['expires_in'],
            $data['scope']
        );
        $accessToken->setPaypalData($data);

        return $accessToken;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Transaction;
use PayPalRestApiClient\Model\TransactionInterface;

/**
 * The TransactionsBuilder builds arrays that represents a paypal transactions array or an array of Transaction onjects given an array
 */
class TransactionsBuilder extends AbstractBuilder
{
    protected $amountBuilder;
    protected $authorizationBuilder;

    public function __construct()
    {
        $this->amountBuilder = new AmountBuilder();
        $this->authorizationBuilder = new AuthorizationBuilder();
    }

    public function setAmountBuilder($amountBuilder)
    {
        $this->amountBuilder = $amountBuilder;
    }

    public function setAuthorizationBuilder($authorizationBuilder)
    {
        $this->authorizationBuilder = $authorizationBuilder;
    }

    /**
     * Build an array of PayPalRestApiClient\Model\Transaction instances given an array of arrays
     *
     * @param array $transactions Should contains arrays with at least the amount key set
     *
     * @return array of PayPalRestApiClient\Model\Transaction instances
     *
     * @throws PayPalRestApiClient\Exception\BuilderException If not all keys are set for each transaction
     *
     * @see https://developer.paypal.com/docs/api/#transaction-object
     */
    public function build(array $transactions)
    {
        $results = array();
        foreach ($transactions as $transaction) {
            $results[] = $this->buildTransaction($transaction);
        }

        return $results;
    }

    protected function buildTransaction(array $data)
    {
        $this->validateArrayKeys(
            array('amount'),
            $data
        );

        $amount = $this->amountBuilder->build($data['amount']);

        $description = null;
        if (isset($data['description'])) {
            $description = $data['description'];
        }

        $itemList = array();
        if (isset($data['item_list'])) {
            $itemList = $data['item_list'];
        }

        $relatedResources = array();
        if (isset($data['related_resources'])) {
            $relatedResources = $this->buildRelatedResources($data['related_resources']);
        }

        return new Transaction(
            $amount,
            $description,
            $itemList,
            $relatedResources
        );

    }

    private function buildRelatedResources($relatedResources)
    {
        $results = array();
        foreach ($relatedResources as $index => $resourceItems) {
            $results[$index] = array();
            foreach ($resourceItems as $key => $resource) {

                switch ($key) {
                    case 'authorization':

                        $results[$index][$key] = $this->authorizationBuilder->build($resource);
                        break;

                    default:
                        $results[$index][$key] = $resource;
                        break;
                }
            }
        }

        return $results;
    }

    /**
     * Build an array that represents a paypal transactions array
     *
     * @param array|\ArrayAccess $transactions an array of arrays or instances of PayPalRestApiClient\Model\TransactionInterface od \ArrayAccess
     *
     * @return array represents an array of transaction
     *
     * @throws PayPalRestApiClient\Exception\BuilderException if at least one transaction is not an array, or an object implementing \ArrayAccess interface or PayPalRestApiClient\Model\TransactionInterface
     *
     * @see https://developer.paypal.com/docs/api/#transaction-object
     */
    public function buildArray($transactions)
    {
        $this->assertTransactions($transactions);

        $transactionsData = array();
        foreach ($transactions as $transaction) {
            if ($transaction instanceof TransactionInterface) {
                $data = array(
                    'amount' => array(
                        'total' => $transaction->getAmount()->getTotal(),
                        'currency' => $transaction->getAmount()->getCurrency(),
                    ),
                    'description' => $transaction->getDescription()
                );

                if ($itemList = $transaction->getItemList())
                {
                    $data['item_list'] = $itemList;
                }

                $transactionsData[] = $data;
            }
            else {
                $data = array(
                    'amount' => array(
                        'total' => $transaction['amount']['total'],
                        'currency' => $transaction['amount']['currency'],
                    ),
                    'description' => $transaction['description']
                );

                if (isset($transaction['item_list']) && ! empty($transaction['item_list']))
                {
                    $data['item_list'] = $transaction['item_list'];
                }

                $transactionsData[] = $data;
            }
        }

        return $transactionsData;
    }

    private function assertTransactions($transactions)
    {
        if (($transactions instanceof \ArrayAccess || is_array($transactions))) {

            foreach ($transactions as $transaction) {

                if ( ! $transaction instanceof TransactionInterface &&
                     ! ($transaction instanceof \ArrayAccess || is_array($transaction))
                ) {

                    throw new BuilderException("transactions is not valid: should contains object implementing TransactionInterface, ArrayAccess or array");
                }
            }

            return;
        }

        throw new BuilderException("transactions is not valid: should be an array or implement \ArrayAccess interface");
    }
}


<?php

namespace PayPalRestApiClient\Traits;

use Guzzle\Http\Exception\ClientErrorResponseException;
use PayPalRestApiClient\Exception\CallException;

/**
 * RequestSender trait define how handle request errors when making a call usgin a client
 */
trait RequestSender
{
    /**
     * @return mixed client obejct
     */
    abstract protected function getClient();

    /**
     * Sends a request and chatches exception if something does wrong.
     *
     * @param mixed $request
     * @param integer $acceptedStatusCode which http status code represens a "all is well" response for the call
     * @param string $errorLabel label that describes the context of an error
     *
     * @return mixed $response
     * @throws PayPalRestApiClient\Exception\CallException
     */
    public function send($request, $acceptedStatusCode = 200, $errorLabel = 'Error sending Request:')
    {
        try {
            $response = $this->getClient()->send($request);
        }
        catch (ClientErrorResponseException $e) {

            $response = $e->getResponse();
            $details = json_decode($response->getBody(), true);

            $reason = implode(
                ', ',
                array_map(
                    function ($vvalue, $key) {
                        return $key . ': ' . $vvalue;
                    },
                    $details,
                    array_keys($details)
                )
            );

            throw new CallException(
                $errorLabel.
                " response status ".$response->getStatusCode()." ".$response->getReasonPhrase().", ".
                " reason: ".$reason
            );
        }

        if ($acceptedStatusCode != $response->getStatusCode()) {
            throw new CallException(
                $errorLabel.
                "response status ".$response->getStatusCode()." ".$response->getReasonPhrase()
            );
        }

        return $response;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Payer;
use PayPalRestApiClient\Model\Payment;

/**
 * The PaymentBuilder builds instances of PayPalRestApiClient\Model\Payment
 *
 * PaymentBuilder depends on 3 other builders: PayerBuilder,  TransactionsBuilder and LinkBuilder
 */
class PaymentBuilder extends AbstractBuilder
{
    protected $payerBuilder;
    protected $linkBuilder;
    protected $transactionsBuilder;

    public function __construct()
    {
        $this->payerBuilder = new PayerBuilder();
        $this->transactionsBuilder = new TransactionsBuilder();
        $this->linkBuilder = new LinkBuilder();
    }

    public function setTransactionsBuilder($transactionsBuilder)
    {
        $this->transactionsBuilder = $transactionsBuilder;
    }

    public function setPayerBuilder($payerBuilder)
    {
        $this->payerBuilder = $payerBuilder;
    }

    public function setLinksBuilder($linkBuilder)
    {
        $this->linkBuilder = $linkBuilder;
    }

    /**
     * Build an instance of PayPalRestApiClient\Model\Payment given an array
     *
     * @param array $data The array should contains the following keys:
     * id, create_time, state, intent, payer, transactions, links
     * The "id" key value should not be empty.
     *
     * @return PayPalRestApiClient\Model\Payment
     *
     * @throws PayPalRestApiClient\Exception\BuilderException If not all keys are set
     *
     * @see https://developer.paypal.com/docs/api/#payment-object
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('id', 'create_time', 'state', 'intent', 'payer', 'transactions', 'links'),
            $data
        );

        if (empty($data['id'])) {
            throw new BuilderException('id is mandatory and should not be empty');
        }

        $links = array();
        foreach ($data['links'] as $link) {
            $links[] = $this->linkBuilder->build($link);
        }

        $payment = new Payment(
            $data['id'],
            $data['create_time'],
            $data['state'],
            $data['intent'],
            $this->payerBuilder->build($data['payer']),
            $this->transactionsBuilder->build($data['transactions']),
            $links,
            isset($data['update_time']) ? $data['update_time'] : null
        );
        $payment->setPaypalData($data);

        return $payment;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\PaypalPaymentAuthorization;
use PayPalRestApiClient\Model\CreditCardPaymentAuthorization;

/**
 * The PaymentAuthorizationBuilder builds instances
 * of PayPalRestApiClient\Model\PaypalPaymentAuthorization
 * or PayPalRestApiClient\Model\CreditCardPaymentAuthorization
 * based on the payer "payment_method"
 *
 * PaymentAuthorizationBuilder depends on 3 other builders: PayerBuilder,  TransactionsBuilder and LinkBuilder
 */
class PaymentAuthorizationBuilder extends AbstractBuilder
{
    protected $payerBuilder;
    protected $linkBuilder;
    protected $transactionsBuilder;

    public function __construct()
    {
        $this->payerBuilder = new PayerBuilder();
        $this->transactionsBuilder = new TransactionsBuilder();
        $this->linkBuilder = new LinkBuilder();
    }

    public function setTransactionsBuilder($transactionsBuilder)
    {
        $this->transactionsBuilder = $transactionsBuilder;
    }

    public function setPayerBuilder($payerBuilder)
    {
        $this->payerBuilder = $payerBuilder;
    }

    public function setLinksBuilder($linkBuilder)
    {
        $this->linkBuilder = $linkBuilder;
    }

    /**
     * Build an instance of PayPalRestApiClient\Model\PaymentAuthorization given an array
     *
     * @param array $data The array should contains the following keys:
     * id, create_time, update_time, state, intent, payer, transactions, links.
     * The "id" key value should not be empty.
     *
     * @return PayPalRestApiClient\Model\PaypalPaymentAuthorization|PayPalRestApiClient\Model\CreditCardPaymentAuthorization
     *
     * @throws PayPalRestApiClient\Exception\BuilderException If not all keys are set or when "id" is empty
     *
     * @see https://developer.paypal.com/docs/integration/direct/capture-payment/#authorize-the-payment
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('id', 'create_time', 'update_time', 'state', 'intent', 'payer', 'transactions', 'links'),
            $data
        );

        if (empty($data['id'])) {
            throw new BuilderException('id is mandatory and should not be empty');
        }

        $payer = $this->payerBuilder->build($data['payer']);
        $transactions = $this->transactionsBuilder->build($data['transactions']);

        $links = array();
        foreach ($data['links'] as $link) {
            $links[] = $this->linkBuilder->build($link);
        }

        if ($data['payer']['payment_method'] === 'paypal') {
            $authorization = new PaypalPaymentAuthorization(
                $data['id'],
                $data['create_time'],
                $data['state'],
                $data['intent'],
                $payer,
                $transactions,
                $links,
                $data['update_time']
            );
            $authorization->setPaypalData($data);

            return $authorization;
        }

        $authorization = new CreditCardPaymentAuthorization(
            $data['id'],
            $data['create_time'],
            $data['state'],
            $data['intent'],
            $payer,
            $transactions,
            $links,
            $data['update_time']
        );
        $authorization->setPaypalData($data);

        return $authorization;
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Payer;
use PayPalRestApiClient\Model\PayerInterface;

/**
 * The PayerBuilder builds arrays that represents a paypal payers or instances of Payer class from an array
 */
class PayerBuilder extends AbstractBuilder
{
    /**
     * Build an instance of PayPalRestApiClient\Model\Payer given an array
     *
     * @param array $data The array should contains the payment_method following
     *
     * @return PayPalRestApiClient\Model\Payer
     *
     * @throws PayPalRestApiClient\Exception\BuilderException
     *
     * @see https://developer.paypal.com/docs/api/#payer-object
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('payment_method'),
            $data
        );

        if ( ! in_array($data['payment_method'], array('credit_card', 'paypal'))) {

            throw new BuilderException('Parameter payment_method not valid. Allowed values: "credit_card" and "paypal"');
        }

        $fundingInstruments = array();
        if (isset($data['funding_instruments'])) {
            $fundingInstruments = $data['funding_instruments'];
        }

        $info = array();
        if (isset($data['payer_info'])) {
            $info = $data['payer_info'];
        }

        return new Payer($data['payment_method'], $fundingInstruments, $info);
    }

    /**
     * Build an array that represents a paypal payer
     *
     * @param array|\ArrayAccess|PayPalRestApiClient\Model\PayerInterface $payer an array of values
     * or an istance of a class implmenting PayPalRestApiClient\Model\PayerInterface or \ArrayAccess interface
     *
     * @return array
     *
     * @throws PayPalRestApiClient\Exception\BuilderException if the given input parameter is not an array or
     * it is not an object implementing PayPalRestApiClient\Model\PayerInterface or \ArrayAccess interface
     *
     * @see https://developer.paypal.com/docs/api/#payer-object
     */
    public function buildArray($payer)
    {
        $payerData = array();
        if ($payer instanceof PayerInterface) {

            $payerData['payment_method'] = $payer->getPaymentMethod();

            if ($fundingInstruments = $payer->getFundingInstruments())
            {
                $payerData['funding_instruments'] = $fundingInstruments;
            }

            if ($info = $payer->getInfo())
            {
                $payerData['payer_info'] = $info;
            }

            return $payerData;
        }

        if (
            ($payer instanceof \ArrayAccess || is_array($payer)) &&
            isset($payer['payment_method'])
        ) {

            $payerData['payment_method'] = $payer['payment_method'];

            if (isset($payer['funding_instruments']))
            {
                $payerData['funding_instruments'] = $payer['funding_instruments'];
            }

            if (isset($payer['payer_info']))
            {
                $payerData['payer_info'] = $payer['payer_info'];
            }

            return $payerData;
        }

        throw new BuilderException('Payer is not valid');
    }
}


<?php

namespace PayPalRestApiClient\Builder;

use PayPalRestApiClient\Exception\BuilderException;
use PayPalRestApiClient\Model\Authorization;

/**
 * The AuthorizationBuilder builds instances of PayPalRestApiClient\Model\Authorization
 *
 * AuthorizationBuilder depends on two other builders: AmountBuilder and LinkBuilder.
 */
class AuthorizationBuilder extends AbstractBuilder
{
    protected $amountBuilder;
    protected $linkBuilder;

    public function __construct()
    {
        $this->amountBuilder = new AmountBuilder();
        $this->linkBuilder = new LinkBuilder();
    }

    public function setAmountBuilder($amountBuilder)
    {
        $this->amountBuilder = $amountBuilder;
    }

    public function setLinksBuilder($linkBuilder)
    {
        $this->linkBuilder = $linkBuilder;
    }

    /**
     * Build an instance of PayPalRestApiClient\Model\Authorization given an array
     *
     * @param array $data The array should contains the following keys:
     * amount, create_time, state, parent_payment, id, valid_until, links.
     *
     * @return PayPalRestApiClient\Model\Authorization
     *
     * @throws PayPalRestApiClient\Exception\BuilderException
     *
     * @see https://developer.paypal.com/docs/api/#authorization-object
     */
    public function build(array $data)
    {
        $this->validateArrayKeys(
            array('amount', 'create_time', 'update_time', 'state', 'parent_payment', 'id', 'valid_until', 'links'),
            $data
        );

        $links = array();
        foreach ($data['links'] as $link) {
            $links[] = $this->linkBuilder->build($link);
        }

        $authorization = new Authorization(
            $data['id'],
            $data['create_time'],
            $data['update_time'],
            $this->amountBuilder->build($data['amount']),
            $data['state'],
            $data['parent_payment'],
            $data['valid_until'],
            $links
        );

        return $authorization;
    }
}


<?php namespace Arcanedev\LaravelHtml\Helpers;

/**
 * Class     Obfuscater
 *
 * @package  Arcanedev\LaravelHtml\Helpers
 * @author   ARCANEDEV <arcanedev.maroc@gmail.com>
 */
class Obfuscater
{
    /* -----------------------------------------------------------------
     |  Main Methods
     | -----------------------------------------------------------------
     */

    /**
     * Obfuscate a string to prevent spam-bots from sniffing it.
     *
     * @param  string  $value
     *
     * @return string
     */
    public static function make($value)
    {
        $safe = '';

        foreach (str_split($value) as $letter) {
            if (ord($letter) > 128)
                return $letter;

            self::makeSafer($safe, $letter);
        }

        return $safe;
    }

    /* -----------------------------------------------------------------
     |  Other Methods
     | -----------------------------------------------------------------
     */

    /**
     * Make safer.
     *
     * @param  string  $letter
     * @param  string  $safe
     */
    private static function makeSafer(&$safe, $letter)
    {
        // To properly obfuscate the value, we will randomly convert each letter to
        // its entity or hexadecimal representation, keeping a bot from sniffing
        // the randomly obfuscated letters out of the string on the responses.
        switch (rand(1, 3)) {
            case 1:
                $safe .= '&#' . ord($letter).';';
                break;

            case 2:
                $safe .= '&#x' . dechex(ord($letter)).';';
                break;

            case 3:
                $safe .= $letter;
                // no break
        }
    }
}


<?php namespace Arcanedev\LaravelHtml\Traits;

use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;

/**
 * Class     FormAccessible
 *
 * @package  Arcanedev\LaravelHtml\Traits
 * @author   ARCANEDEV <arcanedev.maroc@gmail.com>
 */
trait FormAccessible
{
    /* -----------------------------------------------------------------
     |  Properties
     | -----------------------------------------------------------------
     */

    /**
     * A cached ReflectionClass instance for $this.
     *
     * @var \ReflectionClass
     */
    protected $reflection;

    /* -----------------------------------------------------------------
     |  Main Methods
     | -----------------------------------------------------------------
     */

    /**
     * Get form value from the eloquent model.
     *
     * @param  string  $key
     *
     * @return mixed
     */
    public function getFormValue($key)
    {
        $value = $this->getAttributeFromArray($key);

        if (in_array($key, $this->getDates()) && ! is_null($value))
            $value = $this->asDateTime($value);

        return $this->hasFormMutator($key)
            ? $this->mutateFormAttribute($key, $value)
            : data_get($this, $key); // No form mutator, let the model resolve this
    }

    /* -----------------------------------------------------------------
     |  Eloquent Methods
     | -----------------------------------------------------------------
     */

    /**
     * Get an attribute from the $attributes array.
     *
     * @param  string  $key
     *
     * @return mixed
     */
    abstract protected function getAttributeFromArray($key);

    /**
     * Get the attributes that should be converted to dates.
     *
     * @return array
     */
    abstract public function getDates();

    /**
     * Return a timestamp as DateTime object.
     *
     * @param  mixed  $value
     *
     * @return \Carbon\Carbon
     */
    abstract protected function asDateTime($value);

    /* -----------------------------------------------------------------
     |  Other Methods
     | -----------------------------------------------------------------
     */

    /**
     * Check if has a form mutator.
     *
     * @param  string  $key
     *
     * @return bool
     */
    protected function hasFormMutator($key)
    {
        $methods  = $this->getReflection()->getMethods(ReflectionMethod::IS_PUBLIC);

        return collect($methods)->filter(function (ReflectionMethod $method) use ($key) {
            return $method->name === $this->getMutateFromMethodName($key);
        })->isNotEmpty();
    }

    /**
     * Mutate the form attribute.
     *
     * @param  string  $key
     * @param  mixed   $value
     *
     * @return mixed
     */
    private function mutateFormAttribute($key, $value)
    {
        return $this->{$this->getMutateFromMethodName($key)}($value);
    }

    /**
     * Get the mutate form method name.
     *
     * @param  string  $key
     *
     * @return string
     */
    private function getMutateFromMethodName($key)
    {
        return 'form'.Str::studly($key).'Attribute';
    }

    /**
     * Get a ReflectionClass Instance.
     *
     * @return \ReflectionClass
     *
     * @throws \ReflectionException
     */
    protected function getReflection()
    {
        if (is_null($this->reflection)) {
            $this->reflection = new ReflectionClass($this);
        }

        return $this->reflection;
    }
}


<?php namespace Arcanedev\LaravelHtml;

use Arcanedev\Html\Elements;
use Arcanedev\Html\Entities\Attributes;
use Arcanedev\LaravelHtml\Bases\Builder;
use Arcanedev\LaravelHtml\Contracts\HtmlBuilder as HtmlBuilderContract;
use Illuminate\Contracts\Routing\UrlGenerator;

/**
 * Class     HtmlBuilder
 *
 * @package  Arcanedev\LaravelHtml
 * @author   ARCANEDEV <arcanedev.maroc@gmail.com>
 */
class HtmlBuilder extends Builder implements HtmlBuilderContract
{
    /* -----------------------------------------------------------------
     |  Properties
     | -----------------------------------------------------------------
     */

    /**
     * The URL generator instance.
     *
     * @var \Illuminate\Contracts\Routing\UrlGenerator
     */
    protected $url;

    /* -----------------------------------------------------------------
     |  Constructor
     | -----------------------------------------------------------------
     */

    /**
     * Create a new HTML builder instance.
     *
     * @param  \Illuminate\Contracts\Routing\UrlGenerator  $url
     */
    public function __construct(UrlGenerator $url = null)
    {
        $this->url = $url;
    }

    /* -----------------------------------------------------------------
     |  Main Methods
     | -----------------------------------------------------------------
     */

    /**
     * Convert an HTML string to entities.
     *
     * @param  string  $value
     * @param  bool    $doubleEncode
     *
     * @return string
     */
    public function entities($value, $doubleEncode = false)
    {
        return e($value, $doubleEncode);
    }

    /**
     * Convert all applicable characters to HTML entities.
     *
     * @param  string  $value
     *
     * @return string
     */
    public function escape($value)
    {
        return e($value, false);
    }

    /**
     * Convert entities to HTML characters.
     *
     * @param  string  $value
     *
     * @return string
     */
    public function decode($value)
    {
        return html_entity_decode($value, ENT_QUOTES, 'UTF-8');
    }

    /**
     * Generate a link to a JavaScript file.
     *
     * @param  string  $url
     * @param  array   $attributes
     * @param  bool    $secure
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function script($url, array $attributes = [], $secure = null)
    {
        return Elements\Element::withTag('script')
            ->attribute('src', $this->url->asset($url, $secure))
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate a link to a CSS file.
     *
     * @param  string  $url
     * @param  array   $attributes
     * @param  bool    $secure
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function style($url, array $attributes = [], $secure = null)
    {
        $attributes = array_merge($attributes, [
            'rel'  => 'stylesheet',
            'href' => $this->url->asset($url, $secure),
        ]);

        return Elements\Element::withTag('link')
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate an HTML image element.
     *
     * @param  string       $url
     * @param  string|null  $alt
     * @param  array        $attributes
     * @param  bool         $secure
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function image($url, $alt = null, $attributes = [], $secure = null)
    {
        return Elements\Img::make()
            ->src($this->url->asset($url, $secure))
            ->attributeUnless(is_null($alt), 'alt', $alt)
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate a link to a Favicon file.
     *
     * @param  string  $url
     * @param  array   $attributes
     * @param  bool    $secure
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function favicon($url, array $attributes = [], $secure = null)
    {
        $attributes = array_merge([
            'rel'  => 'shortcut icon',
            'type' => 'image/x-icon',
        ], $attributes);

        return Elements\Element::withTag('link')
            ->attribute('href', $this->url->asset($url, $secure))
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate a HTML link.
     *
     * @param  string       $url
     * @param  string|null  $title
     * @param  array        $attributes
     * @param  bool         $secure
     * @param  bool         $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function link($url, $title = null, array $attributes = [], $secure = null, $escaped = true)
    {
        $url = $this->url->to($url, [], $secure);

        if (is_null($title) || $title === false)
            $title = $url;

        return Elements\A::make()
            ->href($this->entities($url))
            ->attributes($attributes)
            ->html($escaped ? $this->entities($title) : $title)
            ->render();
    }

    /**
     * Generate a HTTPS HTML link.
     *
     * @param  string       $url
     * @param  string|null  $title
     * @param  array        $attributes
     * @param  bool         $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function secureLink($url, $title = null, array $attributes = [], $escaped = true)
    {
        return $this->link($url, $title, $attributes, true, $escaped);
    }

    /**
     * Generate a HTML link to an asset.
     *
     * @param  string  $url
     * @param  string  $title
     * @param  array   $attributes
     * @param  bool    $secure
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function linkAsset($url, $title = null, array $attributes = [], $secure = null)
    {
        $url = $this->url->asset($url, $secure);

        return $this->link($url, $title ?: $url, $attributes, $secure);
    }

    /**
     * Generate a HTTPS HTML link to an asset.
     *
     * @param  string       $url
     * @param  string|null  $title
     * @param  array        $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function linkSecureAsset($url, $title = null, array $attributes = [])
    {
        return $this->linkAsset($url, $title, $attributes, true);
    }

    /**
     * Generate a HTML link to a named route.
     *
     * @param  string       $name
     * @param  string|null  $title
     * @param  array|mixed  $parameters
     * @param  array        $attributes
     * @param  bool         $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function linkRoute($name, $title = null, $parameters = [], array $attributes = [], $escaped = true)
    {
        $url = $this->url->route($name, $parameters);

        return $this->link($url, $title, $attributes, null, $escaped);
    }

    /**
     * Generate a HTML link to a controller action.
     *
     * @param  string       $action
     * @param  string       $title
     * @param  array|mixed  $parameters
     * @param  array        $attributes
     * @param  bool         $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function linkAction($action, $title = null, $parameters = [], array $attributes = [], $escaped = true)
    {
        $url = $this->url->action($action, $parameters);

        return $this->link($url, $title, $attributes, null, $escaped);
    }

    /**
     * Generate a HTML link to an email address.
     *
     * @param  string       $email
     * @param  string|null  $title
     * @param  array        $attributes
     * @param  bool         $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function mailto($email, $title = null, array $attributes = [], $escaped = true)
    {
        $email = $this->email($email);
        $title = $title ?: $email;

        return Elements\A::make()
            ->href($this->obfuscate('mailto:').$email)
            ->attributes($attributes)
            ->html(($escaped ? $this->entities($title) : $title))
            ->render();
    }

    /**
     * Obfuscate an e-mail address to prevent spam-bots from sniffing it.
     *
     * @param  string  $email
     *
     * @return string
     */
    public function email($email)
    {
        return str_replace('@', '@', $this->obfuscate($email));
    }

    /**
     * Generate an ordered list of items.
     *
     * @param  array  $items
     * @param  array  $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function ol(array $items, array $attributes = [])
    {
        return Elements\Ol::make()
            ->items($items)
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate an un-ordered list of items.
     *
     * @param  array  $items
     * @param  array  $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function ul(array $items, array $attributes = [])
    {
        return Elements\Ul::make()
            ->items($items)
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate a description list of items.
     *
     * @param  array  $items
     * @param  array  $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function dl(array $items, array $attributes = [])
    {
        return Elements\Dl::make()
            ->items($items)
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generates non-breaking space entities based on a supplied multiplier.
     *
     * @param  int  $multiplier
     *
     * @return string
     */
    public function nbsp($multiplier = 1)
    {
        return str_repeat(' ', $multiplier);
    }

    /**
     * Build an HTML attribute string from an array.
     *
     * @param  array  $attributes
     *
     * @return string
     */
    public function attributes(array $attributes)
    {
        return Attributes::make($attributes)->render();
    }

    /**
     * Obfuscate a string to prevent spam-bots from sniffing it.
     *
     * @param  string  $value
     *
     * @return string
     */
    public function obfuscate($value)
    {
        return Helpers\Obfuscater::make($value);
    }

    /**
     * Generate a meta tag.
     *
     * @param  string  $name
     * @param  string  $content
     * @param  array   $attributes
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function meta($name, $content, array $attributes = [])
    {
        return Elements\Meta::make()
            ->attributeUnless(is_null($name), 'name', $name)
            ->attributeUnless(is_null($content), 'content', $content)
            ->attributes($attributes)
            ->render();
    }

    /**
     * Generate a HTML link to an phone number (call).
     *
     * @param  string  $phone
     * @param  string  $title
     * @param  array   $attributes
     * @param  bool    $escaped
     *
     * @return \Illuminate\Support\HtmlString
     */
    public function tel($phone, $title = null, $attributes = [], $escaped = true)
    {
        $title = $title ?: $phone;

        return Elements\A::make()
            ->href("tel:{$phone}")
            ->attributes($attributes)
            ->html($escaped ? $this->entities($title) : $title)
            ->render();
    }
}