<?php
namespace com\mikebevz\xsd2php\wsdl;

require_once 'AbstractWsdl.php';
require_once 'IWsdl.php';

class Wsdl_1_1 extends AbstractWsdl implements IWsdl
{

    /**
     * WSDL 1.1 XML namespaces
     *
     * @var array
     */
    protected $namespaces = array(
        'wsdl' => 'http://schemas.xmlsoap.org/wsdl/',
        'soap' => 'http://schemas.xmlsoap.org/wsdl/soap/',
        'http' => 'http://schemas.xmlsoap.org/wsdl/http/',
        'mime' => 'http://schemas.xmlsoap.org/wsdl/mime/',
        'soapenc' => 'http://schemas.xmlsoap.org/soap/encoding/',
        'soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/',
        'xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
        'xsd' => 'http://www.w3.org/2001/XMLSchema',
    );

    /**
     * Prepare WSDL DOM model
     *
     * @return void
     */
    public function prepare()
    {
        $this->dom->createAttributeNS($this->namespaces['soap'], 'soap:definitions');
        $this->dom->createAttributeNS($this->namespaces[$this->xmlSchemaPreffix], $this->xmlSchemaPreffix.':definitions');

        //@todo this only required when we have arrays
        $this->dom->createAttributeNS($this->namespaces['soapenc'], 'soapenc:definitions');

        $this->wsTypes = $this->dom->createElement('wsdl:types');
        $this->wsDefinitions->appendChild($this->wsTypes);

        $wsXmlSchema = $this->dom->createElement($this->xmlSchemaPreffix.':schema');
        $wsXmlSchema->setAttribute('targetNamespace', $this->wsdlTargetNamespace);

        $this->wsXmlSchema = $this->wsTypes->appendChild($wsXmlSchema);

        $portType =  $this->dom->createElement('wsdl:portType');
        $portType->setAttribute('name', $this->wsdlName.$this->portNameSuffix);
        $this->wsPortType = $this->wsDefinitions->appendChild($portType);

        $binding = $this->dom->createElement('wsdl:binding');
        $binding->setAttribute('name', $this->wsdlName.$this->bindingNameSuffix);
        $binding->setAttribute('type', $this->targetNsPrefix.":".$this->wsdlName.$this->portNameSuffix);
        $this->wsBinding = $this->wsDefinitions->appendChild($binding);

        $soapBinding = $this->dom->createElement('soap:binding');
        if ($this->soapBindingStyle === SOAP_DOCUMENT) {
            $soapBinding->setAttribute('style', "document");
        } elseif ($this->soapBindingStyle === SOAP_RPC) {
            $soapBinding->setAttribute('style', "rpc");
        }
        $soapBinding->setAttribute('transport', $this->soapBindingTransport);
        $this->wsBinding->appendChild($soapBinding);

        $wsService = $this->dom->createElement('wsdl:service');
        $wsService->setAttribute('name', $this->wsdlName.$this->serviceNameSuffix);
        $this->wsService = $this->wsDefinitions->appendChild($wsService);

        $port = $this->dom->createElement('wsdl:port');
        $port->setAttribute('name', $this->wsdlName.$this->portNameSuffix);
        $port->setAttribute('binding', $this->targetNsPrefix.":".$this->wsdlName.$this->bindingNameSuffix);

        $soapAddress =  $this->dom->createElement('soap:address');
        $soapAddress->setAttribute('location', $this->getLocation());
        $port->appendChild($soapAddress);

        $this->wsService->appendChild($port);

        $this->extractTypes();
    }

    /**
     * Extract types from WSDL
     *
     * @return void
     */
    private function extractTypes()
    {
        $allEl = array();
        $allImp = array();
        $methods = array();

        foreach ($this->methodsMeta as $methodName => $data) {
            $elements = array();
            $input = false;
            $output = false;
            if (count($data['params']) == 1) {
                $type = $this->common->phpTypeToSoap($data['params'][0]['type']);
                $els = array();
                if ($type === false) {
                    $type = $this->getTypeName($data['params'][0]['type']);
                    $els [] = $this->createRefElement($type);
                } else {
                    $els [] = $this->createSimpleElement(ucfirst($data['params'][0]['name']), $data['params'][0]['type']);
                }
                $elements[] = $this->createElementWithComplexType(ucfirst($methodName), $els);
                $this->addMessage(ucfirst($methodName), $this->targetNsPrefix.":".ucfirst($methodName));
                $input = true;
            } elseif ($data['params'] > 1) {
                $els = array();
                foreach ($data['params'] as $input) {
                    $type = $this->common->phpTypeToSoap($input['type']);
                    if ($type === false) {
                        $type = $this->getTypeName($input['type']);
                        $els [] = $this->createRefElement($type);
                    } else {
                        $els [] = $this->createSimpleElement(ucfirst($input['name']), $input['type']);
                    }
                }
                $elements[] = $this->createElementWithComplexType(ucfirst($methodName), $els);
                $this->addMessage(ucfirst($methodName), $this->targetNsPrefix.":".ucfirst($methodName));
                $input = true;
            }

            if (array_key_exists('return', $data)) {
                $type = $this->common->phpTypeToSoap($data['return']['type']);
                if ($type === false) {
                    //@todo Create complex type
                } else {
                    $elements [] = $this->createSimpleElement(ucfirst($methodName)."Response", $type);
                }
                //$messages[] = $this->targetNsPrefix.":".ucfirst($methodName)."Response";
                $this->addMessage(ucfirst($methodName)."Response", $this->targetNsPrefix.":".ucfirst($methodName));
                $output = true;
            }

            foreach ($this->xmlSchemaImports as $imp) {
                array_push($allImp, $imp);
            }

            foreach ($elements as $element) {
                array_push($allEl, $element);
            }

            $this->addPortOperations(ucfirst($methodName), $input, $output);
            $this->addBindingOperations(ucfirst($methodName), $input, $output);
        }

        foreach ($allImp as $import) {
            $this->wsXmlSchema->appendChild($import);
        }

        foreach ($allEl as $element) {
            $this->wsXmlSchema->appendChild($element);
        }

        // Add port operations

        //print_r($messages);
    }

    /**
     * Add operations to bindings
     *
     *
     *  <operation name="$operation">
     *		<soap:operation
     *			soapAction="targetNamespace/$operation" />
     *		<input>
     *			$input
     *		</input>
     *		<output>
     *			$output
     *		</output>
     *	</operation>
     *
     *
     * @param string $operation Operation name
     * @param array  $input     Array of inputs [optional]
     * @param array  $output    Array of outputs [optional]
     *
     * @return void
     */
    private function addBindingOperations($operation, $input = false, $output = false)
    {
        $el = $this->dom->createElement('wsdl:operation');
        $el->setAttribute('name', $operation);

        $soapOperation = $this->dom->createElement('soap:operation');
        $soapOperation->setAttribute('soapAction', $this->wsdlTargetNamespace."/".$operation);
        $el->appendChild($soapOperation);

        if ($input === true) {
            $inp = $this->dom->createElement('wsdl:input');
            $body = $this->dom->createElement('soap:body');
            $body->setAttribute('use', 'literal');
            $inp->appendChild($body);
            $el->appendChild($inp);
        }

        if ($output === true) {
            $out = $this->dom->createElement('wsdl:output');
            $body = $this->dom->createElement('soap:body');
            $body->setAttribute('use', 'literal');
            $out->appendChild($body);
            $el->appendChild($out);
        }

        $this->wsBinding->appendChild($el);
    }

    /**
     * Add operations to port
     *
     *
     *  <operation name="$name">
     *		<input message="tns:messageName" />
     *		<output message="tns:messageName" />
     *	</operation>
     *
     *
     * @param string $operation Name of the operation
     * @param array  $input     Array of inputs
     * @param array  $output    Array of outputs
     *
     * @return void
     */
    private function addPortOperations($operation, $input = false, $output = false)
    {
        $el = $this->dom->createElement("wsdl:operation");
        $el->setAttribute('name', $operation);
        if ($input === true) {
            $input = $this->dom->createElement('wsdl:input');
            $input->setAttribute('message', $this->targetNsPrefix.":".$operation);
        }
        if ($output === true) {
            $output = $this->dom->createElement('wsdl:output');
            $output->setAttribute('message', $this->targetNsPrefix.":". $operation."Response");
        }
        $el->appendChild($input);
        $el->appendChild($output);

        $this->wsPortType->appendChild($el);
    }

    /**
     * Add message
     *
     *
     *   <message name="$name">
     *	   <part name="$name" element="$type" />
     *  </message>
     *
     *
     * @param string $name Message name
     * @param string $type Message type
     *
     * @return void
     */
    private function addMessage($name, $type)
    {
        $el = $this->dom->createElement("wsdl:message");
        $el->setAttribute('name', $name);
        $parts = $this->dom->createElement("wsdl:part");
        $parts->setAttribute('name', $name);
        $parts->setAttribute('element', $type);
        $el->appendChild($parts);

        //$this->wsMessage->appendChild($el);
        //$this->dom->insertBefore($el, $this->wsPortType);
        //$this->wsDefinitions->replaceChild($el, $this->wsMessage);
        $xpath = new \DOMXPath($this->dom);
        $query = "//*[local-name()='definitions']/*";
        $types = $xpath->query($query);
        //print_r($types->item(0)->getNodePath());
        //$this->dom->insertBefore($el, $types->item(1));
        $this->wsDefinitions->insertBefore($el, $types->item(1));
    }

    /**
     * Create ref element
     *
     *
     * <element ref="$ref" />
     *
     *
     * @param string $ref
     *
     * @return DOMElement
     */
    private function createRefElement($ref)
    {
        $el = $this->dom->createElement($this->xmlSchemaPreffix.':element');
        $el->setAttribute('ref', $ref);

        return $el;
    }

    /**
     * Create an element with complex type
     *
     *
     * <element name="$name">
     *   <complexType>
     *   	<sequence>
     *   		$elements
     *   	</sequence>
     *   </complexType>
     * </element>
     *
     *
     * @param string $name     Element name
     * @param array  $elements Array of elements to include into sequence
     *
     * @return DOMElement
     */
    private function createElementWithComplexType($name, $elements)
    {
        //print_r('Create complex type '.$name."\n");
        $el = $this->dom->createElement($this->xmlSchemaPreffix.':element');
        $el->setAttribute('name', $name);

        $compType = $this->dom->createElement($this->xmlSchemaPreffix.':complexType');
        $seqType = $this->dom->createElement($this->xmlSchemaPreffix.':sequence');

        foreach ($elements as $element) {
            $seqType->appendChild($element);
        }

        $compType->appendChild($seqType);
        $el->appendChild($compType);

        return $el;
    }

    /**
     * Create simple element
     *
     *
     * <element name="$name" type="$type" />
     *
     *
     * @param string $name Element name
     * @param string $type Element type
     *
     * @return DOMElement
     */
    private function createSimpleElement($name, $type)
    {
        $el = $this->dom->createElement($this->xmlSchemaPreffix.':element');
        $el->setAttribute('name', $name);
        if (preg_match('/:/', $type)) {
            $el->setAttribute('type', $type);
        } else {
            $el->setAttribute('type', $this->xmlSchemaPreffix.':'.$type);
        }

        return $el;
    }
}


<?php
//namespace com\mikebevz\xsd2php;
namespace QuickBooksOnline\API\XSD2PHP\src\com\mikebevz\xsd2php;

/**
 * Copyright 2010 Mike Bevz <myb@mikebevz.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Common data
 *
 * @author Mike Bevz <myb@mikebevz.com>
 * @version 0.0.1
 */
class Common
{

    /**
     * Prefix for generated class names
     *
     * @var classPrefix
     */
    protected $classPrefix;

    public $basicTypes = array('decimal', 'base64Binary', 'normalizedString',
                                'dateTime', 'date', 'boolean',
                                'positiveInteger', 'anySimpleType',
                                'anyURI', 'byte', 'double', 'duration',
                                //'ENTITIES', 'ENTITY', - not sure about those
                                'float', 'gDay', 'gMonth', 'gMonthDay', 'gYear', 'gYearMonth',
                                'hexBinary',
                                //'ID', 'IDREFS', 'IDREF', - not sure
                                'int', 'integer', 'language', 'long',
                                'negativeInteger', 'nonPositiveInteger', 'nonNegativeInteger',
                                //'Name', 'NSName', 'NMTOKEN', 'NMTOKENS', 'NOTATION',
                                'QName', 'short', 'string', 'time', 'token',
                                'unsignedByte', 'unsignedInt', 'unsignedLong', 'unsignedShort'
                                );

    public $phpTypes = array(
        'int', 'integer', 'float', 'string', 'boolean', 'bool', 'object'
    );

    public $reservedWords = array(
        'and', 'or', 'xor', '__FILE__', 'exception',
        '__LINE__', 'array', 'as', 'break', 'case',
        'class', 'const', 'continue', 'declare', 'default',
        'die', 'do', 'echo', 'else', 'elseif',
        'empty', 'enddeclare', 'endfor', 'endforeach', 'endif',
        'endswitch', 'endwhile', 'eval', 'exit', 'extends',
        'for', 'foreach', 'function', 'global', 'if',
        'include', 'include_once', 'isset', 'list', 'new',
        'print', 'require', 'require_once', 'return', 'static',
        'switch', 'unset', 'use', 'var', 'while',
        '__FUNCTION__', '__CLASS__', '__METHOD__', 'final', 'php_user_filter',
        'interface', 'implements', 'instanceof', 'public', 'private',
        'protected', 'abstract', 'clone', 'try', 'catch',
        'throw', 'this', 'final', '__NAMESPACE__', 'namespace', 'goto',
        '__DIR__'
    );

    public $namespaces = null;

    public $lastNsKey = 0;

    public $debug = false;

    /**
     * Dom document
     * @var DOMDocument
     */
    public $dom;

    /**
     * If all classes should be treated as members of a single namespace, set that string here
     * @var overrideAsSingleNamespace
     */
    public $overrideAsSingleNamespace = false;

    /**
     * __construct
     *
     * @param string  $classPrefix Prefix for generated class names
     *
     * @return void
     */
    public function __construct($classPrefix)
    {
        $this->classPrefix = $classPrefix;
    }

    public function getDocNamespaces($dom)
    {
        $xpath = new \DOMXPath($dom);

        $query = "namespace::*";
        $nss = array();
        $namespaces = $xpath->query($query);

        foreach ($namespaces as $node) {
            $key = "";
            if (preg_match("/:/", $node->nodeName)) {
                list($pref, $key) = explode(":", $node->nodeName);
                if ($key === "xml") {
                    continue;
                }
                $nss[$key] = $node->nodeValue;
            }
        }

        return $nss;
    }

    /**
     *
     * @param string  $qname            QName string (fx, ns1:Name)
     * @param boolean $resolveNamespace [optional] Resolve namespace code to full form
     *
     * @return array($namespace, $nodeName)
     */
    public function parseQName($qname, $resolveNamespace = false)
    {
        if (!preg_match('/:/', $qname)) {
            return array('', $qname);
            //throw new \RuntimeException("Given argument is not of QName type: ".$qname);
        }

        list($ns, $name) = explode(":", $qname);

        if ($resolveNamespace === true) {
            $ns = $this->resolveNamespace($ns);
        }

        return array($ns, $name);
    }

    public function isQName($name)
    {
        if (preg_match('/:/', $name)) {
            return true;
        }

        return false;
    }

    public function urnToPhpName($urn)
    {
        $urn = preg_replace('/urn:|http:\/\//', '', $urn);
        $urn = preg_replace('/-/', '_', $urn);
        return preg_replace('/:|\//', '\\', $urn);
    }

    /**
     * Resolve short namespace to long, or return the same code if not found
     *
     * @param unknown_type $shortNs
     *
     * @return string Long namespace
     */
    public function resolveNamespace($shortNs)
    {
        if (!($this->dom instanceof \DOMDocument)) {
            throw new RuntimeException("DOM is not initialized");
        }

        if (!is_array($this->namespaces)) {
            $this->namespaces = $this->getDocNamespaces($this->dom);
        }

        if (array_key_exists($shortNs, $this->namespaces)) {
            return $this->namespaces[$shortNs];
        } else {
            return $shortNs;
        }
    }

    /**
     * Parse Doc comments
     *
     * @param string $comments
     *
     * @return array
     */
    public function parseDocComments($comments)
    {
        $comments = explode("\n", $comments);
        $commentsOut = array();
        $params = array();
        foreach ($comments as $com) {
            if (preg_match('/@/', $com)) {
                $com = preg_replace('/\* /', '', $com);
                $com = preg_replace('/@([a-zA-Z]*)( *)(.*)/', '$1|$3', $com);
                $com = explode('|', $com);
                //$arr = array();
                if (trim($com[0]) == 'param') {
                    $com[1] = $this->parseParamDocs(trim($com[1]));
                    array_push($params, $com[1]);
                    $com[1] = $params;
                    $com[0] = "params";
                }

                if (trim($com[0]) == 'return') {
                    $com[1] = $this->parseReturnDocs(trim($com[1]));
                }

                $commentsOut[trim($com[0])] = trim($com[1]);
                //array_push($commentsOut, array($com[0] => $com[1]));
            }
        }

        return $commentsOut;
    }

    public function parseParamDocs($string)
    {
        $resp = array();
        $data = explode(" ", $string, 3);
        //print_r($data);
        if (count($data) == 2) {
            $resp['type'] = $data[0];
            $resp['name'] = preg_replace('/\$/', '', $data[1]);
        } elseif (count($data) == 3) {
            $resp['type'] = $data[0];
            $resp['name'] = preg_replace('/\$/', '', $data[1]);
            $resp['docs'] = $data[2];
        } else {
            $resp = $string;
        }

        return $resp;
    }

    public function parseReturnDocs($string)
    {
        $resp = array();
        $data = explode(" ", $string, 2);
        if (count($data) == 1) {
            $resp['type'] = $data[0];
        } elseif (count($data) == 2) {
            $resp['type'] = $data[0];
            $resp['docs'] = $data[1];
        } else {
            $resp = $string;
        }

        return $resp;
    }

    public function getNsCode($longNs, $rt = false)
    {
        // if namespace exists - just use its name
        // otherwise add it as nsatrribute to root and use its name
        if (!is_array($this->namespaces)) {
            $this->namespaces = array();
        }
        if (array_key_exists($longNs, $this->namespaces)) {
            return $this->namespaces[$longNs];
        } else {
            $this->namespaces[$longNs] = 'ns'.$this->lastNsKey;
            //$this->logger->debug($this->namespaces[$longNs]);
            if ($rt === false) {
                $nsAttr = $this->dom->createAttributeNS($longNs, $this->namespaces[$longNs].":".$this->rootTagName);
            }
            $this->lastNsKey++;
            return  $this->namespaces[$longNs];
        }
    }

    /**
     * Convert XMLSchema data type to PHP data type
     *
     * @param mixed $type XML Schema data type
     *
     * @return string
     */
    public function normalizeType($type)
    {
        switch ($type) {
            case 'decimal':
            case 'double':
            case 'float':
                return 'float';
                break;

            case 'base64Binary':
            case 'normalizedString':
            case 'dateTime':
            case 'date':
            case 'anySimpleType':
            case 'anyURI':
            case 'duration':
            case 'gDay':
            case 'gMonth':
            case 'gMonthDay':
            case 'gYear':
            case 'gYearMonth':
            case 'language':
            case 'QName':
            case 'string':
            case 'time':
            case 'token':
                return 'string';
                break;

            case 'boolean':
                return 'boolean';
                break;

            case 'positiveInteger':
            case 'byte':
            case 'hexBinary':
            case 'int':
            case 'integer':
            case 'long':
            case 'negativeInteger':
            case 'nonPositiveInteger':
            case 'nonNegativeInteger':
            case 'short':
            case 'unsignedByte':
            case 'unsignedInt':
            case 'unsignedLong':
            case 'unsignedShort':
                return 'integer';
                break;
            default:
                return $type;
                break;
        }
    }

    public function phpTypeToSoap($phpType)
    {
        switch ($phpType) {
            case 'bool':
            case 'boolean':
                return 'boolean';
                break;
            case 'integer':
            case 'int':
                return 'integer';
                break;
            case 'float':
            case 'double':
                return 'float';
                break;

            case 'string':
                return 'string';
                break;
            //case 'array[integer]':
            //case 'array[integer]':
            default:
                return false;
            //case 'object':
        }
    }
}


<?php
namespace QuickBooksOnline\API\Utility;

/**
 * Array Utility class for testing and converting arrays
 *
 */
class ArrayUtil
{
    /**
     * Test if an array is an associate array
     *
     * @param array $arr
     * @return true if $arr is an associative array
     */
    public static function isAssociateArray(array $arr)
    {
        if (!empty($arr)) {
            foreach ($arr as $k => $v) {
                if (is_int($k)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
}


<?php

namespace QuickBooksOnline\API\WebhooksService;

use QuickBooksOnline\API\Utility\JsonValidator;
use QuickBooksOnline\API\Utility\ReflectionUtil;

class WebhooksService
{
    const WEBHOOKSWRAPPERNAME = "WebhooksEvent";

    /**
     * Convert the payLoad of webhook to object
     *
     * @param $payLoad
     *      The payload to be converted
     * @return object
     *      The deserialization format for the payload
     *
     */
    public static function getWebhooksEvent($payLoad)
    {
        JsonValidator::validate($payLoad);
        $string_arry = json_decode($payLoad, true);
        $obj = ReflectionUtil::constructObjectFromWebhooksArray($string_arry, WebhooksService::WEBHOOKSWRAPPERNAME);
        return $obj;
    }

    /**
     * use Token to verifier the payload is sent from Intuit
     *
     * @param $token
     *      The webhook Token get from Intuit
     * @param $payload
     *      The payload for the webhook events
     * @param $intuitHeaderSignature
     *      The header signature
     * @return bool
     *      True if it is valid; otherwise return false;
     */
    public static function verifyPayload($token, $payload, $intuitHeaderSignature)
    {
        $verifier = new TokenVerifier($token);
        return $verifier->verifyPayLoad($payload, $intuitHeaderSignature);
    }
}


<?php

namespace QuickBooksOnline\API\Facades;

use QuickBooksOnline\API\Data\IPPReferenceType;
use QuickBooksOnline\API\Data\IPPid;
use QuickBooksOnline\API\Data\IPPBillableStatusEnum;
use QuickBooksOnline\API\Data\IPPTaxApplicableOnEnum;
use QuickBooksOnline\API\Data\IPPPostingTypeEnum;
use QuickBooksOnline\API\Data\IPPLinkedTxn;
use QuickBooksOnline\API\Data\IPPLine;
use QuickBooksOnline\API\Data\IPPMarkupInfo;
use QuickBooksOnline\API\Data\IPPLineDetailTypeEnum;
use QuickBooksOnline\API\Facades\Common\LineDetailFacade;
use QuickBooksOnline\API\Exception\SdkException;

use QuickBooksOnline\API\Core\CoreConstants;

/**
 * Helper class for deserialize common Complicate Object Type to Object
 * @author Hao
 */
class FacadeHelper{
   /**
    * A helper reflection class to assign object to Target Object
    */
   public static function reflectArrayToObject($classNameOrKeyName, $data, $throwException = TRUE) {
     if(!isset($classNameOrKeyName)){ throw new \Exception("The Class Name or Key Name cannot be NULL when generating Objects.");}
     if(!isset($data) || empty($data)){ throw new \Exception("The passed data cannot be NULL.");}
     if(is_object($data)){
       if(!FacadeHelper::checkIfTheObjectIsAnInstanceOfTheClass($classNameOrKeyName,$data)){
         throw new \Exception("The assigned object is not an instance of required object:{" . $classNameOrKeyName . "}.");
       }else{
         return $data;
       }
     }
     //Get reflection class of FacadeHelper
     $trimedData = FacadeHelper::trimSpacesForArrayKeys($data);
     //Any key in the Ignored List will not be processed
     $IgnoredList = FacadeClassMapper::IgnoredComplexTypeNameEntity();
     //Intuit does not name every Key with its corresponding class as IPP . $key; for some classes, the IPP . $key was not a class name. Those
     //type will located at the Mapper as an array
     $ObjectMap = FacadeClassMapper::classMethodToList();

     //If the key is in complexList
     if(FacadeHelper::isKeyInComplexList($classNameOrKeyName)){
         $methodFound = FacadeHelper::isKeyInComplexList($classNameOrKeyName);
         $createdObj = FacadeHelper::getComplexListObject($methodFound, $classNameOrKeyName, $trimedData);
         return $createdObj;
     } else if(FacadeHelper::isKeyEnumType($classNameOrKeyName)){
         $enumTypeClassName = FacadeHelper::isKeyEnumType($classNameOrKeyName);
         $createdObj = FacadeHelper::getEnumType($enumTypeClassName, $trimedData);
         return $createdObj;
     }
     else{
         //The key can be constructed with an IPPObject
         $clazz = FacadeHelper::decorateKeyWithNameSpaceAndPrefix($classNameOrKeyName);
         if(class_exists($clazz)){
            $currentObj = new $clazz();
            foreach($trimedData as $key => $val){
                if(is_array($val)){
                    if (FacadeHelper::isAssociateArray($val)){
                       //Key value pair. The value can be another array
                       //For example, SalesItemLineDetail as $key and
                       //          {
                       //  "ItemRef": {
                       //      "value": "14",
                       //      "name": "Sod"
                       //  },
                       //  "Qty": 0,
                       //  "TaxCodeRef": {
                       //      "value": "TAX"
                       //  }
                       // }as $val
                       $obj = FacadeHelper::reflectArrayToObject($key, $val, $throwException);
                       FacadeHelper::assignValue($currentObj, $key, $obj);
                    } else if(FacadeHelper::isArrayOfObj($val)){
                         // Array of LineItem object, for example
                         foreach($val as $valobj){
                            if(!FacadeHelper::checkIfTheObjectIsAnInstanceOfTheClass($key,$valobj)){
                              throw new \Exception("The assigned object is not an instance of required object:{" . $key . "}.");
                            }
                         }
                         FacadeHelper::assignValue($currentObj, $key, $val);
                    } else{
                        //The array is a recursive array. It can be an Line or LinkedTxn
                        //Example:
                        // Line": [{ ....}, {...}]
                        //For each element in the array, it is a line
                        //or It can be an mix of lines and Objects
                        //Line": [{ ....}, $obj1, {...}]
                        $list = array();
                        foreach ($val as $index => $element) {
                            if(is_object($element)){
                              if(!FacadeHelper::checkIfTheObjectIsAnInstanceOfTheClass($key,$element)){
                                throw new \Exception("The assigned object is not an instance of required object:{" . $key . "}.");
                              }
                              array_push($list, $element);
                            }else if(is_array($element)){
                              $obj = FacadeHelper::reflectArrayToObject($key, $element, $throwException);
                              array_push($list, $obj);
                            }else{
                              throw new \Exception("Format Error. Expect an element of an array or an object.");
                            }
                          }
                        FacadeHelper::assignValue($currentObj, $key, $list);
                     }
                }else{
                    //Even the value is a key, the key can be an Enum type or a wrapper
                    if(FacadeHelper::isKeyInComplexList($key)){
                        $methodFound = FacadeHelper::isKeyInComplexList($key);
                        $createdObj = FacadeHelper::getComplexListObject($methodFound, $key, $val);
                        FacadeHelper::assignValue($currentObj, $key, $createdObj);
                    }
                    //If it is enum type
                    else if(FacadeHelper::isKeyEnumType($key)){
                        $enumTypeClassName = FacadeHelper::isKeyEnumType($key);
                        $createdObj = FacadeHelper::getEnumType($enumTypeClassName, $val);
                        FacadeHelper::assignValue($currentObj, $key, $createdObj);
                    }
                    else
                    {
                      //If it is an object
                      if(is_object($val)){
                        if(!FacadeHelper::checkIfTheObjectIsAnInstanceOfTheClass($key,$val)){
                          throw new \Exception("The assigned object is not an instance of required object:{" . $key . "}.");
                        }
                      }
                      FacadeHelper::assignValue($currentObj, $key, $val);
                    }
                }
            }
            return $currentObj;
         }else{
            //The key can't be construct with An App Object
            if($throwException)
            {
              throw new \Exception("The name value:{" . $classNameOrKeyName . "} can't be used to construct an Intuit Entity Object. Please check the name and try again.");
            }else{
              return NULL;
            }
         }
     }
   }

   /**
    * A helper to check if a key is a complex type of an Object
    * @param  $key, $complexList = NULL
    * @return true
    *             If the key is found in the complex type
    *         false
    *             If the key is not found in the complex type
    */
   private static function isKeyInComplexList($key, $complexList = NULL){
     if(isset($complexList)) {$ObjectMap = $complexList;}
     else {$ObjectMap = FacadeClassMapper::classMethodToList();}

     foreach($ObjectMap as $objectMethodName => $entityType)
     {
         if(in_array($key, $entityType)){
            return $objectMethodName;
         }
     }
     return false;
   }

   /**
    * Set the Target Object with the Val converted in Object
    */
   private static function setKeyInComplexList($objectMethodName, $targetObject, $key, $val){
     $reflectionClassOfTargetObject = new \ReflectionClass($targetObject);
     $setObject = FacadeHelper::getComplexListObject($objectMethodName, $key, $val);
     $property = $reflectionClassOfTargetObject->getProperty($key);
     if($property instanceof \ReflectionProperty){
         $property->setValue($targetObject,$setObject);
         return true;
     }else{
       throw new \Exception("No Reflection Property Found.");
     }

   }

   /**
    * Set the Target Object with the Val converted in Object
    */
   private static function getComplexListObject($objectMethodName, $key, $val){
     //call helper method with method name as $objectmethodName
     $correspondingClassMethodOfFacadeHelper = FacadeHelper::getClassMethod(FacadeConstants::FACADE_HELPER_CLASS_NAME, $objectMethodName);
     if(!isset($correspondingClassMethodOfFacadeHelper)) throw new \Exception("The given Method " . $objectMethodName . " can't find at FacadeHelper.php class");
     $setObject = $correspondingClassMethodOfFacadeHelper->invoke(null, $val, $key);
     return $setObject;
   }

   public static function isKeyEnumType($key){
      $enumSupportArray = FacadeClassMapper::EnumTypeMatch();
      foreach($enumSupportArray as $k => $v){
          if(strcmp($key, $k) == 0){
             return FacadeHelper::decorateKeyWithNameSpaceAndPrefix($v);
          }
      }
      return false;
   }

   public static function getEnumType($clazz, $val){
      if(!isset($val)) throw new \Exception("Passed param for Enum can't be null.");
      if(class_exists($clazz)){
          if(is_object($val)){
              if($val instanceof $clazz){
                return $val;
              }else{
                throw new \Exception("The assigned obj to the enum class Type:{" . $clazz . "} is not matched.");
              }
          }
          $enumObj = new $clazz();
          //If $val is string
          if(is_array($val) && !empty($val)){
             $firstElementValue = reset($val);
          }else if(is_string($val) || is_numeric($val)){
             $firstElementValue = $val;
          }else{
             throw new \Exception("Internal Error. The Type of val:" . get_class($val) . " is not handled.");
          }
          $enumObj->value = $firstElementValue;
          return $enumObj;
      }else{
        throw new \Exception("The Enum Type is not found");
      }
   }

   /**
   * Find the Method by given parameter on FacadeHelper.php Class
   * @param The class name
   * @return \ReflectionMethod|null The method if found. If not found, return null.
   */
   public static function getClassMethod($className, $methodName){
          try
          {
              $helperRefelctionMethod = new \ReflectionMethod($className, $methodName);
              return $helperRefelctionMethod;
          } catch(\Exception $e){
               return null;
          }
   }

   //----------------------------------------- Complex Type Methods -------------------------------------------
   /**
    * Construct an IPPReferenceType based on passed Array or String.
    * If it is passed as an array, handle it.
    * If it is passed as a String. Construct an array and put the String on the value
    * If it is passed as an obj, compare it with IPPReferenceType and return the object
    * @param $data
    *       It can either be an array or a String, or obj
    */
   public static function getIPPReferenceTypeBasedOnArray($data){
      $trimedDataArray = FacadeHelper::trimSpacesForArrayKeys($data);
      if(is_object($trimedDataArray)){
           if($trimedDataArray instanceof IPPReferenceType){
               return $trimedDataArray;
           }else{
               throw new \Exception("The assigned obj to IPPReferenceType is not matched with IPPReferenceType.");
           }
      }
      //THe ReferenceDataType should only contain at most Two elements
      if(is_array($trimedDataArray)){
        if(sizeof($trimedDataArray) >= 3){
          throw new \Exception("Trying to construct IPPReferenceType based on Array. The array should contain at most two fields. name and value");
        }

        $IPPReferenceType = new IPPReferenceType();
        if(isset($trimedDataArray['value']) || isset($trimedDataArray['Value']) ) {
            $val = isset($trimedDataArray['value']) ? $trimedDataArray['value'] : $trimedDataArray['Value'];
            $IPPReferenceType->value = $val;
        }else{
            throw new \Exception("Passed array has no key for 'Value' when contructing an ReferenceType");
        }

        if(isset($trimedDataArray['name']) || isset($trimedDataArray['Name']) ){
           $nam = isset($trimedDataArray['name']) ? $trimedDataArray['name'] : $trimedDataArray['Name'];
           $IPPReferenceType->name = $nam;
         }
        return $IPPReferenceType;
      }else if(is_numeric($trimedDataArray) || is_string($trimedDataArray)){
        $IPPReferenceType = new IPPReferenceType();
        $IPPReferenceType->value = $trimedDataArray;
        return $IPPReferenceType;
      }else{
        throw new \Exception("Can't convert Passed Parameter to IPPReferenceType.");
      }
  }

  /**
   * If passed params is array, the first element of Array is used in IPPid.
   * If passed params is not an array, the the value is used for Ippid.
   * If passed params is an obj, the the value is simply returned.
   * @param $data
   *       It can either be an array or a numeric representation
   */
  public static function getIPPId($data){
    //Convert an IPPId based on the Data
    if(!isset($data)) throw new \Exception("Passed param for IPPid can't be null");
    if(is_object($data)){
         if($data instanceof IPPid){
             return $data;
         }else{
             throw new \Exception("The assigned obj to IPPid is not matched with IPPid.");
         }
    }
    if(is_array($data)){
       $firstElementValue = reset($data);
    }else if(is_numeric($data)){
       $firstElementValue = $data;
    }else{
      throw new \Exception("Passed param for Ippid has either be an array or numeric value.");
    }

    $_id = new IPPid();
    $_id->value = $firstElementValue;
    return $_id;
  }

  /**
   * Override the content from Object B to Object A
   * Don't use array_merge here. As the NUll Value will be overriden as well
   * Remove the !empty check. For integer or string that is 0, it needs to be passed and set.
   */
  public static function mergeObj($objA, $objB){
      if(get_class($objA) != get_class($objB)) throw new \Exception("Can't assign object value to a different type.");
      $property_fields = get_object_vars($objA);
      foreach ($property_fields as $propertyName => $val){
          $BsValue = $objB->$propertyName;
          if(isset($BsValue)){
               $objA->$propertyName = $BsValue;
          }
      }
      return $objA;
  }

  public static function cloneObj($obj){
      $clazz = get_class($obj);
      if(class_exists($clazz)){
          $clonedObj = new $clazz();
          $clonedObj = FacadeHelper::mergeObj($clonedObj, $obj);
          return $clonedObj;
      }else{
         throw new \Exception("The passed object:{" . $clazz . "} does not exist.");
      }
  }

  //----------------------------------- Common Helper Methods ----------

   public static function trimSpacesForArrayKeys($data){
       if(!isset($data) || empty($data)) return $data;
       if(is_array($data))
       {
           if(FacadeHelper::isArrayOfObj($data)){
                return $data;
           }else{
             $trimedKeys = array_map('trim', array_keys($data));
             $trimedResult = array_combine($trimedKeys, $data);
             return $trimedResult;
           }
       }else{
           if(is_object($data)){
               return $data;
           }else{
             return trim($data);
           }
       }
   }

   public static function isRecurrsiveArray(array $array){
      foreach($array as $key => $val){
          if(!is_array($val)){
                return false;
          }
      }
      return true;
   }

   /**
    * Test if an array is an associate array
    *
    * @param array $arr
    * @return true if $arr is an associative array
    */
   public static function isAssociateArray(array $arr)
   {
       if(!empty($arr)){
           foreach ($arr as $k => $v) {
               if (is_int($k)) {
                   return false;
               }
           }
           return true;
       }
       return false;
   }

   private static function decorateKeyWithNameSpaceAndPrefix($key){
      $list = FacadeClassMapper::OtherAntiPatternNameEntity();
      foreach($list as $k => $v){
          if(strcmp($k, $key) == 0){
              $key = $v;
              break;
          }
      }
      return FacadeHelper::simpleAppendClassNameSpace($key);
   }

   public static function simpleAppendClassNameSpace($key){
      return CoreConstants::NAMEPSACE_DATA_PREFIX . CoreConstants::PHP_CLASS_PREFIX . $key;
   }

   /**
    *
    * Given the class name, find the field for the key.
    *
    * @param $object
    *      The class object that we are going to call
    * @param $key
    *      The name of the field
    * @param $value
    *      The value to be assigned for the field
    */
   private static function assignValue($targetObject, $key, $value){
     //Reflection Class
     $reflectionClassOfTargetObject = new \ReflectionClass($targetObject);
     if(strcasecmp($key, "JournalEntryEntity") == 0){
         $key = "Entity";
     }else if(strcasecmp($key, "JournalEntryType") == 0){
         $key = "Type";
     }
     $property = $reflectionClassOfTargetObject->getProperty($key);
     if($property instanceof \ReflectionProperty){
        $value = FacadeHelper::convertValueTypeToAppropriateString($value);
        $property->setValue($targetObject,$value);
     }else{
       throw new \Exception("No Reflection Property Found.");
     }
   }

   private static function convertValueTypeToAppropriateString($value){
      if(is_bool($value)){
        $converted_val = ($value) ? 'true' : 'false';
        return $converted_val;
      } else if(is_numeric($value) || is_string($value)){
        return $value;
      } else if($value instanceof \DateTime){
          $result = $value->format('Y-m-d');
          if($result){
            return $result;
          }else{
            throw new SdkException("Cannot Convert the DateTime Object in the Line to String format using format function. Use String value instead.");
          }
      } else {
         return $value;
      }
   }

   private static function checkIfTheObjectIsAnInstanceOfTheClass($className, $object){
      if($object instanceof $className){
          return true;
      } else if($object instanceof \DateTime){
          return true;
      }
      else{
          $className = FacadeHelper::decorateKeyWithNameSpaceAndPrefix($className);
          if($object instanceof $className){
              return true;
          }
      }
      return false;
   }

   private static function isArrayOfObj($data){
       if(is_array($data)){
           foreach($data as $dataMemeber){
               if(!is_object($dataMemeber)){
                   return false;
               }
           }
           return true;
       }

       return false;
   }

}


<?php

namespace QuickBooksOnline\API\Core\HttpClients;

use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Core\CoreConstants;

/**
 * Class CurlHttpClient
 *
 * A Http Client using PHP cURL extension to send HTTP/HTTPS request to QuickBooks Online
 * @package QuickBooksOnline
 *
 */
class CurlHttpClient implements HttpClientInterface{

    /**
    * @var BaseCurl The basecURL instance will be used for performing Http/Https client request.
    */
    private $basecURL = null;

    /**
    * @var IntuitResponse | False The parsed response from curl client to Intuit customized response
    */
    private $intuitResponse = false;

    /**
     * The constructor for constructing the cURL http client for making API calls
     * @param BaseCurl $curl    A predefined BaseCurl instance to be used in this client
     */
    public function __construct(BaseCurl $curl = null)
    {
        if(isset($curl)){
              $this->basecURL = $curl;
        }else{
              $this->basecURL = new BaseCurl();
        }
    }

    /**
     * @inheritdoc
     */
    public function makeAPICall($url, $method, array $headers, $body, $timeOut, $verifySSL){
        $this->clearResponse();
        $this->prepareRequest($url, $method, $headers, $body, $timeOut, $verifySSL);
        $rawResponse = $this->executeRequest();
        $this->handleErrors();
        $this->setIntuitResponse($rawResponse);
        $this->closeConnection();
        return $this->getLastResponse();
    }

    /**
     * @inheritdoc
     */
    public function prepareRequest($url, $method, array $headers, $body, $timeOut, $verifySSL){
        //Set basic Curl Info
        $curl_opt = [
            CURLOPT_URL => $url,
            CURLOPT_CUSTOMREQUEST => trim($method),
            //Set return transfer to true so the curl_exec will return the result
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $this->getHeaders($headers),
            //10 seconds is allowed to make the connection to the server
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => isset($timeOut) ? $timeOut : 100,
            CURLOPT_RETURNTRANSFER => true,
            //When CURLOPT_HEADER is set to 0 the only effect is that header info from the response is excluded from the output.
            //So if you don't need it that's a few less KBs that curl will return to you.
            //In our case, header is required
            CURLOPT_HEADER => true
        ];

        if ($method !== "GET" && isset($body)) {
          $curl_opt[CURLOPT_POSTFIELDS] = $body;
        }

        //Set SSL. Only Enabled for OAuth 2 Request
        $this->setSSL($curl_opt, $verifySSL);

        $this->intializeCurl();
        $this->basecURL->setupCurlOptArray($curl_opt);
    }

    /**
     * Before making any API call, clear the stored response from previous request.
     */
    public function clearResponse(){
      $this->intuitResponse = false;
    }

    /**
     * Send a request and return the response
     * @return mixed TRUE on success or FALSE on failure. However, if the CURLOPT_RETURNTRANSFER
     */
    private function executeRequest(){
        return $this->basecURL->execute();
    }

    /**
     * The error during HTTP/HTTPS call. For example, the certificate expired. The server respond time out. It has nothing to do with the
     * status code returned from server.
     */
    private function handleErrors(){
        if($this->basecURL->errno() || $this->basecURL->error()){
           $errorMsg = $this->basecURL->error();
           $errorNumber = $this->basecURL->errno();
           throw new SdkException("cURL error during making API call. cURL Error Number:[" . $errorNumber . "] with error:[" . $errorMsg . "]");
        }
    }

    /**
     * @inheritdoc
     */
    public function setIntuitResponse($response){
        $headerSize = $this->basecURL->getInfo(CURLINFO_HEADER_SIZE);
        $rawHeaders = mb_substr($response, 0, $headerSize);
        $rawBody = mb_substr($response, $headerSize);
        $httpStatusCode = $this->basecURL->getInfo(CURLINFO_HTTP_CODE);
        $theIntuitResponse = new IntuitResponse($rawHeaders, $rawBody, $httpStatusCode, true);
        $this->intuitResponse = $theIntuitResponse;
    }

    /**
     * Check if the cURL instance exists. If not or closed, create a new BaseCurl instance for this Http client
     */
    private function intializeCurl(){
        if($this->basecURL->isCurlSet()){ return; }
        else {$this->basecURL->init();}
    }

    /**
    * Get headers for the QuciKbooks Online Response
    */
    private function getHeaders($headers){
      if(!isset($headers) || empty($headers)){
          throw new SdkException("Error. The headers set for cURL are either NULL or Empty");
      }else{
          $convertedHeaders = $this->convertHeaderArrayToHeaders($headers);
          return $convertedHeaders;
      }
    }

    /**
     * Set the SSL certifcate path and corresponding varaibles for cURL
     */
    private function setSSL(&$curl_opt, $verifySSL){
      $tlsVersion = $this->basecURL->versionOfTLS();
      $versions = ['TLS 1.2', 'TLS 1.3'];
      if(! in_array($tlsVersion, $versions)){
          throw new SdkException("Error. Checking TLS 1.2/1.3 version failed. Please make sure your PHP cURL supports TSL 1.2/1.3");
      }
      if($verifySSL){
          $curl_opt[CURLOPT_SSL_VERIFYPEER] = true;
          $curl_opt[CURLOPT_SSL_VERIFYHOST] = 2;
          //based on spec, if TSL 1.2 is supported, it will use the TLS 1.2 or latest version by default
          //$curl_opt[CURLOPT_SSLVERSION] = 6;
          $curl_opt[CURLOPT_CAINFO] = CoreConstants::getCertPath(); //Pem certification Key Path
      }
    }

    /**
     * Convert an Array to Curl Headers
     * @param array $headerArray The request headers
     * @return array Curl Headers
     */
    public function convertHeaderArrayToHeaders(array $headerArray){
         $headers = array();
         foreach($headerArray as $k => $v){
              $headers[] = $k . ":" . $v;
         }
         return $headers;
    }

    /**
     * close the connection of current http client
     */
    private function closeConnection(){
        $this->basecURL->close();
    }

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

}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard\SniffRunner\File;

use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File as BaseFile;
use PHP_CodeSniffer\Fixer;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\AssignmentInConditionSniff;
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\PropertyDeclarationSniff;
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\MethodDeclarationSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\PHP\CommentedOutCodeSniff;
use PHP_CodeSniffer\Util\Common;
use Symplify\EasyCodingStandard\Application\AppliedCheckersCollector;
use Symplify\EasyCodingStandard\Console\Style\EasyCodingStandardStyle;
use Symplify\EasyCodingStandard\Error\ErrorAndDiffCollector;
use Symplify\EasyCodingStandard\Skipper;
use Symplify\EasyCodingStandard\SniffRunner\Exception\File\NotImplementedException;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;

final class File extends BaseFile
{
    /**
     * @var string
     */
    public $tokenizerType = 'PHP';

    /**
     * @var Fixer
     */
    public $fixer;

    /**
     * @var string|null
     */
    private $activeSniffClass;

    /**
     * @var string|null
     */
    private $previousActiveSniffClass;

    /**
     * Explicit list for now.
     *
     * @var string[]
     */
    private $reportWarningsSniffs = [
        CommentedOutCodeSniff::class,
        AssignmentInConditionSniff::class,
        PropertyDeclarationSniff::class,
        MethodDeclarationSniff::class,
    ];

    /**
     * @var Sniff[][]
     */
    private $tokenListeners = [];

    /**
     * @var ErrorAndDiffCollector
     */
    private $errorAndDiffCollector;

    /**
     * @var Skipper
     */
    private $skipper;

    /**
     * @var AppliedCheckersCollector
     */
    private $appliedCheckersCollector;

    /**
     * @var EasyCodingStandardStyle
     */
    private $easyCodingStandardStyle;

    /**
     * @var SmartFileInfo
     */
    private $fileInfo;

    public function __construct(
        string $path,
        string $content,
        Fixer $fixer,
        ErrorAndDiffCollector $errorAndDiffCollector,
        Skipper $skipper,
        AppliedCheckersCollector $appliedCheckersCollector,
        EasyCodingStandardStyle $easyCodingStandardStyle
    ) {
        $this->path = $path;
        $this->content = $content;
        $this->fixer = $fixer;
        $this->errorAndDiffCollector = $errorAndDiffCollector;

        $this->eolChar = Common::detectLineEndings($content);
        $this->skipper = $skipper;
        $this->appliedCheckersCollector = $appliedCheckersCollector;

        // compat
        if (! defined('PHP_CODESNIFFER_CBF')) {
            define('PHP_CODESNIFFER_CBF', false);
        }

        // parent required
        $this->config = new Config([], false);
        $this->config->tabWidth = 4;
        $this->config->annotations = false;
        $this->config->encoding = 'UTF-8';
        $this->easyCodingStandardStyle = $easyCodingStandardStyle;
    }

    /**
     * Mimics @see https://github.com/squizlabs/PHP_CodeSniffer/blob/e4da24f399d71d1077f93114a72e305286020415/src/Files/File.php#L310
     */
    public function process(): void
    {
        $this->parse();
        $this->fixer->startFile($this);

        foreach ($this->tokens as $stackPtr => $token) {
            if (isset($this->tokenListeners[$token['code']]) === false) {
                continue;
            }

            foreach ($this->tokenListeners[$token['code']] as $sniff) {
                if ($this->skipper->shouldSkipCheckerAndFile($sniff, $this->fileInfo)) {
                    continue;
                }

                $this->reportActiveSniffClass($sniff);

                $sniff->process($this, $stackPtr);
            }
        }

        $this->fixedCount += $this->fixer->getFixCount();
    }

    public function getErrorCount(): int
    {
        throw new NotImplementedException(sprintf(
            'Method "%s" is not needed to be public. Use "%s" service.',
            __METHOD__,
            ErrorAndDiffCollector::class
        ));
    }

    /**
     * @return mixed[]
     */
    public function getErrors(): array
    {
        throw new NotImplementedException(sprintf(
            'Method "%s" is not needed to be public. Use "%s" service.',
            __METHOD__,
            ErrorAndDiffCollector::class
        ));
    }

    /**
     * Delegate to addError().
     *
     * {@inheritdoc}
     */
    public function addFixableError($error, $stackPtr, $code, $data = [], $severity = 0): bool
    {
        $this->appliedCheckersCollector->addFileInfoAndChecker(
            $this->fileInfo,
            $this->resolveFullyQualifiedCode($code)
        );

        return ! $this->shouldSkipError($error, $code, $data);
    }

    /**
     * {@inheritdoc}
     */
    public function addError($error, $stackPtr, $code, $data = [], $severity = 0, $fixable = false): bool
    {
        if ($this->shouldSkipError($error, $code, $data)) {
            return false;
        }

        return parent::addError($error, $stackPtr, $code, $data, $severity, $fixable);
    }

    /**
     * Allow only specific classes
     *
     * {@inheritdoc}
     */
    public function addWarning($warning, $stackPtr, $code, $data = [], $severity = 0, $fixable = false): bool
    {
        if (! $this->isSniffClassWarningAllowed($this->activeSniffClass)) {
            return false;
        }

        return $this->addError($warning, $stackPtr, $code, $data, $severity, $fixable);
    }

    /**
     * @param Sniff[][] $tokenListeners
     */
    public function processWithTokenListenersAndFileInfo(array $tokenListeners, SmartFileInfo $fileInfo): void
    {
        $this->tokenListeners = $tokenListeners;
        $this->fileInfo = $fileInfo;
        $this->process();
    }

    /**
     * Delegated from addError().
     *
     * {@inheritdoc}
     */
    protected function addMessage(
        $isError,
        $message,
        $line,
        $column,
        $sniffClassOrCode,
        $data,
        $severity,
        $isFixable = false
    ): bool {
        if (! $isError) { // skip warnings
            return false;
        }

        $message = count($data) ? vsprintf($message, $data) : $message;

        if ($isFixable === true) {
            return $isFixable;
        }

        // do not add non-fixable errors twice
        if ($this->fixer->loops > 0) {
            return false;
        }

        $this->errorAndDiffCollector->addErrorMessage(
            $this->fileInfo,
            $line,
            $message,
            $this->resolveFullyQualifiedCode($sniffClassOrCode)
        );

        return true;
    }

    private function reportActiveSniffClass(Sniff $sniff): void
    {
        // used in other places later
        $this->activeSniffClass = get_class($sniff);

        if (! $this->easyCodingStandardStyle->isDebug()) {
            return;
        }

        if ($this->previousActiveSniffClass === $this->activeSniffClass) {
            return;
        }

        $this->easyCodingStandardStyle->writeln($this->activeSniffClass);
        $this->previousActiveSniffClass = $this->activeSniffClass;
    }

    private function resolveFullyQualifiedCode(string $sniffClassOrCode): string
    {
        if (class_exists($sniffClassOrCode)) {
            return $sniffClassOrCode;
        }

        return $this->activeSniffClass . '.' . $sniffClassOrCode;
    }

    /**
     * @param string[] $data
     */
    private function shouldSkipError(string $error, string $code, array $data): bool
    {
        $fullyQualifiedCode = $this->resolveFullyQualifiedCode($code);

        if ($this->skipper->shouldSkipCodeAndFile($fullyQualifiedCode, $this->fileInfo)) {
            return true;
        }

        $message = count($data) ? vsprintf($error, $data) : $error;

        return $this->skipper->shouldSkipMessageAndFile($message, $this->fileInfo);
    }

    private function isSniffClassWarningAllowed(string $sniffClass): bool
    {
        foreach ($this->reportWarningsSniffs as $reportWarningsSniff) {
            if (is_a($sniffClass, $reportWarningsSniff, true)) {
                return true;
            }
        }

        return false;
    }
}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard\HttpKernel;

use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCsFixer\Fixer\FixerInterface;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symplify\EasyCodingStandard\ChangedFilesDetector\CompilerPass\AddSysGetTempDirParameterCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\ConflictingCheckersCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\CustomSourceProviderDefinitionCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\FixerWhitespaceConfigCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\RemoveExcludedCheckersCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\RemoveMutualCheckersCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\DelegatingLoaderFactory;
use Symplify\PackageBuilder\Contract\HttpKernel\ExtraConfigAwareKernelInterface;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutoBindParametersCompilerPass;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutoReturnFactoryCompilerPass;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireArrayParameterCompilerPass;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireInterfacesCompilerPass;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireSinglyImplementedCompilerPass;

final class EasyCodingStandardKernel extends Kernel implements ExtraConfigAwareKernelInterface
{
    /**
     * @var string[]
     */
    private $configs = [];

    public function registerContainerConfiguration(LoaderInterface $loader): void
    {
        $loader->load(__DIR__ . '/../../config/config.yaml');

        foreach ($this->configs as $config) {
            $loader->load($config);
        }
    }

    public function getCacheDir(): string
    {
        return sys_get_temp_dir() . '/easy_coding_standard';
    }

    public function getLogDir(): string
    {
        return sys_get_temp_dir() . '/easy_coding_standard_logs';
    }

    /**
     * @return BundleInterface[]
     */
    public function registerBundles(): iterable
    {
        return [];
    }

    /**
     * @param string[] $configs
     */
    public function setConfigs(array $configs): void
    {
        $this->configs = $configs;
    }

    /**
     * Order matters!
     */
    protected function build(ContainerBuilder $containerBuilder): void
    {
        // needs to be first, since it's adding new service definitions
        $containerBuilder->addCompilerPass(new AutoReturnFactoryCompilerPass());

        // cleanup
        $containerBuilder->addCompilerPass(new RemoveExcludedCheckersCompilerPass());
        $containerBuilder->addCompilerPass(new RemoveMutualCheckersCompilerPass());

        // autowire
        $containerBuilder->addCompilerPass(new AutowireInterfacesCompilerPass([
            FixerInterface::class,
            Sniff::class,
        ]));
        $containerBuilder->addCompilerPass(new AutowireArrayParameterCompilerPass());

        // exceptions
        $containerBuilder->addCompilerPass(new ConflictingCheckersCompilerPass());

        // parameters
        $containerBuilder->addCompilerPass(new AddSysGetTempDirParameterCompilerPass());
        $containerBuilder->addCompilerPass(new AutoBindParametersCompilerPass());

        // method calls
        $containerBuilder->addCompilerPass(new FixerWhitespaceConfigCompilerPass());
        $containerBuilder->addCompilerPass(new CustomSourceProviderDefinitionCompilerPass());
        $containerBuilder->addCompilerPass(new AutowireSinglyImplementedCompilerPass());
    }

    /**
     * @param ContainerInterface|ContainerBuilder $container
     */
    protected function getContainerLoader(ContainerInterface $container): DelegatingLoader
    {
        return (new DelegatingLoaderFactory())->createFromContainerBuilderAndKernel($container, $this);
    }
}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard;

use Nette\Utils\Strings;
use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCsFixer\Fixer\FixerInterface;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;

final class Skipper
{
    /**
     * @var string[][]
     */
    private $skipped = [];

    /**
     * @var string[]
     */
    private $skippedCodes = [];

    /**
     * @var string[]
     */
    private $skippedMessages = [];

    /**
     * @var string[]
     */
    private $excludedFiles = [];

    /**
     * @param mixed[] $skip
     * @param mixed[] $excludeFiles
     */
    public function __construct(array $skip, array $excludeFiles)
    {
        $this->categorizeSkipSettings($skip);
        $this->excludedFiles = $excludeFiles;
    }

    public function shouldSkipCodeAndFile(string $code, SmartFileInfo $smartFileInfo): bool
    {
        return $this->shouldSkipMatchingRuleAndFile($this->skippedCodes, $code, $smartFileInfo);
    }

    public function shouldSkipMessageAndFile(string $message, SmartFileInfo $smartFileInfo): bool
    {
        return $this->shouldSkipMatchingRuleAndFile($this->skippedMessages, $message, $smartFileInfo);
    }

    /**
     * @param FixerInterface|Sniff|string $checker
     */
    public function shouldSkipCheckerAndFile($checker, SmartFileInfo $smartFileInfo): bool
    {
        foreach ($this->skipped as $skippedClass => $skippedFiles) {
            if (! is_a($checker, $skippedClass, true)) {
                continue;
            }

            // skip everywhere
            if (! is_array($skippedFiles)) {
                return true;
            }

            if ($this->doesFileMatchSkippedFiles($smartFileInfo, $skippedFiles)) {
                return true;
            }
        }

        return false;
    }

    public function shouldSkipFileInfo(SmartFileInfo $smartFileInfo): bool
    {
        foreach ($this->excludedFiles as $excludedFile) {
            if ($this->fileMatchesPattern($smartFileInfo, $excludedFile)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param mixed[] $skipped
     */
    private function categorizeSkipSettings(array $skipped): void
    {
        foreach ($skipped as $key => $settings) {
            if (class_exists($key)) {
                $this->skipped[$key] = $settings;
            } elseif (class_exists((string) Strings::before($key, '.'))) {
                $this->skippedCodes[$key] = $settings;
            } else {
                $this->skippedMessages[$key] = $settings;
            }
        }
    }

    /**
     * @param string[]|null[] $rules
     */
    private function shouldSkipMatchingRuleAndFile(array $rules, string $key, SmartFileInfo $smartFileInfo): bool
    {
        if (! array_key_exists($key, $rules)) {
            return false;
        }

        // skip regardless the path
        if ($rules[$key] === null) {
            return true;
        }

        return $this->doesFileMatchSkippedFiles($smartFileInfo, (array) $rules[$key]);
    }

    /**
     * @param string[] $skippedFiles
     */
    private function doesFileMatchSkippedFiles(SmartFileInfo $smartFileInfo, array $skippedFiles): bool
    {
        foreach ($skippedFiles as $skippedFile) {
            if ($this->fileMatchesPattern($smartFileInfo, $skippedFile)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Supports both relative and absolute $file path.
     * They differ for PHP-CS-Fixer and PHP_CodeSniffer.
     */
    private function fileMatchesPattern(SmartFileInfo $smartFileInfo, string $ignoredPath): bool
    {
        $ignoredPath = $this->normalizeForFnmatch($ignoredPath);

        return $smartFileInfo->endsWith($ignoredPath) || $smartFileInfo->fnmatches($ignoredPath);
    }

    /**
     * "value*" → "*value*"
     * "*value" → "*value*"
     */
    private function normalizeForFnmatch(string $path): string
    {
        // ends with *
        if (Strings::match($path, '#^[^*](.*?)\*$#')) {
            return '*' . $path;
        }
        // starts with *
        if (Strings::match($path, '#^\*(.*?)[^*]$#')) {
            return $path . '*';
        }
        return $path;
    }
}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration;

use Jean85\PrettyVersions;
use Symfony\Component\Console\Input\InputInterface;
use Symplify\EasyCodingStandard\Console\Output\JsonOutputFormatter;
use Symplify\EasyCodingStandard\Exception\Configuration\SourceNotFoundException;
use Symplify\PackageBuilder\Configuration\ConfigFileFinder;

final class Configuration
{
    /**
     * @var bool
     */
    private $isFixer = false;

    /**
     * @var bool
     */
    private $shouldClearCache = false;

    /**
     * @var bool
     */
    private $showProgressBar = true;

    /**
     * @var bool
     */
    private $showErrorTable = true;

    /**
     * @var string|null
     */
    private $configFilePath;

    /**
     * @var string[]
     */
    private $sources = [];

    /**
     * Needs to run in the start of the life cycle, since the rest of workflow uses it.
     */
    public function resolveFromInput(InputInterface $input): void
    {
        /** @var string[] $sources */
        $sources = $input->getArgument(Option::SOURCE);
        $this->setSources($sources);
        $this->isFixer = (bool) $input->getOption(Option::FIX);
        $this->shouldClearCache = (bool) $input->getOption(Option::CLEAR_CACHE);
        $this->showProgressBar = $this->canShowProgressBar($input);
        $this->showErrorTable = ! (bool) $input->getOption(Option::NO_ERROR_TABLE);
    }

    /**
     * @param mixed[] $options
     */
    public function resolveFromArray(array $options): void
    {
        $this->isFixer = (bool) $options['isFixer'];
    }

    /**
     * @return string[]
     */
    public function getSources(): array
    {
        return $this->sources;
    }

    public function isFixer(): bool
    {
        return $this->isFixer;
    }

    public function shouldClearCache(): bool
    {
        return $this->shouldClearCache;
    }

    public function showProgressBar(): bool
    {
        return $this->showProgressBar;
    }

    public function showErrorTable(): bool
    {
        return $this->showErrorTable;
    }

    public function setConfigFilePathFromInput(InputInterface $input): void
    {
        if ($input->getParameterOption('--config')) {
            $this->configFilePath = $input->getParameterOption('--config');
            return;
        }

        $this->configFilePath = ConfigFileFinder::provide('ecs');
    }

    public function getConfigFilePath(): ?string
    {
        return $this->configFilePath;
    }

    public function getPrettyVersion(): string
    {
        $version = PrettyVersions::getVersion('symplify/easy-coding-standard');

        return $version->getPrettyVersion();
    }

    /**
     * @param string[] $sources
     */
    private function setSources(array $sources): void
    {
        $this->ensureSourcesExists($sources);
        $this->sources = $this->normalizeSources($sources);
    }

    private function canShowProgressBar(InputInterface $input): bool
    {
        $notJsonOutput = $input->getOption(Option::OUTPUT_FORMAT_OPTION) !== JsonOutputFormatter::NAME;
        $progressBarEnabled = ! (bool) $input->getOption(Option::NO_PROGRESS_BAR);

        return $notJsonOutput && $progressBarEnabled;
    }

    /**
     * @param string[] $sources
     */
    private function ensureSourcesExists(array $sources): void
    {
        foreach ($sources as $source) {
            if (file_exists($source)) {
                continue;
            }

            throw new SourceNotFoundException(sprintf('Source "%s" does not exist.', $source));
        }
    }

    /**
     * @param string[] $sources
     * @return string[]
     */
    private function normalizeSources(array $sources): array
    {
        foreach ($sources as $key => $value) {
            $sources[$key] = rtrim($value, DIRECTORY_SEPARATOR);
        }

        return $sources;
    }
}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard\ChangedFilesDetector;

use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symplify\PackageBuilder\Configuration\ConfigFileFinder;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;

final class ChangedFilesDetector
{
    /**
     * @var string
     */
    public const CHANGED_FILES_CACHE_TAG = 'changed_files';

    /**
     * @var string
     */
    private const CONFIGURATION_HASH_KEY = 'configuration_hash';

    /**
     * @var FileHashComputer
     */
    private $fileHashComputer;

    /**
     * @var TagAwareAdapterInterface
     */
    private $tagAwareAdapter;

    public function __construct(FileHashComputer $fileHashComputer, TagAwareAdapterInterface $tagAwareAdapter)
    {
        $this->fileHashComputer = $fileHashComputer;
        $this->tagAwareAdapter = $tagAwareAdapter;

        $configurationFile = ConfigFileFinder::provide('ecs');
        if ($configurationFile !== null && is_file($configurationFile)) {
            $this->storeConfigurationDataHash($this->fileHashComputer->compute($configurationFile));
        }
    }

    /**
     * For tests
     */
    public function changeConfigurationFile(string $configurationFile): void
    {
        $this->storeConfigurationDataHash($this->fileHashComputer->compute($configurationFile));
    }

    public function addFileInfo(SmartFileInfo $smartFileInfo): void
    {
        /** @var CacheItem $item */
        $item = $this->tagAwareAdapter->getItem($this->fileInfoToKey($smartFileInfo));
        $item->set($this->fileHashComputer->compute($smartFileInfo->getRealPath()));
        $item->tag(self::CHANGED_FILES_CACHE_TAG);
        $this->tagAwareAdapter->save($item);
    }

    public function invalidateFileInfo(SmartFileInfo $smartFileInfo): void
    {
        $this->tagAwareAdapter->deleteItem($this->fileInfoToKey($smartFileInfo));
    }

    public function hasFileInfoChanged(SmartFileInfo $smartFileInfo): bool
    {
        $newFileHash = $this->fileHashComputer->compute($smartFileInfo->getRealPath());

        $cacheItem = $this->tagAwareAdapter->getItem($this->fileInfoToKey($smartFileInfo));
        $oldFileHash = $cacheItem->get();

        if ($newFileHash !== $oldFileHash) {
            return true;
        }

        return false;
    }

    public function clearCache(): void
    {
        // clear cache only for changed files group
        $this->tagAwareAdapter->invalidateTags([self::CHANGED_FILES_CACHE_TAG]);
    }

    private function storeConfigurationDataHash(string $configurationHash): void
    {
        $this->invalidateCacheIfConfigurationChanged($configurationHash);

        $cacheItem = $this->tagAwareAdapter->getItem(self::CONFIGURATION_HASH_KEY);
        $cacheItem->set($configurationHash);
        $this->tagAwareAdapter->save($cacheItem);
    }

    private function fileInfoToKey(SmartFileInfo $smartFileInfo): string
    {
        return sha1($smartFileInfo->getRealPath());
    }

    private function invalidateCacheIfConfigurationChanged(string $configurationHash): void
    {
        $cacheItem = $this->tagAwareAdapter->getItem(self::CONFIGURATION_HASH_KEY);

        $oldConfigurationHash = $cacheItem->get();
        if ($configurationHash !== $oldConfigurationHash) {
            $this->clearCache();
        }
    }
}


<?php declare(strict_types=1);

namespace Symplify\EasyCodingStandard\DependencyInjection;

use Symfony\Component\Config\FileLocator as SimpleFileLocator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\GlobFileLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Config\FileLocator;
use Symfony\Component\HttpKernel\KernelInterface;
use Symplify\EasyCodingStandard\Yaml\FileLoader\CheckerTolerantYamlFileLoader;

final class DelegatingLoaderFactory
{
    public function createFromContainerBuilderAndKernel(
        ContainerBuilder $containerBuilder,
        KernelInterface $kernel
    ): DelegatingLoader {
        return $this->createFromContainerBuilderAndFileLocator($containerBuilder, new FileLocator($kernel));
    }

    /**
     * For tests
     */
    public function createContainerBuilderAndConfig(
        ContainerBuilder $containerBuilder,
        string $config
    ): DelegatingLoader {
        $fileLocator = new SimpleFileLocator(dirname($config));

        return $this->createFromContainerBuilderAndFileLocator($containerBuilder, $fileLocator);
    }

    private function createFromContainerBuilderAndFileLocator(
        ContainerBuilder $containerBuilder,
        SimpleFileLocator $simpleFileLocator
    ): DelegatingLoader {
        $loaderResolver = new LoaderResolver([
            new GlobFileLoader($simpleFileLocator),
            new CheckerTolerantYamlFileLoader($containerBuilder, $simpleFileLocator),
        ]);

        return new DelegatingLoader($loaderResolver);
    }
}


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTaggableTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        $connection = config('taggable.connection');

        if (!Schema::connection($connection)->hasTable('taggable_tags')) {
            Schema::connection($connection)->create('taggable_tags', function(Blueprint $table) {
                $table->increments('tag_id');
                $table->string('name');
                $table->string('normalized');
                $table->timestamps();

                $table->index('normalized');
            });
        }

        if (!Schema::connection($connection)->hasTable('taggable_taggables')) {
            Schema::connection($connection)->create('taggable_taggables', function(Blueprint $table) {
                $table->unsignedInteger('tag_id');
                $table->unsignedInteger('taggable_id');
                $table->string('taggable_type');
                $table->timestamps();

                $table->index(['tag_id', 'taggable_id'], 'i_taggable_fwd');
                $table->index(['taggable_id', 'tag_id'], 'i_taggable_rev');
                $table->index('taggable_type', 'i_taggable_type');
            });
        }
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        $connection = config('taggable.connection');

        if (Schema::connection($connection)->hasTable('taggable_tags')) {
            Schema::connection($connection)->drop('taggable_tags');
        }

        if (Schema::connection($connection)->hasTable('taggable_taggables')) {
            Schema::connection($connection)->drop('taggable_taggables');
        }
    }
}


<?php namespace Cviebrock\EloquentTaggable\Models;

use Cviebrock\EloquentTaggable\Services\TagService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Arr;

/**
 * Class Tag
 */
class Tag extends Model
{

    /**
     * @inheritdoc
     */
    protected $table = 'taggable_tags';

    /**
     * @inheritdoc
     */
    protected $primaryKey = 'tag_id';

    /**
     * @inheritdoc
     */
    protected $fillable = [
        'name',
        'normalized',
    ];

    /**
     * @inheritdoc
     */
    public function __construct(array $attributes = [])
    {
        if ($connection = config('taggable.connection')) {
            $this->setConnection($connection);
        }

        parent::__construct($attributes);
    }

    /**
     * Set the name attribute on the model.
     *
     * @param string $value
     */
    public function setNameAttribute($value)
    {
        $value = trim($value);
        $this->attributes['name'] = $value;
        $this->attributes['normalized'] = app(TagService::class)->normalize($value);
    }

    /**
     * Scope to find tags by name.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $value
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeByName(Builder $query, string $value): Builder
    {
        $normalized = app(TagService::class)->normalize($value);

        return $query->where('normalized', $normalized);
    }

    /**
     * @inheritdoc
     */
    public function getRelationValue($key)
    {
        // Check for regular relation first
        if ($return = parent::getRelationValue($key)) {
            return $return;
        }

        // Check if the relation is defined via configuration
        $relatedClass = Arr::get(config('taggable.taggedModels'), $key);

        if ($relatedClass) {
            $relation = $this->taggedModels($relatedClass);

            return tap($relation->getResults(), function($results) use ($key) {
                $this->setRelation($key, $results);
            });
        }
    }

    /**
     * Get the inverse of the polymorphic relation, via an attribute
     * defining the type of models to return.
     *
     * @param string $class
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
     */
    protected function taggedModels(string $class): MorphToMany
    {
        return $this->morphedByMany($class, 'taggable', 'taggable_taggables', 'tag_id');
    }

    /**
     * Find the tag with the given name.
     *
     * @param string $value
     *
     * @return static|null
     */
    public static function findByName(string $value)
    {
        return app(TagService::class)->find($value);
    }

    /**
     * @inheritdoc
     */
    public function __toString(): string
    {
        return (string) $this->getAttribute('name');
    }

}


<?php namespace Cviebrock\EloquentTaggable;

use Cviebrock\EloquentTaggable\Events\ModelTagged;
use Cviebrock\EloquentTaggable\Events\ModelUntagged;
use Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException;
use Cviebrock\EloquentTaggable\Models\Tag;
use Cviebrock\EloquentTaggable\Services\TagService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Query\JoinClause;

/**
 * Class Taggable
 *
 * @package Cviebrock\EloquentTaggable
 */
trait Taggable
{
    /**
     * Property to control sequence on alias
     *
     * @var int
     */
    private $taggableAliasSequence = 0;

    /**
     * Boot the trait.
     *
     * Listen for the deleting event of a model, then remove the relation between it and tags
     */
    protected static function bootTaggable(): void
    {
        static::deleting(function ($model) {
            if (!method_exists($model, 'runSoftDelete') || $model->isForceDeleting()) {
                $model->detag();
            }
        });
    }

    /**
     * Get a collection of all tags the model has.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
     */
    public function tags(): MorphToMany
    {
        $model = config('taggable.model');
        return $this->morphToMany($model, 'taggable', 'taggable_taggables', 'taggable_id', 'tag_id')
            ->withTimestamps();
    }

    /**
     * Attach one or multiple tags to the model.
     *
     * @param string|array $tags
     *
     * @return self
     */
    public function tag($tags): self
    {
        $tags = app(TagService::class)->buildTagArray($tags);

        foreach ($tags as $tagName) {
            $this->addOneTag($tagName);
            $this->load('tags');
        }

        event(new ModelTagged($this, $tags));

        return $this;
    }

    /**
     * Detach one or multiple tags from the model.
     *
     * @param string|array $tags
     *
     * @return self
     */
    public function untag($tags): self
    {
        $tags = app(TagService::class)->buildTagArray($tags);

        foreach ($tags as $tagName) {
            $this->removeOneTag($tagName);
        }

        event(new ModelUntagged($this, $tags));

        return $this->load('tags');
    }

    /**
     * Remove all tags from the model and assign the given ones.
     *
     * @param string|array $tags
     *
     * @return self
     */
    public function retag($tags): self
    {
        return $this->detag()->tag($tags);
    }

    /**
     * Remove all tags from the model.
     *
     * @return self
     */
    public function detag(): self
    {
        $this->tags()->sync([]);

        return $this->load('tags');
    }

    /**
     * Add one tag to the model.
     *
     * @param string $tagName
     */
    protected function addOneTag(string $tagName): void
    {
        /** @var Tag $tag */
        $tag = app(TagService::class)->findOrCreate($tagName);
        $tagKey = $tag->getKey();

        if (!$this->getAttribute('tags')->contains($tagKey)) {
            $this->tags()->attach($tagKey);
        }
    }

    /**
     * Remove one tag from the model
     *
     * @param string $tagName
     */
    protected function removeOneTag(string $tagName): void
    {
        $tag = app(TagService::class)->find($tagName);

        if ($tag) {
            $this->tags()->detach($tag);
        }
    }

    /**
     * Get all the tags of the model as a delimited string.
     *
     * @return string
     */
    public function getTagListAttribute(): string
    {
        return app(TagService::class)->makeTagList($this);
    }

    /**
     * Get all normalized tags of a model as a delimited string.
     *
     * @return string
     */
    public function getTagListNormalizedAttribute(): string
    {
        return app(TagService::class)->makeTagList($this, 'normalized');
    }

    /**
     * Get all tags of a model as an array.
     *
     * @return array
     */
    public function getTagArrayAttribute(): array
    {
        return app(TagService::class)->makeTagArray($this);
    }

    /**
     * Get all normalized tags of a model as an array.
     *
     * @return array
     */
    public function getTagArrayNormalizedAttribute(): array
    {
        return app(TagService::class)->makeTagArray($this, 'normalized');
    }

    /**
     * Determine if a given tag is attached to the model.
     *
     * @param Tag|string $tag
     *
     * @return bool
     */
    public function hasTag($tag): bool
    {
        if ($tag instanceof Tag) {
            $normalized = $tag->getAttribute('normalized');
        } else {
            $normalized = app(TagService::class)->normalize($tag);
        }

        return in_array($normalized, $this->getTagArrayNormalizedAttribute(), true);
    }

    /**
     * Query scope for models that have all of the given tags.
     *
     * @param Builder $query
     * @param array|string $tags
     *
     * @return Builder
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
     * @throws \ErrorException
     */
    public function scopeWithAllTags(Builder $query, $tags): Builder
    {
        /** @var TagService $service */
        $service = app(TagService::class);
        $normalized = $service->buildTagArrayNormalized($tags);

        // If there are no tags specified, then there
        // can't be any results so short-circuit
        if (count($normalized) === 0) {
            if (config('taggable.throwEmptyExceptions')) {
                throw new NoTagsSpecifiedException('Empty tag data passed to withAllTags scope.');
            }

            return $query->where(\DB::raw(1), 0);
        }

        $tagKeys = $service->getTagModelKeys($normalized);

        // If some of the tags specified don't exist, then there can't
        // be any models with all the tags, so so short-circuit
        if (count($tagKeys) !== count($normalized)) {
            return $query->where(\DB::raw(1), 0);
        }

        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);

        return $this->prepareTableJoin($query, 'inner', $alias)
            ->whereIn($morphTagKeyName, $tagKeys)
            ->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) = ?", [count($tagKeys)]);
    }

    /**
     * Query scope for models that have any of the given tags.
     *
     * @param Builder $query
     * @param array|string $tags
     *
     * @return Builder
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
     * @throws \ErrorException
     */
    public function scopeWithAnyTags(Builder $query, $tags): Builder
    {
        /** @var TagService $service */
        $service = app(TagService::class);
        $normalized = $service->buildTagArrayNormalized($tags);

        // If there are no tags specified, then there is
        // no filtering to be done so short-circuit
        if (count($normalized) === 0) {
            if (config('taggable.throwEmptyExceptions')) {
                throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
            }

            return $query->where(\DB::raw(1), 0);
        }

        $tagKeys = $service->getTagModelKeys($normalized);

        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);

        return $this->prepareTableJoin($query, 'inner', $alias)
            ->whereIn($morphTagKeyName, $tagKeys);
    }

    /**
     * Query scope for models that have any tag.
     *
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeIsTagged(Builder $query): Builder
    {
        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        return $this->prepareTableJoin($query, 'inner', $alias);
    }

    /**
     * Query scope for models that do not have all of the given tags.
     *
     * @param Builder $query
     * @param string|array $tags
     * @param bool $includeUntagged
     *
     * @return Builder
     * @throws \ErrorException
     */
    public function scopeWithoutAllTags(Builder $query, $tags, bool $includeUntagged = false): Builder
    {
        /** @var TagService $service */
        $service = app(TagService::class);
        $normalized = $service->buildTagArrayNormalized($tags);
        $tagKeys = $service->getTagModelKeys($normalized);
        $tagKeyList = implode(',', $tagKeys);

        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);

        $query = $this->prepareTableJoin($query, 'left', $alias)
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
                [count($tagKeys)]);

        if (!$includeUntagged) {
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
        }

        return $query;
    }

    /**
     * Query scope for models that do not have any of the given tags.
     *
     * @param Builder $query
     * @param string|array $tags
     * @param bool $includeUntagged
     *
     * @return Builder
     * @throws \ErrorException
     */
    public function scopeWithoutAnyTags(Builder $query, $tags, bool $includeUntagged = false): Builder
    {
        /** @var TagService $service */
        $service = app(TagService::class);
        $normalized = $service->buildTagArrayNormalized($tags);
        $tagKeys = $service->getTagModelKeys($normalized);
        $tagKeyList = implode(',', $tagKeys);

        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);

        $query = $this->prepareTableJoin($query, 'left', $alias)
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");

        if (!$includeUntagged) {
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
        }

        return $query;
    }

    /**
     * Query scope for models that does not have have any tags.
     *
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeIsNotTagged(Builder $query): Builder
    {
        $alias = $this->taggableCreateNewAlias(__FUNCTION__);
        $morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);

        return $this->prepareTableJoin($query, 'left', $alias)
            ->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
    }

    /**
     * @param Builder $query
     * @param string $joinType
     *
     * @return Builder
     */
    private function prepareTableJoin(Builder $query, string $joinType, string $alias): Builder
    {
        $morphTable = $this->tags()->getTable();
        $morphTableAlias = $morphTable.'_'.$alias;

        $modelKeyName = $this->getQualifiedKeyName();
        $morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);

        $morphTypeName = $morphTableAlias.'.'. $this->tags()->getMorphType();
        $morphClass = $this->tags()->getMorphClass();

        $closure = function(JoinClause $join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName, $morphClass) {
            $join->on($modelKeyName, $morphForeignKeyName)
                ->where($morphTypeName, $morphClass);
        };

        return $query
            ->select($this->getTable() . '.*')
            ->join($morphTable.' as '.$morphTableAlias, $closure, null, null, $joinType)
            ->groupBy($modelKeyName);
    }

    /**
     * Get a collection of all the tag models used for the called class.
     *
     * @return Collection
     */
    public static function allTagModels(): Collection
    {
        return app(TagService::class)->getAllTags(static::class);
    }

    /**
     * Get an array of all tags used for the called class.
     *
     * @return array
     */
    public static function allTags(): array
    {
        /** @var \Illuminate\Database\Eloquent\Collection $tags */
        $tags = static::allTagModels();

        return $tags->pluck('name')->sort()->all();
    }

    /**
     * Get all the tags used for the called class as a delimited string.
     *
     * @return string
     */
    public static function allTagsList(): string
    {
        return app(TagService::class)->joinList(static::allTags());
    }

    /**
     * Rename one the tags for the called class.
     *
     * @param string $oldTag
     * @param string $newTag
     *
     * @return int
     */
    public static function renameTag(string $oldTag, string $newTag): int
    {
        return app(TagService::class)->renameTags($oldTag, $newTag, static::class);
    }

    /**
     * Get the most popular tags for the called class.
     *
     * @param int $limit
     * @param int $minCount
     *
     * @return array
     */
    public static function popularTags(int $limit = null, int $minCount = 1): array
    {
        /** @var Collection $tags */
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);

        return $tags->pluck('taggable_count', 'name')->all();
    }

    /**
     * Get the most popular tags for the called class.
     *
     * @param int $limit
     * @param int $minCount
     *
     * @return array
     */
    public static function popularTagsNormalized(int $limit = null, int $minCount = 1): array
    {
        /** @var Collection $tags */
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);

        return $tags->pluck('taggable_count', 'normalized')->all();
    }

    /**
     * Returns the Related Pivot Key Name with the table alias.
     *
     * @param string $alias
     *
     * @return string
     */
    private function getQualifiedRelatedPivotKeyNameWithAlias(string $alias): string
    {
        $morph = $this->tags();

        return $morph->getTable() . '_' . $alias .
            '.' . $morph->getRelatedPivotKeyName();
    }

    /**
     * Returns the Foreign Pivot Key Name with the table alias.
     *
     * @param string $alias
     *
     * @return string
     */
    private function getQualifiedForeignPivotKeyNameWithAlias(string $alias): string
    {
        $morph = $this->tags();

        return $morph->getTable() . '_' . $alias .
            '.' . $morph->getForeignPivotKeyName();
    }

    /**
     * Create a new alias to use on scopes to be able to combine many scopes
     *
     * @param string $scope
     *
     * @return string
     */
    private function taggableCreateNewAlias(string $scope): string
    {
        $this->taggableAliasSequence++;
        $alias = strtolower($scope) . '_' . $this->taggableAliasSequence;

        return $alias;
    }
}