<?php
/**
 * @author: helei
 * @createTime: 2016-08-04 09:42
 * @description:
 */

namespace Payment\Common\Weixin\Data;

use Payment\Common\PayException;
use Payment\Utils\ArrayUtil;

/**
 * Class TransferData
 *
 * 微信当前也仅支持单笔付款，不支持批量
 *
 * @property string $trans_no  商户转账唯一订单号
 * @property string $openid 商户appid下，某用户的openid
 * @property string $check_name
 *  - NO_CHECK：不校验真实姓名
 *  - FORCE_CHECK：强校验真实姓名（未实名认证的用户会校验失败，无法转账）
 *  - OPTION_CHECK：针对已实名认证的用户才校验真实姓名（未实名认证用户不校验，可以转账成功）
 *
 * @property string $payer_real_name  收款用户真实姓名。
 * @property string $amount  转账金额，单位：元。 只支持2位小数，小数点前最大支持13位，金额必须大于0。
 * @property string $desc 企业付款操作说明信息。必填。
 * @property string $client_ip 调用接口的机器Ip地址
 *
 * @package Payment\Common\Weixin\Data
 * anthor helei
 */
class TransferData extends WxBaseData
{
    protected function buildData()
    {
        $this->retData = [
            'mch_appid' => $this->appId,
            'mchid'    => $this->mchId,
            'device_info' => $this->terminal_id,
            'nonce_str' => $this->nonceStr,
            'partner_trade_no'    => $this->trans_no,
            'openid'    => $this->openid,

            'check_name'    => $this->check_name,
            're_user_name'  => $this->payer_real_name,
            'amount'    => $this->amount,// 此处需要处理单位为分
            'desc'  => $this->desc,

            // $_SERVER["REMOTE_ADDR"]  获取客户端接口。此处获取php所在机器的ip  如果无法获取，则使用该ip
            'spbill_create_ip'  => $this->client_ip,
        ];

        $this->retData = ArrayUtil::paraFilter($this->retData);
    }

    /**
     * 检查相关参数是否设置
     * @author helei
     */
    protected function checkDataParam()
    {
        $openId = $this->openid;
        $transNo = $this->trans_no;
        $checkName = $this->check_name;
        $realName = $this->payer_real_name;
        $amount = $this->amount;
        $clientIp = $this->client_ip;

        if (empty($openId)) {
            throw new PayException('商户appid下，某用户的openid 必须传入');
        }

        if (empty($transNo)) {
            throw new PayException('商户订单号，需保持唯一性');
        }

        if ($checkName !== 'NO_CHECK' && empty($realName)) {
            throw new PayException('请传入收款人真实姓名');
        }

        // 微信使用的单位位分.此处进行转化
        $this->amount = bcmul($amount, 100, 0);
        if (empty($amount) || $amount < 0) {
            throw new PayException('转账金额错误');
        }

        if (empty($clientIp)) {
            $this->client_ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
        }
    }
}


<?php
namespace Payment\Client;

use Payment\Common\PayException;
use Payment\Config;
use Payment\Notify\PayNotifyInterface;
use Payment\NotifyContext;

/**
 * @author: helei
 * @createTime: 2017-09-02 18:20
 * @description: 异步通知的客户端类
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 *
 * Class Notify
 * @package Payment\Client
 */
class Notify
{
    private static $supportChannel = [
        Config::ALI_CHARGE,// 支付宝

        Config::WX_CHARGE,// 微信

        Config::CMB_CHARGE,// 招行一网通
        'applepay_upacp',// Apple Pay
    ];

    /**
     * 异步通知类
     * @var NotifyContext
     */
    protected static $instance;

    protected static function getInstance($type, $config)
    {
        /* 设置内部字符编码为 UTF-8 */
        mb_internal_encoding("UTF-8");

        if (is_null(self::$instance)) {
            static::$instance = new NotifyContext();
        }

        try {
            static::$instance->initNotify($type, $config);
        } catch (PayException $e) {
            throw $e;
        }

        return static::$instance;
    }

    /**
     * 执行异步工作
     * @param string $type
     * @param array $config
     * @param PayNotifyInterface $callback
     * @return array
     * @throws PayException
     */
    public static function run($type, $config, $callback)
    {
        if (! in_array($type, self::$supportChannel)) {
            throw new PayException('sdk当前不支持该异步方式，当前仅支持：' . implode(',', self::$supportChannel));
        }

        try {
            $instance = self::getInstance($type, $config);

            $ret = $instance->notify($callback);
        } catch (PayException $e) {
            throw $e;
        }

        return $ret;
    }

    /**
     * 返回异步通知的结果
     * @param $type
     * @param $config
     * @return array|false
     * @throws PayException
     */
    public static function getNotifyData($type, $config)
    {
        try {
            $instance = self::getInstance($type, $config);

            return $instance->getNotifyData();
        } catch (PayException $e) {
            throw $e;
        }
    }
}


<?php
namespace Payment\Common;

use Payment\Utils\ArrayUtil;
use Payment\Utils\StrUtil;

/**
 * @author: helei
 * @createTime: 2016-07-15 14:56
 * @description: 支付宝配置文件  所有支付的配置文件，均需要继承 ConfigInterface 这个接口
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
final class AliConfig extends ConfigInterface
{
    // 支付宝的网关
    public $getewayUrl;

    // 支付宝分配给开发者的应用ID
    public $appId;

    // 	接口名称
    public $method;// 参考 定义的常量

    // 仅支持JSON
    public $format = 'JSON';

    // 用于同步通知的地址
    public $returnUrl;

    // 采用的编码
    public $charset = 'UTF-8';

    // 发送请求的时间，格式"yyyy-MM-dd HH:mm:ss"
    public $timestamp;

    // 调用的接口版本，固定为：1.0
    public $version = '1.0';

    // 用于rsa加密的私钥文件路径
    public $rsaPrivateKey;

    // 用于rsa解密的支付宝公钥文件路径
    public $rsaAliPubKey;

    /**
     * 初始化配置文件
     * @param array $config
     * @throws PayException
     */
    protected function initConfig(array $config)
    {
        $config = ArrayUtil::paraFilter($config);// 过滤掉空值，下面不用在检查是否为空

        // 初始 支付宝网关地址
        $this->getewayUrl = 'https://openapi.alipay.com/gateway.do';
        if (isset($config['use_sandbox']) && $config['use_sandbox'] === true) {
            $this->getewayUrl = 'https://openapi.alipaydev.com/gateway.do';
        } else {
            $this->useSandbox = false;// 不是沙箱模式
        }

        // 支付宝分配给开发者的应用ID
        if (key_exists('app_id', $config) && is_numeric($config['app_id'])) {
            $this->appId = $config['app_id'];
        } else {
            throw new PayException('支付宝应用ID错误，请检查');
        }

        // 初始 支付宝异步通知地址，可为空
        if (key_exists('notify_url', $config)) {
            $this->notifyUrl = $config['notify_url'];
        }

        // 初始 支付宝 同步通知地址，可为空
        if (key_exists('return_url', $config)) {
            $this->returnUrl = $config['return_url'];
        }

        // 初始 支付宝 签名方式，默认为：RSA
        if (key_exists('sign_type', $config) && in_array($config['sign_type'], ['RSA', 'RSA2'])) {
            $this->signType = $config['sign_type'];
        } else {
            throw new PayException('目前支付宝仅支持RSA2和RSA，推荐使用RSA2');
        }

        // 新版本，需要提供独立的支付宝公钥信息。每一个应用，公钥都不相同
        if (key_exists('ali_public_key', $config) && ! empty($config['ali_public_key'])) {
            $this->rsaAliPubKey = StrUtil::getRsaKeyValue($config['ali_public_key'], 'public');
        } else {
            throw new PayException('请提供支付宝对应的rsa公钥');
        }
        if (empty($this->rsaAliPubKey)) {
            throw new PayException('支付宝公钥生成错误，如果你使用的是文件方式配置，建议修改为字符串配置');
        }

        // 初始 RSA私钥文件 需要检查该文件是否存在
        if (key_exists('rsa_private_key', $config) && ! empty($config['rsa_private_key'])) {
            $this->rsaPrivateKey = StrUtil::getRsaKeyValue($config['rsa_private_key'], 'private');
        } else {
            throw new PayException('请提供商户的rsa私钥文件');
        }
        if (empty($this->rsaPrivateKey)) {
            throw new PayException('支付宝rsa私钥生成错误，如果你使用的是文件方式配置，建议修改为字符串配置');
        }

        // 	发送请求的时间，格式"yyyy-MM-dd HH:mm:ss"  需要正确设置时区
        $this->timestamp = date('Y-m-d H:i:s', time());

        // 设置禁止使用的支付方式
        if (key_exists('limit_pay', $config) && is_array($config['limit_pay'])) {
            $this->limitPay = implode(',', $config['limit_pay']);
        }

        // 是否要求返回支付返回的原始数据
        if (key_exists('return_raw', $config)) {
            $this->returnRaw = filter_var($config['return_raw'], FILTER_VALIDATE_BOOLEAN);
        }
    }
}


<?php
namespace Payment\Common\Weixin;

use GuzzleHttp\Client;
use Payment\Common\BaseData;
use Payment\Common\BaseStrategy;
use Payment\Common\PayException;
use Payment\Common\WxConfig;
use Payment\Utils\ArrayUtil;
use Payment\Utils\DataParser;

/**
 * Class WxBaseStrategy
 * 微信策略基类
 *
 * @package Payment\Common\Weixin
 * @anthor helei
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
abstract class WxBaseStrategy implements BaseStrategy
{
    /**
     * 需要像微信请求的url。默认是统一下单url
     * @var string $reqUrl
     */
    protected $reqUrl = 'https://api.mch.weixin.qq.com/{debug}/pay/unifiedorder';

    /**
     * 微信的配置文件
     * @var WxConfig $config
     */
    protected $config;

    /**
     * 支付数据
     * @var BaseData $reqData
     */
    protected $reqData;

    /**
     * WxBaseStrategy constructor.
     * @param array $config
     * @throws PayException
     */
    public function __construct(array $config)
    {
        try {
            $this->config = new WxConfig($config);
        } catch (PayException $e) {
            throw $e;
        }
    }

    /**
     * 发送完了请求
     * @param string $xml
     * @return mixed
     * @throws PayException
     * @author helei
     */
    protected function sendReq($xml)
    {
        $url = $this->reqUrl;
        if (is_null($url)) {
            throw new PayException('目前不支持该接口。请联系开发者添加');
        }

        if ($this->config->useSandbox) {
            $url = str_ireplace('{debug}', WxConfig::SANDBOX_PRE, $url);
        } else {
            $url = str_ireplace('{debug}/', '', $url);
        }

        $client = new Client([
            'timeout' => '10.0'
        ]);
        // @note: 微信部分接口并不需要证书支持。这里为了统一，全部携带证书进行请求
        $options = [
            'body' => $xml,
            'cert' => $this->config->appCertPem,
            'ssl_key' => $this->config->appKeyPem,
            'verify' => $this->config->cacertPath,
            'http_errors' => false
        ];
        $response = $client->request('POST', $url, $options);
        if ($response->getStatusCode() != '200') {
            throw new PayException('网络发生错误，请稍后再试curl返回码：' . $response->getReasonPhrase());
        }

        $body = $response->getBody()->getContents();
        // 格式化为数组
        $retData = DataParser::toArray($body);
        if (strtoupper($retData['return_code']) != 'SUCCESS') {
            throw new PayException('微信返回错误提示：' . $retData['return_msg']);
        }
        if (strtoupper($retData['result_code']) != 'SUCCESS') {
            $msg = $retData['err_code_des'] ? $retData['err_code_des'] : $retData['err_msg'];
            throw new PayException('微信返回错误提示：' . $msg);
        }

        return $retData;
    }

    /**
     * @param array $data
     * @author helei
     * @throws PayException
     * @return array|string
     */
    public function handle(array $data)
    {
        $buildClass = $this->getBuildDataClass();

        try {
            $this->reqData = new $buildClass($this->config, $data);
        } catch (PayException $e) {
            throw $e;
        }

        $this->reqData->setSign();

        $xml = DataParser::toXml($this->reqData->getData());
        try {
            $ret = $this->sendReq($xml);
        } catch (PayException $e) {
            throw $e;
        }

        // 检查返回的数据是否被篡改
        $flag = $this->verifySign($ret);
        if (!$flag) {
            throw new PayException('微信返回数据被篡改。请检查网络是否安全！');
        }

        return $this->retData($ret);
    }

    /**
     * 处理微信的返回值并返回给客户端
     * @param array $ret
     * @return mixed
     * @author helei
     */
    protected function retData(array $ret)
    {
        return $ret;
    }

    /**
     * 检查微信返回的数据是否被篡改过
     * @param array $retData
     * @return boolean
     * @author helei
     */
    protected function verifySign(array $retData)
    {
        $retSign = $retData['sign'];
        $values = ArrayUtil::removeKeys($retData, ['sign', 'sign_type']);

        $values = ArrayUtil::paraFilter($values);

        $values = ArrayUtil::arraySort($values);

        $signStr = ArrayUtil::createLinkstring($values);

        $signStr .= '&key=' . $this->config->md5Key;
        switch ($this->config->signType) {
            case 'MD5':
                $sign = md5($signStr);
                break;
            case 'HMAC-SHA256':
                $sign = hash_hmac('sha256', $signStr, $this->config->md5Key);
                break;
            default:
                $sign = '';
        }

        return strtoupper($sign) === $retSign;
    }
}


<?php
namespace Payment\Common\Ali;

use GuzzleHttp\Client;
use InvalidArgumentException;
use Payment\Common\AliConfig;
use Payment\Common\BaseData;
use Payment\Common\BaseStrategy;
use Payment\Common\PayException;
use Payment\Config;
use Payment\Utils\ArrayUtil;
use Payment\Utils\Rsa2Encrypt;
use Payment\Utils\RsaEncrypt;
use Payment\Utils\StrUtil;

/**
 * @author: helei
 * @createTime: 2016-07-15 17:10
 * @description: 支付宝支付接口的基类。
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
abstract class AliBaseStrategy implements BaseStrategy
{
    // 支付接口名称
    protected $method;

    /**
     * 支付宝的配置文件
     * @var AliConfig $config
     */
    protected $config;

    /**
     * 用于支付数据
     * @var BaseData $reqData
     */
    protected $reqData;

    /**
     * AliBaseStrategy constructor.
     * @param array $config
     * @throws PayException
     */
    public function __construct(array $config)
    {
        try {
            $this->config = new AliConfig($config);
        } catch (PayException $e) {
            throw $e;
        }
    }

    /**
     * 实际执行操作，策略总控
     * @param array $data
     * @return array|string
     * @throws PayException
     */
    public function handle(array $data)
    {
        $buildClass = $this->getBuildDataClass();

        try {
            $this->reqData = new $buildClass($this->config, $data);
        } catch (PayException $e) {
            throw $e;
        }

        $this->reqData->setSign();

        $data = $this->reqData->getData();

        return $this->retData($data);
    }

    /**
     * 处理支付宝的返回值并返回给客户端
     * @param array $data
     * @return string|array
     * @author helei
     */
    protected function retData(array $data)
    {
        $sign = $data['sign'];
        $data = ArrayUtil::removeKeys($data, ['sign']);

        $data = ArrayUtil::arraySort($data);

        // 支付宝新版本  需要转码
        foreach ($data as &$value) {
            $value = StrUtil::characet($value, $this->config->charset);
        }

        $data['sign'] = $sign;// sign  需要放在末尾

        return $data;
    }

    /**
     * 支付宝业务发送网络请求，并验证签名
     * @param array $data
     * @param string $method 网络请求的方法， get post 等
     * @return mixed
     * @throws PayException
     */
    protected function sendReq(array $data, $method = 'GET')
    {
        $client = new Client([
            'base_uri' => $this->config->getewayUrl,
            'timeout' => '10.0'
        ]);
        $method = strtoupper($method);
        $options = [];
        if ($method === 'GET') {
            $options = [
                'query' => $data,
                'http_errors' => false
            ];
        } elseif ($method === 'POST') {
            $options = [
                'form_params' => $data,
                'http_errors' => false
            ];
        }
        // 发起网络请求
        $response = $client->request($method, '', $options);

        if ($response->getStatusCode() != '200') {
            throw new PayException('网络发生错误，请稍后再试curl返回码：' . $response->getReasonPhrase());
        }

        $body = $response->getBody()->getContents();
        try {
            $body = \GuzzleHttp\json_decode($body, true);
        } catch (InvalidArgumentException $e) {
            throw new PayException('返回数据 json 解析失败');
        }

        $responseKey = str_ireplace('.', '_', $this->config->method) . '_response';
        if (! isset($body[$responseKey])) {
            throw new PayException('支付宝系统故障或非法请求');
        }

        // 验证签名，检查支付宝返回的数据
        $flag = $this->verifySign($body[$responseKey], $body['sign']);
        if (! $flag) {
            throw new PayException('支付宝返回数据被篡改。请检查网络是否安全！');
        }

        // 这里可能带来不兼容问题。原先会检查code ，不正确时会抛出异常，而不是直接返回
        return $body[$responseKey];
    }

    /**
     * 检查支付宝数据 签名是否被篡改
     * @param array $data
     * @param string $sign  支付宝返回的签名结果
     * @return bool
     * @author helei
     */
    protected function verifySign(array $data, $sign)
    {
        $preStr = \GuzzleHttp\json_encode($data, JSON_UNESCAPED_UNICODE);// 主要是为了解决中文问题

        if ($this->config->signType === 'RSA') {// 使用RSA
            $rsa = new RsaEncrypt($this->config->rsaAliPubKey);

            return $rsa->rsaVerify($preStr, $sign);
        } elseif ($this->config->signType === 'RSA2') {// 使用rsa2方式
            $rsa = new Rsa2Encrypt($this->config->rsaAliPubKey);

            return $rsa->rsaVerify($preStr, $sign);
        } else {
            return false;
        }
    }

    /**
     * 返回统一的交易状态  做一些转化，方便处理
     *
     * 交易状态：
     *  - WAIT_BUYER_PAY（交易创建，等待买家付款）
     *  - TRADE_CLOSED（未付款交易超时关闭，或支付完成后全额退款）
     *  - TRADE_SUCCESS（交易支付成功）
     *  - TRADE_FINISHED（交易结束，不可退款）
     *
     * @param $status
     * @return string
     * @author helei
     */
    protected function getTradeStatus($status)
    {
        switch ($status) {
            case 'TRADE_SUCCESS':// 交易支付成功
            case 'TRADE_FINISHED':// 交易结束，不可退款
                return Config::TRADE_STATUS_SUCC;

            case 'WAIT_BUYER_PAY':// 交易创建，等待买家付款
            case 'TRADE_CLOSED':// 未付款交易超时关闭，或支付完成后全额退款
            default:
                return Config::TRADE_STATUS_FAILD;
        }
    }
}


<?php
namespace Payment\Query\Ali;

use Payment\Common\Ali\AliBaseStrategy;
use Payment\Common\Ali\Data\Query\ChargeQueryData;
use Payment\Common\PayException;
use Payment\Config;
use Payment\Utils\ArrayUtil;

/**
 * 统一收单线下交易查询
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 *
 * Class AliChargeQuery
 * @package Payment\Query\Ali
 */
class AliChargeQuery extends AliBaseStrategy
{
    protected $method = 'alipay.trade.query';

    /**
     * 返回数据构建类
     * @return string
     */
    public function getBuildDataClass()
    {
        $this->config->method = $this->method;
        return ChargeQueryData::class;
    }

    /**
     * 请求后得到的返回数据
     * @param array $data
     * @return array|mixed
     * @throws PayException
     */
    protected function retData(array $data)
    {
        $data = parent::retData($data); // TODO: Change the autogenerated stub

        try {
            $ret = $this->sendReq($data);
        } catch (PayException $e) {
            throw $e;
        }

        if ($this->config->returnRaw) {
            $ret['channel'] = Config::ALI_CHARGE;
            return $ret;
        }

        return $this->createBackData($ret);
    }

    /**
     * 处理支付宝返回的数据，统一处理后返回
     * @param array $data  支付宝返回的数据
     * @return array
     * @author helei
     */
    protected function createBackData(array $data)
    {
        // 新版本
        if ($data['code'] !== '10000') {
            return [
                'is_success'    => 'F',
                'error' => $data['sub_msg'],
                'channel'   => Config::ALI_CHARGE,
            ];
        }

        // 正确情况
        $retData = [
            'is_success'    => 'T',
            'response'  => [
                'channel'   => Config::ALI_CHARGE,
                'transaction_id'   => $data['trade_no'],// 支付宝交易号
                'order_no'   => $data['out_trade_no'],// 商家订单号
                'logon_id'   => $data['buyer_logon_id'],// 买家支付宝账号
                'trade_state'   => $this->getTradeStatus($data['trade_status']),
                'amount'   => $data['total_amount'],
                'receipt_amount' => $data['receipt_amount'],// 实收金额，单位为元，两位小数。

                'pay_amount'    => ArrayUtil::get($data, 'buyer_pay_amount'),// 买家实付金额，单位为元
                'point_amount' => ArrayUtil::get($data, 'point_amount'),// 使用集分宝支付的金额
                'invoice_amount' => ArrayUtil::get($data, 'invoice_amount'),// 交易中用户支付的可开具发票的金额，单位为元，两位小数
                'time_end'   => ArrayUtil::get($data, 'send_pay_date'),// 	本次交易打款给卖家的时间
                'store_id' => ArrayUtil::get($data, 'store_id'),
                'terminal_id' => ArrayUtil::get($data, 'terminal_id'),
                'store_name' => ArrayUtil::get($data, 'store_name'),
                'buyer_id'   => ArrayUtil::get($data, 'buyer_user_id'),
                'fund_bill_list' => ArrayUtil::get($data, 'fund_bill_list', []),// 支付成功的各个渠道金额信息
            ],
        ];

        return $retData;
    }
}


<?php
namespace Payment;

use Payment\Notify\AliNotify;
use Payment\Notify\CmbNotify;
use Payment\Notify\NotifyStrategy;
use Payment\Notify\PayNotifyInterface;
use Payment\Notify\WxNotify;
use Payment\Common\PayException;

/**
 * 异步通知的上下文
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 *
 * Class NotifyContext
 * @package Payment
 */
class NotifyContext
{
    /**
     * 支付的渠道
     * @var NotifyStrategy
     */
    protected $notify;

    /**
     * 设置对应的通知渠道
     * @param string $channel 通知渠道
     *  - @see Config
     *
     * @param array $config 配置文件
     * @throws PayException
     * @author helei
     */
    public function initNotify($channel, array $config)
    {
        try {
            switch ($channel) {
                case Config::ALI_CHARGE:
                    $this->notify = new AliNotify($config);
                    break;
                case Config::WX_CHARGE:
                    $this->notify = new WxNotify($config);
                    break;
                case Config::CMB_CHARGE:
                    $this->notify = new CmbNotify($config);
                    break;
                default:
                    throw new PayException('当前仅支持：ALI_CHARGE WX_CHARGE CMB_CHARGE 常量');
            }
        } catch (PayException $e) {
            throw $e;
        }
    }

    /**
     * 返回异步通知的数据
     * @return array|false
     */
    public function getNotifyData()
    {
        return $this->notify->getNotifyData();
    }

    /**
     * 通过环境类调用支付异步通知
     *
     * @param PayNotifyInterface $notify
     * @return array
     * @throws PayException
     * @author helei
     */
    public function notify(PayNotifyInterface $notify)
    {
        if (! $this->notify instanceof NotifyStrategy) {
            throw new PayException('请检查初始化是否正确');
        }

        return $this->notify->handle($notify);
    }
}


<?php
namespace Payment\Common\Ali\Data;

use Payment\Common\BaseData;
use Payment\Utils\ArrayUtil;
use Payment\Utils\Rsa2Encrypt;
use Payment\Utils\RsaEncrypt;

/**
 * Class BaseData
 *
 * @property string $getewayUrl  支付宝网关
 * @property string $appId   支付宝分配给开发者的应用ID
 * @property string $method  接口名称
 * @property string $format  	仅支持JSON
 * @property string $returnUrl  	HTTP/HTTPS开头字符串
 * @property string $charset  请求使用的编码格式，如utf-8,gbk,gb2312等 当前仅支持  utf-8
 * @property string $timestamp  发送请求的时间，格式"yyyy-MM-dd HH:mm:ss"
 * @property string $version   调用的接口版本，固定为：1.0
 * @property string $notifyUrl  支付宝服务器主动通知商户服务器里指定的页面http/https路径
 *
 * @property string $rsaPrivateKey  rsa私钥路径
 * @property string $rsaAliPubKey  rsa支付宝公钥路径
 *
 * @property string $partner  合作id
 *
 * @package Payment\Charge\Ali\Data
 * anthor helei
 */
abstract class AliBaseData extends BaseData
{
    public function getData()
    {
        $data = parent::getData();

        // 新版需要对数据进行排序
        $data = ArrayUtil::arraySort($data);
        return $data;
    }

    /**
     * 签名算法实现
     * @param string $signStr
     * @return string
     * @author helei
     */
    protected function makeSign($signStr)
    {
        switch ($this->signType) {
            case 'RSA':
                $rsa = new RsaEncrypt($this->rsaPrivateKey);

                $sign = $rsa->encrypt($signStr);
                break;
            case 'RSA2':
                $rsa = new Rsa2Encrypt($this->rsaPrivateKey);

                $sign = $rsa->encrypt($signStr);
                break;
            default:
                $sign = '';
        }

        return $sign;
    }

    /**
     * 构建 支付 加密数据
     * @author helei
     */
    protected function buildData()
    {
        $bizContent = $this->getBizContent();
        $bizContent = ArrayUtil::paraFilter($bizContent);// 过滤掉空值，下面不用在检查是否为空

        $signData = [
            // 公共参数
            'app_id'        => $this->appId,
            'method'        => $this->method,
            'format'        => $this->format,
            'charset'       => $this->charset,
            'sign_type'     => $this->signType,
            'timestamp'     => $this->timestamp,
            'version'       => $this->version,
            'notify_url'    => $this->notifyUrl,

            // 业务参数
            'biz_content'   => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
        ];

        // 电脑支付  wap支付添加额外参数
        if (in_array($this->method, ['alipay.trade.page.pay', 'alipay.trade.wap.pay'])) {
            $signData['return_url'] = $this->returnUrl;
        }

        // 移除数组中的空值
        $this->retData = ArrayUtil::paraFilter($signData);
    }

    /**
     * 支付宝构建请求查询的数据
     * @return mixed
     */
    abstract protected function getBizContent();
}


<?php
namespace Payment\Common\Weixin\Data;

use Payment\Common\BaseData;

/**
 * Class BaseData
 *
 * @author: helei
 * @createTime: 2016-07-28 18:05
 * @description: 微信支付相关接口的数据基类
 *
 * @property string $appId   微信分配的公众账号ID
 * @property string $mchId  微信支付分配的商户号
 * @property string $nonceStr  随机字符串，不长于32位
 * @property string $notifyUrl  异步通知的url
 * @property string $feeType  符合ISO 4217标准的三位字母代码 默认位人民币
 * @property string $timeStart  交易开始时间 格式为yyyyMMddHHmmss
 * @property string $md5Key  用于加密的md5Key
 * @property string $appCertPem 从apiclient_cert.p12中导出证书部分的文件，为pem格式，
 * @property string $appKeyPem 从apiclient_key.pem中导出密钥部分的文件，为pem格式
 * @property string $tradeType   支付类型
 * @property string $terminal_id 终端设备号(门店号或收银设备ID)，默认请传"WEB"
 *
 * @note 服务商信息
 * @property string $sub_appid 微信分配的子商户公众账号ID
 * @property string $sub_mch_id 	微信支付分配的子商户号
 *
 * @package Payment\Common\Weixin\Dataa
 *
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
abstract class WxBaseData extends BaseData
{

    /**
     * 签名算法实现  便于后期扩展微信不同的加密方式
     * @param string $signStr
     * @return string
     */
    protected function makeSign($signStr)
    {
        switch ($this->signType) {
            case 'MD5':
                $signStr .= '&key=' . $this->md5Key;
                $sign = md5($signStr);
                break;
            case 'HMAC-SHA256':
                $sign = base64_encode(hash_hmac('sha256', $signStr, $this->md5Key));
                break;
            default:
                $sign = '';
        }
        return strtoupper($sign);
    }
}


<?php
namespace Payment\Common\Weixin\Data\Charge;

use Payment\Common\PayException;
use Payment\Common\Weixin\Data\WxBaseData;
use Payment\Config;

/**
 * Class ChargeBaseData
 *
 * @inheritdoc
 *
 * @property string $order_no
 * @property string $amount
 * @property string $client_ip  用户端实际ip
 * @property string $subject  商品详情  商品详细列表，使用Json格式，传输签名前请务必使用CDATA标签将JSON文本串保护起来。  暂时未使用
 * @property string $body
 * @property string $return_param  附加数据，在查询API和支付通知中原样返回
 * @property integer $timeout_express  订单失效时间   格式为yyyyMMddHHmmss
 * @property array $scene_info 该字段用于上报支付的场景信息
 *
 * @package Payment\Common\Weixin\Data\Charge
 *
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
abstract class ChargeBaseData extends WxBaseData
{

    /**
     * 检查传入的支付信息是否正确
     */
    protected function checkDataParam()
    {
        $orderNo = $this->order_no;
        $amount = $this->amount;
        $subject = $this->subject;
        $body = $this->body;
        $deviceInfo = $this->terminal_id;

        // 检查订单号是否合法
        if (empty($orderNo) || mb_strlen($orderNo) > 64) {
            throw new PayException('订单号不能为空，并且长度不能超过64位');
        }

        // 检查金额不能低于0.01
        if (bccomp($amount, Config::PAY_MIN_FEE, 2) === -1) {
            throw new PayException('支付金额不能低于 ' . Config::PAY_MIN_FEE . ' 元');
        }

        // 检查 商品名称 与 商品描述
        if (empty($subject) || empty($body)) {
            throw new PayException('必须提供商品名称与商品详情');
        }

        // 初始 微信订单过期时间，最短失效时间间隔必须大于5分钟
        if ($this->timeout_express - strtotime($this->timeStart) < 5) {
            throw new PayException('必须设置订单过期时间,且需要大于5分钟.如果不正确请检查是否正确设置时区');
        } else {
            $this->timeout_express = date('YmdHis', $this->timeout_express);
        }

        // 微信使用的单位位分.此处进行转化
        $this->amount = bcmul($amount, 100, 0);

        // 设置ip地址
        $clientIp = $this->client_ip;
        if (empty($clientIp)) {
            $this->client_ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
        }

        // 设置设备号
        if (empty($deviceInfo)) {
            $this->terminal_id = 'WEB';
        }
    }
}


<?php
namespace Payment\Query\Ali;

use Payment\Common\Ali\AliBaseStrategy;
use Payment\Common\Ali\Data\Query\RefundQueryData;
use Payment\Common\PayException;
use Payment\Config;
use Payment\Utils\ArrayUtil;

/**
 *
 * 支付宝退款订单查询
 * Class AliRefundQuery
 * @package Payment\Query
 * anthor helei
 */
class AliRefundQuery extends AliBaseStrategy
{
    protected $method = 'alipay.trade.fastpay.refund.query';

    public function getBuildDataClass()
    {
        $this->config->method = $this->method;
        return RefundQueryData::class;
    }

    protected function retData(array $data)
    {
        $reqData = parent::retData($data);

        try {
            $ret = $this->sendReq($reqData);
        } catch (PayException $e) {
            throw $e;
        }

        if ($this->config->returnRaw) {
            $ret['channel'] = Config::ALI_REFUND;
            return $ret;
        }

        return $this->createBackData($ret);
    }

    /**
     * 返回数据给客户端  未完成，目前没有数据提供
     * @param array $data
     * @return array
     * @author helei
     */
    protected function createBackData(array $data)
    {
        if ($data['code'] !== '10000') {
            return $retData = [
                'is_success'    => 'F',
                'error' => $data['sub_msg'],
                'channel'   => Config::ALI_REFUND,
            ];
        }

        // 这里有个诡异情况。查询数据全部为空。仅返回一个成功的标记
        if (empty($data['out_trade_no'])) {
            return [
                'is_success'    => 'T',
                'msg'   => strtolower($data['msg']),
                'channel'   => Config::ALI_REFUND,
            ];
        }

        $retData = [
            'is_success'    => 'T',
            'response'  => [
                'transaction_id'   => ArrayUtil::get($data, 'trade_no'),// 支付宝订单号
                'order_no'   => ArrayUtil::get($data, 'out_trade_no'),// 商户订单号
                'refund_no' => ArrayUtil::get($data, 'out_request_no'),// 本笔退款对应的退款请求号
                'reason'   => ArrayUtil::get($data, 'refund_reason'),// 退款理由
                'amount'   => ArrayUtil::get($data, 'total_amount'),// 订单总金额
                'refund_amount'   => ArrayUtil::get($data, 'refund_amount'),// 退款金额
                'channel'   => Config::ALI_REFUND,
            ]
        ];

        return $retData;
    }
}


<?php
namespace Payment\Common;

use Payment\Utils\ArrayUtil;
use Payment\Utils\StrUtil;

/**
 * Class CcbConfig
 * @package Payment\Common
 * @desc: 建设银行
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 */
class CcbConfig extends ConfigInterface
{
    // 调用的接口版本，固定为：1.0
    public $version = '1.0';

    // 采用的编码
    public $charset = 'UTF-8';

    // 用于加密的 merKey
    public $merKey;

    // 发送请求的时间，格式"yyyyMMddHHmmss"
    public $dateTime;

    // 商户分行号，4位数字
    public $branchNo;

    // 商户号，6位数字
    public $merchantNo;

    // 用于同步通知的地址
    public $returnUrl;

    // 成功签约结果通知地址:首次签约，必填. 商户接收成功签约结果通知的地址。
    public $signNoticeUrl;

    // 操作员登录密码。
    public $opPwd;

    // 招商请求的网关
    public $getewayUrl;

    // 建行的公钥
    public $rsaPubKey;

    const MAX_EXPIRE_TIME = 30;// 过期时间最大 30分钟

    const REQ_FILED_NAME = 'jsonRequestData';// 报文的参数名：jsonRequestData

    const SUCC_TAG = 'SUC0000';// SUC0000表示成功，其他表示错误，具体错误码见详细API定义。

    const TRADE_CODE = 'FBPK';// 交易码,固定为“FBPK”

    const NOTICE_PAY = 'BKPAYRTN';// 支付成功回调

    const NOTICE_SIGN = 'BKQY';// 签约成功回调

    /**
     * 初始化配置文件
     * @param array $config
     * @throws PayException
     */
    protected function initConfig(array $config)
    {
        $config = ArrayUtil::paraFilter($config);

        // 初始 mer key
        if (key_exists('mer_key', $config) && !empty($config['mer_key'])) {
            $this->merKey = $config['mer_key'];
        } else {
            throw new PayException('Mer Key 不能为空，请前往招商一网通进行设置');
        }

        // 设置操作员登陆密码
        if (key_exists('op_pwd', $config) && !empty($config['op_pwd'])) {
            $this->opPwd = $config['op_pwd'];
        } else {
            throw new PayException('请设置操作员登陆密码');
        }

        // 检查 异步通知的url
        if (key_exists('notify_url', $config) && !empty($config['notify_url'])) {
            $this->notifyUrl = trim($config['notify_url']);
        } else {
            throw new PayException('异步通知的url必须提供.');
        }

        // 检查 签约异步通知的url
        if (key_exists('sign_notify_url', $config) && !empty($config['sign_notify_url'])) {
            $this->signNoticeUrl = trim($config['sign_notify_url']);
        } else {
            throw new PayException('签约 异步通知的url必须提供.');
        }

        // 商户分行号，4位数字
        if (key_exists('branch_no', $config) && !empty($config['branch_no'])) {
            $this->branchNo = trim($config['branch_no']);
        } else {
            throw new PayException('商户分行号必须提供，4位数字.');
        }

        // 商户号，6位数字
        if (key_exists('merchant_no', $config) && !empty($config['merchant_no'])) {
            $this->merchantNo = trim($config['merchant_no']);
        } else {
            throw new PayException('商户号必须提供，6位数字.');
        }

        $this->limitPay = '';
        // 设置禁止使用的支付方式
        if (
            key_exists('limit_pay', $config) &&
            !empty($config['limit_pay']) &&
            strtoupper($config['limit_pay'][0]) === 'A'
        ) {
            $this->limitPay = 'A';
        }

        if (key_exists('return_raw', $config)) {
            $this->returnRaw = filter_var($config['return_raw'], FILTER_VALIDATE_BOOLEAN);
        }

        // 签名算法,固定为“SHA-256”
        if (key_exists('sign_type', $config) && in_array($config['sign_type'], ['SHA-256'])) {
            $this->signType = $config['sign_type'];
        } else {
            $this->signType = 'SHA-256';
        }

        if (isset($config['use_sandbox']) && $config['use_sandbox'] === true) {
            $this->useSandbox = true;
        } else {
            $this->useSandbox = false;// 不是沙箱模式
        }

        if (key_exists('cmb_pub_key', $config) && (file_exists($config['cmb_pub_key']) || ! empty($config['cmb_pub_key']))) {
            $this->rsaPubKey = StrUtil::getRsaKeyValue($config['cmb_pub_key'], 'public');
        } else {
            throw new PayException('请提供招商对应的rsa公钥，可通过Helper接口获取');
        }

        // 初始 招商一网通 同步通知地址，可为空
        if (key_exists('return_url', $config)) {
            $this->returnUrl = $config['return_url'];
        }
        // 设置交易开始时间 格式为yyyyMMddHHmmss   .再此之前一定要设置时区
        $this->dateTime = date('YmdHis', time());
    }
}


<?php
namespace Payment;

use Payment\Common\BaseStrategy;
use Payment\Common\PayException;
use Payment\Red\WxRed;

/**
 * 红包操作
 * Class RedContext
 * @package Payment
 */
class RedContext
{
    /**
     * 发送渠道
     * @var BaseStrategy
     */
    protected $red;

    /**
     * 设置对应的发送渠道
     * @param string $channel 退款渠道
     *  - @see Config
     *
     * @param array $config 配置文件
     * @throws PayException
     * @author IT
     */
    public function initRed($channel, array $config)
    {
        try {
            switch ($channel) {
                case Config::ALI_RED:
                    $this->red = new AliRed($config);
                    break;
                case Config::WX_RED:
                    $this->red = new WxRed($config);
                    break;
                default:
                    throw new PayException('当前仅支持：ALI WEIXIN两个常量');
            }
        } catch (PayException $e) {
            throw $e;
        }
    }

    /**
     * 通过环境类调用支付转款操作
     *
     * @param array $data
     *
     * @return array
     * @throws PayException
     * @author helei
     */
    public function red(array $data)
    {
        if (! $this->red instanceof BaseStrategy) {
            throw new PayException('请检查初始化是否正确');
        }

        try {
            return $this->red->handle($data);
        } catch (PayException $e) {
            throw $e;
        }
    }
}


<?php
namespace Payment\Common\Ali\Data;

use Payment\Common\PayException;
use Payment\Config;

/**
 * 转账到支付宝帐号
 * Class TransData
 *
 * @property string $trans_no  商户转账唯一订单号
 * @property string $payee_type  收款方账户类型。可取值： 1、ALIPAY_USERID：支付宝账号对应的支付宝唯一用户号。以2088开头的16位纯数字组成。 2、ALIPAY_LOGONID：支付宝登录号，支持邮箱和手机号格式。
 * @property string $payee_account 收款方账户。与payee_type配合使用。付款方和收款方不能是同一个账户。
 * @property string $amount  转账金额，单位：元。 只支持2位小数，小数点前最大支持13位，金额必须大于0。
 *
 * // 可选参数
 * @property string $payer_real_name 付款方真实姓名
 * @property string $payee_real_name 收款方真实姓名
 * @property string $payer_show_name  默认显示该账户在支付宝登记的实名。收款方可见。
 * @property string $remark 转账备注  当付款方为企业账户，且转账金额达到（大于等于）50000元，remark不能为空
 *
 * @package Payment\Common\Ali\Data
 * anthor helei
 */
class TransData extends AliBaseData
{
    /**
     * 检查参数是否合法
     * @author helei
     */
    protected function checkDataParam()
    {
        $transNo = $this->trans_no;
        $payeeType = $this->payee_type;
        $payeeAccount = $this->payee_account;
        $amount = $this->amount;
        $remark = $this->remark;

        if (empty($transNo)) {
            throw new PayException('请传入 商户转账唯一订单号');
        }

        if (empty($payeeType) || ! in_array($payeeType, ['ALIPAY_USERID', 'ALIPAY_LOGONID'])) {
            throw new PayException('请传入收款账户类型');
        }

        if (empty($payeeAccount)) {
            throw new PayException('请传入转账帐号');
        }

        if (empty($amount) || bccomp($amount, 0, 2) !== 1) {
            throw new PayException('请输入转账金额，且大于0');
        }

        if (bccomp($amount, Config::TRANS_FEE, 2) !== -1 && empty($remark)) {
            throw new PayException('转账金额大于等于' . Config::TRANS_FEE, '必须设置 remark');
        }
    }

    /**
     * 业务请求参数的集合，最大长度不限，除公共参数外所有请求参数都必须放在这个参数中传递
     *
     * @return string
     */
    protected function getBizContent()
    {
        $content = [
            'out_biz_no'    => $this->trans_no,// 商户转账唯一订单号
            'payee_type'        => strtoupper($this->payee_type),// 收款方账户类型
            'payee_account'     => $this->payee_account,// 收款方账户
            'amount'     => $this->amount,
            'payer_show_name'       => $this->payer_show_name,
            'payer_real_name'    => $this->payer_real_name,
            'payee_real_name'          => $this->payee_real_name,
            'remark'       => $this->remark,
        ];

        return $content;
    }
}


<?php
namespace Payment\Common\Ali\Data\Query;

use Payment\Common\Ali\Data\AliBaseData;
use Payment\Common\PayException;
use Payment\Utils\ArrayUtil;

/**
 * 支付查询数据构造
 * Class ChargeQueryData
 * @link      https://www.gitbook.com/book/helei112g1/payment-sdk/details
 * @link      https://helei112g.github.io/
 *
 * @property string $trade_no 支付宝的订单号，优先使用
 * @property string $out_trade_no 商户系统内部的订单号
 *
 * @package Payment\Common\Ali\Data\Query
 * anthor helei
 */
class ChargeQueryData extends AliBaseData
{
    /**
     * 构建业务数据
     * @return string
     */
    protected function getBizContent()
    {
        $content = [
            'out_trade_no'    => $this->out_trade_no,
            'trade_no'        => $this->trade_no,
        ];

        return $content;
    }

    /**
     * 检查参数
     * @author helei
     */
    protected function checkDataParam()
    {
        $tradeNo = $this->trade_no;// 支付宝交易号，查询效率高
        $outTradeNo = $this->out_trade_no;// 商户订单号，查询效率低，不建议使用

        // 二者不能同时为空
        if (empty($outTradeNo) && empty($tradeNo)) {
            throw new PayException('必须提供支付宝交易号或者商户网站唯一订单号。建议使用支付宝交易号');
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Doctrine\Annotation;

use Doctrine\Common\Annotations\DocLexer;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token as PhpToken;

/**
 * A list of Doctrine annotation tokens.
 *
 * @internal
 */
final class Tokens extends \SplFixedArray
{
    /**
     * @param PhpToken $input
     * @param string[] $ignoredTags
     *
     * @throws \InvalidArgumentException
     *
     * @return self
     */
    public static function createFromDocComment(PhpToken $input, array $ignoredTags = [])
    {
        if (!$input->isGivenKind(T_DOC_COMMENT)) {
            throw new \InvalidArgumentException('Input must be a T_DOC_COMMENT token.');
        }

        $tokens = new self();

        $content = $input->getContent();
        $ignoredTextPosition = 0;
        $currentPosition = 0;
        while (false !== $nextAtPosition = strpos($content, '@', $currentPosition)) {
            if (0 !== $nextAtPosition && !Preg::match('/\s/', $content[$nextAtPosition - 1])) {
                $currentPosition = $nextAtPosition + 1;

                continue;
            }

            $lexer = new DocLexer();
            $lexer->setInput(substr($content, $nextAtPosition));

            $scannedTokens = [];
            $index = 0;
            $nbScannedTokensToUse = 0;
            $nbScopes = 0;
            while (null !== $token = $lexer->peek()) {
                if (0 === $index && DocLexer::T_AT !== $token['type']) {
                    break;
                }

                if (1 === $index) {
                    if (DocLexer::T_IDENTIFIER !== $token['type'] || \in_array($token['value'], $ignoredTags, true)) {
                        break;
                    }

                    $nbScannedTokensToUse = 2;
                }

                if ($index >= 2 && 0 === $nbScopes && !\in_array($token['type'], [DocLexer::T_NONE, DocLexer::T_OPEN_PARENTHESIS], true)) {
                    break;
                }

                $scannedTokens[] = $token;

                if (DocLexer::T_OPEN_PARENTHESIS === $token['type']) {
                    ++$nbScopes;
                } elseif (DocLexer::T_CLOSE_PARENTHESIS === $token['type']) {
                    if (0 === --$nbScopes) {
                        $nbScannedTokensToUse = \count($scannedTokens);

                        break;
                    }
                }

                ++$index;
            }

            if (0 !== $nbScopes) {
                break;
            }

            if (0 !== $nbScannedTokensToUse) {
                $ignoredTextLength = $nextAtPosition - $ignoredTextPosition;
                if (0 !== $ignoredTextLength) {
                    $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition, $ignoredTextLength));
                }

                $lastTokenEndIndex = 0;
                foreach (\array_slice($scannedTokens, 0, $nbScannedTokensToUse) as $token) {
                    if (DocLexer::T_STRING === $token['type']) {
                        $token['value'] = '"'.str_replace('"', '""', $token['value']).'"';
                    }

                    $missingTextLength = $token['position'] - $lastTokenEndIndex;
                    if ($missingTextLength > 0) {
                        $tokens[] = new Token(DocLexer::T_NONE, substr(
                            $content,
                            $nextAtPosition + $lastTokenEndIndex,
                            $missingTextLength
                        ));
                    }

                    $tokens[] = new Token($token['type'], $token['value']);
                    $lastTokenEndIndex = $token['position'] + \strlen($token['value']);
                }

                $currentPosition = $ignoredTextPosition = $nextAtPosition + $token['position'] + \strlen($token['value']);
            } else {
                $currentPosition = $nextAtPosition + 1;
            }
        }

        if ($ignoredTextPosition < \strlen($content)) {
            $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition));
        }

        return $tokens;
    }

    /**
     * Returns the index of the closest next token that is neither a comment nor a whitespace token.
     *
     * @param int $index
     *
     * @return null|int
     */
    public function getNextMeaningfulToken($index)
    {
        return $this->getMeaningfulTokenSibling($index, 1);
    }

    /**
     * Returns the index of the closest previous token that is neither a comment nor a whitespace token.
     *
     * @param int $index
     *
     * @return null|int
     */
    public function getPreviousMeaningfulToken($index)
    {
        return $this->getMeaningfulTokenSibling($index, -1);
    }

    /**
     * Returns the index of the closest next token of the given type.
     *
     * @param string|string[] $type
     * @param int             $index
     *
     * @return null|int
     */
    public function getNextTokenOfType($type, $index)
    {
        return $this->getTokenOfTypeSibling($index, $type, 1);
    }

    /**
     * Returns the index of the closest previous token of the given type.
     *
     * @param string|string[] $type
     * @param int             $index
     *
     * @return null|int
     */
    public function getPreviousTokenOfType($type, $index)
    {
        return $this->getTokenOfTypeSibling($index, $type, -1);
    }

    /**
     * Returns the index of the last token that is part of the annotation at the given index.
     *
     * @param int $index
     *
     * @return null|int
     */
    public function getAnnotationEnd($index)
    {
        $currentIndex = null;

        if (isset($this[$index + 2])) {
            if ($this[$index + 2]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
                $currentIndex = $index + 2;
            } elseif (
                isset($this[$index + 3])
                && $this[$index + 2]->isType(DocLexer::T_NONE)
                && $this[$index + 3]->isType(DocLexer::T_OPEN_PARENTHESIS)
                && Preg::match('/^(\R\s*\*\s*)*\s*$/', $this[$index + 2]->getContent())
            ) {
                $currentIndex = $index + 3;
            }
        }

        if (null !== $currentIndex) {
            $level = 0;
            for ($max = \count($this); $currentIndex < $max; ++$currentIndex) {
                if ($this[$currentIndex]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
                    ++$level;
                } elseif ($this[$currentIndex]->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
                    --$level;
                }

                if (0 === $level) {
                    return $currentIndex;
                }
            }

            return null;
        }

        return $index + 1;
    }

    /**
     * Returns the index of the close brace that matches the open brace at the given index.
     *
     * @param int $index
     *
     * @return null|int
     */
    public function getArrayEnd($index)
    {
        $level = 1;
        for (++$index, $max = \count($this); $index < $max; ++$index) {
            if ($this[$index]->isType(DocLexer::T_OPEN_CURLY_BRACES)) {
                ++$level;
            } elseif ($this[$index]->isType($index, DocLexer::T_CLOSE_CURLY_BRACES)) {
                --$level;
            }

            if (0 === $level) {
                return $index;
            }
        }

        return null;
    }

    /**
     * Returns the code from the tokens.
     *
     * @return string
     */
    public function getCode()
    {
        $code = '';
        foreach ($this as $token) {
            $code .= $token->getContent();
        }

        return $code;
    }

    /**
     * Inserts a token at the given index.
     *
     * @param int   $index
     * @param Token $token
     */
    public function insertAt($index, Token $token)
    {
        $this->setSize($this->getSize() + 1);

        for ($i = $this->getSize() - 1; $i > $index; --$i) {
            $this[$i] = isset($this[$i - 1]) ? $this[$i - 1] : new Token();
        }

        $this[$index] = $token;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     */
    public function offsetSet($index, $token)
    {
        if (!$token instanceof Token) {
            $type = \gettype($token);
            if ('object' === $type) {
                $type = \get_class($token);
            }

            throw new \InvalidArgumentException(sprintf(
                'Token must be an instance of PhpCsFixer\\Doctrine\\Annotation\\Token, %s given.',
                $type
            ));
        }

        if (null === $index) {
            $index = \count($this);
            $this->setSize($this->getSize() + 1);
        }

        parent::offsetSet($index, $token);
    }

    /**
     * {@inheritdoc}
     *
     * @throws \OutOfBoundsException
     */
    public function offsetUnset($index)
    {
        if (!isset($this[$index])) {
            throw new \OutOfBoundsException(sprintf('Index %s is invalid or does not exist.', $index));
        }

        $max = \count($this) - 1;
        while ($index < $max) {
            $this[$index] = $this[$index + 1];
            ++$index;
        }

        parent::offsetUnset($index);

        $this->setSize($max);
    }

    /**
     * @param int $index
     * @param int $direction
     *
     * @return null|int
     */
    private function getMeaningfulTokenSibling($index, $direction)
    {
        while (true) {
            $index += $direction;

            if (!$this->offsetExists($index)) {
                break;
            }

            if (!$this[$index]->isType(DocLexer::T_NONE)) {
                return $index;
            }
        }

        return null;
    }

    /**
     * @param int             $index
     * @param string|string[] $type
     * @param int             $direction
     *
     * @return null|int
     */
    private function getTokenOfTypeSibling($index, $type, $direction)
    {
        while (true) {
            $index += $direction;

            if (!$this->offsetExists($index)) {
                break;
            }

            if ($this[$index]->isType($type)) {
                return $index;
            }
        }

        return null;
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer;

use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * This abstract fixer is responsible for ensuring that a certain number of
 * lines prefix a namespace declaration.
 *
 * @author Graham Campbell <graham@alt-three.com>
 *
 * @internal
 */
abstract class AbstractLinesBeforeNamespaceFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
    /**
     * Make sure # of line breaks prefixing namespace is within given range.
     *
     * @param Tokens $tokens
     * @param int    $index
     * @param int    $expectedMin min. # of line breaks
     * @param int    $expectedMax max. # of line breaks
     */
    protected function fixLinesBeforeNamespace(Tokens $tokens, $index, $expectedMin, $expectedMax)
    {
        // Let's determine the total numbers of new lines before the namespace
        // and the opening token
        $precedingNewlines = 0;
        $newlineInOpening = false;
        $openingToken = null;
        for ($i = 1; $i <= 2; ++$i) {
            if (isset($tokens[$index - $i])) {
                $token = $tokens[$index - $i];
                if ($token->isGivenKind(T_OPEN_TAG)) {
                    $openingToken = $token;
                    $openingTokenIndex = $index - $i;
                    $newlineInOpening = false !== strpos($token->getContent(), "\n");
                    if ($newlineInOpening) {
                        ++$precedingNewlines;
                    }

                    break;
                }
                if (false === $token->isGivenKind(T_WHITESPACE)) {
                    break;
                }
                $precedingNewlines += substr_count($token->getContent(), "\n");
            }
        }

        if ($precedingNewlines >= $expectedMin && $precedingNewlines <= $expectedMax) {
            return;
        }

        $previousIndex = $index - 1;
        $previous = $tokens[$previousIndex];

        if (0 === $expectedMax) {
            // Remove all the previous new lines
            if ($previous->isWhitespace()) {
                $tokens->clearAt($previousIndex);
            }
            // Remove new lines in opening token
            if ($newlineInOpening) {
                $tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, rtrim($openingToken->getContent()).' ']);
            }

            return;
        }

        $lineEnding = $this->whitespacesConfig->getLineEnding();
        $newlinesForWhitespaceToken = $expectedMax;
        if (null !== $openingToken) {
            // Use the configured line ending for the PHP opening tag
            $content = rtrim($openingToken->getContent());
            $newContent = $content.$lineEnding;
            $tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, $newContent]);
            --$newlinesForWhitespaceToken;
        }
        if (0 === $newlinesForWhitespaceToken) {
            // We have all the needed new lines in the opening tag
            if ($previous->isWhitespace()) {
                // Let's remove the previous token containing extra new lines
                $tokens->clearAt($previousIndex);
            }

            return;
        }
        if ($previous->isWhitespace()) {
            // Fix the previous whitespace token
            $tokens[$previousIndex] = new Token([T_WHITESPACE, str_repeat($lineEnding, $newlinesForWhitespaceToken).substr($previous->getContent(), strrpos($previous->getContent(), "\n") + 1)]);
        } else {
            // Add a new whitespace token
            $tokens->insertAt($index, new Token([T_WHITESPACE, str_repeat($lineEnding, $newlinesForWhitespaceToken)]));
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Linter;

/**
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * @internal
 */
final class CachingLinter implements LinterInterface
{
    /**
     * @var LinterInterface
     */
    private $sublinter;

    /**
     * @var array<int, LintingResultInterface>
     */
    private $cache = [];

    /**
     * @param LinterInterface $linter Linter instance
     */
    public function __construct(LinterInterface $linter)
    {
        $this->sublinter = $linter;
    }

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

    /**
     * {@inheritdoc}
     */
    public function lintFile($path)
    {
        $checksum = crc32(file_get_contents($path));

        if (!isset($this->cache[$checksum])) {
            $this->cache[$checksum] = $this->sublinter->lintFile($path);
        }

        return $this->cache[$checksum];
    }

    /**
     * {@inheritdoc}
     */
    public function lintSource($source)
    {
        $checksum = crc32($source);

        if (!isset($this->cache[$checksum])) {
            $this->cache[$checksum] = $this->sublinter->lintSource($source);
        }

        return $this->cache[$checksum];
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Whitespace;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * Fixer for rules defined in PSR2 ¶4.3, ¶4.6, ¶5.
 *
 * @author Marc Aubé
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 */
final class NoSpacesInsideParenthesisFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.',
            [
                new CodeSample("<?php\nif ( \$a ) {\n    foo( );\n}\n"),
                new CodeSample(
                    "<?php
function foo( \$bar, \$baz )
{
}\n"
                ),
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPriority()
    {
        // must run before FunctionToConstantFixer
        return 2;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isTokenKindFound('(');
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        foreach ($tokens as $index => $token) {
            if (!$token->equals('(')) {
                continue;
            }

            $prevIndex = $tokens->getPrevMeaningfulToken($index);

            // ignore parenthesis for T_ARRAY
            if (null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(T_ARRAY)) {
                continue;
            }

            $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);

            // remove space after opening `(`
            if (!$tokens[$tokens->getNextNonWhitespace($index)]->isComment()) {
                $this->removeSpaceAroundToken($tokens, $index + 1);
            }

            // remove space before closing `)` if it is not `list($a, $b, )` case
            if (!$tokens[$tokens->getPrevMeaningfulToken($endIndex)]->equals(',')) {
                $this->removeSpaceAroundToken($tokens, $endIndex - 1);
            }
        }
    }

    /**
     * Remove spaces from token at a given index.
     *
     * @param Tokens $tokens
     * @param int    $index
     */
    private function removeSpaceAroundToken(Tokens $tokens, $index)
    {
        $token = $tokens[$index];

        if ($token->isWhitespace() && false === strpos($token->getContent(), "\n")) {
            $tokens->clearAt($index);
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Phpdoc;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 */
final class PhpdocAnnotationWithoutDotFixer extends AbstractFixer
{
    private $tags = ['throws', 'return', 'param', 'internal', 'deprecated', 'var', 'type'];

    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'PHPDoc annotation descriptions should not be a sentence.',
            [new CodeSample('<?php
/**
 * @param string $bar Some string.
 */
function foo ($bar) {}
')]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isTokenKindFound(T_DOC_COMMENT);
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        foreach ($tokens as $index => $token) {
            if (!$token->isGivenKind(T_DOC_COMMENT)) {
                continue;
            }

            $doc = new DocBlock($token->getContent());
            $annotations = $doc->getAnnotations();

            if (empty($annotations)) {
                continue;
            }

            foreach ($annotations as $annotation) {
                if (
                    !$annotation->getTag()->valid() || !\in_array($annotation->getTag()->getName(), $this->tags, true)
                ) {
                    continue;
                }

                $lineAfterAnnotation = $doc->getLine($annotation->getEnd() + 1);
                if (null !== $lineAfterAnnotation) {
                    $lineAfterAnnotationTrimmed = ltrim($lineAfterAnnotation->getContent());
                    if ('' === $lineAfterAnnotationTrimmed || '*' !== $lineAfterAnnotationTrimmed[0]) {
                        // malformed PHPDoc, missing asterisk !
                        continue;
                    }
                }

                $content = $annotation->getContent();

                if (
                    1 !== Preg::match('/[.。]\h*$/u', $content)
                    || 0 !== Preg::match('/[.。](?!\h*$)/u', $content, $matches)
                ) {
                    continue;
                }

                $endLine = $doc->getLine($annotation->getEnd());
                $endLine->setContent(Preg::replace('/(?<![.。])[.。]\h*(\H+)$/u', '\1', $endLine->getContent()));

                $startLine = $doc->getLine($annotation->getStart());
                $optionalTypeRegEx = $annotation->supportTypes()
                    ? sprintf('(?:%s\s+(?:\$\w+\s+)?)?', preg_quote(implode('|', $annotation->getTypes()), '/'))
                    : '';
                $content = Preg::replaceCallback(
                    '/^(\s*\*\s*@\w+\s+'.$optionalTypeRegEx.')(\p{Lu}?(?=\p{Ll}|\p{Zs}))(.*)$/',
                    static function (array $matches) {
                        return $matches[1].strtolower($matches[2]).$matches[3];
                    },
                    $startLine->getContent(),
                    1
                );
                $startLine->setContent($content);
            }

            $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]);
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Operator;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author ntzm
 */
final class StandardizeIncrementFixer extends AbstractFixer
{
    /**
     * @internal
     */
    const EXPRESSION_END_TOKENS = [
        ';',
        ')',
        ']',
        ',',
        ':',
        [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
        [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
        [T_CLOSE_TAG],
    ];

    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'Increment and decrement operators should be used if possible.',
            [
                new CodeSample("<?php\n\$i += 1;\n"),
                new CodeSample("<?php\n\$i -= 1;\n"),
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPriority()
    {
        // Must be run before IncrementStyleFixer in case user wants to
        // post-increment instead
        return 1;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isAnyTokenKindsFound([T_PLUS_EQUAL, T_MINUS_EQUAL]);
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        for ($index = $tokens->count() - 1; $index > 0; --$index) {
            $expressionEnd = $tokens[$index];
            if (!$expressionEnd->equalsAny(self::EXPRESSION_END_TOKENS)) {
                continue;
            }

            $numberIndex = $tokens->getPrevMeaningfulToken($index);
            $number = $tokens[$numberIndex];
            if (!$number->isGivenKind(T_LNUMBER) || '1' !== $number->getContent()) {
                continue;
            }

            $operatorIndex = $tokens->getPrevMeaningfulToken($numberIndex);
            $operator = $tokens[$operatorIndex];
            if (!$operator->isGivenKind([T_PLUS_EQUAL, T_MINUS_EQUAL])) {
                continue;
            }

            $startIndex = $this->findStart($tokens, $tokens->getPrevMeaningfulToken($operatorIndex));

            $this->clearRangeLeaveComments(
                $tokens,
                $tokens->getPrevMeaningfulToken($operatorIndex) + 1,
                $numberIndex
            );

            $tokens->insertAt(
                $startIndex,
                new Token($operator->isGivenKind(T_PLUS_EQUAL) ? [T_INC, '++'] : [T_DEC, '--'])
            );
        }
    }

    /**
     * Find the start of a reference.
     *
     * @param Tokens $tokens
     * @param int    $index
     *
     * @return int
     */
    private function findStart(Tokens $tokens, $index)
    {
        while (!$tokens[$index]->equalsAny(['$', [T_VARIABLE]])) {
            if ($tokens[$index]->equals(']')) {
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
            } elseif ($tokens[$index]->isGivenKind(CT::T_DYNAMIC_PROP_BRACE_CLOSE)) {
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, $index);
            } elseif ($tokens[$index]->isGivenKind(CT::T_DYNAMIC_VAR_BRACE_CLOSE)) {
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, $index);
            } elseif ($tokens[$index]->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE)) {
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE, $index);
            } else {
                $index = $tokens->getPrevMeaningfulToken($index);
            }
        }

        while ($tokens[$tokens->getPrevMeaningfulToken($index)]->equals('$')) {
            $index = $tokens->getPrevMeaningfulToken($index);
        }

        if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_OBJECT_OPERATOR)) {
            return $this->findStart($tokens, $tokens->getPrevMeaningfulToken($index));
        }

        return $index;
    }

    /**
     * Clear tokens in the given range unless they are comments.
     *
     * @param Tokens $tokens
     * @param int    $indexStart
     * @param int    $indexEnd
     */
    private function clearRangeLeaveComments(Tokens $tokens, $indexStart, $indexEnd)
    {
        for ($i = $indexStart; $i <= $indexEnd; ++$i) {
            $token = $tokens[$i];

            if ($token->isComment()) {
                continue;
            }

            if ($token->isWhitespace("\n\r")) {
                continue;
            }

            $tokens->clearAt($i);
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\DocBlock;

/**
 * This class is responsible for comparing tags to see if they should be kept
 * together, or kept apart.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class TagComparator
{
    /**
     * Groups of tags that should be allowed to immediately follow each other.
     *
     * @var array
     */
    private static $groups = [
        ['deprecated', 'link', 'see', 'since'],
        ['author', 'copyright', 'license'],
        ['category', 'package', 'subpackage'],
        ['property', 'property-read', 'property-write'],
    ];

    /**
     * Should the given tags be kept together, or kept apart?
     *
     * @param Tag $first
     * @param Tag $second
     *
     * @return bool
     */
    public static function shouldBeTogether(Tag $first, Tag $second)
    {
        $firstName = $first->getName();
        $secondName = $second->getName();

        if ($firstName === $secondName) {
            return true;
        }

        foreach (self::$groups as $group) {
            if (\in_array($firstName, $group, true) && \in_array($secondName, $group, true)) {
                return true;
            }
        }

        return false;
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Operator;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author Javier Spagnoletti <phansys@gmail.com>
 */
final class NotOperatorWithSpaceFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'Logical NOT operators (`!`) should have leading and trailing whitespaces.',
            [new CodeSample(
                '<?php

if (!$bar) {
    echo "Help!";
}
'
            )]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPriority()
    {
        // should be run after the UnaryOperatorSpacesFixer
        return -10;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isTokenKindFound('!');
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
            $token = $tokens[$index];

            if ($token->equals('!')) {
                if (!$tokens[$index + 1]->isWhitespace()) {
                    $tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' ']));
                }

                if (!$tokens[$index - 1]->isWhitespace()) {
                    $tokens->insertAt($index, new Token([T_WHITESPACE, ' ']));
                }
            }
        }
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Whitespace;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author Vladimir Boliev <voff.web@gmail.com>
 */
final class MethodChainingIndentationFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'Method chaining MUST be properly indented. Method chaining with different levels of indentation is not supported.',
            [new CodeSample("<?php\n\$user->setEmail('voff.web@gmail.com')\n         ->setPassword('233434');\n")]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPriority()
    {
        // Should run after BracesFixer
        // Should run before ArrayIndentationFixer
        return -29;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isTokenKindFound(T_OBJECT_OPERATOR);
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        $lineEnding = $this->whitespacesConfig->getLineEnding();

        for ($index = 1, $count = \count($tokens); $index < $count; ++$index) {
            if (!$tokens[$index]->isGivenKind(T_OBJECT_OPERATOR)) {
                continue;
            }

            if ($this->canBeMovedToNextLine($index, $tokens)) {
                $newline = new Token([T_WHITESPACE, $lineEnding]);
                if ($tokens[$index - 1]->isWhitespace()) {
                    $tokens[$index - 1] = $newline;
                } else {
                    $tokens->insertAt($index, $newline);
                    ++$index;
                }
            }

            $currentIndent = $this->getIndentAt($tokens, $index - 1);
            if (null === $currentIndent) {
                continue;
            }

            $expectedIndent = $this->getExpectedIndentAt($tokens, $index);
            if ($currentIndent !== $expectedIndent) {
                $tokens[$index - 1] = new Token([T_WHITESPACE, $lineEnding.$expectedIndent]);
            }
        }
    }

    /**
     * @param Tokens $tokens
     * @param int    $index  index of the first token on the line to indent
     *
     * @return string
     */
    private function getExpectedIndentAt(Tokens $tokens, $index)
    {
        $index = $tokens->getPrevMeaningfulToken($index);
        $indent = $this->whitespacesConfig->getIndent();

        for ($i = $index; $i >= 0; --$i) {
            if ($tokens[$i]->equals(')')) {
                $i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
            }

            $currentIndent = $this->getIndentAt($tokens, $i);
            if (null === $currentIndent) {
                continue;
            }

            if ($this->currentLineRequiresExtraIndentLevel($tokens, $i, $index)) {
                return $currentIndent.$indent;
            }

            return $currentIndent;
        }

        return $indent;
    }

    /**
     * @param int    $index  position of the T_OBJECT_OPERATOR token
     * @param Tokens $tokens
     *
     * @return bool
     */
    private function canBeMovedToNextLine($index, Tokens $tokens)
    {
        $prevMeaningful = $tokens->getPrevMeaningfulToken($index);
        $hasCommentBefore = false;

        for ($i = $index - 1; $i > $prevMeaningful; --$i) {
            if ($tokens[$i]->isComment()) {
                $hasCommentBefore = true;

                continue;
            }

            if ($tokens[$i]->isWhitespace() && 1 === Preg::match('/\R/', $tokens[$i]->getContent())) {
                return $hasCommentBefore;
            }
        }

        return false;
    }

    /**
     * @param Tokens $tokens
     * @param int    $index  index of the indentation token
     *
     * @return null|string
     */
    private function getIndentAt(Tokens $tokens, $index)
    {
        if (1 === Preg::match('/\R{1}([ \t]*)$/', $this->getIndentContentAt($tokens, $index), $matches)) {
            return $matches[1];
        }

        return null;
    }

    private function getIndentContentAt(Tokens $tokens, $index)
    {
        if (!$tokens[$index]->isGivenKind([T_WHITESPACE, T_INLINE_HTML])) {
            return '';
        }

        $content = $tokens[$index]->getContent();

        if ($tokens[$index]->isWhitespace() && $tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) {
            $content = $tokens[$index - 1]->getContent().$content;
        }

        if (Preg::match('/\R/', $content)) {
            return $content;
        }

        return '';
    }

    /**
     * @param Tokens $tokens
     * @param int    $start  index of first meaningful token on previous line
     * @param int    $end    index of last token on previous line
     *
     * @return bool
     */
    private function currentLineRequiresExtraIndentLevel(Tokens $tokens, $start, $end)
    {
        if ($tokens[$start + 1]->isGivenKind(T_OBJECT_OPERATOR)) {
            return false;
        }

        if ($tokens[$end]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_CLOSE)) {
            return true;
        }

        return
            !$tokens[$end]->equals(')')
            || $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $end) >= $start
        ;
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\FunctionNotation;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;

/**
 * @author Mark Nielsen
 */
final class VoidReturnFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'Add void return type to functions with missing or empty return statements, but priority is given to `@return` annotations. Requires PHP >= 7.1.',
            [
                new VersionSpecificCodeSample(
                    "<?php\nfunction foo(\$a) {};\n",
                    new VersionSpecification(70100)
                ),
            ],
            null,
            'Modifies the signature of functions.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPriority()
    {
        // must run before ReturnTypeDeclarationFixer and PhpdocNoEmptyReturnFixer
        return 15;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return \PHP_VERSION_ID >= 70100 && $tokens->isTokenKindFound(T_FUNCTION);
    }

    /**
     * {@inheritdoc}
     */
    public function isRisky()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        // These cause syntax errors.
        static $blacklistFuncNames = [
            [T_STRING, '__construct'],
            [T_STRING, '__destruct'],
            [T_STRING, '__clone'],
        ];

        for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
            if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
                continue;
            }

            $funcName = $tokens->getNextMeaningfulToken($index);
            if ($tokens[$funcName]->equalsAny($blacklistFuncNames, false)) {
                continue;
            }

            $startIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);

            if ($this->hasReturnTypeHint($tokens, $startIndex)) {
                continue;
            }

            if ($tokens[$startIndex]->equals(';')) {
                // No function body defined, fallback to PHPDoc.
                if ($this->hasVoidReturnAnnotation($tokens, $index)) {
                    $this->fixFunctionDefinition($tokens, $startIndex);
                }

                continue;
            }

            if ($this->hasReturnAnnotation($tokens, $index)) {
                continue;
            }

            $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);

            if ($this->hasVoidReturn($tokens, $startIndex, $endIndex)) {
                $this->fixFunctionDefinition($tokens, $startIndex);
            }
        }
    }

    /**
     * Determine whether there is a non-void return annotation in the function's PHPDoc comment.
     *
     * @param Tokens $tokens
     * @param int    $index  The index of the function token
     *
     * @return bool
     */
    private function hasReturnAnnotation(Tokens $tokens, $index)
    {
        foreach ($this->findReturnAnnotations($tokens, $index) as $return) {
            if (['void'] !== $return->getTypes()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether there is a void return annotation in the function's PHPDoc comment.
     *
     * @param Tokens $tokens
     * @param int    $index  The index of the function token
     *
     * @return bool
     */
    private function hasVoidReturnAnnotation(Tokens $tokens, $index)
    {
        foreach ($this->findReturnAnnotations($tokens, $index) as $return) {
            if (['void'] === $return->getTypes()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether the function already has a return type hint.
     *
     * @param Tokens $tokens
     * @param int    $index  The index of the end of the function definition line, EG at { or ;
     *
     * @return bool
     */
    private function hasReturnTypeHint(Tokens $tokens, $index)
    {
        $endFuncIndex = $tokens->getPrevTokenOfKind($index, [')']);
        $nextIndex = $tokens->getNextMeaningfulToken($endFuncIndex);

        return $tokens[$nextIndex]->isGivenKind(CT::T_TYPE_COLON);
    }

    /**
     * Determine whether the function has a void return.
     *
     * @param Tokens $tokens
     * @param int    $startIndex Start of function body
     * @param int    $endIndex   End of function body
     *
     * @return bool
     */
    private function hasVoidReturn(Tokens $tokens, $startIndex, $endIndex)
    {
        $tokensAnalyzer = new TokensAnalyzer($tokens);

        for ($i = $startIndex; $i < $endIndex; ++$i) {
            if (
                // skip anonymous classes
                ($tokens[$i]->isGivenKind(T_CLASS) && $tokensAnalyzer->isAnonymousClass($i)) ||
                 // skip lambda functions
                ($tokens[$i]->isGivenKind(T_FUNCTION) && $tokensAnalyzer->isLambda($i))
            ) {
                $i = $tokens->getNextTokenOfKind($i, ['{']);
                $i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);

                continue;
            }

            if ($tokens[$i]->isGivenKind([T_YIELD, T_YIELD_FROM])) {
                return false; // Generators cannot return void.
            }

            if (!$tokens[$i]->isGivenKind(T_RETURN)) {
                continue;
            }

            $i = $tokens->getNextMeaningfulToken($i);
            if (!$tokens[$i]->equals(';')) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param Tokens $tokens
     * @param int    $index  The index of the end of the function definition line, EG at { or ;
     */
    private function fixFunctionDefinition(Tokens $tokens, $index)
    {
        $endFuncIndex = $tokens->getPrevTokenOfKind($index, [')']);
        $tokens->insertAt($endFuncIndex + 1, [
            new Token([CT::T_TYPE_COLON, ':']),
            new Token([T_WHITESPACE, ' ']),
            new Token([T_STRING, 'void']),
        ]);
    }

    /**
     * Find all the return annotations in the function's PHPDoc comment.
     *
     * @param Tokens $tokens
     * @param int    $index  The index of the function token
     *
     * @return Annotation[]
     */
    private function findReturnAnnotations(Tokens $tokens, $index)
    {
        do {
            $index = $tokens->getPrevNonWhitespace($index);
        } while ($tokens[$index]->isGivenKind([
            T_ABSTRACT,
            T_FINAL,
            T_PRIVATE,
            T_PROTECTED,
            T_PUBLIC,
            T_STATIC,
        ]));

        if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
            return [];
        }

        $doc = new DocBlock($tokens[$index]->getContent());

        return $doc->getAnnotationsOfType('return');
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Console\Command;

use PhpCsFixer\Preg;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * @internal
 */
final class ReadmeCommand extends Command
{
    const COMMAND_NAME = 'readme';

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName(self::COMMAND_NAME)
            ->setDescription('Generates the README content, based on the fix command help.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $header = <<<'EOF'
PHP Coding Standards Fixer
==========================

The PHP Coding Standards Fixer (PHP CS Fixer) tool fixes your code to follow standards;
whether you want to follow PHP coding standards as defined in the PSR-1, PSR-2, etc.,
or other community driven ones like the Symfony one.
You can **also** define your (teams) style through configuration.

It can modernize your code (like converting the ``pow`` function to the ``**`` operator on PHP 5.6)
and (micro) optimize it.

If you are already using a linter to identify coding standards problems in your
code, you know that fixing them by hand is tedious, especially on large
projects. This tool does not only detect them, but also fixes them for you.

The PHP CS Fixer is maintained on GitHub at https://github.com/FriendsOfPHP/PHP-CS-Fixer
bug reports and ideas about new features are welcome there.

You can talk to us at https://gitter.im/PHP-CS-Fixer/Lobby about the project,
configuration, possible improvements, ideas and questions, please visit us!

Requirements
------------

PHP needs to be a minimum version of PHP 5.6.0.

Installation
------------

Locally
~~~~~~~

Download the `php-cs-fixer.phar`_ file and store it somewhere on your computer.

Globally (manual)
~~~~~~~~~~~~~~~~~

You can run these commands to easily access latest ``php-cs-fixer`` from anywhere on
your system:

.. code-block:: bash

    $ wget %download.url% -O php-cs-fixer

or with specified version:

.. code-block:: bash

    $ wget %download.version_url% -O php-cs-fixer

or with curl:

.. code-block:: bash

    $ curl -L %download.url% -o php-cs-fixer

then:

.. code-block:: bash

    $ sudo chmod a+x php-cs-fixer
    $ sudo mv php-cs-fixer /usr/local/bin/php-cs-fixer

Then, just run ``php-cs-fixer``.

Globally (Composer)
~~~~~~~~~~~~~~~~~~~

To install PHP CS Fixer, `install Composer <https://getcomposer.org/download/>`_ and issue the following command:

.. code-block:: bash

    $ composer global require friendsofphp/php-cs-fixer

Then make sure you have the global Composer binaries directory in your ``PATH``. This directory is platform-dependent, see `Composer documentation <https://getcomposer.org/doc/03-cli.md#composer-home>`_ for details. Example for some Unix systems:

.. code-block:: bash

    $ export PATH="$PATH:$HOME/.composer/vendor/bin"

Globally (homebrew)
~~~~~~~~~~~~~~~~~~~

.. code-block:: bash

    $ brew install php-cs-fixer

Locally (PHIVE)
~~~~~~~~~~~~~~~

Install `PHIVE <https://phar.io>`_ and issue the following command:

.. code-block:: bash

    $ phive install php-cs-fixer # use `--global` for global install

Update
------

Locally
~~~~~~~

The ``self-update`` command tries to update ``php-cs-fixer`` itself:

.. code-block:: bash

    $ php php-cs-fixer.phar self-update

Globally (manual)
~~~~~~~~~~~~~~~~~

You can update ``php-cs-fixer`` through this command:

.. code-block:: bash

    $ sudo php-cs-fixer self-update

Globally (Composer)
~~~~~~~~~~~~~~~~~~~

You can update ``php-cs-fixer`` through this command:

.. code-block:: bash

    $ ./composer.phar global update friendsofphp/php-cs-fixer

Globally (homebrew)
~~~~~~~~~~~~~~~~~~~

You can update ``php-cs-fixer`` through this command:

.. code-block:: bash

    $ brew upgrade php-cs-fixer

Locally (PHIVE)
~~~~~~~~~~~~~~~~~~~

.. code-block:: bash

    $ phive update php-cs-fixer

Usage
-----

EOF;

        $footer = <<<'EOF'

Helpers
-------

Dedicated plugins exist for:

* `Atom`_
* `NetBeans`_
* `PhpStorm`_
* `Sublime Text`_
* `Vim`_
* `VS Code`_

Contribute
----------

The tool comes with quite a few built-in fixers, but everyone is more than
welcome to `contribute`_ more of them.

Fixers
~~~~~~

A *fixer* is a class that tries to fix one CS issue (a ``Fixer`` class must
implement ``FixerInterface``).

Configs
~~~~~~~

A *config* knows about the CS rules and the files and directories that must be
scanned by the tool when run in the directory of your project. It is useful for
projects that follow a well-known directory structures (like for Symfony
projects for instance).

.. _php-cs-fixer.phar: %download.url%
.. _Atom:              https://github.com/Glavin001/atom-beautify
.. _NetBeans:          http://plugins.netbeans.org/plugin/49042/php-cs-fixer
.. _PhpStorm:          https://medium.com/@valeryan/how-to-configure-phpstorm-to-use-php-cs-fixer-1844991e521f
.. _Sublime Text:      https://github.com/benmatselby/sublime-phpcs
.. _Vim:               https://github.com/stephpy/vim-php-cs-fixer
.. _VS Code:           https://github.com/junstyle/vscode-php-cs-fixer
.. _contribute:        https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/CONTRIBUTING.md

EOF;

        $command = $this->getApplication()->get('fix');
        $help = $command->getHelp();
        $help = str_replace('%command.full_name%', 'php-cs-fixer.phar '.$command->getName(), $help);
        $help = str_replace('%command.name%', $command->getName(), $help);
        $help = Preg::replace('#</?(comment|info)>#', '``', $help);
        $help = Preg::replace('#`(``.+?``)`#', '$1', $help);
        $help = Preg::replace('#^(\s+)``(.+)``$#m', '$1$2', $help);
        $help = Preg::replace('#^ \* ``(.+)``(.*?\n)#m', "* **$1**$2\n", $help);
        $help = Preg::replace('#^   \\| #m', '  ', $help);
        $help = Preg::replace('#^   \\|#m', '', $help);
        $help = Preg::replace('#^(?=  \\*Risky rule: )#m', "\n", $help);
        $help = Preg::replace("#^(  Configuration options:\n)(  - )#m", "$1\n$2", $help);
        $help = Preg::replace("#^\n( +\\$ )#m", "\n.. code-block:: bash\n\n$1", $help);
        $help = Preg::replace("#^\n( +<\\?php)#m", "\n.. code-block:: php\n\n$1", $help);
        $help = Preg::replaceCallback(
            '#^\s*<\?(\w+).*?\?>#ms',
            static function ($matches) {
                $result = Preg::replace("#^\\.\\. code-block:: bash\n\n#m", '', $matches[0]);

                if ('php' !== $matches[1]) {
                    $result = Preg::replace("#<\\?{$matches[1]}\\s*#", '', $result);
                }

                return Preg::replace("#\n\n +\\?>#", '', $result);
            },
            $help
        );

        // Transform links
        // In the console output these have the form
        //      `description` (<url>http://...</url>)
        // Make to RST http://www.sphinx-doc.org/en/stable/rest.html#hyperlinks
        //      `description <http://...>`_

        $help = Preg::replaceCallback(
            '#`(.+)`\s?\(<url>(.+)<\/url>\)#',
            static function (array $matches) {
                return sprintf('`%s <%s>`_', str_replace('\\', '\\\\', $matches[1]), $matches[2]);
            },
            $help
        );

        $help = Preg::replace('#^                        #m', '  ', $help);
        $help = Preg::replace('#\*\* +\[#', '** [', $help);

        $downloadLatestUrl = sprintf('https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v%s/php-cs-fixer.phar', HelpCommand::getLatestReleaseVersionFromChangeLog());
        $downloadUrl = 'https://cs.symfony.com/download/php-cs-fixer-v2.phar';

        $header = str_replace('%download.version_url%', $downloadLatestUrl, $header);
        $header = str_replace('%download.url%', $downloadUrl, $header);
        $footer = str_replace('%download.version_url%', $downloadLatestUrl, $footer);
        $footer = str_replace('%download.url%', $downloadUrl, $footer);

        $output->write($header."\n".$help."\n".$footer);
    }
}


<?php

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Fixer\Semicolon;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author Graham Campbell <graham@alt-three.com>
 */
final class NoSinglelineWhitespaceBeforeSemicolonsFixer extends AbstractFixer
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return new FixerDefinition(
            'Single-line whitespace before closing semicolon are prohibited.',
            [new CodeSample("<?php \$this->foo() ;\n")]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isTokenKindFound(';');
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        foreach ($tokens as $index => $token) {
            if (!$token->equals(';') || !$tokens[$index - 1]->isWhitespace(" \t")) {
                continue;
            }

            if ($tokens[$index - 2]->equals(';')) {
                // do not remove all whitespace before the semicolon because it is also whitespace after another semicolon
                if (!$tokens[$index - 1]->equals(' ')) {
                    $tokens[$index - 1] = new Token([T_WHITESPACE, ' ']);
                }
            } elseif (!$tokens[$index - 2]->isComment()) {
                $tokens->clearAt($index - 1);
            }
        }
    }
}