diff --git a/vendor/phpoffice/phpspreadsheet/LICENSE b/vendor/phpoffice/phpspreadsheet/LICENSE new file mode 100644 index 0000000..3ec5723 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 PhpSpreadsheet Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DMax.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DMax.php new file mode 100644 index 0000000..9c5c730 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DMax.php @@ -0,0 +1,46 @@ +format('m'); + $oYear = (int) $PHPDateObject->format('Y'); + + $adjustmentMonthsString = (string) $adjustmentMonths; + if ($adjustmentMonths > 0) { + $adjustmentMonthsString = '+' . $adjustmentMonths; + } + if ($adjustmentMonths != 0) { + $PHPDateObject->modify($adjustmentMonthsString . ' months'); + } + $nMonth = (int) $PHPDateObject->format('m'); + $nYear = (int) $PHPDateObject->format('Y'); + + $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12); + if ($monthDiff != $adjustmentMonths) { + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + } + + return $PHPDateObject; + } + + /** + * Help reduce perceived complexity of some tests. + * + * @param mixed $value + * @param mixed $altValue + */ + public static function replaceIfEmpty(&$value, $altValue): void + { + $value = $value ?: $altValue; + } + + /** + * Adjust year in ambiguous situations. + */ + public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void + { + if (!is_numeric($testVal1) || $testVal1 < 31) { + if (!is_numeric($testVal2) || $testVal2 < 12) { + if (is_numeric($testVal3) && $testVal3 < 12) { + $testVal3 += 2000; + } + } + } + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return new DateTime( + $dateArray['year'] + . '-' . $dateArray['month'] + . '-' . $dateArray['day'] + . ' ' . $dateArray['hour'] + . ':' . $dateArray['minute'] + . ':' . $dateArray['second'] + ); + } + $excelDateValue = + SharedDateHelper::formattedPHPToExcel( + $dateArray['year'], + $dateArray['month'], + $dateArray['day'], + $dateArray['hour'], + $dateArray['minute'], + $dateArray['second'] + ); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; + } + // RETURNDATE_UNIX_TIMESTAMP) + + return (int) SharedDateHelper::excelToTimestamp($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsFloat(float $excelDateValue) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $excelDateValue; + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) SharedDateHelper::excelToTimestamp($excelDateValue); + } + // RETURNDATE_PHP_DATETIME_OBJECT + + return SharedDateHelper::excelToDateTimeObject($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsObject(DateTime $PHPDateObject) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return $PHPDateObject; + } + if ($retType === Functions::RETURNDATE_EXCEL) { + return (float) SharedDateHelper::PHPToExcel($PHPDateObject); + } + // RETURNDATE_UNIX_TIMESTAMP + $stamp = SharedDateHelper::PHPToExcel($PHPDateObject); + $stamp = is_bool($stamp) ? ((int) $stamp) : $stamp; + + return (int) SharedDateHelper::excelToTimestamp($stamp); + } + + private static function baseDate(): int + { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + return 0; + } + if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) { + return 0; + } + + return 1; + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + */ + public static function nullFalseTrueToNumber(&$number, bool $allowBool = true): void + { + $number = Functions::flattenSingleValue($number); + $nullVal = self::baseDate(); + if ($number === null) { + $number = $nullVal; + } elseif ($allowBool && is_bool($number)) { + $number = $nullVal + (int) $number; + } + } + + /** + * Many functions accept null argument treated as 0. + * + * @param mixed $number + * + * @return float|int + */ + public static function validateNumericNull($number) + { + $number = Functions::flattenSingleValue($number); + if ($number === null) { + return 0; + } + if (is_int($number)) { + return $number; + } + if (is_numeric($number)) { + return (float) $number; + } + + throw new Exception(Functions::VALUE()); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + * + * @return float + */ + public static function validateNotNegative($number) + { + if (!is_numeric($number)) { + throw new Exception(Functions::VALUE()); + } + if ($number >= 0) { + return (float) $number; + } + + throw new Exception(Functions::NAN()); + } + + public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void + { + $isoDate = $PHPDateObject->format('c'); + if ($isoDate < '1900-03-01') { + $PHPDateObject->modify($mod); + } + } + + public static function dateParse(string $string): array + { + return self::forceArray(date_parse($string)); + } + + public static function dateParseSucceeded(array $dateArray): bool + { + return $dateArray['error_count'] === 0; + } + + /** + * Despite documentation, date_parse probably never returns false. + * Just in case, this routine helps guarantee it. + * + * @param array|false $dateArray + */ + private static function forceArray($dateArray): array + { + return is_array($dateArray) ? $dateArray : ['error_count' => 1]; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php new file mode 100644 index 0000000..c72d006 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php @@ -0,0 +1,101 @@ +getMessage(); + } + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } + + /** + * EOMONTH. + * + * Returns the date value for the last day of the month that is the indicated number of months + * before or after start_date. + * Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. + * + * Excel Function: + * EOMONTH(dateValue,adjustmentMonths) + * + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param array|int $adjustmentMonths The number of months before or after start_date. + * A positive value for months yields a future date; + * a negative value yields a past date. + * Or can be an array of adjustment values + * + * @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function lastDay($dateValue, $adjustmentMonths) + { + if (is_array($dateValue) || is_array($adjustmentMonths)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); + } + + try { + $dateValue = Helpers::getDateValue($dateValue, false); + $adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); + } catch (Exception $e) { + return $e->getMessage(); + } + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths + 1); + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php new file mode 100644 index 0000000..3b8942b --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php @@ -0,0 +1,119 @@ +getMessage(); + } + + // Execute function + $startDow = self::calcStartDow($startDate); + $endDow = self::calcEndDow($endDate); + $wholeWeekDays = (int) floor(($endDate - $startDate) / 7) * 5; + $partWeekDays = self::calcPartWeekDays($startDow, $endDow); + + // Test any extra holiday parameters + $holidayCountedArray = []; + foreach ($holidayArray as $holidayDate) { + if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { + if ((Week::day($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) { + --$partWeekDays; + $holidayCountedArray[] = $holidayDate; + } + } + } + + return self::applySign($wholeWeekDays + $partWeekDays, $sDate, $eDate); + } + + private static function calcStartDow(float $startDate): int + { + $startDow = 6 - (int) Week::day($startDate, 2); + if ($startDow < 0) { + $startDow = 5; + } + + return $startDow; + } + + private static function calcEndDow(float $endDate): int + { + $endDow = (int) Week::day($endDate, 2); + if ($endDow >= 6) { + $endDow = 0; + } + + return $endDow; + } + + private static function calcPartWeekDays(int $startDow, int $endDow): int + { + $partWeekDays = $endDow + $startDow; + if ($partWeekDays > 5) { + $partWeekDays -= 5; + } + + return $partWeekDays; + } + + private static function applySign(int $result, float $sDate, float $eDate): int + { + return ($sDate > $eDate) ? -$result : $result; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php new file mode 100644 index 0000000..7f4657c --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -0,0 +1,140 @@ +cellStack = $stack; + } + + /** + * Enable/Disable Calculation engine logging. + * + * @param bool $writeDebugLog + */ + public function setWriteDebugLog($writeDebugLog): void + { + $this->writeDebugLog = $writeDebugLog; + } + + /** + * Return whether calculation engine logging is enabled or disabled. + * + * @return bool + */ + public function getWriteDebugLog() + { + return $this->writeDebugLog; + } + + /** + * Enable/Disable echoing of debug log information. + * + * @param bool $echoDebugLog + */ + public function setEchoDebugLog($echoDebugLog): void + { + $this->echoDebugLog = $echoDebugLog; + } + + /** + * Return whether echoing of debug log information is enabled or disabled. + * + * @return bool + */ + public function getEchoDebugLog() + { + return $this->echoDebugLog; + } + + /** + * Write an entry to the calculation engine debug log. + */ + public function writeDebugLog(...$args): void + { + // Only write the debug log if logging is enabled + if ($this->writeDebugLog) { + $message = implode('', $args); + $cellReference = implode(' -> ', $this->cellStack->showStack()); + if ($this->echoDebugLog) { + echo $cellReference, + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; + } + $this->debugLog[] = $cellReference . + ($this->cellStack->count() > 0 ? ' => ' : '') . + $message; + } + } + + /** + * Write a series of entries to the calculation engine debug log. + * + * @param string[] $args + */ + public function mergeDebugLog(array $args): void + { + if ($this->writeDebugLog) { + foreach ($args as $entry) { + $this->writeDebugLog($entry); + } + } + } + + /** + * Clear the calculation engine debug log. + */ + public function clearLog(): void + { + $this->debugLog = []; + } + + /** + * Return the calculation engine debug log. + * + * @return string[] + */ + public function getLog() + { + return $this->debugLog; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php new file mode 100644 index 0000000..d70b32d --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php @@ -0,0 +1,1446 @@ + $complex->getReal(), + 'imaginary' => $complex->getImaginary(), + 'suffix' => $complex->getSuffix(), + ]; + } + + /** + * BESSELI. + * + * Returns the modified Bessel function In(x), which is equivalent to the Bessel function evaluated + * for purely imaginary arguments + * + * Excel Function: + * BESSELI(x,ord) + * + * @Deprecated 1.17.0 + * + * @see Use the BESSELI() method in the Engineering\BesselI class instead + * + * @param float $x The value at which to evaluate the function. + * If x is nonnumeric, BESSELI returns the #VALUE! error value. + * @param int $ord The order of the Bessel function. + * If ord is not an integer, it is truncated. + * If $ord is nonnumeric, BESSELI returns the #VALUE! error value. + * If $ord < 0, BESSELI returns the #NUM! error value. + * + * @return array|float|string Result, or a string containing an error + */ + public static function BESSELI($x, $ord) + { + return Engineering\BesselI::BESSELI($x, $ord); + } + + /** + * BESSELJ. + * + * Returns the Bessel function + * + * Excel Function: + * BESSELJ(x,ord) + * + * @Deprecated 1.17.0 + * + * @see Use the BESSELJ() method in the Engineering\BesselJ class instead + * + * @param float $x The value at which to evaluate the function. + * If x is nonnumeric, BESSELJ returns the #VALUE! error value. + * @param int $ord The order of the Bessel function. If n is not an integer, it is truncated. + * If $ord is nonnumeric, BESSELJ returns the #VALUE! error value. + * If $ord < 0, BESSELJ returns the #NUM! error value. + * + * @return array|float|string Result, or a string containing an error + */ + public static function BESSELJ($x, $ord) + { + return Engineering\BesselJ::BESSELJ($x, $ord); + } + + /** + * BESSELK. + * + * Returns the modified Bessel function Kn(x), which is equivalent to the Bessel functions evaluated + * for purely imaginary arguments. + * + * Excel Function: + * BESSELK(x,ord) + * + * @Deprecated 1.17.0 + * + * @see Use the BESSELK() method in the Engineering\BesselK class instead + * + * @param float $x The value at which to evaluate the function. + * If x is nonnumeric, BESSELK returns the #VALUE! error value. + * @param int $ord The order of the Bessel function. If n is not an integer, it is truncated. + * If $ord is nonnumeric, BESSELK returns the #VALUE! error value. + * If $ord < 0, BESSELK returns the #NUM! error value. + * + * @return array|float|string Result, or a string containing an error + */ + public static function BESSELK($x, $ord) + { + return Engineering\BesselK::BESSELK($x, $ord); + } + + /** + * BESSELY. + * + * Returns the Bessel function, which is also called the Weber function or the Neumann function. + * + * Excel Function: + * BESSELY(x,ord) + * + * @Deprecated 1.17.0 + * + * @see Use the BESSELY() method in the Engineering\BesselY class instead + * + * @param float $x The value at which to evaluate the function. + * If x is nonnumeric, BESSELY returns the #VALUE! error value. + * @param int $ord The order of the Bessel function. If n is not an integer, it is truncated. + * If $ord is nonnumeric, BESSELY returns the #VALUE! error value. + * If $ord < 0, BESSELY returns the #NUM! error value. + * + * @return array|float|string Result, or a string containing an error + */ + public static function BESSELY($x, $ord) + { + return Engineering\BesselY::BESSELY($x, $ord); + } + + /** + * BINTODEC. + * + * Return a binary value as decimal. + * + * Excel Function: + * BIN2DEC(x) + * + * @Deprecated 1.17.0 + * + * @see Use the toDecimal() method in the Engineering\ConvertBinary class instead + * + * @param mixed $x The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2DEC returns the #NUM! error value. + * + * @return array|string + */ + public static function BINTODEC($x) + { + return Engineering\ConvertBinary::toDecimal($x); + } + + /** + * BINTOHEX. + * + * Return a binary value as hex. + * + * Excel Function: + * BIN2HEX(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toHex() method in the Engineering\ConvertBinary class instead + * + * @param mixed $x The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2HEX returns the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, BIN2HEX uses the + * minimum number of characters necessary. Places is useful for padding the + * return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, BIN2HEX returns the #VALUE! error value. + * If places is negative, BIN2HEX returns the #NUM! error value. + * + * @return array|string + */ + public static function BINTOHEX($x, $places = null) + { + return Engineering\ConvertBinary::toHex($x, $places); + } + + /** + * BINTOOCT. + * + * Return a binary value as octal. + * + * Excel Function: + * BIN2OCT(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toOctal() method in the Engineering\ConvertBinary class instead + * + * @param mixed $x The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2OCT returns the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, BIN2OCT uses the + * minimum number of characters necessary. Places is useful for padding the + * return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, BIN2OCT returns the #VALUE! error value. + * If places is negative, BIN2OCT returns the #NUM! error value. + * + * @return array|string + */ + public static function BINTOOCT($x, $places = null) + { + return Engineering\ConvertBinary::toOctal($x, $places); + } + + /** + * DECTOBIN. + * + * Return a decimal value as binary. + * + * Excel Function: + * DEC2BIN(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toBinary() method in the Engineering\ConvertDecimal class instead + * + * @param mixed $x The decimal integer you want to convert. If number is negative, + * valid place values are ignored and DEC2BIN returns a 10-character + * (10-bit) binary number in which the most significant bit is the sign + * bit. The remaining 9 bits are magnitude bits. Negative numbers are + * represented using two's-complement notation. + * If number < -512 or if number > 511, DEC2BIN returns the #NUM! error + * value. + * If number is nonnumeric, DEC2BIN returns the #VALUE! error value. + * If DEC2BIN requires more than places characters, it returns the #NUM! + * error value. + * @param mixed $places The number of characters to use. If places is omitted, DEC2BIN uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2BIN returns the #VALUE! error value. + * If places is zero or negative, DEC2BIN returns the #NUM! error value. + * + * @return array|string + */ + public static function DECTOBIN($x, $places = null) + { + return Engineering\ConvertDecimal::toBinary($x, $places); + } + + /** + * DECTOHEX. + * + * Return a decimal value as hex. + * + * Excel Function: + * DEC2HEX(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toHex() method in the Engineering\ConvertDecimal class instead + * + * @param mixed $x The decimal integer you want to convert. If number is negative, + * places is ignored and DEC2HEX returns a 10-character (40-bit) + * hexadecimal number in which the most significant bit is the sign + * bit. The remaining 39 bits are magnitude bits. Negative numbers + * are represented using two's-complement notation. + * If number < -549,755,813,888 or if number > 549,755,813,887, + * DEC2HEX returns the #NUM! error value. + * If number is nonnumeric, DEC2HEX returns the #VALUE! error value. + * If DEC2HEX requires more than places characters, it returns the + * #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, DEC2HEX uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2HEX returns the #VALUE! error value. + * If places is zero or negative, DEC2HEX returns the #NUM! error value. + * + * @return array|string + */ + public static function DECTOHEX($x, $places = null) + { + return Engineering\ConvertDecimal::toHex($x, $places); + } + + /** + * DECTOOCT. + * + * Return an decimal value as octal. + * + * Excel Function: + * DEC2OCT(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toOctal() method in the Engineering\ConvertDecimal class instead + * + * @param mixed $x The decimal integer you want to convert. If number is negative, + * places is ignored and DEC2OCT returns a 10-character (30-bit) + * octal number in which the most significant bit is the sign bit. + * The remaining 29 bits are magnitude bits. Negative numbers are + * represented using two's-complement notation. + * If number < -536,870,912 or if number > 536,870,911, DEC2OCT + * returns the #NUM! error value. + * If number is nonnumeric, DEC2OCT returns the #VALUE! error value. + * If DEC2OCT requires more than places characters, it returns the + * #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, DEC2OCT uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2OCT returns the #VALUE! error value. + * If places is zero or negative, DEC2OCT returns the #NUM! error value. + * + * @return array|string + */ + public static function DECTOOCT($x, $places = null) + { + return Engineering\ConvertDecimal::toOctal($x, $places); + } + + /** + * HEXTOBIN. + * + * Return a hex value as binary. + * + * Excel Function: + * HEX2BIN(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toBinary() method in the Engineering\ConvertHex class instead + * + * @param mixed $x the hexadecimal number (as a string) that you want to convert. + * Number cannot contain more than 10 characters. + * The most significant bit of number is the sign bit (40th bit from the right). + * The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is negative, HEX2BIN ignores places and returns a 10-character binary number. + * If number is negative, it cannot be less than FFFFFFFE00, + * and if number is positive, it cannot be greater than 1FF. + * If number is not a valid hexadecimal number, HEX2BIN returns the #NUM! error value. + * If HEX2BIN requires more than places characters, it returns the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, + * HEX2BIN uses the minimum number of characters necessary. Places + * is useful for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, HEX2BIN returns the #VALUE! error value. + * If places is negative, HEX2BIN returns the #NUM! error value. + * + * @return array|string + */ + public static function HEXTOBIN($x, $places = null) + { + return Engineering\ConvertHex::toBinary($x, $places); + } + + /** + * HEXTODEC. + * + * Return a hex value as decimal. + * + * Excel Function: + * HEX2DEC(x) + * + * @Deprecated 1.17.0 + * + * @see Use the toDecimal() method in the Engineering\ConvertHex class instead + * + * @param mixed $x The hexadecimal number (as a string) that you want to convert. This number cannot + * contain more than 10 characters (40 bits). The most significant + * bit of number is the sign bit. The remaining 39 bits are magnitude + * bits. Negative numbers are represented using two's-complement + * notation. + * If number is not a valid hexadecimal number, HEX2DEC returns the + * #NUM! error value. + * + * @return array|string + */ + public static function HEXTODEC($x) + { + return Engineering\ConvertHex::toDecimal($x); + } + + /** + * HEXTOOCT. + * + * Return a hex value as octal. + * + * Excel Function: + * HEX2OCT(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toOctal() method in the Engineering\ConvertHex class instead + * + * @param mixed $x The hexadecimal number (as a string) that you want to convert. Number cannot + * contain more than 10 characters. The most significant bit of + * number is the sign bit. The remaining 39 bits are magnitude + * bits. Negative numbers are represented using two's-complement + * notation. + * If number is negative, HEX2OCT ignores places and returns a + * 10-character octal number. + * If number is negative, it cannot be less than FFE0000000, and + * if number is positive, it cannot be greater than 1FFFFFFF. + * If number is not a valid hexadecimal number, HEX2OCT returns + * the #NUM! error value. + * If HEX2OCT requires more than places characters, it returns + * the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, HEX2OCT + * uses the minimum number of characters necessary. Places is + * useful for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, HEX2OCT returns the #VALUE! error + * value. + * If places is negative, HEX2OCT returns the #NUM! error value. + * + * @return array|string + */ + public static function HEXTOOCT($x, $places = null) + { + return Engineering\ConvertHex::toOctal($x, $places); + } + + /** + * OCTTOBIN. + * + * Return an octal value as binary. + * + * Excel Function: + * OCT2BIN(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toBinary() method in the Engineering\ConvertOctal class instead + * + * @param mixed $x The octal number you want to convert. Number may not + * contain more than 10 characters. The most significant + * bit of number is the sign bit. The remaining 29 bits + * are magnitude bits. Negative numbers are represented + * using two's-complement notation. + * If number is negative, OCT2BIN ignores places and returns + * a 10-character binary number. + * If number is negative, it cannot be less than 7777777000, + * and if number is positive, it cannot be greater than 777. + * If number is not a valid octal number, OCT2BIN returns + * the #NUM! error value. + * If OCT2BIN requires more than places characters, it + * returns the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, + * OCT2BIN uses the minimum number of characters necessary. + * Places is useful for padding the return value with + * leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, OCT2BIN returns the #VALUE! + * error value. + * If places is negative, OCT2BIN returns the #NUM! error + * value. + * + * @return array|string + */ + public static function OCTTOBIN($x, $places = null) + { + return Engineering\ConvertOctal::toBinary($x, $places); + } + + /** + * OCTTODEC. + * + * Return an octal value as decimal. + * + * Excel Function: + * OCT2DEC(x) + * + * @Deprecated 1.17.0 + * + * @see Use the toDecimal() method in the Engineering\ConvertOctal class instead + * + * @param mixed $x The octal number you want to convert. Number may not contain + * more than 10 octal characters (30 bits). The most significant + * bit of number is the sign bit. The remaining 29 bits are + * magnitude bits. Negative numbers are represented using + * two's-complement notation. + * If number is not a valid octal number, OCT2DEC returns the + * #NUM! error value. + * + * @return array|string + */ + public static function OCTTODEC($x) + { + return Engineering\ConvertOctal::toDecimal($x); + } + + /** + * OCTTOHEX. + * + * Return an octal value as hex. + * + * Excel Function: + * OCT2HEX(x[,places]) + * + * @Deprecated 1.17.0 + * + * @see Use the toHex() method in the Engineering\ConvertOctal class instead + * + * @param mixed $x The octal number you want to convert. Number may not contain + * more than 10 octal characters (30 bits). The most significant + * bit of number is the sign bit. The remaining 29 bits are + * magnitude bits. Negative numbers are represented using + * two's-complement notation. + * If number is negative, OCT2HEX ignores places and returns a + * 10-character hexadecimal number. + * If number is not a valid octal number, OCT2HEX returns the + * #NUM! error value. + * If OCT2HEX requires more than places characters, it returns + * the #NUM! error value. + * @param mixed $places The number of characters to use. If places is omitted, OCT2HEX + * uses the minimum number of characters necessary. Places is useful + * for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, OCT2HEX returns the #VALUE! error value. + * If places is negative, OCT2HEX returns the #NUM! error value. + * + * @return array|string + */ + public static function OCTTOHEX($x, $places = null) + { + return Engineering\ConvertOctal::toHex($x, $places); + } + + /** + * COMPLEX. + * + * Converts real and imaginary coefficients into a complex number of the form x +/- yi or x +/- yj. + * + * Excel Function: + * COMPLEX(realNumber,imaginary[,suffix]) + * + * @Deprecated 1.18.0 + * + * @see Use the COMPLEX() method in the Engineering\Complex class instead + * + * @param array|float $realNumber the real coefficient of the complex number + * @param array|float $imaginary the imaginary coefficient of the complex number + * @param array|string $suffix The suffix for the imaginary component of the complex number. + * If omitted, the suffix is assumed to be "i". + * + * @return array|string + */ + public static function COMPLEX($realNumber = 0.0, $imaginary = 0.0, $suffix = 'i') + { + return Engineering\Complex::COMPLEX($realNumber, $imaginary, $suffix); + } + + /** + * IMAGINARY. + * + * Returns the imaginary coefficient of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMAGINARY(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMAGINARY() method in the Engineering\Complex class instead + * + * @param string $complexNumber the complex number for which you want the imaginary + * coefficient + * + * @return array|float|string + */ + public static function IMAGINARY($complexNumber) + { + return Engineering\Complex::IMAGINARY($complexNumber); + } + + /** + * IMREAL. + * + * Returns the real coefficient of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMREAL(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMREAL() method in the Engineering\Complex class instead + * + * @param string $complexNumber the complex number for which you want the real coefficient + * + * @return array|float|string + */ + public static function IMREAL($complexNumber) + { + return Engineering\Complex::IMREAL($complexNumber); + } + + /** + * IMABS. + * + * Returns the absolute value (modulus) of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMABS(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMABS() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the absolute value + * + * @return array|float|string + */ + public static function IMABS($complexNumber) + { + return ComplexFunctions::IMABS($complexNumber); + } + + /** + * IMARGUMENT. + * + * Returns the argument theta of a complex number, i.e. the angle in radians from the real + * axis to the representation of the number in polar coordinates. + * + * Excel Function: + * IMARGUMENT(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the argument theta + * + * @return array|float|string + */ + public static function IMARGUMENT($complexNumber) + { + return ComplexFunctions::IMARGUMENT($complexNumber); + } + + /** + * IMCONJUGATE. + * + * Returns the complex conjugate of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCONJUGATE(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the conjugate + * + * @return array|string + */ + public static function IMCONJUGATE($complexNumber) + { + return ComplexFunctions::IMCONJUGATE($complexNumber); + } + + /** + * IMCOS. + * + * Returns the cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOS(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMCOS() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the cosine + * + * @return array|float|string + */ + public static function IMCOS($complexNumber) + { + return ComplexFunctions::IMCOS($complexNumber); + } + + /** + * IMCOSH. + * + * Returns the hyperbolic cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOSH(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMCOSH() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosine + * + * @return array|float|string + */ + public static function IMCOSH($complexNumber) + { + return ComplexFunctions::IMCOSH($complexNumber); + } + + /** + * IMCOT. + * + * Returns the cotangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOT(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMCOT() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the cotangent + * + * @return array|float|string + */ + public static function IMCOT($complexNumber) + { + return ComplexFunctions::IMCOT($complexNumber); + } + + /** + * IMCSC. + * + * Returns the cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSC(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMCSC() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the cosecant + * + * @return array|float|string + */ + public static function IMCSC($complexNumber) + { + return ComplexFunctions::IMCSC($complexNumber); + } + + /** + * IMCSCH. + * + * Returns the hyperbolic cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSCH(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMCSCH() method in the Engineering\ComplexFunctions class instead + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant + * + * @return array|float|string + */ + public static function IMCSCH($complexNumber) + { + return ComplexFunctions::IMCSCH($complexNumber); + } + + /** + * IMSIN. + * + * Returns the sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSIN(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSIN() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the sine + * + * @return array|float|string + */ + public static function IMSIN($complexNumber) + { + return ComplexFunctions::IMSIN($complexNumber); + } + + /** + * IMSINH. + * + * Returns the hyperbolic sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSINH(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSINH() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the hyperbolic sine + * + * @return array|float|string + */ + public static function IMSINH($complexNumber) + { + return ComplexFunctions::IMSINH($complexNumber); + } + + /** + * IMSEC. + * + * Returns the secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSEC(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSEC() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the secant + * + * @return array|float|string + */ + public static function IMSEC($complexNumber) + { + return ComplexFunctions::IMSEC($complexNumber); + } + + /** + * IMSECH. + * + * Returns the hyperbolic secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSECH(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSECH() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the hyperbolic secant + * + * @return array|float|string + */ + public static function IMSECH($complexNumber) + { + return ComplexFunctions::IMSECH($complexNumber); + } + + /** + * IMTAN. + * + * Returns the tangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMTAN(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMTAN() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the tangent + * + * @return array|float|string + */ + public static function IMTAN($complexNumber) + { + return ComplexFunctions::IMTAN($complexNumber); + } + + /** + * IMSQRT. + * + * Returns the square root of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSQRT(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSQRT() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the square root + * + * @return array|string + */ + public static function IMSQRT($complexNumber) + { + return ComplexFunctions::IMSQRT($complexNumber); + } + + /** + * IMLN. + * + * Returns the natural logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLN(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMLN() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the natural logarithm + * + * @return array|string + */ + public static function IMLN($complexNumber) + { + return ComplexFunctions::IMLN($complexNumber); + } + + /** + * IMLOG10. + * + * Returns the common logarithm (base 10) of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG10(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMLOG10() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the common logarithm + * + * @return array|string + */ + public static function IMLOG10($complexNumber) + { + return ComplexFunctions::IMLOG10($complexNumber); + } + + /** + * IMLOG2. + * + * Returns the base-2 logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG2(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMLOG2() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the base-2 logarithm + * + * @return array|string + */ + public static function IMLOG2($complexNumber) + { + return ComplexFunctions::IMLOG2($complexNumber); + } + + /** + * IMEXP. + * + * Returns the exponential of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMEXP(complexNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMEXP() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number for which you want the exponential + * + * @return array|string + */ + public static function IMEXP($complexNumber) + { + return ComplexFunctions::IMEXP($complexNumber); + } + + /** + * IMPOWER. + * + * Returns a complex number in x + yi or x + yj text format raised to a power. + * + * Excel Function: + * IMPOWER(complexNumber,realNumber) + * + * @Deprecated 1.18.0 + * + * @see Use the IMPOWER() method in the Engineering\ComplexFunctions class instead + * + * @param string $complexNumber the complex number you want to raise to a power + * @param float $realNumber the power to which you want to raise the complex number + * + * @return array|string + */ + public static function IMPOWER($complexNumber, $realNumber) + { + return ComplexFunctions::IMPOWER($complexNumber, $realNumber); + } + + /** + * IMDIV. + * + * Returns the quotient of two complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMDIV(complexDividend,complexDivisor) + * + * @Deprecated 1.18.0 + * + * @see Use the IMDIV() method in the Engineering\ComplexOperations class instead + * + * @param string $complexDividend the complex numerator or dividend + * @param string $complexDivisor the complex denominator or divisor + * + * @return array|string + */ + public static function IMDIV($complexDividend, $complexDivisor) + { + return ComplexOperations::IMDIV($complexDividend, $complexDivisor); + } + + /** + * IMSUB. + * + * Returns the difference of two complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMSUB(complexNumber1,complexNumber2) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSUB() method in the Engineering\ComplexOperations class instead + * + * @param string $complexNumber1 the complex number from which to subtract complexNumber2 + * @param string $complexNumber2 the complex number to subtract from complexNumber1 + * + * @return array|string + */ + public static function IMSUB($complexNumber1, $complexNumber2) + { + return ComplexOperations::IMSUB($complexNumber1, $complexNumber2); + } + + /** + * IMSUM. + * + * Returns the sum of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMSUM(complexNumber[,complexNumber[,...]]) + * + * @Deprecated 1.18.0 + * + * @see Use the IMSUM() method in the Engineering\ComplexOperations class instead + * + * @param string ...$complexNumbers Series of complex numbers to add + * + * @return string + */ + public static function IMSUM(...$complexNumbers) + { + return ComplexOperations::IMSUM(...$complexNumbers); + } + + /** + * IMPRODUCT. + * + * Returns the product of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMPRODUCT(complexNumber[,complexNumber[,...]]) + * + * @Deprecated 1.18.0 + * + * @see Use the IMPRODUCT() method in the Engineering\ComplexOperations class instead + * + * @param string ...$complexNumbers Series of complex numbers to multiply + * + * @return string + */ + public static function IMPRODUCT(...$complexNumbers) + { + return ComplexOperations::IMPRODUCT(...$complexNumbers); + } + + /** + * DELTA. + * + * Tests whether two values are equal. Returns 1 if number1 = number2; returns 0 otherwise. + * Use this function to filter a set of values. For example, by summing several DELTA + * functions you calculate the count of equal pairs. This function is also known as the + * Kronecker Delta function. + * + * Excel Function: + * DELTA(a[,b]) + * + * @Deprecated 1.17.0 + * + * @see Use the DELTA() method in the Engineering\Compare class instead + * + * @param float $a the first number + * @param float $b The second number. If omitted, b is assumed to be zero. + * + * @return array|int|string (string in the event of an error) + */ + public static function DELTA($a, $b = 0) + { + return Engineering\Compare::DELTA($a, $b); + } + + /** + * GESTEP. + * + * Excel Function: + * GESTEP(number[,step]) + * + * Returns 1 if number >= step; returns 0 (zero) otherwise + * Use this function to filter a set of values. For example, by summing several GESTEP + * functions you calculate the count of values that exceed a threshold. + * + * @Deprecated 1.17.0 + * + * @see Use the GESTEP() method in the Engineering\Compare class instead + * + * @param float $number the value to test against step + * @param float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * + * @return array|int|string (string in the event of an error) + */ + public static function GESTEP($number, $step = 0) + { + return Engineering\Compare::GESTEP($number, $step); + } + + /** + * BITAND. + * + * Returns the bitwise AND of two integer values. + * + * Excel Function: + * BITAND(number1, number2) + * + * @Deprecated 1.17.0 + * + * @see Use the BITAND() method in the Engineering\BitWise class instead + * + * @param int $number1 + * @param int $number2 + * + * @return array|int|string + */ + public static function BITAND($number1, $number2) + { + return Engineering\BitWise::BITAND($number1, $number2); + } + + /** + * BITOR. + * + * Returns the bitwise OR of two integer values. + * + * Excel Function: + * BITOR(number1, number2) + * + * @Deprecated 1.17.0 + * + * @see Use the BITOR() method in the Engineering\BitWise class instead + * + * @param int $number1 + * @param int $number2 + * + * @return array|int|string + */ + public static function BITOR($number1, $number2) + { + return Engineering\BitWise::BITOR($number1, $number2); + } + + /** + * BITXOR. + * + * Returns the bitwise XOR of two integer values. + * + * Excel Function: + * BITXOR(number1, number2) + * + * @Deprecated 1.17.0 + * + * @see Use the BITXOR() method in the Engineering\BitWise class instead + * + * @param int $number1 + * @param int $number2 + * + * @return array|int|string + */ + public static function BITXOR($number1, $number2) + { + return Engineering\BitWise::BITXOR($number1, $number2); + } + + /** + * BITLSHIFT. + * + * Returns the number value shifted left by shift_amount bits. + * + * Excel Function: + * BITLSHIFT(number, shift_amount) + * + * @Deprecated 1.17.0 + * + * @see Use the BITLSHIFT() method in the Engineering\BitWise class instead + * + * @param int $number + * @param int $shiftAmount + * + * @return array|float|int|string + */ + public static function BITLSHIFT($number, $shiftAmount) + { + return Engineering\BitWise::BITLSHIFT($number, $shiftAmount); + } + + /** + * BITRSHIFT. + * + * Returns the number value shifted right by shift_amount bits. + * + * Excel Function: + * BITRSHIFT(number, shift_amount) + * + * @Deprecated 1.17.0 + * + * @see Use the BITRSHIFT() method in the Engineering\BitWise class instead + * + * @param int $number + * @param int $shiftAmount + * + * @return array|float|int|string + */ + public static function BITRSHIFT($number, $shiftAmount) + { + return Engineering\BitWise::BITRSHIFT($number, $shiftAmount); + } + + /** + * ERF. + * + * Returns the error function integrated between the lower and upper bound arguments. + * + * Note: In Excel 2007 or earlier, if you input a negative value for the upper or lower bound arguments, + * the function would return a #NUM! error. However, in Excel 2010, the function algorithm was + * improved, so that it can now calculate the function for both positive and negative ranges. + * PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments. + * + * Excel Function: + * ERF(lower[,upper]) + * + * @Deprecated 1.17.0 + * + * @see Use the ERF() method in the Engineering\Erf class instead + * + * @param float $lower lower bound for integrating ERF + * @param float $upper upper bound for integrating ERF. + * If omitted, ERF integrates between zero and lower_limit + * + * @return array|float|string + */ + public static function ERF($lower, $upper = null) + { + return Engineering\Erf::ERF($lower, $upper); + } + + /** + * ERFPRECISE. + * + * Returns the error function integrated between the lower and upper bound arguments. + * + * Excel Function: + * ERF.PRECISE(limit) + * + * @Deprecated 1.17.0 + * + * @see Use the ERFPRECISE() method in the Engineering\Erf class instead + * + * @param float $limit bound for integrating ERF + * + * @return array|float|string + */ + public static function ERFPRECISE($limit) + { + return Engineering\Erf::ERFPRECISE($limit); + } + + /** + * ERFC. + * + * Returns the complementary ERF function integrated between x and infinity + * + * Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument, + * the function would return a #NUM! error. However, in Excel 2010, the function algorithm was + * improved, so that it can now calculate the function for both positive and negative x values. + * PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments. + * + * Excel Function: + * ERFC(x) + * + * @Deprecated 1.17.0 + * + * @see Use the ERFC() method in the Engineering\ErfC class instead + * + * @param float $x The lower bound for integrating ERFC + * + * @return array|float|string + */ + public static function ERFC($x) + { + return Engineering\ErfC::ERFC($x); + } + + /** + * getConversionGroups + * Returns a list of the different conversion groups for UOM conversions. + * + * @Deprecated 1.16.0 + * + * @see Use the getConversionCategories() method in the Engineering\ConvertUOM class instead + * + * @return array + */ + public static function getConversionGroups() + { + return Engineering\ConvertUOM::getConversionCategories(); + } + + /** + * getConversionGroupUnits + * Returns an array of units of measure, for a specified conversion group, or for all groups. + * + * @Deprecated 1.16.0 + * + * @see Use the getConversionCategoryUnits() method in the ConvertUOM class instead + * + * @param null|mixed $category + * + * @return array + */ + public static function getConversionGroupUnits($category = null) + { + return Engineering\ConvertUOM::getConversionCategoryUnits($category); + } + + /** + * getConversionGroupUnitDetails. + * + * @Deprecated 1.16.0 + * + * @see Use the getConversionCategoryUnitDetails() method in the ConvertUOM class instead + * + * @param null|mixed $category + * + * @return array + */ + public static function getConversionGroupUnitDetails($category = null) + { + return Engineering\ConvertUOM::getConversionCategoryUnitDetails($category); + } + + /** + * getConversionMultipliers + * Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @Deprecated 1.16.0 + * + * @see Use the getConversionMultipliers() method in the ConvertUOM class instead + * + * @return mixed[] + */ + public static function getConversionMultipliers() + { + return Engineering\ConvertUOM::getConversionMultipliers(); + } + + /** + * getBinaryConversionMultipliers. + * + * Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure + * in CONVERTUOM(). + * + * @Deprecated 1.16.0 + * + * @see Use the getBinaryConversionMultipliers() method in the ConvertUOM class instead + * + * @return mixed[] + */ + public static function getBinaryConversionMultipliers() + { + return Engineering\ConvertUOM::getBinaryConversionMultipliers(); + } + + /** + * CONVERTUOM. + * + * Converts a number from one measurement system to another. + * For example, CONVERT can translate a table of distances in miles to a table of distances + * in kilometers. + * + * Excel Function: + * CONVERT(value,fromUOM,toUOM) + * + * @Deprecated 1.16.0 + * + * @see Use the CONVERT() method in the ConvertUOM class instead + * + * @param float|int $value the value in fromUOM to convert + * @param string $fromUOM the units for value + * @param string $toUOM the units for the result + * + * @return array|float|string + */ + public static function CONVERTUOM($value, $fromUOM, $toUOM) + { + return Engineering\ConvertUOM::CONVERT($value, $fromUOM, $toUOM); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php new file mode 100644 index 0000000..01630af --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php @@ -0,0 +1,33 @@ + 2.2) { + return 1 - ErfC::ERFC($value); + } + $sum = $term = $value; + $xsqr = ($value * $value); + $j = 1; + do { + $term *= $xsqr / $j; + $sum -= $term / (2 * $j + 1); + ++$j; + $term *= $xsqr / $j; + $sum += $term / (2 * $j + 1); + ++$j; + if ($sum == 0.0) { + break; + } + } while (abs($term / $sum) > Functions::PRECISION); + + return self::$twoSqrtPi * $sum; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php new file mode 100644 index 0000000..da1a932 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php @@ -0,0 +1,76 @@ + Functions::PRECISION); + + return self::$oneSqrtPi * exp(-$value * $value) * $q2; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Exception.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Exception.php new file mode 100644 index 0000000..87c7d22 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Exception.php @@ -0,0 +1,26 @@ +line = $line; + $e->file = $file; + + throw $e; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php new file mode 100644 index 0000000..41e51d4 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php @@ -0,0 +1,22 @@ +getMessage(); + } + + return self::calculateFutureValue($rate, $numberOfPeriods, $payment, $presentValue, $type); + } + + /** + * PV. + * + * Returns the Present Value of a cash flow with constant payments and interest rate (annuities). + * + * @param mixed $rate Interest rate per period + * @param mixed $numberOfPeriods Number of periods as an integer + * @param mixed $payment Periodic payment (annuity) + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function presentValue( + $rate, + $numberOfPeriods, + $payment = 0.0, + $futureValue = 0.0, + $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ) { + $rate = Functions::flattenSingleValue($rate); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); + + try { + $rate = CashFlowValidations::validateRate($rate); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $payment = CashFlowValidations::validateFloat($payment); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($numberOfPeriods < 0) { + return Functions::NAN(); + } + + return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type); + } + + /** + * NPER. + * + * Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. + * + * @param mixed $rate Interest rate per period + * @param mixed $payment Periodic payment (annuity) + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function periods( + $rate, + $payment, + $presentValue, + $futureValue = 0.0, + $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ) { + $rate = Functions::flattenSingleValue($rate); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); + + try { + $rate = CashFlowValidations::validateRate($rate); + $payment = CashFlowValidations::validateFloat($payment); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($payment == 0.0) { + return Functions::NAN(); + } + + return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type); + } + + private static function calculateFutureValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $presentValue, + int $type + ): float { + if ($rate !== null && $rate != 0) { + return -$presentValue * + (1 + $rate) ** $numberOfPeriods - $payment * (1 + $rate * $type) * ((1 + $rate) ** $numberOfPeriods - 1) + / $rate; + } + + return -$presentValue - $payment * $numberOfPeriods; + } + + private static function calculatePresentValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $futureValue, + int $type + ): float { + if ($rate != 0.0) { + return (-$payment * (1 + $rate * $type) + * (((1 + $rate) ** $numberOfPeriods - 1) / $rate) - $futureValue) / (1 + $rate) ** $numberOfPeriods; + } + + return -$futureValue - $payment * $numberOfPeriods; + } + + /** + * @return float|string + */ + private static function calculatePeriods( + float $rate, + float $payment, + float $presentValue, + float $futureValue, + int $type + ) { + if ($rate != 0.0) { + if ($presentValue == 0.0) { + return Functions::NAN(); + } + + return log(($payment * (1 + $rate * $type) / $rate - $futureValue) / + ($presentValue + $payment * (1 + $rate * $type) / $rate)) / log(1 + $rate); + } + + return (-$presentValue - $futureValue) / $payment; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php new file mode 100644 index 0000000..56d2e37 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php @@ -0,0 +1,216 @@ +getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->interest(); + } + + /** + * ISPMT. + * + * Returns the interest payment for an investment based on an interest rate and a constant payment schedule. + * + * Excel Function: + * =ISPMT(interest_rate, period, number_payments, pv) + * + * @param mixed $interestRate is the interest rate for the investment + * @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments. + * @param mixed $numberOfPeriods is the number of payments for the annuity + * @param mixed $principleRemaining is the loan amount or present value of the payments + */ + public static function schedulePayment($interestRate, $period, $numberOfPeriods, $principleRemaining) + { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $principleRemaining = Functions::flattenSingleValue($principleRemaining); + + try { + $interestRate = CashFlowValidations::validateRate($interestRate); + $period = CashFlowValidations::validateInt($period); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $principleRemaining = CashFlowValidations::validateFloat($principleRemaining); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + + // Return value + $returnValue = 0; + + // Calculate + $principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0); + for ($i = 0; $i <= $period; ++$i) { + $returnValue = $interestRate * $principleRemaining * -1; + $principleRemaining -= $principlePayment; + // principle needs to be 0 after the last payment, don't let floating point screw it up + if ($i == $numberOfPeriods) { + $returnValue = 0.0; + } + } + + return $returnValue; + } + + /** + * RATE. + * + * Returns the interest rate per period of an annuity. + * RATE is calculated by iteration and can have zero or more solutions. + * If the successive results of RATE do not converge to within 0.0000001 after 20 iterations, + * RATE returns the #NUM! error value. + * + * Excel Function: + * RATE(nper,pmt,pv[,fv[,type[,guess]]]) + * + * @param mixed $numberOfPeriods The total number of payment periods in an annuity + * @param mixed $payment The payment made each period and cannot change over the life of the annuity. + * Typically, pmt includes principal and interest but no other fees or taxes. + * @param mixed $presentValue The present value - the total amount that a series of future payments is worth now + * @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made. + * If fv is omitted, it is assumed to be 0 (the future value of a loan, + * for example, is 0). + * @param mixed $type A number 0 or 1 and indicates when payments are due: + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. + * @param mixed $guess Your guess for what the rate will be. + * If you omit guess, it is assumed to be 10 percent. + * + * @return float|string + */ + public static function rate( + $numberOfPeriods, + $payment, + $presentValue, + $futureValue = 0.0, + $type = FinancialConstants::PAYMENT_END_OF_PERIOD, + $guess = 0.1 + ) { + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); + $guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); + + try { + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $payment = CashFlowValidations::validateFloat($payment); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + $guess = CashFlowValidations::validateFloat($guess); + } catch (Exception $e) { + return $e->getMessage(); + } + + $rate = $guess; + // rest of code adapted from python/numpy + $close = false; + $iter = 0; + while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { + $nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type); + if (!is_numeric($nextdiff)) { + break; + } + $rate1 = $rate - $nextdiff; + $close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; + ++$iter; + $rate = $rate1; + } + + return $close ? $rate : Functions::NAN(); + } + + private static function rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type) + { + if ($rate == 0.0) { + return Functions::NAN(); + } + $tt1 = ($rate + 1) ** $numberOfPeriods; + $tt2 = ($rate + 1) ** ($numberOfPeriods - 1); + $numerator = $futureValue + $tt1 * $presentValue + $payment * ($tt1 - 1) * ($rate * $type + 1) / $rate; + $denominator = $numberOfPeriods * $tt2 * $presentValue - $payment * ($tt1 - 1) + * ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods + * $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate; + if ($denominator == 0) { + return Functions::NAN(); + } + + return $numerator / $denominator; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php new file mode 100644 index 0000000..ca989e0 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php @@ -0,0 +1,44 @@ +interest = $interest; + $this->principal = $principal; + } + + public function interest(): float + { + return $this->interest; + } + + public function principal(): float + { + return $this->principal; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php new file mode 100644 index 0000000..e103f92 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php @@ -0,0 +1,115 @@ +getMessage(); + } + + // Calculate + if ($interestRate != 0.0) { + return (-$futureValue - $presentValue * (1 + $interestRate) ** $numberOfPeriods) / + (1 + $interestRate * $type) / (((1 + $interestRate) ** $numberOfPeriods - 1) / $interestRate); + } + + return (-$presentValue - $futureValue) / $numberOfPeriods; + } + + /** + * PPMT. + * + * Returns the interest payment for a given period for an investment based on periodic, constant payments + * and a constant interest rate. + * + * @param mixed $interestRate Interest rate per period + * @param mixed $period Period for which we want to find the interest + * @param mixed $numberOfPeriods Number of periods + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function interestPayment( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue = 0, + $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ) { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); + + try { + $interestRate = CashFlowValidations::validateRate($interestRate); + $period = CashFlowValidations::validateInt($period); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->principal(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php new file mode 100644 index 0000000..d590e1a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -0,0 +1,258 @@ + 1; + $datesIsArray = count($dates) > 1; + if (!$valuesIsArray && !$datesIsArray) { + return Functions::NA(); + } + if (count($values) != count($dates)) { + return Functions::NAN(); + } + + $datesCount = count($dates); + for ($i = 0; $i < $datesCount; ++$i) { + try { + $dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + return self::xirrPart2($values); + } + + private static function xirrPart2(array &$values): string + { + $valCount = count($values); + $foundpos = false; + $foundneg = false; + for ($i = 0; $i < $valCount; ++$i) { + $fld = $values[$i]; + if (!is_numeric($fld)) { + return Functions::VALUE(); + } elseif ($fld > 0) { + $foundpos = true; + } elseif ($fld < 0) { + $foundneg = true; + } + } + if (!self::bothNegAndPos($foundneg, $foundpos)) { + return Functions::NAN(); + } + + return ''; + } + + /** + * @return float|string + */ + private static function xirrPart3(array $values, array $dates, float $x1, float $x2) + { + $f = self::xnpvOrdered($x1, $values, $dates, false); + if ($f < 0.0) { + $rtb = $x1; + $dx = $x2 - $x1; + } else { + $rtb = $x2; + $dx = $x1 - $x2; + } + + $rslt = Functions::VALUE(); + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $dx *= 0.5; + $x_mid = $rtb + $dx; + $f_mid = (float) self::xnpvOrdered($x_mid, $values, $dates, false); + if ($f_mid <= 0.0) { + $rtb = $x_mid; + } + if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { + $rslt = $x_mid; + + break; + } + } + + return $rslt; + } + + /** + * @param mixed $rate + * @param mixed $values + * @param mixed $dates + * + * @return float|string + */ + private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true) + { + $rate = Functions::flattenSingleValue($rate); + $values = Functions::flattenArray($values); + $dates = Functions::flattenArray($dates); + $valCount = count($values); + + try { + self::validateXnpv($rate, $values, $dates); + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); + } catch (Exception $e) { + return $e->getMessage(); + } + + $xnpv = 0.0; + for ($i = 0; $i < $valCount; ++$i) { + if (!is_numeric($values[$i])) { + return Functions::VALUE(); + } + + try { + $datei = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); + } + if ($date0 > $datei) { + $dif = $ordered ? Functions::NAN() : -((int) DateTimeExcel\Difference::interval($datei, $date0, 'd')); + } else { + $dif = DateTimeExcel\Difference::interval($date0, $datei, 'd'); + } + if (!is_numeric($dif)) { + return $dif; + } + if ($rate <= -1.0) { + $xnpv += -abs($values[$i]) / (-1 - $rate) ** ($dif / 365); + } else { + $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); + } + } + + return is_finite($xnpv) ? $xnpv : Functions::VALUE(); + } + + /** + * @param mixed $rate + */ + private static function validateXnpv($rate, array $values, array $dates): void + { + if (!is_numeric($rate)) { + throw new Exception(Functions::VALUE()); + } + $valCount = count($values); + if ($valCount != count($dates)) { + throw new Exception(Functions::NAN()); + } + if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { + throw new Exception(Functions::NAN()); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php new file mode 100644 index 0000000..0dde0c1 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -0,0 +1,131 @@ +getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return Functions::NAN(); + } + if ($fraction == 0) { + return Functions::DIV0(); + } + + $dollars = ($fractionalDollar < 0) ? ceil($fractionalDollar) : floor($fractionalDollar); + $cents = fmod($fractionalDollar, 1.0); + $cents /= $fraction; + $cents *= 10 ** ceil(log10($fraction)); + + return $dollars + $cents; + } + + /** + * DOLLARFR. + * + * Converts a dollar price expressed as a decimal number into a dollar price + * expressed as a fraction. + * Fractional dollar numbers are sometimes used for security prices. + * + * Excel Function: + * DOLLARFR(decimal_dollar,fraction) + * + * @param mixed $decimalDollar Decimal Dollar + * Or can be an array of values + * @param mixed $fraction Fraction + * Or can be an array of values + * + * @return array|float|string + */ + public static function fractional($decimalDollar = null, $fraction = 0) + { + if (is_array($decimalDollar) || is_array($fraction)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $decimalDollar, $fraction); + } + + try { + $decimalDollar = FinancialValidations::validateFloat( + Functions::flattenSingleValue($decimalDollar) ?? 0.0 + ); + $fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return Functions::NAN(); + } + if ($fraction == 0) { + return Functions::DIV0(); + } + + $dollars = ($decimalDollar < 0.0) ? ceil($decimalDollar) : floor($decimalDollar); + $cents = fmod($decimalDollar, 1); + $cents *= $fraction; + $cents *= 10 ** (-ceil(log10($fraction))); + + return $dollars + $cents; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php new file mode 100644 index 0000000..310e005 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php @@ -0,0 +1,158 @@ + 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } + + /** + * @param mixed $price + */ + public static function validatePrice($price): float + { + $price = self::validateFloat($price); + if ($price < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $price; + } + + /** + * @param mixed $parValue + */ + public static function validateParValue($parValue): float + { + $parValue = self::validateFloat($parValue); + if ($parValue < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $parValue; + } + + /** + * @param mixed $yield + */ + public static function validateYield($yield): float + { + $yield = self::validateFloat($yield); + if ($yield < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $yield; + } + + /** + * @param mixed $discount + */ + public static function validateDiscount($discount): float + { + $discount = self::validateFloat($discount); + if ($discount <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $discount; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php new file mode 100644 index 0000000..d339b13 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php @@ -0,0 +1,58 @@ +format('d') === $date->format('t'); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php new file mode 100644 index 0000000..72df31e --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -0,0 +1,72 @@ +getMessage(); + } + + if ($nominalRate <= 0 || $periodsPerYear < 1) { + return Functions::NAN(); + } + + return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1; + } + + /** + * NOMINAL. + * + * Returns the nominal interest rate given the effective rate and the number of compounding payments per year. + * + * @param mixed $effectiveRate Effective interest rate as a float + * @param mixed $periodsPerYear Integer number of compounding payments per year + * + * @return float|string Result, or a string containing an error + */ + public static function nominal($effectiveRate = 0, $periodsPerYear = 0) + { + $effectiveRate = Functions::flattenSingleValue($effectiveRate); + $periodsPerYear = Functions::flattenSingleValue($periodsPerYear); + + try { + $effectiveRate = FinancialValidations::validateFloat($effectiveRate); + $periodsPerYear = FinancialValidations::validateInt($periodsPerYear); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($effectiveRate <= 0 || $periodsPerYear < 1) { + return Functions::NAN(); + } + + // Calculate + return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php new file mode 100644 index 0000000..ddf45b2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -0,0 +1,629 @@ +<'; + const OPERATORS_POSTFIX = '%'; + + /** + * Formula. + * + * @var string + */ + private $formula; + + /** + * Tokens. + * + * @var FormulaToken[] + */ + private $tokens = []; + + /** + * Create a new FormulaParser. + * + * @param string $formula Formula to parse + */ + public function __construct($formula = '') + { + // Check parameters + if ($formula === null) { + throw new Exception('Invalid parameter passed: formula'); + } + + // Initialise values + $this->formula = trim($formula); + // Parse! + $this->parseToTokens(); + } + + /** + * Get Formula. + * + * @return string + */ + public function getFormula() + { + return $this->formula; + } + + /** + * Get Token. + * + * @param int $id Token id + */ + public function getToken(int $id = 0): FormulaToken + { + if (isset($this->tokens[$id])) { + return $this->tokens[$id]; + } + + throw new Exception("Token with id $id does not exist."); + } + + /** + * Get Token count. + * + * @return int + */ + public function getTokenCount() + { + return count($this->tokens); + } + + /** + * Get Tokens. + * + * @return FormulaToken[] + */ + public function getTokens() + { + return $this->tokens; + } + + /** + * Parse to tokens. + */ + private function parseToTokens(): void + { + // No attempt is made to verify formulas; assumes formulas are derived from Excel, where + // they can only exist if valid; stack overflows/underflows sunk as nulls without exceptions. + + // Check if the formula has a valid starting = + $formulaLength = strlen($this->formula); + if ($formulaLength < 2 || $this->formula[0] != '=') { + return; + } + + // Helper variables + $tokens1 = $tokens2 = $stack = []; + $inString = $inPath = $inRange = $inError = false; + $token = $previousToken = $nextToken = null; + + $index = 1; + $value = ''; + + $ERRORS = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A']; + $COMPARATORS_MULTI = ['>=', '<=', '<>']; + + while ($index < $formulaLength) { + // state-dependent character evaluation (order is important) + + // double-quoted strings + // embeds are doubled + // end marks token + if ($inString) { + if ($this->formula[$index] == self::QUOTE_DOUBLE) { + if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_DOUBLE)) { + $value .= self::QUOTE_DOUBLE; + ++$index; + } else { + $inString = false; + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_TEXT); + $value = ''; + } + } else { + $value .= $this->formula[$index]; + } + ++$index; + + continue; + } + + // single-quoted strings (links) + // embeds are double + // end does not mark a token + if ($inPath) { + if ($this->formula[$index] == self::QUOTE_SINGLE) { + if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_SINGLE)) { + $value .= self::QUOTE_SINGLE; + ++$index; + } else { + $inPath = false; + } + } else { + $value .= $this->formula[$index]; + } + ++$index; + + continue; + } + + // bracked strings (R1C1 range index or linked workbook name) + // no embeds (changed to "()" by Excel) + // end does not mark a token + if ($inRange) { + if ($this->formula[$index] == self::BRACKET_CLOSE) { + $inRange = false; + } + $value .= $this->formula[$index]; + ++$index; + + continue; + } + + // error values + // end marks a token, determined from absolute list of values + if ($inError) { + $value .= $this->formula[$index]; + ++$index; + if (in_array($value, $ERRORS)) { + $inError = false; + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_ERROR); + $value = ''; + } + + continue; + } + + // scientific notation check + if (strpos(self::OPERATORS_SN, $this->formula[$index]) !== false) { + if (strlen($value) > 1) { + if (preg_match('/^[1-9]{1}(\\.\\d+)?E{1}$/', $this->formula[$index]) != 0) { + $value .= $this->formula[$index]; + ++$index; + + continue; + } + } + } + + // independent character evaluation (order not important) + + // establish state-dependent character evaluations + if ($this->formula[$index] == self::QUOTE_DOUBLE) { + if (strlen($value) > 0) { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inString = true; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::QUOTE_SINGLE) { + if (strlen($value) > 0) { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inPath = true; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::BRACKET_OPEN) { + $inRange = true; + $value .= self::BRACKET_OPEN; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::ERROR_START) { + if (strlen($value) > 0) { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inError = true; + $value .= self::ERROR_START; + ++$index; + + continue; + } + + // mark start and end of arrays and array rows + if ($this->formula[$index] == self::BRACE_OPEN) { + if (strlen($value) > 0) { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + + $tmp = new FormulaToken('ARRAY', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + ++$index; + + continue; + } + + if ($this->formula[$index] == self::SEMICOLON) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + $tmp = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT); + $tokens1[] = $tmp; + + $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + ++$index; + + continue; + } + + if ($this->formula[$index] == self::BRACE_CLOSE) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + ++$index; + + continue; + } + + // trim white-space + if ($this->formula[$index] == self::WHITESPACE) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken('', FormulaToken::TOKEN_TYPE_WHITESPACE); + ++$index; + while (($this->formula[$index] == self::WHITESPACE) && ($index < $formulaLength)) { + ++$index; + } + + continue; + } + + // multi-character comparators + if (($index + 2) <= $formulaLength) { + if (in_array(substr($this->formula, $index, 2), $COMPARATORS_MULTI)) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken(substr($this->formula, $index, 2), FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_LOGICAL); + $index += 2; + + continue; + } + } + + // standard infix operators + if (strpos(self::OPERATORS_INFIX, $this->formula[$index]) !== false) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORINFIX); + ++$index; + + continue; + } + + // standard postfix operators (only one) + if (strpos(self::OPERATORS_POSTFIX, $this->formula[$index]) !== false) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX); + ++$index; + + continue; + } + + // start subexpression or function + if ($this->formula[$index] == self::PAREN_OPEN) { + if (strlen($value) > 0) { + $tmp = new FormulaToken($value, FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + $value = ''; + } else { + $tmp = new FormulaToken('', FormulaToken::TOKEN_TYPE_SUBEXPRESSION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + } + ++$index; + + continue; + } + + // function, subexpression, or array parameters, or operand unions + if ($this->formula[$index] == self::COMMA) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $stack[] = $tmp; + + if ($tmp->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) { + $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_UNION); + } else { + $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT); + } + ++$index; + + continue; + } + + // stop subexpression + if ($this->formula[$index] == self::PAREN_CLOSE) { + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + ++$index; + + continue; + } + + // token accumulation + $value .= $this->formula[$index]; + ++$index; + } + + // dump remaining accumulation + if (strlen($value) > 0) { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + } + + // move tokenList to new set, excluding unnecessary white-space tokens and converting necessary ones to intersections + $tokenCount = count($tokens1); + for ($i = 0; $i < $tokenCount; ++$i) { + $token = $tokens1[$i]; + if (isset($tokens1[$i - 1])) { + $previousToken = $tokens1[$i - 1]; + } else { + $previousToken = null; + } + if (isset($tokens1[$i + 1])) { + $nextToken = $tokens1[$i + 1]; + } else { + $nextToken = null; + } + + if ($token === null) { + continue; + } + + if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) { + $tokens2[] = $token; + + continue; + } + + if ($previousToken === null) { + continue; + } + + if ( + !( + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) + ) { + continue; + } + + if ($nextToken === null) { + continue; + } + + if ( + !( + (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) || + (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) || + ($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) + ) { + continue; + } + + $tokens2[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_INTERSECTION); + } + + // move tokens to final list, switching infix "-" operators to prefix when appropriate, switching infix "+" operators + // to noop when appropriate, identifying operand and infix-operator subtypes, and pulling "@" from function names + $this->tokens = []; + + $tokenCount = count($tokens2); + for ($i = 0; $i < $tokenCount; ++$i) { + $token = $tokens2[$i]; + if (isset($tokens2[$i - 1])) { + $previousToken = $tokens2[$i - 1]; + } else { + $previousToken = null; + } + if (isset($tokens2[$i + 1])) { + $nextToken = $tokens2[$i + 1]; + } else { + $nextToken = null; + } + + if ($token === null) { + continue; + } + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') { + if ($i == 0) { + $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); + } elseif ( + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && + ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && + ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) || + ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } else { + $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); + } + + $this->tokens[] = $token; + + continue; + } + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '+') { + if ($i == 0) { + continue; + } elseif ( + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && + ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && + ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) || + ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) || + ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } else { + continue; + } + + $this->tokens[] = $token; + + continue; + } + + if ( + $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && + $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING + ) { + if (strpos('<>=', substr($token->getValue(), 0, 1)) !== false) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL); + } elseif ($token->getValue() == '&') { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_CONCATENATION); + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } + + $this->tokens[] = $token; + + continue; + } + + if ( + $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND && + $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING + ) { + if (!is_numeric($token->getValue())) { + if (strtoupper($token->getValue()) == 'TRUE' || strtoupper($token->getValue()) == 'FALSE') { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL); + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_RANGE); + } + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_NUMBER); + } + + $this->tokens[] = $token; + + continue; + } + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) { + if (strlen($token->getValue()) > 0) { + if (substr($token->getValue(), 0, 1) == '@') { + $token->setValue(substr($token->getValue(), 1)); + } + } + } + + $this->tokens[] = $token; + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php new file mode 100644 index 0000000..68e5eea --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaToken.php @@ -0,0 +1,150 @@ +value = $value; + $this->tokenType = $tokenType; + $this->tokenSubType = $tokenSubType; + } + + /** + * Get Value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set Value. + * + * @param string $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * Get Token Type (represented by TOKEN_TYPE_*). + * + * @return string + */ + public function getTokenType() + { + return $this->tokenType; + } + + /** + * Set Token Type (represented by TOKEN_TYPE_*). + * + * @param string $value + */ + public function setTokenType($value): void + { + $this->tokenType = $value; + } + + /** + * Get Token SubType (represented by TOKEN_SUBTYPE_*). + * + * @return string + */ + public function getTokenSubType() + { + return $this->tokenSubType; + } + + /** + * Set Token SubType (represented by TOKEN_SUBTYPE_*). + * + * @param string $value + */ + public function setTokenSubType($value): void + { + $this->tokenSubType = $value; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php new file mode 100644 index 0000000..0ccf72e --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php @@ -0,0 +1,729 @@ + '#NULL!', + 'divisionbyzero' => '#DIV/0!', + 'value' => '#VALUE!', + 'reference' => '#REF!', + 'name' => '#NAME?', + 'num' => '#NUM!', + 'na' => '#N/A', + 'gettingdata' => '#GETTING_DATA', + ]; + + /** + * Set the Compatibility Mode. + * + * @param string $compatibilityMode Compatibility Mode + * Permitted values are: + * Functions::COMPATIBILITY_EXCEL 'Excel' + * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' + * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' + * + * @return bool (Success or Failure) + */ + public static function setCompatibilityMode($compatibilityMode) + { + if ( + ($compatibilityMode == self::COMPATIBILITY_EXCEL) || + ($compatibilityMode == self::COMPATIBILITY_GNUMERIC) || + ($compatibilityMode == self::COMPATIBILITY_OPENOFFICE) + ) { + self::$compatibilityMode = $compatibilityMode; + + return true; + } + + return false; + } + + /** + * Return the current Compatibility Mode. + * + * @return string Compatibility Mode + * Possible Return values are: + * Functions::COMPATIBILITY_EXCEL 'Excel' + * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' + * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' + */ + public static function getCompatibilityMode() + { + return self::$compatibilityMode; + } + + /** + * Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object). + * + * @param string $returnDateType Return Date Format + * Permitted values are: + * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' + * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' + * Functions::RETURNDATE_EXCEL 'E' + * + * @return bool Success or failure + */ + public static function setReturnDateType($returnDateType) + { + if ( + ($returnDateType == self::RETURNDATE_UNIX_TIMESTAMP) || + ($returnDateType == self::RETURNDATE_PHP_DATETIME_OBJECT) || + ($returnDateType == self::RETURNDATE_EXCEL) + ) { + self::$returnDateType = $returnDateType; + + return true; + } + + return false; + } + + /** + * Return the current Return Date Format for functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object). + * + * @return string Return Date Format + * Possible Return values are: + * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' + * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' + * Functions::RETURNDATE_EXCEL 'E' + */ + public static function getReturnDateType() + { + return self::$returnDateType; + } + + /** + * DUMMY. + * + * @return string #Not Yet Implemented + */ + public static function DUMMY() + { + return '#Not Yet Implemented'; + } + + /** + * DIV0. + * + * @return string #Not Yet Implemented + */ + public static function DIV0() + { + return self::$errorCodes['divisionbyzero']; + } + + /** + * NA. + * + * Excel Function: + * =NA() + * + * Returns the error value #N/A + * #N/A is the error value that means "no value is available." + * + * @return string #N/A! + */ + public static function NA() + { + return self::$errorCodes['na']; + } + + /** + * NaN. + * + * Returns the error value #NUM! + * + * @return string #NUM! + */ + public static function NAN() + { + return self::$errorCodes['num']; + } + + /** + * NAME. + * + * Returns the error value #NAME? + * + * @return string #NAME? + */ + public static function NAME() + { + return self::$errorCodes['name']; + } + + /** + * REF. + * + * Returns the error value #REF! + * + * @return string #REF! + */ + public static function REF() + { + return self::$errorCodes['reference']; + } + + /** + * NULL. + * + * Returns the error value #NULL! + * + * @return string #NULL! + */ + public static function null() + { + return self::$errorCodes['null']; + } + + /** + * VALUE. + * + * Returns the error value #VALUE! + * + * @return string #VALUE! + */ + public static function VALUE() + { + return self::$errorCodes['value']; + } + + public static function isMatrixValue($idx) + { + return (substr_count($idx, '.') <= 1) || (preg_match('/\.[A-Z]/', $idx) > 0); + } + + public static function isValue($idx) + { + return substr_count($idx, '.') == 0; + } + + public static function isCellValue($idx) + { + return substr_count($idx, '.') > 1; + } + + public static function ifCondition($condition) + { + $condition = self::flattenSingleValue($condition); + + if ($condition === '') { + return '=""'; + } + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { + $condition = self::operandSpecialHandling($condition); + if (is_bool($condition)) { + return '=' . ($condition ? 'TRUE' : 'FALSE'); + } elseif (!is_numeric($condition)) { + $condition = Calculation::wrapResult(strtoupper($condition)); + } + + return str_replace('""""', '""', '=' . $condition); + } + preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches); + [, $operator, $operand] = $matches; + + $operand = self::operandSpecialHandling($operand); + if (is_numeric(trim($operand, '"'))) { + $operand = trim($operand, '"'); + } elseif (!is_numeric($operand) && $operand !== 'FALSE' && $operand !== 'TRUE') { + $operand = str_replace('"', '""', $operand); + $operand = Calculation::wrapResult(strtoupper($operand)); + } + + return str_replace('""""', '""', $operator . $operand); + } + + private static function operandSpecialHandling($operand) + { + if (is_numeric($operand) || is_bool($operand)) { + return $operand; + } elseif (strtoupper($operand) === Calculation::getTRUE() || strtoupper($operand) === Calculation::getFALSE()) { + return strtoupper($operand); + } + + // Check for percentage + if (preg_match('/^\-?\d*\.?\d*\s?\%$/', $operand)) { + return ((float) rtrim($operand, '%')) / 100; + } + + // Check for dates + if (($dateValueOperand = Date::stringToExcel($operand)) !== false) { + return $dateValueOperand; + } + + return $operand; + } + + /** + * ERROR_TYPE. + * + * @param mixed $value Value to check + * + * @return int|string + */ + public static function errorType($value = '') + { + $value = self::flattenSingleValue($value); + + $i = 1; + foreach (self::$errorCodes as $errorCode) { + if ($value === $errorCode) { + return $i; + } + ++$i; + } + + return self::NA(); + } + + /** + * IS_BLANK. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isBlank($value = null) + { + if ($value !== null) { + $value = self::flattenSingleValue($value); + } + + return $value === null; + } + + /** + * IS_ERR. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isErr($value = '') + { + $value = self::flattenSingleValue($value); + + return self::isError($value) && (!self::isNa(($value))); + } + + /** + * IS_ERROR. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isError($value = '') + { + $value = self::flattenSingleValue($value); + + if (!is_string($value)) { + return false; + } + + return in_array($value, self::$errorCodes); + } + + /** + * IS_NA. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isNa($value = '') + { + $value = self::flattenSingleValue($value); + + return $value === self::NA(); + } + + /** + * IS_EVEN. + * + * @param mixed $value Value to check + * + * @return bool|string + */ + public static function isEven($value = null) + { + $value = self::flattenSingleValue($value); + + if ($value === null) { + return self::NAME(); + } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { + return self::VALUE(); + } + + return $value % 2 == 0; + } + + /** + * IS_ODD. + * + * @param mixed $value Value to check + * + * @return bool|string + */ + public static function isOdd($value = null) + { + $value = self::flattenSingleValue($value); + + if ($value === null) { + return self::NAME(); + } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { + return self::VALUE(); + } + + return abs($value) % 2 == 1; + } + + /** + * IS_NUMBER. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isNumber($value = null) + { + $value = self::flattenSingleValue($value); + + if (is_string($value)) { + return false; + } + + return is_numeric($value); + } + + /** + * IS_LOGICAL. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isLogical($value = null) + { + $value = self::flattenSingleValue($value); + + return is_bool($value); + } + + /** + * IS_TEXT. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isText($value = null) + { + $value = self::flattenSingleValue($value); + + return is_string($value) && !self::isError($value); + } + + /** + * IS_NONTEXT. + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isNonText($value = null) + { + return !self::isText($value); + } + + /** + * N. + * + * Returns a value converted to a number + * + * @param null|mixed $value The value you want converted + * + * @return number N converts values listed in the following table + * If value is or refers to N returns + * A number That number + * A date The serial number of that date + * TRUE 1 + * FALSE 0 + * An error value The error value + * Anything else 0 + */ + public static function n($value = null) + { + while (is_array($value)) { + $value = array_shift($value); + } + + switch (gettype($value)) { + case 'double': + case 'float': + case 'integer': + return $value; + case 'boolean': + return (int) $value; + case 'string': + // Errors + if ((strlen($value) > 0) && ($value[0] == '#')) { + return $value; + } + + break; + } + + return 0; + } + + /** + * TYPE. + * + * Returns a number that identifies the type of a value + * + * @param null|mixed $value The value you want tested + * + * @return number N converts values listed in the following table + * If value is or refers to N returns + * A number 1 + * Text 2 + * Logical Value 4 + * An error value 16 + * Array or Matrix 64 + */ + public static function TYPE($value = null) + { + $value = self::flattenArrayIndexed($value); + if (is_array($value) && (count($value) > 1)) { + end($value); + $a = key($value); + // Range of cells is an error + if (self::isCellValue($a)) { + return 16; + // Test for Matrix + } elseif (self::isMatrixValue($a)) { + return 64; + } + } elseif (empty($value)) { + // Empty Cell + return 1; + } + $value = self::flattenSingleValue($value); + + if (($value === null) || (is_float($value)) || (is_int($value))) { + return 1; + } elseif (is_bool($value)) { + return 4; + } elseif (is_array($value)) { + return 64; + } elseif (is_string($value)) { + // Errors + if ((strlen($value) > 0) && ($value[0] == '#')) { + return 16; + } + + return 2; + } + + return 0; + } + + /** + * Convert a multi-dimensional array to a simple 1-dimensional array. + * + * @param array|mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArray($array) + { + if (!is_array($array)) { + return (array) $array; + } + + $arrayValues = []; + foreach ($array as $value) { + if (is_array($value)) { + foreach ($value as $val) { + if (is_array($val)) { + foreach ($val as $v) { + $arrayValues[] = $v; + } + } else { + $arrayValues[] = $val; + } + } + } else { + $arrayValues[] = $value; + } + } + + return $arrayValues; + } + + /** + * @param mixed $value + * + * @return null|mixed + */ + public static function scalar($value) + { + if (!is_array($value)) { + return $value; + } + + do { + $value = array_pop($value); + } while (is_array($value)); + + return $value; + } + + /** + * Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing. + * + * @param array|mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArrayIndexed($array) + { + if (!is_array($array)) { + return (array) $array; + } + + $arrayValues = []; + foreach ($array as $k1 => $value) { + if (is_array($value)) { + foreach ($value as $k2 => $val) { + if (is_array($val)) { + foreach ($val as $k3 => $v) { + $arrayValues[$k1 . '.' . $k2 . '.' . $k3] = $v; + } + } else { + $arrayValues[$k1 . '.' . $k2] = $val; + } + } + } else { + $arrayValues[$k1] = $value; + } + } + + return $arrayValues; + } + + /** + * Convert an array to a single scalar value by extracting the first element. + * + * @param mixed $value Array or scalar value + * + * @return mixed + */ + public static function flattenSingleValue($value = '') + { + while (is_array($value)) { + $value = array_shift($value); + } + + return $value; + } + + /** + * ISFORMULA. + * + * @param mixed $cellReference The cell to check + * @param ?Cell $cell The current cell (containing this formula) + * + * @return bool|string + */ + public static function isFormula($cellReference = '', ?Cell $cell = null) + { + if ($cell === null) { + return self::REF(); + } + $cellReference = self::expandDefinedName((string) $cellReference, $cell); + $cellReference = self::trimTrailingRange($cellReference); + + preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches); + + $cellReference = $matches[6] . $matches[7]; + $worksheetName = str_replace("''", "'", trim($matches[2], "'")); + + $worksheet = (!empty($worksheetName)) + ? $cell->getWorksheet()->getParent()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + + return $worksheet->getCell($cellReference)->isFormula(); + } + + public static function expandDefinedName(string $coordinate, Cell $cell): string + { + $worksheet = $cell->getWorksheet(); + $spreadsheet = $worksheet->getParent(); + // Uppercase coordinate + $pCoordinatex = strtoupper($coordinate); + // Eliminate leading equal sign + $pCoordinatex = Worksheet::pregReplace('/^=/', '', $pCoordinatex); + $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); + if ($defined !== null) { + $worksheet2 = $defined->getWorkSheet(); + if (!$defined->isFormula() && $worksheet2 !== null) { + $coordinate = "'" . $worksheet2->getTitle() . "'!" . Worksheet::pregReplace('/^=/', '', $defined->getValue()); + } + } + + return $coordinate; + } + + public static function trimTrailingRange(string $coordinate): string + { + return Worksheet::pregReplace('/:[\\w\$]+$/', '', $coordinate); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php new file mode 100644 index 0000000..8b53464 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -0,0 +1,11 @@ + 0) && ($returnValue == $argCount); + } + + /** + * LOGICAL_OR. + * + * Returns boolean TRUE if any argument is TRUE; returns FALSE if all arguments are FALSE. + * + * Excel Function: + * =OR(logical1[,logical2[, ...]]) + * + * The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays + * or references that contain logical values. + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $args Data values + * + * @return bool|string the logical OR of the arguments + */ + public static function logicalOr(...$args) + { + $args = Functions::flattenArray($args); + + if (count($args) == 0) { + return Functions::VALUE(); + } + + $args = array_filter($args, function ($value) { + return $value !== null || (is_string($value) && trim($value) == ''); + }); + + $returnValue = self::countTrueValues($args); + if (is_string($returnValue)) { + return $returnValue; + } + + return $returnValue > 0; + } + + /** + * LOGICAL_XOR. + * + * Returns the Exclusive Or logical operation for one or more supplied conditions. + * i.e. the Xor function returns TRUE if an odd number of the supplied conditions evaluate to TRUE, + * and FALSE otherwise. + * + * Excel Function: + * =XOR(logical1[,logical2[, ...]]) + * + * The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays + * or references that contain logical values. + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $args Data values + * + * @return bool|string the logical XOR of the arguments + */ + public static function logicalXor(...$args) + { + $args = Functions::flattenArray($args); + + if (count($args) == 0) { + return Functions::VALUE(); + } + + $args = array_filter($args, function ($value) { + return $value !== null || (is_string($value) && trim($value) == ''); + }); + + $returnValue = self::countTrueValues($args); + if (is_string($returnValue)) { + return $returnValue; + } + + return $returnValue % 2 == 1; + } + + /** + * NOT. + * + * Returns the boolean inverse of the argument. + * + * Excel Function: + * =NOT(logical) + * + * The argument must evaluate to a logical value such as TRUE or FALSE + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $logical A value or expression that can be evaluated to TRUE or FALSE + * Or can be an array of values + * + * @return array|bool|string the boolean inverse of the argument + * If an array of values is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function NOT($logical = false) + { + if (is_array($logical)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $logical); + } + + if (is_string($logical)) { + $logical = mb_strtoupper($logical, 'UTF-8'); + if (($logical == 'TRUE') || ($logical == Calculation::getTRUE())) { + return false; + } elseif (($logical == 'FALSE') || ($logical == Calculation::getFALSE())) { + return true; + } + + return Functions::VALUE(); + } + + return !$logical; + } + + /** + * @return int|string + */ + private static function countTrueValues(array $args) + { + $trueValueCount = 0; + + foreach ($args as $arg) { + // Is it a boolean value? + if (is_bool($arg)) { + $trueValueCount += $arg; + } elseif ((is_numeric($arg)) && (!is_string($arg))) { + $trueValueCount += ((int) $arg != 0); + } elseif (is_string($arg)) { + $arg = mb_strtoupper($arg, 'UTF-8'); + if (($arg == 'TRUE') || ($arg == Calculation::getTRUE())) { + $arg = true; + } elseif (($arg == 'FALSE') || ($arg == Calculation::getFALSE())) { + $arg = false; + } else { + return Functions::VALUE(); + } + $trueValueCount += ($arg != 0); + } + } + + return $trueValueCount; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php new file mode 100644 index 0000000..6765048 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -0,0 +1,416 @@ +getMessage(); + } + + // MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type. + if (is_string($lookupValue)) { + $lookupValue = StringHelper::strToLower($lookupValue); + } + + $valueKey = null; + switch ($matchType) { + case self::MATCHTYPE_LARGEST_VALUE: + $valueKey = self::matchLargestValue($lookupArray, $lookupValue, $keySet); + + break; + case self::MATCHTYPE_FIRST_VALUE: + $valueKey = self::matchFirstValue($lookupArray, $lookupValue); + + break; + case self::MATCHTYPE_SMALLEST_VALUE: + default: + $valueKey = self::matchSmallestValue($lookupArray, $lookupValue); + } + + if ($valueKey !== null) { + return ++$valueKey; + } + + // Unsuccessful in finding a match, return #N/A error value + return Functions::NA(); + } + + private static function matchFirstValue($lookupArray, $lookupValue) + { + $wildcardLookup = ((bool) preg_match('/([\?\*])/', $lookupValue)); + $wildcard = WildcardMatch::wildcard($lookupValue); + + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || + (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); + + if ( + $typeMatch && is_string($lookupValue) && + $wildcardLookup && WildcardMatch::compare($lookupArrayValue, $wildcard) + ) { + // wildcard match + return $i; + } elseif ($lookupArrayValue === $lookupValue) { + // exact match + return $i; + } + } + + return null; + } + + private static function matchLargestValue($lookupArray, $lookupValue, $keySet) + { + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || + (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); + + if ($typeMatch && ($lookupArrayValue <= $lookupValue)) { + return array_search($i, $keySet); + } + } + + return null; + } + + private static function matchSmallestValue($lookupArray, $lookupValue) + { + $valueKey = null; + + // The basic algorithm is: + // Iterate and keep the highest match until the next element is smaller than the searched value. + // Return immediately if perfect match is found + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + + if ($lookupArrayValue === $lookupValue) { + // Another "special" case. If a perfect match is found, + // the algorithm gives up immediately + return $i; + } elseif ($typeMatch && $lookupArrayValue >= $lookupValue) { + $valueKey = $i; + } elseif ($typeMatch && $lookupArrayValue < $lookupValue) { + //Excel algorithm gives up immediately if the first element is smaller than the searched value + break; + } + } + + return $valueKey; + } + + private static function validateLookupValue($lookupValue): void + { + // Lookup_value type has to be number, text, or logical values + if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { + throw new Exception(Functions::NA()); + } + } + + private static function validateMatchType($matchType): void + { + // Match_type is 0, 1 or -1 + if ( + ($matchType !== self::MATCHTYPE_FIRST_VALUE) && + ($matchType !== self::MATCHTYPE_LARGEST_VALUE) && ($matchType !== self::MATCHTYPE_SMALLEST_VALUE) + ) { + throw new Exception(Functions::NA()); + } + } + + private static function validateLookupArray($lookupArray): void + { + // Lookup_array should not be empty + $lookupArraySize = count($lookupArray); + if ($lookupArraySize <= 0) { + throw new Exception(Functions::NA()); + } + } + + private static function prepareLookupArray($lookupArray, $matchType) + { + // Lookup_array should contain only number, text, or logical values, or empty (null) cells + foreach ($lookupArray as $i => $value) { + // check the type of the value + if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) { + throw new Exception(Functions::NA()); + } + // Convert strings to lowercase for case-insensitive testing + if (is_string($value)) { + $lookupArray[$i] = StringHelper::strToLower($value); + } + if ( + ($value === null) && + (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE)) + ) { + unset($lookupArray[$i]); + } + } + + return $lookupArray; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php new file mode 100644 index 0000000..f3e6c41 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php @@ -0,0 +1,43 @@ +getWorksheet()->getParent()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + + if ( + $worksheet === null || + !$worksheet->cellExists($cellReference) || + !$worksheet->getCell($cellReference)->isFormula() + ) { + return Functions::NA(); + } + + return $worksheet->getCell($cellReference)->getValue(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php new file mode 100644 index 0000000..7db2780 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -0,0 +1,116 @@ +getMessage(); + } + + $f = array_keys($lookupArray); + $firstRow = reset($f); + if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray))) { + return Functions::REF(); + } + + $firstkey = $f[0] - 1; + $returnColumn = $firstkey + $indexNumber; + $firstColumn = array_shift($f); + $rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); + + if ($rowNumber !== null) { + // otherwise return the appropriate value + return $lookupArray[$returnColumn][Coordinate::stringFromColumnIndex($rowNumber)]; + } + + return Functions::NA(); + } + + /** + * @param mixed $lookupValue The value that you want to match in lookup_array + * @param mixed $column The column to look up + * @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value + */ + private static function hLookupSearch($lookupValue, array $lookupArray, $column, $notExactMatch): ?int + { + $lookupLower = StringHelper::strToLower($lookupValue); + + $rowNumber = null; + foreach ($lookupArray[$column] as $rowKey => $rowData) { + // break if we have passed possible keys + $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); + $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); + $cellDataLower = StringHelper::strToLower($rowData); + + if ( + $notExactMatch && + (($bothNumeric && $rowData > $lookupValue) || ($bothNotNumeric && $cellDataLower > $lookupLower)) + ) { + break; + } + + $rowNumber = self::checkMatch( + $bothNumeric, + $bothNotNumeric, + $notExactMatch, + Coordinate::columnIndexFromString($rowKey), + $cellDataLower, + $lookupLower, + $rowNumber + ); + } + + return $rowNumber; + } + + private static function convertLiteralArray(array $lookupArray): array + { + if (array_key_exists(0, $lookupArray)) { + $lookupArray2 = []; + $row = 0; + foreach ($lookupArray as $arrayVal) { + ++$row; + if (!is_array($arrayVal)) { + $arrayVal = [$arrayVal]; + } + $arrayVal2 = []; + foreach ($arrayVal as $key2 => $val2) { + $index = Coordinate::stringFromColumnIndex($key2 + 1); + $arrayVal2[$index] = $val2; + } + $lookupArray2[$row] = $arrayVal2; + } + $lookupArray = $lookupArray2; + } + + return $lookupArray; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php new file mode 100644 index 0000000..28e8df8 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -0,0 +1,74 @@ +getWorkSheet(); + $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); + $value = preg_replace('/^=/', '', $namedRange->getValue()); + self::adjustSheetTitle($sheetTitle, $value); + $cellAddress1 = $sheetTitle . $value; + $cellAddress = $cellAddress1; + $a1 = self::CELLADDRESS_USE_A1; + } + if (strpos($cellAddress, ':') !== false) { + [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); + } + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + + return [$cellAddress1, $cellAddress2, $cellAddress]; + } + + public static function extractWorksheet(string $cellAddress, Cell $cell): array + { + $sheetName = ''; + if (strpos($cellAddress, '!') !== false) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $sheetName = trim($sheetName, "'"); + } + + $worksheet = ($sheetName !== '') + ? $cell->getWorksheet()->getParent()->getSheetByName($sheetName) + : $cell->getWorksheet(); + + return [$cellAddress, $worksheet, $sheetName]; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php new file mode 100644 index 0000000..1448c08 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php @@ -0,0 +1,40 @@ +getHyperlink()->setUrl($linkURL); + $cell->getHyperlink()->setTooltip($displayName); + + return $displayName; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php new file mode 100644 index 0000000..b5a3541 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -0,0 +1,97 @@ +getMessage(); + } + + [$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + + if ( + (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || + (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches))) + ) { + return Functions::REF(); + } + + return self::extractRequiredCells($worksheet, $cellAddress); + } + + /** + * Extract range values. + * + * @return mixed Array of values in range if range contains more than one element. + * Otherwise, a single value is returned. + */ + private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress) + { + return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null) + ->extractCellRange($cellAddress, $worksheet, false); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php new file mode 100644 index 0000000..e21d35d --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php @@ -0,0 +1,105 @@ + 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) { + $lookupVector = LookupRef\Matrix::transpose($lookupVector); + $lookupRows = self::rowCount($lookupVector); + $lookupColumns = self::columnCount($lookupVector); + } + + $resultVector = self::verifyResultVector($lookupVector, $resultVector); + + if ($lookupRows === 2 && !$hasResultVector) { + $resultVector = array_pop($lookupVector); + $lookupVector = array_shift($lookupVector); + } + + if ($lookupColumns !== 2) { + $lookupVector = self::verifyLookupValues($lookupVector, $resultVector); + } + + return VLookup::lookup($lookupValue, $lookupVector, 2); + } + + private static function verifyLookupValues(array $lookupVector, array $resultVector): array + { + foreach ($lookupVector as &$value) { + if (is_array($value)) { + $k = array_keys($value); + $key1 = $key2 = array_shift($k); + ++$key2; + $dataValue1 = $value[$key1]; + } else { + $key1 = 0; + $key2 = 1; + $dataValue1 = $value; + } + + $dataValue2 = array_shift($resultVector); + if (is_array($dataValue2)) { + $dataValue2 = array_shift($dataValue2); + } + $value = [$key1 => $dataValue1, $key2 => $dataValue2]; + } + unset($value); + + return $lookupVector; + } + + private static function verifyResultVector(array $lookupVector, $resultVector) + { + if ($resultVector === null) { + $resultVector = $lookupVector; + } + + $resultRows = self::rowCount($resultVector); + $resultColumns = self::columnCount($resultVector); + + // we correctly orient our results + if ($resultRows === 1 && $resultColumns > 1) { + $resultVector = LookupRef\Matrix::transpose($resultVector); + } + + return $resultVector; + } + + private static function rowCount(array $dataArray): int + { + return count($dataArray); + } + + private static function columnCount(array $dataArray): int + { + $rowKeys = array_keys($dataArray); + $row = array_shift($rowKeys); + + return count($dataArray[$row]); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php new file mode 100644 index 0000000..80fc99a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -0,0 +1,48 @@ +getMessage(); + } + + if (!is_array($matrix) || ($rowNum > count($matrix))) { + return Functions::REF(); + } + + $rowKeys = array_keys($matrix); + $columnKeys = @array_keys($matrix[$rowKeys[0]]); + + if ($columnNum > count($columnKeys)) { + return Functions::REF(); + } + + if ($columnNum === 0) { + return self::extractRowValue($matrix, $rowKeys, $rowNum); + } + + $columnNum = $columnKeys[--$columnNum]; + if ($rowNum === 0) { + return array_map( + function ($value) { + return [$value]; + }, + array_column($matrix, $columnNum) + ); + } + $rowNum = $rowKeys[--$rowNum]; + + return $matrix[$rowNum][$columnNum]; + } + + private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum) + { + if ($rowNum === 0) { + return $matrix; + } + + $rowNum = $rowKeys[--$rowNum]; + $row = $matrix[$rowNum]; + if (is_array($row)) { + return [$rowNum => $row]; + } + + return $row; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php new file mode 100644 index 0000000..7e33f55 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -0,0 +1,136 @@ +getParent() : null) + ->extractCellRange($cellAddress, $worksheet, false); + } + + private static function extractWorksheet($cellAddress, Cell $cell): array + { + $sheetName = ''; + if (strpos($cellAddress, '!') !== false) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $sheetName = trim($sheetName, "'"); + } + + $worksheet = ($sheetName !== '') + ? $cell->getWorksheet()->getParent()->getSheetByName($sheetName) + : $cell->getWorksheet(); + + return [$cellAddress, $worksheet]; + } + + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) + { + $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; + if (($width !== null) && (!is_object($width))) { + $endCellColumn = $startCellColumn + (int) $width - 1; + } else { + $endCellColumn += (int) $columns; + } + + return $endCellColumn; + } + + private static function adustEndCellRowForHeight($height, int $startCellRow, $rows, $endCellRow): int + { + if (($height !== null) && (!is_object($height))) { + $endCellRow = $startCellRow + (int) $height - 1; + } else { + $endCellRow += (int) $rows; + } + + return $endCellRow; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php new file mode 100644 index 0000000..993154c --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -0,0 +1,1520 @@ +getMessage(); + } + + return exp($number); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php new file mode 100644 index 0000000..b6883e2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php @@ -0,0 +1,125 @@ +getMessage(); + } + + $factLoop = floor($factVal); + if ($factVal > $factLoop) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return Statistical\Distributions\Gamma::gammaValue($factVal + 1); + } + } + + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop--; + } + + return $factorial; + } + + /** + * FACTDOUBLE. + * + * Returns the double factorial of a number. + * + * Excel Function: + * FACTDOUBLE(factVal) + * + * @param array|float $factVal Factorial Value, or can be an array of numbers + * + * @return array|float|int|string Double Factorial, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function factDouble($factVal) + { + if (is_array($factVal)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $factVal); + } + + try { + $factVal = Helpers::validateNumericNullSubstitution($factVal, 0); + Helpers::validateNotNegative($factVal); + } catch (Exception $e) { + return $e->getMessage(); + } + + $factLoop = floor($factVal); + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop; + $factLoop -= 2; + } + + return $factorial; + } + + /** + * MULTINOMIAL. + * + * Returns the ratio of the factorial of a sum of values to the product of factorials. + * + * @param mixed[] $args An array of mixed values for the Data Series + * + * @return float|string The result, or a string containing an error + */ + public static function multinomial(...$args) + { + $summer = 0; + $divisor = 1; + + try { + // Loop through arguments + foreach (Functions::flattenArray($args) as $argx) { + $arg = Helpers::validateNumericNullSubstitution($argx, null); + Helpers::validateNotNegative($arg); + $arg = (int) $arg; + $summer += $arg; + $divisor *= self::fact($arg); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $summer = self::fact($summer); + + return $summer / $divisor; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php new file mode 100644 index 0000000..16cde98 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php @@ -0,0 +1,194 @@ +getMessage(); + } + + return self::argumentsOk((float) $number, (float) $significance); + } + + /** + * FLOOR.MATH. + * + * Round a number down to the nearest integer or to the nearest multiple of significance. + * + * Excel Function: + * FLOOR.MATH(number[,significance[,mode]]) + * + * @param mixed $number Number to round + * Or can be an array of values + * @param mixed $significance Significance + * Or can be an array of values + * @param mixed $mode direction to round negative numbers + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function math($number, $significance = null, $mode = 0) + { + if (is_array($number) || is_array($significance) || is_array($mode)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + $mode = Helpers::validateNumericNullSubstitution($mode, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::argsOk((float) $number, (float) $significance, (int) $mode); + } + + /** + * FLOOR.PRECISE. + * + * Rounds number down, toward zero, to the nearest multiple of significance. + * + * Excel Function: + * FLOOR.PRECISE(number[,significance]) + * + * @param array|float $number Number to round + * Or can be an array of values + * @param array|float $significance Significance + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function precise($number, $significance = 1) + { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::argumentsOkPrecise((float) $number, (float) $significance); + } + + /** + * Avoid Scrutinizer problems concerning complexity. + * + * @return float|string + */ + private static function argumentsOkPrecise(float $number, float $significance) + { + if ($significance == 0.0) { + return Functions::DIV0(); + } + if ($number == 0.0) { + return 0.0; + } + + return floor($number / abs($significance)) * abs($significance); + } + + /** + * Avoid Scrutinizer complexity problems. + * + * @return float|string Rounded Number, or a string containing an error + */ + private static function argsOk(float $number, float $significance, int $mode) + { + if (!$significance) { + return Functions::DIV0(); + } + if (!$number) { + return 0.0; + } + if (self::floorMathTest($number, $significance, $mode)) { + return ceil($number / $significance) * $significance; + } + + return floor($number / $significance) * $significance; + } + + /** + * Let FLOORMATH complexity pass Scrutinizer. + */ + private static function floorMathTest(float $number, float $significance, int $mode): bool + { + return Helpers::returnSign($significance) == -1 || (Helpers::returnSign($number) == -1 && !empty($mode)); + } + + /** + * Avoid Scrutinizer problems concerning complexity. + * + * @return float|string + */ + private static function argumentsOk(float $number, float $significance) + { + if ($significance == 0.0) { + return Functions::DIV0(); + } + if ($number == 0.0) { + return 0.0; + } + if (Helpers::returnSign($significance) == 1) { + return floor($number / $significance) * $significance; + } + if (Helpers::returnSign($number) == -1 && Helpers::returnSign($significance) == -1) { + return floor($number / $significance) * $significance; + } + + return Functions::NAN(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php new file mode 100644 index 0000000..1dd52fa --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php @@ -0,0 +1,69 @@ +getMessage(); + } + + if (count($arrayArgs) <= 0) { + return Functions::VALUE(); + } + $gcd = (int) array_pop($arrayArgs); + do { + $gcd = self::evaluateGCD($gcd, (int) array_pop($arrayArgs)); + } while (!empty($arrayArgs)); + + return $gcd; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php new file mode 100644 index 0000000..b89644a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -0,0 +1,129 @@ += 0. + * + * @param float|int $number + */ + public static function validateNotNegative($number, ?string $except = null): void + { + if ($number >= 0) { + return; + } + + throw new Exception($except ?? Functions::NAN()); + } + + /** + * Confirm number > 0. + * + * @param float|int $number + */ + public static function validatePositive($number, ?string $except = null): void + { + if ($number > 0) { + return; + } + + throw new Exception($except ?? Functions::NAN()); + } + + /** + * Confirm number != 0. + * + * @param float|int $number + */ + public static function validateNotZero($number): void + { + if ($number) { + return; + } + + throw new Exception(Functions::DIV0()); + } + + public static function returnSign(float $number): int + { + return $number ? (($number > 0) ? 1 : -1) : 0; + } + + public static function getEven(float $number): float + { + $significance = 2 * self::returnSign($number); + + return $significance ? (ceil($number / $significance) * $significance) : 0; + } + + /** + * Return NAN or value depending on argument. + * + * @param float $result Number + * + * @return float|string + */ + public static function numberOrNan($result) + { + return is_nan($result) ? Functions::NAN() : $result; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php new file mode 100644 index 0000000..f7f7764 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php @@ -0,0 +1,40 @@ +getMessage(); + } + + return (int) floor($number); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php new file mode 100644 index 0000000..46e9816 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php @@ -0,0 +1,110 @@ + 1; --$i) { + if (($value % $i) == 0) { + $factorArray = array_merge($factorArray, self::factors($value / $i)); + $factorArray = array_merge($factorArray, self::factors($i)); + if ($i <= sqrt($value)) { + break; + } + } + } + if (!empty($factorArray)) { + rsort($factorArray); + + return $factorArray; + } + + return [(int) $value]; + } + + /** + * LCM. + * + * Returns the lowest common multiplier of a series of numbers + * The least common multiple is the smallest positive integer that is a multiple + * of all integer arguments number1, number2, and so on. Use LCM to add fractions + * with different denominators. + * + * Excel Function: + * LCM(number1[,number2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return int|string Lowest Common Multiplier, or a string containing an error + */ + public static function evaluate(...$args) + { + try { + $arrayArgs = []; + $anyZeros = 0; + $anyNonNulls = 0; + foreach (Functions::flattenArray($args) as $value1) { + $anyNonNulls += (int) ($value1 !== null); + $value = Helpers::validateNumericNullSubstitution($value1, 1); + Helpers::validateNotNegative($value); + $arrayArgs[] = (int) $value; + $anyZeros += (int) !((bool) $value); + } + self::testNonNulls($anyNonNulls); + if ($anyZeros) { + return 0; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $returnValue = 1; + $allPoweredFactors = []; + // Loop through arguments + foreach ($arrayArgs as $value) { + $myFactors = self::factors(floor($value)); + $myCountedFactors = array_count_values($myFactors); + $myPoweredFactors = []; + foreach ($myCountedFactors as $myCountedFactor => $myCountedPower) { + $myPoweredFactors[$myCountedFactor] = $myCountedFactor ** $myCountedPower; + } + self::processPoweredFactors($allPoweredFactors, $myPoweredFactors); + } + foreach ($allPoweredFactors as $allPoweredFactor) { + $returnValue *= (int) $allPoweredFactor; + } + + return $returnValue; + } + + private static function processPoweredFactors(array &$allPoweredFactors, array &$myPoweredFactors): void + { + foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { + if (isset($allPoweredFactors[$myPoweredValue])) { + if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } else { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } + } + + private static function testNonNulls(int $anyNonNulls): void + { + if (!$anyNonNulls) { + throw new Exception(Functions::VALUE()); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php new file mode 100644 index 0000000..7b07f09 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php @@ -0,0 +1,102 @@ +getMessage(); + } + + return log($number, $base); + } + + /** + * LOG10. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * Or can be an array of values + * + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function base10($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log10($number); + } + + /** + * LN. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * Or can be an array of values + * + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function natural($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log($number); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php new file mode 100644 index 0000000..474f862 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php @@ -0,0 +1,179 @@ +getMessage(); + } + + if ($step === 0) { + return array_chunk( + array_fill(0, $rows * $columns, $start), + max($columns, 1) + ); + } + + return array_chunk( + range($start, $start + (($rows * $columns - 1) * $step), $step), + max($columns, 1) + ); + } + + /** + * MDETERM. + * + * Returns the matrix determinant of an array. + * + * Excel Function: + * MDETERM(array) + * + * @param mixed $matrixValues A matrix of values + * + * @return float|string The result, or a string containing an error + */ + public static function determinant($matrixValues) + { + try { + $matrix = self::getMatrix($matrixValues); + + return $matrix->determinant(); + } catch (MatrixException $ex) { + return Functions::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MINVERSE. + * + * Returns the inverse matrix for the matrix stored in an array. + * + * Excel Function: + * MINVERSE(array) + * + * @param mixed $matrixValues A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function inverse($matrixValues) + { + try { + $matrix = self::getMatrix($matrixValues); + + return $matrix->inverse()->toArray(); + } catch (MatrixDiv0Exception $e) { + return Functions::NAN(); + } catch (MatrixException $e) { + return Functions::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MMULT. + * + * @param mixed $matrixData1 A matrix of values + * @param mixed $matrixData2 A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function multiply($matrixData1, $matrixData2) + { + try { + $matrixA = self::getMatrix($matrixData1); + $matrixB = self::getMatrix($matrixData2); + + return $matrixA->multiply($matrixB)->toArray(); + } catch (MatrixException $ex) { + return Functions::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MUnit. + * + * @param mixed $dimension Number of rows and columns + * + * @return array|string The result, or a string containing an error + */ + public static function identity($dimension) + { + try { + $dimension = (int) Helpers::validateNumericNullBool($dimension); + Helpers::validatePositive($dimension, Functions::VALUE()); + $matrix = Builder::createIdentityMatrix($dimension, 0)->toArray(); + + return $matrix; + } catch (Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php new file mode 100644 index 0000000..4e48151 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -0,0 +1,163 @@ +getMessage(); + } + + if (($dividend < 0.0) && ($divisor > 0.0)) { + return $divisor - fmod(abs($dividend), $divisor); + } + if (($dividend > 0.0) && ($divisor < 0.0)) { + return $divisor + fmod($dividend, abs($divisor)); + } + + return fmod($dividend, $divisor); + } + + /** + * POWER. + * + * Computes x raised to the power y. + * + * @param array|float|int $x + * Or can be an array of values + * @param array|float|int $y + * Or can be an array of values + * + * @return array|float|int|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function power($x, $y) + { + if (is_array($x) || is_array($y)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $y); + } + + try { + $x = Helpers::validateNumericNullBool($x); + $y = Helpers::validateNumericNullBool($y); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if (!$x && !$y) { + return Functions::NAN(); + } + if (!$x && $y < 0.0) { + return Functions::DIV0(); + } + + // Return + $result = $x ** $y; + + return Helpers::numberOrNan($result); + } + + /** + * PRODUCT. + * + * PRODUCT returns the product of all the values and cells referenced in the argument list. + * + * Excel Function: + * PRODUCT(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string + */ + public static function product(...$args) + { + // Return value + $returnValue = null; + + // Loop through arguments + foreach (Functions::flattenArray($args) as $arg) { + // Is it a numeric value? + if (is_numeric($arg)) { + if ($returnValue === null) { + $returnValue = $arg; + } else { + $returnValue *= $arg; + } + } else { + return Functions::VALUE(); + } + } + + // Return + if ($returnValue === null) { + return 0; + } + + return $returnValue; + } + + /** + * QUOTIENT. + * + * QUOTIENT function returns the integer portion of a division. Numerator is the divided number + * and denominator is the divisor. + * + * Excel Function: + * QUOTIENT(value1,value2) + * + * @param mixed $numerator Expect float|int + * Or can be an array of values + * @param mixed $denominator Expect float|int + * Or can be an array of values + * + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function quotient($numerator, $denominator) + { + if (is_array($numerator) || is_array($denominator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numerator, $denominator); + } + + try { + $numerator = Helpers::validateNumericNullSubstitution($numerator, 0); + $denominator = Helpers::validateNumericNullSubstitution($denominator, 0); + Helpers::validateNotZero($denominator); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (int) ($numerator / $denominator); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php new file mode 100644 index 0000000..d4d0c7e --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php @@ -0,0 +1,131 @@ + 0)) { + $aCount = Counts::COUNT($aArgs); + if (Minimum::min($aArgs) > 0) { + return $aMean ** (1 / $aCount); + } + } + + return Functions::NAN(); + } + + /** + * HARMEAN. + * + * Returns the harmonic mean of a data set. The harmonic mean is the reciprocal of the + * arithmetic mean of reciprocals. + * + * Excel Function: + * HARMEAN(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string + */ + public static function harmonic(...$args) + { + // Loop through arguments + $aArgs = Functions::flattenArray($args); + if (Minimum::min($aArgs) < 0) { + return Functions::NAN(); + } + + $returnValue = 0; + $aCount = 0; + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + if ($arg <= 0) { + return Functions::NAN(); + } + $returnValue += (1 / $arg); + ++$aCount; + } + } + + // Return + if ($aCount > 0) { + return 1 / ($returnValue / $aCount); + } + + return Functions::NA(); + } + + /** + * TRIMMEAN. + * + * Returns the mean of the interior of a data set. TRIMMEAN calculates the mean + * taken by excluding a percentage of data points from the top and bottom tails + * of a data set. + * + * Excel Function: + * TRIMEAN(value1[,value2[, ...]], $discard) + * + * @param mixed $args Data values + * + * @return float|string + */ + public static function trim(...$args) + { + $aArgs = Functions::flattenArray($args); + + // Calculate + $percent = array_pop($aArgs); + + if ((is_numeric($percent)) && (!is_string($percent))) { + if (($percent < 0) || ($percent > 1)) { + return Functions::NAN(); + } + + $mArgs = []; + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $mArgs[] = $arg; + } + } + + $discard = floor(Counts::COUNT($mArgs) * $percent / 2); + sort($mArgs); + + for ($i = 0; $i < $discard; ++$i) { + array_pop($mArgs); + array_shift($mArgs); + } + + return Averages::average($mArgs); + } + + return Functions::VALUE(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php new file mode 100644 index 0000000..57ef00a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php @@ -0,0 +1,24 @@ + 1.0) { + throw new Exception(Functions::NAN()); + } + + return $probability; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php new file mode 100644 index 0000000..ca55857 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php @@ -0,0 +1,55 @@ +getMessage(); + } + + if (($value < 0) || ($lambda < 0)) { + return Functions::NAN(); + } + + if ($cumulative === true) { + return 1 - exp(0 - $value * $lambda); + } + + return $lambda * exp(0 - $value * $lambda); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php new file mode 100644 index 0000000..0fd0aab --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php @@ -0,0 +1,64 @@ +getMessage(); + } + + if ($value < 0 || $u < 1 || $v < 1) { + return Functions::NAN(); + } + + if ($cumulative) { + $adjustedValue = ($u * $value) / ($u * $value + $v); + + return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2); + } + + return (Gamma::gammaValue(($v + $u) / 2) / + (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2))) * + (($u / $v) ** ($u / 2)) * + (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2))); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php new file mode 100644 index 0000000..8d1b5ce --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php @@ -0,0 +1,74 @@ +getMessage(); + } + + if (($value <= -1) || ($value >= 1)) { + return Functions::NAN(); + } + + return 0.5 * log((1 + $value) / (1 - $value)); + } + + /** + * FISHERINV. + * + * Returns the inverse of the Fisher transformation. Use this transformation when + * analyzing correlations between ranges or arrays of data. If y = FISHER(x), then + * FISHERINV(y) = x. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse($probability) + { + if (is_array($probability)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $probability); + } + + try { + DistributionValidations::validateFloat($probability); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (exp(2 * $probability) - 1) / (exp(2 * $probability) + 1); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php new file mode 100644 index 0000000..adf5342 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -0,0 +1,151 @@ +getMessage(); + } + + if ((((int) $value) == ((float) $value)) && $value <= 0.0) { + return Functions::NAN(); + } + + return self::gammaValue($value); + } + + /** + * GAMMADIST. + * + * Returns the gamma distribution. + * + * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $a Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $b Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution($value, $a, $b, $cumulative) + { + if (is_array($value) || is_array($a) || is_array($b) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $a, $b, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $a = DistributionValidations::validateFloat($a); + $b = DistributionValidations::validateFloat($b); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($a <= 0) || ($b <= 0)) { + return Functions::NAN(); + } + + return self::calculateDistribution($value, $a, $b, $cumulative); + } + + /** + * GAMMAINV. + * + * Returns the inverse of the Gamma distribution. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values + * + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse($probability, $alpha, $beta) + { + if (is_array($probability) || is_array($alpha) || is_array($beta)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $alpha = DistributionValidations::validateFloat($alpha); + $beta = DistributionValidations::validateFloat($beta); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($alpha <= 0.0) || ($beta <= 0.0)) { + return Functions::NAN(); + } + + return self::calculateInverse($probability, $alpha, $beta); + } + + /** + * GAMMALN. + * + * Returns the natural logarithm of the gamma function. + * + * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values + * + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ln($value) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = DistributionValidations::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($value <= 0) { + return Functions::NAN(); + } + + return log(self::gammaValue($value)); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php new file mode 100644 index 0000000..89170f7 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -0,0 +1,381 @@ + Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = self::calculateDistribution($x, $alpha, $beta, true); + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + $pdf = self::calculateDistribution($x, $alpha, $beta, false); + // Avoid division by zero + if ($pdf !== 0.0) { + $dx = $error / $pdf; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i === self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return $x; + } + + // + // Implementation of the incomplete Gamma function + // + public static function incompleteGamma(float $a, float $x): float + { + static $max = 32; + $summer = 0; + for ($n = 0; $n <= $max; ++$n) { + $divisor = $a; + for ($i = 1; $i <= $n; ++$i) { + $divisor *= ($a + $i); + } + $summer += ($x ** $n / $divisor); + } + + return $x ** $a * exp(0 - $x) * $summer; + } + + // + // Implementation of the Gamma function + // + public static function gammaValue(float $value): float + { + if ($value == 0.0) { + return 0; + } + + static $p0 = 1.000000000190015; + static $p = [ + 1 => 76.18009172947146, + 2 => -86.50532032941677, + 3 => 24.01409824083091, + 4 => -1.231739572450155, + 5 => 1.208650973866179e-3, + 6 => -5.395239384953e-6, + ]; + + $y = $x = $value; + $tmp = $x + 5.5; + $tmp -= ($x + 0.5) * log($tmp); + + $summer = $p0; + for ($j = 1; $j <= 6; ++$j) { + $summer += ($p[$j] / ++$y); + } + + return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x)); + } + + /** + * logGamma function. + * + * @version 1.1 + * + * @author Jaco van Kooten + * + * Original author was Jaco van Kooten. Ported to PHP by Paul Meagher. + * + * The natural logarithm of the gamma function.
+ * Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz
+ * Applied Mathematics Division
+ * Argonne National Laboratory
+ * Argonne, IL 60439
+ *

+ * References: + *

    + *
  1. W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural + * Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.
  2. + *
  3. K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.
  4. + *
  5. Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.
  6. + *
+ *

+ *

+ * From the original documentation: + *

+ *

+ * This routine calculates the LOG(GAMMA) function for a positive real argument X. + * Computation is based on an algorithm outlined in references 1 and 2. + * The program uses rational functions that theoretically approximate LOG(GAMMA) + * to at least 18 significant decimal digits. The approximation for X > 12 is from + * reference 3, while approximations for X < 12.0 are similar to those in reference + * 1, but are unpublished. The accuracy achieved depends on the arithmetic system, + * the compiler, the intrinsic functions, and proper selection of the + * machine-dependent constants. + *

+ *

+ * Error returns:
+ * The program returns the value XINF for X .LE. 0.0 or when overflow would occur. + * The computation is believed to be free of underflow and overflow. + *

+ * + * @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305 + */ + + // Log Gamma related constants + private const LG_D1 = -0.5772156649015328605195174; + + private const LG_D2 = 0.4227843350984671393993777; + + private const LG_D4 = 1.791759469228055000094023; + + private const LG_P1 = [ + 4.945235359296727046734888, + 201.8112620856775083915565, + 2290.838373831346393026739, + 11319.67205903380828685045, + 28557.24635671635335736389, + 38484.96228443793359990269, + 26377.48787624195437963534, + 7225.813979700288197698961, + ]; + + private const LG_P2 = [ + 4.974607845568932035012064, + 542.4138599891070494101986, + 15506.93864978364947665077, + 184793.2904445632425417223, + 1088204.76946882876749847, + 3338152.967987029735917223, + 5106661.678927352456275255, + 3074109.054850539556250927, + ]; + + private const LG_P4 = [ + 14745.02166059939948905062, + 2426813.369486704502836312, + 121475557.4045093227939592, + 2663432449.630976949898078, + 29403789566.34553899906876, + 170266573776.5398868392998, + 492612579337.743088758812, + 560625185622.3951465078242, + ]; + + private const LG_Q1 = [ + 67.48212550303777196073036, + 1113.332393857199323513008, + 7738.757056935398733233834, + 27639.87074403340708898585, + 54993.10206226157329794414, + 61611.22180066002127833352, + 36351.27591501940507276287, + 8785.536302431013170870835, + ]; + + private const LG_Q2 = [ + 183.0328399370592604055942, + 7765.049321445005871323047, + 133190.3827966074194402448, + 1136705.821321969608938755, + 5267964.117437946917577538, + 13467014.54311101692290052, + 17827365.30353274213975932, + 9533095.591844353613395747, + ]; + + private const LG_Q4 = [ + 2690.530175870899333379843, + 639388.5654300092398984238, + 41355999.30241388052042842, + 1120872109.61614794137657, + 14886137286.78813811542398, + 101680358627.2438228077304, + 341747634550.7377132798597, + 446315818741.9713286462081, + ]; + + private const LG_C = [ + -0.001910444077728, + 8.4171387781295e-4, + -5.952379913043012e-4, + 7.93650793500350248e-4, + -0.002777777777777681622553, + 0.08333333333333333331554247, + 0.0057083835261, + ]; + + // Rough estimate of the fourth root of logGamma_xBig + private const LG_FRTBIG = 2.25e76; + + private const PNT68 = 0.6796875; + + // Function cache for logGamma + private static $logGammaCacheResult = 0.0; + + private static $logGammaCacheX = 0.0; + + public static function logGamma(float $x): float + { + if ($x == self::$logGammaCacheX) { + return self::$logGammaCacheResult; + } + + $y = $x; + if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) { + if ($y <= self::EPS) { + $res = -log($y); + } elseif ($y <= 1.5) { + $res = self::logGamma1($y); + } elseif ($y <= 4.0) { + $res = self::logGamma2($y); + } elseif ($y <= 12.0) { + $res = self::logGamma3($y); + } else { + $res = self::logGamma4($y); + } + } else { + // -------------------------- + // Return for bad arguments + // -------------------------- + $res = self::MAX_VALUE; + } + + // ------------------------------ + // Final adjustments and return + // ------------------------------ + self::$logGammaCacheX = $x; + self::$logGammaCacheResult = $res; + + return $res; + } + + private static function logGamma1(float $y) + { + // --------------------- + // EPS .LT. X .LE. 1.5 + // --------------------- + if ($y < self::PNT68) { + $corr = -log($y); + $xm1 = $y; + } else { + $corr = 0.0; + $xm1 = $y - 1.0; + } + + $xden = 1.0; + $xnum = 0.0; + if ($y <= 0.5 || $y >= self::PNT68) { + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm1 + self::LG_P1[$i]; + $xden = $xden * $xm1 + self::LG_Q1[$i]; + } + + return $corr + $xm1 * (self::LG_D1 + $xm1 * ($xnum / $xden)); + } + + $xm2 = $y - 1.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $corr + $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + private static function logGamma2(float $y) + { + // --------------------- + // 1.5 .LT. X .LE. 4.0 + // --------------------- + $xm2 = $y - 2.0; + $xden = 1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + protected static function logGamma3(float $y) + { + // ---------------------- + // 4.0 .LT. X .LE. 12.0 + // ---------------------- + $xm4 = $y - 4.0; + $xden = -1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm4 + self::LG_P4[$i]; + $xden = $xden * $xm4 + self::LG_Q4[$i]; + } + + return self::LG_D4 + $xm4 * ($xnum / $xden); + } + + protected static function logGamma4(float $y) + { + // --------------------------------- + // Evaluate for argument .GE. 12.0 + // --------------------------------- + $res = 0.0; + if ($y <= self::LG_FRTBIG) { + $res = self::LG_C[6]; + $ysq = $y * $y; + for ($i = 0; $i < 6; ++$i) { + $res = $res / $ysq + self::LG_C[$i]; + } + $res /= $y; + $corr = log($y); + $res = $res + log(self::SQRT2PI) - 0.5 * $corr; + $res += $y * ($corr - 1.0); + } + + return $res; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php new file mode 100644 index 0000000..bb7adf7 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php @@ -0,0 +1,76 @@ +getMessage(); + } + + if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) { + return Functions::NAN(); + } + if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) { + return Functions::NAN(); + } + if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) { + return Functions::NAN(); + } + + $successesPopulationAndSample = (float) Combinations::withoutRepetition($populationSuccesses, $sampleSuccesses); + $numbersPopulationAndSample = (float) Combinations::withoutRepetition($populationNumber, $sampleNumber); + $adjustedPopulationAndSample = (float) Combinations::withoutRepetition( + $populationNumber - $populationSuccesses, + $sampleNumber - $sampleSuccesses + ); + + return $successesPopulationAndSample * $adjustedPopulationAndSample / $numbersPopulationAndSample; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php new file mode 100644 index 0000000..4bf2e0c --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php @@ -0,0 +1,137 @@ +getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return Functions::NAN(); + } + + return StandardNormal::cumulative((log($value) - $mean) / $stdDev); + } + + /** + * LOGNORM.DIST. + * + * Returns the lognormal distribution of x, where ln(x) is normally distributed + * with parameters mean and standard_dev. + * + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * @param mixed $mean Mean value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution($value, $mean, $stdDev, $cumulative = false) + { + if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return Functions::NAN(); + } + + if ($cumulative === true) { + return StandardNormal::distribution((log($value) - $mean) / $stdDev, true); + } + + return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) * + exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2))); + } + + /** + * LOGINV. + * + * Returns the inverse of the lognormal cumulative distribution + * + * @param mixed $probability Float probability for which we want the value + * Or can be an array of values + * @param mixed $mean Mean Value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + * + * @TODO Try implementing P J Acklam's refinement algorithm for greater + * accuracy if I can get my head round the mathematics + * (as described at) http://home.online.no/~pjacklam/notes/invnorm/ + */ + public static function inverse($probability, $mean, $stdDev) + { + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev <= 0) { + return Functions::NAN(); + } + + return exp($mean + $stdDev * StandardNormal::inverse($probability)); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php new file mode 100644 index 0000000..2621167 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -0,0 +1,62 @@ +callback = $callback; + } + + public function execute(float $probability) + { + $xLo = 100; + $xHi = 0; + + $dx = 1; + $x = $xNew = 1; + $i = 0; + + while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = call_user_func($this->callback, $x); + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + // Avoid division by zero + if ($result != 0.0) { + $dx = $error / $result; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i == self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return $x; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php new file mode 100644 index 0000000..f876ed6 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php @@ -0,0 +1,180 @@ +getMessage(); + } + + if ($stdDev < 0) { + return Functions::NAN(); + } + + if ($cumulative) { + return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2)))); + } + + return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev)))); + } + + /** + * NORMINV. + * + * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. + * + * @param mixed $probability Float probability for which we want the value + * Or can be an array of values + * @param mixed $mean Mean Value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse($probability, $mean, $stdDev) + { + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev < 0) { + return Functions::NAN(); + } + + return (self::inverseNcdf($probability) * $stdDev) + $mean; + } + + /* + * inverse_ncdf.php + * ------------------- + * begin : Friday, January 16, 2004 + * copyright : (C) 2004 Michael Nickerson + * email : nickersonm@yahoo.com + * + */ + private static function inverseNcdf($p) + { + // Inverse ncdf approximation by Peter J. Acklam, implementation adapted to + // PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as + // a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html + // I have not checked the accuracy of this implementation. Be aware that PHP + // will truncate the coeficcients to 14 digits. + + // You have permission to use and distribute this function freely for + // whatever purpose you want, but please show common courtesy and give credit + // where credit is due. + + // Input paramater is $p - probability - where 0 < p < 1. + + // Coefficients in rational approximations + static $a = [ + 1 => -3.969683028665376e+01, + 2 => 2.209460984245205e+02, + 3 => -2.759285104469687e+02, + 4 => 1.383577518672690e+02, + 5 => -3.066479806614716e+01, + 6 => 2.506628277459239e+00, + ]; + + static $b = [ + 1 => -5.447609879822406e+01, + 2 => 1.615858368580409e+02, + 3 => -1.556989798598866e+02, + 4 => 6.680131188771972e+01, + 5 => -1.328068155288572e+01, + ]; + + static $c = [ + 1 => -7.784894002430293e-03, + 2 => -3.223964580411365e-01, + 3 => -2.400758277161838e+00, + 4 => -2.549732539343734e+00, + 5 => 4.374664141464968e+00, + 6 => 2.938163982698783e+00, + ]; + + static $d = [ + 1 => 7.784695709041462e-03, + 2 => 3.224671290700398e-01, + 3 => 2.445134137142996e+00, + 4 => 3.754408661907416e+00, + ]; + + // Define lower and upper region break-points. + $p_low = 0.02425; //Use lower region approx. below this + $p_high = 1 - $p_low; //Use upper region approx. above this + + if (0 < $p && $p < $p_low) { + // Rational approximation for lower region. + $q = sqrt(-2 * log($p)); + + return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / + (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } elseif ($p_high < $p && $p < 1) { + // Rational approximation for upper region. + $q = sqrt(-2 * log(1 - $p)); + + return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / + (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } + + // Rational approximation for central region. + $q = $p - 0.5; + $r = $q * $q; + + return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q / + ((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php new file mode 100644 index 0000000..bd17b06 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php @@ -0,0 +1,17 @@ + $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + + return $returnValue; + } + + /** + * MAXA. + * + * Returns the greatest value in a list of arguments, including numbers, text, and logical values + * + * Excel Function: + * MAXA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float + */ + public static function maxA(...$args) + { + $returnValue = null; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + if (($returnValue === null) || ($arg > $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + + return $returnValue; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php new file mode 100644 index 0000000..596aad7 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php @@ -0,0 +1,78 @@ +getMessage(); + } + + if (($entry < 0) || ($entry > 1)) { + return Functions::NAN(); + } + + $mArgs = self::percentileFilterValues($aArgs); + $mValueCount = count($mArgs); + if ($mValueCount > 0) { + sort($mArgs); + $count = Counts::COUNT($mArgs); + $index = $entry * ($count - 1); + $iBase = floor($index); + if ($index == $iBase) { + return $mArgs[$index]; + } + $iNext = $iBase + 1; + $iProportion = $index - $iBase; + + return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion); + } + + return Functions::NAN(); + } + + /** + * PERCENTRANK. + * + * Returns the rank of a value in a data set as a percentage of the data set. + * Note that the returned rank is simply rounded to the appropriate significant digits, + * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return + * 0.667 rather than 0.666 + * + * @param mixed $valueSet An array of (float) values, or a reference to, a list of numbers + * @param mixed $value The number whose rank you want to find + * @param mixed $significance The (integer) number of significant digits for the returned percentage value + * + * @return float|string (string if result is an error) + */ + public static function PERCENTRANK($valueSet, $value, $significance = 3) + { + $valueSet = Functions::flattenArray($valueSet); + $value = Functions::flattenSingleValue($value); + $significance = ($significance === null) ? 3 : Functions::flattenSingleValue($significance); + + try { + $value = StatisticalValidations::validateFloat($value); + $significance = StatisticalValidations::validateInt($significance); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + $valueCount = count($valueSet); + if ($valueCount == 0) { + return Functions::NA(); + } + sort($valueSet, SORT_NUMERIC); + + $valueAdjustor = $valueCount - 1; + if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) { + return Functions::NA(); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + $pos = 0; + $testValue = $valueSet[0]; + while ($testValue < $value) { + $testValue = $valueSet[++$pos]; + } + --$pos; + $pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos])); + } + + return round($pos / $valueAdjustor, $significance); + } + + /** + * QUARTILE. + * + * Returns the quartile of a data set. + * + * Excel Function: + * QUARTILE(value1[,value2[, ...]],entry) + * + * @param mixed $args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function QUARTILE(...$args) + { + $aArgs = Functions::flattenArray($args); + $entry = array_pop($aArgs); + + try { + $entry = StatisticalValidations::validateFloat($entry); + } catch (Exception $e) { + return $e->getMessage(); + } + + $entry = floor($entry); + $entry /= 4; + if (($entry < 0) || ($entry > 1)) { + return Functions::NAN(); + } + + return self::PERCENTILE($aArgs, $entry); + } + + /** + * RANK. + * + * Returns the rank of a number in a list of numbers. + * + * @param mixed $value The number whose rank you want to find + * @param mixed $valueSet An array of float values, or a reference to, a list of numbers + * @param mixed $order Order to sort the values in the value set + * + * @return float|string The result, or a string containing an error (0 = Descending, 1 = Ascending) + */ + public static function RANK($value, $valueSet, $order = self::RANK_SORT_DESCENDING) + { + $value = Functions::flattenSingleValue($value); + $valueSet = Functions::flattenArray($valueSet); + $order = ($order === null) ? self::RANK_SORT_DESCENDING : Functions::flattenSingleValue($order); + + try { + $value = StatisticalValidations::validateFloat($value); + $order = StatisticalValidations::validateInt($order); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + if ($order === self::RANK_SORT_DESCENDING) { + rsort($valueSet, SORT_NUMERIC); + } else { + sort($valueSet, SORT_NUMERIC); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + return Functions::NA(); + } + + return ++$pos; + } + + protected static function percentileFilterValues(array $dataSet) + { + return array_filter( + $dataSet, + function ($value): bool { + return is_numeric($value) && !is_string($value); + } + ); + } + + protected static function rankFilterValues(array $dataSet) + { + return array_filter( + $dataSet, + function ($value): bool { + return is_numeric($value); + } + ); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php new file mode 100644 index 0000000..d29f80c --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -0,0 +1,98 @@ +getMessage(); + } + + return mb_substr($value ?? '', 0, $chars, 'UTF-8'); + } + + /** + * MID. + * + * @param mixed $value String value from which to extract characters + * Or can be an array of values + * @param mixed $start Integer offset of the first character that we want to extract + * Or can be an array of values + * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value, $start or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function mid($value, $start, $chars) + { + if (is_array($value) || is_array($start) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $start, $chars); + } + + try { + $value = Helpers::extractString($value); + $start = Helpers::extractInt($start, 1); + $chars = Helpers::extractInt($chars, 0); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_substr($value ?? '', --$start, $chars, 'UTF-8'); + } + + /** + * RIGHT. + * + * @param mixed $value String value from which to extract characters + * Or can be an array of values + * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function right($value, $chars = 1) + { + if (is_array($value) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars); + } + + try { + $value = Helpers::extractString($value); + $chars = Helpers::extractInt($chars, 0, 1); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8'); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php new file mode 100644 index 0000000..b4b634f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -0,0 +1,279 @@ +getMessage(); + } + + $mask = '$#,##0'; + if ($decimals > 0) { + $mask .= '.' . str_repeat('0', $decimals); + } else { + $round = 10 ** abs($decimals); + if ($value < 0) { + $round = 0 - $round; + } + $value = MathTrig\Round::multiple($value, $round); + } + $mask = "{$mask};-{$mask}"; + + return NumberFormat::toFormattedString($value, $mask); + } + + /** + * FIXED. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $decimals Integer value for the number of decimal places that should be formatted + * Or can be an array of values + * @param mixed $noCommas Boolean value indicating whether the value should have thousands separators or not + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false) + { + if (is_array($value) || is_array($decimals) || is_array($noCommas)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimals, $noCommas); + } + + try { + $value = Helpers::extractFloat($value); + $decimals = Helpers::extractInt($decimals, -100, 0, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $valueResult = round($value, $decimals); + if ($decimals < 0) { + $decimals = 0; + } + if ($noCommas === false) { + $valueResult = number_format( + $valueResult, + $decimals, + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + } + + return (string) $valueResult; + } + + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format A string with the Format mask that should be used + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function TEXTFORMAT($value, $format) + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $value = Helpers::extractString($value); + $format = Helpers::extractString($format); + + if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) { + $value = DateTimeExcel\DateValue::fromString($value); + } + + return (string) NumberFormat::toFormattedString($value, $format); + } + + /** + * @param mixed $value Value to check + * + * @return mixed + */ + private static function convertValue($value) + { + $value = $value ?? 0; + if (is_bool($value)) { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + $value = (int) $value; + } else { + throw new CalcExp(Functions::VALUE()); + } + } + + return $value; + } + + /** + * VALUE. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|DateTimeInterface|float|int|string A string if arguments are invalid + * If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions + */ + public static function VALUE($value = '') + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = self::convertValue($value); + } catch (CalcExp $e) { + return $e->getMessage(); + } + if (!is_numeric($value)) { + $numberValue = str_replace( + StringHelper::getThousandsSeparator(), + '', + trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode()) + ); + if (is_numeric($numberValue)) { + return (float) $numberValue; + } + + $dateSetting = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + + if (strpos($value, ':') !== false) { + $timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value)); + if ($timeValue !== Functions::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $timeValue; + } + } + $dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value)); + if ($dateValue !== Functions::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $dateValue; + } + Functions::setReturnDateType($dateSetting); + + return Functions::VALUE(); + } + + return (float) $value; + } + + /** + * @param mixed $decimalSeparator + */ + private static function getDecimalSeparator($decimalSeparator): string + { + return empty($decimalSeparator) ? StringHelper::getDecimalSeparator() : (string) $decimalSeparator; + } + + /** + * @param mixed $groupSeparator + */ + private static function getGroupSeparator($groupSeparator): string + { + return empty($groupSeparator) ? StringHelper::getThousandsSeparator() : (string) $groupSeparator; + } + + /** + * NUMBERVALUE. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $decimalSeparator A string with the decimal separator to use, defaults to locale defined value + * Or can be an array of values + * @param mixed $groupSeparator A string with the group/thousands separator to use, defaults to locale defined value + * Or can be an array of values + * + * @return array|float|string + */ + public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) + { + if (is_array($value) || is_array($decimalSeparator) || is_array($groupSeparator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimalSeparator, $groupSeparator); + } + + try { + $value = self::convertValue($value); + $decimalSeparator = self::getDecimalSeparator($decimalSeparator); + $groupSeparator = self::getGroupSeparator($groupSeparator); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + if (!is_numeric($value)) { + $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE); + if ($decimalPositions > 1) { + return Functions::VALUE(); + } + $decimalOffset = array_pop($matches[0])[1]; + if (strpos($value, $groupSeparator, $decimalOffset) !== false) { + return Functions::VALUE(); + } + + $value = str_replace([$groupSeparator, $decimalSeparator], ['', '.'], $value); + + // Handle the special case of trailing % signs + $percentageString = rtrim($value, '%'); + if (!is_numeric($percentageString)) { + return Functions::VALUE(); + } + + $percentageAdjustment = strlen($value) - strlen($percentageString); + if ($percentageAdjustment) { + $value = (float) $percentageString; + $value /= 10 ** ($percentageAdjustment * 2); + } + } + + return is_array($value) ? Functions::VALUE() : (float) $value; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php new file mode 100644 index 0000000..86c405a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -0,0 +1,86 @@ +url = $url; + $this->tooltip = $tooltip; + } + + /** + * Get URL. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set URL. + * + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Get tooltip. + * + * @return string + */ + public function getTooltip() + { + return $this->tooltip; + } + + /** + * Set tooltip. + * + * @param string $tooltip + * + * @return $this + */ + public function setTooltip($tooltip) + { + $this->tooltip = $tooltip; + + return $this; + } + + /** + * Is this hyperlink internal? (to another worksheet). + * + * @return bool + */ + public function isInternal() + { + return strpos($this->url, 'sheet://') !== false; + } + + /** + * @return string + */ + public function getTypeHyperlink() + { + return $this->isInternal() ? '' : 'External'; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->url . + $this->tooltip . + __CLASS__ + ); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IValueBinder.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IValueBinder.php new file mode 100644 index 0000000..5af9f5f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/IValueBinder.php @@ -0,0 +1,16 @@ + [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => null, + 'alpha' => 0, + ], + 'style' => [ + 'width' => '9525', + 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => self::LINE_STYLE_DASH_SOLID, + 'cap' => self::LINE_STYLE_CAP_FLAT, + 'join' => self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => self::LINE_STYLE_ARROW_SIZE_5, + ], + 'end' => [ + 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => self::LINE_STYLE_ARROW_SIZE_8, + ], + ], + ], + ]; + + private $shadowProperties = [ + 'presets' => self::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 85, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ]; + + private $glowProperties = [ + 'size' => null, + 'color' => [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + ]; + + private $softEdges = [ + 'size' => null, + ]; + + /** + * Get Object State. + * + * @return bool + */ + public function getObjectState() + { + return $this->objectState; + } + + /** + * Change Object State to True. + * + * @return $this + */ + private function activateObject() + { + $this->objectState = true; + + return $this; + } + + /** + * Set Line Color Properties. + * + * @param string $value + * @param int $alpha + * @param string $colorType + */ + public function setLineColorProperties($value, $alpha = 0, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void + { + $this->activateObject() + ->lineProperties['color'] = $this->setColorProperties( + $value, + $alpha, + $colorType + ); + } + + /** + * Set Line Color Properties. + * + * @param float $lineWidth + * @param string $compoundType + * @param string $dashType + * @param string $capType + * @param string $joinType + * @param string $headArrowType + * @param string $headArrowSize + * @param string $endArrowType + * @param string $endArrowSize + */ + public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void + { + $this->activateObject(); + ($lineWidth !== null) + ? $this->lineProperties['style']['width'] = $this->getExcelPointsWidth((float) $lineWidth) + : null; + ($compoundType !== null) + ? $this->lineProperties['style']['compound'] = (string) $compoundType + : null; + ($dashType !== null) + ? $this->lineProperties['style']['dash'] = (string) $dashType + : null; + ($capType !== null) + ? $this->lineProperties['style']['cap'] = (string) $capType + : null; + ($joinType !== null) + ? $this->lineProperties['style']['join'] = (string) $joinType + : null; + ($headArrowType !== null) + ? $this->lineProperties['style']['arrow']['head']['type'] = (string) $headArrowType + : null; + ($headArrowSize !== null) + ? $this->lineProperties['style']['arrow']['head']['size'] = (string) $headArrowSize + : null; + ($endArrowType !== null) + ? $this->lineProperties['style']['arrow']['end']['type'] = (string) $endArrowType + : null; + ($endArrowSize !== null) + ? $this->lineProperties['style']['arrow']['end']['size'] = (string) $endArrowSize + : null; + } + + /** + * Get Line Color Property. + * + * @param string $propertyName + * + * @return string + */ + public function getLineColorProperty($propertyName) + { + return $this->lineProperties['color'][$propertyName]; + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + * + * @return string + */ + public function getLineStyleProperty($elements) + { + return $this->getArrayElementsValue($this->lineProperties['style'], $elements); + } + + /** + * Set Glow Properties. + * + * @param float $size + * @param string $colorValue + * @param int $colorAlpha + * @param string $colorType + */ + public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size) + ->setGlowColor($colorValue, $colorAlpha, $colorType); + } + + /** + * Get Glow Color Property. + * + * @param string $propertyName + * + * @return string + */ + public function getGlowColor($propertyName) + { + return $this->glowProperties['color'][$propertyName]; + } + + /** + * Get Glow Size. + * + * @return string + */ + public function getGlowSize() + { + return $this->glowProperties['size']; + } + + /** + * Set Glow Size. + * + * @param float $size + * + * @return $this + */ + private function setGlowSize($size) + { + $this->glowProperties['size'] = $this->getExcelPointsWidth((float) $size); + + return $this; + } + + /** + * Set Glow Color. + * + * @param string $color + * @param int $alpha + * @param string $colorType + * + * @return $this + */ + private function setGlowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->glowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->glowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); + } + if ($colorType !== null) { + $this->glowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Get Line Style Arrow Parameters. + * + * @param string $arrowSelector + * @param string $propertySelector + * + * @return string + */ + public function getLineStyleArrowParameters($arrowSelector, $propertySelector) + { + return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Set Shadow Properties. + * + * @param int $presets + * @param string $colorValue + * @param string $colorType + * @param string $colorAlpha + * @param string $blur + * @param int $angle + * @param float $distance + */ + public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + { + $this->activateObject() + ->setShadowPresetsProperties((int) $presets) + ->setShadowColor( + $colorValue ?? $this->shadowProperties['color']['value'], + $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : $this->getTrueAlpha($colorAlpha), + $colorType ?? $this->shadowProperties['color']['type'] + ) + ->setShadowBlur((float) $blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @param int $presets + * + * @return $this + */ + private function setShadowPresetsProperties($presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + /** + * Set Shadow Properties Values. + * + * @param mixed $reference + * + * @return $this + */ + private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if ($reference === null) { + $reference = &$this->shadowProperties[$property_key]; + } else { + $reference = &$reference[$property_key]; + } + $this->setShadowPropertiesMapValues($property_val, $reference); + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Color. + * + * @param string $color + * @param int $alpha + * @param string $colorType + * + * @return $this + */ + private function setShadowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->shadowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->shadowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); + } + if ($colorType !== null) { + $this->shadowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @param float $blur + * + * @return $this + */ + private function setShadowBlur($blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = (string) $this->getExcelPointsWidth($blur); + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @param int $angle + * + * @return $this + */ + private function setShadowAngle($angle) + { + if ($angle !== null) { + $this->shadowProperties['direction'] = (string) $this->getExcelPointsAngle($angle); + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @param float $distance + * + * @return $this + */ + private function setShadowDistance($distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = (string) $this->getExcelPointsWidth($distance); + } + + return $this; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return string + */ + public function getShadowProperty($elements) + { + return $this->getArrayElementsValue($this->shadowProperties, $elements); + } + + /** + * Set Soft Edges Size. + * + * @param float $size + */ + public function setSoftEdgesSize($size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = (string) $this->getExcelPointsWidth($size); + } + } + + /** + * Get Soft Edges Size. + * + * @return string + */ + public function getSoftEdgesSize() + { + return $this->softEdges['size']; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php new file mode 100644 index 0000000..cea9655 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php @@ -0,0 +1,481 @@ +layoutTarget = $layout['layoutTarget']; + } + if (isset($layout['xMode'])) { + $this->xMode = $layout['xMode']; + } + if (isset($layout['yMode'])) { + $this->yMode = $layout['yMode']; + } + if (isset($layout['x'])) { + $this->xPos = (float) $layout['x']; + } + if (isset($layout['y'])) { + $this->yPos = (float) $layout['y']; + } + if (isset($layout['w'])) { + $this->width = (float) $layout['w']; + } + if (isset($layout['h'])) { + $this->height = (float) $layout['h']; + } + } + + /** + * Get Layout Target. + * + * @return string + */ + public function getLayoutTarget() + { + return $this->layoutTarget; + } + + /** + * Set Layout Target. + * + * @param string $target + * + * @return $this + */ + public function setLayoutTarget($target) + { + $this->layoutTarget = $target; + + return $this; + } + + /** + * Get X-Mode. + * + * @return string + */ + public function getXMode() + { + return $this->xMode; + } + + /** + * Set X-Mode. + * + * @param string $mode + * + * @return $this + */ + public function setXMode($mode) + { + $this->xMode = (string) $mode; + + return $this; + } + + /** + * Get Y-Mode. + * + * @return string + */ + public function getYMode() + { + return $this->yMode; + } + + /** + * Set Y-Mode. + * + * @param string $mode + * + * @return $this + */ + public function setYMode($mode) + { + $this->yMode = (string) $mode; + + return $this; + } + + /** + * Get X-Position. + * + * @return number + */ + public function getXPosition() + { + return $this->xPos; + } + + /** + * Set X-Position. + * + * @param float $position + * + * @return $this + */ + public function setXPosition($position) + { + $this->xPos = (float) $position; + + return $this; + } + + /** + * Get Y-Position. + * + * @return number + */ + public function getYPosition() + { + return $this->yPos; + } + + /** + * Set Y-Position. + * + * @param float $position + * + * @return $this + */ + public function setYPosition($position) + { + $this->yPos = (float) $position; + + return $this; + } + + /** + * Get Width. + * + * @return number + */ + public function getWidth() + { + return $this->width; + } + + /** + * Set Width. + * + * @param float $width + * + * @return $this + */ + public function setWidth($width) + { + $this->width = $width; + + return $this; + } + + /** + * Get Height. + * + * @return number + */ + public function getHeight() + { + return $this->height; + } + + /** + * Set Height. + * + * @param float $height + * + * @return $this + */ + public function setHeight($height) + { + $this->height = $height; + + return $this; + } + + /** + * Get show legend key. + * + * @return bool + */ + public function getShowLegendKey() + { + return $this->showLegendKey; + } + + /** + * Set show legend key + * Specifies that legend keys should be shown in data labels. + * + * @param bool $showLegendKey Show legend key + * + * @return $this + */ + public function setShowLegendKey($showLegendKey) + { + $this->showLegendKey = $showLegendKey; + + return $this; + } + + /** + * Get show value. + * + * @return bool + */ + public function getShowVal() + { + return $this->showVal; + } + + /** + * Set show val + * Specifies that the value should be shown in data labels. + * + * @param bool $showDataLabelValues Show val + * + * @return $this + */ + public function setShowVal($showDataLabelValues) + { + $this->showVal = $showDataLabelValues; + + return $this; + } + + /** + * Get show category name. + * + * @return bool + */ + public function getShowCatName() + { + return $this->showCatName; + } + + /** + * Set show cat name + * Specifies that the category name should be shown in data labels. + * + * @param bool $showCategoryName Show cat name + * + * @return $this + */ + public function setShowCatName($showCategoryName) + { + $this->showCatName = $showCategoryName; + + return $this; + } + + /** + * Get show data series name. + * + * @return bool + */ + public function getShowSerName() + { + return $this->showSerName; + } + + /** + * Set show ser name + * Specifies that the series name should be shown in data labels. + * + * @param bool $showSeriesName Show series name + * + * @return $this + */ + public function setShowSerName($showSeriesName) + { + $this->showSerName = $showSeriesName; + + return $this; + } + + /** + * Get show percentage. + * + * @return bool + */ + public function getShowPercent() + { + return $this->showPercent; + } + + /** + * Set show percentage + * Specifies that the percentage should be shown in data labels. + * + * @param bool $showPercentage Show percentage + * + * @return $this + */ + public function setShowPercent($showPercentage) + { + $this->showPercent = $showPercentage; + + return $this; + } + + /** + * Get show bubble size. + * + * @return bool + */ + public function getShowBubbleSize() + { + return $this->showBubbleSize; + } + + /** + * Set show bubble size + * Specifies that the bubble size should be shown in data labels. + * + * @param bool $showBubbleSize Show bubble size + * + * @return $this + */ + public function setShowBubbleSize($showBubbleSize) + { + $this->showBubbleSize = $showBubbleSize; + + return $this; + } + + /** + * Get show leader lines. + * + * @return bool + */ + public function getShowLeaderLines() + { + return $this->showLeaderLines; + } + + /** + * Set show leader lines + * Specifies that leader lines should be shown in data labels. + * + * @param bool $showLeaderLines Show leader lines + * + * @return $this + */ + public function setShowLeaderLines($showLeaderLines) + { + $this->showLeaderLines = $showLeaderLines; + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php new file mode 100644 index 0000000..2f003cd --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php @@ -0,0 +1,149 @@ + self::POSITION_BOTTOM, + self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT, + self::XL_LEGEND_POSITION_CUSTOM => '??', + self::XL_LEGEND_POSITION_LEFT => self::POSITION_LEFT, + self::XL_LEGEND_POSITION_RIGHT => self::POSITION_RIGHT, + self::XL_LEGEND_POSITION_TOP => self::POSITION_TOP, + ]; + + /** + * Legend position. + * + * @var string + */ + private $position = self::POSITION_RIGHT; + + /** + * Allow overlay of other elements? + * + * @var bool + */ + private $overlay = true; + + /** + * Legend Layout. + * + * @var Layout + */ + private $layout; + + /** + * Create a new Legend. + * + * @param string $position + * @param bool $overlay + */ + public function __construct($position = self::POSITION_RIGHT, ?Layout $layout = null, $overlay = false) + { + $this->setPosition($position); + $this->layout = $layout; + $this->setOverlay($overlay); + } + + /** + * Get legend position as an excel string value. + * + * @return string + */ + public function getPosition() + { + return $this->position; + } + + /** + * Get legend position using an excel string value. + * + * @param string $position see self::POSITION_* + * + * @return bool + */ + public function setPosition($position) + { + if (!in_array($position, self::$positionXLref)) { + return false; + } + + $this->position = $position; + + return true; + } + + /** + * Get legend position as an Excel internal numeric value. + * + * @return int + */ + public function getPositionXL() + { + return array_search($this->position, self::$positionXLref); + } + + /** + * Set legend position using an Excel internal numeric value. + * + * @param int $positionXL see self::XL_LEGEND_POSITION_* + * + * @return bool + */ + public function setPositionXL($positionXL) + { + if (!isset(self::$positionXLref[$positionXL])) { + return false; + } + + $this->position = self::$positionXLref[$positionXL]; + + return true; + } + + /** + * Get allow overlay of other elements? + * + * @return bool + */ + public function getOverlay() + { + return $this->overlay; + } + + /** + * Set allow overlay of other elements? + * + * @param bool $overlay + */ + public function setOverlay($overlay): void + { + $this->overlay = $overlay; + } + + /** + * Get Layout. + * + * @return Layout + */ + public function getLayout() + { + return $this->layout; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php new file mode 100644 index 0000000..3032f6b --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php @@ -0,0 +1,22 @@ +graph = null; + $this->chart = $chart; + } + + private static function init(): void + { + static $loaded = false; + if ($loaded) { + return; + } + + \JpGraph\JpGraph::load(); + \JpGraph\JpGraph::module('bar'); + \JpGraph\JpGraph::module('contour'); + \JpGraph\JpGraph::module('line'); + \JpGraph\JpGraph::module('pie'); + \JpGraph\JpGraph::module('pie3d'); + \JpGraph\JpGraph::module('radar'); + \JpGraph\JpGraph::module('regstat'); + \JpGraph\JpGraph::module('scatter'); + \JpGraph\JpGraph::module('stock'); + + self::$markSet = [ + 'diamond' => MARK_DIAMOND, + 'square' => MARK_SQUARE, + 'triangle' => MARK_UTRIANGLE, + 'x' => MARK_X, + 'star' => MARK_STAR, + 'dot' => MARK_FILLEDCIRCLE, + 'dash' => MARK_DTRIANGLE, + 'circle' => MARK_CIRCLE, + 'plus' => MARK_CROSS, + ]; + + $loaded = true; + } + + private function formatPointMarker($seriesPlot, $markerID) + { + $plotMarkKeys = array_keys(self::$markSet); + if ($markerID === null) { + // Use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } elseif ($markerID !== 'none') { + // Use specified plot marker (if it exists) + if (isset(self::$markSet[$markerID])) { + $seriesPlot->mark->SetType(self::$markSet[$markerID]); + } else { + // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } + } else { + // Hide plot marker + $seriesPlot->mark->Hide(); + } + $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); + $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + + return $seriesPlot; + } + + private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') + { + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); + if ($datasetLabelFormatCode !== null) { + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); + } + + $testCurrentIndex = 0; + foreach ($datasetLabels as $i => $datasetLabel) { + if (is_array($datasetLabel)) { + if ($rotation == 'bar') { + $datasetLabels[$i] = implode(' ', $datasetLabel); + } else { + $datasetLabel = array_reverse($datasetLabel); + $datasetLabels[$i] = implode("\n", $datasetLabel); + } + } else { + // Format labels according to any formatting code + if ($datasetLabelFormatCode !== null) { + $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); + } + } + ++$testCurrentIndex; + } + + return $datasetLabels; + } + + private function percentageSumCalculation($groupID, $seriesCount) + { + $sumValues = []; + // Adjust our values to a percentage value across all series in the group + for ($i = 0; $i < $seriesCount; ++$i) { + if ($i == 0) { + $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + } else { + $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + foreach ($nextValues as $k => $value) { + if (isset($sumValues[$k])) { + $sumValues[$k] += $value; + } else { + $sumValues[$k] = $value; + } + } + } + } + + return $sumValues; + } + + private function percentageAdjustValues($dataValues, $sumValues) + { + foreach ($dataValues as $k => $dataValue) { + $dataValues[$k] = $dataValue / $sumValues[$k] * 100; + } + + return $dataValues; + } + + private function getCaption($captionElement) + { + // Read any caption + $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; + // Test if we have a title caption to display + if ($caption !== null) { + // If we do, it could be a plain string or an array + if (is_array($caption)) { + // Implode an array to a plain string + $caption = implode('', $caption); + } + } + + return $caption; + } + + private function renderTitle(): void + { + $title = $this->getCaption($this->chart->getTitle()); + if ($title !== null) { + $this->graph->title->Set($title); + } + } + + private function renderLegend(): void + { + $legend = $this->chart->getLegend(); + if ($legend !== null) { + $legendPosition = $legend->getPosition(); + switch ($legendPosition) { + case 'r': + $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right + $this->graph->legend->SetColumns(1); + + break; + case 'l': + $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left + $this->graph->legend->SetColumns(1); + + break; + case 't': + $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + + break; + case 'b': + $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + + break; + default: + $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right + $this->graph->legend->SetColumns(1); + + break; + } + } else { + $this->graph->legend->Hide(); + } + } + + private function renderCartesianPlotArea($type = 'textlin'): void + { + $this->graph = new Graph(self::$width, self::$height); + $this->graph->SetScale($type); + + $this->renderTitle(); + + // Rotate for bar rather than column chart + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); + $reverse = $rotation == 'bar'; + + $xAxisLabel = $this->chart->getXAxisLabel(); + if ($xAxisLabel !== null) { + $title = $this->getCaption($xAxisLabel); + if ($title !== null) { + $this->graph->xaxis->SetTitle($title, 'center'); + $this->graph->xaxis->title->SetMargin(35); + if ($reverse) { + $this->graph->xaxis->title->SetAngle(90); + $this->graph->xaxis->title->SetMargin(90); + } + } + } + + $yAxisLabel = $this->chart->getYAxisLabel(); + if ($yAxisLabel !== null) { + $title = $this->getCaption($yAxisLabel); + if ($title !== null) { + $this->graph->yaxis->SetTitle($title, 'center'); + if ($reverse) { + $this->graph->yaxis->title->SetAngle(0); + $this->graph->yaxis->title->SetMargin(-55); + } + } + } + } + + private function renderPiePlotArea(): void + { + $this->graph = new PieGraph(self::$width, self::$height); + + $this->renderTitle(); + } + + private function renderRadarPlotArea(): void + { + $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph->SetScale('lin'); + + $this->renderTitle(); + } + + private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + $seriesPlot = new LinePlot($dataValues); + if ($combination) { + $seriesPlot->SetBarCenter(); + } + + if ($filled) { + $seriesPlot->SetFilled(true); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + } else { + // Set the appropriate plot marker + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + + if ($grouping == 'standard') { + $groupPlot = $seriesPlots; + } else { + $groupPlot = new AccLinePlot($seriesPlots); + } + $this->graph->Add($groupPlot); + } + + private function renderPlotBar($groupID, $dimensions = '2d'): void + { + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); + // Rotate for bar rather than column chart + if (($groupID == 0) && ($rotation == 'bar')) { + $this->graph->Set90AndMargin(); + } + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); + // Rotate for bar rather than column chart + if ($rotation == 'bar') { + $datasetLabels = array_reverse($datasetLabels); + $this->graph->yaxis->SetPos('max'); + $this->graph->yaxis->SetLabelAlign('center', 'top'); + $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); + } + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($j = 0; $j < $seriesCount; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + // Reverse the $dataValues order for bar rather than column chart + if ($rotation == 'bar') { + $dataValues = array_reverse($dataValues); + } + $seriesPlot = new BarPlot($dataValues); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + if ($dimensions == '3d') { + $seriesPlot->SetShadow(); + } + if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { + $dataLabel = ''; + } else { + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); + } + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + // Reverse the plot order for bar rather than column chart + if (($rotation == 'bar') && ($grouping != 'percentStacked')) { + $seriesPlots = array_reverse($seriesPlots); + } + + if ($grouping == 'clustered') { + $groupPlot = new GroupBarPlot($seriesPlots); + } elseif ($grouping == 'standard') { + $groupPlot = new GroupBarPlot($seriesPlots); + } else { + $groupPlot = new AccBarPlot($seriesPlots); + if ($dimensions == '3d') { + $groupPlot->SetShadow(); + } + } + + $this->graph->Add($groupPlot); + } + + private function renderPlotScatter($groupID, $bubble): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + foreach ($dataValuesY as $k => $dataValueY) { + $dataValuesY[$k] = $k; + } + + $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); + if ($scatterStyle == 'lineMarker') { + $seriesPlot->SetLinkPoints(); + $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); + } elseif ($scatterStyle == 'smoothMarker') { + $spline = new Spline($dataValuesY, $dataValuesX); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + $lplot = new LinePlot($splineDataX, $splineDataY); + $lplot->SetColor(self::$colourSet[self::$plotColour]); + + $this->graph->Add($lplot); + } + + if ($bubble) { + $this->formatPointMarker($seriesPlot, 'dot'); + $seriesPlot->mark->SetColor('black'); + $seriesPlot->mark->SetSize($bubbleSize); + } else { + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotRadar($groupID): void + { + $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + $dataValues = []; + foreach ($dataValuesY as $k => $dataValueY) { + $dataValues[$k] = implode(' ', array_reverse($dataValueY)); + } + $tmp = array_shift($dataValues); + $dataValues[] = $tmp; + $tmp = array_shift($dataValuesX); + $dataValuesX[] = $tmp; + + $this->graph->SetTitles(array_reverse($dataValues)); + + $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); + + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if ($radarStyle == 'filled') { + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); + } + $this->formatPointMarker($seriesPlot, $marker); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotContour($groupID): void + { + $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + $dataValues = []; + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $dataValues[$i] = $dataValuesX; + } + $seriesPlot = new ContourPlot($dataValues); + + $this->graph->Add($seriesPlot); + } + + private function renderPlotStock($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); + + $dataValues = []; + // Loop through each data series in turn and build the plot arrays + foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); + foreach ($dataValuesX as $j => $dataValueX) { + $dataValues[$plotOrder[$i]][$j] = $dataValueX; + } + } + if (empty($dataValues)) { + return; + } + + $dataValuesPlot = []; + // Flatten the plot arrays to a single dimensional array to work with jpgraph + $jMax = count($dataValues[0]); + for ($j = 0; $j < $jMax; ++$j) { + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesPlot[] = $dataValues[$i][$j]; + } + } + + // Set the x-axis labels + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesPlot = new StockPlot($dataValuesPlot); + $seriesPlot->SetWidth(20); + + $this->graph->Add($seriesPlot); + } + + private function renderAreaChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, true, false, $dimensions); + } + } + + private function renderLineChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, false, false, $dimensions); + } + } + + private function renderBarChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotBar($i, $dimensions); + } + } + + private function renderScatterChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, false); + } + } + + private function renderBubbleChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, true); + } + } + + private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void + { + $this->renderPiePlotArea(); + + $iLimit = ($multiplePlots) ? $groupCount : 1; + for ($groupID = 0; $groupID < $iLimit; ++$groupID) { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + $datasetLabels = []; + if ($groupID == 0) { + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + } + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + // For pie charts, we only display the first series: doughnut charts generally display all series + $jLimit = ($multiplePlots) ? $seriesCount : 1; + // Loop through each data series in turn + for ($j = 0; $j < $jLimit; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + if ($dimensions == '3d') { + $seriesPlot = new PiePlot3D($dataValues); + } else { + if ($doughnut) { + $seriesPlot = new PiePlotC($dataValues); + } else { + $seriesPlot = new PiePlot($dataValues); + } + } + + if ($multiplePlots) { + $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); + } + + if ($doughnut) { + $seriesPlot->SetMidColor('white'); + } + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if (count($datasetLabels) > 0) { + $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); + } + if ($dimensions != '3d') { + $seriesPlot->SetGuideLines(false); + } + if ($j == 0) { + if ($exploded) { + $seriesPlot->ExplodeAll(); + } + $seriesPlot->SetLegends($datasetLabels); + } + + $this->graph->Add($seriesPlot); + } + } + } + + private function renderRadarChart($groupCount): void + { + $this->renderRadarPlotArea(); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotRadar($groupID); + } + } + + private function renderStockChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotStock($groupID); + } + } + + private function renderContourChart($groupCount, $dimensions): void + { + $this->renderCartesianPlotArea('intint'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotContour($i); + } + } + + private function renderCombinationChart($groupCount, $dimensions, $outputDestination) + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $dimensions = null; + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderPlotLine($i, true, true, $dimensions); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderPlotBar($i, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderPlotLine($i, false, true, $dimensions); + + break; + case 'scatterChart': + $this->renderPlotScatter($i, false); + + break; + case 'bubbleChart': + $this->renderPlotScatter($i, true); + + break; + default: + $this->graph = null; + + return false; + } + } + + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } + + public function render($outputDestination) + { + self::$plotColour = 0; + + $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); + + $dimensions = null; + if ($groupCount == 1) { + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = array_pop($chartTypes); + } elseif (count($chartTypes) == 0) { + echo 'Chart is not yet implemented
'; + + return false; + } else { + return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); + } + } + + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderAreaChart($groupCount, $dimensions); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderBarChart($groupCount, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderLineChart($groupCount, $dimensions); + + break; + case 'pie3DChart': + $dimensions = '3d'; + // no break + case 'pieChart': + $this->renderPieChart($groupCount, $dimensions, false, false); + + break; + case 'doughnut3DChart': + $dimensions = '3d'; + // no break + case 'doughnutChart': + $this->renderPieChart($groupCount, $dimensions, true, true); + + break; + case 'scatterChart': + $this->renderScatterChart($groupCount); + + break; + case 'bubbleChart': + $this->renderBubbleChart($groupCount); + + break; + case 'radarChart': + $this->renderRadarChart($groupCount); + + break; + case 'surface3DChart': + $dimensions = '3d'; + // no break + case 'surfaceChart': + $this->renderContourChart($groupCount, $dimensions); + + break; + case 'stockChart': + $this->renderStockChart($groupCount); + + break; + default: + echo $chartType . ' is not yet implemented
'; + + return false; + } + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php new file mode 100644 index 0000000..2690ab7 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php @@ -0,0 +1,123 @@ +cache = []; + + return true; + } + + /** + * @param string $key + * + * @return bool + */ + public function delete($key) + { + unset($this->cache[$key]); + + return true; + } + + /** + * @param iterable $keys + * + * @return bool + */ + public function deleteMultiple($keys) + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * @param iterable $keys + * @param mixed $default + * + * @return iterable + */ + public function getMultiple($keys, $default = null) + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + /** + * @param string $key + * + * @return bool + */ + public function has($key) + { + return array_key_exists($key, $this->cache); + } + + /** + * @param string $key + * @param mixed $value + * @param null|DateInterval|int $ttl + * + * @return bool + */ + public function set($key, $value, $ttl = null) + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param iterable $values + * @param null|DateInterval|int $ttl + * + * @return bool + */ + public function setMultiple($values, $ttl = null) + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Exception.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Exception.php new file mode 100644 index 0000000..9c5ab30 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Exception.php @@ -0,0 +1,7 @@ + + */ + protected $items = []; + + /** + * HashTable key map. + * + * @var array + */ + protected $keyMap = []; + + /** + * Create a new HashTable. + * + * @param T[] $source Optional source array to create HashTable from + */ + public function __construct($source = null) + { + if ($source !== null) { + // Create HashTable + $this->addFromSource($source); + } + } + + /** + * Add HashTable items from source. + * + * @param T[] $source Source array to create HashTable from + */ + public function addFromSource(?array $source = null): void + { + // Check if an array was passed + if ($source === null) { + return; + } + + foreach ($source as $item) { + $this->add($item); + } + } + + /** + * Add HashTable item. + * + * @param T $source Item to add + */ + public function add(IComparable $source): void + { + $hash = $source->getHashCode(); + if (!isset($this->items[$hash])) { + $this->items[$hash] = $source; + $this->keyMap[count($this->items) - 1] = $hash; + } + } + + /** + * Remove HashTable item. + * + * @param T $source Item to remove + */ + public function remove(IComparable $source): void + { + $hash = $source->getHashCode(); + if (isset($this->items[$hash])) { + unset($this->items[$hash]); + + $deleteKey = -1; + foreach ($this->keyMap as $key => $value) { + if ($deleteKey >= 0) { + $this->keyMap[$key - 1] = $value; + } + + if ($value == $hash) { + $deleteKey = $key; + } + } + unset($this->keyMap[count($this->keyMap) - 1]); + } + } + + /** + * Clear HashTable. + */ + public function clear(): void + { + $this->items = []; + $this->keyMap = []; + } + + /** + * Count. + * + * @return int + */ + public function count() + { + return count($this->items); + } + + /** + * Get index for hash code. + * + * @return false|int Index + */ + public function getIndexForHashCode(string $hashCode) + { + return array_search($hashCode, $this->keyMap, true); + } + + /** + * Get by index. + * + * @return null|T + */ + public function getByIndex(int $index) + { + if (isset($this->keyMap[$index])) { + return $this->getByHashCode($this->keyMap[$index]); + } + + return null; + } + + /** + * Get by hashcode. + * + * @return null|T + */ + public function getByHashCode(string $hashCode) + { + if (isset($this->items[$hashCode])) { + return $this->items[$hashCode]; + } + + return null; + } + + /** + * HashTable to array. + * + * @return T[] + */ + public function toArray() + { + return $this->items; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + // each member of this class is an array + if (is_array($value)) { + $array1 = $value; + foreach ($array1 as $key1 => $value1) { + if (is_object($value1)) { + $array1[$key1] = clone $value1; + } + } + $this->$key = $array1; + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php new file mode 100644 index 0000000..73a3308 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php @@ -0,0 +1,839 @@ + 'f0f8ff', + 'antiquewhite' => 'faebd7', + 'antiquewhite1' => 'ffefdb', + 'antiquewhite2' => 'eedfcc', + 'antiquewhite3' => 'cdc0b0', + 'antiquewhite4' => '8b8378', + 'aqua' => '00ffff', + 'aquamarine1' => '7fffd4', + 'aquamarine2' => '76eec6', + 'aquamarine4' => '458b74', + 'azure1' => 'f0ffff', + 'azure2' => 'e0eeee', + 'azure3' => 'c1cdcd', + 'azure4' => '838b8b', + 'beige' => 'f5f5dc', + 'bisque1' => 'ffe4c4', + 'bisque2' => 'eed5b7', + 'bisque3' => 'cdb79e', + 'bisque4' => '8b7d6b', + 'black' => '000000', + 'blanchedalmond' => 'ffebcd', + 'blue' => '0000ff', + 'blue1' => '0000ff', + 'blue2' => '0000ee', + 'blue4' => '00008b', + 'blueviolet' => '8a2be2', + 'brown' => 'a52a2a', + 'brown1' => 'ff4040', + 'brown2' => 'ee3b3b', + 'brown3' => 'cd3333', + 'brown4' => '8b2323', + 'burlywood' => 'deb887', + 'burlywood1' => 'ffd39b', + 'burlywood2' => 'eec591', + 'burlywood3' => 'cdaa7d', + 'burlywood4' => '8b7355', + 'cadetblue' => '5f9ea0', + 'cadetblue1' => '98f5ff', + 'cadetblue2' => '8ee5ee', + 'cadetblue3' => '7ac5cd', + 'cadetblue4' => '53868b', + 'chartreuse1' => '7fff00', + 'chartreuse2' => '76ee00', + 'chartreuse3' => '66cd00', + 'chartreuse4' => '458b00', + 'chocolate' => 'd2691e', + 'chocolate1' => 'ff7f24', + 'chocolate2' => 'ee7621', + 'chocolate3' => 'cd661d', + 'coral' => 'ff7f50', + 'coral1' => 'ff7256', + 'coral2' => 'ee6a50', + 'coral3' => 'cd5b45', + 'coral4' => '8b3e2f', + 'cornflowerblue' => '6495ed', + 'cornsilk1' => 'fff8dc', + 'cornsilk2' => 'eee8cd', + 'cornsilk3' => 'cdc8b1', + 'cornsilk4' => '8b8878', + 'cyan1' => '00ffff', + 'cyan2' => '00eeee', + 'cyan3' => '00cdcd', + 'cyan4' => '008b8b', + 'darkgoldenrod' => 'b8860b', + 'darkgoldenrod1' => 'ffb90f', + 'darkgoldenrod2' => 'eead0e', + 'darkgoldenrod3' => 'cd950c', + 'darkgoldenrod4' => '8b6508', + 'darkgreen' => '006400', + 'darkkhaki' => 'bdb76b', + 'darkolivegreen' => '556b2f', + 'darkolivegreen1' => 'caff70', + 'darkolivegreen2' => 'bcee68', + 'darkolivegreen3' => 'a2cd5a', + 'darkolivegreen4' => '6e8b3d', + 'darkorange' => 'ff8c00', + 'darkorange1' => 'ff7f00', + 'darkorange2' => 'ee7600', + 'darkorange3' => 'cd6600', + 'darkorange4' => '8b4500', + 'darkorchid' => '9932cc', + 'darkorchid1' => 'bf3eff', + 'darkorchid2' => 'b23aee', + 'darkorchid3' => '9a32cd', + 'darkorchid4' => '68228b', + 'darksalmon' => 'e9967a', + 'darkseagreen' => '8fbc8f', + 'darkseagreen1' => 'c1ffc1', + 'darkseagreen2' => 'b4eeb4', + 'darkseagreen3' => '9bcd9b', + 'darkseagreen4' => '698b69', + 'darkslateblue' => '483d8b', + 'darkslategray' => '2f4f4f', + 'darkslategray1' => '97ffff', + 'darkslategray2' => '8deeee', + 'darkslategray3' => '79cdcd', + 'darkslategray4' => '528b8b', + 'darkturquoise' => '00ced1', + 'darkviolet' => '9400d3', + 'deeppink1' => 'ff1493', + 'deeppink2' => 'ee1289', + 'deeppink3' => 'cd1076', + 'deeppink4' => '8b0a50', + 'deepskyblue1' => '00bfff', + 'deepskyblue2' => '00b2ee', + 'deepskyblue3' => '009acd', + 'deepskyblue4' => '00688b', + 'dimgray' => '696969', + 'dodgerblue1' => '1e90ff', + 'dodgerblue2' => '1c86ee', + 'dodgerblue3' => '1874cd', + 'dodgerblue4' => '104e8b', + 'firebrick' => 'b22222', + 'firebrick1' => 'ff3030', + 'firebrick2' => 'ee2c2c', + 'firebrick3' => 'cd2626', + 'firebrick4' => '8b1a1a', + 'floralwhite' => 'fffaf0', + 'forestgreen' => '228b22', + 'fuchsia' => 'ff00ff', + 'gainsboro' => 'dcdcdc', + 'ghostwhite' => 'f8f8ff', + 'gold1' => 'ffd700', + 'gold2' => 'eec900', + 'gold3' => 'cdad00', + 'gold4' => '8b7500', + 'goldenrod' => 'daa520', + 'goldenrod1' => 'ffc125', + 'goldenrod2' => 'eeb422', + 'goldenrod3' => 'cd9b1d', + 'goldenrod4' => '8b6914', + 'gray' => 'bebebe', + 'gray1' => '030303', + 'gray10' => '1a1a1a', + 'gray11' => '1c1c1c', + 'gray12' => '1f1f1f', + 'gray13' => '212121', + 'gray14' => '242424', + 'gray15' => '262626', + 'gray16' => '292929', + 'gray17' => '2b2b2b', + 'gray18' => '2e2e2e', + 'gray19' => '303030', + 'gray2' => '050505', + 'gray20' => '333333', + 'gray21' => '363636', + 'gray22' => '383838', + 'gray23' => '3b3b3b', + 'gray24' => '3d3d3d', + 'gray25' => '404040', + 'gray26' => '424242', + 'gray27' => '454545', + 'gray28' => '474747', + 'gray29' => '4a4a4a', + 'gray3' => '080808', + 'gray30' => '4d4d4d', + 'gray31' => '4f4f4f', + 'gray32' => '525252', + 'gray33' => '545454', + 'gray34' => '575757', + 'gray35' => '595959', + 'gray36' => '5c5c5c', + 'gray37' => '5e5e5e', + 'gray38' => '616161', + 'gray39' => '636363', + 'gray4' => '0a0a0a', + 'gray40' => '666666', + 'gray41' => '696969', + 'gray42' => '6b6b6b', + 'gray43' => '6e6e6e', + 'gray44' => '707070', + 'gray45' => '737373', + 'gray46' => '757575', + 'gray47' => '787878', + 'gray48' => '7a7a7a', + 'gray49' => '7d7d7d', + 'gray5' => '0d0d0d', + 'gray50' => '7f7f7f', + 'gray51' => '828282', + 'gray52' => '858585', + 'gray53' => '878787', + 'gray54' => '8a8a8a', + 'gray55' => '8c8c8c', + 'gray56' => '8f8f8f', + 'gray57' => '919191', + 'gray58' => '949494', + 'gray59' => '969696', + 'gray6' => '0f0f0f', + 'gray60' => '999999', + 'gray61' => '9c9c9c', + 'gray62' => '9e9e9e', + 'gray63' => 'a1a1a1', + 'gray64' => 'a3a3a3', + 'gray65' => 'a6a6a6', + 'gray66' => 'a8a8a8', + 'gray67' => 'ababab', + 'gray68' => 'adadad', + 'gray69' => 'b0b0b0', + 'gray7' => '121212', + 'gray70' => 'b3b3b3', + 'gray71' => 'b5b5b5', + 'gray72' => 'b8b8b8', + 'gray73' => 'bababa', + 'gray74' => 'bdbdbd', + 'gray75' => 'bfbfbf', + 'gray76' => 'c2c2c2', + 'gray77' => 'c4c4c4', + 'gray78' => 'c7c7c7', + 'gray79' => 'c9c9c9', + 'gray8' => '141414', + 'gray80' => 'cccccc', + 'gray81' => 'cfcfcf', + 'gray82' => 'd1d1d1', + 'gray83' => 'd4d4d4', + 'gray84' => 'd6d6d6', + 'gray85' => 'd9d9d9', + 'gray86' => 'dbdbdb', + 'gray87' => 'dedede', + 'gray88' => 'e0e0e0', + 'gray89' => 'e3e3e3', + 'gray9' => '171717', + 'gray90' => 'e5e5e5', + 'gray91' => 'e8e8e8', + 'gray92' => 'ebebeb', + 'gray93' => 'ededed', + 'gray94' => 'f0f0f0', + 'gray95' => 'f2f2f2', + 'gray97' => 'f7f7f7', + 'gray98' => 'fafafa', + 'gray99' => 'fcfcfc', + 'green' => '00ff00', + 'green1' => '00ff00', + 'green2' => '00ee00', + 'green3' => '00cd00', + 'green4' => '008b00', + 'greenyellow' => 'adff2f', + 'honeydew1' => 'f0fff0', + 'honeydew2' => 'e0eee0', + 'honeydew3' => 'c1cdc1', + 'honeydew4' => '838b83', + 'hotpink' => 'ff69b4', + 'hotpink1' => 'ff6eb4', + 'hotpink2' => 'ee6aa7', + 'hotpink3' => 'cd6090', + 'hotpink4' => '8b3a62', + 'indianred' => 'cd5c5c', + 'indianred1' => 'ff6a6a', + 'indianred2' => 'ee6363', + 'indianred3' => 'cd5555', + 'indianred4' => '8b3a3a', + 'ivory1' => 'fffff0', + 'ivory2' => 'eeeee0', + 'ivory3' => 'cdcdc1', + 'ivory4' => '8b8b83', + 'khaki' => 'f0e68c', + 'khaki1' => 'fff68f', + 'khaki2' => 'eee685', + 'khaki3' => 'cdc673', + 'khaki4' => '8b864e', + 'lavender' => 'e6e6fa', + 'lavenderblush1' => 'fff0f5', + 'lavenderblush2' => 'eee0e5', + 'lavenderblush3' => 'cdc1c5', + 'lavenderblush4' => '8b8386', + 'lawngreen' => '7cfc00', + 'lemonchiffon1' => 'fffacd', + 'lemonchiffon2' => 'eee9bf', + 'lemonchiffon3' => 'cdc9a5', + 'lemonchiffon4' => '8b8970', + 'light' => 'eedd82', + 'lightblue' => 'add8e6', + 'lightblue1' => 'bfefff', + 'lightblue2' => 'b2dfee', + 'lightblue3' => '9ac0cd', + 'lightblue4' => '68838b', + 'lightcoral' => 'f08080', + 'lightcyan1' => 'e0ffff', + 'lightcyan2' => 'd1eeee', + 'lightcyan3' => 'b4cdcd', + 'lightcyan4' => '7a8b8b', + 'lightgoldenrod1' => 'ffec8b', + 'lightgoldenrod2' => 'eedc82', + 'lightgoldenrod3' => 'cdbe70', + 'lightgoldenrod4' => '8b814c', + 'lightgoldenrodyellow' => 'fafad2', + 'lightgray' => 'd3d3d3', + 'lightpink' => 'ffb6c1', + 'lightpink1' => 'ffaeb9', + 'lightpink2' => 'eea2ad', + 'lightpink3' => 'cd8c95', + 'lightpink4' => '8b5f65', + 'lightsalmon1' => 'ffa07a', + 'lightsalmon2' => 'ee9572', + 'lightsalmon3' => 'cd8162', + 'lightsalmon4' => '8b5742', + 'lightseagreen' => '20b2aa', + 'lightskyblue' => '87cefa', + 'lightskyblue1' => 'b0e2ff', + 'lightskyblue2' => 'a4d3ee', + 'lightskyblue3' => '8db6cd', + 'lightskyblue4' => '607b8b', + 'lightslateblue' => '8470ff', + 'lightslategray' => '778899', + 'lightsteelblue' => 'b0c4de', + 'lightsteelblue1' => 'cae1ff', + 'lightsteelblue2' => 'bcd2ee', + 'lightsteelblue3' => 'a2b5cd', + 'lightsteelblue4' => '6e7b8b', + 'lightyellow1' => 'ffffe0', + 'lightyellow2' => 'eeeed1', + 'lightyellow3' => 'cdcdb4', + 'lightyellow4' => '8b8b7a', + 'lime' => '00ff00', + 'limegreen' => '32cd32', + 'linen' => 'faf0e6', + 'magenta' => 'ff00ff', + 'magenta2' => 'ee00ee', + 'magenta3' => 'cd00cd', + 'magenta4' => '8b008b', + 'maroon' => 'b03060', + 'maroon1' => 'ff34b3', + 'maroon2' => 'ee30a7', + 'maroon3' => 'cd2990', + 'maroon4' => '8b1c62', + 'medium' => '66cdaa', + 'mediumaquamarine' => '66cdaa', + 'mediumblue' => '0000cd', + 'mediumorchid' => 'ba55d3', + 'mediumorchid1' => 'e066ff', + 'mediumorchid2' => 'd15fee', + 'mediumorchid3' => 'b452cd', + 'mediumorchid4' => '7a378b', + 'mediumpurple' => '9370db', + 'mediumpurple1' => 'ab82ff', + 'mediumpurple2' => '9f79ee', + 'mediumpurple3' => '8968cd', + 'mediumpurple4' => '5d478b', + 'mediumseagreen' => '3cb371', + 'mediumslateblue' => '7b68ee', + 'mediumspringgreen' => '00fa9a', + 'mediumturquoise' => '48d1cc', + 'mediumvioletred' => 'c71585', + 'midnightblue' => '191970', + 'mintcream' => 'f5fffa', + 'mistyrose1' => 'ffe4e1', + 'mistyrose2' => 'eed5d2', + 'mistyrose3' => 'cdb7b5', + 'mistyrose4' => '8b7d7b', + 'moccasin' => 'ffe4b5', + 'navajowhite1' => 'ffdead', + 'navajowhite2' => 'eecfa1', + 'navajowhite3' => 'cdb38b', + 'navajowhite4' => '8b795e', + 'navy' => '000080', + 'navyblue' => '000080', + 'oldlace' => 'fdf5e6', + 'olive' => '808000', + 'olivedrab' => '6b8e23', + 'olivedrab1' => 'c0ff3e', + 'olivedrab2' => 'b3ee3a', + 'olivedrab4' => '698b22', + 'orange' => 'ffa500', + 'orange1' => 'ffa500', + 'orange2' => 'ee9a00', + 'orange3' => 'cd8500', + 'orange4' => '8b5a00', + 'orangered1' => 'ff4500', + 'orangered2' => 'ee4000', + 'orangered3' => 'cd3700', + 'orangered4' => '8b2500', + 'orchid' => 'da70d6', + 'orchid1' => 'ff83fa', + 'orchid2' => 'ee7ae9', + 'orchid3' => 'cd69c9', + 'orchid4' => '8b4789', + 'pale' => 'db7093', + 'palegoldenrod' => 'eee8aa', + 'palegreen' => '98fb98', + 'palegreen1' => '9aff9a', + 'palegreen2' => '90ee90', + 'palegreen3' => '7ccd7c', + 'palegreen4' => '548b54', + 'paleturquoise' => 'afeeee', + 'paleturquoise1' => 'bbffff', + 'paleturquoise2' => 'aeeeee', + 'paleturquoise3' => '96cdcd', + 'paleturquoise4' => '668b8b', + 'palevioletred' => 'db7093', + 'palevioletred1' => 'ff82ab', + 'palevioletred2' => 'ee799f', + 'palevioletred3' => 'cd6889', + 'palevioletred4' => '8b475d', + 'papayawhip' => 'ffefd5', + 'peachpuff1' => 'ffdab9', + 'peachpuff2' => 'eecbad', + 'peachpuff3' => 'cdaf95', + 'peachpuff4' => '8b7765', + 'pink' => 'ffc0cb', + 'pink1' => 'ffb5c5', + 'pink2' => 'eea9b8', + 'pink3' => 'cd919e', + 'pink4' => '8b636c', + 'plum' => 'dda0dd', + 'plum1' => 'ffbbff', + 'plum2' => 'eeaeee', + 'plum3' => 'cd96cd', + 'plum4' => '8b668b', + 'powderblue' => 'b0e0e6', + 'purple' => 'a020f0', + 'rebeccapurple' => '663399', + 'purple1' => '9b30ff', + 'purple2' => '912cee', + 'purple3' => '7d26cd', + 'purple4' => '551a8b', + 'red' => 'ff0000', + 'red1' => 'ff0000', + 'red2' => 'ee0000', + 'red3' => 'cd0000', + 'red4' => '8b0000', + 'rosybrown' => 'bc8f8f', + 'rosybrown1' => 'ffc1c1', + 'rosybrown2' => 'eeb4b4', + 'rosybrown3' => 'cd9b9b', + 'rosybrown4' => '8b6969', + 'royalblue' => '4169e1', + 'royalblue1' => '4876ff', + 'royalblue2' => '436eee', + 'royalblue3' => '3a5fcd', + 'royalblue4' => '27408b', + 'saddlebrown' => '8b4513', + 'salmon' => 'fa8072', + 'salmon1' => 'ff8c69', + 'salmon2' => 'ee8262', + 'salmon3' => 'cd7054', + 'salmon4' => '8b4c39', + 'sandybrown' => 'f4a460', + 'seagreen1' => '54ff9f', + 'seagreen2' => '4eee94', + 'seagreen3' => '43cd80', + 'seagreen4' => '2e8b57', + 'seashell1' => 'fff5ee', + 'seashell2' => 'eee5de', + 'seashell3' => 'cdc5bf', + 'seashell4' => '8b8682', + 'sienna' => 'a0522d', + 'sienna1' => 'ff8247', + 'sienna2' => 'ee7942', + 'sienna3' => 'cd6839', + 'sienna4' => '8b4726', + 'silver' => 'c0c0c0', + 'skyblue' => '87ceeb', + 'skyblue1' => '87ceff', + 'skyblue2' => '7ec0ee', + 'skyblue3' => '6ca6cd', + 'skyblue4' => '4a708b', + 'slateblue' => '6a5acd', + 'slateblue1' => '836fff', + 'slateblue2' => '7a67ee', + 'slateblue3' => '6959cd', + 'slateblue4' => '473c8b', + 'slategray' => '708090', + 'slategray1' => 'c6e2ff', + 'slategray2' => 'b9d3ee', + 'slategray3' => '9fb6cd', + 'slategray4' => '6c7b8b', + 'snow1' => 'fffafa', + 'snow2' => 'eee9e9', + 'snow3' => 'cdc9c9', + 'snow4' => '8b8989', + 'springgreen1' => '00ff7f', + 'springgreen2' => '00ee76', + 'springgreen3' => '00cd66', + 'springgreen4' => '008b45', + 'steelblue' => '4682b4', + 'steelblue1' => '63b8ff', + 'steelblue2' => '5cacee', + 'steelblue3' => '4f94cd', + 'steelblue4' => '36648b', + 'tan' => 'd2b48c', + 'tan1' => 'ffa54f', + 'tan2' => 'ee9a49', + 'tan3' => 'cd853f', + 'tan4' => '8b5a2b', + 'teal' => '008080', + 'thistle' => 'd8bfd8', + 'thistle1' => 'ffe1ff', + 'thistle2' => 'eed2ee', + 'thistle3' => 'cdb5cd', + 'thistle4' => '8b7b8b', + 'tomato1' => 'ff6347', + 'tomato2' => 'ee5c42', + 'tomato3' => 'cd4f39', + 'tomato4' => '8b3626', + 'turquoise' => '40e0d0', + 'turquoise1' => '00f5ff', + 'turquoise2' => '00e5ee', + 'turquoise3' => '00c5cd', + 'turquoise4' => '00868b', + 'violet' => 'ee82ee', + 'violetred' => 'd02090', + 'violetred1' => 'ff3e96', + 'violetred2' => 'ee3a8c', + 'violetred3' => 'cd3278', + 'violetred4' => '8b2252', + 'wheat' => 'f5deb3', + 'wheat1' => 'ffe7ba', + 'wheat2' => 'eed8ae', + 'wheat3' => 'cdba96', + 'wheat4' => '8b7e66', + 'white' => 'ffffff', + 'whitesmoke' => 'f5f5f5', + 'yellow' => 'ffff00', + 'yellow1' => 'ffff00', + 'yellow2' => 'eeee00', + 'yellow3' => 'cdcd00', + 'yellow4' => '8b8b00', + 'yellowgreen' => '9acd32', + ]; + + protected $face; + + protected $size; + + protected $color; + + protected $bold = false; + + protected $italic = false; + + protected $underline = false; + + protected $superscript = false; + + protected $subscript = false; + + protected $strikethrough = false; + + protected $startTagCallbacks = [ + 'font' => 'startFontTag', + 'b' => 'startBoldTag', + 'strong' => 'startBoldTag', + 'i' => 'startItalicTag', + 'em' => 'startItalicTag', + 'u' => 'startUnderlineTag', + 'ins' => 'startUnderlineTag', + 'del' => 'startStrikethruTag', + 'sup' => 'startSuperscriptTag', + 'sub' => 'startSubscriptTag', + ]; + + protected $endTagCallbacks = [ + 'font' => 'endFontTag', + 'b' => 'endBoldTag', + 'strong' => 'endBoldTag', + 'i' => 'endItalicTag', + 'em' => 'endItalicTag', + 'u' => 'endUnderlineTag', + 'ins' => 'endUnderlineTag', + 'del' => 'endStrikethruTag', + 'sup' => 'endSuperscriptTag', + 'sub' => 'endSubscriptTag', + 'br' => 'breakTag', + 'p' => 'breakTag', + 'h1' => 'breakTag', + 'h2' => 'breakTag', + 'h3' => 'breakTag', + 'h4' => 'breakTag', + 'h5' => 'breakTag', + 'h6' => 'breakTag', + ]; + + protected $stack = []; + + protected $stringData = ''; + + /** + * @var RichText + */ + protected $richTextObject; + + protected function initialise(): void + { + $this->face = $this->size = $this->color = null; + $this->bold = $this->italic = $this->underline = $this->superscript = $this->subscript = $this->strikethrough = false; + + $this->stack = []; + + $this->stringData = ''; + } + + /** + * Parse HTML formatting and return the resulting RichText. + * + * @param string $html + * + * @return RichText + */ + public function toRichTextObject($html) + { + $this->initialise(); + + // Create a new DOM object + $dom = new DOMDocument(); + // Load the HTML file into the DOM object + // Note the use of error suppression, because typically this will be an html fragment, so not fully valid markup + $prefix = ''; + @$dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + // Discard excess white space + $dom->preserveWhiteSpace = false; + + $this->richTextObject = new RichText(); + $this->parseElements($dom); + + // Clean any further spurious whitespace + $this->cleanWhitespace(); + + return $this->richTextObject; + } + + protected function cleanWhitespace(): void + { + foreach ($this->richTextObject->getRichTextElements() as $key => $element) { + $text = $element->getText(); + // Trim any leading spaces on the first run + if ($key == 0) { + $text = ltrim($text); + } + // Trim any spaces immediately after a line break + $text = preg_replace('/\n */mu', "\n", $text); + $element->setText($text); + } + } + + protected function buildTextRun(): void + { + $text = $this->stringData; + if (trim($text) === '') { + return; + } + + $richtextRun = $this->richTextObject->createTextRun($this->stringData); + if ($this->face) { + $richtextRun->getFont()->setName($this->face); + } + if ($this->size) { + $richtextRun->getFont()->setSize($this->size); + } + if ($this->color) { + $richtextRun->getFont()->setColor(new Color('ff' . $this->color)); + } + if ($this->bold) { + $richtextRun->getFont()->setBold(true); + } + if ($this->italic) { + $richtextRun->getFont()->setItalic(true); + } + if ($this->underline) { + $richtextRun->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } + if ($this->superscript) { + $richtextRun->getFont()->setSuperscript(true); + } + if ($this->subscript) { + $richtextRun->getFont()->setSubscript(true); + } + if ($this->strikethrough) { + $richtextRun->getFont()->setStrikethrough(true); + } + $this->stringData = ''; + } + + protected function rgbToColour(string $rgbValue): string + { + preg_match_all('/\d+/', $rgbValue, $values); + foreach ($values[0] as &$value) { + $value = str_pad(dechex($value), 2, '0', STR_PAD_LEFT); + } + + return implode('', $values[0]); + } + + public static function colourNameLookup(string $colorName): string + { + return self::$colourMap[$colorName] ?? ''; + } + + protected function startFontTag($tag): void + { + foreach ($tag->attributes as $attribute) { + $attributeName = strtolower($attribute->name); + $attributeValue = $attribute->value; + + if ($attributeName == 'color') { + if (preg_match('/rgb\s*\(/', $attributeValue)) { + $this->$attributeName = $this->rgbToColour($attributeValue); + } elseif (strpos(trim($attributeValue), '#') === 0) { + $this->$attributeName = ltrim($attributeValue, '#'); + } else { + $this->$attributeName = static::colourNameLookup($attributeValue); + } + } else { + $this->$attributeName = $attributeValue; + } + } + } + + protected function endFontTag(): void + { + $this->face = $this->size = $this->color = null; + } + + protected function startBoldTag(): void + { + $this->bold = true; + } + + protected function endBoldTag(): void + { + $this->bold = false; + } + + protected function startItalicTag(): void + { + $this->italic = true; + } + + protected function endItalicTag(): void + { + $this->italic = false; + } + + protected function startUnderlineTag(): void + { + $this->underline = true; + } + + protected function endUnderlineTag(): void + { + $this->underline = false; + } + + protected function startSubscriptTag(): void + { + $this->subscript = true; + } + + protected function endSubscriptTag(): void + { + $this->subscript = false; + } + + protected function startSuperscriptTag(): void + { + $this->superscript = true; + } + + protected function endSuperscriptTag(): void + { + $this->superscript = false; + } + + protected function startStrikethruTag(): void + { + $this->strikethrough = true; + } + + protected function endStrikethruTag(): void + { + $this->strikethrough = false; + } + + protected function breakTag(): void + { + $this->stringData .= "\n"; + } + + protected function parseTextNode(DOMText $textNode): void + { + $domText = preg_replace( + '/\s+/u', + ' ', + str_replace(["\r", "\n"], ' ', $textNode->nodeValue) + ); + $this->stringData .= $domText; + $this->buildTextRun(); + } + + /** + * @param string $callbackTag + */ + protected function handleCallback(DOMElement $element, $callbackTag, array $callbacks): void + { + if (isset($callbacks[$callbackTag])) { + $elementHandler = $callbacks[$callbackTag]; + if (method_exists($this, $elementHandler)) { + call_user_func([$this, $elementHandler], $element); + } + } + } + + protected function parseElementNode(DOMElement $element): void + { + $callbackTag = strtolower($element->nodeName); + $this->stack[] = $callbackTag; + + $this->handleCallback($element, $callbackTag, $this->startTagCallbacks); + + $this->parseElements($element); + array_pop($this->stack); + + $this->handleCallback($element, $callbackTag, $this->endTagCallbacks); + } + + protected function parseElements(DOMNode $element): void + { + foreach ($element->childNodes as $child) { + if ($child instanceof DOMText) { + $this->parseTextNode($child); + } elseif ($child instanceof DOMElement) { + $this->parseElementNode($child); + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IComparable.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IComparable.php new file mode 100644 index 0000000..c215847 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IComparable.php @@ -0,0 +1,13 @@ + Reader\Xlsx::class, + 'Xls' => Reader\Xls::class, + 'Xml' => Reader\Xml::class, + 'Ods' => Reader\Ods::class, + 'Slk' => Reader\Slk::class, + 'Gnumeric' => Reader\Gnumeric::class, + 'Html' => Reader\Html::class, + 'Csv' => Reader\Csv::class, + ]; + + private static $writers = [ + 'Xls' => Writer\Xls::class, + 'Xlsx' => Writer\Xlsx::class, + 'Ods' => Writer\Ods::class, + 'Csv' => Writer\Csv::class, + 'Html' => Writer\Html::class, + 'Tcpdf' => Writer\Pdf\Tcpdf::class, + 'Dompdf' => Writer\Pdf\Dompdf::class, + 'Mpdf' => Writer\Pdf\Mpdf::class, + ]; + + /** + * Create Writer\IWriter. + */ + public static function createWriter(Spreadsheet $spreadsheet, string $writerType): IWriter + { + if (!isset(self::$writers[$writerType])) { + throw new Writer\Exception("No writer found for type $writerType"); + } + + // Instantiate writer + $className = self::$writers[$writerType]; + + return new $className($spreadsheet); + } + + /** + * Create IReader. + */ + public static function createReader(string $readerType): IReader + { + if (!isset(self::$readers[$readerType])) { + throw new Reader\Exception("No reader found for type $readerType"); + } + + // Instantiate reader + $className = self::$readers[$readerType]; + + return new $className(); + } + + /** + * Loads Spreadsheet from file using automatic Reader\IReader resolution. + * + * @param string $filename The name of the spreadsheet file + */ + public static function load(string $filename, int $flags = 0): Spreadsheet + { + $reader = self::createReaderForFile($filename); + + return $reader->load($filename, $flags); + } + + /** + * Identify file type using automatic IReader resolution. + */ + public static function identify(string $filename): string + { + $reader = self::createReaderForFile($filename); + $className = get_class($reader); + $classType = explode('\\', $className); + unset($reader); + + return array_pop($classType); + } + + /** + * Create Reader\IReader for file using automatic IReader resolution. + */ + public static function createReaderForFile(string $filename): IReader + { + File::assertFile($filename); + + // First, lucky guess by inspecting file extension + $guessedReader = self::getReaderTypeFromExtension($filename); + if ($guessedReader !== null) { + $reader = self::createReader($guessedReader); + + // Let's see if we are lucky + if ($reader->canRead($filename)) { + return $reader; + } + } + + // If we reach here then "lucky guess" didn't give any result + // Try walking through all the options in self::$autoResolveClasses + foreach (self::$readers as $type => $class) { + // Ignore our original guess, we know that won't work + if ($type !== $guessedReader) { + $reader = self::createReader($type); + if ($reader->canRead($filename)) { + return $reader; + } + } + } + + throw new Reader\Exception('Unable to identify a reader for this file'); + } + + /** + * Guess a reader type from the file extension, if any. + */ + private static function getReaderTypeFromExtension(string $filename): ?string + { + $pathinfo = pathinfo($filename); + if (!isset($pathinfo['extension'])) { + return null; + } + + switch (strtolower($pathinfo['extension'])) { + case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet + case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded) + case 'xltx': // Excel (OfficeOpenXML) Template + case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded) + return 'Xlsx'; + case 'xls': // Excel (BIFF) Spreadsheet + case 'xlt': // Excel (BIFF) Template + return 'Xls'; + case 'ods': // Open/Libre Offic Calc + case 'ots': // Open/Libre Offic Calc Template + return 'Ods'; + case 'slk': + return 'Slk'; + case 'xml': // Excel 2003 SpreadSheetML + return 'Xml'; + case 'gnumeric': + return 'Gnumeric'; + case 'htm': + case 'html': + return 'Html'; + case 'csv': + // Do nothing + // We must not try to use CSV reader since it loads + // all files including Excel files etc. + return null; + default: + return null; + } + } + + /** + * Register a writer with its type and class name. + */ + public static function registerWriter(string $writerType, string $writerClass): void + { + if (!is_a($writerClass, IWriter::class, true)) { + throw new Writer\Exception('Registered writers must implement ' . IWriter::class); + } + + self::$writers[$writerType] = $writerClass; + } + + /** + * Register a reader with its type and class name. + */ + public static function registerReader(string $readerType, string $readerClass): void + { + if (!is_a($readerClass, IReader::class, true)) { + throw new Reader\Exception('Registered readers must implement ' . IReader::class); + } + + self::$readers[$readerType] = $readerClass; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedFormula.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedFormula.php new file mode 100644 index 0000000..500151f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedFormula.php @@ -0,0 +1,45 @@ +value; + } + + /** + * Set the formula value. + */ + public function setFormula(string $formula): self + { + if (!empty($formula)) { + $this->value = $formula; + } + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedRange.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedRange.php new file mode 100644 index 0000000..db9c5f1 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/NamedRange.php @@ -0,0 +1,55 @@ +value; + } + + /** + * Set the range value. + */ + public function setRange(string $range): self + { + if (!empty($range)) { + $this->value = $range; + } + + return $this; + } + + public function getCellsInRange(): array + { + $range = $this->value; + if (substr($range, 0, 1) === '=') { + $range = substr($range, 1); + } + + return Coordinate::extractAllCellReferencesInRange($range); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Exception.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Exception.php new file mode 100644 index 0000000..ddbc9b5 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Exception.php @@ -0,0 +1,9 @@ + [ + '10' => DataType::TYPE_NULL, + '20' => DataType::TYPE_BOOL, + '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel + '40' => DataType::TYPE_NUMERIC, // Float + '50' => DataType::TYPE_ERROR, + '60' => DataType::TYPE_STRING, + //'70': // Cell Range + //'80': // Array + ], + ]; + + /** + * Create a new Gnumeric. + */ + public function __construct() + { + parent::__construct(); + $this->referenceHelper = ReferenceHelper::getInstance(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + // Check if gzlib functions are available + if (File::testFileNoThrow($filename) && function_exists('gzread')) { + // Read signature data (first 3 bytes) + $fh = fopen($filename, 'rb'); + if ($fh !== false) { + $data = fread($fh, 2); + fclose($fh); + } + } + + return isset($data) && $data === chr(0x1F) . chr(0x8B); + } + + private static function matchXml(XMLReader $xml, string $expectedLocalName): bool + { + return $xml->namespaceURI === self::NAMESPACE_GNM + && $xml->localName === $expectedLocalName + && $xml->nodeType === XMLReader::ELEMENT; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @param string $filename + * + * @return array + */ + public function listWorksheetNames($filename) + { + File::assertFile($filename); + + $xml = new XMLReader(); + $xml->xml($this->securityScanner->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions()); + $xml->setParserProperty(2, true); + + $worksheetNames = []; + while ($xml->read()) { + if (self::matchXml($xml, 'SheetName')) { + $xml->read(); // Move onto the value node + $worksheetNames[] = (string) $xml->value; + } elseif (self::matchXml($xml, 'Sheets')) { + // break out of the loop once we've got our sheet names rather than parse the entire file + break; + } + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @param string $filename + * + * @return array + */ + public function listWorksheetInfo($filename) + { + File::assertFile($filename); + + $xml = new XMLReader(); + $xml->xml($this->securityScanner->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions()); + $xml->setParserProperty(2, true); + + $worksheetInfo = []; + while ($xml->read()) { + if (self::matchXml($xml, 'Sheet')) { + $tmpInfo = [ + 'worksheetName' => '', + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + ]; + + while ($xml->read()) { + if (self::matchXml($xml, 'Name')) { + $xml->read(); // Move onto the value node + $tmpInfo['worksheetName'] = (string) $xml->value; + } elseif (self::matchXml($xml, 'MaxCol')) { + $xml->read(); // Move onto the value node + $tmpInfo['lastColumnIndex'] = (int) $xml->value; + $tmpInfo['totalColumns'] = (int) $xml->value + 1; + } elseif (self::matchXml($xml, 'MaxRow')) { + $xml->read(); // Move onto the value node + $tmpInfo['totalRows'] = (int) $xml->value + 1; + + break; + } + } + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $worksheetInfo[] = $tmpInfo; + } + } + + return $worksheetInfo; + } + + /** + * @param string $filename + * + * @return string + */ + private function gzfileGetContents($filename) + { + $file = @gzopen($filename, 'rb'); + $data = ''; + if ($file !== false) { + while (!gzeof($file)) { + $data .= gzread($file, 1024); + } + gzclose($file); + } + + return $data; + } + + public static function gnumericMappings(): array + { + return array_merge(self::$mappings, Styles::$mappings); + } + + private function processComments(SimpleXMLElement $sheet): void + { + if ((!$this->readDataOnly) && (isset($sheet->Objects))) { + foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) { + $commentAttributes = $comment->attributes(); + // Only comment objects are handled at the moment + if ($commentAttributes && $commentAttributes->Text) { + $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound) + ->setAuthor((string) $commentAttributes->Author) + ->setText($this->parseRichText((string) $commentAttributes->Text)); + } + } + } + } + + /** + * @param mixed $value + */ + private static function testSimpleXml($value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + + /** + * Loads Spreadsheet from file. + * + * @return Spreadsheet + */ + public function load(string $filename, int $flags = 0) + { + $this->processFlags($flags); + + // Create new Spreadsheet + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads from file into Spreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + $this->spreadsheet = $spreadsheet; + File::assertFile($filename); + + $gFileData = $this->gzfileGetContents($filename); + + $xml2 = simplexml_load_string($this->securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); + $xml = self::testSimpleXml($xml2); + + $gnmXML = $xml->children(self::NAMESPACE_GNM); + (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML); + + $worksheetID = 0; + foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) { + $sheet = self::testSimpleXml($sheetOrNull); + $worksheetName = (string) $sheet->Name; + if (is_array($this->loadSheetsOnly) && !in_array($worksheetName, $this->loadSheetsOnly, true)) { + continue; + } + + $maxRow = $maxCol = 0; + + // Create new Worksheet + $this->spreadsheet->createSheet(); + $this->spreadsheet->setActiveSheetIndex($worksheetID); + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula + // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet + // name in line with the formula, not the reverse + $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + + if (!$this->readDataOnly) { + (new PageSetup($this->spreadsheet)) + ->printInformation($sheet) + ->sheetMargins($sheet); + } + + foreach ($sheet->Cells->Cell as $cellOrNull) { + $cell = self::testSimpleXml($cellOrNull); + $cellAttributes = self::testSimpleXml($cell->attributes()); + $row = (int) $cellAttributes->Row + 1; + $column = (int) $cellAttributes->Col; + + if ($row > $maxRow) { + $maxRow = $row; + } + if ($column > $maxCol) { + $maxCol = $column; + } + + $column = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if ($this->getReadFilter() !== null) { + if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) { + continue; + } + } + + $ValueType = $cellAttributes->ValueType; + $ExprID = (string) $cellAttributes->ExprID; + $type = DataType::TYPE_FORMULA; + if ($ExprID > '') { + if (((string) $cell) > '') { + $this->expressions[$ExprID] = [ + 'column' => $cellAttributes->Col, + 'row' => $cellAttributes->Row, + 'formula' => (string) $cell, + ]; + } else { + $expression = $this->expressions[$ExprID]; + + $cell = $this->referenceHelper->updateFormulaReferences( + $expression['formula'], + 'A1', + $cellAttributes->Col - $expression['column'], + $cellAttributes->Row - $expression['row'], + $worksheetName + ); + } + $type = DataType::TYPE_FORMULA; + } else { + $vtype = (string) $ValueType; + if (array_key_exists($vtype, self::$mappings['dataType'])) { + $type = self::$mappings['dataType'][$vtype]; + } + if ($vtype === '20') { // Boolean + $cell = $cell == 'TRUE'; + } + } + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); + } + + if ($sheet->Styles !== null) { + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + } + + $this->processComments($sheet); + $this->processColumnWidths($sheet, $maxCol); + $this->processRowHeights($sheet, $maxRow); + $this->processMergedCells($sheet); + $this->processAutofilter($sheet); + + ++$worksheetID; + } + + $this->processDefinedNames($gnmXML); + + // Return + return $this->spreadsheet; + } + + private function processMergedCells(?SimpleXMLElement $sheet): void + { + // Handle Merged Cells in this worksheet + if ($sheet !== null && isset($sheet->MergedRegions)) { + foreach ($sheet->MergedRegions->Merge as $mergeCells) { + if (strpos((string) $mergeCells, ':') !== false) { + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + } + } + } + } + + private function processAutofilter(?SimpleXMLElement $sheet): void + { + if ($sheet !== null && isset($sheet->Filters)) { + foreach ($sheet->Filters->Filter as $autofilter) { + if ($autofilter !== null) { + $attributes = $autofilter->attributes(); + if (isset($attributes['Area'])) { + $this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']); + } + } + } + } + } + + private function setColumnWidth(int $whichColumn, float $defaultWidth): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setWidth($defaultWidth); + } + } + + private function setColumnInvisible(int $whichColumn): void + { + $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + if ($columnDimension !== null) { + $columnDimension->setVisible(false); + } + } + + private function processColumnLoop(int $whichColumn, int $maxCol, ?SimpleXMLElement $columnOverride, float $defaultWidth): int + { + $columnOverride = self::testSimpleXml($columnOverride); + $columnAttributes = self::testSimpleXml($columnOverride->attributes()); + $column = $columnAttributes['No']; + $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; + $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); + $columnCount = (int) ($columnAttributes['Count'] ?? 1); + while ($whichColumn < $column) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; + } + while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) { + $this->setColumnWidth($whichColumn, $columnWidth); + if ($hidden) { + $this->setColumnInvisible($whichColumn); + } + ++$whichColumn; + } + + return $whichColumn; + } + + private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void + { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) { + // Column Widths + $defaultWidth = 0; + $columnAttributes = $sheet->Cols->attributes(); + if ($columnAttributes !== null) { + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; + } + $whichColumn = 0; + foreach ($sheet->Cols->ColInfo as $columnOverride) { + $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth); + } + while ($whichColumn <= $maxCol) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; + } + } + } + + private function setRowHeight(int $whichRow, float $defaultHeight): void + { + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setRowHeight($defaultHeight); + } + } + + private function setRowInvisible(int $whichRow): void + { + $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow); + if ($rowDimension !== null) { + $rowDimension->setVisible(false); + } + } + + private function processRowLoop(int $whichRow, int $maxRow, ?SimpleXMLElement $rowOverride, float $defaultHeight): int + { + $rowOverride = self::testSimpleXml($rowOverride); + $rowAttributes = self::testSimpleXml($rowOverride->attributes()); + $row = $rowAttributes['No']; + $rowHeight = (float) $rowAttributes['Unit']; + $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); + $rowCount = (int) ($rowAttributes['Count'] ?? 1); + while ($whichRow < $row) { + ++$whichRow; + $this->setRowHeight($whichRow, $defaultHeight); + } + while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) { + ++$whichRow; + $this->setRowHeight($whichRow, $rowHeight); + if ($hidden) { + $this->setRowInvisible($whichRow); + } + } + + return $whichRow; + } + + private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void + { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) { + // Row Heights + $defaultHeight = 0; + $rowAttributes = $sheet->Rows->attributes(); + if ($rowAttributes !== null) { + $defaultHeight = (float) $rowAttributes['DefaultSizePts']; + } + $whichRow = 0; + + foreach ($sheet->Rows->RowInfo as $rowOverride) { + $whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight); + } + // never executed, I can't figure out any circumstances + // under which it would be executed, and, even if + // such exist, I'm not convinced this is needed. + //while ($whichRow < $maxRow) { + // ++$whichRow; + // $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight); + //} + } + } + + private function processDefinedNames(?SimpleXMLElement $gnmXML): void + { + // Loop through definedNames (global named ranges) + if ($gnmXML !== null && isset($gnmXML->Names)) { + foreach ($gnmXML->Names->Name as $definedName) { + $name = (string) $definedName->name; + $value = (string) $definedName->value; + if (stripos($value, '#REF!') !== false) { + continue; + } + + [$worksheetName] = Worksheet::extractSheetTitle($value, true); + $worksheetName = trim($worksheetName, "'"); + $worksheet = $this->spreadsheet->getSheetByName($worksheetName); + // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet + if ($worksheet !== null) { + $this->spreadsheet->addDefinedName(DefinedName::createInstance($name, $worksheet, $value)); + } + } + } + } + + private function parseRichText(string $is): RichText + { + $value = new RichText(); + $value->createText($is); + + return $value; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php new file mode 100644 index 0000000..5b501e0 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php @@ -0,0 +1,142 @@ +spreadsheet = $spreadsheet; + } + + public function printInformation(SimpleXMLElement $sheet): self + { + if (isset($sheet->PrintInformation)) { + $printInformation = $sheet->PrintInformation[0]; + if (!$printInformation) { + return $this; + } + + $scale = (string) $printInformation->Scale->attributes()['percentage']; + $pageOrder = (string) $printInformation->order; + $orientation = (string) $printInformation->orientation; + $horizontalCentered = (string) $printInformation->hcenter->attributes()['value']; + $verticalCentered = (string) $printInformation->vcenter->attributes()['value']; + + $this->spreadsheet->getActiveSheet()->getPageSetup() + ->setPageOrder($pageOrder === 'r_then_d' ? WorksheetPageSetup::PAGEORDER_OVER_THEN_DOWN : WorksheetPageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) $scale) + ->setOrientation($orientation ?? WorksheetPageSetup::ORIENTATION_DEFAULT) + ->setHorizontalCentered((bool) $horizontalCentered) + ->setVerticalCentered((bool) $verticalCentered); + } + + return $this; + } + + public function sheetMargins(SimpleXMLElement $sheet): self + { + if (isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { + $marginSet = [ + // Default Settings + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ]; + + $marginSet = $this->buildMarginSet($sheet, $marginSet); + $this->adjustMargins($marginSet); + } + + return $this; + } + + private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array + { + foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) { + $marginAttributes = $margin->attributes(); + $marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt + // Convert value in points to inches + $marginSize = PageMargins::fromPoints((float) $marginSize); + $marginSet[$key] = $marginSize; + } + + return $marginSet; + } + + private function adjustMargins(array $marginSet): void + { + foreach ($marginSet as $key => $marginSize) { + // Gnumeric is quirky in the way it displays the header/footer values: + // header is actually the sum of top and header; footer is actually the sum of bottom and footer + // then top is actually the header value, and bottom is actually the footer value + switch ($key) { + case 'left': + case 'right': + $this->sheetMargin($key, $marginSize); + + break; + case 'top': + $this->sheetMargin($key, $marginSet['header'] ?? 0); + + break; + case 'bottom': + $this->sheetMargin($key, $marginSet['footer'] ?? 0); + + break; + case 'header': + $this->sheetMargin($key, ($marginSet['top'] ?? 0) - $marginSize); + + break; + case 'footer': + $this->sheetMargin($key, ($marginSet['bottom'] ?? 0) - $marginSize); + + break; + } + } + } + + private function sheetMargin(string $key, float $marginSize): void + { + switch ($key) { + case 'top': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setTop($marginSize); + + break; + case 'bottom': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom($marginSize); + + break; + case 'left': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft($marginSize); + + break; + case 'right': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setRight($marginSize); + + break; + case 'header': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader($marginSize); + + break; + case 'footer': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter($marginSize); + + break; + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php new file mode 100644 index 0000000..c37f1c1 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php @@ -0,0 +1,1057 @@ + [ + 'font' => [ + 'bold' => true, + 'size' => 24, + ], + ], // Bold, 24pt + 'h2' => [ + 'font' => [ + 'bold' => true, + 'size' => 18, + ], + ], // Bold, 18pt + 'h3' => [ + 'font' => [ + 'bold' => true, + 'size' => 13.5, + ], + ], // Bold, 13.5pt + 'h4' => [ + 'font' => [ + 'bold' => true, + 'size' => 12, + ], + ], // Bold, 12pt + 'h5' => [ + 'font' => [ + 'bold' => true, + 'size' => 10, + ], + ], // Bold, 10pt + 'h6' => [ + 'font' => [ + 'bold' => true, + 'size' => 7.5, + ], + ], // Bold, 7.5pt + 'a' => [ + 'font' => [ + 'underline' => true, + 'color' => [ + 'argb' => Color::COLOR_BLUE, + ], + ], + ], // Blue underlined + 'hr' => [ + 'borders' => [ + 'bottom' => [ + 'borderStyle' => Border::BORDER_THIN, + 'color' => [ + Color::COLOR_BLACK, + ], + ], + ], + ], // Bottom border + 'strong' => [ + 'font' => [ + 'bold' => true, + ], + ], // Bold + 'b' => [ + 'font' => [ + 'bold' => true, + ], + ], // Bold + 'i' => [ + 'font' => [ + 'italic' => true, + ], + ], // Italic + 'em' => [ + 'font' => [ + 'italic' => true, + ], + ], // Italic + ]; + + /** @var array */ + protected $rowspan = []; + + /** + * Create a new HTML Reader instance. + */ + public function __construct() + { + parent::__construct(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Validate that the current file is an HTML file. + */ + public function canRead(string $filename): bool + { + // Check if file exists + try { + $this->openFile($filename); + } catch (Exception $e) { + return false; + } + + $beginning = $this->readBeginning(); + $startWithTag = self::startsWithTag($beginning); + $containsTags = self::containsTags($beginning); + $endsWithTag = self::endsWithTag($this->readEnding()); + + fclose($this->fileHandle); + + return $startWithTag && $containsTags && $endsWithTag; + } + + private function readBeginning(): string + { + fseek($this->fileHandle, 0); + + return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE); + } + + private function readEnding(): string + { + $meta = stream_get_meta_data($this->fileHandle); + $filename = $meta['uri']; + + $size = (int) filesize($filename); + if ($size === 0) { + return ''; + } + + $blockSize = self::TEST_SAMPLE_SIZE; + if ($size < $blockSize) { + $blockSize = $size; + } + + fseek($this->fileHandle, $size - $blockSize); + + return (string) fread($this->fileHandle, $blockSize); + } + + private static function startsWithTag(string $data): bool + { + return '<' === substr(trim($data), 0, 1); + } + + private static function endsWithTag(string $data): bool + { + return '>' === substr(trim($data), -1, 1); + } + + private static function containsTags(string $data): bool + { + return strlen($data) !== strlen(strip_tags($data)); + } + + /** + * Loads Spreadsheet from file. + * + * @return Spreadsheet + */ + public function load(string $filename, int $flags = 0) + { + $this->processFlags($flags); + + // Create new Spreadsheet + $spreadsheet = new Spreadsheet(); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Set input encoding. + * + * @param string $inputEncoding Input encoding, eg: 'ANSI' + * + * @return $this + * + * @codeCoverageIgnore + * + * @deprecated no use is made of this property + */ + public function setInputEncoding($inputEncoding) + { + $this->inputEncoding = $inputEncoding; + + return $this; + } + + /** + * Get input encoding. + * + * @return string + * + * @codeCoverageIgnore + * + * @deprecated no use is made of this property + */ + public function getInputEncoding() + { + return $this->inputEncoding; + } + + // Data Array used for testing only, should write to Spreadsheet object on completion of tests + + /** @var array */ + protected $dataArray = []; + + /** @var int */ + protected $tableLevel = 0; + + /** @var array */ + protected $nestedColumn = ['A']; + + protected function setTableStartColumn(string $column): string + { + if ($this->tableLevel == 0) { + $column = 'A'; + } + ++$this->tableLevel; + $this->nestedColumn[$this->tableLevel] = $column; + + return $this->nestedColumn[$this->tableLevel]; + } + + protected function getTableStartColumn(): string + { + return $this->nestedColumn[$this->tableLevel]; + } + + protected function releaseTableStartColumn(): string + { + --$this->tableLevel; + + return array_pop($this->nestedColumn); + } + + /** + * Flush cell. + * + * @param string $column + * @param int|string $row + * @param mixed $cellContent + */ + protected function flushCell(Worksheet $sheet, $column, $row, &$cellContent): void + { + if (is_string($cellContent)) { + // Simple String content + if (trim($cellContent) > '') { + // Only actually write it if there's content in the string + // Write to worksheet to be done here... + // ... we return the cell so we can mess about with styles more easily + $sheet->setCellValue($column . $row, $cellContent); + $this->dataArray[$row][$column] = $cellContent; + } + } else { + // We have a Rich Text run + // TODO + $this->dataArray[$row][$column] = 'RICH TEXT: ' . $cellContent; + } + $cellContent = (string) ''; + } + + private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void + { + $attributeArray = []; + foreach (($child->attributes ?? []) as $attribute) { + $attributeArray[$attribute->name] = $attribute->value; + } + + if ($child->nodeName === 'body') { + $row = 1; + $column = 'A'; + $cellContent = ''; + $this->tableLevel = 0; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'title') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $sheet->setTitle($cellContent, true, true); + $cellContent = ''; + } else { + $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b']; + + private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) { + if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') { + $sheet->getComment($column . $row) + ->getText() + ->createTextRun($child->textContent); + } else { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } + + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + } else { + $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'hr') { + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + ++$row; + } + // fall through to br + $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + + private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'br' || $child->nodeName === 'hr') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n and set the cell to wrap + $cellContent .= "\n"; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + } else { + // Otherwise flush our existing content and move the row cursor on + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + } + } else { + $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'a') { + foreach ($attributeArray as $attributeName => $attributeValue) { + switch ($attributeName) { + case 'href': + $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue); + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + + break; + case 'class': + if ($attributeValue === 'comment-indicator') { + break; // Ignore - it's just a red square. + } + } + } + // no idea why this should be needed + //$cellContent .= ' '; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p']; + + private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array((string) $child->nodeName, self::H1_ETC, true)) { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n + $cellContent .= $cellContent ? "\n" : ''; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent); + ++$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent); + + if (isset($this->formats[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]); + } + + ++$row; + $column = 'A'; + } + } else { + $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'li') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a \n + $cellContent .= $cellContent ? "\n" : ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent); + } + ++$row; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent); + $column = 'A'; + } + } else { + $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'img') { + $this->insertImage($sheet, $column, $row, $attributeArray); + } else { + $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'table') { + $this->flushCell($sheet, $column, $row, $cellContent); + $column = $this->setTableStartColumn($column); + if ($this->tableLevel > 1 && $row > 1) { + --$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $column = $this->releaseTableStartColumn(); + if ($this->tableLevel > 1) { + ++$column; + } else { + ++$row; + } + } else { + $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'tr') { + $column = $this->getTableStartColumn(); + $cellContent = ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); + } + + ++$row; + } else { + $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName !== 'td' && $child->nodeName !== 'th') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['bgcolor'])) { + $sheet->getStyle("$column$row")->applyFromArray( + [ + 'fill' => [ + 'fillType' => Fill::FILL_SOLID, + 'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])], + ], + ] + ); + } + } + + private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void + { + if (isset($attributeArray['width'])) { + $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width()); + } + } + + private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void + { + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height()); + } + } + + private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['align'])) { + $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']); + } + } + + private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['valign'])) { + $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']); + } + } + + private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['data-format'])) { + $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']); + } + } + + private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + while (isset($this->rowspan[$column . $row])) { + ++$column; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + // apply inline style + $this->applyInlineStyle($sheet, $row, $column, $attributeArray); + + $this->flushCell($sheet, $column, $row, $cellContent); + + $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray); + $this->processDomElementWidth($sheet, $column, $attributeArray); + $this->processDomElementHeight($sheet, $row, $attributeArray); + $this->processDomElementAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementVAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray); + + if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + //create merging rowspan and colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + $column = $columnTo; + } elseif (isset($attributeArray['rowspan'])) { + //create merging rowspan + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + } elseif (isset($attributeArray['colspan'])) { + //create merging colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $sheet->mergeCells($column . $row . ':' . $columnTo . $row); + $column = $columnTo; + } + + ++$column; + } + + protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void + { + foreach ($element->childNodes as $child) { + if ($child instanceof DOMText) { + $domText = preg_replace('/\s+/u', ' ', trim($child->nodeValue)); + if (is_string($cellContent)) { + // simply append the text if the cell content is a plain text string + $cellContent .= $domText; + } + // but if we have a rich text run instead, we need to append it correctly + // TODO + } elseif ($child instanceof DOMElement) { + $this->processDomElementBody($sheet, $row, $column, $cellContent, $child); + } + } + } + + /** + * Make sure mb_convert_encoding returns string. + * + * @param mixed $result + */ + private static function ensureString($result): string + { + return is_string($result) ? $result : ''; + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + * + * @param string $filename + * + * @return Spreadsheet + */ + public function loadIntoExisting($filename, Spreadsheet $spreadsheet) + { + // Validate + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an Invalid HTML file.'); + } + + // Create a new DOM object + $dom = new DOMDocument(); + // Reload the HTML file into the DOM object + try { + $convert = mb_convert_encoding($this->securityScanner->scanFile($filename), 'HTML-ENTITIES', 'UTF-8'); + $loaded = $dom->loadHTML(self::ensureString($convert)); + } catch (Throwable $e) { + $loaded = false; + } + if ($loaded === false) { + throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null); + } + + return $this->loadDocument($dom, $spreadsheet); + } + + /** + * Spreadsheet from content. + * + * @param string $content + */ + public function loadFromString($content, ?Spreadsheet $spreadsheet = null): Spreadsheet + { + // Create a new DOM object + $dom = new DOMDocument(); + // Reload the HTML file into the DOM object + try { + $convert = mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'); + $loaded = $dom->loadHTML(self::ensureString($convert)); + } catch (Throwable $e) { + $loaded = false; + } + if ($loaded === false) { + throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null); + } + + return $this->loadDocument($dom, $spreadsheet ?? new Spreadsheet()); + } + + /** + * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance. + */ + private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet + { + while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { + $spreadsheet->createSheet(); + } + $spreadsheet->setActiveSheetIndex($this->sheetIndex); + + // Discard white space + $document->preserveWhiteSpace = false; + + $row = 0; + $column = 'A'; + $content = ''; + $this->rowspan = []; + $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content); + + // Return + return $spreadsheet; + } + + /** + * Get sheet index. + * + * @return int + */ + public function getSheetIndex() + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $sheetIndex Sheet index + * + * @return $this + */ + public function setSheetIndex($sheetIndex) + { + $this->sheetIndex = $sheetIndex; + + return $this; + } + + /** + * Apply inline css inline style. + * + * NOTES : + * Currently only intended for td & th element, + * and only takes 'background-color' and 'color'; property with HEX color + * + * TODO : + * - Implement to other propertie, such as border + * + * @param int $row + * @param string $column + * @param array $attributeArray + */ + private function applyInlineStyle(Worksheet &$sheet, $row, $column, $attributeArray): void + { + if (!isset($attributeArray['style'])) { + return; + } + + if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); + $cellStyle = $sheet->getStyle($range); + } elseif (isset($attributeArray['rowspan'])) { + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); + $cellStyle = $sheet->getStyle($range); + } elseif (isset($attributeArray['colspan'])) { + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + ++$columnTo; + } + $range = $column . $row . ':' . $columnTo . $row; + $cellStyle = $sheet->getStyle($range); + } else { + $cellStyle = $sheet->getStyle($column . $row); + } + + // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color + $styles = explode(';', $attributeArray['style']); + foreach ($styles as $st) { + $value = explode(':', $st); + $styleName = isset($value[0]) ? trim($value[0]) : null; + $styleValue = isset($value[1]) ? trim($value[1]) : null; + $styleValueString = (string) $styleValue; + + if (!$styleName) { + continue; + } + + switch ($styleName) { + case 'background': + case 'background-color': + $styleColor = $this->getStyleColor($styleValueString); + + if (!$styleColor) { + continue 2; + } + + $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]); + + break; + case 'color': + $styleColor = $this->getStyleColor($styleValueString); + + if (!$styleColor) { + continue 2; + } + + $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]); + + break; + + case 'border': + $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders'); + + break; + + case 'border-top': + $this->setBorderStyle($cellStyle, $styleValueString, 'top'); + + break; + + case 'border-bottom': + $this->setBorderStyle($cellStyle, $styleValueString, 'bottom'); + + break; + + case 'border-left': + $this->setBorderStyle($cellStyle, $styleValueString, 'left'); + + break; + + case 'border-right': + $this->setBorderStyle($cellStyle, $styleValueString, 'right'); + + break; + + case 'font-size': + $cellStyle->getFont()->setSize( + (float) $styleValue + ); + + break; + + case 'font-weight': + if ($styleValue === 'bold' || $styleValue >= 500) { + $cellStyle->getFont()->setBold(true); + } + + break; + + case 'font-style': + if ($styleValue === 'italic') { + $cellStyle->getFont()->setItalic(true); + } + + break; + + case 'font-family': + $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString)); + + break; + + case 'text-decoration': + switch ($styleValue) { + case 'underline': + $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + + break; + case 'line-through': + $cellStyle->getFont()->setStrikethrough(true); + + break; + } + + break; + + case 'text-align': + $cellStyle->getAlignment()->setHorizontal($styleValueString); + + break; + + case 'vertical-align': + $cellStyle->getAlignment()->setVertical($styleValueString); + + break; + + case 'width': + $sheet->getColumnDimension($column)->setWidth( + (new CssDimension($styleValue ?? ''))->width() + ); + + break; + + case 'height': + $sheet->getRowDimension($row)->setRowHeight( + (new CssDimension($styleValue ?? ''))->height() + ); + + break; + + case 'word-wrap': + $cellStyle->getAlignment()->setWrapText( + $styleValue === 'break-word' + ); + + break; + + case 'text-indent': + $cellStyle->getAlignment()->setIndent( + (int) str_replace(['px'], '', $styleValueString) + ); + + break; + } + } + } + + /** + * Check if has #, so we can get clean hex. + * + * @param mixed $value + * + * @return null|string + */ + public function getStyleColor($value) + { + $value = (string) $value; + if (strpos($value ?? '', '#') === 0) { + return substr($value, 1); + } + + return \PhpOffice\PhpSpreadsheet\Helper\Html::colourNameLookup($value); + } + + /** + * @param string $column + * @param int $row + */ + private function insertImage(Worksheet $sheet, $column, $row, array $attributes): void + { + if (!isset($attributes['src'])) { + return; + } + + $src = urldecode($attributes['src']); + $width = isset($attributes['width']) ? (float) $attributes['width'] : null; + $height = isset($attributes['height']) ? (float) $attributes['height'] : null; + $name = $attributes['alt'] ?? null; + + $drawing = new Drawing(); + $drawing->setPath($src); + $drawing->setWorksheet($sheet); + $drawing->setCoordinates($column . $row); + $drawing->setOffsetX(0); + $drawing->setOffsetY(10); + $drawing->setResizeProportional(true); + + if ($name) { + $drawing->setName($name); + } + + if ($width) { + $drawing->setWidth((int) $width); + } + + if ($height) { + $drawing->setHeight((int) $height); + } + + $sheet->getColumnDimension($column)->setWidth( + $drawing->getWidth() / 6 + ); + + $sheet->getRowDimension($row)->setRowHeight( + $drawing->getHeight() * 0.9 + ); + } + + private const BORDER_MAPPINGS = [ + 'dash-dot' => Border::BORDER_DASHDOT, + 'dash-dot-dot' => Border::BORDER_DASHDOTDOT, + 'dashed' => Border::BORDER_DASHED, + 'dotted' => Border::BORDER_DOTTED, + 'double' => Border::BORDER_DOUBLE, + 'hair' => Border::BORDER_HAIR, + 'medium' => Border::BORDER_MEDIUM, + 'medium-dashed' => Border::BORDER_MEDIUMDASHED, + 'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT, + 'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT, + 'none' => Border::BORDER_NONE, + 'slant-dash-dot' => Border::BORDER_SLANTDASHDOT, + 'solid' => Border::BORDER_THIN, + 'thick' => Border::BORDER_THICK, + ]; + + public static function getBorderMappings(): array + { + return self::BORDER_MAPPINGS; + } + + /** + * Map html border style to PhpSpreadsheet border style. + * + * @param string $style + * + * @return null|string + */ + public function getBorderStyle($style) + { + return self::BORDER_MAPPINGS[$style] ?? null; + } + + /** + * @param string $styleValue + * @param string $type + */ + private function setBorderStyle(Style $cellStyle, $styleValue, $type): void + { + if (trim($styleValue) === Border::BORDER_NONE) { + $borderStyle = Border::BORDER_NONE; + $color = null; + } else { + $borderArray = explode(' ', $styleValue); + $borderCount = count($borderArray); + if ($borderCount >= 3) { + $borderStyle = $borderArray[1]; + $color = $borderArray[2]; + } else { + $borderStyle = $borderArray[0]; + $color = $borderArray[1] ?? null; + } + } + + $cellStyle->applyFromArray([ + 'borders' => [ + $type => [ + 'borderStyle' => $this->getBorderStyle($borderStyle), + 'color' => ['rgb' => $this->getStyleColor($color)], + ], + ], + ]); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReadFilter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReadFilter.php new file mode 100644 index 0000000..9f68a7f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/IReadFilter.php @@ -0,0 +1,17 @@ +securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + $mimeType = 'UNKNOWN'; + + // Load file + + if (File::testFileNoThrow($filename, '')) { + $zip = new ZipArchive(); + if ($zip->open($filename) === true) { + // check if it is an OOXML archive + $stat = $zip->statName('mimetype'); + if ($stat && ($stat['size'] <= 255)) { + $mimeType = $zip->getFromName($stat['name']); + } elseif ($zip->statName('META-INF/manifest.xml')) { + $xml = simplexml_load_string( + $this->securityScanner->scan($zip->getFromName('META-INF/manifest.xml')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + $namespacesContent = $xml->getNamespaces(true); + if (isset($namespacesContent['manifest'])) { + $manifest = $xml->children($namespacesContent['manifest']); + foreach ($manifest as $manifestDataSet) { + $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']); + if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') { + $mimeType = (string) $manifestAttributes->{'media-type'}; + + break; + } + } + } + } + + $zip->close(); + } + } + + return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet'; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. + * + * @param string $filename + * + * @return string[] + */ + public function listWorksheetNames($filename) + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetNames = []; + + $xml = new XMLReader(); + $xml->xml( + $this->securityScanner->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), + null, + Settings::getLibXmlLoaderOptions() + ); + $xml->setParserProperty(2, true); + + // Step into the first level of content of the XML + $xml->read(); + while ($xml->read()) { + // Quickly jump through to the office:body node + while ($xml->name !== 'office:body') { + if ($xml->isEmptyElement) { + $xml->read(); + } else { + $xml->next(); + } + } + // Now read each node until we find our first table:table node + while ($xml->read()) { + if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + // Loop through each table:table node reading the table:name attribute for each worksheet name + do { + $worksheetNames[] = $xml->getAttribute('table:name'); + $xml->next(); + } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); + } + } + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @param string $filename + * + * @return array + */ + public function listWorksheetInfo($filename) + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetInfo = []; + + $xml = new XMLReader(); + $xml->xml( + $this->securityScanner->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), + null, + Settings::getLibXmlLoaderOptions() + ); + $xml->setParserProperty(2, true); + + // Step into the first level of content of the XML + $xml->read(); + while ($xml->read()) { + // Quickly jump through to the office:body node + while ($xml->name !== 'office:body') { + if ($xml->isEmptyElement) { + $xml->read(); + } else { + $xml->next(); + } + } + // Now read each node until we find our first table:table node + while ($xml->read()) { + if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + $worksheetNames[] = $xml->getAttribute('table:name'); + + $tmpInfo = [ + 'worksheetName' => $xml->getAttribute('table:name'), + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + ]; + + // Loop through each child node of the table:table element reading + $currCells = 0; + do { + $xml->read(); + if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { + $rowspan = $xml->getAttribute('table:number-rows-repeated'); + $rowspan = empty($rowspan) ? 1 : $rowspan; + $tmpInfo['totalRows'] += $rowspan; + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $currCells = 0; + // Step into the row + $xml->read(); + do { + $doread = true; + if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + if (!$xml->isEmptyElement) { + ++$currCells; + $xml->next(); + $doread = false; + } + } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + $mergeSize = $xml->getAttribute('table:number-columns-repeated'); + $currCells += (int) $mergeSize; + } + if ($doread) { + $xml->read(); + } + } while ($xml->name != 'table:table-row'); + } + } while ($xml->name != 'table:table'); + + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $worksheetInfo[] = $tmpInfo; + } + } + } + + return $worksheetInfo; + } + + /** + * Loads PhpSpreadsheet from file. + * + * @return Spreadsheet + */ + public function load(string $filename, int $flags = 0) + { + $this->processFlags($flags); + + // Create new Spreadsheet + $spreadsheet = new Spreadsheet(); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + * + * @param string $filename + * + * @return Spreadsheet + */ + public function loadIntoExisting($filename, Spreadsheet $spreadsheet) + { + File::assertFile($filename, self::INITIAL_FILE); + + $zip = new ZipArchive(); + $zip->open($filename); + + // Meta + + $xml = @simplexml_load_string( + $this->securityScanner->scan($zip->getFromName('meta.xml')), + 'SimpleXMLElement', + Settings::getLibXmlLoaderOptions() + ); + if ($xml === false) { + throw new Exception('Unable to read data from {$pFilename}'); + } + + $namespacesMeta = $xml->getNamespaces(true); + + (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta); + + // Styles + + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->securityScanner->scan($zip->getFromName('styles.xml')), + Settings::getLibXmlLoaderOptions() + ); + + $pageSettings = new PageSettings($dom); + + // Main Content + + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->securityScanner->scan($zip->getFromName(self::INITIAL_FILE)), + Settings::getLibXmlLoaderOptions() + ); + + $officeNs = $dom->lookupNamespaceUri('office'); + $tableNs = $dom->lookupNamespaceUri('table'); + $textNs = $dom->lookupNamespaceUri('text'); + $xlinkNs = $dom->lookupNamespaceUri('xlink'); + + $pageSettings->readStyleCrossReferences($dom); + + $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); + $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + + // Content + $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') + ->item(0) + ->getElementsByTagNameNS($officeNs, 'spreadsheet'); + + foreach ($spreadsheets as $workbookData) { + /** @var DOMElement $workbookData */ + $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); + + $worksheetID = 0; + foreach ($tables as $worksheetDataSet) { + /** @var DOMElement $worksheetDataSet */ + $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); + + // Check loadSheetsOnly + if ( + isset($this->loadSheetsOnly) + && $worksheetName + && !in_array($worksheetName, $this->loadSheetsOnly) + ) { + continue; + } + + $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name'); + + // Create sheet + if ($worksheetID > 0) { + $spreadsheet->createSheet(); // First sheet is added by default + } + $spreadsheet->setActiveSheetIndex($worksheetID); + + if ($worksheetName) { + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in + // formula cells... during the load, all formulae should be correct, and we're simply + // bringing the worksheet name in line with the formula, not the reverse + $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false); + } + + // Go through every child of table element + $rowID = 1; + foreach ($worksheetDataSet->childNodes as $childNode) { + /** @var DOMElement $childNode */ + + // Filter elements which are not under the "table" ns + if ($childNode->namespaceURI != $tableNs) { + continue; + } + + $key = $childNode->nodeName; + + // Remove ns from node name + if (strpos($key, ':') !== false) { + $keyChunks = explode(':', $key); + $key = array_pop($keyChunks); + } + + switch ($key) { + case 'table-header-rows': + /// TODO :: Figure this out. This is only a partial implementation I guess. + // ($rowData it's not used at all and I'm not sure that PHPExcel + // has an API for this) + +// foreach ($rowData as $keyRowData => $cellData) { +// $rowData = $cellData; +// break; +// } + break; + case 'table-row': + if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); + } else { + $rowRepeats = 1; + } + + $columnID = 'A'; + /** @var DOMElement $cellData */ + foreach ($childNode->childNodes as $cellData) { + if ($this->getReadFilter() !== null) { + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + ++$columnID; + + continue; + } + } + + // Initialize variables + $formatting = $hyperlink = null; + $hasCalculatedValue = false; + $cellDataFormula = ''; + + if ($cellData->hasAttributeNS($tableNs, 'formula')) { + $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); + $hasCalculatedValue = true; + } + + // Annotations + $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); + + if ($annotation->length > 0) { + $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + + if ($textNode->length > 0) { + $text = $this->scanElementForText($textNode->item(0)); + + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->setText($this->parseRichText($text)); +// ->setAuthor( $author ) + } + } + + // Content + + /** @var DOMElement[] $paragraphs */ + $paragraphs = []; + + foreach ($cellData->childNodes as $item) { + /** @var DOMElement $item */ + + // Filter text:p elements + if ($item->nodeName == 'text:p') { + $paragraphs[] = $item; + } + } + + if (count($paragraphs) > 0) { + // Consolidate if there are multiple p records (maybe with spans as well) + $dataArray = []; + + // Text can have multiple text:p and within those, multiple text:span. + // text:p newlines, but text:span does not. + // Also, here we assume there is no text data is span fields are specified, since + // we have no way of knowing proper positioning anyway. + + foreach ($paragraphs as $pData) { + $dataArray[] = $this->scanElementForText($pData); + } + $allCellDataText = implode("\n", $dataArray); + + $type = $cellData->getAttributeNS($officeNs, 'value-type'); + + switch ($type) { + case 'string': + $type = DataType::TYPE_STRING; + $dataValue = $allCellDataText; + + foreach ($paragraphs as $paragraph) { + $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); + if ($link->length > 0) { + $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); + } + } + + break; + case 'boolean': + $type = DataType::TYPE_BOOL; + $dataValue = ($allCellDataText == 'TRUE') ? true : false; + + break; + case 'percentage': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + // percentage should always be float + //if (floor($dataValue) == $dataValue) { + // $dataValue = (int) $dataValue; + //} + $formatting = NumberFormat::FORMAT_PERCENTAGE_00; + + break; + case 'currency': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + $dataValue = (int) $dataValue; + } + $formatting = NumberFormat::FORMAT_CURRENCY_USD_SIMPLE; + + break; + case 'float': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + if ($dataValue == (int) $dataValue) { + $dataValue = (int) $dataValue; + } + } + + break; + case 'date': + $type = DataType::TYPE_NUMERIC; + $value = $cellData->getAttributeNS($officeNs, 'date-value'); + + $dateObj = new DateTime($value); + [$year, $month, $day, $hour, $minute, $second] = explode( + ' ', + $dateObj->format('Y m d H i s') + ); + + $dataValue = Date::formattedPHPToExcel( + (int) $year, + (int) $month, + (int) $day, + (int) $hour, + (int) $minute, + (int) $second + ); + + if ($dataValue != floor($dataValue)) { + $formatting = NumberFormat::FORMAT_DATE_XLSX15 + . ' ' + . NumberFormat::FORMAT_DATE_TIME4; + } else { + $formatting = NumberFormat::FORMAT_DATE_XLSX15; + } + + break; + case 'time': + $type = DataType::TYPE_NUMERIC; + + $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); + + $dataValue = Date::PHPToExcel( + strtotime( + '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) + ) + ); + $formatting = NumberFormat::FORMAT_DATE_TIME4; + + break; + default: + $dataValue = null; + } + } else { + $type = DataType::TYPE_NULL; + $dataValue = null; + } + + if ($hasCalculatedValue) { + $type = DataType::TYPE_FORMULA; + $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); + $cellDataFormula = $this->convertToExcelFormulaValue($cellDataFormula); + } + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + + if ($type !== null) { + for ($i = 0; $i < $colRepeats; ++$i) { + if ($i > 0) { + ++$columnID; + } + + if ($type !== DataType::TYPE_NULL) { + for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { + $rID = $rowID + $rowAdjust; + + $cell = $spreadsheet->getActiveSheet() + ->getCell($columnID . $rID); + + // Set value + if ($hasCalculatedValue) { + $cell->setValueExplicit($cellDataFormula, $type); + } else { + $cell->setValueExplicit($dataValue, $type); + } + + if ($hasCalculatedValue) { + $cell->setCalculatedValue($dataValue); + } + + // Set other properties + if ($formatting !== null) { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode($formatting); + } else { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_GENERAL); + } + + if ($hyperlink !== null) { + $cell->getHyperlink() + ->setUrl($hyperlink); + } + } + } + } + } + + // Merged cells + if ( + $cellData->hasAttributeNS($tableNs, 'number-columns-spanned') + || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') + ) { + if (($type !== DataType::TYPE_NULL) || (!$this->readDataOnly)) { + $columnTo = $columnID; + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { + $columnIndex = Coordinate::columnIndexFromString($columnID); + $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); + $columnIndex -= 2; + + $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); + } + + $rowTo = $rowID; + + if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { + $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; + } + + $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; + $spreadsheet->getActiveSheet()->mergeCells($cellRange); + } + } + + ++$columnID; + } + $rowID += $rowRepeats; + + break; + } + } + $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); + ++$worksheetID; + } + + $autoFilterReader->read($workbookData); + $definedNameReader->read($workbookData); + } + $spreadsheet->setActiveSheetIndex(0); + + if ($zip->locateName('settings.xml') !== false) { + $this->processSettings($zip, $spreadsheet); + } + + // Return + return $spreadsheet; + } + + private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void + { + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->securityScanner->scan($zip->getFromName('settings.xml')), + Settings::getLibXmlLoaderOptions() + ); + //$xlinkNs = $dom->lookupNamespaceUri('xlink'); + $configNs = $dom->lookupNamespaceUri('config'); + //$oooNs = $dom->lookupNamespaceUri('ooo'); + $officeNs = $dom->lookupNamespaceUri('office'); + $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') + ->item(0); + $this->lookForActiveSheet($settings, $spreadsheet, $configNs); + $this->lookForSelectedCells($settings, $spreadsheet, $configNs); + } + + private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void + { + /** @var DOMElement $t */ + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { + try { + $spreadsheet->setActiveSheetIndexByName($t->nodeValue); + } catch (Throwable $e) { + // do nothing + } + + break; + } + } + } + + private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void + { + /** @var DOMElement $t */ + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'Tables') { + foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { + $setRow = $setCol = ''; + $wsname = $ws->getAttributeNs($configNs, 'name'); + foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) { + $attrName = $configItem->getAttributeNs($configNs, 'name'); + if ($attrName === 'CursorPositionX') { + $setCol = $configItem->nodeValue; + } + if ($attrName === 'CursorPositionY') { + $setRow = $configItem->nodeValue; + } + } + $this->setSelected($spreadsheet, $wsname, $setCol, $setRow); + } + + break; + } + } + } + + private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void + { + if (is_numeric($setCol) && is_numeric($setRow)) { + try { + $spreadsheet->getSheetByName($wsname)->setSelectedCellByColumnAndRow($setCol + 1, $setRow + 1); + } catch (Throwable $e) { + // do nothing + } + } + } + + /** + * Recursively scan element. + * + * @return string + */ + protected function scanElementForText(DOMNode $element) + { + $str = ''; + foreach ($element->childNodes as $child) { + /** @var DOMNode $child */ + if ($child->nodeType == XML_TEXT_NODE) { + $str .= $child->nodeValue; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { + // It's a space + + // Multiple spaces? + /** @var DOMAttr $cAttr */ + $cAttr = $child->attributes->getNamedItem('c'); + if ($cAttr) { + $multiplier = (int) $cAttr->nodeValue; + } else { + $multiplier = 1; + } + + $str .= str_repeat(' ', $multiplier); + } + + if ($child->hasChildNodes()) { + $str .= $this->scanElementForText($child); + } + } + + return $str; + } + + /** + * @param string $is + * + * @return RichText + */ + private function parseRichText($is) + { + $value = new RichText(); + $value->createText($is); + + return $value; + } + + private function convertToExcelFormulaValue(string $openOfficeFormula): string + { + $temp = explode('"', $openOfficeFormula); + $tKey = false; + foreach ($temp as &$value) { + // Only replace in alternate array entries (i.e. non-quoted blocks) + if ($tKey = !$tKey) { + // Cell range reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); + // Cell reference in another sheet + $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); + // Cell range reference + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); + // Simple cell reference + $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); + // Convert references to defined names/formulae + $value = str_replace('$$', '', $value ?? ''); + + $value = Calculation::translateSeparator(';', ',', $value, $inBraces); + } + } + + // Then rebuild the formula string + $excelFormula = implode('"', $temp); + + return $excelFormula; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/PageSettings.php new file mode 100644 index 0000000..8d24fd0 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -0,0 +1,139 @@ +setDomNameSpaces($styleDom); + $this->readPageSettingStyles($styleDom); + $this->readStyleMasterLookup($styleDom); + } + + private function setDomNameSpaces(DOMDocument $styleDom): void + { + $this->officeNs = $styleDom->lookupNamespaceUri('office'); + $this->stylesNs = $styleDom->lookupNamespaceUri('style'); + $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); + } + + private function readPageSettingStyles(DOMDocument $styleDom): void + { + $styles = $styleDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'page-layout'); + + foreach ($styles as $styleSet) { + $styleName = $styleSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutProperties = $styleSet->getElementsByTagNameNS($this->stylesNs, 'page-layout-properties')[0]; + $styleOrientation = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-orientation'); + $styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to'); + $stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order'); + $centered = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'table-centering'); + + $marginLeft = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-left'); + $marginRight = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-right'); + $marginTop = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-top'); + $marginBottom = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-bottom'); + $header = $styleSet->getElementsByTagNameNS($this->stylesNs, 'header-style')[0]; + $headerProperties = $header->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0]; + $marginHeader = isset($headerProperties) ? $headerProperties->getAttributeNS($this->stylesFo, 'min-height') : null; + $footer = $styleSet->getElementsByTagNameNS($this->stylesNs, 'footer-style')[0]; + $footerProperties = $footer->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0]; + $marginFooter = isset($footerProperties) ? $footerProperties->getAttributeNS($this->stylesFo, 'min-height') : null; + + $this->pageLayoutStyles[$styleName] = (object) [ + 'orientation' => $styleOrientation ?: PageSetup::ORIENTATION_DEFAULT, + 'scale' => $styleScale ?: 100, + 'printOrder' => $stylePrintOrder, + 'horizontalCentered' => $centered === 'horizontal' || $centered === 'both', + 'verticalCentered' => $centered === 'vertical' || $centered === 'both', + // margin size is already stored in inches, so no UOM conversion is required + 'marginLeft' => (float) $marginLeft ?? 0.7, + 'marginRight' => (float) $marginRight ?? 0.7, + 'marginTop' => (float) $marginTop ?? 0.3, + 'marginBottom' => (float) $marginBottom ?? 0.3, + 'marginHeader' => (float) $marginHeader ?? 0.45, + 'marginFooter' => (float) $marginFooter ?? 0.45, + ]; + } + } + + private function readStyleMasterLookup(DOMDocument $styleDom): void + { + $styleMasterLookup = $styleDom->getElementsByTagNameNS($this->officeNs, 'master-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'master-page'); + + foreach ($styleMasterLookup as $styleMasterSet) { + $styleMasterName = $styleMasterSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutName = $styleMasterSet->getAttributeNS($this->stylesNs, 'page-layout-name'); + $this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName; + } + } + + public function readStyleCrossReferences(DOMDocument $contentDom): void + { + $styleXReferences = $contentDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles') + ->item(0) + ->getElementsByTagNameNS($this->stylesNs, 'style'); + + foreach ($styleXReferences as $styleXreferenceSet) { + $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); + $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + if (!empty($stylePageLayoutName)) { + $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; + } + } + } + + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { + return; + } + $masterStyleName = $this->masterStylesCrossReference[$styleName]; + + if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) { + return; + } + $printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName]; + + if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) { + return; + } + $printSettings = $this->pageLayoutStyles[$printSettingsIndex]; + + $worksheet->getPageSetup() + ->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT) + ->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) trim($printSettings->scale, '%')) + ->setHorizontalCentered($printSettings->horizontalCentered) + ->setVerticalCentered($printSettings->verticalCentered); + + $worksheet->getPageMargins() + ->setLeft($printSettings->marginLeft) + ->setRight($printSettings->marginRight) + ->setTop($printSettings->marginTop) + ->setBottom($printSettings->marginBottom) + ->setHeader($printSettings->marginHeader) + ->setFooter($printSettings->marginFooter); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php new file mode 100644 index 0000000..7daf723 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php @@ -0,0 +1,32 @@ + '#NULL!', + 0x07 => '#DIV/0!', + 0x0F => '#VALUE!', + 0x17 => '#REF!', + 0x1D => '#NAME?', + 0x24 => '#NUM!', + 0x2A => '#N/A', + ]; + + /** + * Map error code, e.g. '#N/A'. + * + * @param int $code + * + * @return bool|string + */ + public static function lookup($code) + { + if (isset(self::$map[$code])) { + return self::$map[$code]; + } + + return false; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Escher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Escher.php new file mode 100644 index 0000000..e9c95c8 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Escher.php @@ -0,0 +1,624 @@ +object = $object; + } + + private const WHICH_ROUTINE = [ + self::DGGCONTAINER => 'readDggContainer', + self::DGG => 'readDgg', + self::BSTORECONTAINER => 'readBstoreContainer', + self::BSE => 'readBSE', + self::BLIPJPEG => 'readBlipJPEG', + self::BLIPPNG => 'readBlipPNG', + self::OPT => 'readOPT', + self::TERTIARYOPT => 'readTertiaryOPT', + self::SPLITMENUCOLORS => 'readSplitMenuColors', + self::DGCONTAINER => 'readDgContainer', + self::DG => 'readDg', + self::SPGRCONTAINER => 'readSpgrContainer', + self::SPCONTAINER => 'readSpContainer', + self::SPGR => 'readSpgr', + self::SP => 'readSp', + self::CLIENTTEXTBOX => 'readClientTextbox', + self::CLIENTANCHOR => 'readClientAnchor', + self::CLIENTDATA => 'readClientData', + ]; + + /** + * Load Escher stream data. May be a partial Escher stream. + * + * @param string $data + * + * @return BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer + */ + public function load($data) + { + $this->data = $data; + + // total byte size of Excel data (workbook global substream + sheet substreams) + $this->dataSize = strlen($this->data); + + $this->pos = 0; + + // Parse Escher stream + while ($this->pos < $this->dataSize) { + // offset: 2; size: 2: Record Type + $fbt = Xls::getUInt2d($this->data, $this->pos + 2); + $routine = self::WHICH_ROUTINE[$fbt] ?? 'readDefault'; + if (method_exists($this, $routine)) { + $this->$routine(); + } + } + + return $this->object; + } + + /** + * Read a generic record. + */ + private function readDefault(): void + { + // offset 0; size: 2; recVer and recInstance + //$verInstance = Xls::getUInt2d($this->data, $this->pos); + + // offset: 2; size: 2: Record Type + //$fbt = Xls::getUInt2d($this->data, $this->pos + 2); + + // bit: 0-3; mask: 0x000F; recVer + //$recVer = (0x000F & $verInstance) >> 0; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DggContainer record (Drawing Group Container). + */ + private function readDggContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dggContainer = new DggContainer(); + $this->applyAttribute('setDggContainer', $dggContainer); + $reader = new self($dggContainer); + $reader->load($recordData); + } + + /** + * Read Dgg record (Drawing Group). + */ + private function readDgg(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read BstoreContainer record (Blip Store Container). + */ + private function readBstoreContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $bstoreContainer = new BstoreContainer(); + $this->applyAttribute('setBstoreContainer', $bstoreContainer); + $reader = new self($bstoreContainer); + $reader->load($recordData); + } + + /** + * Read BSE record. + */ + private function readBSE(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // add BSE to BstoreContainer + $BSE = new BSE(); + $this->applyAttribute('addBSE', $BSE); + + $BSE->setBLIPType($recInstance); + + // offset: 0; size: 1; btWin32 (MSOBLIPTYPE) + //$btWin32 = ord($recordData[0]); + + // offset: 1; size: 1; btWin32 (MSOBLIPTYPE) + //$btMacOS = ord($recordData[1]); + + // offset: 2; size: 16; MD4 digest + //$rgbUid = substr($recordData, 2, 16); + + // offset: 18; size: 2; tag + //$tag = Xls::getUInt2d($recordData, 18); + + // offset: 20; size: 4; size of BLIP in bytes + //$size = Xls::getInt4d($recordData, 20); + + // offset: 24; size: 4; number of references to this BLIP + //$cRef = Xls::getInt4d($recordData, 24); + + // offset: 28; size: 4; MSOFO file offset + //$foDelay = Xls::getInt4d($recordData, 28); + + // offset: 32; size: 1; unused1 + //$unused1 = ord($recordData[32]); + + // offset: 33; size: 1; size of nameData in bytes (including null terminator) + $cbName = ord($recordData[33]); + + // offset: 34; size: 1; unused2 + //$unused2 = ord($recordData[34]); + + // offset: 35; size: 1; unused3 + //$unused3 = ord($recordData[35]); + + // offset: 36; size: $cbName; nameData + //$nameData = substr($recordData, 36, $cbName); + + // offset: 36 + $cbName, size: var; the BLIP data + $blipData = substr($recordData, 36 + $cbName); + + // record is a container, read contents + $reader = new self($BSE); + $reader->load($blipData); + } + + /** + * Read BlipJPEG record. Holds raw JPEG image data. + */ + private function readBlipJPEG(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + //$rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if (in_array($recInstance, [0x046B, 0x06E3])) { + //$rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + //$tag = ord($recordData[$pos]); + ++$pos; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->applyAttribute('setBlip', $blip); + } + + /** + * Read BlipPNG record. Holds raw PNG image data. + */ + private function readBlipPNG(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + //$rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if ($recInstance == 0x06E1) { + //$rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + //$tag = ord($recordData[$pos]); + ++$pos; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->applyAttribute('setBlip', $blip); + } + + /** + * Read OPT record. This record may occur within DggContainer record or SpContainer. + */ + private function readOPT(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $this->readOfficeArtRGFOPTE($recordData, $recInstance); + } + + /** + * Read TertiaryOPT record. + */ + private function readTertiaryOPT(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SplitMenuColors record. + */ + private function readSplitMenuColors(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DgContainer record (Drawing Container). + */ + private function readDgContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dgContainer = new DgContainer(); + $this->applyAttribute('setDgContainer', $dgContainer); + $reader = new self($dgContainer); + $reader->load($recordData); + } + + /** + * Read Dg record (Drawing). + */ + private function readDg(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SpgrContainer record (Shape Group Container). + */ + private function readSpgrContainer(): void + { + // context is either context DgContainer or SpgrContainer + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $spgrContainer = new SpgrContainer(); + + if ($this->object instanceof DgContainer) { + // DgContainer + $this->object->setSpgrContainer($spgrContainer); + } elseif ($this->object instanceof SpgrContainer) { + // SpgrContainer + $this->object->addChild($spgrContainer); + } + + $reader = new self($spgrContainer); + $reader->load($recordData); + } + + /** + * Read SpContainer record (Shape Container). + */ + private function readSpContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // add spContainer to spgrContainer + $spContainer = new SpContainer(); + $this->applyAttribute('addChild', $spContainer); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $reader = new self($spContainer); + $reader->load($recordData); + } + + /** + * Read Spgr record (Shape Group). + */ + private function readSpgr(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read Sp record (Shape). + */ + private function readSp(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientTextbox record. + */ + private function readClientTextbox(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet. + */ + private function readClientAnchor(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // offset: 2; size: 2; upper-left corner column index (0-based) + $c1 = Xls::getUInt2d($recordData, 2); + + // offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width + $startOffsetX = Xls::getUInt2d($recordData, 4); + + // offset: 6; size: 2; upper-left corner row index (0-based) + $r1 = Xls::getUInt2d($recordData, 6); + + // offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height + $startOffsetY = Xls::getUInt2d($recordData, 8); + + // offset: 10; size: 2; bottom-right corner column index (0-based) + $c2 = Xls::getUInt2d($recordData, 10); + + // offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width + $endOffsetX = Xls::getUInt2d($recordData, 12); + + // offset: 14; size: 2; bottom-right corner row index (0-based) + $r2 = Xls::getUInt2d($recordData, 14); + + // offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height + $endOffsetY = Xls::getUInt2d($recordData, 16); + + $this->applyAttribute('setStartCoordinates', Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1)); + $this->applyAttribute('setStartOffsetX', $startOffsetX); + $this->applyAttribute('setStartOffsetY', $startOffsetY); + $this->applyAttribute('setEndCoordinates', Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1)); + $this->applyAttribute('setEndOffsetX', $endOffsetX); + $this->applyAttribute('setEndOffsetY', $endOffsetY); + } + + /** + * @param mixed $value + */ + private function applyAttribute(string $name, $value): void + { + if (method_exists($this->object, $name)) { + $this->object->$name($value); + } + } + + /** + * Read ClientData record. + */ + private function readClientData(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read OfficeArtRGFOPTE table of property-value pairs. + * + * @param string $data Binary data + * @param int $n Number of properties + */ + private function readOfficeArtRGFOPTE($data, $n): void + { + $splicedComplexData = substr($data, 6 * $n); + + // loop through property-value pairs + for ($i = 0; $i < $n; ++$i) { + // read 6 bytes at a time + $fopte = substr($data, 6 * $i, 6); + + // offset: 0; size: 2; opid + $opid = Xls::getUInt2d($fopte, 0); + + // bit: 0-13; mask: 0x3FFF; opid.opid + $opidOpid = (0x3FFF & $opid) >> 0; + + // bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier + //$opidFBid = (0x4000 & $opid) >> 14; + + // bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data + $opidFComplex = (0x8000 & $opid) >> 15; + + // offset: 2; size: 4; the value for this property + $op = Xls::getInt4d($fopte, 2); + + if ($opidFComplex) { + $complexData = substr($splicedComplexData, 0, $op); + $splicedComplexData = substr($splicedComplexData, $op); + + // we store string value with complex data + $value = $complexData; + } else { + // we store integer value + $value = $op; + } + + if (method_exists($this->object, 'setOPT')) { + $this->object->setOPT($opidOpid, $value); + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/MD5.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/MD5.php new file mode 100644 index 0000000..3e15f64 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/MD5.php @@ -0,0 +1,197 @@ +reset(); + } + + /** + * Reset the MD5 stream context. + */ + public function reset(): void + { + $this->a = 0x67452301; + $this->b = 0xEFCDAB89; + $this->c = 0x98BADCFE; + $this->d = 0x10325476; + } + + /** + * Get MD5 stream context. + * + * @return string + */ + public function getContext() + { + $s = ''; + foreach (['a', 'b', 'c', 'd'] as $i) { + $v = $this->{$i}; + $s .= chr($v & 0xff); + $s .= chr(($v >> 8) & 0xff); + $s .= chr(($v >> 16) & 0xff); + $s .= chr(($v >> 24) & 0xff); + } + + return $s; + } + + /** + * Add data to context. + * + * @param string $data Data to add + */ + public function add(string $data): void + { + $words = array_values(unpack('V16', $data)); + + $A = $this->a; + $B = $this->b; + $C = $this->c; + $D = $this->d; + + $F = ['self', 'f']; + $G = ['self', 'g']; + $H = ['self', 'h']; + $I = ['self', 'i']; + + // ROUND 1 + self::step($F, $A, $B, $C, $D, $words[0], 7, 0xd76aa478); + self::step($F, $D, $A, $B, $C, $words[1], 12, 0xe8c7b756); + self::step($F, $C, $D, $A, $B, $words[2], 17, 0x242070db); + self::step($F, $B, $C, $D, $A, $words[3], 22, 0xc1bdceee); + self::step($F, $A, $B, $C, $D, $words[4], 7, 0xf57c0faf); + self::step($F, $D, $A, $B, $C, $words[5], 12, 0x4787c62a); + self::step($F, $C, $D, $A, $B, $words[6], 17, 0xa8304613); + self::step($F, $B, $C, $D, $A, $words[7], 22, 0xfd469501); + self::step($F, $A, $B, $C, $D, $words[8], 7, 0x698098d8); + self::step($F, $D, $A, $B, $C, $words[9], 12, 0x8b44f7af); + self::step($F, $C, $D, $A, $B, $words[10], 17, 0xffff5bb1); + self::step($F, $B, $C, $D, $A, $words[11], 22, 0x895cd7be); + self::step($F, $A, $B, $C, $D, $words[12], 7, 0x6b901122); + self::step($F, $D, $A, $B, $C, $words[13], 12, 0xfd987193); + self::step($F, $C, $D, $A, $B, $words[14], 17, 0xa679438e); + self::step($F, $B, $C, $D, $A, $words[15], 22, 0x49b40821); + + // ROUND 2 + self::step($G, $A, $B, $C, $D, $words[1], 5, 0xf61e2562); + self::step($G, $D, $A, $B, $C, $words[6], 9, 0xc040b340); + self::step($G, $C, $D, $A, $B, $words[11], 14, 0x265e5a51); + self::step($G, $B, $C, $D, $A, $words[0], 20, 0xe9b6c7aa); + self::step($G, $A, $B, $C, $D, $words[5], 5, 0xd62f105d); + self::step($G, $D, $A, $B, $C, $words[10], 9, 0x02441453); + self::step($G, $C, $D, $A, $B, $words[15], 14, 0xd8a1e681); + self::step($G, $B, $C, $D, $A, $words[4], 20, 0xe7d3fbc8); + self::step($G, $A, $B, $C, $D, $words[9], 5, 0x21e1cde6); + self::step($G, $D, $A, $B, $C, $words[14], 9, 0xc33707d6); + self::step($G, $C, $D, $A, $B, $words[3], 14, 0xf4d50d87); + self::step($G, $B, $C, $D, $A, $words[8], 20, 0x455a14ed); + self::step($G, $A, $B, $C, $D, $words[13], 5, 0xa9e3e905); + self::step($G, $D, $A, $B, $C, $words[2], 9, 0xfcefa3f8); + self::step($G, $C, $D, $A, $B, $words[7], 14, 0x676f02d9); + self::step($G, $B, $C, $D, $A, $words[12], 20, 0x8d2a4c8a); + + // ROUND 3 + self::step($H, $A, $B, $C, $D, $words[5], 4, 0xfffa3942); + self::step($H, $D, $A, $B, $C, $words[8], 11, 0x8771f681); + self::step($H, $C, $D, $A, $B, $words[11], 16, 0x6d9d6122); + self::step($H, $B, $C, $D, $A, $words[14], 23, 0xfde5380c); + self::step($H, $A, $B, $C, $D, $words[1], 4, 0xa4beea44); + self::step($H, $D, $A, $B, $C, $words[4], 11, 0x4bdecfa9); + self::step($H, $C, $D, $A, $B, $words[7], 16, 0xf6bb4b60); + self::step($H, $B, $C, $D, $A, $words[10], 23, 0xbebfbc70); + self::step($H, $A, $B, $C, $D, $words[13], 4, 0x289b7ec6); + self::step($H, $D, $A, $B, $C, $words[0], 11, 0xeaa127fa); + self::step($H, $C, $D, $A, $B, $words[3], 16, 0xd4ef3085); + self::step($H, $B, $C, $D, $A, $words[6], 23, 0x04881d05); + self::step($H, $A, $B, $C, $D, $words[9], 4, 0xd9d4d039); + self::step($H, $D, $A, $B, $C, $words[12], 11, 0xe6db99e5); + self::step($H, $C, $D, $A, $B, $words[15], 16, 0x1fa27cf8); + self::step($H, $B, $C, $D, $A, $words[2], 23, 0xc4ac5665); + + // ROUND 4 + self::step($I, $A, $B, $C, $D, $words[0], 6, 0xf4292244); + self::step($I, $D, $A, $B, $C, $words[7], 10, 0x432aff97); + self::step($I, $C, $D, $A, $B, $words[14], 15, 0xab9423a7); + self::step($I, $B, $C, $D, $A, $words[5], 21, 0xfc93a039); + self::step($I, $A, $B, $C, $D, $words[12], 6, 0x655b59c3); + self::step($I, $D, $A, $B, $C, $words[3], 10, 0x8f0ccc92); + self::step($I, $C, $D, $A, $B, $words[10], 15, 0xffeff47d); + self::step($I, $B, $C, $D, $A, $words[1], 21, 0x85845dd1); + self::step($I, $A, $B, $C, $D, $words[8], 6, 0x6fa87e4f); + self::step($I, $D, $A, $B, $C, $words[15], 10, 0xfe2ce6e0); + self::step($I, $C, $D, $A, $B, $words[6], 15, 0xa3014314); + self::step($I, $B, $C, $D, $A, $words[13], 21, 0x4e0811a1); + self::step($I, $A, $B, $C, $D, $words[4], 6, 0xf7537e82); + self::step($I, $D, $A, $B, $C, $words[11], 10, 0xbd3af235); + self::step($I, $C, $D, $A, $B, $words[2], 15, 0x2ad7d2bb); + self::step($I, $B, $C, $D, $A, $words[9], 21, 0xeb86d391); + + $this->a = ($this->a + $A) & 0xffffffff; + $this->b = ($this->b + $B) & 0xffffffff; + $this->c = ($this->c + $C) & 0xffffffff; + $this->d = ($this->d + $D) & 0xffffffff; + } + + private static function f(int $X, int $Y, int $Z) + { + return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z + } + + private static function g(int $X, int $Y, int $Z) + { + return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z + } + + private static function h(int $X, int $Y, int $Z) + { + return $X ^ $Y ^ $Z; // X XOR Y XOR Z + } + + private static function i(int $X, int $Y, int $Z) + { + return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z) + } + + private static function step($func, int &$A, int $B, int $C, int $D, int $M, int $s, int $t): void + { + $A = ($A + call_user_func($func, $B, $C, $D) + $M + $t) & 0xffffffff; + $A = self::rotate($A, $s); + $A = ($B + $A) & 0xffffffff; + } + + private static function rotate(int $decimal, int $bits) + { + $binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT); + + return bindec(substr($binary, $bits) . substr($binary, 0, $bits)); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php new file mode 100644 index 0000000..32ab5c8 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php @@ -0,0 +1,50 @@ + + */ + protected static $fillPatternMap = [ + 0x00 => Fill::FILL_NONE, + 0x01 => Fill::FILL_SOLID, + 0x02 => Fill::FILL_PATTERN_MEDIUMGRAY, + 0x03 => Fill::FILL_PATTERN_DARKGRAY, + 0x04 => Fill::FILL_PATTERN_LIGHTGRAY, + 0x05 => Fill::FILL_PATTERN_DARKHORIZONTAL, + 0x06 => Fill::FILL_PATTERN_DARKVERTICAL, + 0x07 => Fill::FILL_PATTERN_DARKDOWN, + 0x08 => Fill::FILL_PATTERN_DARKUP, + 0x09 => Fill::FILL_PATTERN_DARKGRID, + 0x0A => Fill::FILL_PATTERN_DARKTRELLIS, + 0x0B => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + 0x0C => Fill::FILL_PATTERN_LIGHTVERTICAL, + 0x0D => Fill::FILL_PATTERN_LIGHTDOWN, + 0x0E => Fill::FILL_PATTERN_LIGHTUP, + 0x0F => Fill::FILL_PATTERN_LIGHTGRID, + 0x10 => Fill::FILL_PATTERN_LIGHTTRELLIS, + 0x11 => Fill::FILL_PATTERN_GRAY125, + 0x12 => Fill::FILL_PATTERN_GRAY0625, + ]; + + /** + * Get fill pattern from index + * OpenOffice documentation: 2.5.12. + * + * @param int $index + * + * @return string + */ + public static function lookup($index) + { + if (isset(self::$fillPatternMap[$index])) { + return self::$fillPatternMap[$index]; + } + + return Fill::FILL_NONE; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php new file mode 100644 index 0000000..8488499 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -0,0 +1,64 @@ +worksheet = $workSheet; + } + + public function readHyperlinks(SimpleXMLElement $relsWorksheet): void + { + foreach ($relsWorksheet->children(Namespaces::RELATIONSHIPS)->Relationship as $elementx) { + $element = Xlsx::getAttributes($elementx); + if ($element->Type == Namespaces::HYPERLINK) { + $this->hyperlinks[(string) $element->Id] = (string) $element->Target; + } + } + } + + public function setHyperlinks(SimpleXMLElement $worksheetXml): void + { + foreach ($worksheetXml->children(Namespaces::MAIN)->hyperlink as $hyperlink) { + if ($hyperlink !== null) { + $this->setHyperlink($hyperlink, $this->worksheet); + } + } + } + + private function setHyperlink(SimpleXMLElement $hyperlink, Worksheet $worksheet): void + { + // Link url + $linkRel = Xlsx::getAttributes($hyperlink, Namespaces::SCHEMA_OFFICE_DOCUMENT); + + $attributes = Xlsx::getAttributes($hyperlink); + foreach (Coordinate::extractAllCellReferencesInRange($attributes->ref) as $cellReference) { + $cell = $worksheet->getCell($cellReference); + if (isset($linkRel['id'])) { + $hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? null; + if (isset($attributes['location'])) { + $hyperlinkUrl .= '#' . (string) $attributes['location']; + } + $cell->getHyperlink()->setUrl($hyperlinkUrl); + } elseif (isset($attributes['location'])) { + $cell->getHyperlink()->setUrl('sheet://' . (string) $attributes['location']); + } + + // Tooltip + if (isset($attributes['tooltip'])) { + $cell->getHyperlink()->setTooltip((string) $attributes['tooltip']); + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php new file mode 100644 index 0000000..c0713ae --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -0,0 +1,78 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + } + + public function load(array $unparsedLoadedData) + { + if (!$this->worksheetXml) { + return $unparsedLoadedData; + } + + $this->margins($this->worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($this->worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($this->worksheetXml, $this->worksheet); + $this->pageBreaks($this->worksheetXml, $this->worksheet); + + return $unparsedLoadedData; + } + + private function margins(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->pageMargins) { + $docPageMargins = $worksheet->getPageMargins(); + $docPageMargins->setLeft((float) ($xmlSheet->pageMargins['left'])); + $docPageMargins->setRight((float) ($xmlSheet->pageMargins['right'])); + $docPageMargins->setTop((float) ($xmlSheet->pageMargins['top'])); + $docPageMargins->setBottom((float) ($xmlSheet->pageMargins['bottom'])); + $docPageMargins->setHeader((float) ($xmlSheet->pageMargins['header'])); + $docPageMargins->setFooter((float) ($xmlSheet->pageMargins['footer'])); + } + } + + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData) + { + if ($xmlSheet->pageSetup) { + $docPageSetup = $worksheet->getPageSetup(); + + if (isset($xmlSheet->pageSetup['orientation'])) { + $docPageSetup->setOrientation((string) $xmlSheet->pageSetup['orientation']); + } + if (isset($xmlSheet->pageSetup['paperSize'])) { + $docPageSetup->setPaperSize((int) ($xmlSheet->pageSetup['paperSize'])); + } + if (isset($xmlSheet->pageSetup['scale'])) { + $docPageSetup->setScale((int) ($xmlSheet->pageSetup['scale']), false); + } + if (isset($xmlSheet->pageSetup['fitToHeight']) && (int) ($xmlSheet->pageSetup['fitToHeight']) >= 0) { + $docPageSetup->setFitToHeight((int) ($xmlSheet->pageSetup['fitToHeight']), false); + } + if (isset($xmlSheet->pageSetup['fitToWidth']) && (int) ($xmlSheet->pageSetup['fitToWidth']) >= 0) { + $docPageSetup->setFitToWidth((int) ($xmlSheet->pageSetup['fitToWidth']), false); + } + if ( + isset($xmlSheet->pageSetup['firstPageNumber'], $xmlSheet->pageSetup['useFirstPageNumber']) && + self::boolean((string) $xmlSheet->pageSetup['useFirstPageNumber']) + ) { + $docPageSetup->setFirstPageNumber((int) ($xmlSheet->pageSetup['firstPageNumber'])); + } + if (isset($xmlSheet->pageSetup['pageOrder'])) { + $docPageSetup->setPageOrder((string) $xmlSheet->pageSetup['pageOrder']); + } + + $relAttributes = $xmlSheet->pageSetup->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT); + if (isset($relAttributes['id'])) { + $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'] = (string) $relAttributes['id']; + } + } + + return $unparsedLoadedData; + } + + private function headerFooter(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->headerFooter) { + $docHeaderFooter = $worksheet->getHeaderFooter(); + + if ( + isset($xmlSheet->headerFooter['differentOddEven']) && + self::boolean((string) $xmlSheet->headerFooter['differentOddEven']) + ) { + $docHeaderFooter->setDifferentOddEven(true); + } else { + $docHeaderFooter->setDifferentOddEven(false); + } + if ( + isset($xmlSheet->headerFooter['differentFirst']) && + self::boolean((string) $xmlSheet->headerFooter['differentFirst']) + ) { + $docHeaderFooter->setDifferentFirst(true); + } else { + $docHeaderFooter->setDifferentFirst(false); + } + if ( + isset($xmlSheet->headerFooter['scaleWithDoc']) && + !self::boolean((string) $xmlSheet->headerFooter['scaleWithDoc']) + ) { + $docHeaderFooter->setScaleWithDocument(false); + } else { + $docHeaderFooter->setScaleWithDocument(true); + } + if ( + isset($xmlSheet->headerFooter['alignWithMargins']) && + !self::boolean((string) $xmlSheet->headerFooter['alignWithMargins']) + ) { + $docHeaderFooter->setAlignWithMargins(false); + } else { + $docHeaderFooter->setAlignWithMargins(true); + } + + $docHeaderFooter->setOddHeader((string) $xmlSheet->headerFooter->oddHeader); + $docHeaderFooter->setOddFooter((string) $xmlSheet->headerFooter->oddFooter); + $docHeaderFooter->setEvenHeader((string) $xmlSheet->headerFooter->evenHeader); + $docHeaderFooter->setEvenFooter((string) $xmlSheet->headerFooter->evenFooter); + $docHeaderFooter->setFirstHeader((string) $xmlSheet->headerFooter->firstHeader); + $docHeaderFooter->setFirstFooter((string) $xmlSheet->headerFooter->firstFooter); + } + } + + private function pageBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->rowBreaks && $xmlSheet->rowBreaks->brk) { + $this->rowBreaks($xmlSheet, $worksheet); + } + if ($xmlSheet->colBreaks && $xmlSheet->colBreaks->brk) { + $this->columnBreaks($xmlSheet, $worksheet); + } + } + + private function rowBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + foreach ($xmlSheet->rowBreaks->brk as $brk) { + if ($brk['man']) { + $worksheet->setBreak("A{$brk['id']}", Worksheet::BREAK_ROW); + } + } + } + + private function columnBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + foreach ($xmlSheet->colBreaks->brk as $brk) { + if ($brk['man']) { + $worksheet->setBreak( + Coordinate::stringFromColumnIndex(((int) $brk['id']) + 1) . '1', + Worksheet::BREAK_COLUMN + ); + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/PageSettings.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/PageSettings.php new file mode 100644 index 0000000..c0caca3 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/PageSettings.php @@ -0,0 +1,134 @@ +pageSetup($xmlX, $namespaces, $this->getPrintDefaults()); + $this->printSettings = $this->printSetup($xmlX, $printSettings); + } + + public function loadPageSettings(Spreadsheet $spreadsheet): void + { + $spreadsheet->getActiveSheet()->getPageSetup() + ->setPaperSize($this->printSettings->paperSize) + ->setOrientation($this->printSettings->orientation) + ->setScale($this->printSettings->scale) + ->setVerticalCentered($this->printSettings->verticalCentered) + ->setHorizontalCentered($this->printSettings->horizontalCentered) + ->setPageOrder($this->printSettings->printOrder); + $spreadsheet->getActiveSheet()->getPageMargins() + ->setTop($this->printSettings->topMargin) + ->setHeader($this->printSettings->headerMargin) + ->setLeft($this->printSettings->leftMargin) + ->setRight($this->printSettings->rightMargin) + ->setBottom($this->printSettings->bottomMargin) + ->setFooter($this->printSettings->footerMargin); + } + + private function getPrintDefaults(): stdClass + { + return (object) [ + 'paperSize' => 9, + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'printOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + 'topMargin' => 0.75, + 'headerMargin' => 0.3, + 'leftMargin' => 0.7, + 'rightMargin' => 0.7, + 'bottomMargin' => 0.75, + 'footerMargin' => 0.3, + ]; + } + + private function pageSetup(SimpleXMLElement $xmlX, array $namespaces, stdClass $printDefaults): stdClass + { + if (isset($xmlX->WorksheetOptions->PageSetup)) { + foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) { + foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) { + $pageSetupAttributes = $pageSetupValue->attributes($namespaces['x']); + if (!$pageSetupAttributes) { + continue; + } + + switch ($pageSetupKey) { + case 'Layout': + $this->setLayout($printDefaults, $pageSetupAttributes); + + break; + case 'Header': + $printDefaults->headerMargin = (float) $pageSetupAttributes->Margin ?: 1.0; + + break; + case 'Footer': + $printDefaults->footerMargin = (float) $pageSetupAttributes->Margin ?: 1.0; + + break; + case 'PageMargins': + $this->setMargins($printDefaults, $pageSetupAttributes); + + break; + } + } + } + } + + return $printDefaults; + } + + private function printSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass + { + if (isset($xmlX->WorksheetOptions->Print)) { + foreach ($xmlX->WorksheetOptions->Print as $printData) { + foreach ($printData as $printKey => $printValue) { + switch ($printKey) { + case 'LeftToRight': + $printDefaults->printOrder = PageSetup::PAGEORDER_OVER_THEN_DOWN; + + break; + case 'PaperSizeIndex': + $printDefaults->paperSize = (int) $printValue ?: 9; + + break; + case 'Scale': + $printDefaults->scale = (int) $printValue ?: 100; + + break; + } + } + } + } + + return $printDefaults; + } + + private function setLayout(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void + { + $printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation ?? '') ?: PageSetup::ORIENTATION_PORTRAIT; + $printDefaults->horizontalCentered = (bool) $pageSetupAttributes->CenterHorizontal ?: false; + $printDefaults->verticalCentered = (bool) $pageSetupAttributes->CenterVertical ?: false; + } + + private function setMargins(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void + { + $printDefaults->leftMargin = (float) $pageSetupAttributes->Left ?: 1.0; + $printDefaults->rightMargin = (float) $pageSetupAttributes->Right ?: 1.0; + $printDefaults->topMargin = (float) $pageSetupAttributes->Top ?: 1.0; + $printDefaults->bottomMargin = (float) $pageSetupAttributes->Bottom ?: 1.0; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php new file mode 100644 index 0000000..9a61215 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php @@ -0,0 +1,63 @@ + [ + 'solid' => FillStyles::FILL_SOLID, + 'gray75' => FillStyles::FILL_PATTERN_DARKGRAY, + 'gray50' => FillStyles::FILL_PATTERN_MEDIUMGRAY, + 'gray25' => FillStyles::FILL_PATTERN_LIGHTGRAY, + 'gray125' => FillStyles::FILL_PATTERN_GRAY125, + 'gray0625' => FillStyles::FILL_PATTERN_GRAY0625, + 'horzstripe' => FillStyles::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + 'vertstripe' => FillStyles::FILL_PATTERN_DARKVERTICAL, // vertical stripe + 'reversediagstripe' => FillStyles::FILL_PATTERN_DARKUP, // reverse diagonal stripe + 'diagstripe' => FillStyles::FILL_PATTERN_DARKDOWN, // diagonal stripe + 'diagcross' => FillStyles::FILL_PATTERN_DARKGRID, // diagoanl crosshatch + 'thickdiagcross' => FillStyles::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + 'thinhorzstripe' => FillStyles::FILL_PATTERN_LIGHTHORIZONTAL, + 'thinvertstripe' => FillStyles::FILL_PATTERN_LIGHTVERTICAL, + 'thinreversediagstripe' => FillStyles::FILL_PATTERN_LIGHTUP, + 'thindiagstripe' => FillStyles::FILL_PATTERN_LIGHTDOWN, + 'thinhorzcross' => FillStyles::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + 'thindiagcross' => FillStyles::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + ]; + + public function parseStyle(SimpleXMLElement $styleAttributes): array + { + $style = []; + + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) { + $styleAttributeValue = (string) $styleAttributeValuex; + switch ($styleAttributeKey) { + case 'Color': + $style['fill']['endColor']['rgb'] = substr($styleAttributeValue, 1); + $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'PatternColor': + $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Pattern': + $lcStyleAttributeValue = strtolower((string) $styleAttributeValue); + $style['fill']['fillType'] + = self::FILL_MAPPINGS['fillType'][$lcStyleAttributeValue] ?? FillStyles::FILL_NONE; + + break; + } + } + + return $style; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Font.php new file mode 100644 index 0000000..16ab44d --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/Font.php @@ -0,0 +1,79 @@ + $styleAttributeValue) { + $styleAttributeValue = (string) $styleAttributeValue; + switch ($styleAttributeKey) { + case 'FontName': + $style['font']['name'] = $styleAttributeValue; + + break; + case 'Size': + $style['font']['size'] = $styleAttributeValue; + + break; + case 'Color': + $style['font']['color']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Bold': + $style['font']['bold'] = true; + + break; + case 'Italic': + $style['font']['italic'] = true; + + break; + case 'Underline': + $style = $this->parseUnderline($style, $styleAttributeValue); + + break; + case 'VerticalAlign': + $style = $this->parseVerticalAlign($style, $styleAttributeValue); + + break; + } + } + + return $style; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php new file mode 100644 index 0000000..a31aa9e --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php @@ -0,0 +1,33 @@ + $styleAttributeValue) { + $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue); + + switch ($styleAttributeValue) { + case 'Short Date': + $styleAttributeValue = 'dd/mm/yyyy'; + + break; + } + + if ($styleAttributeValue > '') { + $style['numberFormat']['formatCode'] = $styleAttributeValue; + } + } + + return $style; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/ITextElement.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/ITextElement.php new file mode 100644 index 0000000..39b70c8 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/ITextElement.php @@ -0,0 +1,36 @@ +getName(); + $size = $defaultFont->getSize(); + + if (isset(Font::$defaultColumnWidths[$name][$size])) { + // Exact width can be determined + return $pixelValue * Font::$defaultColumnWidths[$name][$size]['width'] + / Font::$defaultColumnWidths[$name][$size]['px']; + } + + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + return $pixelValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width'] + / Font::$defaultColumnWidths['Calibri'][11]['px'] / $size; + } + + /** + * Convert column width from (intrinsic) Excel units to pixels. + * + * @param float $cellWidth Value in cell dimension + * @param \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Default font of the workbook + * + * @return int Value in pixels + */ + public static function cellDimensionToPixels($cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont) + { + // Font name and size + $name = $defaultFont->getName(); + $size = $defaultFont->getSize(); + + if (isset(Font::$defaultColumnWidths[$name][$size])) { + // Exact width can be determined + $colWidth = $cellWidth * Font::$defaultColumnWidths[$name][$size]['px'] + / Font::$defaultColumnWidths[$name][$size]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $colWidth = $cellWidth * $size * Font::$defaultColumnWidths['Calibri'][11]['px'] + / Font::$defaultColumnWidths['Calibri'][11]['width'] / 11; + } + + // Round pixels to closest integer + $colWidth = (int) round($colWidth); + + return $colWidth; + } + + /** + * Convert pixels to points. + * + * @param int $pixelValue Value in pixels + * + * @return float Value in points + */ + public static function pixelsToPoints($pixelValue) + { + return $pixelValue * 0.75; + } + + /** + * Convert points to pixels. + * + * @param int $pointValue Value in points + * + * @return int Value in pixels + */ + public static function pointsToPixels($pointValue) + { + if ($pointValue != 0) { + return (int) ceil($pointValue / 0.75); + } + + return 0; + } + + /** + * Convert degrees to angle. + * + * @param int $degrees Degrees + * + * @return int Angle + */ + public static function degreesToAngle($degrees) + { + return (int) round($degrees * 60000); + } + + /** + * Convert angle to degrees. + * + * @param int|SimpleXMLElement $angle Angle + * + * @return int Degrees + */ + public static function angleToDegrees($angle) + { + $angle = (int) $angle; + if ($angle != 0) { + return (int) round($angle / 60000); + } + + return 0; + } + + /** + * Create a new image from file. By alexander at alexauto dot nl. + * + * @see http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214 + * + * @param string $bmpFilename Path to Windows DIB (BMP) image + * + * @return GdImage|resource + */ + public static function imagecreatefrombmp($bmpFilename) + { + // Load the image into a string + $file = fopen($bmpFilename, 'rb'); + $read = fread($file, 10); + while (!feof($file) && ($read != '')) { + $read .= fread($file, 1024); + } + + $temp = unpack('H*', $read); + $hex = $temp[1]; + $header = substr($hex, 0, 108); + + // Process the header + // Structure: http://www.fastgraph.com/help/bmp_header_format.html + $width = 0; + $height = 0; + if (substr($header, 0, 4) == '424d') { + // Cut it in parts of 2 bytes + $header_parts = str_split($header, 2); + + // Get the width 4 bytes + $width = hexdec($header_parts[19] . $header_parts[18]); + + // Get the height 4 bytes + $height = hexdec($header_parts[23] . $header_parts[22]); + + // Unset the header params + unset($header_parts); + } + + // Define starting X and Y + $x = 0; + $y = 1; + + // Create newimage + $image = imagecreatetruecolor($width, $height); + + // Grab the body from the image + $body = substr($hex, 108); + + // Calculate if padding at the end-line is needed + // Divided by two to keep overview. + // 1 byte = 2 HEX-chars + $body_size = (strlen($body) / 2); + $header_size = ($width * $height); + + // Use end-line padding? Only when needed + $usePadding = ($body_size > ($header_size * 3) + 4); + + // Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption + // Calculate the next DWORD-position in the body + for ($i = 0; $i < $body_size; $i += 3) { + // Calculate line-ending and padding + if ($x >= $width) { + // If padding needed, ignore image-padding + // Shift i to the ending of the current 32-bit-block + if ($usePadding) { + $i += $width % 4; + } + + // Reset horizontal position + $x = 0; + + // Raise the height-position (bottom-up) + ++$y; + + // Reached the image-height? Break the for-loop + if ($y > $height) { + break; + } + } + + // Calculation of the RGB-pixel (defined as BGR in image-data) + // Define $i_pos as absolute position in the body + $i_pos = $i * 2; + $r = hexdec($body[$i_pos + 4] . $body[$i_pos + 5]); + $g = hexdec($body[$i_pos + 2] . $body[$i_pos + 3]); + $b = hexdec($body[$i_pos] . $body[$i_pos + 1]); + + // Calculate and draw the pixel + $color = imagecolorallocate($image, $r, $g, $b); + imagesetpixel($image, $x, $height - $y, $color); + + // Raise the horizontal position + ++$x; + } + + // Unset the body / free the memory + unset($body); + + // Return image-object + return $image; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php new file mode 100644 index 0000000..c6d0a6f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php @@ -0,0 +1,64 @@ +dggContainer; + } + + /** + * Set Drawing Group Container. + * + * @param Escher\DggContainer $dggContainer + * + * @return Escher\DggContainer + */ + public function setDggContainer($dggContainer) + { + return $this->dggContainer = $dggContainer; + } + + /** + * Get Drawing Container. + * + * @return Escher\DgContainer + */ + public function getDgContainer() + { + return $this->dgContainer; + } + + /** + * Set Drawing Container. + * + * @param Escher\DgContainer $dgContainer + * + * @return Escher\DgContainer + */ + public function setDgContainer($dgContainer) + { + return $this->dgContainer = $dgContainer; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php new file mode 100644 index 0000000..f2fe8ca --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php @@ -0,0 +1,185 @@ +open($zipFile); + if ($res === true) { + $returnValue = ($zip->getFromName($archiveFile) !== false); + $zip->close(); + + return $returnValue; + } + } + + return false; + } + + return file_exists($filename); + } + + /** + * Returns canonicalized absolute pathname, also for ZIP archives. + */ + public static function realpath(string $filename): string + { + // Returnvalue + $returnValue = ''; + + // Try using realpath() + if (file_exists($filename)) { + $returnValue = realpath($filename) ?: ''; + } + + // Found something? + if ($returnValue === '') { + $pathArray = explode('/', $filename); + while (in_array('..', $pathArray) && $pathArray[0] != '..') { + $iMax = count($pathArray); + for ($i = 0; $i < $iMax; ++$i) { + if ($pathArray[$i] == '..' && $i > 0) { + unset($pathArray[$i], $pathArray[$i - 1]); + + break; + } + } + } + $returnValue = implode('/', $pathArray); + } + + // Return + return $returnValue; + } + + /** + * Get the systems temporary directory. + */ + public static function sysGetTempDir(): string + { + $path = sys_get_temp_dir(); + if (self::$useUploadTempDirectory) { + // use upload-directory when defined to allow running on environments having very restricted + // open_basedir configs + if (ini_get('upload_tmp_dir') !== false) { + if ($temp = ini_get('upload_tmp_dir')) { + if (file_exists($temp)) { + $path = $temp; + } + } + } + } + + return realpath($path) ?: ''; + } + + public static function temporaryFilename(): string + { + $filename = tempnam(self::sysGetTempDir(), 'phpspreadsheet'); + if ($filename === false) { + throw new Exception('Could not create temporary file'); + } + + return $filename; + } + + /** + * Assert that given path is an existing file and is readable, otherwise throw exception. + */ + public static function assertFile(string $filename, string $zipMember = ''): void + { + if (!is_file($filename)) { + throw new ReaderException('File "' . $filename . '" does not exist.'); + } + + if (!is_readable($filename)) { + throw new ReaderException('Could not open "' . $filename . '" for reading.'); + } + + if ($zipMember !== '') { + $zipfile = "zip://$filename#$zipMember"; + if (!self::fileExists($zipfile)) { + throw new ReaderException("Could not find zip member $zipfile"); + } + } + } + + /** + * Same as assertFile, except return true/false and don't throw Exception. + */ + public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool + { + if (!is_file($filename)) { + return false; + } + if (!is_readable($filename)) { + return false; + } + if ($zipMember === null) { + return true; + } + // validate zip, but don't check specific member + if ($zipMember === '') { + return self::validateZipFirst4($filename); + } + + return self::fileExists("zip://$filename#$zipMember"); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php new file mode 100644 index 0000000..9a74bef --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php @@ -0,0 +1,758 @@ + [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 56, 'width' => 9.33203125], + 9 => ['px' => 64, 'width' => 9.14062500], + 10 => ['px' => 64, 'width' => 9.14062500], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 56, 'width' => 9.33203125], + 9 => ['px' => 56, 'width' => 9.33203125], + 10 => ['px' => 64, 'width' => 9.14062500], + 11 => ['px' => 64, 'width' => 9.14062500], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000], + 2 => ['px' => 24, 'width' => 12.00000000], + 3 => ['px' => 32, 'width' => 10.66406250], + 4 => ['px' => 32, 'width' => 10.66406250], + 5 => ['px' => 40, 'width' => 10.00000000], + 6 => ['px' => 48, 'width' => 9.59765625], + 7 => ['px' => 48, 'width' => 9.59765625], + 8 => ['px' => 64, 'width' => 9.14062500], + 9 => ['px' => 72, 'width' => 9.00000000], + 10 => ['px' => 72, 'width' => 9.00000000], + ], + ]; + + /** + * Set autoSize method. + * + * @param string $method see self::AUTOSIZE_METHOD_* + * + * @return bool Success or failure + */ + public static function setAutoSizeMethod($method) + { + if (!in_array($method, self::$autoSizeMethods)) { + return false; + } + self::$autoSizeMethod = $method; + + return true; + } + + /** + * Get autoSize method. + * + * @return string + */ + public static function getAutoSizeMethod() + { + return self::$autoSizeMethod; + } + + /** + * Set the path to the folder containing .ttf files. There should be a trailing slash. + * Typical locations on variout some platforms: + * . + * + * @param string $folderPath + */ + public static function setTrueTypeFontPath($folderPath): void + { + self::$trueTypeFontPath = $folderPath; + } + + /** + * Get the path to the folder containing .ttf files. + * + * @return string + */ + public static function getTrueTypeFontPath() + { + return self::$trueTypeFontPath; + } + + /** + * Calculate an (approximate) OpenXML column width, based on font size and text contained. + * + * @param FontStyle $font Font object + * @param RichText|string $cellText Text to calculate width + * @param int $rotation Rotation angle + * @param null|FontStyle $defaultFont Font object + * + * @return int Column width + */ + public static function calculateColumnWidth(FontStyle $font, $cellText = '', $rotation = 0, ?FontStyle $defaultFont = null) + { + // If it is rich text, use plain text + if ($cellText instanceof RichText) { + $cellText = $cellText->getPlainText(); + } + + // Special case if there are one or more newline characters ("\n") + if (strpos($cellText ?? '', "\n") !== false) { + $lineTexts = explode("\n", $cellText); + $lineWidths = []; + foreach ($lineTexts as $lineText) { + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont); + } + + return max($lineWidths); // width of longest line in cell + } + + // Try to get the exact text width in pixels + $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; + $columnWidth = 0; + if (!$approximate) { + $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); + + try { + // Width of text in pixels excl. padding + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsExact($cellText, $font, $rotation) + $columnWidthAdjust; + } catch (PhpSpreadsheetException $e) { + $approximate = true; + } + } + + if ($approximate) { + $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0); + // Width of text in pixels excl. padding, approximation + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; + } + + // Convert from pixel width to column width + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); + + // Return + return (int) round($columnWidth, 6); + } + + /** + * Get GD text width in pixels for a string of text in a certain font at a certain rotation angle. + */ + public static function getTextWidthPixelsExact(string $text, FontStyle $font, int $rotation = 0): int + { + if (!function_exists('imagettfbbox')) { + throw new PhpSpreadsheetException('GD library needs to be enabled'); + } + + // font size should really be supplied in pixels in GD2, + // but since GD2 seems to assume 72dpi, pixels and points are the same + $fontFile = self::getTrueTypeFontFileFromFont($font); + $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + + // Get corners positions + $lowerLeftCornerX = $textBox[0]; + $lowerRightCornerX = $textBox[2]; + $upperRightCornerX = $textBox[4]; + $upperLeftCornerX = $textBox[6]; + + // Consider the rotation when calculating the width + return max($lowerRightCornerX - $upperLeftCornerX, $upperRightCornerX - $lowerLeftCornerX); + } + + /** + * Get approximate width in pixels for a string of text in a certain font at a certain rotation angle. + * + * @param string $columnText + * @param int $rotation + * + * @return int Text width in pixels (no padding added) + */ + public static function getTextWidthPixelsApprox($columnText, FontStyle $font, $rotation = 0) + { + $fontName = $font->getName(); + $fontSize = $font->getSize(); + + // Calculate column width in pixels. We assume fixed glyph width. Result varies with font name and size. + switch ($fontName) { + case 'Calibri': + // value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font. + $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + + break; + case 'Arial': + // value 8 was set because of experience in different exports at Arial 10 font. + $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + + break; + case 'Verdana': + // value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font. + $columnWidth = (int) (8 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + + break; + default: + // just assume Calibri + $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText)); + $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + + break; + } + + // Calculate approximate rotated column width + if ($rotation !== 0) { + if ($rotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + // stacked text + $columnWidth = 4; // approximation + } else { + // rotated text + $columnWidth = $columnWidth * cos(deg2rad($rotation)) + + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation + } + } + + // pixel width is an integer + return (int) $columnWidth; + } + + /** + * Calculate an (approximate) pixel size, based on a font points size. + * + * @param int $fontSizeInPoints Font size (in points) + * + * @return int Font size (in pixels) + */ + public static function fontSizeToPixels($fontSizeInPoints) + { + return (int) ((4 / 3) * $fontSizeInPoints); + } + + /** + * Calculate an (approximate) pixel size, based on inch size. + * + * @param int $sizeInInch Font size (in inch) + * + * @return int Size (in pixels) + */ + public static function inchSizeToPixels($sizeInInch) + { + return $sizeInInch * 96; + } + + /** + * Calculate an (approximate) pixel size, based on centimeter size. + * + * @param int $sizeInCm Font size (in centimeters) + * + * @return float Size (in pixels) + */ + public static function centimeterSizeToPixels($sizeInCm) + { + return $sizeInCm * 37.795275591; + } + + /** + * Returns the font path given the font. + * + * @return string Path to TrueType font file + */ + public static function getTrueTypeFontFileFromFont(FontStyle $font) + { + if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); + } + + $name = $font->getName(); + $bold = $font->getBold(); + $italic = $font->getItalic(); + + // Check if we can map font to true type font file + switch ($name) { + case 'Arial': + $fontFile = ( + $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) + : ($italic ? self::ARIAL_ITALIC : self::ARIAL) + ); + + break; + case 'Calibri': + $fontFile = ( + $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) + : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) + ); + + break; + case 'Courier New': + $fontFile = ( + $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) + : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) + ); + + break; + case 'Comic Sans MS': + $fontFile = ( + $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS + ); + + break; + case 'Georgia': + $fontFile = ( + $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) + : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) + ); + + break; + case 'Impact': + $fontFile = self::IMPACT; + + break; + case 'Liberation Sans': + $fontFile = ( + $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) + : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) + ); + + break; + case 'Lucida Console': + $fontFile = self::LUCIDA_CONSOLE; + + break; + case 'Lucida Sans Unicode': + $fontFile = self::LUCIDA_SANS_UNICODE; + + break; + case 'Microsoft Sans Serif': + $fontFile = self::MICROSOFT_SANS_SERIF; + + break; + case 'Palatino Linotype': + $fontFile = ( + $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) + : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) + ); + + break; + case 'Symbol': + $fontFile = self::SYMBOL; + + break; + case 'Tahoma': + $fontFile = ( + $bold ? self::TAHOMA_BOLD : self::TAHOMA + ); + + break; + case 'Times New Roman': + $fontFile = ( + $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) + : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) + ); + + break; + case 'Trebuchet MS': + $fontFile = ( + $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) + : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) + ); + + break; + case 'Verdana': + $fontFile = ( + $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) + : ($italic ? self::VERDANA_ITALIC : self::VERDANA) + ); + + break; + default: + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + + break; + } + + $fontFile = self::$trueTypeFontPath . $fontFile; + + // Check if file actually exists + if (!file_exists($fontFile)) { + throw new PhpSpreadsheetException('TrueType Font file not found'); + } + + return $fontFile; + } + + /** + * Returns the associated charset for the font name. + * + * @param string $fontName Font name + * + * @return int Character set code + */ + public static function getCharsetFromFontName($fontName) + { + switch ($fontName) { + // Add more cases. Check FONT records in real Excel files. + case 'EucrosiaUPC': + return self::CHARSET_ANSI_THAI; + case 'Wingdings': + return self::CHARSET_SYMBOL; + case 'Wingdings 2': + return self::CHARSET_SYMBOL; + case 'Wingdings 3': + return self::CHARSET_SYMBOL; + default: + return self::CHARSET_ANSI_LATIN; + } + } + + /** + * Get the effective column width for columns without a column dimension or column with width -1 + * For example, for Calibri 11 this is 9.140625 (64 px). + * + * @param FontStyle $font The workbooks default font + * @param bool $returnAsPixels true = return column width in pixels, false = return in OOXML units + * + * @return mixed Column width + */ + public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false) + { + if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + // Exact width can be determined + $columnWidth = $returnAsPixels ? + self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] + : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $columnWidth = $returnAsPixels ? + self::$defaultColumnWidths['Calibri'][11]['px'] + : self::$defaultColumnWidths['Calibri'][11]['width']; + $columnWidth = $columnWidth * $font->getSize() / 11; + + // Round pixels to closest integer + if ($returnAsPixels) { + $columnWidth = (int) round($columnWidth); + } + } + + return $columnWidth; + } + + /** + * Get the effective row height for rows without a row dimension or rows with height -1 + * For example, for Calibri 11 this is 15 points. + * + * @param FontStyle $font The workbooks default font + * + * @return float Row height in points + */ + public static function getDefaultRowHeightByFont(FontStyle $font) + { + switch ($font->getName()) { + case 'Arial': + switch ($font->getSize()) { + case 10: + // inspection of Arial 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Arial 9 workbook says 12.00pt ~16px + $rowHeight = 12; + + break; + case 8: + // inspection of Arial 8 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 7: + // inspection of Arial 7 workbook says 9.00pt ~12px + $rowHeight = 9; + + break; + case 6: + case 5: + // inspection of Arial 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Arial 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Arial 3 workbook says 6.00pt ~8px + $rowHeight = 6; + + break; + case 2: + case 1: + // inspection of Arial 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Arial 10 workbook as an approximation, extrapolation + $rowHeight = 12.75 * $font->getSize() / 10; + + break; + } + + break; + case 'Calibri': + switch ($font->getSize()) { + case 11: + // inspection of Calibri 11 workbook says 15.00pt ~20px + $rowHeight = 15; + + break; + case 10: + // inspection of Calibri 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Calibri 9 workbook says 12.00pt ~16px + $rowHeight = 12; + + break; + case 8: + // inspection of Calibri 8 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 7: + // inspection of Calibri 7 workbook says 9.00pt ~12px + $rowHeight = 9; + + break; + case 6: + case 5: + // inspection of Calibri 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Calibri 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Calibri 3 workbook says 6.00pt ~8px + $rowHeight = 6.00; + + break; + case 2: + case 1: + // inspection of Calibri 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Calibri 11 workbook as an approximation, extrapolation + $rowHeight = 15 * $font->getSize() / 11; + + break; + } + + break; + case 'Verdana': + switch ($font->getSize()) { + case 10: + // inspection of Verdana 10 workbook says 12.75pt ~17px + $rowHeight = 12.75; + + break; + case 9: + // inspection of Verdana 9 workbook says 11.25pt ~15px + $rowHeight = 11.25; + + break; + case 8: + // inspection of Verdana 8 workbook says 10.50pt ~14px + $rowHeight = 10.50; + + break; + case 7: + // inspection of Verdana 7 workbook says 9.00pt ~12px + $rowHeight = 9.00; + + break; + case 6: + case 5: + // inspection of Verdana 5,6 workbook says 8.25pt ~11px + $rowHeight = 8.25; + + break; + case 4: + // inspection of Verdana 4 workbook says 6.75pt ~9px + $rowHeight = 6.75; + + break; + case 3: + // inspection of Verdana 3 workbook says 6.00pt ~8px + $rowHeight = 6; + + break; + case 2: + case 1: + // inspection of Verdana 1,2 workbook says 5.25pt ~7px + $rowHeight = 5.25; + + break; + default: + // use Verdana 10 workbook as an approximation, extrapolation + $rowHeight = 12.75 * $font->getSize() / 10; + + break; + } + + break; + default: + // just use Calibri as an approximation + $rowHeight = 15 * $font->getSize() / 11; + + break; + } + + return $rowHeight; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php new file mode 100644 index 0000000..060f09c --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php @@ -0,0 +1,21 @@ +d = $this->V[$this->n - 1]; + $j = 0; + // Householder reduction to tridiagonal form. + for ($i = $this->n - 1; $i > 0; --$i) { + $i_ = $i - 1; + // Scale to avoid under/overflow. + $h = $scale = 0.0; + $scale += array_sum(array_map('abs', $this->d)); + if ($scale == 0.0) { + $this->e[$i] = $this->d[$i_]; + $this->d = array_slice($this->V[$i_], 0, $i_); + for ($j = 0; $j < $i; ++$j) { + $this->V[$j][$i] = $this->V[$i][$j] = 0.0; + } + } else { + // Generate Householder vector. + for ($k = 0; $k < $i; ++$k) { + $this->d[$k] /= $scale; + $h += $this->d[$k] ** 2; + } + $f = $this->d[$i_]; + $g = sqrt($h); + if ($f > 0) { + $g = -$g; + } + $this->e[$i] = $scale * $g; + $h = $h - $f * $g; + $this->d[$i_] = $f - $g; + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] = 0.0; + } + // Apply similarity transformation to remaining columns. + for ($j = 0; $j < $i; ++$j) { + $f = $this->d[$j]; + $this->V[$j][$i] = $f; + $g = $this->e[$j] + $this->V[$j][$j] * $f; + for ($k = $j + 1; $k <= $i_; ++$k) { + $g += $this->V[$k][$j] * $this->d[$k]; + $this->e[$k] += $this->V[$k][$j] * $f; + } + $this->e[$j] = $g; + } + $f = 0.0; + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] /= $h; + $f += $this->e[$j] * $this->d[$j]; + } + $hh = $f / (2 * $h); + for ($j = 0; $j < $i; ++$j) { + $this->e[$j] -= $hh * $this->d[$j]; + } + for ($j = 0; $j < $i; ++$j) { + $f = $this->d[$j]; + $g = $this->e[$j]; + for ($k = $j; $k <= $i_; ++$k) { + $this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]); + } + $this->d[$j] = $this->V[$i - 1][$j]; + $this->V[$i][$j] = 0.0; + } + } + $this->d[$i] = $h; + } + + // Accumulate transformations. + for ($i = 0; $i < $this->n - 1; ++$i) { + $this->V[$this->n - 1][$i] = $this->V[$i][$i]; + $this->V[$i][$i] = 1.0; + $h = $this->d[$i + 1]; + if ($h != 0.0) { + for ($k = 0; $k <= $i; ++$k) { + $this->d[$k] = $this->V[$k][$i + 1] / $h; + } + for ($j = 0; $j <= $i; ++$j) { + $g = 0.0; + for ($k = 0; $k <= $i; ++$k) { + $g += $this->V[$k][$i + 1] * $this->V[$k][$j]; + } + for ($k = 0; $k <= $i; ++$k) { + $this->V[$k][$j] -= $g * $this->d[$k]; + } + } + } + for ($k = 0; $k <= $i; ++$k) { + $this->V[$k][$i + 1] = 0.0; + } + } + + $this->d = $this->V[$this->n - 1]; + $this->V[$this->n - 1] = array_fill(0, $j, 0.0); + $this->V[$this->n - 1][$this->n - 1] = 1.0; + $this->e[0] = 0.0; + } + + /** + * Symmetric tridiagonal QL algorithm. + * + * This is derived from the Algol procedures tql2, by + * Bowdler, Martin, Reinsch, and Wilkinson, Handbook for + * Auto. Comp., Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutine in EISPACK. + */ + private function tql2(): void + { + for ($i = 1; $i < $this->n; ++$i) { + $this->e[$i - 1] = $this->e[$i]; + } + $this->e[$this->n - 1] = 0.0; + $f = 0.0; + $tst1 = 0.0; + $eps = 2.0 ** (-52.0); + + for ($l = 0; $l < $this->n; ++$l) { + // Find small subdiagonal element + $tst1 = max($tst1, abs($this->d[$l]) + abs($this->e[$l])); + $m = $l; + while ($m < $this->n) { + if (abs($this->e[$m]) <= $eps * $tst1) { + break; + } + ++$m; + } + // If m == l, $this->d[l] is an eigenvalue, + // otherwise, iterate. + if ($m > $l) { + $iter = 0; + do { + // Could check iteration count here. + ++$iter; + // Compute implicit shift + $g = $this->d[$l]; + $p = ($this->d[$l + 1] - $g) / (2.0 * $this->e[$l]); + $r = hypo($p, 1.0); + if ($p < 0) { + $r *= -1; + } + $this->d[$l] = $this->e[$l] / ($p + $r); + $this->d[$l + 1] = $this->e[$l] * ($p + $r); + $dl1 = $this->d[$l + 1]; + $h = $g - $this->d[$l]; + for ($i = $l + 2; $i < $this->n; ++$i) { + $this->d[$i] -= $h; + } + $f += $h; + // Implicit QL transformation. + $p = $this->d[$m]; + $c = 1.0; + $c2 = $c3 = $c; + $el1 = $this->e[$l + 1]; + $s = $s2 = 0.0; + for ($i = $m - 1; $i >= $l; --$i) { + $c3 = $c2; + $c2 = $c; + $s2 = $s; + $g = $c * $this->e[$i]; + $h = $c * $p; + $r = hypo($p, $this->e[$i]); + $this->e[$i + 1] = $s * $r; + $s = $this->e[$i] / $r; + $c = $p / $r; + $p = $c * $this->d[$i] - $s * $g; + $this->d[$i + 1] = $h + $s * ($c * $g + $s * $this->d[$i]); + // Accumulate transformation. + for ($k = 0; $k < $this->n; ++$k) { + $h = $this->V[$k][$i + 1]; + $this->V[$k][$i + 1] = $s * $this->V[$k][$i] + $c * $h; + $this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h; + } + } + $p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1; + $this->e[$l] = $s * $p; + $this->d[$l] = $c * $p; + // Check for convergence. + } while (abs($this->e[$l]) > $eps * $tst1); + } + $this->d[$l] = $this->d[$l] + $f; + $this->e[$l] = 0.0; + } + + // Sort eigenvalues and corresponding vectors. + for ($i = 0; $i < $this->n - 1; ++$i) { + $k = $i; + $p = $this->d[$i]; + for ($j = $i + 1; $j < $this->n; ++$j) { + if ($this->d[$j] < $p) { + $k = $j; + $p = $this->d[$j]; + } + } + if ($k != $i) { + $this->d[$k] = $this->d[$i]; + $this->d[$i] = $p; + for ($j = 0; $j < $this->n; ++$j) { + $p = $this->V[$j][$i]; + $this->V[$j][$i] = $this->V[$j][$k]; + $this->V[$j][$k] = $p; + } + } + } + } + + /** + * Nonsymmetric reduction to Hessenberg form. + * + * This is derived from the Algol procedures orthes and ortran, + * by Martin and Wilkinson, Handbook for Auto. Comp., + * Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutines in EISPACK. + */ + private function orthes(): void + { + $low = 0; + $high = $this->n - 1; + + for ($m = $low + 1; $m <= $high - 1; ++$m) { + // Scale column. + $scale = 0.0; + for ($i = $m; $i <= $high; ++$i) { + $scale = $scale + abs($this->H[$i][$m - 1]); + } + if ($scale != 0.0) { + // Compute Householder transformation. + $h = 0.0; + for ($i = $high; $i >= $m; --$i) { + $this->ort[$i] = $this->H[$i][$m - 1] / $scale; + $h += $this->ort[$i] * $this->ort[$i]; + } + $g = sqrt($h); + if ($this->ort[$m] > 0) { + $g *= -1; + } + $h -= $this->ort[$m] * $g; + $this->ort[$m] -= $g; + // Apply Householder similarity transformation + // H = (I -u * u' / h) * H * (I -u * u') / h) + for ($j = $m; $j < $this->n; ++$j) { + $f = 0.0; + for ($i = $high; $i >= $m; --$i) { + $f += $this->ort[$i] * $this->H[$i][$j]; + } + $f /= $h; + for ($i = $m; $i <= $high; ++$i) { + $this->H[$i][$j] -= $f * $this->ort[$i]; + } + } + for ($i = 0; $i <= $high; ++$i) { + $f = 0.0; + for ($j = $high; $j >= $m; --$j) { + $f += $this->ort[$j] * $this->H[$i][$j]; + } + $f = $f / $h; + for ($j = $m; $j <= $high; ++$j) { + $this->H[$i][$j] -= $f * $this->ort[$j]; + } + } + $this->ort[$m] = $scale * $this->ort[$m]; + $this->H[$m][$m - 1] = $scale * $g; + } + } + + // Accumulate transformations (Algol's ortran). + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->V[$i][$j] = ($i == $j ? 1.0 : 0.0); + } + } + for ($m = $high - 1; $m >= $low + 1; --$m) { + if ($this->H[$m][$m - 1] != 0.0) { + for ($i = $m + 1; $i <= $high; ++$i) { + $this->ort[$i] = $this->H[$i][$m - 1]; + } + for ($j = $m; $j <= $high; ++$j) { + $g = 0.0; + for ($i = $m; $i <= $high; ++$i) { + $g += $this->ort[$i] * $this->V[$i][$j]; + } + // Double division avoids possible underflow + $g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1]; + for ($i = $m; $i <= $high; ++$i) { + $this->V[$i][$j] += $g * $this->ort[$i]; + } + } + } + } + } + + /** + * Performs complex division. + * + * @param mixed $xr + * @param mixed $xi + * @param mixed $yr + * @param mixed $yi + */ + private function cdiv($xr, $xi, $yr, $yi): void + { + if (abs($yr) > abs($yi)) { + $r = $yi / $yr; + $d = $yr + $r * $yi; + $this->cdivr = ($xr + $r * $xi) / $d; + $this->cdivi = ($xi - $r * $xr) / $d; + } else { + $r = $yr / $yi; + $d = $yi + $r * $yr; + $this->cdivr = ($r * $xr + $xi) / $d; + $this->cdivi = ($r * $xi - $xr) / $d; + } + } + + /** + * Nonsymmetric reduction from Hessenberg to real Schur form. + * + * Code is derived from the Algol procedure hqr2, + * by Martin and Wilkinson, Handbook for Auto. Comp., + * Vol.ii-Linear Algebra, and the corresponding + * Fortran subroutine in EISPACK. + */ + private function hqr2(): void + { + // Initialize + $nn = $this->n; + $n = $nn - 1; + $low = 0; + $high = $nn - 1; + $eps = 2.0 ** (-52.0); + $exshift = 0.0; + $p = $q = $r = $s = $z = 0; + // Store roots isolated by balanc and compute matrix norm + $norm = 0.0; + + for ($i = 0; $i < $nn; ++$i) { + if ($i > $high) { + $this->d[$i] = $this->H[$i][$i]; + $this->e[$i] = 0.0; + } + for ($j = max($i - 1, 0); $j < $nn; ++$j) { + $norm = $norm + abs($this->H[$i][$j]); + } + } + + // Outer loop over eigenvalue index + $iter = 0; + while ($n >= $low) { + // Look for single small sub-diagonal element + $l = $n; + while ($l > $low) { + $s = abs($this->H[$l - 1][$l - 1]) + abs($this->H[$l][$l]); + if ($s == 0.0) { + $s = $norm; + } + if (abs($this->H[$l][$l - 1]) < $eps * $s) { + break; + } + --$l; + } + // Check for convergence + // One root found + if ($l == $n) { + $this->H[$n][$n] = $this->H[$n][$n] + $exshift; + $this->d[$n] = $this->H[$n][$n]; + $this->e[$n] = 0.0; + --$n; + $iter = 0; + // Two roots found + } elseif ($l == $n - 1) { + $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; + $p = ($this->H[$n - 1][$n - 1] - $this->H[$n][$n]) / 2.0; + $q = $p * $p + $w; + $z = sqrt(abs($q)); + $this->H[$n][$n] = $this->H[$n][$n] + $exshift; + $this->H[$n - 1][$n - 1] = $this->H[$n - 1][$n - 1] + $exshift; + $x = $this->H[$n][$n]; + // Real pair + if ($q >= 0) { + if ($p >= 0) { + $z = $p + $z; + } else { + $z = $p - $z; + } + $this->d[$n - 1] = $x + $z; + $this->d[$n] = $this->d[$n - 1]; + if ($z != 0.0) { + $this->d[$n] = $x - $w / $z; + } + $this->e[$n - 1] = 0.0; + $this->e[$n] = 0.0; + $x = $this->H[$n][$n - 1]; + $s = abs($x) + abs($z); + $p = $x / $s; + $q = $z / $s; + $r = sqrt($p * $p + $q * $q); + $p = $p / $r; + $q = $q / $r; + // Row modification + for ($j = $n - 1; $j < $nn; ++$j) { + $z = $this->H[$n - 1][$j]; + $this->H[$n - 1][$j] = $q * $z + $p * $this->H[$n][$j]; + $this->H[$n][$j] = $q * $this->H[$n][$j] - $p * $z; + } + // Column modification + for ($i = 0; $i <= $n; ++$i) { + $z = $this->H[$i][$n - 1]; + $this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n]; + $this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z; + } + // Accumulate transformations + for ($i = $low; $i <= $high; ++$i) { + $z = $this->V[$i][$n - 1]; + $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; + $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; + } + // Complex pair + } else { + $this->d[$n - 1] = $x + $p; + $this->d[$n] = $x + $p; + $this->e[$n - 1] = $z; + $this->e[$n] = -$z; + } + $n = $n - 2; + $iter = 0; + // No convergence yet + } else { + // Form shift + $x = $this->H[$n][$n]; + $y = 0.0; + $w = 0.0; + if ($l < $n) { + $y = $this->H[$n - 1][$n - 1]; + $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; + } + // Wilkinson's original ad hoc shift + if ($iter == 10) { + $exshift += $x; + for ($i = $low; $i <= $n; ++$i) { + $this->H[$i][$i] -= $x; + } + $s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]); + $x = $y = 0.75 * $s; + $w = -0.4375 * $s * $s; + } + // MATLAB's new ad hoc shift + if ($iter == 30) { + $s = ($y - $x) / 2.0; + $s = $s * $s + $w; + if ($s > 0) { + $s = sqrt($s); + if ($y < $x) { + $s = -$s; + } + $s = $x - $w / (($y - $x) / 2.0 + $s); + for ($i = $low; $i <= $n; ++$i) { + $this->H[$i][$i] -= $s; + } + $exshift += $s; + $x = $y = $w = 0.964; + } + } + // Could check iteration count here. + $iter = $iter + 1; + // Look for two consecutive small sub-diagonal elements + $m = $n - 2; + while ($m >= $l) { + $z = $this->H[$m][$m]; + $r = $x - $z; + $s = $y - $z; + $p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1]; + $q = $this->H[$m + 1][$m + 1] - $z - $r - $s; + $r = $this->H[$m + 2][$m + 1]; + $s = abs($p) + abs($q) + abs($r); + $p = $p / $s; + $q = $q / $s; + $r = $r / $s; + if ($m == $l) { + break; + } + if ( + abs($this->H[$m][$m - 1]) * (abs($q) + abs($r)) < + $eps * (abs($p) * (abs($this->H[$m - 1][$m - 1]) + abs($z) + abs($this->H[$m + 1][$m + 1]))) + ) { + break; + } + --$m; + } + for ($i = $m + 2; $i <= $n; ++$i) { + $this->H[$i][$i - 2] = 0.0; + if ($i > $m + 2) { + $this->H[$i][$i - 3] = 0.0; + } + } + // Double QR step involving rows l:n and columns m:n + for ($k = $m; $k <= $n - 1; ++$k) { + $notlast = ($k != $n - 1); + if ($k != $m) { + $p = $this->H[$k][$k - 1]; + $q = $this->H[$k + 1][$k - 1]; + $r = ($notlast ? $this->H[$k + 2][$k - 1] : 0.0); + $x = abs($p) + abs($q) + abs($r); + if ($x != 0.0) { + $p = $p / $x; + $q = $q / $x; + $r = $r / $x; + } + } + if ($x == 0.0) { + break; + } + $s = sqrt($p * $p + $q * $q + $r * $r); + if ($p < 0) { + $s = -$s; + } + if ($s != 0) { + if ($k != $m) { + $this->H[$k][$k - 1] = -$s * $x; + } elseif ($l != $m) { + $this->H[$k][$k - 1] = -$this->H[$k][$k - 1]; + } + $p = $p + $s; + $x = $p / $s; + $y = $q / $s; + $z = $r / $s; + $q = $q / $p; + $r = $r / $p; + // Row modification + for ($j = $k; $j < $nn; ++$j) { + $p = $this->H[$k][$j] + $q * $this->H[$k + 1][$j]; + if ($notlast) { + $p = $p + $r * $this->H[$k + 2][$j]; + $this->H[$k + 2][$j] = $this->H[$k + 2][$j] - $p * $z; + } + $this->H[$k][$j] = $this->H[$k][$j] - $p * $x; + $this->H[$k + 1][$j] = $this->H[$k + 1][$j] - $p * $y; + } + // Column modification + $iMax = min($n, $k + 3); + for ($i = 0; $i <= $iMax; ++$i) { + $p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1]; + if ($notlast) { + $p = $p + $z * $this->H[$i][$k + 2]; + $this->H[$i][$k + 2] = $this->H[$i][$k + 2] - $p * $r; + } + $this->H[$i][$k] = $this->H[$i][$k] - $p; + $this->H[$i][$k + 1] = $this->H[$i][$k + 1] - $p * $q; + } + // Accumulate transformations + for ($i = $low; $i <= $high; ++$i) { + $p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1]; + if ($notlast) { + $p = $p + $z * $this->V[$i][$k + 2]; + $this->V[$i][$k + 2] = $this->V[$i][$k + 2] - $p * $r; + } + $this->V[$i][$k] = $this->V[$i][$k] - $p; + $this->V[$i][$k + 1] = $this->V[$i][$k + 1] - $p * $q; + } + } // ($s != 0) + } // k loop + } // check convergence + } // while ($n >= $low) + + // Backsubstitute to find vectors of upper triangular form + if ($norm == 0.0) { + return; + } + + for ($n = $nn - 1; $n >= 0; --$n) { + $p = $this->d[$n]; + $q = $this->e[$n]; + // Real vector + if ($q == 0) { + $l = $n; + $this->H[$n][$n] = 1.0; + for ($i = $n - 1; $i >= 0; --$i) { + $w = $this->H[$i][$i] - $p; + $r = 0.0; + for ($j = $l; $j <= $n; ++$j) { + $r = $r + $this->H[$i][$j] * $this->H[$j][$n]; + } + if ($this->e[$i] < 0.0) { + $z = $w; + $s = $r; + } else { + $l = $i; + if ($this->e[$i] == 0.0) { + if ($w != 0.0) { + $this->H[$i][$n] = -$r / $w; + } else { + $this->H[$i][$n] = -$r / ($eps * $norm); + } + // Solve real equations + } else { + $x = $this->H[$i][$i + 1]; + $y = $this->H[$i + 1][$i]; + $q = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i]; + $t = ($x * $s - $z * $r) / $q; + $this->H[$i][$n] = $t; + if (abs($x) > abs($z)) { + $this->H[$i + 1][$n] = (-$r - $w * $t) / $x; + } else { + $this->H[$i + 1][$n] = (-$s - $y * $t) / $z; + } + } + // Overflow control + $t = abs($this->H[$i][$n]); + if (($eps * $t) * $t > 1) { + for ($j = $i; $j <= $n; ++$j) { + $this->H[$j][$n] = $this->H[$j][$n] / $t; + } + } + } + } + // Complex vector + } elseif ($q < 0) { + $l = $n - 1; + // Last vector component imaginary so matrix is triangular + if (abs($this->H[$n][$n - 1]) > abs($this->H[$n - 1][$n])) { + $this->H[$n - 1][$n - 1] = $q / $this->H[$n][$n - 1]; + $this->H[$n - 1][$n] = -($this->H[$n][$n] - $p) / $this->H[$n][$n - 1]; + } else { + $this->cdiv(0.0, -$this->H[$n - 1][$n], $this->H[$n - 1][$n - 1] - $p, $q); + $this->H[$n - 1][$n - 1] = $this->cdivr; + $this->H[$n - 1][$n] = $this->cdivi; + } + $this->H[$n][$n - 1] = 0.0; + $this->H[$n][$n] = 1.0; + for ($i = $n - 2; $i >= 0; --$i) { + // double ra,sa,vr,vi; + $ra = 0.0; + $sa = 0.0; + for ($j = $l; $j <= $n; ++$j) { + $ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1]; + $sa = $sa + $this->H[$i][$j] * $this->H[$j][$n]; + } + $w = $this->H[$i][$i] - $p; + if ($this->e[$i] < 0.0) { + $z = $w; + $r = $ra; + $s = $sa; + } else { + $l = $i; + if ($this->e[$i] == 0) { + $this->cdiv(-$ra, -$sa, $w, $q); + $this->H[$i][$n - 1] = $this->cdivr; + $this->H[$i][$n] = $this->cdivi; + } else { + // Solve complex equations + $x = $this->H[$i][$i + 1]; + $y = $this->H[$i + 1][$i]; + $vr = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i] - $q * $q; + $vi = ($this->d[$i] - $p) * 2.0 * $q; + if ($vr == 0.0 & $vi == 0.0) { + $vr = $eps * $norm * (abs($w) + abs($q) + abs($x) + abs($y) + abs($z)); + } + $this->cdiv($x * $r - $z * $ra + $q * $sa, $x * $s - $z * $sa - $q * $ra, $vr, $vi); + $this->H[$i][$n - 1] = $this->cdivr; + $this->H[$i][$n] = $this->cdivi; + if (abs($x) > (abs($z) + abs($q))) { + $this->H[$i + 1][$n - 1] = (-$ra - $w * $this->H[$i][$n - 1] + $q * $this->H[$i][$n]) / $x; + $this->H[$i + 1][$n] = (-$sa - $w * $this->H[$i][$n] - $q * $this->H[$i][$n - 1]) / $x; + } else { + $this->cdiv(-$r - $y * $this->H[$i][$n - 1], -$s - $y * $this->H[$i][$n], $z, $q); + $this->H[$i + 1][$n - 1] = $this->cdivr; + $this->H[$i + 1][$n] = $this->cdivi; + } + } + // Overflow control + $t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n])); + if (($eps * $t) * $t > 1) { + for ($j = $i; $j <= $n; ++$j) { + $this->H[$j][$n - 1] = $this->H[$j][$n - 1] / $t; + $this->H[$j][$n] = $this->H[$j][$n] / $t; + } + } + } // end else + } // end for + } // end else for complex case + } // end for + + // Vectors of isolated roots + for ($i = 0; $i < $nn; ++$i) { + if ($i > $high) { + for ($j = $i; $j < $nn; ++$j) { + $this->V[$i][$j] = $this->H[$i][$j]; + } + } + } + + // Back transformation to get eigenvectors of original matrix + for ($j = $nn - 1; $j >= $low; --$j) { + for ($i = $low; $i <= $high; ++$i) { + $z = 0.0; + $kMax = min($j, $high); + for ($k = $low; $k <= $kMax; ++$k) { + $z = $z + $this->V[$i][$k] * $this->H[$k][$j]; + } + $this->V[$i][$j] = $z; + } + } + } + + // end hqr2 + + /** + * Constructor: Check for symmetry, then construct the eigenvalue decomposition. + * + * @param Matrix $Arg A Square matrix + */ + public function __construct(Matrix $Arg) + { + $this->A = $Arg->getArray(); + $this->n = $Arg->getColumnDimension(); + + $issymmetric = true; + for ($j = 0; ($j < $this->n) & $issymmetric; ++$j) { + for ($i = 0; ($i < $this->n) & $issymmetric; ++$i) { + $issymmetric = ($this->A[$i][$j] == $this->A[$j][$i]); + } + } + + if ($issymmetric) { + $this->V = $this->A; + // Tridiagonalize. + $this->tred2(); + // Diagonalize. + $this->tql2(); + } else { + $this->H = $this->A; + $this->ort = []; + // Reduce to Hessenberg form. + $this->orthes(); + // Reduce Hessenberg to real Schur form. + $this->hqr2(); + } + } + + /** + * Return the eigenvector matrix. + * + * @return Matrix V + */ + public function getV() + { + return new Matrix($this->V, $this->n, $this->n); + } + + /** + * Return the real parts of the eigenvalues. + * + * @return array real(diag(D)) + */ + public function getRealEigenvalues() + { + return $this->d; + } + + /** + * Return the imaginary parts of the eigenvalues. + * + * @return array imag(diag(D)) + */ + public function getImagEigenvalues() + { + return $this->e; + } + + /** + * Return the block diagonal eigenvalue matrix. + * + * @return Matrix D + */ + public function getD() + { + $D = []; + for ($i = 0; $i < $this->n; ++$i) { + $D[$i] = array_fill(0, $this->n, 0.0); + $D[$i][$i] = $this->d[$i]; + if ($this->e[$i] == 0) { + continue; + } + $o = ($this->e[$i] > 0) ? $i + 1 : $i - 1; + $D[$i][$o] = $this->e[$i]; + } + + return new Matrix($D); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php new file mode 100644 index 0000000..ecfe42b --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php @@ -0,0 +1,284 @@ += n, the LU decomposition is an m-by-n + * unit lower triangular matrix L, an n-by-n upper triangular matrix U, + * and a permutation vector piv of length m so that A(piv,:) = L*U. + * If m < n, then L is m-by-m and U is m-by-n. + * + * The LU decompostion with pivoting always exists, even if the matrix is + * singular, so the constructor will never fail. The primary use of the + * LU decomposition is in the solution of square systems of simultaneous + * linear equations. This will fail if isNonsingular() returns false. + * + * @author Paul Meagher + * @author Bartosz Matosiuk + * @author Michael Bommarito + * + * @version 1.1 + */ +class LUDecomposition +{ + const MATRIX_SINGULAR_EXCEPTION = 'Can only perform operation on singular matrix.'; + const MATRIX_SQUARE_EXCEPTION = 'Mismatched Row dimension'; + + /** + * Decomposition storage. + * + * @var array + */ + private $LU = []; + + /** + * Row dimension. + * + * @var int + */ + private $m; + + /** + * Column dimension. + * + * @var int + */ + private $n; + + /** + * Pivot sign. + * + * @var int + */ + private $pivsign; + + /** + * Internal storage of pivot vector. + * + * @var array + */ + private $piv = []; + + /** + * LU Decomposition constructor. + * + * @param Matrix $A Rectangular matrix + */ + public function __construct($A) + { + if ($A instanceof Matrix) { + // Use a "left-looking", dot-product, Crout/Doolittle algorithm. + $this->LU = $A->getArray(); + $this->m = $A->getRowDimension(); + $this->n = $A->getColumnDimension(); + for ($i = 0; $i < $this->m; ++$i) { + $this->piv[$i] = $i; + } + $this->pivsign = 1; + $LUrowi = $LUcolj = []; + + // Outer loop. + for ($j = 0; $j < $this->n; ++$j) { + // Make a copy of the j-th column to localize references. + for ($i = 0; $i < $this->m; ++$i) { + $LUcolj[$i] = &$this->LU[$i][$j]; + } + // Apply previous transformations. + for ($i = 0; $i < $this->m; ++$i) { + $LUrowi = $this->LU[$i]; + // Most of the time is spent in the following dot product. + $kmax = min($i, $j); + $s = 0.0; + for ($k = 0; $k < $kmax; ++$k) { + $s += $LUrowi[$k] * $LUcolj[$k]; + } + $LUrowi[$j] = $LUcolj[$i] -= $s; + } + // Find pivot and exchange if necessary. + $p = $j; + for ($i = $j + 1; $i < $this->m; ++$i) { + if (abs($LUcolj[$i]) > abs($LUcolj[$p])) { + $p = $i; + } + } + if ($p != $j) { + for ($k = 0; $k < $this->n; ++$k) { + $t = $this->LU[$p][$k]; + $this->LU[$p][$k] = $this->LU[$j][$k]; + $this->LU[$j][$k] = $t; + } + $k = $this->piv[$p]; + $this->piv[$p] = $this->piv[$j]; + $this->piv[$j] = $k; + $this->pivsign = $this->pivsign * -1; + } + // Compute multipliers. + if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) { + for ($i = $j + 1; $i < $this->m; ++$i) { + $this->LU[$i][$j] /= $this->LU[$j][$j]; + } + } + } + } else { + throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION); + } + } + + // function __construct() + + /** + * Get lower triangular factor. + * + * @return Matrix Lower triangular factor + */ + public function getL() + { + $L = []; + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i > $j) { + $L[$i][$j] = $this->LU[$i][$j]; + } elseif ($i == $j) { + $L[$i][$j] = 1.0; + } else { + $L[$i][$j] = 0.0; + } + } + } + + return new Matrix($L); + } + + // function getL() + + /** + * Get upper triangular factor. + * + * @return Matrix Upper triangular factor + */ + public function getU() + { + $U = []; + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + if ($i <= $j) { + $U[$i][$j] = $this->LU[$i][$j]; + } else { + $U[$i][$j] = 0.0; + } + } + } + + return new Matrix($U); + } + + // function getU() + + /** + * Return pivot permutation vector. + * + * @return array Pivot vector + */ + public function getPivot() + { + return $this->piv; + } + + // function getPivot() + + /** + * Alias for getPivot. + * + * @see getPivot + */ + public function getDoublePivot() + { + return $this->getPivot(); + } + + // function getDoublePivot() + + /** + * Is the matrix nonsingular? + * + * @return bool true if U, and hence A, is nonsingular + */ + public function isNonsingular() + { + for ($j = 0; $j < $this->n; ++$j) { + if ($this->LU[$j][$j] == 0) { + return false; + } + } + + return true; + } + + // function isNonsingular() + + /** + * Count determinants. + * + * @return float + */ + public function det() + { + if ($this->m == $this->n) { + $d = $this->pivsign; + for ($j = 0; $j < $this->n; ++$j) { + $d *= $this->LU[$j][$j]; + } + + return $d; + } + + throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); + } + + // function det() + + /** + * Solve A*X = B. + * + * @param Matrix $B a Matrix with as many rows as A and any number of columns + * + * @return Matrix X so that L*U*X = B(piv,:) + */ + public function solve(Matrix $B) + { + if ($B->getRowDimension() == $this->m) { + if ($this->isNonsingular()) { + // Copy right hand side with pivoting + $nx = $B->getColumnDimension(); + $X = $B->getMatrix($this->piv, 0, $nx - 1); + // Solve L*Y = B(piv,:) + for ($k = 0; $k < $this->n; ++$k) { + for ($i = $k + 1; $i < $this->n; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; + } + } + } + // Solve U*X = Y; + for ($k = $this->n - 1; $k >= 0; --$k) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$k][$j] /= $this->LU[$k][$k]; + } + for ($i = 0; $i < $k; ++$i) { + for ($j = 0; $j < $nx; ++$j) { + $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; + } + } + } + + return $X; + } + + throw new CalculationException(self::MATRIX_SINGULAR_EXCEPTION); + } + + throw new CalculationException(self::MATRIX_SQUARE_EXCEPTION); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php new file mode 100644 index 0000000..adf399a --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -0,0 +1,1189 @@ + 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + //Rectangular matrix - m x n initialized from 2D array + case 'array': + $this->m = count($args[0]); + $this->n = count($args[0][0]); + $this->A = $args[0]; + + break; + //Square matrix - n x n + case 'integer': + $this->m = $args[0]; + $this->n = $args[0]; + $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); + + break; + //Rectangular matrix - m x n + case 'integer,integer': + $this->m = $args[0]; + $this->n = $args[1]; + $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); + + break; + //Rectangular matrix - m x n initialized from packed array + case 'array,integer': + $this->m = $args[1]; + if ($this->m != 0) { + $this->n = count($args[0]) / $this->m; + } else { + $this->n = 0; + } + if (($this->m * $this->n) == count($args[0])) { + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $args[0][$i + $j * $this->m]; + } + } + } else { + throw new CalculationException(self::ARRAY_LENGTH_EXCEPTION); + } + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * getArray. + * + * @return array Matrix array + */ + public function getArray() + { + return $this->A; + } + + /** + * getRowDimension. + * + * @return int Row dimension + */ + public function getRowDimension() + { + return $this->m; + } + + /** + * getColumnDimension. + * + * @return int Column dimension + */ + public function getColumnDimension() + { + return $this->n; + } + + /** + * get. + * + * Get the i,j-th element of the matrix. + * + * @param int $i Row position + * @param int $j Column position + * + * @return float|int + */ + public function get($i = null, $j = null) + { + return $this->A[$i][$j]; + } + + /** + * getMatrix. + * + * Get a submatrix + * + * @return Matrix Submatrix + */ + public function getMatrix(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + //A($i0...; $j0...) + case 'integer,integer': + [$i0, $j0] = $args; + if ($i0 >= 0) { + $m = $this->m - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if ($j0 >= 0) { + $n = $this->n - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = $i0; $i < $this->m; ++$i) { + for ($j = $j0; $j < $this->n; ++$j) { + $R->set($i, $j, $this->A[$i][$j]); + } + } + + return $R; + + break; + //A($i0...$iF; $j0...$jF) + case 'integer,integer,integer,integer': + [$i0, $iF, $j0, $jF] = $args; + if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { + $m = $iF - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (($jF > $j0) && ($this->n >= $jF) && ($j0 >= 0)) { + $n = $jF - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m + 1, $n + 1); + for ($i = $i0; $i <= $iF; ++$i) { + for ($j = $j0; $j <= $jF; ++$j) { + $R->set($i - $i0, $j - $j0, $this->A[$i][$j]); + } + } + + return $R; + + break; + //$R = array of row indices; $C = array of column indices + case 'array,array': + [$RL, $CL] = $args; + if (count($RL) > 0) { + $m = count($RL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (count($CL) > 0) { + $n = count($CL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = 0; $i < $m; ++$i) { + for ($j = 0; $j < $n; ++$j) { + $R->set($i, $j, $this->A[$RL[$i]][$CL[$j]]); + } + } + + return $R; + + break; + //A($i0...$iF); $CL = array of column indices + case 'integer,integer,array': + [$i0, $iF, $CL] = $args; + if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { + $m = $iF - $i0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (count($CL) > 0) { + $n = count($CL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n); + for ($i = $i0; $i < $iF; ++$i) { + for ($j = 0; $j < $n; ++$j) { + $R->set($i - $i0, $j, $this->A[$i][$CL[$j]]); + } + } + + return $R; + + break; + //$RL = array of row indices + case 'array,integer,integer': + [$RL, $j0, $jF] = $args; + if (count($RL) > 0) { + $m = count($RL); + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + if (($jF >= $j0) && ($this->n >= $jF) && ($j0 >= 0)) { + $n = $jF - $j0; + } else { + throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); + } + $R = new self($m, $n + 1); + for ($i = 0; $i < $m; ++$i) { + for ($j = $j0; $j <= $jF; ++$j) { + $R->set($i, $j - $j0, $this->A[$RL[$i]][$j]); + } + } + + return $R; + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * checkMatrixDimensions. + * + * Is matrix B the same size? + * + * @param Matrix $B Matrix B + * + * @return bool + */ + public function checkMatrixDimensions($B = null) + { + if ($B instanceof self) { + if (($this->m == $B->getRowDimension()) && ($this->n == $B->getColumnDimension())) { + return true; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + // function checkMatrixDimensions() + + /** + * set. + * + * Set the i,j-th element of the matrix. + * + * @param int $i Row position + * @param int $j Column position + * @param float|int $c value + */ + public function set($i = null, $j = null, $c = null): void + { + // Optimized set version just has this + $this->A[$i][$j] = $c; + } + + // function set() + + /** + * identity. + * + * Generate an identity matrix. + * + * @param int $m Row dimension + * @param int $n Column dimension + * + * @return Matrix Identity matrix + */ + public function identity($m = null, $n = null) + { + return $this->diagonal($m, $n, 1); + } + + /** + * diagonal. + * + * Generate a diagonal matrix + * + * @param int $m Row dimension + * @param int $n Column dimension + * @param mixed $c Diagonal value + * + * @return Matrix Diagonal matrix + */ + public function diagonal($m = null, $n = null, $c = 1) + { + $R = new self($m, $n); + for ($i = 0; $i < $m; ++$i) { + $R->set($i, $i, $c); + } + + return $R; + } + + /** + * getMatrixByRow. + * + * Get a submatrix by row index/range + * + * @param int $i0 Initial row index + * @param int $iF Final row index + * + * @return Matrix Submatrix + */ + public function getMatrixByRow($i0 = null, $iF = null) + { + if (is_int($i0)) { + if (is_int($iF)) { + return $this->getMatrix($i0, 0, $iF + 1, $this->n); + } + + return $this->getMatrix($i0, 0, $i0 + 1, $this->n); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + /** + * getMatrixByCol. + * + * Get a submatrix by column index/range + * + * @param int $j0 Initial column index + * @param int $jF Final column index + * + * @return Matrix Submatrix + */ + public function getMatrixByCol($j0 = null, $jF = null) + { + if (is_int($j0)) { + if (is_int($jF)) { + return $this->getMatrix(0, $j0, $this->m, $jF + 1); + } + + return $this->getMatrix(0, $j0, $this->m, $j0 + 1); + } + + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + /** + * transpose. + * + * Tranpose matrix + * + * @return Matrix Transposed matrix + */ + public function transpose() + { + $R = new self($this->n, $this->m); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $R->set($j, $i, $this->A[$i][$j]); + } + } + + return $R; + } + + // function transpose() + + /** + * trace. + * + * Sum of diagonal elements + * + * @return float Sum of diagonal elements + */ + public function trace() + { + $s = 0; + $n = min($this->m, $this->n); + for ($i = 0; $i < $n; ++$i) { + $s += $this->A[$i][$i]; + } + + return $s; + } + + /** + * plus. + * + * A + B + * + * @return Matrix Sum + */ + public function plus(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) + $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * plusEquals. + * + * A = A + B + * + * @return $this + */ + public function plusEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] += $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * minus. + * + * A - B + * + * @return Matrix Sum + */ + public function minus(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) - $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * minusEquals. + * + * A = A - B + * + * @return $this + */ + public function minusEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] -= $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayTimes. + * + * Element-by-element multiplication + * Cij = Aij * Bij + * + * @return Matrix Matrix Cij + */ + public function arrayTimes(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) * $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayTimesEquals. + * + * Element-by-element multiplication + * Aij = Aij * Bij + * + * @return $this + */ + public function arrayTimesEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] *= $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayRightDivide. + * + * Element-by-element right division + * A / B + * + * @return Matrix Division result + */ + public function arrayRightDivide(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + if ($value == 0) { + // Trap for Divide by Zero error + $M->set($i, $j, '#DIV/0!'); + } else { + $M->set($i, $j, $this->A[$i][$j] / $value); + } + } else { + $M->set($i, $j, Functions::NAN()); + } + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayRightDivideEquals. + * + * Element-by-element right division + * Aij = Aij / Bij + * + * @return Matrix Matrix Aij + */ + public function arrayRightDivideEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $this->A[$i][$j] / $M->get($i, $j); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayLeftDivide. + * + * Element-by-element Left division + * A / B + * + * @return Matrix Division result + */ + public function arrayLeftDivide(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $M->set($i, $j, $M->get($i, $j) / $this->A[$i][$j]); + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * arrayLeftDivideEquals. + * + * Element-by-element Left division + * Aij = Aij / Bij + * + * @return Matrix Matrix Aij + */ + public function arrayLeftDivideEquals(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = $M->get($i, $j) / $this->A[$i][$j]; + } + } + + return $M; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * times. + * + * Matrix multiplication + * + * @return Matrix Product + */ + public function times(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $B = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + if ($this->n == $B->m) { + $C = new self($this->m, $B->n); + for ($j = 0; $j < $B->n; ++$j) { + $Bcolj = []; + for ($k = 0; $k < $this->n; ++$k) { + $Bcolj[$k] = $B->A[$k][$j]; + } + for ($i = 0; $i < $this->m; ++$i) { + $Arowi = $this->A[$i]; + $s = 0; + for ($k = 0; $k < $this->n; ++$k) { + $s += $Arowi[$k] * $Bcolj[$k]; + } + $C->A[$i][$j] = $s; + } + } + + return $C; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + case 'array': + $B = new self($args[0]); + if ($this->n == $B->m) { + $C = new self($this->m, $B->n); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $s = '0'; + for ($k = 0; $k < $C->n; ++$k) { + $s += $this->A[$i][$k] * $B->A[$k][$j]; + } + $C->A[$i][$j] = $s; + } + } + + return $C; + } + + throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); + case 'integer': + $C = new self($this->A); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] *= $args[0]; + } + } + + return $C; + case 'double': + $C = new self($this->m, $this->n); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] = $args[0] * $this->A[$i][$j]; + } + } + + return $C; + case 'float': + $C = new self($this->A); + for ($i = 0; $i < $C->m; ++$i) { + for ($j = 0; $j < $C->n; ++$j) { + $C->A[$i][$j] *= $args[0]; + } + } + + return $C; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } else { + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + } + + /** + * power. + * + * A = A ^ B + * + * @return $this + */ + public function power(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $validValues = true; + $value = $M->get($i, $j); + if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"'); + $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if ($validValues) { + $this->A[$i][$j] = $this->A[$i][$j] ** $value; + } else { + $this->A[$i][$j] = Functions::NAN(); + } + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * concat. + * + * A = A & B + * + * @return $this + */ + public function concat(...$args) + { + if (count($args) > 0) { + $match = implode(',', array_map('gettype', $args)); + + switch ($match) { + case 'object': + if ($args[0] instanceof self) { + $M = $args[0]; + } else { + throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); + } + + break; + case 'array': + $M = new self($args[0]); + + break; + default: + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + + break; + } + $this->checkMatrixDimensions($M); + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $this->A[$i][$j] = trim($this->A[$i][$j], '"') . trim($M->get($i, $j), '"'); + } + } + + return $this; + } + + throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); + } + + /** + * Solve A*X = B. + * + * @param Matrix $B Right hand side + * + * @return Matrix ... Solution if A is square, least squares solution otherwise + */ + public function solve(self $B) + { + if ($this->m == $this->n) { + $LU = new LUDecomposition($this); + + return $LU->solve($B); + } + $QR = new QRDecomposition($this); + + return $QR->solve($B); + } + + /** + * Matrix inverse or pseudoinverse. + * + * @return Matrix ... Inverse(A) if A is square, pseudoinverse otherwise. + */ + public function inverse() + { + return $this->solve($this->identity($this->m, $this->m)); + } + + /** + * det. + * + * Calculate determinant + * + * @return float Determinant + */ + public function det() + { + $L = new LUDecomposition($this); + + return $L->det(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php new file mode 100644 index 0000000..49877b2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php @@ -0,0 +1,31 @@ + abs($b)) { + $r = $b / $a; + $r = abs($a) * sqrt(1 + $r * $r); + } elseif ($b != 0) { + $r = $a / $b; + $r = abs($b) * sqrt(1 + $r * $r); + } else { + $r = 0.0; + } + + return $r; +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE.php new file mode 100644 index 0000000..ca97573 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE.php @@ -0,0 +1,556 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// + +use PhpOffice\PhpSpreadsheet\Exception; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root; + +/* + * Array for storing OLE instances that are accessed from + * OLE_ChainedBlockStream::stream_open(). + * + * @var array + */ +$GLOBALS['_OLE_INSTANCES'] = []; + +/** + * OLE package base class. + * + * @author Xavier Noguer + * @author Christian Schmidt + */ +class OLE +{ + const OLE_PPS_TYPE_ROOT = 5; + const OLE_PPS_TYPE_DIR = 1; + const OLE_PPS_TYPE_FILE = 2; + const OLE_DATA_SIZE_SMALL = 0x1000; + const OLE_LONG_INT_SIZE = 4; + const OLE_PPS_SIZE = 0x80; + + /** + * The file handle for reading an OLE container. + * + * @var resource + */ + public $_file_handle; + + /** + * Array of PPS's found on the OLE container. + * + * @var array + */ + public $_list = []; + + /** + * Root directory of OLE container. + * + * @var Root + */ + public $root; + + /** + * Big Block Allocation Table. + * + * @var array (blockId => nextBlockId) + */ + public $bbat; + + /** + * Short Block Allocation Table. + * + * @var array (blockId => nextBlockId) + */ + public $sbat; + + /** + * Size of big blocks. This is usually 512. + * + * @var int number of octets per block + */ + public $bigBlockSize; + + /** + * Size of small blocks. This is usually 64. + * + * @var int number of octets per block + */ + public $smallBlockSize; + + /** + * Threshold for big blocks. + * + * @var int + */ + public $bigBlockThreshold; + + /** + * Reads an OLE container from the contents of the file given. + * + * @acces public + * + * @param string $filename + * + * @return bool true on success, PEAR_Error on failure + */ + public function read($filename) + { + $fh = fopen($filename, 'rb'); + if (!$fh) { + throw new ReaderException("Can't open file $filename"); + } + $this->_file_handle = $fh; + + $signature = fread($fh, 8); + if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) { + throw new ReaderException("File doesn't seem to be an OLE container."); + } + fseek($fh, 28); + if (fread($fh, 2) != "\xFE\xFF") { + // This shouldn't be a problem in practice + throw new ReaderException('Only Little-Endian encoding is supported.'); + } + // Size of blocks and short blocks in bytes + $this->bigBlockSize = 2 ** self::readInt2($fh); + $this->smallBlockSize = 2 ** self::readInt2($fh); + + // Skip UID, revision number and version number + fseek($fh, 44); + // Number of blocks in Big Block Allocation Table + $bbatBlockCount = self::readInt4($fh); + + // Root chain 1st block + $directoryFirstBlockId = self::readInt4($fh); + + // Skip unused bytes + fseek($fh, 56); + // Streams shorter than this are stored using small blocks + $this->bigBlockThreshold = self::readInt4($fh); + // Block id of first sector in Short Block Allocation Table + $sbatFirstBlockId = self::readInt4($fh); + // Number of blocks in Short Block Allocation Table + $sbbatBlockCount = self::readInt4($fh); + // Block id of first sector in Master Block Allocation Table + $mbatFirstBlockId = self::readInt4($fh); + // Number of blocks in Master Block Allocation Table + $mbbatBlockCount = self::readInt4($fh); + $this->bbat = []; + + // Remaining 4 * 109 bytes of current block is beginning of Master + // Block Allocation Table + $mbatBlocks = []; + for ($i = 0; $i < 109; ++$i) { + $mbatBlocks[] = self::readInt4($fh); + } + + // Read rest of Master Block Allocation Table (if any is left) + $pos = $this->getBlockOffset($mbatFirstBlockId); + for ($i = 0; $i < $mbbatBlockCount; ++$i) { + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) { + $mbatBlocks[] = self::readInt4($fh); + } + // Last block id in each block points to next block + $pos = $this->getBlockOffset(self::readInt4($fh)); + } + + // Read Big Block Allocation Table according to chain specified by $mbatBlocks + for ($i = 0; $i < $bbatBlockCount; ++$i) { + $pos = $this->getBlockOffset($mbatBlocks[$i]); + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) { + $this->bbat[] = self::readInt4($fh); + } + } + + // Read short block allocation table (SBAT) + $this->sbat = []; + $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4; + $sbatFh = $this->getStream($sbatFirstBlockId); + for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) { + $this->sbat[$blockId] = self::readInt4($sbatFh); + } + fclose($sbatFh); + + $this->readPpsWks($directoryFirstBlockId); + + return true; + } + + /** + * @param int $blockId byte offset from beginning of file + * + * @return int + */ + public function getBlockOffset($blockId) + { + return 512 + $blockId * $this->bigBlockSize; + } + + /** + * Returns a stream for use with fread() etc. External callers should + * use \PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream(). + * + * @param int|OLE\PPS $blockIdOrPps block id or PPS + * + * @return resource read-only stream + */ + public function getStream($blockIdOrPps) + { + static $isRegistered = false; + if (!$isRegistered) { + stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class); + $isRegistered = true; + } + + // Store current instance in global array, so that it can be accessed + // in OLE_ChainedBlockStream::stream_open(). + // Object is removed from self::$instances in OLE_Stream::close(). + $GLOBALS['_OLE_INSTANCES'][] = $this; + $keys = array_keys($GLOBALS['_OLE_INSTANCES']); + $instanceId = end($keys); + + $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId; + if ($blockIdOrPps instanceof OLE\PPS) { + $path .= '&blockId=' . $blockIdOrPps->startBlock; + $path .= '&size=' . $blockIdOrPps->Size; + } else { + $path .= '&blockId=' . $blockIdOrPps; + } + + return fopen($path, 'rb'); + } + + /** + * Reads a signed char. + * + * @param resource $fileHandle file handle + * + * @return int + */ + private static function readInt1($fileHandle) + { + [, $tmp] = unpack('c', fread($fileHandle, 1)); + + return $tmp; + } + + /** + * Reads an unsigned short (2 octets). + * + * @param resource $fileHandle file handle + * + * @return int + */ + private static function readInt2($fileHandle) + { + [, $tmp] = unpack('v', fread($fileHandle, 2)); + + return $tmp; + } + + /** + * Reads an unsigned long (4 octets). + * + * @param resource $fileHandle file handle + * + * @return int + */ + private static function readInt4($fileHandle) + { + [, $tmp] = unpack('V', fread($fileHandle, 4)); + + return $tmp; + } + + /** + * Gets information about all PPS's on the OLE container from the PPS WK's + * creates an OLE_PPS object for each one. + * + * @param int $blockId the block id of the first block + * + * @return bool true on success, PEAR_Error on failure + */ + public function readPpsWks($blockId) + { + $fh = $this->getStream($blockId); + for ($pos = 0; true; $pos += 128) { + fseek($fh, $pos, SEEK_SET); + $nameUtf16 = fread($fh, 64); + $nameLength = self::readInt2($fh); + $nameUtf16 = substr($nameUtf16, 0, $nameLength - 2); + // Simple conversion from UTF-16LE to ISO-8859-1 + $name = str_replace("\x00", '', $nameUtf16); + $type = self::readInt1($fh); + switch ($type) { + case self::OLE_PPS_TYPE_ROOT: + $pps = new OLE\PPS\Root(null, null, []); + $this->root = $pps; + + break; + case self::OLE_PPS_TYPE_DIR: + $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []); + + break; + case self::OLE_PPS_TYPE_FILE: + $pps = new OLE\PPS\File($name); + + break; + default: + throw new Exception('Unsupported PPS type'); + } + fseek($fh, 1, SEEK_CUR); + $pps->Type = $type; + $pps->Name = $name; + $pps->PrevPps = self::readInt4($fh); + $pps->NextPps = self::readInt4($fh); + $pps->DirPps = self::readInt4($fh); + fseek($fh, 20, SEEK_CUR); + $pps->Time1st = self::OLE2LocalDate(fread($fh, 8)); + $pps->Time2nd = self::OLE2LocalDate(fread($fh, 8)); + $pps->startBlock = self::readInt4($fh); + $pps->Size = self::readInt4($fh); + $pps->No = count($this->_list); + $this->_list[] = $pps; + + // check if the PPS tree (starting from root) is complete + if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) { + break; + } + } + fclose($fh); + + // Initialize $pps->children on directories + foreach ($this->_list as $pps) { + if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) { + $nos = [$pps->DirPps]; + $pps->children = []; + while ($nos) { + $no = array_pop($nos); + if ($no != -1) { + $childPps = $this->_list[$no]; + $nos[] = $childPps->PrevPps; + $nos[] = $childPps->NextPps; + $pps->children[] = $childPps; + } + } + } + } + + return true; + } + + /** + * It checks whether the PPS tree is complete (all PPS's read) + * starting with the given PPS (not necessarily root). + * + * @param int $index The index of the PPS from which we are checking + * + * @return bool Whether the PPS tree for the given PPS is complete + */ + private function ppsTreeComplete($index) + { + return isset($this->_list[$index]) && + ($pps = $this->_list[$index]) && + ($pps->PrevPps == -1 || + $this->ppsTreeComplete($pps->PrevPps)) && + ($pps->NextPps == -1 || + $this->ppsTreeComplete($pps->NextPps)) && + ($pps->DirPps == -1 || + $this->ppsTreeComplete($pps->DirPps)); + } + + /** + * Checks whether a PPS is a File PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index The index for the PPS + * + * @return bool true if it's a File PPS, false otherwise + */ + public function isFile($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE; + } + + return false; + } + + /** + * Checks whether a PPS is a Root PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index the index for the PPS + * + * @return bool true if it's a Root PPS, false otherwise + */ + public function isRoot($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT; + } + + return false; + } + + /** + * Gives the total number of PPS's found in the OLE container. + * + * @return int The total number of PPS's found in the OLE container + */ + public function ppsTotal() + { + return count($this->_list); + } + + /** + * Gets data from a PPS + * If there is no PPS for the index given, it will return an empty string. + * + * @param int $index The index for the PPS + * @param int $position The position from which to start reading + * (relative to the PPS) + * @param int $length The amount of bytes to read (at most) + * + * @return string The binary string containing the data requested + * + * @see OLE_PPS_File::getStream() + */ + public function getData($index, $position, $length) + { + // if position is not valid return empty string + if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) { + return ''; + } + $fh = $this->getStream($this->_list[$index]); + $data = stream_get_contents($fh, $length, $position); + fclose($fh); + + return $data; + } + + /** + * Gets the data length from a PPS + * If there is no PPS for the index given, it will return 0. + * + * @param int $index The index for the PPS + * + * @return int The amount of bytes in data the PPS has + */ + public function getDataLength($index) + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Size; + } + + return 0; + } + + /** + * Utility function to transform ASCII text to Unicode. + * + * @param string $ascii The ASCII string to transform + * + * @return string The string in Unicode + */ + public static function ascToUcs($ascii) + { + $rawname = ''; + $iMax = strlen($ascii); + for ($i = 0; $i < $iMax; ++$i) { + $rawname .= $ascii[$i] + . "\x00"; + } + + return $rawname; + } + + /** + * Utility function + * Returns a string for the OLE container with the date given. + * + * @param float|int $date A timestamp + * + * @return string The string for the OLE container + */ + public static function localDateToOLE($date) + { + if (!$date) { + return "\x00\x00\x00\x00\x00\x00\x00\x00"; + } + $dateTime = Date::dateTimeFromTimestamp("$date"); + + // days from 1-1-1601 until the beggining of UNIX era + $days = 134774; + // calculate seconds + $big_date = $days * 24 * 3600 + (float) $dateTime->format('U'); + // multiply just to make MS happy + $big_date *= 10000000; + + // Make HEX string + $res = ''; + + $factor = 2 ** 56; + while ($factor >= 1) { + $hex = (int) floor($big_date / $factor); + $res = pack('c', $hex) . $res; + $big_date = fmod($big_date, $factor); + $factor /= 256; + } + + return $res; + } + + /** + * Returns a timestamp from an OLE container's date. + * + * @param string $oleTimestamp A binary string with the encoded date + * + * @return float|int The Unix timestamp corresponding to the string + */ + public static function OLE2LocalDate($oleTimestamp) + { + if (strlen($oleTimestamp) != 8) { + throw new ReaderException('Expecting 8 byte string'); + } + + // convert to units of 100 ns since 1601: + $unpackedTimestamp = unpack('v4', $oleTimestamp); + $timestampHigh = (float) $unpackedTimestamp[4] * 65536 + (float) $unpackedTimestamp[3]; + $timestampLow = (float) $unpackedTimestamp[2] * 65536 + (float) $unpackedTimestamp[1]; + + // translate to seconds since 1601: + $timestampHigh /= 10000000; + $timestampLow /= 10000000; + + // days from 1601 to 1970: + $days = 134774; + + // translate to seconds since 1970: + $unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5); + + return IntOrFloat::evaluate($unixTimestamp); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/File.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/File.php new file mode 100644 index 0000000..dd1cda2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/File.php @@ -0,0 +1,64 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; + +/** + * Class for creating File PPS's for OLE containers. + * + * @author Xavier Noguer + */ +class File extends PPS +{ + /** + * The constructor. + * + * @param string $name The name of the file (in Unicode) + * + * @see OLE::ascToUcs() + */ + public function __construct($name) + { + parent::__construct(null, $name, OLE::OLE_PPS_TYPE_FILE, null, null, null, null, null, '', []); + } + + /** + * Initialization method. Has to be called right after OLE_PPS_File(). + * + * @return mixed true on success + */ + public function init() + { + return true; + } + + /** + * Append data to PPS. + * + * @param string $data The data to append + */ + public function append($data): void + { + $this->_data .= $data; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php new file mode 100644 index 0000000..91f1d04 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php @@ -0,0 +1,347 @@ +data = file_get_contents($filename, false, null, 0, 8); + + // Check OLE identifier + $identifierOle = pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1); + if ($this->data != $identifierOle) { + throw new ReaderException('The filename ' . $filename . ' is not recognised as an OLE file'); + } + + // Get the file data + $this->data = file_get_contents($filename); + + // Total number of sectors used for the SAT + $this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS); + + // SecID of the first sector of the directory stream + $this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS); + + // SecID of the first sector of the SSAT (or -2 if not extant) + $this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS); + + // SecID of the first sector of the MSAT (or -2 if no additional sectors are used) + $this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS); + + // Total number of sectors used by MSAT + $this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS); + + $bigBlockDepotBlocks = []; + $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; + + $bbdBlocks = $this->numBigBlockDepotBlocks; + + if ($this->numExtensionBlocks != 0) { + $bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4; + } + + for ($i = 0; $i < $bbdBlocks; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + for ($j = 0; $j < $this->numExtensionBlocks; ++$j) { + $pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE; + $blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1); + + for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + $bbdBlocks += $blocksToRead; + if ($bbdBlocks < $this->numBigBlockDepotBlocks) { + $this->extensionBlock = self::getInt4d($this->data, $pos); + } + } + + $pos = 0; + $this->bigBlockChain = ''; + $bbs = self::BIG_BLOCK_SIZE / 4; + for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) { + $pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE; + + $this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + } + + $pos = 0; + $sbdBlock = $this->sbdStartBlock; + $this->smallBlockChain = ''; + while ($sbdBlock != -2) { + $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; + + $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + + $sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4); + } + + // read the directory stream + $block = $this->rootStartBlock; + $this->entry = $this->readData($block); + + $this->readPropertySets(); + } + + /** + * Extract binary stream data. + * + * @param int $stream + * + * @return null|string + */ + public function getStream($stream) + { + if ($stream === null) { + return null; + } + + $streamData = ''; + + if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) { + $rootdata = $this->readData($this->props[$this->rootentry]['startBlock']); + + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = $block * self::SMALL_BLOCK_SIZE; + $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); + + $block = self::getInt4d($this->smallBlockChain, $block * 4); + } + + return $streamData; + } + $numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; + if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { + ++$numBlocks; + } + + if ($numBlocks == 0) { + return ''; + } + + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $streamData; + } + + /** + * Read a standard stream (by joining sectors using information from SAT). + * + * @param int $block Sector ID where the stream starts + * + * @return string Data for standard stream + */ + private function readData($block) + { + $data = ''; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $data; + } + + /** + * Read entries in the directory stream. + */ + private function readPropertySets(): void + { + $offset = 0; + + // loop through entires, each entry is 128 bytes + $entryLen = strlen($this->entry); + while ($offset < $entryLen) { + // entry data (128 bytes) + $d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE); + + // size in bytes of name + $nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8); + + // type of entry + $type = ord($d[self::TYPE_POS]); + + // sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook) + // sectorID of first sector of the short-stream container stream, if this entry is root entry + $startBlock = self::getInt4d($d, self::START_BLOCK_POS); + + $size = self::getInt4d($d, self::SIZE_POS); + + $name = str_replace("\x00", '', substr($d, 0, $nameSize)); + + $this->props[] = [ + 'name' => $name, + 'type' => $type, + 'startBlock' => $startBlock, + 'size' => $size, + ]; + + // tmp helper to simplify checks + $upName = strtoupper($name); + + // Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook) + if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) { + $this->wrkbook = count($this->props) - 1; + } elseif ($upName === 'ROOT ENTRY' || $upName === 'R') { + // Root entry + $this->rootentry = count($this->props) - 1; + } + + // Summary information + if ($name == chr(5) . 'SummaryInformation') { + $this->summaryInformation = count($this->props) - 1; + } + + // Additional Document Summary information + if ($name == chr(5) . 'DocumentSummaryInformation') { + $this->documentSummaryInformation = count($this->props) - 1; + } + + $offset += self::PROPERTY_STORAGE_BLOCK_SIZE; + } + } + + /** + * Read 4 bytes of data at specified position. + * + * @param string $data + * @param int $pos + * + * @return int + */ + private static function getInt4d($data, $pos) + { + if ($pos < 0) { + // Invalid position + throw new ReaderException('Parameter pos=' . $pos . ' is invalid.'); + } + + $len = strlen($data); + if ($len < $pos + 4) { + $data .= str_repeat("\0", $pos + 4 - $len); + } + + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/PasswordHasher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/PasswordHasher.php new file mode 100644 index 0000000..0d58a86 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/PasswordHasher.php @@ -0,0 +1,109 @@ + 'md2', + Protection::ALGORITHM_MD4 => 'md4', + Protection::ALGORITHM_MD5 => 'md5', + Protection::ALGORITHM_SHA_1 => 'sha1', + Protection::ALGORITHM_SHA_256 => 'sha256', + Protection::ALGORITHM_SHA_384 => 'sha384', + Protection::ALGORITHM_SHA_512 => 'sha512', + Protection::ALGORITHM_RIPEMD_128 => 'ripemd128', + Protection::ALGORITHM_RIPEMD_160 => 'ripemd160', + Protection::ALGORITHM_WHIRLPOOL => 'whirlpool', + ]; + + if (array_key_exists($algorithmName, $mapping)) { + return $mapping[$algorithmName]; + } + + throw new SpException('Unsupported password algorithm: ' . $algorithmName); + } + + /** + * Create a password hash from a given string. + * + * This method is based on the spec at: + * https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf + * 2.3.7.1 Binary Document Password Verifier Derivation Method 1 + * + * It replaces a method based on the algorithm provided by + * Daniel Rentz of OpenOffice and the PEAR package + * Spreadsheet_Excel_Writer by Xavier Noguer . + * + * Scrutinizer will squawk at the use of bitwise operations here, + * but it should ultimately pass. + * + * @param string $password Password to hash + */ + private static function defaultHashPassword(string $password): string + { + $verifier = 0; + $pwlen = strlen($password); + $passwordArray = pack('c', $pwlen) . $password; + for ($i = $pwlen; $i >= 0; --$i) { + $intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1; + $intermediate2 = 2 * $verifier; + $intermediate2 = $intermediate2 & 0x7fff; + $intermediate3 = $intermediate1 | $intermediate2; + $verifier = $intermediate3 ^ ord($passwordArray[$i]); + } + $verifier ^= 0xCE4B; + + return strtoupper(dechex($verifier)); + } + + /** + * Create a password hash from a given string by a specific algorithm. + * + * 2.4.2.4 ISO Write Protection Method + * + * @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f + * + * @param string $password Password to hash + * @param string $algorithm Hash algorithm used to compute the password hash value + * @param string $salt Pseudorandom string + * @param int $spinCount Number of times to iterate on a hash of a password + * + * @return string Hashed password + */ + public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string + { + if (strlen($password) > self::MAX_PASSWORD_LENGTH) { + throw new SpException('Password exceeds ' . self::MAX_PASSWORD_LENGTH . ' characters'); + } + $phpAlgorithm = self::getAlgorithm($algorithm); + if (!$phpAlgorithm) { + return self::defaultHashPassword($password); + } + + $saltValue = base64_decode($salt); + $encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + + $hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true); + for ($i = 0; $i < $spinCount; ++$i) { + $hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true); + } + + return base64_encode($hashValue); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php new file mode 100644 index 0000000..eb8cd74 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -0,0 +1,119 @@ +getIntersect() * $this->getSlope() ** ($xValue - $this->xOffset); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return log(($yValue + $this->yOffset) / $this->getIntersect()) / log($this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' * ' . $slope . '^X'; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getSlope($dp = 0) + { + if ($dp != 0) { + return round(exp($this->slope), $dp); + } + + return exp($this->slope); + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + * + * @return float + */ + public function getIntersect($dp = 0) + { + if ($dp != 0) { + return round(exp($this->intersect), $dp); + } + + return exp($this->intersect); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function exponentialRegression(array $yValues, array $xValues, bool $const): void + { + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); + + $this->leastSquareFit($adjustedYValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->exponentialRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php new file mode 100644 index 0000000..65d6b4f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -0,0 +1,80 @@ +getIntersect() + $this->getSlope() * $xValue; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return ($yValue - $this->getIntersect()) / $this->getSlope(); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' + ' . $slope . ' * X'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function linearRegression(array $yValues, array $xValues, bool $const): void + { + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->linearRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php new file mode 100644 index 0000000..2366dc6 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -0,0 +1,87 @@ +getIntersect() + $this->getSlope() * log($xValue - $this->xOffset); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY($yValue) + { + return exp(($yValue - $this->getIntersect()) / $this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + * + * @return string + */ + public function getEquation($dp = 0) + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function logarithmicRegression(array $yValues, array $xValues, bool $const): void + { + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); + + $this->leastSquareFit($adjustedYValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + * @param bool $const + */ + public function __construct($yValues, $xValues = [], $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->logarithmicRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php new file mode 100644 index 0000000..3f063fe --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php @@ -0,0 +1,78 @@ + false, + 'unique' => true, + ]; + + /** + * @var bool + */ + protected $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + public function getConditional(): Conditional + { + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_UNIQUE : Conditional::CONDITION_DUPLICATES + ); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_DUPLICATES && + $conditional->getConditionType() !== Conditional::CONDITION_UNIQUE + ) { + throw new Exception('Conditional is not a Duplicates CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_UNIQUE; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php new file mode 100644 index 0000000..56b9086 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php @@ -0,0 +1,95 @@ + false, + 'isError' => true, + ]; + + protected const EXPRESSIONS = [ + Wizard::NOT_ERRORS => 'NOT(ISERROR(%s))', + Wizard::ERRORS => 'ISERROR(%s)', + ]; + + /** + * @var bool + */ + protected $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + protected function getExpression(): void + { + $this->expression = sprintf( + self::EXPRESSIONS[$this->inverse ? Wizard::ERRORS : Wizard::NOT_ERRORS], + $this->referenceCell + ); + } + + public function getConditional(): Conditional + { + $this->getExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_CONTAINSERRORS : Conditional::CONDITION_NOTCONTAINSERRORS + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_CONTAINSERRORS && + $conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSERRORS + ) { + throw new Exception('Conditional is not an Errors CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSERRORS; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php new file mode 100644 index 0000000..8c8c7f2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php @@ -0,0 +1,73 @@ +validateOperand($expression, Wizard::VALUE_TYPE_FORMULA); + $this->expression = $expression; + + return $this; + } + + public function getConditional(): Conditional + { + $expression = $this->adjustConditionsForCellReferences([$this->expression]); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_EXPRESSION); + $conditional->setConditions($expression); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_EXPRESSION) { + throw new Exception('Conditional is not an Expression CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->expression = self::reverseAdjustCellRef($conditional->getConditions()[0], $cellRange); + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if ($methodName !== 'formula') { + throw new Exception('Invalid Operation for Expression CF Rule Wizard'); + } + + $this->expression(...$arguments); + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php new file mode 100644 index 0000000..bd87a79 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php @@ -0,0 +1,347 @@ +fillType = null; + } + $this->startColor = new Color(Color::COLOR_WHITE, $isSupervisor, $isConditional); + $this->endColor = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->startColor->bindParent($this, 'startColor'); + $this->endColor->bindParent($this, 'endColor'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Fill + */ + public function getSharedComponent() + { + /** @var Style */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getFill(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['fill' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFill()->applyFromArray( + * [ + * 'fillType' => Fill::FILL_GRADIENT_LINEAR, + * 'rotation' => 0.0, + * 'startColor' => [ + * 'rgb' => '000000' + * ], + * 'endColor' => [ + * 'argb' => 'FFFFFFFF' + * ] + * ] + * ); + * + * + * @param array $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['fillType'])) { + $this->setFillType($styleArray['fillType']); + } + if (isset($styleArray['rotation'])) { + $this->setRotation($styleArray['rotation']); + } + if (isset($styleArray['startColor'])) { + $this->getStartColor()->applyFromArray($styleArray['startColor']); + } + if (isset($styleArray['endColor'])) { + $this->getEndColor()->applyFromArray($styleArray['endColor']); + } + if (isset($styleArray['color'])) { + $this->getStartColor()->applyFromArray($styleArray['color']); + $this->getEndColor()->applyFromArray($styleArray['color']); + } + } + + return $this; + } + + /** + * Get Fill Type. + * + * @return null|string + */ + public function getFillType() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFillType(); + } + + return $this->fillType; + } + + /** + * Set Fill Type. + * + * @param string $fillType Fill type, see self::FILL_* + * + * @return $this + */ + public function setFillType($fillType) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['fillType' => $fillType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->fillType = $fillType; + } + + return $this; + } + + /** + * Get Rotation. + * + * @return float + */ + public function getRotation() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getRotation(); + } + + return $this->rotation; + } + + /** + * Set Rotation. + * + * @param float $angleInDegrees + * + * @return $this + */ + public function setRotation($angleInDegrees) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['rotation' => $angleInDegrees]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->rotation = $angleInDegrees; + } + + return $this; + } + + /** + * Get Start Color. + * + * @return Color + */ + public function getStartColor() + { + return $this->startColor; + } + + /** + * Set Start Color. + * + * @return $this + */ + public function setStartColor(Color $color) + { + $this->colorChanged = true; + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getStartColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->startColor = $color; + } + + return $this; + } + + /** + * Get End Color. + * + * @return Color + */ + public function getEndColor() + { + return $this->endColor; + } + + /** + * Set End Color. + * + * @return $this + */ + public function setEndColor(Color $color) + { + $this->colorChanged = true; + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getEndColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->endColor = $color; + } + + return $this; + } + + public function getColorsChanged(): bool + { + if ($this->isSupervisor) { + $changed = $this->getSharedComponent()->colorChanged; + } else { + $changed = $this->colorChanged; + } + + return $changed || $this->startColor->getHasChanged() || $this->endColor->getHasChanged(); + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + // Note that we don't care about colours for fill type NONE, but could have duplicate NONEs with + // different hashes if we don't explicitly prevent this + return md5( + $this->getFillType() . + $this->getRotation() . + ($this->getFillType() !== self::FILL_NONE ? $this->getStartColor()->getHashCode() : '') . + ($this->getFillType() !== self::FILL_NONE ? $this->getEndColor()->getHashCode() : '') . + ((string) $this->getColorsChanged()) . + __CLASS__ + ); + } + + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'fillType', $this->getFillType()); + $this->exportArray2($exportedArray, 'rotation', $this->getRotation()); + if ($this->getColorsChanged()) { + $this->exportArray2($exportedArray, 'endColor', $this->getEndColor()); + $this->exportArray2($exportedArray, 'startColor', $this->getStartColor()); + } + + return $exportedArray; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Font.php new file mode 100644 index 0000000..13fe2b6 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Font.php @@ -0,0 +1,568 @@ +name = null; + $this->size = null; + $this->bold = null; + $this->italic = null; + $this->superscript = null; + $this->subscript = null; + $this->underline = null; + $this->strikethrough = null; + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + } else { + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor); + } + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->color->bindParent($this, 'color'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return Font + */ + public function getSharedComponent() + { + /** @var Style */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getFont(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['font' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->applyFromArray( + * [ + * 'name' => 'Arial', + * 'bold' => TRUE, + * 'italic' => FALSE, + * 'underline' => \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE, + * 'strikethrough' => FALSE, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ); + * + * + * @param array $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['name'])) { + $this->setName($styleArray['name']); + } + if (isset($styleArray['bold'])) { + $this->setBold($styleArray['bold']); + } + if (isset($styleArray['italic'])) { + $this->setItalic($styleArray['italic']); + } + if (isset($styleArray['superscript'])) { + $this->setSuperscript($styleArray['superscript']); + } + if (isset($styleArray['subscript'])) { + $this->setSubscript($styleArray['subscript']); + } + if (isset($styleArray['underline'])) { + $this->setUnderline($styleArray['underline']); + } + if (isset($styleArray['strikethrough'])) { + $this->setStrikethrough($styleArray['strikethrough']); + } + if (isset($styleArray['color'])) { + $this->getColor()->applyFromArray($styleArray['color']); + } + if (isset($styleArray['size'])) { + $this->setSize($styleArray['size']); + } + } + + return $this; + } + + /** + * Get Name. + * + * @return null|string + */ + public function getName() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getName(); + } + + return $this->name; + } + + /** + * Set Name. + * + * @param string $fontname + * + * @return $this + */ + public function setName($fontname) + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['name' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->name = $fontname; + } + + return $this; + } + + /** + * Get Size. + * + * @return null|float + */ + public function getSize() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSize(); + } + + return $this->size; + } + + /** + * Set Size. + * + * @param mixed $sizeInPoints A float representing the value of a positive measurement in points (1/72 of an inch) + * + * @return $this + */ + public function setSize($sizeInPoints) + { + if (is_string($sizeInPoints) || is_int($sizeInPoints)) { + $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric + } + + // Size must be a positive floating point number + // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 + if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { + $sizeInPoints = 10.0; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['size' => $sizeInPoints]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->size = $sizeInPoints; + } + + return $this; + } + + /** + * Get Bold. + * + * @return null|bool + */ + public function getBold() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBold(); + } + + return $this->bold; + } + + /** + * Set Bold. + * + * @param bool $bold + * + * @return $this + */ + public function setBold($bold) + { + if ($bold == '') { + $bold = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['bold' => $bold]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->bold = $bold; + } + + return $this; + } + + /** + * Get Italic. + * + * @return null|bool + */ + public function getItalic() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getItalic(); + } + + return $this->italic; + } + + /** + * Set Italic. + * + * @param bool $italic + * + * @return $this + */ + public function setItalic($italic) + { + if ($italic == '') { + $italic = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['italic' => $italic]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->italic = $italic; + } + + return $this; + } + + /** + * Get Superscript. + * + * @return null|bool + */ + public function getSuperscript() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSuperscript(); + } + + return $this->superscript; + } + + /** + * Set Superscript. + * + * @return $this + */ + public function setSuperscript(bool $superscript) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['superscript' => $superscript]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->superscript = $superscript; + if ($this->superscript) { + $this->subscript = false; + } + } + + return $this; + } + + /** + * Get Subscript. + * + * @return null|bool + */ + public function getSubscript() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSubscript(); + } + + return $this->subscript; + } + + /** + * Set Subscript. + * + * @return $this + */ + public function setSubscript(bool $subscript) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['subscript' => $subscript]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->subscript = $subscript; + if ($this->subscript) { + $this->superscript = false; + } + } + + return $this; + } + + /** + * Get Underline. + * + * @return null|string + */ + public function getUnderline() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUnderline(); + } + + return $this->underline; + } + + /** + * Set Underline. + * + * @param bool|string $underlineStyle \PhpOffice\PhpSpreadsheet\Style\Font underline type + * If a boolean is passed, then TRUE equates to UNDERLINE_SINGLE, + * false equates to UNDERLINE_NONE + * + * @return $this + */ + public function setUnderline($underlineStyle) + { + if (is_bool($underlineStyle)) { + $underlineStyle = ($underlineStyle) ? self::UNDERLINE_SINGLE : self::UNDERLINE_NONE; + } elseif ($underlineStyle == '') { + $underlineStyle = self::UNDERLINE_NONE; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['underline' => $underlineStyle]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->underline = $underlineStyle; + } + + return $this; + } + + /** + * Get Strikethrough. + * + * @return null|bool + */ + public function getStrikethrough() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikethrough(); + } + + return $this->strikethrough; + } + + /** + * Set Strikethrough. + * + * @param bool $strikethru + * + * @return $this + */ + public function setStrikethrough($strikethru) + { + if ($strikethru == '') { + $strikethru = false; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikethrough' => $strikethru]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikethrough = $strikethru; + } + + return $this; + } + + /** + * Get Color. + * + * @return Color + */ + public function getColor() + { + return $this->color; + } + + /** + * Set Color. + * + * @return $this + */ + public function setColor(Color $color) + { + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->color = $color; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->name . + $this->size . + ($this->bold ? 't' : 'f') . + ($this->italic ? 't' : 'f') . + ($this->superscript ? 't' : 'f') . + ($this->subscript ? 't' : 'f') . + $this->underline . + ($this->strikethrough ? 't' : 'f') . + $this->color->getHashCode() . + __CLASS__ + ); + } + + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'name', $this->getName()); + $this->exportArray2($exportedArray, 'size', $this->getSize()); + $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); + $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); + $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); + + return $exportedArray; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php new file mode 100644 index 0000000..6f552cb --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php @@ -0,0 +1,414 @@ +formatCode = null; + $this->builtInFormatCode = false; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + * + * @return NumberFormat + */ + public function getSharedComponent() + { + /** @var Style */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getNumberFormat(); + } + + /** + * Build style array from subcomponents. + * + * @param array $array + * + * @return array + */ + public function getStyleArray($array) + { + return ['numberFormat' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray( + * [ + * 'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE + * ] + * ); + * + * + * @param array $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray) + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['formatCode'])) { + $this->setFormatCode($styleArray['formatCode']); + } + } + + return $this; + } + + /** + * Get Format Code. + * + * @return null|string + */ + public function getFormatCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFormatCode(); + } + if (is_int($this->builtInFormatCode)) { + return self::builtInFormatCode($this->builtInFormatCode); + } + + return $this->formatCode; + } + + /** + * Set Format Code. + * + * @param string $formatCode see self::FORMAT_* + * + * @return $this + */ + public function setFormatCode($formatCode) + { + if ($formatCode == '') { + $formatCode = self::FORMAT_GENERAL; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => $formatCode]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->formatCode = $formatCode; + $this->builtInFormatCode = self::builtInFormatCodeIndex($formatCode); + } + + return $this; + } + + /** + * Get Built-In Format Code. + * + * @return false|int + */ + public function getBuiltInFormatCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBuiltInFormatCode(); + } + + return $this->builtInFormatCode; + } + + /** + * Set Built-In Format Code. + * + * @param int $formatCodeIndex + * + * @return $this + */ + public function setBuiltInFormatCode($formatCodeIndex) + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($formatCodeIndex)]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->builtInFormatCode = $formatCodeIndex; + $this->formatCode = self::builtInFormatCode($formatCodeIndex); + } + + return $this; + } + + /** + * Fill built-in format codes. + */ + private static function fillBuiltInFormatCodes(): void + { + // [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance] + // 18.8.30. numFmt (Number Format) + // + // The ECMA standard defines built-in format IDs + // 14: "mm-dd-yy" + // 22: "m/d/yy h:mm" + // 37: "#,##0 ;(#,##0)" + // 38: "#,##0 ;[Red](#,##0)" + // 39: "#,##0.00;(#,##0.00)" + // 40: "#,##0.00;[Red](#,##0.00)" + // 47: "mmss.0" + // KOR fmt 55: "yyyy-mm-dd" + // Excel defines built-in format IDs + // 14: "m/d/yyyy" + // 22: "m/d/yyyy h:mm" + // 37: "#,##0_);(#,##0)" + // 38: "#,##0_);[Red](#,##0)" + // 39: "#,##0.00_);(#,##0.00)" + // 40: "#,##0.00_);[Red](#,##0.00)" + // 47: "mm:ss.0" + // KOR fmt 55: "yyyy/mm/dd" + + // Built-in format codes + if (self::$builtInFormats === null) { + self::$builtInFormats = []; + + // General + self::$builtInFormats[0] = self::FORMAT_GENERAL; + self::$builtInFormats[1] = '0'; + self::$builtInFormats[2] = '0.00'; + self::$builtInFormats[3] = '#,##0'; + self::$builtInFormats[4] = '#,##0.00'; + + self::$builtInFormats[9] = '0%'; + self::$builtInFormats[10] = '0.00%'; + self::$builtInFormats[11] = '0.00E+00'; + self::$builtInFormats[12] = '# ?/?'; + self::$builtInFormats[13] = '# ??/??'; + self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy'; + self::$builtInFormats[15] = 'd-mmm-yy'; + self::$builtInFormats[16] = 'd-mmm'; + self::$builtInFormats[17] = 'mmm-yy'; + self::$builtInFormats[18] = 'h:mm AM/PM'; + self::$builtInFormats[19] = 'h:mm:ss AM/PM'; + self::$builtInFormats[20] = 'h:mm'; + self::$builtInFormats[21] = 'h:mm:ss'; + self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm'; + + self::$builtInFormats[37] = '#,##0_);(#,##0)'; // Despite ECMA '#,##0 ;(#,##0)'; + self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; // Despite ECMA '#,##0 ;[Red](#,##0)'; + self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; // Despite ECMA '#,##0.00;(#,##0.00)'; + self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; // Despite ECMA '#,##0.00;[Red](#,##0.00)'; + + self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)'; + self::$builtInFormats[45] = 'mm:ss'; + self::$builtInFormats[46] = '[h]:mm:ss'; + self::$builtInFormats[47] = 'mm:ss.0'; // Despite ECMA 'mmss.0'; + self::$builtInFormats[48] = '##0.0E+0'; + self::$builtInFormats[49] = '@'; + + // CHT + self::$builtInFormats[27] = '[$-404]e/m/d'; + self::$builtInFormats[30] = 'm/d/yy'; + self::$builtInFormats[36] = '[$-404]e/m/d'; + self::$builtInFormats[50] = '[$-404]e/m/d'; + self::$builtInFormats[57] = '[$-404]e/m/d'; + + // THA + self::$builtInFormats[59] = 't0'; + self::$builtInFormats[60] = 't0.00'; + self::$builtInFormats[61] = 't#,##0'; + self::$builtInFormats[62] = 't#,##0.00'; + self::$builtInFormats[67] = 't0%'; + self::$builtInFormats[68] = 't0.00%'; + self::$builtInFormats[69] = 't# ?/?'; + self::$builtInFormats[70] = 't# ??/??'; + + // JPN + self::$builtInFormats[28] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[29] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[31] = 'yyyy"年"m"月"d"日"'; + self::$builtInFormats[32] = 'h"時"mm"分"'; + self::$builtInFormats[33] = 'h"時"mm"分"ss"秒"'; + self::$builtInFormats[34] = 'yyyy"年"m"月"'; + self::$builtInFormats[35] = 'm"月"d"日"'; + self::$builtInFormats[51] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[52] = 'yyyy"年"m"月"'; + self::$builtInFormats[53] = 'm"月"d"日"'; + self::$builtInFormats[54] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[55] = 'yyyy"年"m"月"'; + self::$builtInFormats[56] = 'm"月"d"日"'; + self::$builtInFormats[58] = '[$-411]ggge"年"m"月"d"日"'; + + // Flip array (for faster lookups) + self::$flippedBuiltInFormats = array_flip(self::$builtInFormats); + } + } + + /** + * Get built-in format code. + * + * @param int $index + * + * @return string + */ + public static function builtInFormatCode($index) + { + // Clean parameter + $index = (int) $index; + + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (isset(self::$builtInFormats[$index])) { + return self::$builtInFormats[$index]; + } + + return ''; + } + + /** + * Get built-in format code index. + * + * @param string $formatCodeIndex + * + * @return false|int + */ + public static function builtInFormatCodeIndex($formatCodeIndex) + { + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (array_key_exists($formatCodeIndex, self::$flippedBuiltInFormats)) { + return self::$flippedBuiltInFormats[$formatCodeIndex]; + } + + return false; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->formatCode . + $this->builtInFormatCode . + __CLASS__ + ); + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param mixed $value Value to format + * @param string $format Format code, see = self::FORMAT_* + * @param array $callBack Callback function for additional formatting of string + * + * @return string Formatted string + */ + public static function toFormattedString($value, $format, $callBack = null) + { + return NumberFormat\Formatter::toFormattedString($value, $format, $callBack); + } + + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'formatCode', $this->getFormatCode()); + + return $exportedArray; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php new file mode 100644 index 0000000..01407e6 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -0,0 +1,162 @@ +': + return $value > $val; + + case '<': + return $value < $val; + + case '<=': + return $value <= $val; + + case '<>': + return $value != $val; + + case '=': + return $value == $val; + } + + return $value >= $val; + } + + private static function splitFormat($sections, $value) + { + // Extract the relevant section depending on whether number is positive, negative, or zero? + // Text not supported yet. + // Here is how the sections apply to various values in Excel: + // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] + // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE] + // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] + // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] + $cnt = count($sections); + $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/mui'; + $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/'; + $colors = ['', '', '', '', '']; + $condops = ['', '', '', '', '']; + $condvals = [0, 0, 0, 0, 0]; + for ($idx = 0; $idx < $cnt; ++$idx) { + if (preg_match($color_regex, $sections[$idx], $matches)) { + $colors[$idx] = $matches[0]; + $sections[$idx] = preg_replace($color_regex, '', $sections[$idx]); + } + if (preg_match($cond_regex, $sections[$idx], $matches)) { + $condops[$idx] = $matches[1]; + $condvals[$idx] = $matches[2]; + $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); + } + } + $color = $colors[0]; + $format = $sections[0]; + $absval = $value; + switch ($cnt) { + case 2: + $absval = abs($value); + if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>=', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } + + break; + case 3: + case 4: + $absval = abs($value); + if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>', 0)) { + if (self::splitFormatCompare($value, $condops[1], $condvals[1], '<', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } else { + $color = $colors[2]; + $format = $sections[2]; + } + } + + break; + } + + return [$color, $format, $absval]; + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param mixed $value Value to format + * @param string $format Format code, see = NumberFormat::FORMAT_* + * @param array $callBack Callback function for additional formatting of string + * + * @return string Formatted string + */ + public static function toFormattedString($value, $format, $callBack = null) + { + // For now we do not treat strings although section 4 of a format code affects strings + if (!is_numeric($value)) { + return $value; + } + + // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, + // it seems to round numbers to a total of 10 digits. + if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) { + return $value; + } + + $format = preg_replace_callback( + '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', + function ($matches) { + return str_replace('.', chr(0x00), $matches[0]); + }, + $format + ); + + // Convert any other escaped characters to quoted strings, e.g. (\T to "T") + $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); + + // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) + $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); + + [$colors, $format, $value] = self::splitFormat($sections, $value); + + // In Excel formats, "_" is used to add spacing, + // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space + $format = preg_replace('/_.?/ui', ' ', $format); + + // Let's begin inspecting the format and converting the value to a formatted string + + // Check for date/time characters (not inside quotes) + if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) { + // datetime format + $value = DateFormatter::format($value, $format); + } else { + if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"' && substr_count($format, '"') === 2) { + $value = substr($format, 1, -1); + } elseif (preg_match('/[0#, ]%/', $format)) { + // % number format + $value = PercentageFormatter::format($value, $format); + } else { + $value = NumberFormatter::format($value, $format); + } + } + + // Additional formatting provided by callback function + if ($callBack !== null) { + [$writerInstance, $function] = $callBack; + $value = $writerInstance->$function($value, $colors); + } + + $value = str_replace(chr(0x00), '.', $value); + + return $value; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php new file mode 100644 index 0000000..46f27cc --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -0,0 +1,64 @@ + 0); + + return [ + implode('.', $masks), + implode('.', array_reverse($postDecimalMasks)), + ]; + } + + /** + * @param mixed $number + */ + private static function processComplexNumberFormatMask($number, string $mask): string + { + /** @var string */ + $result = $number; + $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); + + if ($maskingBlockCount > 1) { + $maskingBlocks = array_reverse($maskingBlocks[0]); + + $offset = 0; + foreach ($maskingBlocks as $block) { + $size = strlen($block[0]); + $divisor = 10 ** $size; + $offset = $block[1]; + + /** @var float */ + $numberFloat = $number; + $blockValue = sprintf("%0{$size}d", fmod($numberFloat, $divisor)); + $number = floor($numberFloat / $divisor); + $mask = substr_replace($mask, $blockValue, $offset, $size); + } + /** @var string */ + $numberString = $number; + if ($number > 0) { + $mask = substr_replace($mask, $numberString, $offset, 0); + } + $result = $mask; + } + + return self::makeString($result); + } + + /** + * @param mixed $number + */ + private static function complexNumberFormatMask($number, string $mask, bool $splitOnPoint = true): string + { + $sign = ($number < 0.0) ? '-' : ''; + /** @var float */ + $numberFloat = $number; + $number = (string) abs($numberFloat); + + if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) { + $numbers = explode('.', $number); + $masks = explode('.', $mask); + if (count($masks) > 2) { + $masks = self::mergeComplexNumberFormatMasks($numbers, $masks); + } + $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false); + $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false)); + + return "{$sign}{$integerPart}.{$decimalPart}"; + } + + $result = self::processComplexNumberFormatMask($number, $mask); + + return "{$sign}{$result}"; + } + + /** + * @param mixed $value + */ + private static function formatStraightNumericValue($value, string $format, array $matches, bool $useThousands): string + { + /** @var float */ + $valueFloat = $value; + $left = $matches[1]; + $dec = $matches[2]; + $right = $matches[3]; + + // minimun width of formatted number (including dot) + $minWidth = strlen($left) + strlen($dec) + strlen($right); + if ($useThousands) { + $value = number_format( + $valueFloat, + strlen($right), + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + + return self::pregReplace(self::NUMBER_REGEX, $value, $format); + } + + if (preg_match('/[0#]E[+-]0/i', $format)) { + // Scientific format + return sprintf('%5.2E', $valueFloat); + } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { + if ($value == (int) $valueFloat && substr_count($format, '.') === 1) { + $value *= 10 ** strlen(explode('.', $format)[1]); + } + + return self::complexNumberFormatMask($value, $format); + } + + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; + /** @var float */ + $valueFloat = $value; + $value = sprintf($sprintf_pattern, round($valueFloat, strlen($right))); + + return self::pregReplace(self::NUMBER_REGEX, $value, $format); + } + + /** + * @param mixed $value + */ + public static function format($value, string $format): string + { + // The "_" in this string has already been stripped out, + // so this test is never true. Furthermore, testing + // on Excel shows this format uses Euro symbol, not "EUR". + //if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) { + // return 'EUR ' . sprintf('%1.2f', $value); + //} + + // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols + $format = self::makeString(str_replace(['"', '*'], '', $format)); + + // Find out if we need thousands separator + // This is indicated by a comma enclosed by a digit placeholder: + // #,# or 0,0 + $useThousands = (bool) preg_match('/(#,#|0,0)/', $format); + if ($useThousands) { + $format = self::pregReplace('/0,0/', '00', $format); + $format = self::pregReplace('/#,#/', '##', $format); + } + + // Scale thousands, millions,... + // This is indicated by a number of commas after a digit placeholder: + // #, or 0.0,, + $scale = 1; // same as no scale + $matches = []; + if (preg_match('/(#|0)(,+)/', $format, $matches)) { + $scale = 1000 ** strlen($matches[2]); + + // strip the commas + $format = self::pregReplace('/0,+/', '0', $format); + $format = self::pregReplace('/#,+/', '#', $format); + } + if (preg_match('/#?.*\?\/\?/', $format, $m)) { + $value = FractionFormatter::format($value, $format); + } else { + // Handle the number itself + + // scale number + $value = $value / $scale; + // Strip # + $format = self::pregReplace('/\\#/', '0', $format); + // Remove locale code [$-###] + $format = self::pregReplace('/\[\$\-.*\]/', '', $format); + + $n = '/\\[[^\\]]+\\]/'; + $m = self::pregReplace($n, '', $format); + if (preg_match(self::NUMBER_REGEX, $m, $matches)) { + // There are placeholders for digits, so inject digits from the value into the mask + $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands); + } elseif ($format !== NumberFormat::FORMAT_GENERAL) { + // Yes, I know that this is basically just a hack; + // if there's no placeholders for digits, just return the format mask "as is" + $value = self::makeString(str_replace('?', '', $format)); + } + } + + if (preg_match('/\[\$(.*)\]/u', $format, $m)) { + // Currency or Accounting + $currencyCode = $m[1]; + [$currencyCode] = explode('-', $currencyCode); + if ($currencyCode == '') { + $currencyCode = StringHelper::getCurrencyCode(); + } + $value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value); + } + + return (string) $value; + } + + /** + * @param array|string $value + */ + private static function makeString($value): string + { + return is_array($value) ? '' : "$value"; + } + + private static function pregReplace(string $pattern, string $replacement, string $subject): string + { + return self::makeString(preg_replace($pattern, $replacement, $subject) ?? ''); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php new file mode 100644 index 0000000..334c40d --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -0,0 +1,45 @@ + IMAGETYPE_PNG, + IMAGETYPE_JPEG => IMAGETYPE_JPEG, + IMAGETYPE_PNG => IMAGETYPE_PNG, + IMAGETYPE_BMP => IMAGETYPE_PNG, + ]; + + /** + * Path. + * + * @var string + */ + private $path; + + /** + * Whether or not we are dealing with a URL. + * + * @var bool + */ + private $isUrl; + + /** + * Create a new Drawing. + */ + public function __construct() + { + // Initialise values + $this->path = ''; + $this->isUrl = false; + + // Initialize parent + parent::__construct(); + } + + /** + * Get Filename. + * + * @return string + */ + public function getFilename() + { + return basename($this->path); + } + + /** + * Get indexed filename (using image index). + */ + public function getIndexedFilename(): string + { + return md5($this->path) . '.' . $this->getExtension(); + } + + /** + * Get Extension. + * + * @return string + */ + public function getExtension() + { + $exploded = explode('.', basename($this->path)); + + return $exploded[count($exploded) - 1]; + } + + /** + * Get full filepath to store drawing in zip archive. + * + * @return string + */ + public function getMediaFilename() + { + if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) { + throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + + return sprintf('image%d%s', $this->getImageIndex(), $this->getImageFileExtensionForSave()); + } + + /** + * Get Path. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Set Path. + * + * @param string $path File path + * @param bool $verifyFile Verify file + * @param ZipArchive $zip Zip archive instance + * + * @return $this + */ + public function setPath($path, $verifyFile = true, $zip = null) + { + if ($verifyFile) { + // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 + if (filter_var($path, FILTER_VALIDATE_URL)) { + $this->path = $path; + // Implicit that it is a URL, rather store info than running check above on value in other places. + $this->isUrl = true; + $imageContents = file_get_contents($path); + $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); + if ($filePath) { + file_put_contents($filePath, $imageContents); + if (file_exists($filePath)) { + $this->setSizesAndType($filePath); + unlink($filePath); + } + } + } elseif (file_exists($path)) { + $this->path = $path; + $this->setSizesAndType($path); + } elseif ($zip instanceof ZipArchive) { + $zipPath = explode('#', $path)[1]; + if ($zip->locateName($zipPath) !== false) { + $this->path = $path; + $this->setSizesAndType($path); + } + } else { + throw new PhpSpreadsheetException("File $path not found!"); + } + } else { + $this->path = $path; + } + + return $this; + } + + /** + * Get isURL. + */ + public function getIsURL(): bool + { + return $this->isUrl; + } + + /** + * Set isURL. + * + * @return $this + */ + public function setIsURL(bool $isUrl): self + { + $this->isUrl = $isUrl; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->path . + parent::getHashCode() . + __CLASS__ + ); + } + + /** + * Get Image Type for Save. + */ + public function getImageTypeForSave(): int + { + if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) { + throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + + return self::IMAGE_TYPES_CONVERTION_MAP[$this->type]; + } + + /** + * Get Image file extention for Save. + */ + public function getImageFileExtensionForSave(bool $includeDot = true): string + { + if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) { + throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + + $result = image_type_to_extension(self::IMAGE_TYPES_CONVERTION_MAP[$this->type], $includeDot); + + return is_string($result) ? $result : ''; + } + + /** + * Get Image mime type. + */ + public function getImageMimeType(): string + { + if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) { + throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + + return image_type_to_mime_type(self::IMAGE_TYPES_CONVERTION_MAP[$this->type]); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php new file mode 100644 index 0000000..c3504d8 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php @@ -0,0 +1,490 @@ + + * Header/Footer Formatting Syntax taken from Office Open XML Part 4 - Markup Language Reference, page 1970:. + * + * There are a number of formatting codes that can be written inline with the actual header / footer text, which + * affect the formatting in the header or footer. + * + * Example: This example shows the text "Center Bold Header" on the first line (center section), and the date on + * the second line (center section). + * &CCenter &"-,Bold"Bold&"-,Regular"Header_x000A_&D + * + * General Rules: + * There is no required order in which these codes must appear. + * + * The first occurrence of the following codes turns the formatting ON, the second occurrence turns it OFF again: + * - strikethrough + * - superscript + * - subscript + * Superscript and subscript cannot both be ON at same time. Whichever comes first wins and the other is ignored, + * while the first is ON. + * &L - code for "left section" (there are three header / footer locations, "left", "center", and "right"). When + * two or more occurrences of this section marker exist, the contents from all markers are concatenated, in the + * order of appearance, and placed into the left section. + * &P - code for "current page #" + * &N - code for "total pages" + * &font size - code for "text font size", where font size is a font size in points. + * &K - code for "text font color" + * RGB Color is specified as RRGGBB + * Theme Color is specifed as TTSNN where TT is the theme color Id, S is either "+" or "-" of the tint/shade + * value, NN is the tint/shade value. + * &S - code for "text strikethrough" on / off + * &X - code for "text super script" on / off + * &Y - code for "text subscript" on / off + * &C - code for "center section". When two or more occurrences of this section marker exist, the contents + * from all markers are concatenated, in the order of appearance, and placed into the center section. + * + * &D - code for "date" + * &T - code for "time" + * &G - code for "picture as background" + * &U - code for "text single underline" + * &E - code for "double underline" + * &R - code for "right section". When two or more occurrences of this section marker exist, the contents + * from all markers are concatenated, in the order of appearance, and placed into the right section. + * &Z - code for "this workbook's file path" + * &F - code for "this workbook's file name" + * &A - code for "sheet tab name" + * &+ - code for add to page #. + * &- - code for subtract from page #. + * &"font name,font type" - code for "text font name" and "text font type", where font name and font type + * are strings specifying the name and type of the font, separated by a comma. When a hyphen appears in font + * name, it means "none specified". Both of font name and font type can be localized values. + * &"-,Bold" - code for "bold font style" + * &B - also means "bold font style". + * &"-,Regular" - code for "regular font style" + * &"-,Italic" - code for "italic font style" + * &I - also means "italic font style" + * &"-,Bold Italic" code for "bold italic font style" + * &O - code for "outline style" + * &H - code for "shadow style" + * + */ +class HeaderFooter +{ + // Header/footer image location + const IMAGE_HEADER_LEFT = 'LH'; + const IMAGE_HEADER_CENTER = 'CH'; + const IMAGE_HEADER_RIGHT = 'RH'; + const IMAGE_FOOTER_LEFT = 'LF'; + const IMAGE_FOOTER_CENTER = 'CF'; + const IMAGE_FOOTER_RIGHT = 'RF'; + + /** + * OddHeader. + * + * @var string + */ + private $oddHeader = ''; + + /** + * OddFooter. + * + * @var string + */ + private $oddFooter = ''; + + /** + * EvenHeader. + * + * @var string + */ + private $evenHeader = ''; + + /** + * EvenFooter. + * + * @var string + */ + private $evenFooter = ''; + + /** + * FirstHeader. + * + * @var string + */ + private $firstHeader = ''; + + /** + * FirstFooter. + * + * @var string + */ + private $firstFooter = ''; + + /** + * Different header for Odd/Even, defaults to false. + * + * @var bool + */ + private $differentOddEven = false; + + /** + * Different header for first page, defaults to false. + * + * @var bool + */ + private $differentFirst = false; + + /** + * Scale with document, defaults to true. + * + * @var bool + */ + private $scaleWithDocument = true; + + /** + * Align with margins, defaults to true. + * + * @var bool + */ + private $alignWithMargins = true; + + /** + * Header/footer images. + * + * @var HeaderFooterDrawing[] + */ + private $headerFooterImages = []; + + /** + * Create a new HeaderFooter. + */ + public function __construct() + { + } + + /** + * Get OddHeader. + * + * @return string + */ + public function getOddHeader() + { + return $this->oddHeader; + } + + /** + * Set OddHeader. + * + * @param string $oddHeader + * + * @return $this + */ + public function setOddHeader($oddHeader) + { + $this->oddHeader = $oddHeader; + + return $this; + } + + /** + * Get OddFooter. + * + * @return string + */ + public function getOddFooter() + { + return $this->oddFooter; + } + + /** + * Set OddFooter. + * + * @param string $oddFooter + * + * @return $this + */ + public function setOddFooter($oddFooter) + { + $this->oddFooter = $oddFooter; + + return $this; + } + + /** + * Get EvenHeader. + * + * @return string + */ + public function getEvenHeader() + { + return $this->evenHeader; + } + + /** + * Set EvenHeader. + * + * @param string $eventHeader + * + * @return $this + */ + public function setEvenHeader($eventHeader) + { + $this->evenHeader = $eventHeader; + + return $this; + } + + /** + * Get EvenFooter. + * + * @return string + */ + public function getEvenFooter() + { + return $this->evenFooter; + } + + /** + * Set EvenFooter. + * + * @param string $evenFooter + * + * @return $this + */ + public function setEvenFooter($evenFooter) + { + $this->evenFooter = $evenFooter; + + return $this; + } + + /** + * Get FirstHeader. + * + * @return string + */ + public function getFirstHeader() + { + return $this->firstHeader; + } + + /** + * Set FirstHeader. + * + * @param string $firstHeader + * + * @return $this + */ + public function setFirstHeader($firstHeader) + { + $this->firstHeader = $firstHeader; + + return $this; + } + + /** + * Get FirstFooter. + * + * @return string + */ + public function getFirstFooter() + { + return $this->firstFooter; + } + + /** + * Set FirstFooter. + * + * @param string $firstFooter + * + * @return $this + */ + public function setFirstFooter($firstFooter) + { + $this->firstFooter = $firstFooter; + + return $this; + } + + /** + * Get DifferentOddEven. + * + * @return bool + */ + public function getDifferentOddEven() + { + return $this->differentOddEven; + } + + /** + * Set DifferentOddEven. + * + * @param bool $differentOddEvent + * + * @return $this + */ + public function setDifferentOddEven($differentOddEvent) + { + $this->differentOddEven = $differentOddEvent; + + return $this; + } + + /** + * Get DifferentFirst. + * + * @return bool + */ + public function getDifferentFirst() + { + return $this->differentFirst; + } + + /** + * Set DifferentFirst. + * + * @param bool $differentFirst + * + * @return $this + */ + public function setDifferentFirst($differentFirst) + { + $this->differentFirst = $differentFirst; + + return $this; + } + + /** + * Get ScaleWithDocument. + * + * @return bool + */ + public function getScaleWithDocument() + { + return $this->scaleWithDocument; + } + + /** + * Set ScaleWithDocument. + * + * @param bool $scaleWithDocument + * + * @return $this + */ + public function setScaleWithDocument($scaleWithDocument) + { + $this->scaleWithDocument = $scaleWithDocument; + + return $this; + } + + /** + * Get AlignWithMargins. + * + * @return bool + */ + public function getAlignWithMargins() + { + return $this->alignWithMargins; + } + + /** + * Set AlignWithMargins. + * + * @param bool $alignWithMargins + * + * @return $this + */ + public function setAlignWithMargins($alignWithMargins) + { + $this->alignWithMargins = $alignWithMargins; + + return $this; + } + + /** + * Add header/footer image. + * + * @param string $location + * + * @return $this + */ + public function addImage(HeaderFooterDrawing $image, $location = self::IMAGE_HEADER_LEFT) + { + $this->headerFooterImages[$location] = $image; + + return $this; + } + + /** + * Remove header/footer image. + * + * @param string $location + * + * @return $this + */ + public function removeImage($location = self::IMAGE_HEADER_LEFT) + { + if (isset($this->headerFooterImages[$location])) { + unset($this->headerFooterImages[$location]); + } + + return $this; + } + + /** + * Set header/footer images. + * + * @param HeaderFooterDrawing[] $images + * + * @return $this + */ + public function setImages(array $images) + { + $this->headerFooterImages = $images; + + return $this; + } + + /** + * Get header/footer images. + * + * @return HeaderFooterDrawing[] + */ + public function getImages() + { + // Sort array + $images = []; + if (isset($this->headerFooterImages[self::IMAGE_HEADER_LEFT])) { + $images[self::IMAGE_HEADER_LEFT] = $this->headerFooterImages[self::IMAGE_HEADER_LEFT]; + } + if (isset($this->headerFooterImages[self::IMAGE_HEADER_CENTER])) { + $images[self::IMAGE_HEADER_CENTER] = $this->headerFooterImages[self::IMAGE_HEADER_CENTER]; + } + if (isset($this->headerFooterImages[self::IMAGE_HEADER_RIGHT])) { + $images[self::IMAGE_HEADER_RIGHT] = $this->headerFooterImages[self::IMAGE_HEADER_RIGHT]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_LEFT])) { + $images[self::IMAGE_FOOTER_LEFT] = $this->headerFooterImages[self::IMAGE_FOOTER_LEFT]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_CENTER])) { + $images[self::IMAGE_FOOTER_CENTER] = $this->headerFooterImages[self::IMAGE_FOOTER_CENTER]; + } + if (isset($this->headerFooterImages[self::IMAGE_FOOTER_RIGHT])) { + $images[self::IMAGE_FOOTER_RIGHT] = $this->headerFooterImages[self::IMAGE_FOOTER_RIGHT]; + } + $this->headerFooterImages = $images; + + return $this->headerFooterImages; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php new file mode 100644 index 0000000..b42c732 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php @@ -0,0 +1,24 @@ +getPath() . + $this->name . + $this->offsetX . + $this->offsetY . + $this->width . + $this->height . + __CLASS__ + ); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php new file mode 100644 index 0000000..dc08204 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php @@ -0,0 +1,74 @@ + + */ +class Iterator implements \Iterator +{ + /** + * Spreadsheet to iterate. + * + * @var Spreadsheet + */ + private $subject; + + /** + * Current iterator position. + * + * @var int + */ + private $position = 0; + + /** + * Create a new worksheet iterator. + */ + public function __construct(Spreadsheet $subject) + { + // Set subject + $this->subject = $subject; + } + + /** + * Rewind iterator. + */ + public function rewind(): void + { + $this->position = 0; + } + + /** + * Current Worksheet. + */ + public function current(): Worksheet + { + return $this->subject->getSheet($this->position); + } + + /** + * Current key. + */ + public function key(): int + { + return $this->position; + } + + /** + * Next value. + */ + public function next(): void + { + ++$this->position; + } + + /** + * Are there more Worksheet instances available? + */ + public function valid(): bool + { + return $this->position < $this->subject->getSheetCount() && $this->position >= 0; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php new file mode 100644 index 0000000..91acbb7 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -0,0 +1,230 @@ +renderingFunction = self::RENDERING_DEFAULT; + $this->mimeType = self::MIMETYPE_DEFAULT; + $this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + + // Initialize parent + parent::__construct(); + } + + public function __destruct() + { + if ($this->imageResource) { + imagedestroy($this->imageResource); + $this->imageResource = null; + } + } + + public function __clone() + { + parent::__clone(); + $this->cloneResource(); + } + + private function cloneResource(): void + { + if (!$this->imageResource) { + return; + } + + $width = imagesx($this->imageResource); + $height = imagesy($this->imageResource); + + if (imageistruecolor($this->imageResource)) { + $clone = imagecreatetruecolor($width, $height); + if (!$clone) { + throw new Exception('Could not clone image resource'); + } + + imagealphablending($clone, false); + imagesavealpha($clone, true); + } else { + $clone = imagecreate($width, $height); + if (!$clone) { + throw new Exception('Could not clone image resource'); + } + + // If the image has transparency... + $transparent = imagecolortransparent($this->imageResource); + if ($transparent >= 0) { + $rgb = imagecolorsforindex($this->imageResource, $transparent); + if (empty($rgb)) { + throw new Exception('Could not get image colors'); + } + + imagesavealpha($clone, true); + $color = imagecolorallocatealpha($clone, $rgb['red'], $rgb['green'], $rgb['blue'], $rgb['alpha']); + if ($color === false) { + throw new Exception('Could not get image alpha color'); + } + + imagefill($clone, 0, 0, $color); + } + } + + //Create the Clone!! + imagecopy($clone, $this->imageResource, 0, 0, 0, 0, $width, $height); + + $this->imageResource = $clone; + } + + /** + * Get image resource. + * + * @return null|GdImage|resource + */ + public function getImageResource() + { + return $this->imageResource; + } + + /** + * Set image resource. + * + * @param GdImage|resource $value + * + * @return $this + */ + public function setImageResource($value) + { + $this->imageResource = $value; + + if ($this->imageResource !== null) { + // Get width/height + $this->width = imagesx($this->imageResource); + $this->height = imagesy($this->imageResource); + } + + return $this; + } + + /** + * Get rendering function. + * + * @return string + */ + public function getRenderingFunction() + { + return $this->renderingFunction; + } + + /** + * Set rendering function. + * + * @param string $value see self::RENDERING_* + * + * @return $this + */ + public function setRenderingFunction($value) + { + $this->renderingFunction = $value; + + return $this; + } + + /** + * Get mime type. + * + * @return string + */ + public function getMimeType() + { + return $this->mimeType; + } + + /** + * Set mime type. + * + * @param string $value see self::MIMETYPE_* + * + * @return $this + */ + public function setMimeType($value) + { + $this->mimeType = $value; + + return $this; + } + + /** + * Get indexed filename (using image index). + */ + public function getIndexedFilename(): string + { + $extension = strtolower($this->getMimeType()); + $extension = explode('/', $extension); + $extension = $extension[1]; + + return $this->uniqueName . $this->getImageIndex() . '.' . $extension; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode() + { + return md5( + $this->renderingFunction . + $this->mimeType . + $this->uniqueName . + parent::getHashCode() . + __CLASS__ + ); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php new file mode 100644 index 0000000..34e1145 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php @@ -0,0 +1,244 @@ +left; + } + + /** + * Set Left. + * + * @param float $left + * + * @return $this + */ + public function setLeft($left) + { + $this->left = $left; + + return $this; + } + + /** + * Get Right. + * + * @return float + */ + public function getRight() + { + return $this->right; + } + + /** + * Set Right. + * + * @param float $right + * + * @return $this + */ + public function setRight($right) + { + $this->right = $right; + + return $this; + } + + /** + * Get Top. + * + * @return float + */ + public function getTop() + { + return $this->top; + } + + /** + * Set Top. + * + * @param float $top + * + * @return $this + */ + public function setTop($top) + { + $this->top = $top; + + return $this; + } + + /** + * Get Bottom. + * + * @return float + */ + public function getBottom() + { + return $this->bottom; + } + + /** + * Set Bottom. + * + * @param float $bottom + * + * @return $this + */ + public function setBottom($bottom) + { + $this->bottom = $bottom; + + return $this; + } + + /** + * Get Header. + * + * @return float + */ + public function getHeader() + { + return $this->header; + } + + /** + * Set Header. + * + * @param float $header + * + * @return $this + */ + public function setHeader($header) + { + $this->header = $header; + + return $this; + } + + /** + * Get Footer. + * + * @return float + */ + public function getFooter() + { + return $this->footer; + } + + /** + * Set Footer. + * + * @param float $footer + * + * @return $this + */ + public function setFooter($footer) + { + $this->footer = $footer; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } + + public static function fromCentimeters(float $value): float + { + return $value / 2.54; + } + + public static function toCentimeters(float $value): float + { + return $value * 2.54; + } + + public static function fromMillimeters(float $value): float + { + return $value / 25.4; + } + + public static function toMillimeters(float $value): float + { + return $value * 25.4; + } + + public static function fromPoints(float $value): float + { + return $value / 72; + } + + public static function toPoints(float $value): float + { + return $value * 72; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php new file mode 100644 index 0000000..9640782 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -0,0 +1,902 @@ + + * Paper size taken from Office Open XML Part 4 - Markup Language Reference, page 1988:. + * + * 1 = Letter paper (8.5 in. by 11 in.) + * 2 = Letter small paper (8.5 in. by 11 in.) + * 3 = Tabloid paper (11 in. by 17 in.) + * 4 = Ledger paper (17 in. by 11 in.) + * 5 = Legal paper (8.5 in. by 14 in.) + * 6 = Statement paper (5.5 in. by 8.5 in.) + * 7 = Executive paper (7.25 in. by 10.5 in.) + * 8 = A3 paper (297 mm by 420 mm) + * 9 = A4 paper (210 mm by 297 mm) + * 10 = A4 small paper (210 mm by 297 mm) + * 11 = A5 paper (148 mm by 210 mm) + * 12 = B4 paper (250 mm by 353 mm) + * 13 = B5 paper (176 mm by 250 mm) + * 14 = Folio paper (8.5 in. by 13 in.) + * 15 = Quarto paper (215 mm by 275 mm) + * 16 = Standard paper (10 in. by 14 in.) + * 17 = Standard paper (11 in. by 17 in.) + * 18 = Note paper (8.5 in. by 11 in.) + * 19 = #9 envelope (3.875 in. by 8.875 in.) + * 20 = #10 envelope (4.125 in. by 9.5 in.) + * 21 = #11 envelope (4.5 in. by 10.375 in.) + * 22 = #12 envelope (4.75 in. by 11 in.) + * 23 = #14 envelope (5 in. by 11.5 in.) + * 24 = C paper (17 in. by 22 in.) + * 25 = D paper (22 in. by 34 in.) + * 26 = E paper (34 in. by 44 in.) + * 27 = DL envelope (110 mm by 220 mm) + * 28 = C5 envelope (162 mm by 229 mm) + * 29 = C3 envelope (324 mm by 458 mm) + * 30 = C4 envelope (229 mm by 324 mm) + * 31 = C6 envelope (114 mm by 162 mm) + * 32 = C65 envelope (114 mm by 229 mm) + * 33 = B4 envelope (250 mm by 353 mm) + * 34 = B5 envelope (176 mm by 250 mm) + * 35 = B6 envelope (176 mm by 125 mm) + * 36 = Italy envelope (110 mm by 230 mm) + * 37 = Monarch envelope (3.875 in. by 7.5 in.). + * 38 = 6 3/4 envelope (3.625 in. by 6.5 in.) + * 39 = US standard fanfold (14.875 in. by 11 in.) + * 40 = German standard fanfold (8.5 in. by 12 in.) + * 41 = German legal fanfold (8.5 in. by 13 in.) + * 42 = ISO B4 (250 mm by 353 mm) + * 43 = Japanese double postcard (200 mm by 148 mm) + * 44 = Standard paper (9 in. by 11 in.) + * 45 = Standard paper (10 in. by 11 in.) + * 46 = Standard paper (15 in. by 11 in.) + * 47 = Invite envelope (220 mm by 220 mm) + * 50 = Letter extra paper (9.275 in. by 12 in.) + * 51 = Legal extra paper (9.275 in. by 15 in.) + * 52 = Tabloid extra paper (11.69 in. by 18 in.) + * 53 = A4 extra paper (236 mm by 322 mm) + * 54 = Letter transverse paper (8.275 in. by 11 in.) + * 55 = A4 transverse paper (210 mm by 297 mm) + * 56 = Letter extra transverse paper (9.275 in. by 12 in.) + * 57 = SuperA/SuperA/A4 paper (227 mm by 356 mm) + * 58 = SuperB/SuperB/A3 paper (305 mm by 487 mm) + * 59 = Letter plus paper (8.5 in. by 12.69 in.) + * 60 = A4 plus paper (210 mm by 330 mm) + * 61 = A5 transverse paper (148 mm by 210 mm) + * 62 = JIS B5 transverse paper (182 mm by 257 mm) + * 63 = A3 extra paper (322 mm by 445 mm) + * 64 = A5 extra paper (174 mm by 235 mm) + * 65 = ISO B5 extra paper (201 mm by 276 mm) + * 66 = A2 paper (420 mm by 594 mm) + * 67 = A3 transverse paper (297 mm by 420 mm) + * 68 = A3 extra transverse paper (322 mm by 445 mm) + * + */ +class PageSetup +{ + // Paper size + const PAPERSIZE_LETTER = 1; + const PAPERSIZE_LETTER_SMALL = 2; + const PAPERSIZE_TABLOID = 3; + const PAPERSIZE_LEDGER = 4; + const PAPERSIZE_LEGAL = 5; + const PAPERSIZE_STATEMENT = 6; + const PAPERSIZE_EXECUTIVE = 7; + const PAPERSIZE_A3 = 8; + const PAPERSIZE_A4 = 9; + const PAPERSIZE_A4_SMALL = 10; + const PAPERSIZE_A5 = 11; + const PAPERSIZE_B4 = 12; + const PAPERSIZE_B5 = 13; + const PAPERSIZE_FOLIO = 14; + const PAPERSIZE_QUARTO = 15; + const PAPERSIZE_STANDARD_1 = 16; + const PAPERSIZE_STANDARD_2 = 17; + const PAPERSIZE_NOTE = 18; + const PAPERSIZE_NO9_ENVELOPE = 19; + const PAPERSIZE_NO10_ENVELOPE = 20; + const PAPERSIZE_NO11_ENVELOPE = 21; + const PAPERSIZE_NO12_ENVELOPE = 22; + const PAPERSIZE_NO14_ENVELOPE = 23; + const PAPERSIZE_C = 24; + const PAPERSIZE_D = 25; + const PAPERSIZE_E = 26; + const PAPERSIZE_DL_ENVELOPE = 27; + const PAPERSIZE_C5_ENVELOPE = 28; + const PAPERSIZE_C3_ENVELOPE = 29; + const PAPERSIZE_C4_ENVELOPE = 30; + const PAPERSIZE_C6_ENVELOPE = 31; + const PAPERSIZE_C65_ENVELOPE = 32; + const PAPERSIZE_B4_ENVELOPE = 33; + const PAPERSIZE_B5_ENVELOPE = 34; + const PAPERSIZE_B6_ENVELOPE = 35; + const PAPERSIZE_ITALY_ENVELOPE = 36; + const PAPERSIZE_MONARCH_ENVELOPE = 37; + const PAPERSIZE_6_3_4_ENVELOPE = 38; + const PAPERSIZE_US_STANDARD_FANFOLD = 39; + const PAPERSIZE_GERMAN_STANDARD_FANFOLD = 40; + const PAPERSIZE_GERMAN_LEGAL_FANFOLD = 41; + const PAPERSIZE_ISO_B4 = 42; + const PAPERSIZE_JAPANESE_DOUBLE_POSTCARD = 43; + const PAPERSIZE_STANDARD_PAPER_1 = 44; + const PAPERSIZE_STANDARD_PAPER_2 = 45; + const PAPERSIZE_STANDARD_PAPER_3 = 46; + const PAPERSIZE_INVITE_ENVELOPE = 47; + const PAPERSIZE_LETTER_EXTRA_PAPER = 48; + const PAPERSIZE_LEGAL_EXTRA_PAPER = 49; + const PAPERSIZE_TABLOID_EXTRA_PAPER = 50; + const PAPERSIZE_A4_EXTRA_PAPER = 51; + const PAPERSIZE_LETTER_TRANSVERSE_PAPER = 52; + const PAPERSIZE_A4_TRANSVERSE_PAPER = 53; + const PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER = 54; + const PAPERSIZE_SUPERA_SUPERA_A4_PAPER = 55; + const PAPERSIZE_SUPERB_SUPERB_A3_PAPER = 56; + const PAPERSIZE_LETTER_PLUS_PAPER = 57; + const PAPERSIZE_A4_PLUS_PAPER = 58; + const PAPERSIZE_A5_TRANSVERSE_PAPER = 59; + const PAPERSIZE_JIS_B5_TRANSVERSE_PAPER = 60; + const PAPERSIZE_A3_EXTRA_PAPER = 61; + const PAPERSIZE_A5_EXTRA_PAPER = 62; + const PAPERSIZE_ISO_B5_EXTRA_PAPER = 63; + const PAPERSIZE_A2_PAPER = 64; + const PAPERSIZE_A3_TRANSVERSE_PAPER = 65; + const PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER = 66; + + // Page orientation + const ORIENTATION_DEFAULT = 'default'; + const ORIENTATION_LANDSCAPE = 'landscape'; + const ORIENTATION_PORTRAIT = 'portrait'; + + // Print Range Set Method + const SETPRINTRANGE_OVERWRITE = 'O'; + const SETPRINTRANGE_INSERT = 'I'; + + const PAGEORDER_OVER_THEN_DOWN = 'overThenDown'; + const PAGEORDER_DOWN_THEN_OVER = 'downThenOver'; + + /** + * Paper size default. + * + * @var int + */ + private static $paperSizeDefault = self::PAPERSIZE_LETTER; + + /** + * Paper size. + * + * @var ?int + */ + private $paperSize; + + /** + * Orientation default. + * + * @var string + */ + private static $orientationDefault = self::ORIENTATION_DEFAULT; + + /** + * Orientation. + * + * @var string + */ + private $orientation; + + /** + * Scale (Print Scale). + * + * Print scaling. Valid values range from 10 to 400 + * This setting is overridden when fitToWidth and/or fitToHeight are in use + * + * @var null|int + */ + private $scale = 100; + + /** + * Fit To Page + * Whether scale or fitToWith / fitToHeight applies. + * + * @var bool + */ + private $fitToPage = false; + + /** + * Fit To Height + * Number of vertical pages to fit on. + * + * @var null|int + */ + private $fitToHeight = 1; + + /** + * Fit To Width + * Number of horizontal pages to fit on. + * + * @var null|int + */ + private $fitToWidth = 1; + + /** + * Columns to repeat at left. + * + * @var array Containing start column and end column, empty array if option unset + */ + private $columnsToRepeatAtLeft = ['', '']; + + /** + * Rows to repeat at top. + * + * @var array Containing start row number and end row number, empty array if option unset + */ + private $rowsToRepeatAtTop = [0, 0]; + + /** + * Center page horizontally. + * + * @var bool + */ + private $horizontalCentered = false; + + /** + * Center page vertically. + * + * @var bool + */ + private $verticalCentered = false; + + /** + * Print area. + * + * @var null|string + */ + private $printArea; + + /** + * First page number. + * + * @var int + */ + private $firstPageNumber; + + private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; + + /** + * Create a new PageSetup. + */ + public function __construct() + { + $this->orientation = self::$orientationDefault; + } + + /** + * Get Paper Size. + * + * @return int + */ + public function getPaperSize() + { + return $this->paperSize ?? self::$paperSizeDefault; + } + + /** + * Set Paper Size. + * + * @param int $paperSize see self::PAPERSIZE_* + * + * @return $this + */ + public function setPaperSize($paperSize) + { + $this->paperSize = $paperSize; + + return $this; + } + + /** + * Get Paper Size default. + */ + public static function getPaperSizeDefault(): int + { + return self::$paperSizeDefault; + } + + /** + * Set Paper Size Default. + */ + public static function setPaperSizeDefault(int $paperSize): void + { + self::$paperSizeDefault = $paperSize; + } + + /** + * Get Orientation. + * + * @return string + */ + public function getOrientation() + { + return $this->orientation; + } + + /** + * Set Orientation. + * + * @param string $orientation see self::ORIENTATION_* + * + * @return $this + */ + public function setOrientation($orientation) + { + if ($orientation === self::ORIENTATION_LANDSCAPE || $orientation === self::ORIENTATION_PORTRAIT || $orientation === self::ORIENTATION_DEFAULT) { + $this->orientation = $orientation; + } + + return $this; + } + + public static function getOrientationDefault(): string + { + return self::$orientationDefault; + } + + public static function setOrientationDefault(string $orientation): void + { + if ($orientation === self::ORIENTATION_LANDSCAPE || $orientation === self::ORIENTATION_PORTRAIT || $orientation === self::ORIENTATION_DEFAULT) { + self::$orientationDefault = $orientation; + } + } + + /** + * Get Scale. + * + * @return null|int + */ + public function getScale() + { + return $this->scale; + } + + /** + * Set Scale. + * Print scaling. Valid values range from 10 to 400 + * This setting is overridden when fitToWidth and/or fitToHeight are in use. + * + * @param null|int $scale + * @param bool $update Update fitToPage so scaling applies rather than fitToHeight / fitToWidth + * + * @return $this + */ + public function setScale($scale, $update = true) + { + // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, + // but it is apparently still able to handle any scale >= 0, where 0 results in 100 + if (($scale >= 0) || $scale === null) { + $this->scale = $scale; + if ($update) { + $this->fitToPage = false; + } + } else { + throw new PhpSpreadsheetException('Scale must not be negative'); + } + + return $this; + } + + /** + * Get Fit To Page. + * + * @return bool + */ + public function getFitToPage() + { + return $this->fitToPage; + } + + /** + * Set Fit To Page. + * + * @param bool $fitToPage + * + * @return $this + */ + public function setFitToPage($fitToPage) + { + $this->fitToPage = $fitToPage; + + return $this; + } + + /** + * Get Fit To Height. + * + * @return null|int + */ + public function getFitToHeight() + { + return $this->fitToHeight; + } + + /** + * Set Fit To Height. + * + * @param null|int $fitToHeight + * @param bool $update Update fitToPage so it applies rather than scaling + * + * @return $this + */ + public function setFitToHeight($fitToHeight, $update = true) + { + $this->fitToHeight = $fitToHeight; + if ($update) { + $this->fitToPage = true; + } + + return $this; + } + + /** + * Get Fit To Width. + * + * @return null|int + */ + public function getFitToWidth() + { + return $this->fitToWidth; + } + + /** + * Set Fit To Width. + * + * @param null|int $value + * @param bool $update Update fitToPage so it applies rather than scaling + * + * @return $this + */ + public function setFitToWidth($value, $update = true) + { + $this->fitToWidth = $value; + if ($update) { + $this->fitToPage = true; + } + + return $this; + } + + /** + * Is Columns to repeat at left set? + * + * @return bool + */ + public function isColumnsToRepeatAtLeftSet() + { + if (is_array($this->columnsToRepeatAtLeft)) { + if ($this->columnsToRepeatAtLeft[0] != '' && $this->columnsToRepeatAtLeft[1] != '') { + return true; + } + } + + return false; + } + + /** + * Get Columns to repeat at left. + * + * @return array Containing start column and end column, empty array if option unset + */ + public function getColumnsToRepeatAtLeft() + { + return $this->columnsToRepeatAtLeft; + } + + /** + * Set Columns to repeat at left. + * + * @param array $columnsToRepeatAtLeft Containing start column and end column, empty array if option unset + * + * @return $this + */ + public function setColumnsToRepeatAtLeft(array $columnsToRepeatAtLeft) + { + $this->columnsToRepeatAtLeft = $columnsToRepeatAtLeft; + + return $this; + } + + /** + * Set Columns to repeat at left by start and end. + * + * @param string $start eg: 'A' + * @param string $end eg: 'B' + * + * @return $this + */ + public function setColumnsToRepeatAtLeftByStartAndEnd($start, $end) + { + $this->columnsToRepeatAtLeft = [$start, $end]; + + return $this; + } + + /** + * Is Rows to repeat at top set? + * + * @return bool + */ + public function isRowsToRepeatAtTopSet() + { + if (is_array($this->rowsToRepeatAtTop)) { + if ($this->rowsToRepeatAtTop[0] != 0 && $this->rowsToRepeatAtTop[1] != 0) { + return true; + } + } + + return false; + } + + /** + * Get Rows to repeat at top. + * + * @return array Containing start column and end column, empty array if option unset + */ + public function getRowsToRepeatAtTop() + { + return $this->rowsToRepeatAtTop; + } + + /** + * Set Rows to repeat at top. + * + * @param array $rowsToRepeatAtTop Containing start column and end column, empty array if option unset + * + * @return $this + */ + public function setRowsToRepeatAtTop(array $rowsToRepeatAtTop) + { + $this->rowsToRepeatAtTop = $rowsToRepeatAtTop; + + return $this; + } + + /** + * Set Rows to repeat at top by start and end. + * + * @param int $start eg: 1 + * @param int $end eg: 1 + * + * @return $this + */ + public function setRowsToRepeatAtTopByStartAndEnd($start, $end) + { + $this->rowsToRepeatAtTop = [$start, $end]; + + return $this; + } + + /** + * Get center page horizontally. + * + * @return bool + */ + public function getHorizontalCentered() + { + return $this->horizontalCentered; + } + + /** + * Set center page horizontally. + * + * @param bool $value + * + * @return $this + */ + public function setHorizontalCentered($value) + { + $this->horizontalCentered = $value; + + return $this; + } + + /** + * Get center page vertically. + * + * @return bool + */ + public function getVerticalCentered() + { + return $this->verticalCentered; + } + + /** + * Set center page vertically. + * + * @param bool $value + * + * @return $this + */ + public function setVerticalCentered($value) + { + $this->verticalCentered = $value; + + return $this; + } + + /** + * Get print area. + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or a index value of 0, will return all ranges as a comma-separated string + * Otherwise, the specific range identified by the value of $index will be returned + * Print areas are numbered from 1 + * + * @return string + */ + public function getPrintArea($index = 0) + { + if ($index == 0) { + return $this->printArea; + } + $printAreas = explode(',', $this->printArea); + if (isset($printAreas[$index - 1])) { + return $printAreas[$index - 1]; + } + + throw new PhpSpreadsheetException('Requested Print Area does not exist'); + } + + /** + * Is print area set? + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or an index value of 0, will identify whether any print range is set + * Otherwise, existence of the range identified by the value of $index will be returned + * Print areas are numbered from 1 + * + * @return bool + */ + public function isPrintAreaSet($index = 0) + { + if ($index == 0) { + return $this->printArea !== null; + } + $printAreas = explode(',', $this->printArea); + + return isset($printAreas[$index - 1]); + } + + /** + * Clear a print area. + * + * @param int $index Identifier for a specific print area range if several ranges have been set + * Default behaviour, or an index value of 0, will clear all print ranges that are set + * Otherwise, the range identified by the value of $index will be removed from the series + * Print areas are numbered from 1 + * + * @return $this + */ + public function clearPrintArea($index = 0) + { + if ($index == 0) { + $this->printArea = null; + } else { + $printAreas = explode(',', $this->printArea); + if (isset($printAreas[$index - 1])) { + unset($printAreas[$index - 1]); + $this->printArea = implode(',', $printAreas); + } + } + + return $this; + } + + /** + * Set print area. e.g. 'A1:D10' or 'A1:D10,G5:M20'. + * + * @param string $value + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * When the method is "O"verwrite, then a positive integer index will overwrite that indexed + * entry in the print areas list; a negative index value will identify which entry to + * overwrite working bacward through the print area to the list, with the last entry as -1. + * Specifying an index value of 0, will overwrite all existing print ranges. + * When the method is "I"nsert, then a positive index will insert after that indexed entry in + * the print areas list, while a negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * @param string $method Determines the method used when setting multiple print areas + * Default behaviour, or the "O" method, overwrites existing print area + * The "I" method, inserts the new print area before any specified index, or at the end of the list + * + * @return $this + */ + public function setPrintArea($value, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE) + { + if (strpos($value, '!') !== false) { + throw new PhpSpreadsheetException('Cell coordinate must not specify a worksheet.'); + } elseif (strpos($value, ':') === false) { + throw new PhpSpreadsheetException('Cell coordinate must be a range of cells.'); + } elseif (strpos($value, '$') !== false) { + throw new PhpSpreadsheetException('Cell coordinate must not be absolute.'); + } + $value = strtoupper($value); + if (!$this->printArea) { + $index = 0; + } + + if ($method == self::SETPRINTRANGE_OVERWRITE) { + if ($index == 0) { + $this->printArea = $value; + } else { + $printAreas = explode(',', $this->printArea); + if ($index < 0) { + $index = count($printAreas) - abs($index) + 1; + } + if (($index <= 0) || ($index > count($printAreas))) { + throw new PhpSpreadsheetException('Invalid index for setting print range.'); + } + $printAreas[$index - 1] = $value; + $this->printArea = implode(',', $printAreas); + } + } elseif ($method == self::SETPRINTRANGE_INSERT) { + if ($index == 0) { + $this->printArea = $this->printArea ? ($this->printArea . ',' . $value) : $value; + } else { + $printAreas = explode(',', $this->printArea); + if ($index < 0) { + $index = abs($index) - 1; + } + if ($index > count($printAreas)) { + throw new PhpSpreadsheetException('Invalid index for setting print range.'); + } + $printAreas = array_merge(array_slice($printAreas, 0, $index), [$value], array_slice($printAreas, $index)); + $this->printArea = implode(',', $printAreas); + } + } else { + throw new PhpSpreadsheetException('Invalid method for setting print range.'); + } + + return $this; + } + + /** + * Add a new print area (e.g. 'A1:D10' or 'A1:D10,G5:M20') to the list of print areas. + * + * @param string $value + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * A positive index will insert after that indexed entry in the print areas list, while a + * negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * + * @return $this + */ + public function addPrintArea($value, $index = -1) + { + return $this->setPrintArea($value, $index, self::SETPRINTRANGE_INSERT); + } + + /** + * Set print area. + * + * @param int $column1 Column 1 + * @param int $row1 Row 1 + * @param int $column2 Column 2 + * @param int $row2 Row 2 + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * When the method is "O"verwrite, then a positive integer index will overwrite that indexed + * entry in the print areas list; a negative index value will identify which entry to + * overwrite working backward through the print area to the list, with the last entry as -1. + * Specifying an index value of 0, will overwrite all existing print ranges. + * When the method is "I"nsert, then a positive index will insert after that indexed entry in + * the print areas list, while a negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * @param string $method Determines the method used when setting multiple print areas + * Default behaviour, or the "O" method, overwrites existing print area + * The "I" method, inserts the new print area before any specified index, or at the end of the list + * + * @return $this + */ + public function setPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE) + { + return $this->setPrintArea( + Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2, + $index, + $method + ); + } + + /** + * Add a new print area to the list of print areas. + * + * @param int $column1 Start Column for the print area + * @param int $row1 Start Row for the print area + * @param int $column2 End Column for the print area + * @param int $row2 End Row for the print area + * @param int $index Identifier for a specific print area range allowing several ranges to be set + * A positive index will insert after that indexed entry in the print areas list, while a + * negative index will insert before the indexed entry. + * Specifying an index value of 0, will always append the new print range at the end of the + * list. + * Print areas are numbered from 1 + * + * @return $this + */ + public function addPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = -1) + { + return $this->setPrintArea( + Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2, + $index, + self::SETPRINTRANGE_INSERT + ); + } + + /** + * Get first page number. + * + * @return int + */ + public function getFirstPageNumber() + { + return $this->firstPageNumber; + } + + /** + * Set first page number. + * + * @param int $value + * + * @return $this + */ + public function setFirstPageNumber($value) + { + $this->firstPageNumber = $value; + + return $this; + } + + /** + * Reset first page number. + * + * @return $this + */ + public function resetFirstPageNumber() + { + return $this->setFirstPageNumber(null); + } + + public function getPageOrder(): string + { + return $this->pageOrder; + } + + public function setPageOrder(?string $pageOrder): self + { + if ($pageOrder === null || $pageOrder === self::PAGEORDER_DOWN_THEN_OVER || $pageOrder === self::PAGEORDER_OVER_THEN_DOWN) { + $this->pageOrder = $pageOrder ?? self::PAGEORDER_DOWN_THEN_OVER; + } + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Exception.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Exception.php new file mode 100644 index 0000000..92e6f5f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Exception.php @@ -0,0 +1,9 @@ +spreadsheet = $spreadsheet; + $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont(); + } + + /** + * Save Spreadsheet to file. + * + * @param resource|string $filename + */ + public function save($filename, int $flags = 0): void + { + $this->processFlags($flags); + + // Open file + $this->openFileHandle($filename); + + // Write html + fwrite($this->fileHandle, $this->generateHTMLAll()); + + // Close file + $this->maybeCloseFileHandle(); + } + + /** + * Save Spreadsheet as html to variable. + * + * @return string + */ + public function generateHtmlAll() + { + // garbage collect + $this->spreadsheet->garbageCollect(); + + $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog(); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false); + $saveArrayReturnType = Calculation::getArrayReturnType(); + Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); + + // Build CSS + $this->buildCSS(!$this->useInlineCss); + + $html = ''; + + // Write headers + $html .= $this->generateHTMLHeader(!$this->useInlineCss); + + // Write navigation (tabs) + if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) { + $html .= $this->generateNavigation(); + } + + // Write data + $html .= $this->generateSheetData(); + + // Write footer + $html .= $this->generateHTMLFooter(); + $callback = $this->editHtmlCallback; + if ($callback) { + $html = $callback($html); + } + + Calculation::setArrayReturnType($saveArrayReturnType); + Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + + return $html; + } + + /** + * Set a callback to edit the entire HTML. + * + * The callback must accept the HTML as string as first parameter, + * and it must return the edited HTML as string. + */ + public function setEditHtmlCallback(?callable $callback): void + { + $this->editHtmlCallback = $callback; + } + + const VALIGN_ARR = [ + Alignment::VERTICAL_BOTTOM => 'bottom', + Alignment::VERTICAL_TOP => 'top', + Alignment::VERTICAL_CENTER => 'middle', + Alignment::VERTICAL_JUSTIFY => 'middle', + ]; + + /** + * Map VAlign. + * + * @param string $vAlign Vertical alignment + * + * @return string + */ + private function mapVAlign($vAlign) + { + return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; + } + + const HALIGN_ARR = [ + Alignment::HORIZONTAL_LEFT => 'left', + Alignment::HORIZONTAL_RIGHT => 'right', + Alignment::HORIZONTAL_CENTER => 'center', + Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', + Alignment::HORIZONTAL_JUSTIFY => 'justify', + ]; + + /** + * Map HAlign. + * + * @param string $hAlign Horizontal alignment + * + * @return string + */ + private function mapHAlign($hAlign) + { + return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; + } + + const BORDER_ARR = [ + Border::BORDER_NONE => 'none', + Border::BORDER_DASHDOT => '1px dashed', + Border::BORDER_DASHDOTDOT => '1px dotted', + Border::BORDER_DASHED => '1px dashed', + Border::BORDER_DOTTED => '1px dotted', + Border::BORDER_DOUBLE => '3px double', + Border::BORDER_HAIR => '1px solid', + Border::BORDER_MEDIUM => '2px solid', + Border::BORDER_MEDIUMDASHDOT => '2px dashed', + Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted', + Border::BORDER_SLANTDASHDOT => '2px dashed', + Border::BORDER_THICK => '3px solid', + ]; + + /** + * Map border style. + * + * @param int $borderStyle Sheet index + * + * @return string + */ + private function mapBorderStyle($borderStyle) + { + return array_key_exists($borderStyle, self::BORDER_ARR) ? self::BORDER_ARR[$borderStyle] : '1px solid'; + } + + /** + * Get sheet index. + */ + public function getSheetIndex(): ?int + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $sheetIndex Sheet index + * + * @return $this + */ + public function setSheetIndex($sheetIndex) + { + $this->sheetIndex = $sheetIndex; + + return $this; + } + + /** + * Get sheet index. + * + * @return bool + */ + public function getGenerateSheetNavigationBlock() + { + return $this->generateSheetNavigationBlock; + } + + /** + * Set sheet index. + * + * @param bool $generateSheetNavigationBlock Flag indicating whether the sheet navigation block should be generated or not + * + * @return $this + */ + public function setGenerateSheetNavigationBlock($generateSheetNavigationBlock) + { + $this->generateSheetNavigationBlock = (bool) $generateSheetNavigationBlock; + + return $this; + } + + /** + * Write all sheets (resets sheetIndex to NULL). + * + * @return $this + */ + public function writeAllSheets() + { + $this->sheetIndex = null; + + return $this; + } + + private static function generateMeta($val, $desc) + { + return $val + ? (' ' . PHP_EOL) + : ''; + } + + public const BODY_LINE = ' ' . PHP_EOL; + + /** + * Generate HTML header. + * + * @param bool $includeStyles Include styles? + * + * @return string + */ + public function generateHTMLHeader($includeStyles = false) + { + // Construct HTML + $properties = $this->spreadsheet->getProperties(); + $html = '' . PHP_EOL; + $html .= '' . PHP_EOL; + $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; + $html .= ' ' . htmlspecialchars($properties->getTitle(), Settings::htmlEntityFlags()) . '' . PHP_EOL; + $html .= self::generateMeta($properties->getCreator(), 'author'); + $html .= self::generateMeta($properties->getTitle(), 'title'); + $html .= self::generateMeta($properties->getDescription(), 'description'); + $html .= self::generateMeta($properties->getSubject(), 'subject'); + $html .= self::generateMeta($properties->getKeywords(), 'keywords'); + $html .= self::generateMeta($properties->getCategory(), 'category'); + $html .= self::generateMeta($properties->getCompany(), 'company'); + $html .= self::generateMeta($properties->getManager(), 'manager'); + + $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); + + $html .= ' ' . PHP_EOL; + $html .= '' . PHP_EOL; + $html .= self::BODY_LINE; + + return $html; + } + + private function generateSheetPrep() + { + // Ensure that Spans have been calculated? + $this->calculateSpans(); + + // Fetch sheets + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)]; + } + + return $sheets; + } + + private function generateSheetStarts($sheet, $rowMin) + { + // calculate start of , + $tbodyStart = $rowMin; + $theadStart = $theadEnd = 0; // default: no no + if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) { + $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop(); + + // we can only support repeating rows that start at top row + if ($rowsToRepeatAtTop[0] == 1) { + $theadStart = $rowsToRepeatAtTop[0]; + $theadEnd = $rowsToRepeatAtTop[1]; + $tbodyStart = $rowsToRepeatAtTop[1] + 1; + } + } + + return [$theadStart, $theadEnd, $tbodyStart]; + } + + private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) + { + // ? + $startTag = ($row == $theadStart) ? (' ' . PHP_EOL) : ''; + if (!$startTag) { + $startTag = ($row == $tbodyStart) ? (' ' . PHP_EOL) : ''; + } + $endTag = ($row == $theadEnd) ? (' ' . PHP_EOL) : ''; + $cellType = ($row >= $tbodyStart) ? 'td' : 'th'; + + return [$cellType, $startTag, $endTag]; + } + + /** + * Generate sheet data. + * + * @return string + */ + public function generateSheetData() + { + $sheets = $this->generateSheetPrep(); + + // Construct HTML + $html = ''; + + // Loop all sheets + $sheetId = 0; + foreach ($sheets as $sheet) { + // Write table header + $html .= $this->generateTableHeader($sheet); + + // Get worksheet dimension + [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); + [$minCol, $minRow] = Coordinate::indexesFromString($min); + [$maxCol, $maxRow] = Coordinate::indexesFromString($max); + + [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); + + // Loop through cells + $row = $minRow - 1; + while ($row++ < $maxRow) { + [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart); + $html .= $startTag; + + // Write row if there are HTML table cells in it + if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) { + // Start a new rowData + $rowData = []; + // Loop through columns + $column = $minCol; + while ($column <= $maxCol) { + // Cell exists? + if ($sheet->cellExistsByColumnAndRow($column, $row)) { + $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row; + } else { + $rowData[$column] = ''; + } + ++$column; + } + $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType); + } + + $html .= $endTag; + } + $html .= $this->extendRowsForChartsAndImages($sheet, $row); + + // Write table footer + $html .= $this->generateTableFooter(); + // Writing PDF? + if ($this->isPdf && $this->useInlineCss) { + if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) { + $html .= '
'; + } + } + + // Next sheet + ++$sheetId; + } + + return $html; + } + + /** + * Generate sheet tabs. + * + * @return string + */ + public function generateNavigation() + { + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Construct HTML + $html = ''; + + // Only if there are more than 1 sheets + if (count($sheets) > 1) { + // Loop all sheets + $sheetId = 0; + + $html .= '' . PHP_EOL; + } + + return $html; + } + + /** + * Extend Row if chart is placed after nominal end of row. + * This code should be exercised by sample: + * Chart/32_Chart_read_write_PDF.php. + * However, that test is suppressed due to out-of-date + * Jpgraph code issuing warnings. So, don't measure + * code coverage for this function till that is fixed. + * + * @param int $row Row to check for charts + * + * @return array + * + * @codeCoverageIgnore + */ + private function extendRowsForCharts(Worksheet $worksheet, int $row) + { + $rowMax = $row; + $colMax = 'A'; + $anyfound = false; + if ($this->includeCharts) { + foreach ($worksheet->getChartCollection() as $chart) { + if ($chart instanceof Chart) { + $anyfound = true; + $chartCoordinates = $chart->getTopLeftPosition(); + $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); + $chartCol = Coordinate::columnIndexFromString($chartTL[0]); + if ($chartTL[1] > $rowMax) { + $rowMax = $chartTL[1]; + if ($chartCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $chartTL[0]; + } + } + } + } + } + + return [$rowMax, $colMax, $anyfound]; + } + + private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string + { + [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row); + + foreach ($worksheet->getDrawingCollection() as $drawing) { + $anyfound = true; + $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); + $imageCol = Coordinate::columnIndexFromString($imageTL[0]); + if ($imageTL[1] > $rowMax) { + $rowMax = $imageTL[1]; + if ($imageCol > Coordinate::columnIndexFromString($colMax)) { + $colMax = $imageTL[0]; + } + } + } + + // Don't extend rows if not needed + if ($row === $rowMax || !$anyfound) { + return ''; + } + + $html = ''; + ++$colMax; + ++$row; + while ($row <= $rowMax) { + $html .= ''; + for ($col = 'A'; $col != $colMax; ++$col) { + $htmlx = $this->writeImageInCell($worksheet, $col . $row); + $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : ''; + if ($htmlx) { + $html .= "$htmlx"; + } else { + $html .= ""; + } + } + ++$row; + $html .= '' . PHP_EOL; + } + + return $html; + } + + /** + * Convert Windows file name to file protocol URL. + * + * @param string $filename file name on local system + * + * @return string + */ + public static function winFileToUrl($filename) + { + // Windows filename + if (substr($filename, 1, 2) === ':\\') { + $filename = 'file:///' . str_replace('\\', '/', $filename); + } + + return $filename; + } + + /** + * Generate image tag in cell. + * + * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet + * @param string $coordinates Cell coordinates + * + * @return string + */ + private function writeImageInCell(Worksheet $worksheet, $coordinates) + { + // Construct HTML + $html = ''; + + // Write images + foreach ($worksheet->getDrawingCollection() as $drawing) { + if ($drawing->getCoordinates() != $coordinates) { + continue; + } + $filedesc = $drawing->getDescription(); + $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image'; + if ($drawing instanceof Drawing) { + $filename = $drawing->getPath(); + + // Strip off eventual '.' + $filename = preg_replace('/^[.]/', '', $filename); + + // Prepend images root + $filename = $this->getImagesRoot() . $filename; + + // Strip off eventual '.' if followed by non-/ + $filename = preg_replace('@^[.]([^/])@', '$1', $filename); + + // Convert UTF8 data to PCDATA + $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); + + $html .= PHP_EOL; + $imageData = self::winFileToUrl($filename); + + if ($this->embedImages && !$this->isPdf) { + $picture = @file_get_contents($filename); + if ($picture !== false) { + $imageDetails = getimagesize($filename); + // base64 encode the binary data + $base64 = base64_encode($picture); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + } + } + + $html .= '' . $filedesc . ''; + } elseif ($drawing instanceof MemoryDrawing) { + $imageResource = $drawing->getImageResource(); + if ($imageResource) { + ob_start(); // Let's start output buffering. + imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. + $contents = ob_get_contents(); // Instead, output above is saved to $contents + ob_end_clean(); // End the output buffer. + + $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents); + + // Because of the nature of tables, width is more important than height. + // max-width: 100% ensures that image doesnt overflow containing cell + // width: X sets width of supplied image. + // As a result, images bigger than cell will be contained and images smaller will not get stretched + $html .= '' . $filedesc . ''; + } + } + } + + return $html; + } + + /** + * Generate chart tag in cell. + * This code should be exercised by sample: + * Chart/32_Chart_read_write_PDF.php. + * However, that test is suppressed due to out-of-date + * Jpgraph code issuing warnings. So, don't measure + * code coverage for this function till that is fixed. + * + * @codeCoverageIgnore + */ + private function writeChartInCell(Worksheet $worksheet, string $coordinates): string + { + // Construct HTML + $html = ''; + + // Write charts + foreach ($worksheet->getChartCollection() as $chart) { + if ($chart instanceof Chart) { + $chartCoordinates = $chart->getTopLeftPosition(); + if ($chartCoordinates['cell'] == $coordinates) { + $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png'; + if (!$chart->render($chartFileName)) { + return ''; + } + + $html .= PHP_EOL; + $imageDetails = getimagesize($chartFileName); + $filedesc = $chart->getTitle(); + $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; + $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; + if ($fp = fopen($chartFileName, 'rb', 0)) { + $picture = fread($fp, filesize($chartFileName)); + fclose($fp); + // base64 encode the binary data + $base64 = base64_encode($picture); + $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; + + $html .= '' . $filedesc . '' . PHP_EOL; + + unlink($chartFileName); + } + } + } + } + + // Return + return $html; + } + + /** + * Generate CSS styles. + * + * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (<style> and </style>) + * + * @return string + */ + public function generateStyles($generateSurroundingHTML = true) + { + // Build CSS + $css = $this->buildCSS($generateSurroundingHTML); + + // Construct HTML + $html = ''; + + // Start styles + if ($generateSurroundingHTML) { + $html .= ' ' . PHP_EOL; + } + + // Return + return $html; + } + + private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void + { + // Calculate row heights + foreach ($sheet->getRowDimensions() as $rowDimension) { + $row = $rowDimension->getRowIndex() - 1; + + // table.sheetN tr.rowYYYYYY { } + $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = []; + + if ($rowDimension->getRowHeight() != -1) { + $pt_height = $rowDimension->getRowHeight(); + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt'; + } + if ($rowDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none'; + $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden'; + } + } + } + + private function buildCssPerSheet(Worksheet $sheet, array &$css): void + { + // Calculate hash code + $sheetIndex = $sheet->getParent()->getIndex($sheet); + + // Build styles + // Calculate column widths + $sheet->calculateColumnWidths(); + + // col elements, initialize + $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1; + $column = -1; + while ($column++ < $highestColumnIndex) { + $this->columnWidths[$sheetIndex][$column] = 42; // approximation + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt'; + } + + // col elements, loop through columnDimensions and set width + foreach ($sheet->getColumnDimensions() as $columnDimension) { + $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1; + $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont); + $width = SharedDrawing::pixelsToPoints($width); + if ($columnDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none'; + } + if ($width >= 0) { + $this->columnWidths[$sheetIndex][$column] = $width; + $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt'; + } + } + + // Default row height + $rowDimension = $sheet->getDefaultRowDimension(); + + // table.sheetN tr { } + $css['table.sheet' . $sheetIndex . ' tr'] = []; + + if ($rowDimension->getRowHeight() == -1) { + $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont()); + } else { + $pt_height = $rowDimension->getRowHeight(); + } + $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt'; + if ($rowDimension->getVisible() === false) { + $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none'; + $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden'; + } + + $this->buildCssRowHeights($sheet, $css, $sheetIndex); + } + + /** + * Build CSS styles. + * + * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { }) + * + * @return array + */ + public function buildCSS($generateSurroundingHTML = true) + { + // Cached? + if ($this->cssStyles !== null) { + return $this->cssStyles; + } + + // Ensure that spans have been calculated + $this->calculateSpans(); + + // Construct CSS + $css = []; + + // Start styles + if ($generateSurroundingHTML) { + // html { } + $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif'; + $css['html']['font-size'] = '11pt'; + $css['html']['background-color'] = 'white'; + } + + // CSS for comments as found in LibreOffice + $css['a.comment-indicator:hover + div.comment'] = [ + 'background' => '#ffd', + 'position' => 'absolute', + 'display' => 'block', + 'border' => '1px solid black', + 'padding' => '0.5em', + ]; + + $css['a.comment-indicator'] = [ + 'background' => 'red', + 'display' => 'inline-block', + 'border' => '1px solid black', + 'width' => '0.5em', + 'height' => '0.5em', + ]; + + $css['div.comment']['display'] = 'none'; + + // table { } + $css['table']['border-collapse'] = 'collapse'; + + // .b {} + $css['.b']['text-align'] = 'center'; // BOOL + + // .e {} + $css['.e']['text-align'] = 'center'; // ERROR + + // .f {} + $css['.f']['text-align'] = 'right'; // FORMULA + + // .inlineStr {} + $css['.inlineStr']['text-align'] = 'left'; // INLINE + + // .n {} + $css['.n']['text-align'] = 'right'; // NUMERIC + + // .s {} + $css['.s']['text-align'] = 'left'; // STRING + + // Calculate cell style hashes + foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { + $css['td.style' . $index] = $this->createCSSStyle($style); + $css['th.style' . $index] = $this->createCSSStyle($style); + } + + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Build styles per sheet + foreach ($sheets as $sheet) { + $this->buildCssPerSheet($sheet, $css); + } + + // Cache + if ($this->cssStyles === null) { + $this->cssStyles = $css; + } + + // Return + return $css; + } + + /** + * Create CSS style. + * + * @return array + */ + private function createCSSStyle(Style $style) + { + // Create CSS + return array_merge( + $this->createCSSStyleAlignment($style->getAlignment()), + $this->createCSSStyleBorders($style->getBorders()), + $this->createCSSStyleFont($style->getFont()), + $this->createCSSStyleFill($style->getFill()) + ); + } + + /** + * Create CSS style. + * + * @return array + */ + private function createCSSStyleAlignment(Alignment $alignment) + { + // Construct CSS + $css = []; + + // Create CSS + $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); + $textAlign = $this->mapHAlign($alignment->getHorizontal()); + if ($textAlign) { + $css['text-align'] = $textAlign; + if (in_array($textAlign, ['left', 'right'])) { + $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; + } + } + + return $css; + } + + /** + * Create CSS style. + * + * @return array + */ + private function createCSSStyleFont(Font $font) + { + // Construct CSS + $css = []; + + // Create CSS + if ($font->getBold()) { + $css['font-weight'] = 'bold'; + } + if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) { + $css['text-decoration'] = 'underline line-through'; + } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) { + $css['text-decoration'] = 'underline'; + } elseif ($font->getStrikethrough()) { + $css['text-decoration'] = 'line-through'; + } + if ($font->getItalic()) { + $css['font-style'] = 'italic'; + } + + $css['color'] = '#' . $font->getColor()->getRGB(); + $css['font-family'] = '\'' . $font->getName() . '\''; + $css['font-size'] = $font->getSize() . 'pt'; + + return $css; + } + + /** + * Create CSS style. + * + * @param Borders $borders Borders + * + * @return array + */ + private function createCSSStyleBorders(Borders $borders) + { + // Construct CSS + $css = []; + + // Create CSS + $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom()); + $css['border-top'] = $this->createCSSStyleBorder($borders->getTop()); + $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft()); + $css['border-right'] = $this->createCSSStyleBorder($borders->getRight()); + + return $css; + } + + /** + * Create CSS style. + * + * @param Border $border Border + * + * @return string + */ + private function createCSSStyleBorder(Border $border) + { + // Create CSS - add !important to non-none border styles for merged cells + $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); + + return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important'); + } + + /** + * Create CSS style (Fill). + * + * @param Fill $fill Fill + * + * @return array + */ + private function createCSSStyleFill(Fill $fill) + { + // Construct HTML + $css = []; + + // Create CSS + $value = $fill->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $fill->getStartColor()->getRGB(); + $css['background-color'] = $value; + + return $css; + } + + /** + * Generate HTML footer. + */ + public function generateHTMLFooter() + { + // Construct HTML + $html = ''; + $html .= ' ' . PHP_EOL; + $html .= '' . PHP_EOL; + + return $html; + } + + private function generateTableTagInline(Worksheet $worksheet, $id) + { + $style = isset($this->cssStyles['table']) ? + $this->assembleCSS($this->cssStyles['table']) : ''; + + $prntgrid = $worksheet->getPrintGridlines(); + $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines(); + if ($viewgrid && $prntgrid) { + $html = " " . PHP_EOL; + } elseif ($viewgrid) { + $html = "
" . PHP_EOL; + } elseif ($prntgrid) { + $html = "
" . PHP_EOL; + } else { + $html = "
" . PHP_EOL; + } + + return $html; + } + + private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void + { + if (!$this->useInlineCss) { + $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; + $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : ''; + $html .= "
" . PHP_EOL; + } else { + $html .= $this->generateTableTagInline($worksheet, $id); + } + } + + /** + * Generate table header. + * + * @param Worksheet $worksheet The worksheet for the table we are writing + * @param bool $showid whether or not to add id to table tag + * + * @return string + */ + private function generateTableHeader(Worksheet $worksheet, $showid = true) + { + $sheetIndex = $worksheet->getParent()->getIndex($worksheet); + + // Construct HTML + $html = ''; + $id = $showid ? "id='sheet$sheetIndex'" : ''; + if ($showid) { + $html .= "
\n"; + } else { + $html .= "
\n"; + } + + $this->generateTableTag($worksheet, $id, $html, $sheetIndex); + + // Write
elements + $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1; + $i = -1; + while ($i++ < $highestColumnIndex) { + if (!$this->useInlineCss) { + $html .= ' ' . PHP_EOL; + } else { + $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ? + $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : ''; + $html .= ' ' . PHP_EOL; + } + } + + return $html; + } + + /** + * Generate table footer. + */ + private function generateTableFooter() + { + return '
' . PHP_EOL . '' . PHP_EOL; + } + + /** + * Generate row start. + * + * @param int $sheetIndex Sheet index (0-based) + * @param int $row row number + * + * @return string + */ + private function generateRowStart(Worksheet $worksheet, $sheetIndex, $row) + { + $html = ''; + if (count($worksheet->getBreaks()) > 0) { + $breaks = $worksheet->getBreaks(); + + // check if a break is needed before this row + if (isset($breaks['A' . $row])) { + // close table: + $html .= $this->generateTableFooter(); + if ($this->isPdf && $this->useInlineCss) { + $html .= '
'; + } + + // open table again: + etc. + $html .= $this->generateTableHeader($worksheet, false); + $html .= '' . PHP_EOL; + } + } + + // Write row start + if (!$this->useInlineCss) { + $html .= ' ' . PHP_EOL; + } else { + $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) + ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : ''; + + $html .= ' ' . PHP_EOL; + } + + return $html; + } + + private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) + { + $cell = ($cellAddress > '') ? $worksheet->getCell($cellAddress) : ''; + $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); + if (!$this->useInlineCss) { + $cssClass = 'column' . $columnNumber; + } else { + $cssClass = []; + // The statements below do nothing. + // Commenting out the code rather than deleting it + // in case someone can figure out what their intent was. + //if ($cellType == 'th') { + // if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) { + // $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum]; + // } + //} else { + // if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) { + // $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum]; + // } + //} + // End of mystery statements. + } + + return [$cell, $cssClass, $coordinate]; + } + + private function generateRowCellDataValueRich($cell, &$cellData): void + { + // Loop through rich text elements + $elements = $cell->getValue()->getRichTextElements(); + foreach ($elements as $element) { + // Rich text start? + if ($element instanceof Run) { + $cellData .= ''; + + $cellEnd = ''; + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + $cellEnd = ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + $cellEnd = ''; + } + + // Convert UTF8 data to PCDATA + $cellText = $element->getText(); + $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags()); + + $cellData .= $cellEnd; + + $cellData .= ''; + } else { + // Convert UTF8 data to PCDATA + $cellText = $element->getText(); + $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags()); + } + } + } + + private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void + { + if ($cell->getValue() instanceof RichText) { + $this->generateRowCellDataValueRich($cell, $cellData); + } else { + $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); + $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); + if ($formatCode !== null) { + $cellData = NumberFormat::toFormattedString( + $origData, + $formatCode, + [$this, 'formatColor'] + ); + } + + if ($cellData === $origData) { + $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); + } + if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { + $cellData = '' . $cellData . ''; + } elseif ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) { + $cellData = '' . $cellData . ''; + } + } + } + + private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) + { + $cellData = ' '; + if ($cell instanceof Cell) { + $cellData = ''; + // Don't know what this does, and no test cases. + //if ($cell->getParent() === null) { + // $cell->attach($worksheet); + //} + // Value + $this->generateRowCellDataValue($worksheet, $cell, $cellData); + + // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   + // Example: " Hello\n to the world" is converted to "  Hello\n to the world" + $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + + // convert newline "\n" to '
' + $cellData = nl2br($cellData); + + // Extend CSS class? + if (!$this->useInlineCss) { + $cssClass .= ' style' . $cell->getXfIndex(); + $cssClass .= ' ' . $cell->getDataType(); + } else { + if ($cellType == 'th') { + if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { + $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); + } + } else { + if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) { + $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]); + } + } + + // General horizontal alignment: Actual horizontal alignment depends on dataType + $sharedStyle = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex()); + if ( + $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL + && isset($this->cssStyles['.' . $cell->getDataType()]['text-align']) + ) { + $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align']; + } + } + } else { + // Use default borders for empty cell + if (is_string($cssClass)) { + $cssClass .= ' style0'; + } + } + + return $cellData; + } + + private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) + { + return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; + } + + private function generateRowSpans($html, $rowSpan, $colSpan) + { + $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; + $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; + + return $html; + } + + private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void + { + // Image? + $htmlx = $this->writeImageInCell($worksheet, $coordinate); + // Chart? + $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); + // Column start + $html .= ' <' . $cellType; + if (!$this->useInlineCss && !$this->isPdf) { + $html .= ' class="' . $cssClass . '"'; + if ($htmlx) { + $html .= " style='position: relative;'"; + } + } else { + //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf ** + // We must explicitly write the width of the + if ($this->useInlineCss) { + $xcssClass = $cssClass; + } else { + $html .= ' class="' . $cssClass . '"'; + $xcssClass = []; + } + $width = 0; + $i = $colNum - 1; + $e = $colNum + $colSpan - 1; + while ($i++ < $e) { + if (isset($this->columnWidths[$sheetIndex][$i])) { + $width += $this->columnWidths[$sheetIndex][$i]; + } + } + $xcssClass['width'] = $width . 'pt'; + + // We must also explicitly write the height of the + if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { + $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height']; + $xcssClass['height'] = $height; + } + //** end of redundant code ** + + if ($htmlx) { + $xcssClass['position'] = 'relative'; + } + $html .= ' style="' . $this->assembleCSS($xcssClass) . '"'; + } + $html = $this->generateRowSpans($html, $rowSpan, $colSpan); + + $html .= '>'; + $html .= $htmlx; + + $html .= $this->writeComment($worksheet, $coordinate); + + // Cell data + $html .= $cellData; + + // Column end + $html .= '' . PHP_EOL; + } + + /** + * Generate row. + * + * @param array $values Array containing cells in a row + * @param int $row Row number (0-based) + * @param string $cellType eg: 'td' + * + * @return string + */ + private function generateRow(Worksheet $worksheet, array $values, $row, $cellType) + { + // Sheet index + $sheetIndex = $worksheet->getParent()->getIndex($worksheet); + $html = $this->generateRowStart($worksheet, $sheetIndex, $row); + + // Write cells + $colNum = 0; + foreach ($values as $cellAddress) { + [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); + + $colSpan = 1; + $rowSpan = 1; + + // Cell Data + $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); + + // Hyperlink? + if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { + $cellData = '' . $cellData . ''; + } + + // Should the cell be written or is it swallowed by a rowspan or colspan? + $writeCell = !(isset($this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]) + && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); + + // Colspan and Rowspan + $colspan = 1; + $rowspan = 1; + if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { + $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; + $rowSpan = $spans['rowspan']; + $colSpan = $spans['colspan']; + + // Also apply style from last cell in merge to fix borders - + // relies on !important for non-none border declarations in createCSSStyleBorder + $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan); + if (!$this->useInlineCss) { + $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex(); + } + } + + // Write + if ($writeCell) { + $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row); + } + + // Next column + ++$colNum; + } + + // Write row end + $html .= ' ' . PHP_EOL; + + // Return + return $html; + } + + /** + * Takes array where of CSS properties / values and converts to CSS string. + * + * @return string + */ + private function assembleCSS(array $values = []) + { + $pairs = []; + foreach ($values as $property => $value) { + $pairs[] = $property . ':' . $value; + } + $string = implode('; ', $pairs); + + return $string; + } + + /** + * Get images root. + * + * @return string + */ + public function getImagesRoot() + { + return $this->imagesRoot; + } + + /** + * Set images root. + * + * @param string $imagesRoot + * + * @return $this + */ + public function setImagesRoot($imagesRoot) + { + $this->imagesRoot = $imagesRoot; + + return $this; + } + + /** + * Get embed images. + * + * @return bool + */ + public function getEmbedImages() + { + return $this->embedImages; + } + + /** + * Set embed images. + * + * @param bool $embedImages + * + * @return $this + */ + public function setEmbedImages($embedImages) + { + $this->embedImages = $embedImages; + + return $this; + } + + /** + * Get use inline CSS? + * + * @return bool + */ + public function getUseInlineCss() + { + return $this->useInlineCss; + } + + /** + * Set use inline CSS? + * + * @param bool $useInlineCss + * + * @return $this + */ + public function setUseInlineCss($useInlineCss) + { + $this->useInlineCss = $useInlineCss; + + return $this; + } + + /** + * Get use embedded CSS? + * + * @return bool + * + * @codeCoverageIgnore + * + * @deprecated no longer used + */ + public function getUseEmbeddedCSS() + { + return $this->useEmbeddedCSS; + } + + /** + * Set use embedded CSS? + * + * @param bool $useEmbeddedCSS + * + * @return $this + * + * @codeCoverageIgnore + * + * @deprecated no longer used + */ + public function setUseEmbeddedCSS($useEmbeddedCSS) + { + $this->useEmbeddedCSS = $useEmbeddedCSS; + + return $this; + } + + /** + * Add color to formatted string as inline style. + * + * @param string $value Plain formatted value without color + * @param string $format Format code + * + * @return string + */ + public function formatColor($value, $format) + { + // Color information, e.g. [Red] is always at the beginning + $color = null; // initialize + $matches = []; + + $color_regex = '/^\\[[a-zA-Z]+\\]/'; + if (preg_match($color_regex, $format, $matches)) { + $color = str_replace(['[', ']'], '', $matches[0]); + $color = strtolower($color); + } + + // convert to PCDATA + $result = htmlspecialchars($value, Settings::htmlEntityFlags()); + + // color span tag + if ($color !== null) { + $result = '' . $result . ''; + } + + return $result; + } + + /** + * Calculate information about HTML colspan and rowspan which is not always the same as Excel's. + */ + private function calculateSpans(): void + { + if ($this->spansAreCalculated) { + return; + } + // Identify all cells that should be omitted in HTML due to cell merge. + // In HTML only the upper-left cell should be written and it should have + // appropriate rowspan / colspan attribute + $sheetIndexes = $this->sheetIndex !== null ? + [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1); + + foreach ($sheetIndexes as $sheetIndex) { + $sheet = $this->spreadsheet->getSheet($sheetIndex); + + $candidateSpannedRow = []; + + // loop through all Excel merged cells + foreach ($sheet->getMergeCells() as $cells) { + [$cells] = Coordinate::splitRange($cells); + $first = $cells[0]; + $last = $cells[1]; + + [$fc, $fr] = Coordinate::indexesFromString($first); + $fc = $fc - 1; + + [$lc, $lr] = Coordinate::indexesFromString($last); + $lc = $lc - 1; + + // loop through the individual cells in the individual merge + $r = $fr - 1; + while ($r++ < $lr) { + // also, flag this row as a HTML row that is candidate to be omitted + $candidateSpannedRow[$r] = $r; + + $c = $fc - 1; + while ($c++ < $lc) { + if (!($c == $fc && $r == $fr)) { + // not the upper-left cell (should not be written in HTML) + $this->isSpannedCell[$sheetIndex][$r][$c] = [ + 'baseCell' => [$fr, $fc], + ]; + } else { + // upper-left is the base cell that should hold the colspan/rowspan attribute + $this->isBaseCell[$sheetIndex][$r][$c] = [ + 'xlrowspan' => $lr - $fr + 1, // Excel rowspan + 'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change + 'xlcolspan' => $lc - $fc + 1, // Excel colspan + 'colspan' => $lc - $fc + 1, // HTML colspan, value may change + ]; + } + } + } + } + + $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow); + + // TODO: Same for columns + } + + // We have calculated the spans + $this->spansAreCalculated = true; + } + + private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void + { + // Identify which rows should be omitted in HTML. These are the rows where all the cells + // participate in a merge and the where base cells are somewhere above. + $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn()); + foreach ($candidateSpannedRow as $rowIndex) { + if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) { + if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) { + $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex; + } + } + } + + // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1 + if (isset($this->isSpannedRow[$sheetIndex])) { + foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) { + $adjustedBaseCells = []; + $c = -1; + $e = $countColumns - 1; + while ($c++ < $e) { + $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; + + if (!in_array($baseCell, $adjustedBaseCells)) { + // subtract rowspan by 1 + --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; + $adjustedBaseCells[] = $baseCell; + } + } + } + } + } + + /** + * Write a comment in the same format as LibreOffice. + * + * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092 + * + * @param string $coordinate + * + * @return string + */ + private function writeComment(Worksheet $worksheet, $coordinate) + { + $result = ''; + if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) { + $sanitizer = new HTMLPurifier(); + $cachePath = File::sysGetTempDir() . '/phpsppur'; + if (is_dir($cachePath) || mkdir($cachePath)) { + $sanitizer->config->set('Cache.SerializerPath', $cachePath); + } + $sanitizedString = $sanitizer->purify($worksheet->getComment($coordinate)->getText()->getPlainText()); + if ($sanitizedString !== '') { + $result .= ''; + $result .= '
' . nl2br($sanitizedString) . '
'; + $result .= PHP_EOL; + } + } + + return $result; + } + + public function getOrientation(): ?string + { + return null; + } + + /** + * Generate @page declarations. + * + * @param bool $generateSurroundingHTML + * + * @return string + */ + private function generatePageDeclarations($generateSurroundingHTML) + { + // Ensure that Spans have been calculated? + $this->calculateSpans(); + + // Fetch sheets + $sheets = []; + if ($this->sheetIndex === null) { + $sheets = $this->spreadsheet->getAllSheets(); + } else { + $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex); + } + + // Construct HTML + $htmlPage = $generateSurroundingHTML ? ('' . PHP_EOL) : ''; + + return $htmlPage; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php new file mode 100644 index 0000000..b0a6272 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php @@ -0,0 +1,91 @@ +setSpreadsheet($spreadsheet); + + $this->writerPartContent = new Content($this); + $this->writerPartMeta = new Meta($this); + $this->writerPartMetaInf = new MetaInf($this); + $this->writerPartMimetype = new Mimetype($this); + $this->writerPartSettings = new Settings($this); + $this->writerPartStyles = new Styles($this); + $this->writerPartThumbnails = new Thumbnails($this); + } + + public function getWriterPartContent(): Content + { + return $this->writerPartContent; + } + + public function getWriterPartMeta(): Meta + { + return $this->writerPartMeta; + } + + public function getWriterPartMetaInf(): MetaInf + { + return $this->writerPartMetaInf; + } + + public function getWriterPartMimetype(): Mimetype + { + return $this->writerPartMimetype; + } + + public function getWriterPartSettings(): Settings + { + return $this->writerPartSettings; + } + + public function getWriterPartStyles(): Styles + { + return $this->writerPartStyles; + } + + public function getWriterPartThumbnails(): Thumbnails + { + return $this->writerPartThumbnails; + } + + /** + * Save PhpSpreadsheet to file. + * + * @param resource|string $filename + */ + public function save($filename, int $flags = 0): void + { + if (!$this->spreadSheet) { + throw new WriterException('PhpSpreadsheet object unassigned.'); + } + + $this->processFlags($flags); + + // garbage collect + $this->spreadSheet->garbageCollect(); + + $this->openFileHandle($filename); + + $zip = $this->createZip(); + + $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); + $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->write()); + $zip->addFile('content.xml', $this->getWriterPartcontent()->write()); + $zip->addFile('meta.xml', $this->getWriterPartmeta()->write()); + $zip->addFile('mimetype', $this->getWriterPartmimetype()->write()); + $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); + $zip->addFile('styles.xml', $this->getWriterPartstyles()->write()); + + // Close file + try { + $zip->finish(); + } catch (OverflowException $e) { + throw new WriterException('Could not close resource.'); + } + + $this->maybeCloseFileHandle(); + } + + /** + * Create zip object. + * + * @return ZipStream + */ + private function createZip() + { + // Try opening the ZIP file + if (!is_resource($this->fileHandle)) { + throw new WriterException('Could not open resource for writing.'); + } + + // Create new ZIP stream + $options = new Archive(); + $options->setEnableZip64(false); + $options->setOutputStream($this->fileHandle); + + return new ZipStream(null, $options); + } + + /** + * Get Spreadsheet object. + * + * @return Spreadsheet + */ + public function getSpreadsheet() + { + if ($this->spreadSheet !== null) { + return $this->spreadSheet; + } + + throw new WriterException('No PhpSpreadsheet assigned.'); + } + + /** + * Set Spreadsheet object. + * + * @param Spreadsheet $spreadsheet PhpSpreadsheet object + * + * @return $this + */ + public function setSpreadsheet(Spreadsheet $spreadsheet) + { + $this->spreadSheet = $spreadsheet; + + return $this; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Formula.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Formula.php new file mode 100644 index 0000000..db766fb --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Formula.php @@ -0,0 +1,119 @@ +definedNames[] = $definedName->getName(); + } + } + + public function convertFormula(string $formula, string $worksheetName = ''): string + { + $formula = $this->convertCellReferences($formula, $worksheetName); + $formula = $this->convertDefinedNames($formula); + + if (substr($formula, 0, 1) !== '=') { + $formula = '=' . $formula; + } + + return 'of:' . $formula; + } + + private function convertDefinedNames(string $formula): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '/mui', + $formula, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $offsets = array_column($splitRanges[0], 1); + $values = array_column($splitRanges[0], 0); + + while ($splitCount > 0) { + --$splitCount; + $length = $lengths[$splitCount]; + $offset = $offsets[$splitCount]; + $value = $values[$splitCount]; + + if (in_array($value, $this->definedNames, true)) { + $formula = substr($formula, 0, $offset) . '$$' . $value . substr($formula, $offset + $length); + } + } + + return $formula; + } + + private function convertCellReferences(string $formula, string $worksheetName): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', + $formula, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $offsets = array_column($splitRanges[0], 1); + + $worksheets = $splitRanges[2]; + $columns = $splitRanges[6]; + $rows = $splitRanges[7]; + + // Replace any commas in the formula with semi-colons for Ods + // If by chance there are commas in worksheet names, then they will be "fixed" again in the loop + // because we've already extracted worksheet names with our preg_match_all() + $formula = str_replace(',', ';', $formula); + while ($splitCount > 0) { + --$splitCount; + $length = $lengths[$splitCount]; + $offset = $offsets[$splitCount]; + $worksheet = $worksheets[$splitCount][0]; + $column = $columns[$splitCount][0]; + $row = $rows[$splitCount][0]; + + $newRange = ''; + if (empty($worksheet)) { + if (($offset === 0) || ($formula[$offset - 1] !== ':')) { + // We need a worksheet + $worksheet = $worksheetName; + } + } else { + $worksheet = str_replace("''", "'", trim($worksheet, "'")); + } + if (!empty($worksheet)) { + $newRange = "['" . str_replace("'", "''", $worksheet) . "'"; + } elseif (substr($formula, $offset - 1, 1) !== ':') { + $newRange = '['; + } + $newRange .= '.'; + + if (!empty($column)) { + $newRange .= $column; + } + if (!empty($row)) { + $newRange .= $row; + } + // close the wrapping [] unless this is the first part of a range + $newRange .= substr($formula, $offset + $length, 1) !== ':' ? ']' : ''; + + $formula = substr($formula, 0, $offset) . $newRange . substr($formula, $offset + $length); + } + + return $formula; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Meta.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Meta.php new file mode 100644 index 0000000..16f7c8b --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -0,0 +1,122 @@ +getParentWriter()->getSpreadsheet(); + + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Meta + $objWriter->startElement('office:document-meta'); + + $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); + $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'); + $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); + $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#'); + $objWriter->writeAttribute('office:version', '1.2'); + + $objWriter->startElement('office:meta'); + + $objWriter->writeElement('meta:initial-creator', $spreadsheet->getProperties()->getCreator()); + $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator()); + $created = $spreadsheet->getProperties()->getCreated(); + $date = Date::dateTimeFromTimestamp("$created"); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $objWriter->writeElement('meta:creation-date', $date->format(DATE_W3C)); + $created = $spreadsheet->getProperties()->getModified(); + $date = Date::dateTimeFromTimestamp("$created"); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $objWriter->writeElement('dc:date', $date->format(DATE_W3C)); + $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle()); + $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription()); + $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject()); + $objWriter->writeElement('meta:keyword', $spreadsheet->getProperties()->getKeywords()); + // Don't know if this changed over time, but the keywords are all + // in a single declaration now. + //$keywords = explode(' ', $spreadsheet->getProperties()->getKeywords()); + //foreach ($keywords as $keyword) { + // $objWriter->writeElement('meta:keyword', $keyword); + //} + + // + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', 'Company'); + $objWriter->writeRaw($spreadsheet->getProperties()->getCompany()); + $objWriter->endElement(); + + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', 'category'); + $objWriter->writeRaw($spreadsheet->getProperties()->getCategory()); + $objWriter->endElement(); + + self::writeDocPropsCustom($objWriter, $spreadsheet); + + $objWriter->endElement(); + + $objWriter->endElement(); + + return $objWriter->getData(); + } + + private static function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + { + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + foreach ($customPropertyList as $key => $customProperty) { + $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty); + $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty); + + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', $customProperty); + + switch ($propertyType) { + case Properties::PROPERTY_TYPE_INTEGER: + case Properties::PROPERTY_TYPE_FLOAT: + $objWriter->writeAttribute('meta:value-type', 'float'); + $objWriter->writeRawData($propertyValue); + + break; + case Properties::PROPERTY_TYPE_BOOLEAN: + $objWriter->writeAttribute('meta:value-type', 'boolean'); + $objWriter->writeRawData($propertyValue ? 'true' : 'false'); + + break; + case Properties::PROPERTY_TYPE_DATE: + $objWriter->writeAttribute('meta:value-type', 'date'); + $dtobj = Date::dateTimeFromTimestamp($propertyValue ?? 0); + $objWriter->writeRawData($dtobj->format(DATE_W3C)); + + break; + default: + $objWriter->writeRawData($propertyValue); + + break; + } + + $objWriter->endElement(); + } + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/MetaInf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/MetaInf.php new file mode 100644 index 0000000..f3f0d5f --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/MetaInf.php @@ -0,0 +1,60 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8'); + + // Manifest + $objWriter->startElement('manifest:manifest'); + $objWriter->writeAttribute('xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'); + $objWriter->writeAttribute('manifest:version', '1.2'); + + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', '/'); + $objWriter->writeAttribute('manifest:version', '1.2'); + $objWriter->writeAttribute('manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'meta.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'settings.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'content.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'Thumbnails/thumbnail.png'); + $objWriter->writeAttribute('manifest:media-type', 'image/png'); + $objWriter->endElement(); + $objWriter->startElement('manifest:file-entry'); + $objWriter->writeAttribute('manifest:full-path', 'styles.xml'); + $objWriter->writeAttribute('manifest:media-type', 'text/xml'); + $objWriter->endElement(); + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Mimetype.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Mimetype.php new file mode 100644 index 0000000..e109e6e --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Mimetype.php @@ -0,0 +1,16 @@ +objWriter = $objWriter; + $this->spreadsheet = $spreadsheet; + $this->formulaConvertor = $formulaConvertor; + } + + public function write(): string + { + $this->objWriter->startElement('table:named-expressions'); + $this->writeExpressions(); + $this->objWriter->endElement(); + + return ''; + } + + private function writeExpressions(): void + { + $definedNames = $this->spreadsheet->getDefinedNames(); + + foreach ($definedNames as $definedName) { + if ($definedName->isFormula()) { + $this->objWriter->startElement('table:named-expression'); + $this->writeNamedFormula($definedName, $this->spreadsheet->getActiveSheet()); + } else { + $this->objWriter->startElement('table:named-range'); + $this->writeNamedRange($definedName); + } + + $this->objWriter->endElement(); + } + } + + private function writeNamedFormula(DefinedName $definedName, Worksheet $defaultWorksheet): void + { + $title = ($definedName->getWorksheet() !== null) ? $definedName->getWorksheet()->getTitle() : $defaultWorksheet->getTitle(); + $this->objWriter->writeAttribute('table:name', $definedName->getName()); + $this->objWriter->writeAttribute( + 'table:expression', + $this->formulaConvertor->convertFormula($definedName->getValue(), $title) + ); + $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress( + $definedName, + "'" . $title . "'!\$A\$1" + )); + } + + private function writeNamedRange(DefinedName $definedName): void + { + $baseCell = '$A$1'; + $ws = $definedName->getWorksheet(); + if ($ws !== null) { + $baseCell = "'" . $ws->getTitle() . "'!$baseCell"; + } + $this->objWriter->writeAttribute('table:name', $definedName->getName()); + $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress( + $definedName, + $baseCell + )); + $this->objWriter->writeAttribute('table:cell-range-address', $this->convertAddress($definedName, $definedName->getValue())); + } + + private function convertAddress(DefinedName $definedName, string $address): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', + $address, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $offsets = array_column($splitRanges[0], 1); + + $worksheets = $splitRanges[2]; + $columns = $splitRanges[6]; + $rows = $splitRanges[7]; + + while ($splitCount > 0) { + --$splitCount; + $length = $lengths[$splitCount]; + $offset = $offsets[$splitCount]; + $worksheet = $worksheets[$splitCount][0]; + $column = $columns[$splitCount][0]; + $row = $rows[$splitCount][0]; + + $newRange = ''; + if (empty($worksheet)) { + if (($offset === 0) || ($address[$offset - 1] !== ':')) { + // We need a worksheet + $ws = $definedName->getWorksheet(); + if ($ws !== null) { + $worksheet = $ws->getTitle(); + } + } + } else { + $worksheet = str_replace("''", "'", trim($worksheet, "'")); + } + if (!empty($worksheet)) { + $newRange = "'" . str_replace("'", "''", $worksheet) . "'."; + } + + if (!empty($column)) { + $newRange .= $column; + } + if (!empty($row)) { + $newRange .= $row; + } + + $address = substr($address, 0, $offset) . $newRange . substr($address, $offset + $length); + } + + if (substr($address, 0, 1) === '=') { + $address = substr($address, 1); + } + + return $address; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php new file mode 100644 index 0000000..493bbba --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php @@ -0,0 +1,251 @@ + 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_LETTER_SMALL => 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_TABLOID => [792.00, 1224.00], // (11 in. by 17 in.) + PageSetup::PAPERSIZE_LEDGER => [1224.00, 792.00], // (17 in. by 11 in.) + PageSetup::PAPERSIZE_LEGAL => 'LEGAL', // (8.5 in. by 14 in.) + PageSetup::PAPERSIZE_STATEMENT => [396.00, 612.00], // (5.5 in. by 8.5 in.) + PageSetup::PAPERSIZE_EXECUTIVE => 'EXECUTIVE', // (7.25 in. by 10.5 in.) + PageSetup::PAPERSIZE_A3 => 'A3', // (297 mm by 420 mm) + PageSetup::PAPERSIZE_A4 => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_A4_SMALL => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_A5 => 'A5', // (148 mm by 210 mm) + PageSetup::PAPERSIZE_B4 => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_B5 => 'B5', // (176 mm by 250 mm) + PageSetup::PAPERSIZE_FOLIO => 'FOLIO', // (8.5 in. by 13 in.) + PageSetup::PAPERSIZE_QUARTO => [609.45, 779.53], // (215 mm by 275 mm) + PageSetup::PAPERSIZE_STANDARD_1 => [720.00, 1008.00], // (10 in. by 14 in.) + PageSetup::PAPERSIZE_STANDARD_2 => [792.00, 1224.00], // (11 in. by 17 in.) + PageSetup::PAPERSIZE_NOTE => 'LETTER', // (8.5 in. by 11 in.) + PageSetup::PAPERSIZE_NO9_ENVELOPE => [279.00, 639.00], // (3.875 in. by 8.875 in.) + PageSetup::PAPERSIZE_NO10_ENVELOPE => [297.00, 684.00], // (4.125 in. by 9.5 in.) + PageSetup::PAPERSIZE_NO11_ENVELOPE => [324.00, 747.00], // (4.5 in. by 10.375 in.) + PageSetup::PAPERSIZE_NO12_ENVELOPE => [342.00, 792.00], // (4.75 in. by 11 in.) + PageSetup::PAPERSIZE_NO14_ENVELOPE => [360.00, 828.00], // (5 in. by 11.5 in.) + PageSetup::PAPERSIZE_C => [1224.00, 1584.00], // (17 in. by 22 in.) + PageSetup::PAPERSIZE_D => [1584.00, 2448.00], // (22 in. by 34 in.) + PageSetup::PAPERSIZE_E => [2448.00, 3168.00], // (34 in. by 44 in.) + PageSetup::PAPERSIZE_DL_ENVELOPE => [311.81, 623.62], // (110 mm by 220 mm) + PageSetup::PAPERSIZE_C5_ENVELOPE => 'C5', // (162 mm by 229 mm) + PageSetup::PAPERSIZE_C3_ENVELOPE => 'C3', // (324 mm by 458 mm) + PageSetup::PAPERSIZE_C4_ENVELOPE => 'C4', // (229 mm by 324 mm) + PageSetup::PAPERSIZE_C6_ENVELOPE => 'C6', // (114 mm by 162 mm) + PageSetup::PAPERSIZE_C65_ENVELOPE => [323.15, 649.13], // (114 mm by 229 mm) + PageSetup::PAPERSIZE_B4_ENVELOPE => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_B5_ENVELOPE => 'B5', // (176 mm by 250 mm) + PageSetup::PAPERSIZE_B6_ENVELOPE => [498.90, 354.33], // (176 mm by 125 mm) + PageSetup::PAPERSIZE_ITALY_ENVELOPE => [311.81, 651.97], // (110 mm by 230 mm) + PageSetup::PAPERSIZE_MONARCH_ENVELOPE => [279.00, 540.00], // (3.875 in. by 7.5 in.) + PageSetup::PAPERSIZE_6_3_4_ENVELOPE => [261.00, 468.00], // (3.625 in. by 6.5 in.) + PageSetup::PAPERSIZE_US_STANDARD_FANFOLD => [1071.00, 792.00], // (14.875 in. by 11 in.) + PageSetup::PAPERSIZE_GERMAN_STANDARD_FANFOLD => [612.00, 864.00], // (8.5 in. by 12 in.) + PageSetup::PAPERSIZE_GERMAN_LEGAL_FANFOLD => 'FOLIO', // (8.5 in. by 13 in.) + PageSetup::PAPERSIZE_ISO_B4 => 'B4', // (250 mm by 353 mm) + PageSetup::PAPERSIZE_JAPANESE_DOUBLE_POSTCARD => [566.93, 419.53], // (200 mm by 148 mm) + PageSetup::PAPERSIZE_STANDARD_PAPER_1 => [648.00, 792.00], // (9 in. by 11 in.) + PageSetup::PAPERSIZE_STANDARD_PAPER_2 => [720.00, 792.00], // (10 in. by 11 in.) + PageSetup::PAPERSIZE_STANDARD_PAPER_3 => [1080.00, 792.00], // (15 in. by 11 in.) + PageSetup::PAPERSIZE_INVITE_ENVELOPE => [623.62, 623.62], // (220 mm by 220 mm) + PageSetup::PAPERSIZE_LETTER_EXTRA_PAPER => [667.80, 864.00], // (9.275 in. by 12 in.) + PageSetup::PAPERSIZE_LEGAL_EXTRA_PAPER => [667.80, 1080.00], // (9.275 in. by 15 in.) + PageSetup::PAPERSIZE_TABLOID_EXTRA_PAPER => [841.68, 1296.00], // (11.69 in. by 18 in.) + PageSetup::PAPERSIZE_A4_EXTRA_PAPER => [668.98, 912.76], // (236 mm by 322 mm) + PageSetup::PAPERSIZE_LETTER_TRANSVERSE_PAPER => [595.80, 792.00], // (8.275 in. by 11 in.) + PageSetup::PAPERSIZE_A4_TRANSVERSE_PAPER => 'A4', // (210 mm by 297 mm) + PageSetup::PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER => [667.80, 864.00], // (9.275 in. by 12 in.) + PageSetup::PAPERSIZE_SUPERA_SUPERA_A4_PAPER => [643.46, 1009.13], // (227 mm by 356 mm) + PageSetup::PAPERSIZE_SUPERB_SUPERB_A3_PAPER => [864.57, 1380.47], // (305 mm by 487 mm) + PageSetup::PAPERSIZE_LETTER_PLUS_PAPER => [612.00, 913.68], // (8.5 in. by 12.69 in.) + PageSetup::PAPERSIZE_A4_PLUS_PAPER => [595.28, 935.43], // (210 mm by 330 mm) + PageSetup::PAPERSIZE_A5_TRANSVERSE_PAPER => 'A5', // (148 mm by 210 mm) + PageSetup::PAPERSIZE_JIS_B5_TRANSVERSE_PAPER => [515.91, 728.50], // (182 mm by 257 mm) + PageSetup::PAPERSIZE_A3_EXTRA_PAPER => [912.76, 1261.42], // (322 mm by 445 mm) + PageSetup::PAPERSIZE_A5_EXTRA_PAPER => [493.23, 666.14], // (174 mm by 235 mm) + PageSetup::PAPERSIZE_ISO_B5_EXTRA_PAPER => [569.76, 782.36], // (201 mm by 276 mm) + PageSetup::PAPERSIZE_A2_PAPER => 'A2', // (420 mm by 594 mm) + PageSetup::PAPERSIZE_A3_TRANSVERSE_PAPER => 'A3', // (297 mm by 420 mm) + PageSetup::PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER => [912.76, 1261.42], // (322 mm by 445 mm) + ]; + + /** + * Create a new PDF Writer instance. + * + * @param Spreadsheet $spreadsheet Spreadsheet object + */ + public function __construct(Spreadsheet $spreadsheet) + { + parent::__construct($spreadsheet); + //$this->setUseInlineCss(true); + $this->tempDir = File::sysGetTempDir() . '/phpsppdf'; + $this->isPdf = true; + } + + /** + * Get Font. + * + * @return string + */ + public function getFont() + { + return $this->font; + } + + /** + * Set font. Examples: + * 'arialunicid0-chinese-simplified' + * 'arialunicid0-chinese-traditional' + * 'arialunicid0-korean' + * 'arialunicid0-japanese'. + * + * @param string $fontName + * + * @return $this + */ + public function setFont($fontName) + { + $this->font = $fontName; + + return $this; + } + + /** + * Get Paper Size. + * + * @return ?int + */ + public function getPaperSize() + { + return $this->paperSize; + } + + /** + * Set Paper Size. + * + * @param int $paperSize Paper size see PageSetup::PAPERSIZE_* + * + * @return self + */ + public function setPaperSize($paperSize) + { + $this->paperSize = $paperSize; + + return $this; + } + + /** + * Get Orientation. + */ + public function getOrientation(): ?string + { + return $this->orientation; + } + + /** + * Set Orientation. + * + * @param string $orientation Page orientation see PageSetup::ORIENTATION_* + * + * @return self + */ + public function setOrientation($orientation) + { + $this->orientation = $orientation; + + return $this; + } + + /** + * Get temporary storage directory. + * + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } + + /** + * Set temporary storage directory. + * + * @param string $temporaryDirectory Temporary storage directory + * + * @return self + */ + public function setTempDir($temporaryDirectory) + { + if (is_dir($temporaryDirectory)) { + $this->tempDir = $temporaryDirectory; + } else { + throw new WriterException("Directory does not exist: $temporaryDirectory"); + } + + return $this; + } + + /** + * Save Spreadsheet to PDF file, pre-save. + * + * @param string $filename Name of the file to save as + * + * @return resource + */ + protected function prepareForSave($filename) + { + // Open file + $this->openFileHandle($filename); + + return $this->fileHandle; + } + + /** + * Save PhpSpreadsheet to PDF file, post-save. + */ + protected function restoreStateAfterSave(): void + { + $this->maybeCloseFileHandle(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php new file mode 100644 index 0000000..fc96f90 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -0,0 +1,53 @@ +spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); + $orientation = $this->getOrientation() ?? $setup->getOrientation(); + $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); + $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + + $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; + + // Create PDF + $pdf = $this->createExternalWriterInstance(); + $pdf->setPaper($paperSize, $orientation); + + $pdf->loadHtml($this->generateHTMLAll()); + $pdf->render(); + + // Write to file + fwrite($fileHandle, $pdf->output() ?? ''); + + parent::restoreStateAfterSave(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php new file mode 100644 index 0000000..281e1a4 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -0,0 +1,90 @@ +spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); + $orientation = $this->getOrientation() ?? $setup->getOrientation(); + $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; + $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); + $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + + // Create PDF + $config = ['tempDir' => $this->tempDir . '/mpdf']; + $pdf = $this->createExternalWriterInstance($config); + $ortmp = $orientation; + $pdf->_setPageSize($paperSize, $ortmp); + $pdf->DefOrientation = $orientation; + $pdf->AddPageByArray([ + 'orientation' => $orientation, + 'margin-left' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getLeft()), + 'margin-right' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getRight()), + 'margin-top' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getTop()), + 'margin-bottom' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getBottom()), + ]); + + // Document info + $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle()); + $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator()); + $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject()); + $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords()); + $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); + + $html = $this->generateHTMLAll(); + $bodyLocation = strpos($html, Html::BODY_LINE); + // Make sure first data presented to Mpdf includes body tag + // so that Mpdf doesn't parse it as content. Issue 2432. + if ($bodyLocation !== false) { + $bodyLocation += strlen(Html::BODY_LINE); + $pdf->WriteHTML(substr($html, 0, $bodyLocation)); + $html = substr($html, $bodyLocation); + } + foreach (\array_chunk(\explode(PHP_EOL, $html), 1000) as $lines) { + $pdf->WriteHTML(\implode(PHP_EOL, $lines)); + } + + // Write to file + fwrite($fileHandle, $pdf->Output('', 'S')); + + parent::restoreStateAfterSave(); + } + + /** + * Convert inches to mm. + * + * @param float $inches + * + * @return float + */ + private function inchesToMm($inches) + { + return $inches * 25.4; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ErrorCode.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ErrorCode.php new file mode 100644 index 0000000..7a864f5 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ErrorCode.php @@ -0,0 +1,28 @@ + + */ + protected static $errorCodeMap = [ + '#NULL!' => 0x00, + '#DIV/0!' => 0x07, + '#VALUE!' => 0x0F, + '#REF!' => 0x17, + '#NAME?' => 0x1D, + '#NUM!' => 0x24, + '#N/A' => 0x2A, + ]; + + public static function error(string $errorCode): int + { + if (array_key_exists($errorCode, self::$errorCodeMap)) { + return self::$errorCodeMap[$errorCode]; + } + + return 0; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php new file mode 100644 index 0000000..e42139b --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php @@ -0,0 +1,510 @@ +object = $object; + } + + /** + * Process the object to be written. + * + * @return string + */ + public function close() + { + // initialize + $this->data = ''; + + switch (get_class($this->object)) { + case \PhpOffice\PhpSpreadsheet\Shared\Escher::class: + if ($dggContainer = $this->object->getDggContainer()) { + $writer = new self($dggContainer); + $this->data = $writer->close(); + } elseif ($dgContainer = $this->object->getDgContainer()) { + $writer = new self($dgContainer); + $this->data = $writer->close(); + $this->spOffsets = $writer->getSpOffsets(); + $this->spTypes = $writer->getSpTypes(); + } + + break; + case DggContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // write the dgg + $recVer = 0x0; + $recInstance = 0x0000; + $recType = 0xF006; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + // dgg data + $dggData = + pack( + 'VVVV', + $this->object->getSpIdMax(), // maximum shape identifier increased by one + $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one + $this->object->getCSpSaved(), + $this->object->getCDgSaved() // count total number of drawings saved + ); + + // add file identifier clusters (one per drawing) + $IDCLs = $this->object->getIDCLs(); + + foreach ($IDCLs as $dgId => $maxReducedSpId) { + $dggData .= pack('VV', $dgId, $maxReducedSpId + 1); + } + + $header = pack('vvV', $recVerInstance, $recType, strlen($dggData)); + $innerData .= $header . $dggData; + + // write the bstoreContainer + if ($bstoreContainer = $this->object->getBstoreContainer()) { + $writer = new self($bstoreContainer); + $innerData .= $writer->close(); + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF000; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case BstoreContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // treat the inner data + if ($BSECollection = $this->object->getBSECollection()) { + foreach ($BSECollection as $BSE) { + $writer = new self($BSE); + $innerData .= $writer->close(); + } + } + + // write the record + $recVer = 0xF; + $recInstance = count($this->object->getBSECollection()); + $recType = 0xF001; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case BSE::class: + // this is a semi-container record + + // initialize + $innerData = ''; + + // here we treat the inner data + if ($blip = $this->object->getBlip()) { + $writer = new self($blip); + $innerData .= $writer->close(); + } + + // initialize + $data = ''; + + $btWin32 = $this->object->getBlipType(); + $btMacOS = $this->object->getBlipType(); + $data .= pack('CC', $btWin32, $btMacOS); + + $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo + $data .= $rgbUid; + + $tag = 0; + $size = strlen($innerData); + $cRef = 1; + $foDelay = 0; //todo + $unused1 = 0x0; + $cbName = 0x0; + $unused2 = 0x0; + $unused3 = 0x0; + $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3); + + $data .= $innerData; + + // write the record + $recVer = 0x2; + $recInstance = $this->object->getBlipType(); + $recType = 0xF007; + $length = strlen($data); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $data; + + break; + case Blip::class: + // this is an atom record + + // write the record + switch ($this->object->getParent()->getBlipType()) { + case BSE::BLIPTYPE_JPEG: + // initialize + $innerData = ''; + + $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo + $innerData .= $rgbUid1; + + $tag = 0xFF; // todo + $innerData .= pack('C', $tag); + + $innerData .= $this->object->getData(); + + $recVer = 0x0; + $recInstance = 0x46A; + $recType = 0xF01D; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $innerData; + + break; + case BSE::BLIPTYPE_PNG: + // initialize + $innerData = ''; + + $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo + $innerData .= $rgbUid1; + + $tag = 0xFF; // todo + $innerData .= pack('C', $tag); + + $innerData .= $this->object->getData(); + + $recVer = 0x0; + $recInstance = 0x6E0; + $recType = 0xF01E; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header; + + $this->data .= $innerData; + + break; + } + + break; + case DgContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // write the dg + $recVer = 0x0; + $recInstance = $this->object->getDgId(); + $recType = 0xF008; + $length = 8; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + // number of shapes in this drawing (including group shape) + $countShapes = count($this->object->getSpgrContainer()->getChildren()); + $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId()); + + // write the spgrContainer + if ($spgrContainer = $this->object->getSpgrContainer()) { + $writer = new self($spgrContainer); + $innerData .= $writer->close(); + + // get the shape offsets relative to the spgrContainer record + $spOffsets = $writer->getSpOffsets(); + $spTypes = $writer->getSpTypes(); + + // save the shape offsets relative to dgContainer + foreach ($spOffsets as &$spOffset) { + $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes) + } + + $this->spOffsets = $spOffsets; + $this->spTypes = $spTypes; + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF002; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + + break; + case SpgrContainer::class: + // this is a container record + + // initialize + $innerData = ''; + + // initialize spape offsets + $totalSize = 8; + $spOffsets = []; + $spTypes = []; + + // treat the inner data + foreach ($this->object->getChildren() as $spContainer) { + $writer = new self($spContainer); + $spData = $writer->close(); + $innerData .= $spData; + + // save the shape offsets (where new shape records begin) + $totalSize += strlen($spData); + $spOffsets[] = $totalSize; + + $spTypes = array_merge($spTypes, $writer->getSpTypes()); + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF003; + $length = strlen($innerData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $innerData; + $this->spOffsets = $spOffsets; + $this->spTypes = $spTypes; + + break; + case SpContainer::class: + // initialize + $data = ''; + + // build the data + + // write group shape record, if necessary? + if ($this->object->getSpgr()) { + $recVer = 0x1; + $recInstance = 0x0000; + $recType = 0xF009; + $length = 0x00000010; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $data .= $header . pack('VVVV', 0, 0, 0, 0); + } + $this->spTypes[] = ($this->object->getSpType()); + + // write the shape record + $recVer = 0x2; + $recInstance = $this->object->getSpType(); // shape type + $recType = 0xF00A; + $length = 0x00000008; + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $data .= $header . pack('VV', $this->object->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00); + + // the options + if ($this->object->getOPTCollection()) { + $optData = ''; + + $recVer = 0x3; + $recInstance = count($this->object->getOPTCollection()); + $recType = 0xF00B; + foreach ($this->object->getOPTCollection() as $property => $value) { + $optData .= pack('vV', $property, $value); + } + $length = strlen($optData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $optData; + } + + // the client anchor + if ($this->object->getStartCoordinates()) { + $clientAnchorData = ''; + + $recVer = 0x0; + $recInstance = 0x0; + $recType = 0xF010; + + // start coordinates + [$column, $row] = Coordinate::indexesFromString($this->object->getStartCoordinates()); + $c1 = $column - 1; + $r1 = $row - 1; + + // start offsetX + $startOffsetX = $this->object->getStartOffsetX(); + + // start offsetY + $startOffsetY = $this->object->getStartOffsetY(); + + // end coordinates + [$column, $row] = Coordinate::indexesFromString($this->object->getEndCoordinates()); + $c2 = $column - 1; + $r2 = $row - 1; + + // end offsetX + $endOffsetX = $this->object->getEndOffsetX(); + + // end offsetY + $endOffsetY = $this->object->getEndOffsetY(); + + $clientAnchorData = pack('vvvvvvvvv', $this->object->getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY); + + $length = strlen($clientAnchorData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $clientAnchorData; + } + + // the client data, just empty for now + if (!$this->object->getSpgr()) { + $clientDataData = ''; + + $recVer = 0x0; + $recInstance = 0x0; + $recType = 0xF011; + + $length = strlen($clientDataData); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + $data .= $header . $clientDataData; + } + + // write the record + $recVer = 0xF; + $recInstance = 0x0000; + $recType = 0xF004; + $length = strlen($data); + + $recVerInstance = $recVer; + $recVerInstance |= $recInstance << 4; + + $header = pack('vvV', $recVerInstance, $recType, $length); + + $this->data = $header . $data; + + break; + } + + return $this->data; + } + + /** + * Gets the shape offsets. + * + * @return array + */ + public function getSpOffsets() + { + return $this->spOffsets; + } + + /** + * Gets the shape types. + * + * @return array + */ + public function getSpTypes() + { + return $this->spTypes; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php new file mode 100644 index 0000000..1266cf2 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php @@ -0,0 +1,147 @@ +colorIndex = 0x7FFF; + $this->font = $font; + } + + /** + * Set the color index. + * + * @param int $colorIndex + */ + public function setColorIndex($colorIndex): void + { + $this->colorIndex = $colorIndex; + } + + /** + * Get font record data. + * + * @return string + */ + public function writeFont() + { + $font_outline = 0; + $font_shadow = 0; + + $icv = $this->colorIndex; // Index to color palette + if ($this->font->getSuperscript()) { + $sss = 1; + } elseif ($this->font->getSubscript()) { + $sss = 2; + } else { + $sss = 0; + } + $bFamily = 0; // Font family + $bCharSet = \PhpOffice\PhpSpreadsheet\Shared\Font::getCharsetFromFontName($this->font->getName()); // Character set + + $record = 0x31; // Record identifier + $reserved = 0x00; // Reserved + $grbit = 0x00; // Font attributes + if ($this->font->getItalic()) { + $grbit |= 0x02; + } + if ($this->font->getStrikethrough()) { + $grbit |= 0x08; + } + if ($font_outline) { + $grbit |= 0x10; + } + if ($font_shadow) { + $grbit |= 0x20; + } + + $data = pack( + 'vvvvvCCCC', + // Fontsize (in twips) + $this->font->getSize() * 20, + $grbit, + // Colour + $icv, + // Font weight + self::mapBold($this->font->getBold()), + // Superscript/Subscript + $sss, + self::mapUnderline($this->font->getUnderline()), + $bFamily, + $bCharSet, + $reserved + ); + $data .= StringHelper::UTF8toBIFF8UnicodeShort($this->font->getName()); + + $length = strlen($data); + $header = pack('vv', $record, $length); + + return $header . $data; + } + + /** + * Map to BIFF5-BIFF8 codes for bold. + * + * @param bool $bold + * + * @return int + */ + private static function mapBold($bold) + { + if ($bold) { + return 0x2BC; // 700 = Bold font weight + } + + return 0x190; // 400 = Normal font weight + } + + /** + * Map of BIFF2-BIFF8 codes for underline styles. + * + * @var int[] + */ + private static $mapUnderline = [ + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE => 0x00, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE => 0x01, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE => 0x02, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING => 0x21, + \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING => 0x22, + ]; + + /** + * Map underline. + * + * @param string $underline + * + * @return int + */ + private static function mapUnderline($underline) + { + if (isset(self::$mapUnderline[$underline])) { + return self::$mapUnderline[$underline]; + } + + return 0x00; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php new file mode 100644 index 0000000..4033fd5 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -0,0 +1,1486 @@ +=,;#()"{} + const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+'; + + // Sheet title in quoted form (without surrounding quotes) + // Invalid sheet title characters cannot occur in the sheet title: + // *:/\?[] (usual invalid sheet title characters) + // Single quote is represented as a pair '' + const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+'; + + /** + * The index of the character we are currently looking at. + * + * @var int + */ + public $currentCharacter; + + /** + * The token we are working on. + * + * @var string + */ + public $currentToken; + + /** + * The formula to parse. + * + * @var string + */ + private $formula; + + /** + * The character ahead of the current char. + * + * @var string + */ + public $lookAhead; + + /** + * The parse tree to be generated. + * + * @var string + */ + public $parseTree; + + /** + * Array of external sheets. + * + * @var array + */ + private $externalSheets; + + /** + * Array of sheet references in the form of REF structures. + * + * @var array + */ + public $references; + + /** + * The Excel ptg indices. + * + * @var array + */ + private $ptg = [ + 'ptgExp' => 0x01, + 'ptgTbl' => 0x02, + 'ptgAdd' => 0x03, + 'ptgSub' => 0x04, + 'ptgMul' => 0x05, + 'ptgDiv' => 0x06, + 'ptgPower' => 0x07, + 'ptgConcat' => 0x08, + 'ptgLT' => 0x09, + 'ptgLE' => 0x0A, + 'ptgEQ' => 0x0B, + 'ptgGE' => 0x0C, + 'ptgGT' => 0x0D, + 'ptgNE' => 0x0E, + 'ptgIsect' => 0x0F, + 'ptgUnion' => 0x10, + 'ptgRange' => 0x11, + 'ptgUplus' => 0x12, + 'ptgUminus' => 0x13, + 'ptgPercent' => 0x14, + 'ptgParen' => 0x15, + 'ptgMissArg' => 0x16, + 'ptgStr' => 0x17, + 'ptgAttr' => 0x19, + 'ptgSheet' => 0x1A, + 'ptgEndSheet' => 0x1B, + 'ptgErr' => 0x1C, + 'ptgBool' => 0x1D, + 'ptgInt' => 0x1E, + 'ptgNum' => 0x1F, + 'ptgArray' => 0x20, + 'ptgFunc' => 0x21, + 'ptgFuncVar' => 0x22, + 'ptgName' => 0x23, + 'ptgRef' => 0x24, + 'ptgArea' => 0x25, + 'ptgMemArea' => 0x26, + 'ptgMemErr' => 0x27, + 'ptgMemNoMem' => 0x28, + 'ptgMemFunc' => 0x29, + 'ptgRefErr' => 0x2A, + 'ptgAreaErr' => 0x2B, + 'ptgRefN' => 0x2C, + 'ptgAreaN' => 0x2D, + 'ptgMemAreaN' => 0x2E, + 'ptgMemNoMemN' => 0x2F, + 'ptgNameX' => 0x39, + 'ptgRef3d' => 0x3A, + 'ptgArea3d' => 0x3B, + 'ptgRefErr3d' => 0x3C, + 'ptgAreaErr3d' => 0x3D, + 'ptgArrayV' => 0x40, + 'ptgFuncV' => 0x41, + 'ptgFuncVarV' => 0x42, + 'ptgNameV' => 0x43, + 'ptgRefV' => 0x44, + 'ptgAreaV' => 0x45, + 'ptgMemAreaV' => 0x46, + 'ptgMemErrV' => 0x47, + 'ptgMemNoMemV' => 0x48, + 'ptgMemFuncV' => 0x49, + 'ptgRefErrV' => 0x4A, + 'ptgAreaErrV' => 0x4B, + 'ptgRefNV' => 0x4C, + 'ptgAreaNV' => 0x4D, + 'ptgMemAreaNV' => 0x4E, + 'ptgMemNoMemNV' => 0x4F, + 'ptgFuncCEV' => 0x58, + 'ptgNameXV' => 0x59, + 'ptgRef3dV' => 0x5A, + 'ptgArea3dV' => 0x5B, + 'ptgRefErr3dV' => 0x5C, + 'ptgAreaErr3dV' => 0x5D, + 'ptgArrayA' => 0x60, + 'ptgFuncA' => 0x61, + 'ptgFuncVarA' => 0x62, + 'ptgNameA' => 0x63, + 'ptgRefA' => 0x64, + 'ptgAreaA' => 0x65, + 'ptgMemAreaA' => 0x66, + 'ptgMemErrA' => 0x67, + 'ptgMemNoMemA' => 0x68, + 'ptgMemFuncA' => 0x69, + 'ptgRefErrA' => 0x6A, + 'ptgAreaErrA' => 0x6B, + 'ptgRefNA' => 0x6C, + 'ptgAreaNA' => 0x6D, + 'ptgMemAreaNA' => 0x6E, + 'ptgMemNoMemNA' => 0x6F, + 'ptgFuncCEA' => 0x78, + 'ptgNameXA' => 0x79, + 'ptgRef3dA' => 0x7A, + 'ptgArea3dA' => 0x7B, + 'ptgRefErr3dA' => 0x7C, + 'ptgAreaErr3dA' => 0x7D, + ]; + + /** + * Thanks to Michael Meeks and Gnumeric for the initial arg values. + * + * The following hash was generated by "function_locale.pl" in the distro. + * Refer to function_locale.pl for non-English function names. + * + * The array elements are as follow: + * ptg: The Excel function ptg code. + * args: The number of arguments that the function takes: + * >=0 is a fixed number of arguments. + * -1 is a variable number of arguments. + * class: The reference, value or array class of the function args. + * vol: The function is volatile. + * + * @var array + */ + private $functions = [ + // function ptg args class vol + 'COUNT' => [0, -1, 0, 0], + 'IF' => [1, -1, 1, 0], + 'ISNA' => [2, 1, 1, 0], + 'ISERROR' => [3, 1, 1, 0], + 'SUM' => [4, -1, 0, 0], + 'AVERAGE' => [5, -1, 0, 0], + 'MIN' => [6, -1, 0, 0], + 'MAX' => [7, -1, 0, 0], + 'ROW' => [8, -1, 0, 0], + 'COLUMN' => [9, -1, 0, 0], + 'NA' => [10, 0, 0, 0], + 'NPV' => [11, -1, 1, 0], + 'STDEV' => [12, -1, 0, 0], + 'DOLLAR' => [13, -1, 1, 0], + 'FIXED' => [14, -1, 1, 0], + 'SIN' => [15, 1, 1, 0], + 'COS' => [16, 1, 1, 0], + 'TAN' => [17, 1, 1, 0], + 'ATAN' => [18, 1, 1, 0], + 'PI' => [19, 0, 1, 0], + 'SQRT' => [20, 1, 1, 0], + 'EXP' => [21, 1, 1, 0], + 'LN' => [22, 1, 1, 0], + 'LOG10' => [23, 1, 1, 0], + 'ABS' => [24, 1, 1, 0], + 'INT' => [25, 1, 1, 0], + 'SIGN' => [26, 1, 1, 0], + 'ROUND' => [27, 2, 1, 0], + 'LOOKUP' => [28, -1, 0, 0], + 'INDEX' => [29, -1, 0, 1], + 'REPT' => [30, 2, 1, 0], + 'MID' => [31, 3, 1, 0], + 'LEN' => [32, 1, 1, 0], + 'VALUE' => [33, 1, 1, 0], + 'TRUE' => [34, 0, 1, 0], + 'FALSE' => [35, 0, 1, 0], + 'AND' => [36, -1, 0, 0], + 'OR' => [37, -1, 0, 0], + 'NOT' => [38, 1, 1, 0], + 'MOD' => [39, 2, 1, 0], + 'DCOUNT' => [40, 3, 0, 0], + 'DSUM' => [41, 3, 0, 0], + 'DAVERAGE' => [42, 3, 0, 0], + 'DMIN' => [43, 3, 0, 0], + 'DMAX' => [44, 3, 0, 0], + 'DSTDEV' => [45, 3, 0, 0], + 'VAR' => [46, -1, 0, 0], + 'DVAR' => [47, 3, 0, 0], + 'TEXT' => [48, 2, 1, 0], + 'LINEST' => [49, -1, 0, 0], + 'TREND' => [50, -1, 0, 0], + 'LOGEST' => [51, -1, 0, 0], + 'GROWTH' => [52, -1, 0, 0], + 'PV' => [56, -1, 1, 0], + 'FV' => [57, -1, 1, 0], + 'NPER' => [58, -1, 1, 0], + 'PMT' => [59, -1, 1, 0], + 'RATE' => [60, -1, 1, 0], + 'MIRR' => [61, 3, 0, 0], + 'IRR' => [62, -1, 0, 0], + 'RAND' => [63, 0, 1, 1], + 'MATCH' => [64, -1, 0, 0], + 'DATE' => [65, 3, 1, 0], + 'TIME' => [66, 3, 1, 0], + 'DAY' => [67, 1, 1, 0], + 'MONTH' => [68, 1, 1, 0], + 'YEAR' => [69, 1, 1, 0], + 'WEEKDAY' => [70, -1, 1, 0], + 'HOUR' => [71, 1, 1, 0], + 'MINUTE' => [72, 1, 1, 0], + 'SECOND' => [73, 1, 1, 0], + 'NOW' => [74, 0, 1, 1], + 'AREAS' => [75, 1, 0, 1], + 'ROWS' => [76, 1, 0, 1], + 'COLUMNS' => [77, 1, 0, 1], + 'OFFSET' => [78, -1, 0, 1], + 'SEARCH' => [82, -1, 1, 0], + 'TRANSPOSE' => [83, 1, 1, 0], + 'TYPE' => [86, 1, 1, 0], + 'ATAN2' => [97, 2, 1, 0], + 'ASIN' => [98, 1, 1, 0], + 'ACOS' => [99, 1, 1, 0], + 'CHOOSE' => [100, -1, 1, 0], + 'HLOOKUP' => [101, -1, 0, 0], + 'VLOOKUP' => [102, -1, 0, 0], + 'ISREF' => [105, 1, 0, 0], + 'LOG' => [109, -1, 1, 0], + 'CHAR' => [111, 1, 1, 0], + 'LOWER' => [112, 1, 1, 0], + 'UPPER' => [113, 1, 1, 0], + 'PROPER' => [114, 1, 1, 0], + 'LEFT' => [115, -1, 1, 0], + 'RIGHT' => [116, -1, 1, 0], + 'EXACT' => [117, 2, 1, 0], + 'TRIM' => [118, 1, 1, 0], + 'REPLACE' => [119, 4, 1, 0], + 'SUBSTITUTE' => [120, -1, 1, 0], + 'CODE' => [121, 1, 1, 0], + 'FIND' => [124, -1, 1, 0], + 'CELL' => [125, -1, 0, 1], + 'ISERR' => [126, 1, 1, 0], + 'ISTEXT' => [127, 1, 1, 0], + 'ISNUMBER' => [128, 1, 1, 0], + 'ISBLANK' => [129, 1, 1, 0], + 'T' => [130, 1, 0, 0], + 'N' => [131, 1, 0, 0], + 'DATEVALUE' => [140, 1, 1, 0], + 'TIMEVALUE' => [141, 1, 1, 0], + 'SLN' => [142, 3, 1, 0], + 'SYD' => [143, 4, 1, 0], + 'DDB' => [144, -1, 1, 0], + 'INDIRECT' => [148, -1, 1, 1], + 'CALL' => [150, -1, 1, 0], + 'CLEAN' => [162, 1, 1, 0], + 'MDETERM' => [163, 1, 2, 0], + 'MINVERSE' => [164, 1, 2, 0], + 'MMULT' => [165, 2, 2, 0], + 'IPMT' => [167, -1, 1, 0], + 'PPMT' => [168, -1, 1, 0], + 'COUNTA' => [169, -1, 0, 0], + 'PRODUCT' => [183, -1, 0, 0], + 'FACT' => [184, 1, 1, 0], + 'DPRODUCT' => [189, 3, 0, 0], + 'ISNONTEXT' => [190, 1, 1, 0], + 'STDEVP' => [193, -1, 0, 0], + 'VARP' => [194, -1, 0, 0], + 'DSTDEVP' => [195, 3, 0, 0], + 'DVARP' => [196, 3, 0, 0], + 'TRUNC' => [197, -1, 1, 0], + 'ISLOGICAL' => [198, 1, 1, 0], + 'DCOUNTA' => [199, 3, 0, 0], + 'USDOLLAR' => [204, -1, 1, 0], + 'FINDB' => [205, -1, 1, 0], + 'SEARCHB' => [206, -1, 1, 0], + 'REPLACEB' => [207, 4, 1, 0], + 'LEFTB' => [208, -1, 1, 0], + 'RIGHTB' => [209, -1, 1, 0], + 'MIDB' => [210, 3, 1, 0], + 'LENB' => [211, 1, 1, 0], + 'ROUNDUP' => [212, 2, 1, 0], + 'ROUNDDOWN' => [213, 2, 1, 0], + 'ASC' => [214, 1, 1, 0], + 'DBCS' => [215, 1, 1, 0], + 'RANK' => [216, -1, 0, 0], + 'ADDRESS' => [219, -1, 1, 0], + 'DAYS360' => [220, -1, 1, 0], + 'TODAY' => [221, 0, 1, 1], + 'VDB' => [222, -1, 1, 0], + 'MEDIAN' => [227, -1, 0, 0], + 'SUMPRODUCT' => [228, -1, 2, 0], + 'SINH' => [229, 1, 1, 0], + 'COSH' => [230, 1, 1, 0], + 'TANH' => [231, 1, 1, 0], + 'ASINH' => [232, 1, 1, 0], + 'ACOSH' => [233, 1, 1, 0], + 'ATANH' => [234, 1, 1, 0], + 'DGET' => [235, 3, 0, 0], + 'INFO' => [244, 1, 1, 1], + 'DB' => [247, -1, 1, 0], + 'FREQUENCY' => [252, 2, 0, 0], + 'ERROR.TYPE' => [261, 1, 1, 0], + 'REGISTER.ID' => [267, -1, 1, 0], + 'AVEDEV' => [269, -1, 0, 0], + 'BETADIST' => [270, -1, 1, 0], + 'GAMMALN' => [271, 1, 1, 0], + 'BETAINV' => [272, -1, 1, 0], + 'BINOMDIST' => [273, 4, 1, 0], + 'CHIDIST' => [274, 2, 1, 0], + 'CHIINV' => [275, 2, 1, 0], + 'COMBIN' => [276, 2, 1, 0], + 'CONFIDENCE' => [277, 3, 1, 0], + 'CRITBINOM' => [278, 3, 1, 0], + 'EVEN' => [279, 1, 1, 0], + 'EXPONDIST' => [280, 3, 1, 0], + 'FDIST' => [281, 3, 1, 0], + 'FINV' => [282, 3, 1, 0], + 'FISHER' => [283, 1, 1, 0], + 'FISHERINV' => [284, 1, 1, 0], + 'FLOOR' => [285, 2, 1, 0], + 'GAMMADIST' => [286, 4, 1, 0], + 'GAMMAINV' => [287, 3, 1, 0], + 'CEILING' => [288, 2, 1, 0], + 'HYPGEOMDIST' => [289, 4, 1, 0], + 'LOGNORMDIST' => [290, 3, 1, 0], + 'LOGINV' => [291, 3, 1, 0], + 'NEGBINOMDIST' => [292, 3, 1, 0], + 'NORMDIST' => [293, 4, 1, 0], + 'NORMSDIST' => [294, 1, 1, 0], + 'NORMINV' => [295, 3, 1, 0], + 'NORMSINV' => [296, 1, 1, 0], + 'STANDARDIZE' => [297, 3, 1, 0], + 'ODD' => [298, 1, 1, 0], + 'PERMUT' => [299, 2, 1, 0], + 'POISSON' => [300, 3, 1, 0], + 'TDIST' => [301, 3, 1, 0], + 'WEIBULL' => [302, 4, 1, 0], + 'SUMXMY2' => [303, 2, 2, 0], + 'SUMX2MY2' => [304, 2, 2, 0], + 'SUMX2PY2' => [305, 2, 2, 0], + 'CHITEST' => [306, 2, 2, 0], + 'CORREL' => [307, 2, 2, 0], + 'COVAR' => [308, 2, 2, 0], + 'FORECAST' => [309, 3, 2, 0], + 'FTEST' => [310, 2, 2, 0], + 'INTERCEPT' => [311, 2, 2, 0], + 'PEARSON' => [312, 2, 2, 0], + 'RSQ' => [313, 2, 2, 0], + 'STEYX' => [314, 2, 2, 0], + 'SLOPE' => [315, 2, 2, 0], + 'TTEST' => [316, 4, 2, 0], + 'PROB' => [317, -1, 2, 0], + 'DEVSQ' => [318, -1, 0, 0], + 'GEOMEAN' => [319, -1, 0, 0], + 'HARMEAN' => [320, -1, 0, 0], + 'SUMSQ' => [321, -1, 0, 0], + 'KURT' => [322, -1, 0, 0], + 'SKEW' => [323, -1, 0, 0], + 'ZTEST' => [324, -1, 0, 0], + 'LARGE' => [325, 2, 0, 0], + 'SMALL' => [326, 2, 0, 0], + 'QUARTILE' => [327, 2, 0, 0], + 'PERCENTILE' => [328, 2, 0, 0], + 'PERCENTRANK' => [329, -1, 0, 0], + 'MODE' => [330, -1, 2, 0], + 'TRIMMEAN' => [331, 2, 0, 0], + 'TINV' => [332, 2, 1, 0], + 'CONCATENATE' => [336, -1, 1, 0], + 'POWER' => [337, 2, 1, 0], + 'RADIANS' => [342, 1, 1, 0], + 'DEGREES' => [343, 1, 1, 0], + 'SUBTOTAL' => [344, -1, 0, 0], + 'SUMIF' => [345, -1, 0, 0], + 'COUNTIF' => [346, 2, 0, 0], + 'COUNTBLANK' => [347, 1, 0, 0], + 'ISPMT' => [350, 4, 1, 0], + 'DATEDIF' => [351, 3, 1, 0], + 'DATESTRING' => [352, 1, 1, 0], + 'NUMBERSTRING' => [353, 2, 1, 0], + 'ROMAN' => [354, -1, 1, 0], + 'GETPIVOTDATA' => [358, -1, 0, 0], + 'HYPERLINK' => [359, -1, 1, 0], + 'PHONETIC' => [360, 1, 0, 0], + 'AVERAGEA' => [361, -1, 0, 0], + 'MAXA' => [362, -1, 0, 0], + 'MINA' => [363, -1, 0, 0], + 'STDEVPA' => [364, -1, 0, 0], + 'VARPA' => [365, -1, 0, 0], + 'STDEVA' => [366, -1, 0, 0], + 'VARA' => [367, -1, 0, 0], + 'BAHTTEXT' => [368, 1, 0, 0], + ]; + + private $spreadsheet; + + /** + * The class constructor. + */ + public function __construct(Spreadsheet $spreadsheet) + { + $this->spreadsheet = $spreadsheet; + + $this->currentCharacter = 0; + $this->currentToken = ''; // The token we are working on. + $this->formula = ''; // The formula to parse. + $this->lookAhead = ''; // The character ahead of the current char. + $this->parseTree = ''; // The parse tree to be generated. + $this->externalSheets = []; + $this->references = []; + } + + /** + * Convert a token to the proper ptg value. + * + * @param mixed $token the token to convert + * + * @return mixed the converted token on success + */ + private function convert($token) + { + if (preg_match('/"([^"]|""){0,255}"/', $token)) { + return $this->convertString($token); + } elseif (is_numeric($token)) { + return $this->convertNumber($token); + // match references like A1 or $A$1 + } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { + return $this->convertRef2d($token); + // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { + return $this->convertRef3d($token); + // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { + return $this->convertRef3d($token); + // match ranges like A1:B2 or $A$1:$B$2 + } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { + return $this->convertRange2d($token); + // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { + return $this->convertRange3d($token); + // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { + return $this->convertRange3d($token); + // operators (including parentheses) + } elseif (isset($this->ptg[$token])) { + return pack('C', $this->ptg[$token]); + // match error codes + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { + return $this->convertError($token); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + return $this->convertDefinedName($token); + // commented so argument number can be processed correctly. See toReversePolish(). + /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) + { + return($this->convertFunction($token, $this->_func_args)); + }*/ + // if it's an argument, ignore the token (the argument remains) + } elseif ($token == 'arg') { + return ''; + } + + // TODO: use real error codes + throw new WriterException("Unknown token $token"); + } + + /** + * Convert a number token to ptgInt or ptgNum. + * + * @param mixed $num an integer or double for conversion to its ptg value + * + * @return string + */ + private function convertNumber($num) + { + // Integer in the range 0..2**16-1 + if ((preg_match('/^\\d+$/', $num)) && ($num <= 65535)) { + return pack('Cv', $this->ptg['ptgInt'], $num); + } + + // A float + if (BIFFwriter::getByteOrder()) { // if it's Big Endian + $num = strrev($num); + } + + return pack('Cd', $this->ptg['ptgNum'], $num); + } + + /** + * Convert a string token to ptgStr. + * + * @param string $string a string for conversion to its ptg value + * + * @return mixed the converted token on success + */ + private function convertString($string) + { + // chop away beggining and ending quotes + $string = substr($string, 1, -1); + if (strlen($string) > 255) { + throw new WriterException('String is too long'); + } + + return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string); + } + + /** + * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of + * args that it takes. + * + * @param string $token the name of the function for convertion to ptg value + * @param int $num_args the number of arguments the function receives + * + * @return string The packed ptg for the function + */ + private function convertFunction($token, $num_args) + { + $args = $this->functions[$token][1]; + + // Fixed number of args eg. TIME($i, $j, $k). + if ($args >= 0) { + return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); + } + + // Variable number of args eg. SUM($i, $j, $k, ..). + return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); + } + + /** + * Convert an Excel range such as A1:D4 to a ptgRefV. + * + * @param string $range An Excel range in the A1:A2 + * @param int $class + * + * @return string + */ + private function convertRange2d($range, $class = 0) + { + // TODO: possible class value 0,1,2 check Formula.pm + // Split the range into 2 cell refs + if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { + [$cell1, $cell2] = explode(':', $range); + } else { + // TODO: use real error codes + throw new WriterException('Unknown range separator'); + } + + // Convert the cell references + [$row1, $col1] = $this->cellToPackedRowcol($cell1); + [$row2, $col2] = $this->cellToPackedRowcol($cell2); + + // The ptg value depends on the class of the ptg. + if ($class == 0) { + $ptgArea = pack('C', $this->ptg['ptgArea']); + } elseif ($class == 1) { + $ptgArea = pack('C', $this->ptg['ptgAreaV']); + } elseif ($class == 2) { + $ptgArea = pack('C', $this->ptg['ptgAreaA']); + } else { + // TODO: use real error codes + throw new WriterException("Unknown class $class"); + } + + return $ptgArea . $row1 . $row2 . $col1 . $col2; + } + + /** + * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to + * a ptgArea3d. + * + * @param string $token an Excel range in the Sheet1!A1:A2 format + * + * @return mixed the packed ptgArea3d token on success + */ + private function convertRange3d($token) + { + // Split the ref at the ! symbol + [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true); + + // Convert the external reference part (different for BIFF8) + $ext_ref = $this->getRefIndex($ext_ref); + + // Split the range into 2 cell refs + [$cell1, $cell2] = explode(':', $range); + + // Convert the cell references + if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) { + [$row1, $col1] = $this->cellToPackedRowcol($cell1); + [$row2, $col2] = $this->cellToPackedRowcol($cell2); + } else { // It's a rows range (like 26:27) + [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2); + } + + // The ptg value depends on the class of the ptg. + $ptgArea = pack('C', $this->ptg['ptgArea3d']); + + return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2; + } + + /** + * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. + * + * @param string $cell An Excel cell reference + * + * @return string The cell in packed() format with the corresponding ptg + */ + private function convertRef2d($cell) + { + // Convert the cell reference + $cell_array = $this->cellToPackedRowcol($cell); + [$row, $col] = $cell_array; + + // The ptg value depends on the class of the ptg. + $ptgRef = pack('C', $this->ptg['ptgRefA']); + + return $ptgRef . $row . $col; + } + + /** + * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a + * ptgRef3d. + * + * @param string $cell An Excel cell reference + * + * @return mixed the packed ptgRef3d token on success + */ + private function convertRef3d($cell) + { + // Split the ref at the ! symbol + [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true); + + // Convert the external reference part (different for BIFF8) + $ext_ref = $this->getRefIndex($ext_ref); + + // Convert the cell reference part + [$row, $col] = $this->cellToPackedRowcol($cell); + + // The ptg value depends on the class of the ptg. + $ptgRef = pack('C', $this->ptg['ptgRef3dA']); + + return $ptgRef . $ext_ref . $row . $col; + } + + /** + * Convert an error code to a ptgErr. + * + * @param string $errorCode The error code for conversion to its ptg value + * + * @return string The error code ptgErr + */ + private function convertError($errorCode) + { + switch ($errorCode) { + case '#NULL!': + return pack('C', 0x00); + case '#DIV/0!': + return pack('C', 0x07); + case '#VALUE!': + return pack('C', 0x0F); + case '#REF!': + return pack('C', 0x17); + case '#NAME?': + return pack('C', 0x1D); + case '#NUM!': + return pack('C', 0x24); + case '#N/A': + return pack('C', 0x2A); + } + + return pack('C', 0xFF); + } + + private function convertDefinedName(string $name): string + { + if (strlen($name) > 255) { + throw new WriterException('Defined Name is too long'); + } + + $nameReference = 1; + foreach ($this->spreadsheet->getDefinedNames() as $definedName) { + if ($name === $definedName->getName()) { + break; + } + ++$nameReference; + } + + $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); + + throw new WriterException('Cannot yet write formulae with defined names to Xls'); + + return $ptgRef; + } + + /** + * Look up the REF index that corresponds to an external sheet name + * (or range). If it doesn't exist yet add it to the workbook's references + * array. It assumes all sheet names given must exist. + * + * @param string $ext_ref The name of the external reference + * + * @return mixed The reference index in packed() format on success + */ + private function getRefIndex($ext_ref) + { + $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any. + $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any. + $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' + + // Check if there is a sheet range eg., Sheet1:Sheet2. + if (preg_match('/:/', $ext_ref)) { + [$sheet_name1, $sheet_name2] = explode(':', $ext_ref); + + $sheet1 = $this->getSheetIndex($sheet_name1); + if ($sheet1 == -1) { + throw new WriterException("Unknown sheet name $sheet_name1 in formula"); + } + $sheet2 = $this->getSheetIndex($sheet_name2); + if ($sheet2 == -1) { + throw new WriterException("Unknown sheet name $sheet_name2 in formula"); + } + + // Reverse max and min sheet numbers if necessary + if ($sheet1 > $sheet2) { + [$sheet1, $sheet2] = [$sheet2, $sheet1]; + } + } else { // Single sheet name only. + $sheet1 = $this->getSheetIndex($ext_ref); + if ($sheet1 == -1) { + throw new WriterException("Unknown sheet name $ext_ref in formula"); + } + $sheet2 = $sheet1; + } + + // assume all references belong to this document + $supbook_index = 0x00; + $ref = pack('vvv', $supbook_index, $sheet1, $sheet2); + $totalreferences = count($this->references); + $index = -1; + for ($i = 0; $i < $totalreferences; ++$i) { + if ($ref == $this->references[$i]) { + $index = $i; + + break; + } + } + // if REF was not found add it to references array + if ($index == -1) { + $this->references[$totalreferences] = $ref; + $index = $totalreferences; + } + + return pack('v', $index); + } + + /** + * Look up the index that corresponds to an external sheet name. The hash of + * sheet names is updated by the addworksheet() method of the + * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. + * + * @param string $sheet_name Sheet name + * + * @return int The sheet index, -1 if the sheet was not found + */ + private function getSheetIndex($sheet_name) + { + if (!isset($this->externalSheets[$sheet_name])) { + return -1; + } + + return $this->externalSheets[$sheet_name]; + } + + /** + * This method is used to update the array of sheet names. It is + * called by the addWorksheet() method of the + * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. + * + * @param string $name The name of the worksheet being added + * @param int $index The index of the worksheet being added + * + * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() + */ + public function setExtSheet($name, $index): void + { + $this->externalSheets[$name] = $index; + } + + /** + * pack() row and column into the required 3 or 4 byte format. + * + * @param string $cell The Excel cell reference to be packed + * + * @return array Array containing the row and column in packed() format + */ + private function cellToPackedRowcol($cell) + { + $cell = strtoupper($cell); + [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell); + if ($col >= 256) { + throw new WriterException("Column in: $cell greater than 255"); + } + if ($row >= 65536) { + throw new WriterException("Row in: $cell greater than 65536 "); + } + + // Set the high bits to indicate if row or col are relative. + $col |= $col_rel << 14; + $col |= $row_rel << 15; + $col = pack('v', $col); + + $row = pack('v', $row); + + return [$row, $col]; + } + + /** + * pack() row range into the required 3 or 4 byte format. + * Just using maximum col/rows, which is probably not the correct solution. + * + * @param string $range The Excel range to be packed + * + * @return array Array containing (row1,col1,row2,col2) in packed() format + */ + private function rangeToPackedRange($range) + { + preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); + // return absolute rows if there is a $ in the ref + $row1_rel = empty($match[1]) ? 1 : 0; + $row1 = $match[2]; + $row2_rel = empty($match[3]) ? 1 : 0; + $row2 = $match[4]; + // Convert 1-index to zero-index + --$row1; + --$row2; + // Trick poor inocent Excel + $col1 = 0; + $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!) + + // FIXME: this changes for BIFF8 + if (($row1 >= 65536) || ($row2 >= 65536)) { + throw new WriterException("Row in: $range greater than 65536 "); + } + + // Set the high bits to indicate if rows are relative. + $col1 |= $row1_rel << 15; + $col2 |= $row2_rel << 15; + $col1 = pack('v', $col1); + $col2 = pack('v', $col2); + + $row1 = pack('v', $row1); + $row2 = pack('v', $row2); + + return [$row1, $col1, $row2, $col2]; + } + + /** + * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero + * indexed row and column number. Also returns two (0,1) values to indicate + * whether the row or column are relative references. + * + * @param string $cell the Excel cell reference in A1 format + * + * @return array + */ + private function cellToRowcol($cell) + { + preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); + // return absolute column if there is a $ in the ref + $col_rel = empty($match[1]) ? 1 : 0; + $col_ref = $match[2]; + $row_rel = empty($match[3]) ? 1 : 0; + $row = $match[4]; + + // Convert base26 column string to a number. + $expn = strlen($col_ref) - 1; + $col = 0; + $col_ref_length = strlen($col_ref); + for ($i = 0; $i < $col_ref_length; ++$i) { + $col += (ord($col_ref[$i]) - 64) * 26 ** $expn; + --$expn; + } + + // Convert 1-index to zero-index + --$row; + --$col; + + return [$row, $col, $row_rel, $col_rel]; + } + + /** + * Advance to the next valid token. + */ + private function advance() + { + $token = ''; + $i = $this->currentCharacter; + $formula_length = strlen($this->formula); + // eat up white spaces + if ($i < $formula_length) { + while ($this->formula[$i] == ' ') { + ++$i; + } + + if ($i < ($formula_length - 1)) { + $this->lookAhead = $this->formula[$i + 1]; + } + $token = ''; + } + + while ($i < $formula_length) { + $token .= $this->formula[$i]; + + if ($i < ($formula_length - 1)) { + $this->lookAhead = $this->formula[$i + 1]; + } else { + $this->lookAhead = ''; + } + + if ($this->match($token) != '') { + $this->currentCharacter = $i + 1; + $this->currentToken = $token; + + return 1; + } + + if ($i < ($formula_length - 2)) { + $this->lookAhead = $this->formula[$i + 2]; + } else { // if we run out of characters lookAhead becomes empty + $this->lookAhead = ''; + } + ++$i; + } + //die("Lexical error ".$this->currentCharacter); + } + + /** + * Checks if it's a valid token. + * + * @param mixed $token the token to check + * + * @return mixed The checked token or false on failure + */ + private function match($token) + { + switch ($token) { + case '+': + case '-': + case '*': + case '/': + case '(': + case ')': + case ',': + case ';': + case '>=': + case '<=': + case '=': + case '<>': + case '^': + case '&': + case '%': + return $token; + + break; + case '>': + if ($this->lookAhead === '=') { // it's a GE token + break; + } + + return $token; + + break; + case '<': + // it's a LE or a NE token + if (($this->lookAhead === '=') || ($this->lookAhead === '>')) { + break; + } + + return $token; + + break; + default: + // if it's a reference A1 or $A$1 or $A1 or A$1 + if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) { + return $token; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) + return $token; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) + return $token; + } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { + // if it's a range A1:A2 or $A$1:$A$2 + return $token; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) { + // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 + return $token; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) { + // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 + return $token; + } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { + // If it's a number (check that it's not a sheet name or range) + return $token; + } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { + // If it's a string (of maximum 255 characters) + return $token; + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') { + // If it's an error code + return $token; + } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) { + // if it's a function call + return $token; + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + return $token; + } elseif (substr($token, -1) === ')') { + // It's an argument of some description (e.g. a named range), + // precise nature yet to be determined + return $token; + } + + return ''; + } + } + + /** + * The parsing method. It parses a formula. + * + * @param string $formula the formula to parse, without the initial equal + * sign (=) + * + * @return mixed true on success + */ + public function parse($formula) + { + $this->currentCharacter = 0; + $this->formula = (string) $formula; + $this->lookAhead = $formula[1] ?? ''; + $this->advance(); + $this->parseTree = $this->condition(); + + return true; + } + + /** + * It parses a condition. It assumes the following rule: + * Cond -> Expr [(">" | "<") Expr]. + * + * @return mixed The parsed ptg'd tree on success + */ + private function condition() + { + $result = $this->expression(); + if ($this->currentToken == '<') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgLT', $result, $result2); + } elseif ($this->currentToken == '>') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgGT', $result, $result2); + } elseif ($this->currentToken == '<=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgLE', $result, $result2); + } elseif ($this->currentToken == '>=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgGE', $result, $result2); + } elseif ($this->currentToken == '=') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgEQ', $result, $result2); + } elseif ($this->currentToken == '<>') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgNE', $result, $result2); + } + + return $result; + } + + /** + * It parses a expression. It assumes the following rule: + * Expr -> Term [("+" | "-") Term] + * -> "string" + * -> "-" Term : Negative value + * -> "+" Term : Positive value + * -> Error code. + * + * @return mixed The parsed ptg'd tree on success + */ + private function expression() + { + // If it's a string return a string node + if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) { + $tmp = str_replace('""', '"', $this->currentToken); + if (($tmp == '"') || ($tmp == '')) { + // Trap for "" that has been used for an empty string + $tmp = '""'; + } + $result = $this->createTree($tmp, '', ''); + $this->advance(); + + return $result; + // If it's an error code + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') { + $result = $this->createTree($this->currentToken, 'ptgErr', ''); + $this->advance(); + + return $result; + // If it's a negative value + } elseif ($this->currentToken == '-') { + // catch "-" Term + $this->advance(); + $result2 = $this->expression(); + + return $this->createTree('ptgUminus', $result2, ''); + // If it's a positive value + } elseif ($this->currentToken == '+') { + // catch "+" Term + $this->advance(); + $result2 = $this->expression(); + + return $this->createTree('ptgUplus', $result2, ''); + } + $result = $this->term(); + while ($this->currentToken === '&') { + $this->advance(); + $result2 = $this->expression(); + $result = $this->createTree('ptgConcat', $result, $result2); + } + while ( + ($this->currentToken == '+') || + ($this->currentToken == '-') || + ($this->currentToken == '^') + ) { + if ($this->currentToken == '+') { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgAdd', $result, $result2); + } elseif ($this->currentToken == '-') { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgSub', $result, $result2); + } else { + $this->advance(); + $result2 = $this->term(); + $result = $this->createTree('ptgPower', $result, $result2); + } + } + + return $result; + } + + /** + * This function just introduces a ptgParen element in the tree, so that Excel + * doesn't get confused when working with a parenthesized formula afterwards. + * + * @return array The parsed ptg'd tree + * + * @see fact() + */ + private function parenthesizedExpression() + { + return $this->createTree('ptgParen', $this->expression(), ''); + } + + /** + * It parses a term. It assumes the following rule: + * Term -> Fact [("*" | "/") Fact]. + * + * @return mixed The parsed ptg'd tree on success + */ + private function term() + { + $result = $this->fact(); + while ( + ($this->currentToken == '*') || + ($this->currentToken == '/') + ) { + if ($this->currentToken == '*') { + $this->advance(); + $result2 = $this->fact(); + $result = $this->createTree('ptgMul', $result, $result2); + } else { + $this->advance(); + $result2 = $this->fact(); + $result = $this->createTree('ptgDiv', $result, $result2); + } + } + + return $result; + } + + /** + * It parses a factor. It assumes the following rule: + * Fact -> ( Expr ) + * | CellRef + * | CellRange + * | Number + * | Function. + * + * @return mixed The parsed ptg'd tree on success + */ + private function fact() + { + if ($this->currentToken === '(') { + $this->advance(); // eat the "(" + $result = $this->parenthesizedExpression(); + if ($this->currentToken !== ')') { + throw new WriterException("')' token expected."); + } + $this->advance(); // eat the ")" + + return $result; + } + // if it's a reference + if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { + // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { + // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif ( + preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) || + preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) + ) { + // if it's a range A1:B2 or $A$1:$B$2 + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { + // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { + // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) + // must be an error? + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } elseif (is_numeric($this->currentToken)) { + // If it's a number or a percent + if ($this->lookAhead === '%') { + $result = $this->createTree('ptgPercent', $this->currentToken, ''); + $this->advance(); // Skip the percentage operator once we've pre-built that tree + } else { + $result = $this->createTree($this->currentToken, '', ''); + } + $this->advance(); + + return $result; + } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) { + // if it's a function call + return $this->func(); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) { + $result = $this->createTree('ptgName', $this->currentToken, ''); + $this->advance(); + + return $result; + } + + throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter); + } + + /** + * It parses a function call. It assumes the following rule: + * Func -> ( Expr [,Expr]* ). + * + * @return mixed The parsed ptg'd tree on success + */ + private function func() + { + $num_args = 0; // number of arguments received + $function = strtoupper($this->currentToken); + $result = ''; // initialize result + $this->advance(); + $this->advance(); // eat the "(" + while ($this->currentToken !== ')') { + if ($num_args > 0) { + if ($this->currentToken === ',' || $this->currentToken === ';') { + $this->advance(); // eat the "," or ";" + } else { + throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}"); + } + $result2 = $this->condition(); + $result = $this->createTree('arg', $result, $result2); + } else { // first argument + $result2 = $this->condition(); + $result = $this->createTree('arg', '', $result2); + } + ++$num_args; + } + if (!isset($this->functions[$function])) { + throw new WriterException("Function $function() doesn't exist"); + } + $args = $this->functions[$function][1]; + // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid. + if (($args >= 0) && ($args != $num_args)) { + throw new WriterException("Incorrect number of arguments in function $function() "); + } + + $result = $this->createTree($function, $result, $num_args); + $this->advance(); // eat the ")" + + return $result; + } + + /** + * Creates a tree. In fact an array which may have one or two arrays (sub-trees) + * as elements. + * + * @param mixed $value the value of this node + * @param mixed $left the left array (sub-tree) or a final node + * @param mixed $right the right array (sub-tree) or a final node + * + * @return array A tree + */ + private function createTree($value, $left, $right) + { + return ['value' => $value, 'left' => $left, 'right' => $right]; + } + + /** + * Builds a string containing the tree in reverse polish notation (What you + * would use in a HP calculator stack). + * The following tree:. + * + * + + * / \ + * 2 3 + * + * produces: "23+" + * + * The following tree: + * + * + + * / \ + * 3 * + * / \ + * 6 A1 + * + * produces: "36A1*+" + * + * In fact all operands, functions, references, etc... are written as ptg's + * + * @param array $tree the optional tree to convert + * + * @return string The tree in reverse polish notation + */ + public function toReversePolish($tree = []) + { + $polish = ''; // the string we are going to return + if (empty($tree)) { // If it's the first call use parseTree + $tree = $this->parseTree; + } + + if (is_array($tree['left'])) { + $converted_tree = $this->toReversePolish($tree['left']); + $polish .= $converted_tree; + } elseif ($tree['left'] != '') { // It's a final node + $converted_tree = $this->convert($tree['left']); + $polish .= $converted_tree; + } + if (is_array($tree['right'])) { + $converted_tree = $this->toReversePolish($tree['right']); + $polish .= $converted_tree; + } elseif ($tree['right'] != '') { // It's a final node + $converted_tree = $this->convert($tree['right']); + $polish .= $converted_tree; + } + // if it's a function convert it here (so we can set it's arguments) + if ( + preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) && + !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) && + !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) && + !is_numeric($tree['value']) && + !isset($this->ptg[$tree['value']]) + ) { + // left subtree for a function is always an array. + if ($tree['left'] != '') { + $left_tree = $this->toReversePolish($tree['left']); + } else { + $left_tree = ''; + } + + // add it's left subtree and return. + return $left_tree . $this->convertFunction($tree['value'], $tree['right']); + } + $converted_tree = $this->convert($tree['value']); + + return $polish . $converted_tree; + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php new file mode 100644 index 0000000..43ce442 --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -0,0 +1,246 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Properties + $objWriter->startElement('Properties'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'); + $objWriter->writeAttribute('xmlns:vt', 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + + // Application + $objWriter->writeElement('Application', 'Microsoft Excel'); + + // DocSecurity + $objWriter->writeElement('DocSecurity', '0'); + + // ScaleCrop + $objWriter->writeElement('ScaleCrop', 'false'); + + // HeadingPairs + $objWriter->startElement('HeadingPairs'); + + // Vector + $objWriter->startElement('vt:vector'); + $objWriter->writeAttribute('size', '2'); + $objWriter->writeAttribute('baseType', 'variant'); + + // Variant + $objWriter->startElement('vt:variant'); + $objWriter->writeElement('vt:lpstr', 'Worksheets'); + $objWriter->endElement(); + + // Variant + $objWriter->startElement('vt:variant'); + $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // TitlesOfParts + $objWriter->startElement('TitlesOfParts'); + + // Vector + $objWriter->startElement('vt:vector'); + $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('baseType', 'lpstr'); + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $objWriter->writeElement('vt:lpstr', $spreadsheet->getSheet($i)->getTitle()); + } + + $objWriter->endElement(); + + $objWriter->endElement(); + + // Company + $objWriter->writeElement('Company', $spreadsheet->getProperties()->getCompany()); + + // Company + $objWriter->writeElement('Manager', $spreadsheet->getProperties()->getManager()); + + // LinksUpToDate + $objWriter->writeElement('LinksUpToDate', 'false'); + + // SharedDoc + $objWriter->writeElement('SharedDoc', 'false'); + + // HyperlinksChanged + $objWriter->writeElement('HyperlinksChanged', 'false'); + + // AppVersion + $objWriter->writeElement('AppVersion', '12.0000'); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write docProps/core.xml to XML format. + * + * @return string XML Output + */ + public function writeDocPropsCore(Spreadsheet $spreadsheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // cp:coreProperties + $objWriter->startElement('cp:coreProperties'); + $objWriter->writeAttribute('xmlns:cp', 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'); + $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $objWriter->writeAttribute('xmlns:dcterms', 'http://purl.org/dc/terms/'); + $objWriter->writeAttribute('xmlns:dcmitype', 'http://purl.org/dc/dcmitype/'); + $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + + // dc:creator + $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator()); + + // cp:lastModifiedBy + $objWriter->writeElement('cp:lastModifiedBy', $spreadsheet->getProperties()->getLastModifiedBy()); + + // dcterms:created + $objWriter->startElement('dcterms:created'); + $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); + $created = $spreadsheet->getProperties()->getCreated(); + $date = Date::dateTimeFromTimestamp("$created"); + $objWriter->writeRawData($date->format(DATE_W3C)); + $objWriter->endElement(); + + // dcterms:modified + $objWriter->startElement('dcterms:modified'); + $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); + $created = $spreadsheet->getProperties()->getModified(); + $date = Date::dateTimeFromTimestamp("$created"); + $objWriter->writeRawData($date->format(DATE_W3C)); + $objWriter->endElement(); + + // dc:title + $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle()); + + // dc:description + $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription()); + + // dc:subject + $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject()); + + // cp:keywords + $objWriter->writeElement('cp:keywords', $spreadsheet->getProperties()->getKeywords()); + + // cp:category + $objWriter->writeElement('cp:category', $spreadsheet->getProperties()->getCategory()); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write docProps/custom.xml to XML format. + * + * @return null|string XML Output + */ + public function writeDocPropsCustom(Spreadsheet $spreadsheet) + { + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + if (empty($customPropertyList)) { + return null; + } + + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // cp:coreProperties + $objWriter->startElement('Properties'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties'); + $objWriter->writeAttribute('xmlns:vt', 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + + foreach ($customPropertyList as $key => $customProperty) { + $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty); + $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty); + + $objWriter->startElement('property'); + $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); + $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('name', $customProperty); + + switch ($propertyType) { + case Properties::PROPERTY_TYPE_INTEGER: + $objWriter->writeElement('vt:i4', $propertyValue); + + break; + case Properties::PROPERTY_TYPE_FLOAT: + $objWriter->writeElement('vt:r8', $propertyValue); + + break; + case Properties::PROPERTY_TYPE_BOOLEAN: + $objWriter->writeElement('vt:bool', ($propertyValue) ? 'true' : 'false'); + + break; + case Properties::PROPERTY_TYPE_DATE: + $objWriter->startElement('vt:filetime'); + $date = Date::dateTimeFromTimestamp("$propertyValue"); + $objWriter->writeRawData($date->format(DATE_W3C)); + $objWriter->endElement(); + + break; + default: + $objWriter->writeElement('vt:lpwstr', $propertyValue); + + break; + } + + $objWriter->endElement(); + } + + $objWriter->endElement(); + + return $objWriter->getData(); + } +} diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php new file mode 100644 index 0000000..fa77e2d --- /dev/null +++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -0,0 +1,500 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // xdr:wsDr + $objWriter->startElement('xdr:wsDr'); + $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing'); + $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + + // Loop through images and write drawings + $i = 1; + $iterator = $worksheet->getDrawingCollection()->getIterator(); + while ($iterator->valid()) { + /** @var BaseDrawing $pDrawing */ + $pDrawing = $iterator->current(); + $pRelationId = $i; + $hlinkClickId = $pDrawing->getHyperlink() === null ? null : ++$i; + + $this->writeDrawing($objWriter, $pDrawing, $pRelationId, $hlinkClickId); + + $iterator->next(); + ++$i; + } + + if ($includeCharts) { + $chartCount = $worksheet->getChartCount(); + // Loop through charts and write the chart position + if ($chartCount > 0) { + for ($c = 0; $c < $chartCount; ++$c) { + $this->writeChart($objWriter, $worksheet->getChartByIndex($c), $c + $i); + } + } + } + + // unparsed AlternateContent + $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData(); + if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingAlternateContents'])) { + foreach ($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingAlternateContents'] as $drawingAlternateContent) { + $objWriter->writeRaw($drawingAlternateContent); + } + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write drawings to XML format. + * + * @param int $relationId + */ + public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $relationId = -1): void + { + $tl = $chart->getTopLeftPosition(); + $tlColRow = Coordinate::indexesFromString($tl['cell']); + $br = $chart->getBottomRightPosition(); + $brColRow = Coordinate::indexesFromString($br['cell']); + + $objWriter->startElement('xdr:twoCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', $tlColRow[0] - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); + $objWriter->writeElement('xdr:row', $tlColRow[1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', $brColRow[0] - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); + $objWriter->writeElement('xdr:row', $brColRow[1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); + $objWriter->endElement(); + + $objWriter->startElement('xdr:graphicFrame'); + $objWriter->writeAttribute('macro', ''); + $objWriter->startElement('xdr:nvGraphicFramePr'); + $objWriter->startElement('xdr:cNvPr'); + $objWriter->writeAttribute('name', 'Chart ' . $relationId); + $objWriter->writeAttribute('id', 1025 * $relationId); + $objWriter->endElement(); + $objWriter->startElement('xdr:cNvGraphicFramePr'); + $objWriter->startElement('a:graphicFrameLocks'); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('xdr:xfrm'); + $objWriter->startElement('a:off'); + $objWriter->writeAttribute('x', '0'); + $objWriter->writeAttribute('y', '0'); + $objWriter->endElement(); + $objWriter->startElement('a:ext'); + $objWriter->writeAttribute('cx', '0'); + $objWriter->writeAttribute('cy', '0'); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('a:graphic'); + $objWriter->startElement('a:graphicData'); + $objWriter->writeAttribute('uri', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $objWriter->startElement('c:chart'); + $objWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:id', 'rId' . $relationId); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + $objWriter->endElement(); + + $objWriter->startElement('xdr:clientData'); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Write drawings to XML format. + * + * @param int $relationId + * @param null|int $hlinkClickId + */ + public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void + { + if ($relationId >= 0) { + // xdr:oneCellAnchor + $objWriter->startElement('xdr:oneCellAnchor'); + // Image location + $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); + + // xdr:from + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); + $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); + $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); + $objWriter->endElement(); + + // xdr:ext + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); + $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); + $objWriter->endElement(); + + // xdr:pic + $objWriter->startElement('xdr:pic'); + + // xdr:nvPicPr + $objWriter->startElement('xdr:nvPicPr'); + + // xdr:cNvPr + $objWriter->startElement('xdr:cNvPr'); + $objWriter->writeAttribute('id', $relationId); + $objWriter->writeAttribute('name', $drawing->getName()); + $objWriter->writeAttribute('descr', $drawing->getDescription()); + + //a:hlinkClick + $this->writeHyperLinkDrawing($objWriter, $hlinkClickId); + + $objWriter->endElement(); + + // xdr:cNvPicPr + $objWriter->startElement('xdr:cNvPicPr'); + + // a:picLocks + $objWriter->startElement('a:picLocks'); + $objWriter->writeAttribute('noChangeAspect', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:blipFill + $objWriter->startElement('xdr:blipFill'); + + // a:blip + $objWriter->startElement('a:blip'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:embed', 'rId' . $relationId); + $objWriter->endElement(); + + // a:stretch + $objWriter->startElement('a:stretch'); + $objWriter->writeElement('a:fillRect', null); + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:spPr + $objWriter->startElement('xdr:spPr'); + + // a:xfrm + $objWriter->startElement('a:xfrm'); + $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getRotation())); + $objWriter->endElement(); + + // a:prstGeom + $objWriter->startElement('a:prstGeom'); + $objWriter->writeAttribute('prst', 'rect'); + + // a:avLst + $objWriter->writeElement('a:avLst', null); + + $objWriter->endElement(); + + if ($drawing->getShadow()->getVisible()) { + // a:effectLst + $objWriter->startElement('a:effectLst'); + + // a:outerShdw + $objWriter->startElement('a:outerShdw'); + $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getBlurRadius())); + $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getDistance())); + $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getShadow()->getDirection())); + $objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment()); + $objWriter->writeAttribute('rotWithShape', '0'); + + // a:srgbClr + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $drawing->getShadow()->getColor()->getRGB()); + + // a:alpha + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', $drawing->getShadow()->getAlpha() * 1000); + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + + $objWriter->endElement(); + } + $objWriter->endElement(); + + $objWriter->endElement(); + + // xdr:clientData + $objWriter->writeElement('xdr:clientData', null); + + $objWriter->endElement(); + } else { + throw new WriterException('Invalid parameters passed.'); + } + } + + /** + * Write VML header/footer images to XML format. + * + * @return string XML Output + */ + public function writeVMLHeaderFooterImages(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet) + { + // Create XML writer + $objWriter = null; + if ($this->getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Header/footer images + $images = $worksheet->getHeaderFooter()->getImages(); + + // xml + $objWriter->startElement('xml'); + $objWriter->writeAttribute('xmlns:v', 'urn:schemas-microsoft-com:vml'); + $objWriter->writeAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office'); + $objWriter->writeAttribute('xmlns:x', 'urn:schemas-microsoft-com:office:excel'); + + // o:shapelayout + $objWriter->startElement('o:shapelayout'); + $objWriter->writeAttribute('v:ext', 'edit'); + + // o:idmap + $objWriter->startElement('o:idmap'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('data', '1'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // v:shapetype + $objWriter->startElement('v:shapetype'); + $objWriter->writeAttribute('id', '_x0000_t75'); + $objWriter->writeAttribute('coordsize', '21600,21600'); + $objWriter->writeAttribute('o:spt', '75'); + $objWriter->writeAttribute('o:preferrelative', 't'); + $objWriter->writeAttribute('path', 'm@4@5l@4@11@9@11@9@5xe'); + $objWriter->writeAttribute('filled', 'f'); + $objWriter->writeAttribute('stroked', 'f'); + + // v:stroke + $objWriter->startElement('v:stroke'); + $objWriter->writeAttribute('joinstyle', 'miter'); + $objWriter->endElement(); + + // v:formulas + $objWriter->startElement('v:formulas'); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'if lineDrawn pixelLineWidth 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @0 1 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum 0 0 @1'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @2 1 2'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelWidth'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelHeight'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @0 0 1'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @6 1 2'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelWidth'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @8 21600 0'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelHeight'); + $objWriter->endElement(); + + // v:f + $objWriter->startElement('v:f'); + $objWriter->writeAttribute('eqn', 'sum @10 21600 0'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // v:path + $objWriter->startElement('v:path'); + $objWriter->writeAttribute('o:extrusionok', 'f'); + $objWriter->writeAttribute('gradientshapeok', 't'); + $objWriter->writeAttribute('o:connecttype', 'rect'); + $objWriter->endElement(); + + // o:lock + $objWriter->startElement('o:lock'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('aspectratio', 't'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // Loop through images + foreach ($images as $key => $value) { + $this->writeVMLHeaderFooterImage($objWriter, $key, $value); + } + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } + + /** + * Write VML comment to XML format. + * + * @param string $reference Reference + */ + private function writeVMLHeaderFooterImage(XMLWriter $objWriter, $reference, HeaderFooterDrawing $image): void + { + // Calculate object id + preg_match('{(\d+)}', md5($reference), $m); + $id = 1500 + ((int) substr($m[1], 0, 2) * 1); + + // Calculate offset + $width = $image->getWidth(); + $height = $image->getHeight(); + $marginLeft = $image->getOffsetX(); + $marginTop = $image->getOffsetY(); + + // v:shape + $objWriter->startElement('v:shape'); + $objWriter->writeAttribute('id', $reference); + $objWriter->writeAttribute('o:spid', '_x0000_s' . $id); + $objWriter->writeAttribute('type', '#_x0000_t75'); + $objWriter->writeAttribute('style', "position:absolute;margin-left:{$marginLeft}px;margin-top:{$marginTop}px;width:{$width}px;height:{$height}px;z-index:1"); + + // v:imagedata + $objWriter->startElement('v:imagedata'); + $objWriter->writeAttribute('o:relid', 'rId' . $reference); + $objWriter->writeAttribute('o:title', $image->getName()); + $objWriter->endElement(); + + // o:lock + $objWriter->startElement('o:lock'); + $objWriter->writeAttribute('v:ext', 'edit'); + $objWriter->writeAttribute('textRotation', 't'); + $objWriter->endElement(); + + $objWriter->endElement(); + } + + /** + * Get an array of all drawings. + * + * @return BaseDrawing[] All drawings in PhpSpreadsheet + */ + public function allDrawings(Spreadsheet $spreadsheet) + { + // Get an array of all drawings + $aDrawings = []; + + // Loop through PhpSpreadsheet + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + // Loop through images and add to array + $iterator = $spreadsheet->getSheet($i)->getDrawingCollection()->getIterator(); + while ($iterator->valid()) { + $aDrawings[] = $iterator->current(); + + $iterator->next(); + } + } + + return $aDrawings; + } + + /** + * @param null|int $hlinkClickId + */ + private function writeHyperLinkDrawing(XMLWriter $objWriter, $hlinkClickId): void + { + if ($hlinkClickId === null) { + return; + } + + $objWriter->startElement('a:hlinkClick'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); + $objWriter->endElement(); + } +} diff --git a/vendor/topthink/framework/LICENSE.txt b/vendor/topthink/framework/LICENSE.txt new file mode 100644 index 0000000..4e910bb --- /dev/null +++ b/vendor/topthink/framework/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2019 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/topthink/framework/logo.png b/vendor/topthink/framework/logo.png new file mode 100644 index 0000000..25fd059 Binary files /dev/null and b/vendor/topthink/framework/logo.png differ diff --git a/vendor/topthink/framework/src/helper.php b/vendor/topthink/framework/src/helper.php new file mode 100644 index 0000000..caba0ae --- /dev/null +++ b/vendor/topthink/framework/src/helper.php @@ -0,0 +1,664 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +//------------------------ +// ThinkPHP 助手函数 +//------------------------- + +use think\App; +use think\Container; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\facade\Cache; +use think\facade\Config; +use think\facade\Cookie; +use think\facade\Env; +use think\facade\Event; +use think\facade\Lang; +use think\facade\Log; +use think\facade\Request; +use think\facade\Route; +use think\facade\Session; +use think\Response; +use think\response\File; +use think\response\Json; +use think\response\Jsonp; +use think\response\Redirect; +use think\response\View; +use think\response\Xml; +use think\route\Url as UrlBuild; +use think\Validate; + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer|Response $code 状态码 或者 Response对象实例 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, string $message = '', array $header = []) + { + if ($code instanceof Response) { + throw new HttpResponseException($code); + } else { + throw new HttpException($code, $message, null, $header); + } + } +} + +if (!function_exists('app')) { + /** + * 快速获取容器中的实例 支持依赖注入 + * @template T + * @param string|class-string $name 类名或标识 默认获取当前应用实例 + * @param array $args 参数 + * @param bool $newInstance 是否每次创建新的实例 + * @return T|object|App + */ + function app(string $name = '', array $args = [], bool $newInstance = false) + { + return Container::getInstance()->make($name ?: App::class, $args, $newInstance); + } +} + +if (!function_exists('bind')) { + /** + * 绑定一个类到容器 + * @param string|array $abstract 类标识、接口(支持批量绑定) + * @param mixed $concrete 要绑定的类、闭包或者实例 + * @return Container + */ + function bind($abstract, $concrete = null) + { + return Container::getInstance()->bind($abstract, $concrete); + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param string $name 缓存名称 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @param string $tag 缓存标签 + * @return mixed + */ + function cache(string $name = null, $value = '', $options = null, $tag = null) + { + if (is_null($name)) { + return app('cache'); + } + + if ('' === $value) { + // 获取缓存 + return 0 === strpos($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name); + } elseif (is_null($value)) { + // 删除缓存 + return Cache::delete($name); + } + + // 缓存数据 + if (is_array($options)) { + $expire = $options['expire'] ?? null; //修复查询缓存无法设置过期时间 + } else { + $expire = $options; + } + + if (is_null($tag)) { + return Cache::set($name, $value, $expire); + } else { + return Cache::tag($tag)->set($name, $value, $expire); + } + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + function config($name = '', $value = null) + { + if (is_array($name)) { + return Config::set($name, $value); + } + + return 0 === strpos($name, '?') ? Config::has(substr($name, 1)) : Config::get($name, $value); + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie(string $name, $value = '', $option = null) + { + if (is_null($value)) { + // 删除 + Cookie::delete($name, $option ?: []); + } elseif ('' === $value) { + // 获取 + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1)) : Cookie::get($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('download')) { + /** + * 获取\think\response\Download对象实例 + * @param string $filename 要下载的文件 + * @param string $name 显示文件名 + * @param bool $content 是否为内容 + * @param int $expire 有效期(秒) + * @return \think\response\File + */ + function download(string $filename, string $name = '', bool $content = false, int $expire = 180): File + { + return Response::create($filename, 'file')->name($name)->isContent($content)->expire($expire); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $vars 要输出的变量 + * @return void + */ + function dump(...$vars) + { + ob_start(); + var_dump(...$vars); + + $output = ob_get_clean(); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + + if (PHP_SAPI == 'cli') { + $output = PHP_EOL . $output . PHP_EOL; + } else { + if (!extension_loaded('xdebug')) { + $output = htmlspecialchars($output, ENT_SUBSTITUTE); + } + $output = '
' . $output . '
'; + } + + echo $output; + } +} + +if (!function_exists('env')) { + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名(支持二级 .号分割) + * @param string $default 默认值 + * @return mixed + */ + function env(string $name = null, $default = null) + { + return Env::get($name, $default); + } +} + +if (!function_exists('event')) { + /** + * 触发事件 + * @param mixed $event 事件名(或者类名) + * @param mixed $args 参数 + * @return mixed + */ + function event($event, $args = null) + { + return Event::trigger($event, $args); + } +} + +if (!function_exists('halt')) { + /** + * 调试变量并且中断输出 + * @param mixed $vars 调试变量或者信息 + */ + function halt(...$vars) + { + dump(...$vars); + + throw new HttpResponseException(Response::create()); + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input(string $key = '', $default = null, $filter = '') + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + + if ($pos = strpos($key, '.')) { + // 指定参数来源 + $method = substr($key, 0, $pos); + if (in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = substr($key, $pos + 1); + if ('server' == $method && is_null($default)) { + $default = ''; + } + } else { + $method = 'param'; + } + } else { + // 默认为自动判断 + $method = 'param'; + } + + return isset($has) ? + request()->has($key, $method) : + request()->$method($key, $default, $filter); + } +} + +if (!function_exists('invoke')) { + /** + * 调用反射实例化对象或者执行方法 支持依赖注入 + * @param mixed $call 类名或者callable + * @param array $args 参数 + * @return mixed + */ + function invoke($call, array $args = []) + { + if (is_callable($call)) { + return Container::getInstance()->invoke($call, $args); + } + + return Container::getInstance()->invokeClass($call, $args); + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param int $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []): Json + { + return Response::create($data, 'json', $code)->header($header)->options($options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param int $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []): Jsonp + { + return Response::create($data, 'jsonp', $code)->header($header)->options($options); + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang(string $name, array $vars = [], string $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('parse_name')) { + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param int $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) + * @return string + */ + function parse_name(string $name, int $type = 0, bool $ucfirst = true): string + { + if ($type) { + $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name); + + return $ucfirst ? ucfirst($name) : lcfirst($name); + } + + return strtolower(trim(preg_replace('/[A-Z]/', '_\\0', $name), '_')); + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param string $url 重定向地址 + * @param int $code 状态码 + * @return \think\response\Redirect + */ + function redirect(string $url = '', int $code = 302): Redirect + { + return Response::create($url, 'redirect', $code); + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request(): \think\Request + { + return app('request'); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = '', $code = 200, $header = [], $type = 'html'): Response + { + return Response::create($data, $type, $code)->header($header); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string $name session名称 + * @param mixed $value session值 + * @return mixed + */ + function session($name = '', $value = '') + { + if (is_null($name)) { + // 清除 + Session::clear(); + } elseif ('' === $name) { + return Session::all(); + } elseif (is_null($value)) { + // 删除 + Session::delete($name); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1)) : Session::get($name); + } else { + // 设置 + Session::set($name, $value); + } + } +} + +if (!function_exists('token')) { + /** + * 获取Token令牌 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token(string $name = '__token__', string $type = 'md5'): string + { + return Request::buildToken($name, $type); + } +} + +if (!function_exists('token_field')) { + /** + * 生成令牌隐藏表单 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token_field(string $name = '__token__', string $type = 'md5'): string + { + $token = Request::buildToken($name, $type); + + return ''; + } +} + +if (!function_exists('token_meta')) { + /** + * 生成令牌meta + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token_meta(string $name = '__token__', string $type = 'md5'): string + { + $token = Request::buildToken($name, $type); + + return ''; + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return array|void + */ + function trace($log = '[think]', string $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } + + Log::record($log, $level); + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @return UrlBuild + */ + function url(string $url = '', array $vars = [], $suffix = true, $domain = false): UrlBuild + { + return Route::buildUrl($url, $vars)->suffix($suffix)->domain($domain); + } +} + +if (!function_exists('validate')) { + /** + * 生成验证对象 + * @param string|array $validate 验证器类名或者验证规则数组 + * @param array $message 错误提示信息 + * @param bool $batch 是否批量验证 + * @param bool $failException 是否抛出异常 + * @return Validate + */ + function validate($validate = '', array $message = [], bool $batch = false, bool $failException = true): Validate + { + if (is_array($validate) || '' === $validate) { + $v = new Validate(); + if (is_array($validate)) { + $v->rule($validate); + } + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + + $class = false !== strpos($validate, '\\') ? $validate : app()->parseClass('validate', $validate); + + $v = new $class(); + + if (!empty($scene)) { + $v->scene($scene); + } + } + + return $v->message($message)->batch($batch)->failException($failException); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param int $code 状态码 + * @param callable $filter 内容过滤 + * @return \think\response\View + */ + function view(string $template = '', $vars = [], $code = 200, $filter = null): View + { + return Response::create($template, 'view', $code)->assign($vars)->filter($filter); + } +} + +if (!function_exists('display')) { + /** + * 渲染模板输出 + * @param string $content 渲染内容 + * @param array $vars 模板变量 + * @param int $code 状态码 + * @param callable $filter 内容过滤 + * @return \think\response\View + */ + function display(string $content, $vars = [], $code = 200, $filter = null): View + { + return Response::create($content, 'view', $code)->isContent(true)->assign($vars)->filter($filter); + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param int $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []): Xml + { + return Response::create($data, 'xml', $code)->header($header)->options($options); + } +} + +if (!function_exists('app_path')) { + /** + * 获取当前应用目录 + * + * @param string $path + * @return string + */ + function app_path($path = '') + { + return app()->getAppPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path); + } +} + +if (!function_exists('base_path')) { + /** + * 获取应用基础目录 + * + * @param string $path + * @return string + */ + function base_path($path = '') + { + return app()->getBasePath() . ($path ? $path . DIRECTORY_SEPARATOR : $path); + } +} + +if (!function_exists('config_path')) { + /** + * 获取应用配置目录 + * + * @param string $path + * @return string + */ + function config_path($path = '') + { + return app()->getConfigPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path); + } +} + +if (!function_exists('public_path')) { + /** + * 获取web根目录 + * + * @param string $path + * @return string + */ + function public_path($path = '') + { + return app()->getRootPath() . 'public' . DIRECTORY_SEPARATOR . ($path ? ltrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $path); + } +} + +if (!function_exists('runtime_path')) { + /** + * 获取应用运行时目录 + * + * @param string $path + * @return string + */ + function runtime_path($path = '') + { + return app()->getRuntimePath() . ($path ? $path . DIRECTORY_SEPARATOR : $path); + } +} + +if (!function_exists('root_path')) { + /** + * 获取项目根目录 + * + * @param string $path + * @return string + */ + function root_path($path = '') + { + return app()->getRootPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path); + } +} diff --git a/vendor/topthink/framework/src/think/Env.php b/vendor/topthink/framework/src/think/Env.php new file mode 100644 index 0000000..9e1b7f4 --- /dev/null +++ b/vendor/topthink/framework/src/think/Env.php @@ -0,0 +1,201 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use ArrayAccess; + +/** + * Env管理类 + * @package think + */ +class Env implements ArrayAccess +{ + /** + * 环境变量数据 + * @var array + */ + protected $data = []; + + /** + * 数据转换映射 + * @var array + */ + protected $convert = [ + 'true' => true, + 'false' => false, + 'off' => false, + 'on' => true, + ]; + + public function __construct() + { + $this->data = $_ENV; + } + + /** + * 读取环境变量定义文件 + * @access public + * @param string $file 环境变量定义文件 + * @return void + */ + public function load(string $file): void + { + $env = parse_ini_file($file, true, INI_SCANNER_RAW) ?: []; + $this->set($env); + } + + /** + * 获取环境变量值 + * @access public + * @param string $name 环境变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get(string $name = null, $default = null) + { + if (is_null($name)) { + return $this->data; + } + + $name = strtoupper(str_replace('.', '_', $name)); + if (isset($this->data[$name])) { + $result = $this->data[$name]; + + if (is_string($result) && isset($this->convert[$result])) { + return $this->convert[$result]; + } + + return $result; + } + + return $this->getEnv($name, $default); + } + + protected function getEnv(string $name, $default = null) + { + $result = getenv('PHP_' . $name); + + if (false === $result) { + return $default; + } + + if ('false' === $result) { + $result = false; + } elseif ('true' === $result) { + $result = true; + } + + if (!isset($this->data[$name])) { + $this->data[$name] = $result; + } + + return $result; + } + + /** + * 设置环境变量值 + * @access public + * @param string|array $env 环境变量 + * @param mixed $value 值 + * @return void + */ + public function set($env, $value = null): void + { + if (is_array($env)) { + $env = array_change_key_case($env, CASE_UPPER); + + foreach ($env as $key => $val) { + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->data[$key . '_' . strtoupper($k)] = $v; + } + } else { + $this->data[$key] = $val; + } + } + } else { + $name = strtoupper(str_replace('.', '_', $env)); + + $this->data[$name] = $value; + } + } + + /** + * 检测是否存在环境变量 + * @access public + * @param string $name 参数名 + * @return bool + */ + public function has(string $name): bool + { + return !is_null($this->get($name)); + } + + /** + * 设置环境变量 + * @access public + * @param string $name 参数名 + * @param mixed $value 值 + */ + public function __set(string $name, $value): void + { + $this->set($name, $value); + } + + /** + * 获取环境变量 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function __get(string $name) + { + return $this->get($name); + } + + /** + * 检测是否存在环境变量 + * @access public + * @param string $name 参数名 + * @return bool + */ + public function __isset(string $name): bool + { + return $this->has($name); + } + + // ArrayAccess + #[\ReturnTypeWillChange] + public function offsetSet($name, $value): void + { + $this->set($name, $value); + } + + #[\ReturnTypeWillChange] + public function offsetExists($name): bool + { + return $this->__isset($name); + } + + #[\ReturnTypeWillChange] + public function offsetUnset($name): void + { + throw new Exception('not support: unset'); + } + + #[\ReturnTypeWillChange] + public function offsetGet($name) + { + return $this->get($name); + } +} diff --git a/vendor/topthink/framework/src/think/Event.php b/vendor/topthink/framework/src/think/Event.php new file mode 100644 index 0000000..3c70aad --- /dev/null +++ b/vendor/topthink/framework/src/think/Event.php @@ -0,0 +1,272 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use ReflectionClass; +use ReflectionMethod; + +/** + * 事件管理类 + * @package think + */ +class Event +{ + /** + * 监听者 + * @var array + */ + protected $listener = []; + + /** + * 事件别名 + * @var array + */ + protected $bind = [ + 'AppInit' => event\AppInit::class, + 'HttpRun' => event\HttpRun::class, + 'HttpEnd' => event\HttpEnd::class, + 'RouteLoaded' => event\RouteLoaded::class, + 'LogWrite' => event\LogWrite::class, + 'LogRecord' => event\LogRecord::class, + ]; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * 批量注册事件监听 + * @access public + * @param array $events 事件定义 + * @return $this + */ + public function listenEvents(array $events) + { + foreach ($events as $event => $listeners) { + if (isset($this->bind[$event])) { + $event = $this->bind[$event]; + } + + $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners); + } + + return $this; + } + + /** + * 注册事件监听 + * @access public + * @param string $event 事件名称 + * @param mixed $listener 监听操作(或者类名) + * @param bool $first 是否优先执行 + * @return $this + */ + public function listen(string $event, $listener, bool $first = false) + { + if (isset($this->bind[$event])) { + $event = $this->bind[$event]; + } + + if ($first && isset($this->listener[$event])) { + array_unshift($this->listener[$event], $listener); + } else { + $this->listener[$event][] = $listener; + } + + return $this; + } + + /** + * 是否存在事件监听 + * @access public + * @param string $event 事件名称 + * @return bool + */ + public function hasListener(string $event): bool + { + if (isset($this->bind[$event])) { + $event = $this->bind[$event]; + } + + return isset($this->listener[$event]); + } + + /** + * 移除事件监听 + * @access public + * @param string $event 事件名称 + * @return void + */ + public function remove(string $event): void + { + if (isset($this->bind[$event])) { + $event = $this->bind[$event]; + } + + unset($this->listener[$event]); + } + + /** + * 指定事件别名标识 便于调用 + * @access public + * @param array $events 事件别名 + * @return $this + */ + public function bind(array $events) + { + $this->bind = array_merge($this->bind, $events); + + return $this; + } + + /** + * 注册事件订阅者 + * @access public + * @param mixed $subscriber 订阅者 + * @return $this + */ + public function subscribe($subscriber) + { + $subscribers = (array) $subscriber; + + foreach ($subscribers as $subscriber) { + if (is_string($subscriber)) { + $subscriber = $this->app->make($subscriber); + } + + if (method_exists($subscriber, 'subscribe')) { + // 手动订阅 + $subscriber->subscribe($this); + } else { + // 智能订阅 + $this->observe($subscriber); + } + } + + return $this; + } + + /** + * 自动注册事件观察者 + * @access public + * @param string|object $observer 观察者 + * @param null|string $prefix 事件名前缀 + * @return $this + */ + public function observe($observer, string $prefix = '') + { + if (is_string($observer)) { + $observer = $this->app->make($observer); + } + + $reflect = new ReflectionClass($observer); + $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC); + + if (empty($prefix) && $reflect->hasProperty('eventPrefix')) { + $reflectProperty = $reflect->getProperty('eventPrefix'); + $reflectProperty->setAccessible(true); + $prefix = $reflectProperty->getValue($observer); + } + + foreach ($methods as $method) { + $name = $method->getName(); + if (0 === strpos($name, 'on')) { + $this->listen($prefix . substr($name, 2), [$observer, $name]); + } + } + + return $this; + } + + /** + * 触发事件 + * @access public + * @param string|object $event 事件名称 + * @param mixed $params 传入参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public function trigger($event, $params = null, bool $once = false) + { + if (is_object($event)) { + $params = $event; + $event = get_class($event); + } + + if (isset($this->bind[$event])) { + $event = $this->bind[$event]; + } + + $result = []; + $listeners = $this->listener[$event] ?? []; + + if (strpos($event, '.')) { + [$prefix, $event] = explode('.', $event, 2); + if (isset($this->listener[$prefix . '.*'])) { + $listeners = array_merge($listeners, $this->listener[$prefix . '.*']); + } + } + + $listeners = array_unique($listeners, SORT_REGULAR); + + foreach ($listeners as $key => $listener) { + $result[$key] = $this->dispatch($listener, $params); + + if (false === $result[$key] || (!is_null($result[$key]) && $once)) { + break; + } + } + + return $once ? end($result) : $result; + } + + /** + * 触发事件(只获取一个有效返回值) + * @param $event + * @param null $params + * @return mixed + */ + public function until($event, $params = null) + { + return $this->trigger($event, $params, true); + } + + /** + * 执行事件调度 + * @access protected + * @param mixed $event 事件方法 + * @param mixed $params 参数 + * @return mixed + */ + protected function dispatch($event, $params = null) + { + if (!is_string($event)) { + $call = $event; + } elseif (strpos($event, '::')) { + $call = $event; + } else { + $obj = $this->app->make($event); + $call = [$obj, 'handle']; + } + + return $this->app->invoke($call, [$params]); + } + +} diff --git a/vendor/topthink/framework/src/think/Exception.php b/vendor/topthink/framework/src/think/Exception.php new file mode 100644 index 0000000..5cf7954 --- /dev/null +++ b/vendor/topthink/framework/src/think/Exception.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +/** + * 异常基础类 + * @package think + */ +class Exception extends \Exception +{ + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData(string $label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @access public + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } + +} diff --git a/vendor/topthink/framework/src/think/Facade.php b/vendor/topthink/framework/src/think/Facade.php new file mode 100644 index 0000000..9a0e333 --- /dev/null +++ b/vendor/topthink/framework/src/think/Facade.php @@ -0,0 +1,98 @@ + +// +---------------------------------------------------------------------- +namespace think; + +/** + * Facade管理类 + */ +class Facade +{ + /** + * 始终创建新的对象实例 + * @var bool + */ + protected static $alwaysNewInstance; + + /** + * 创建Facade实例 + * @static + * @access protected + * @param string $class 类名或标识 + * @param array $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + protected static function createFacade(string $class = '', array $args = [], bool $newInstance = false) + { + $class = $class ?: static::class; + + $facadeClass = static::getFacadeClass(); + + if ($facadeClass) { + $class = $facadeClass; + } + + if (static::$alwaysNewInstance) { + $newInstance = true; + } + + return Container::getInstance()->make($class, $args, $newInstance); + } + + /** + * 获取当前Facade对应类名 + * @access protected + * @return string + */ + protected static function getFacadeClass() + {} + + /** + * 带参数实例化当前Facade类 + * @access public + * @return object + */ + public static function instance(...$args) + { + if (__CLASS__ != static::class) { + return self::createFacade('', $args); + } + } + + /** + * 调用类的实例 + * @access public + * @param string $class 类名或者标识 + * @param array|true $args 变量 + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + public static function make(string $class, $args = [], $newInstance = false) + { + if (__CLASS__ != static::class) { + return self::__callStatic('make', func_get_args()); + } + + if (true === $args) { + // 总是创建新的实例化对象 + $newInstance = true; + $args = []; + } + + return self::createFacade($class, $args, $newInstance); + } + + // 调用实际类的方法 + public static function __callStatic($method, $params) + { + return call_user_func_array([static::createFacade(), $method], $params); + } +} diff --git a/vendor/topthink/framework/src/think/File.php b/vendor/topthink/framework/src/think/File.php new file mode 100644 index 0000000..59230b5 --- /dev/null +++ b/vendor/topthink/framework/src/think/File.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use SplFileInfo; +use think\exception\FileException; + +/** + * 文件上传类 + * @package think + */ +class File extends SplFileInfo +{ + + /** + * 文件hash规则 + * @var array + */ + protected $hash = []; + + protected $hashName; + + /** + * 保存的文件后缀 + * @var string + */ + protected $extension; + + public function __construct(string $path, bool $checkPath = true) + { + if ($checkPath && !is_file($path)) { + throw new FileException(sprintf('The file "%s" does not exist', $path)); + } + + parent::__construct($path); + } + + /** + * 获取文件的哈希散列值 + * @access public + * @param string $type + * @return string + */ + public function hash(string $type = 'sha1'): string + { + if (!isset($this->hash[$type])) { + $this->hash[$type] = hash_file($type, $this->getPathname()); + } + + return $this->hash[$type]; + } + + /** + * 获取文件的MD5值 + * @access public + * @return string + */ + public function md5(): string + { + return $this->hash('md5'); + } + + /** + * 获取文件的SHA1值 + * @access public + * @return string + */ + public function sha1(): string + { + return $this->hash('sha1'); + } + + /** + * 获取文件类型信息 + * @access public + * @return string + */ + public function getMime(): string + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + return finfo_file($finfo, $this->getPathname()); + } + + /** + * 移动文件 + * @access public + * @param string $directory 保存路径 + * @param string|null $name 保存的文件名 + * @return File + */ + public function move(string $directory, string $name = null): File + { + $target = $this->getTargetFile($directory, $name); + + set_error_handler(function ($type, $msg) use (&$error) { + $error = $msg; + }); + $renamed = rename($this->getPathname(), (string) $target); + restore_error_handler(); + if (!$renamed) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); + } + + @chmod((string) $target, 0666 & ~umask()); + + return $target; + } + + /** + * 实例化一个新文件 + * @param string $directory + * @param null|string $name + * @return File + */ + protected function getTargetFile(string $directory, string $name = null): File + { + if (!is_dir($directory)) { + if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new FileException(sprintf('Unable to create the "%s" directory', $directory)); + } + } elseif (!is_writable($directory)) { + throw new FileException(sprintf('Unable to write in the "%s" directory', $directory)); + } + + $target = rtrim($directory, '/\\') . \DIRECTORY_SEPARATOR . (null === $name ? $this->getBasename() : $this->getName($name)); + + return new self($target, false); + } + + /** + * 获取文件名 + * @param string $name + * @return string + */ + protected function getName(string $name): string + { + $originalName = str_replace('\\', '/', $name); + $pos = strrpos($originalName, '/'); + $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); + + return $originalName; + } + + /** + * 文件扩展名 + * @return string + */ + public function extension(): string + { + return $this->getExtension(); + } + + /** + * 指定保存文件的扩展名 + * @param string $extension + * @return void + */ + public function setExtension(string $extension): void + { + $this->extension = $extension; + } + + /** + * 自动生成文件名 + * @access public + * @param string|\Closure $rule + * @return string + */ + public function hashName($rule = ''): string + { + if (!$this->hashName) { + if ($rule instanceof \Closure) { + $this->hashName = call_user_func_array($rule, [$this]); + } else { + switch (true) { + case in_array($rule, hash_algos()): + $hash = $this->hash($rule); + $this->hashName = substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2); + break; + case is_callable($rule): + $this->hashName = call_user_func($rule); + break; + default: + $this->hashName = date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true) . $this->getPathname()); + break; + } + } + } + + $extension = $this->extension ?? $this->extension(); + return $this->hashName . ($extension ? '.' . $extension : ''); + } +} diff --git a/vendor/topthink/framework/src/think/Http.php b/vendor/topthink/framework/src/think/Http.php new file mode 100644 index 0000000..4e49c88 --- /dev/null +++ b/vendor/topthink/framework/src/think/Http.php @@ -0,0 +1,288 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use think\event\HttpEnd; +use think\event\HttpRun; +use think\event\RouteLoaded; +use think\exception\Handle; +use Throwable; + +/** + * Web应用管理类 + * @package think + */ +class Http +{ + + /** + * @var App + */ + protected $app; + + /** + * 应用名称 + * @var string + */ + protected $name; + + /** + * 应用路径 + * @var string + */ + protected $path; + + /** + * 路由路径 + * @var string + */ + protected $routePath; + + /** + * 是否绑定应用 + * @var bool + */ + protected $isBind = false; + + public function __construct(App $app) + { + $this->app = $app; + + $this->routePath = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR; + } + + /** + * 设置应用名称 + * @access public + * @param string $name 应用名称 + * @return $this + */ + public function name(string $name) + { + $this->name = $name; + return $this; + } + + /** + * 获取应用名称 + * @access public + * @return string + */ + public function getName(): string + { + return $this->name ?: ''; + } + + /** + * 设置应用目录 + * @access public + * @param string $path 应用目录 + * @return $this + */ + public function path(string $path) + { + if (substr($path, -1) != DIRECTORY_SEPARATOR) { + $path .= DIRECTORY_SEPARATOR; + } + + $this->path = $path; + return $this; + } + + /** + * 获取应用路径 + * @access public + * @return string + */ + public function getPath(): string + { + return $this->path ?: ''; + } + + /** + * 获取路由目录 + * @access public + * @return string + */ + public function getRoutePath(): string + { + return $this->routePath; + } + + /** + * 设置路由目录 + * @access public + * @param string $path 路由定义目录 + */ + public function setRoutePath(string $path): void + { + $this->routePath = $path; + } + + /** + * 设置应用绑定 + * @access public + * @param bool $bind 是否绑定 + * @return $this + */ + public function setBind(bool $bind = true) + { + $this->isBind = $bind; + return $this; + } + + /** + * 是否绑定应用 + * @access public + * @return bool + */ + public function isBind(): bool + { + return $this->isBind; + } + + /** + * 执行应用程序 + * @access public + * @param Request|null $request + * @return Response + */ + public function run(Request $request = null): Response + { + //初始化 + $this->initialize(); + + //自动创建request对象 + $request = $request ?? $this->app->make('request', [], true); + $this->app->instance('request', $request); + + try { + $response = $this->runWithRequest($request); + } catch (Throwable $e) { + $this->reportException($e); + + $response = $this->renderException($request, $e); + } + + return $response; + } + + /** + * 初始化 + */ + protected function initialize() + { + if (!$this->app->initialized()) { + $this->app->initialize(); + } + } + + /** + * 执行应用程序 + * @param Request $request + * @return mixed + */ + protected function runWithRequest(Request $request) + { + // 加载全局中间件 + $this->loadMiddleware(); + + // 监听HttpRun + $this->app->event->trigger(HttpRun::class); + + return $this->app->middleware->pipeline() + ->send($request) + ->then(function ($request) { + return $this->dispatchToRoute($request); + }); + } + + protected function dispatchToRoute($request) + { + $withRoute = $this->app->config->get('app.with_route', true) ? function () { + $this->loadRoutes(); + } : null; + + return $this->app->route->dispatch($request, $withRoute); + } + + /** + * 加载全局中间件 + */ + protected function loadMiddleware(): void + { + if (is_file($this->app->getBasePath() . 'middleware.php')) { + $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php'); + } + } + + /** + * 加载路由 + * @access protected + * @return void + */ + protected function loadRoutes(): void + { + // 加载路由定义 + $routePath = $this->getRoutePath(); + + if (is_dir($routePath)) { + $files = glob($routePath . '*.php'); + foreach ($files as $file) { + include $file; + } + } + + $this->app->event->trigger(RouteLoaded::class); + } + + /** + * Report the exception to the exception handler. + * + * @param Throwable $e + * @return void + */ + protected function reportException(Throwable $e) + { + $this->app->make(Handle::class)->report($e); + } + + /** + * Render the exception to a response. + * + * @param Request $request + * @param Throwable $e + * @return Response + */ + protected function renderException($request, Throwable $e) + { + return $this->app->make(Handle::class)->render($request, $e); + } + + /** + * HttpEnd + * @param Response $response + * @return void + */ + public function end(Response $response): void + { + $this->app->event->trigger(HttpEnd::class, $response); + + //执行中间件 + $this->app->middleware->end($response); + + // 写入日志 + $this->app->log->save(); + } + +} diff --git a/vendor/topthink/framework/src/think/Lang.php b/vendor/topthink/framework/src/think/Lang.php new file mode 100644 index 0000000..b891084 --- /dev/null +++ b/vendor/topthink/framework/src/think/Lang.php @@ -0,0 +1,290 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +/** + * 多语言管理类 + * @package think + */ +class Lang +{ + protected $app; + + /** + * 配置参数 + * @var array + */ + protected $config = [ + // 默认语言 + 'default_lang' => 'zh-cn', + // 允许的语言列表 + 'allow_lang_list' => [], + // 是否使用Cookie记录 + 'use_cookie' => true, + // 扩展语言包 + 'extend_list' => [], + // 多语言cookie变量 + 'cookie_var' => 'think_lang', + // 多语言header变量 + 'header_var' => 'think-lang', + // 多语言自动侦测变量名 + 'detect_var' => 'lang', + // Accept-Language转义为对应语言包名称 + 'accept_language' => [ + 'zh-hans-cn' => 'zh-cn', + ], + // 是否支持语言分组 + 'allow_group' => false, + ]; + + /** + * 多语言信息 + * @var array + */ + private $lang = []; + + /** + * 当前语言 + * @var string + */ + private $range = 'zh-cn'; + + /** + * 构造方法 + * @access public + * @param array $config + */ + public function __construct(App $app, array $config = []) + { + $this->config = array_merge($this->config, array_change_key_case($config)); + $this->range = $this->config['default_lang']; + $this->app = $app; + } + + public static function __make(App $app, Config $config) + { + return new static($app, $config->get('lang')); + } + + /** + * 获取当前语言配置 + * @access public + * @return array + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * 设置当前语言 + * @access public + * @param string $lang 语言 + * @return void + */ + public function setLangSet(string $lang): void + { + $this->range = $lang; + } + + /** + * 获取当前语言 + * @access public + * @return string + */ + public function getLangSet(): string + { + return $this->range; + } + + /** + * 获取默认语言 + * @access public + * @return string + */ + public function defaultLangSet() + { + return $this->config['default_lang']; + } + + /** + * 切换语言 + * @access public + * @param string $langset 语言 + * @return void + */ + public function switchLangSet(string $langset) + { + if (empty($langset)) { + return; + } + + $this->setLangSet($langset); + + // 加载系统语言包 + $this->load([ + $this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php', + ]); + + // 加载系统语言包 + $files = glob($this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.*'); + $this->load($files); + + // 加载扩展(自定义)语言包 + $list = $this->app->config->get('lang.extend_list', []); + + if (isset($list[$langset])) { + $this->load($list[$langset]); + } + } + + /** + * 加载语言定义(不区分大小写) + * @access public + * @param string|array $file 语言文件 + * @param string $range 语言作用域 + * @return array + */ + public function load($file, $range = ''): array + { + $range = $range ?: $this->range; + if (!isset($this->lang[$range])) { + $this->lang[$range] = []; + } + + $lang = []; + + foreach ((array) $file as $name) { + if (is_file($name)) { + $result = $this->parse($name); + $lang = array_change_key_case($result) + $lang; + } + } + + if (!empty($lang)) { + $this->lang[$range] = $lang + $this->lang[$range]; + } + + return $this->lang[$range]; + } + + /** + * 解析语言文件 + * @access protected + * @param string $file 语言文件名 + * @return array + */ + protected function parse(string $file): array + { + $type = pathinfo($file, PATHINFO_EXTENSION); + + switch ($type) { + case 'php': + $result = include $file; + break; + case 'yml': + case 'yaml': + if (function_exists('yaml_parse_file')) { + $result = yaml_parse_file($file); + } + break; + case 'json': + $data = file_get_contents($file); + + if (false !== $data) { + $data = json_decode($data, true); + + if (json_last_error() === JSON_ERROR_NONE) { + $result = $data; + } + } + + break; + } + + return isset($result) && is_array($result) ? $result : []; + } + + /** + * 判断是否存在语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param string $range 语言作用域 + * @return bool + */ + public function has(string $name, string $range = ''): bool + { + $range = $range ?: $this->range; + + if ($this->config['allow_group'] && strpos($name, '.')) { + [$name1, $name2] = explode('.', $name, 2); + return isset($this->lang[$range][strtolower($name1)][$name2]); + } + + return isset($this->lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @access public + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public function get(string $name = null, array $vars = [], string $range = '') + { + $range = $range ?: $this->range; + + if (!isset($this->lang[$range])) { + $this->switchLangSet($range); + } + + // 空参数返回所有定义 + if (is_null($name)) { + return $this->lang[$range] ?? []; + } + + if ($this->config['allow_group'] && strpos($name, '.')) { + [$name1, $name2] = explode('.', $name, 2); + + $value = $this->lang[$range][strtolower($name1)][$name2] ?? $name; + } else { + $value = $this->lang[$range][strtolower($name)] ?? $name; + } + + // 变量解析 + if (!empty($vars) && is_array($vars)) { + /** + * Notes: + * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 + * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 + */ + if (key($vars) === 0) { + // 数字索引解析 + array_unshift($vars, $value); + $value = call_user_func_array('sprintf', $vars); + } else { + // 关联索引解析 + $replace = array_keys($vars); + foreach ($replace as &$v) { + $v = "{:{$v}}"; + } + $value = str_replace($replace, $vars, $value); + } + } + + return $value; + } + +} diff --git a/vendor/topthink/framework/src/think/Log.php b/vendor/topthink/framework/src/think/Log.php new file mode 100644 index 0000000..c31210c --- /dev/null +++ b/vendor/topthink/framework/src/think/Log.php @@ -0,0 +1,342 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use InvalidArgumentException; +use Psr\Log\LoggerInterface; +use think\event\LogWrite; +use think\helper\Arr; +use think\log\Channel; +use think\log\ChannelSet; + +/** + * 日志管理类 + * @package think + * @mixin Channel + */ +class Log extends Manager implements LoggerInterface +{ + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; + const SQL = 'sql'; + + protected $namespace = '\\think\\log\\driver\\'; + + /** + * 默认驱动 + * @return string|null + */ + public function getDefaultDriver() + { + return $this->getConfig('default'); + } + + /** + * 获取日志配置 + * @access public + * @param null|string $name 名称 + * @param mixed $default 默认值 + * @return mixed + */ + public function getConfig(string $name = null, $default = null) + { + if (!is_null($name)) { + return $this->app->config->get('log.' . $name, $default); + } + + return $this->app->config->get('log'); + } + + /** + * 获取渠道配置 + * @param string $channel + * @param null $name + * @param null $default + * @return array + */ + public function getChannelConfig($channel, $name = null, $default = null) + { + if ($config = $this->getConfig("channels.{$channel}")) { + return Arr::get($config, $name, $default); + } + + throw new InvalidArgumentException("Channel [$channel] not found."); + } + + /** + * driver()的别名 + * @param string|array $name 渠道名 + * @return Channel|ChannelSet + */ + public function channel($name = null) + { + if (is_array($name)) { + return new ChannelSet($this, $name); + } + + return $this->driver($name); + } + + protected function resolveType(string $name) + { + return $this->getChannelConfig($name, 'type', 'file'); + } + + public function createDriver(string $name) + { + $driver = parent::createDriver($name); + + $lazy = !$this->getChannelConfig($name, "realtime_write", false) && !$this->app->runningInConsole(); + $allow = array_merge($this->getConfig("level", []), $this->getChannelConfig($name, "level", [])); + + return new Channel($name, $driver, $allow, $lazy, $this->app->event); + } + + protected function resolveConfig(string $name) + { + return $this->getChannelConfig($name); + } + + /** + * 清空日志信息 + * @access public + * @param string|array $channel 日志通道名 + * @return $this + */ + public function clear($channel = '*') + { + if ('*' == $channel) { + $channel = array_keys($this->drivers); + } + + $this->channel($channel)->clear(); + + return $this; + } + + /** + * 关闭本次请求日志写入 + * @access public + * @param string|array $channel 日志通道名 + * @return $this + */ + public function close($channel = '*') + { + if ('*' == $channel) { + $channel = array_keys($this->drivers); + } + + $this->channel($channel)->close(); + + return $this; + } + + /** + * 获取日志信息 + * @access public + * @param string $channel 日志通道名 + * @return array + */ + public function getLog(string $channel = null): array + { + return $this->channel($channel)->getLog(); + } + + /** + * 保存日志信息 + * @access public + * @return bool + */ + public function save(): bool + { + /** @var Channel $channel */ + foreach ($this->drivers as $channel) { + $channel->save(); + } + + return true; + } + + /** + * 记录日志信息 + * @access public + * @param mixed $msg 日志信息 + * @param string $type 日志级别 + * @param array $context 替换内容 + * @param bool $lazy + * @return $this + */ + public function record($msg, string $type = 'info', array $context = [], bool $lazy = true) + { + $channel = $this->getConfig('type_channel.' . $type); + + $this->channel($channel)->record($msg, $type, $context, $lazy); + + return $this; + } + + /** + * 实时写入日志信息 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 日志级别 + * @param array $context 替换内容 + * @return $this + */ + public function write($msg, string $type = 'info', array $context = []) + { + return $this->record($msg, $type, $context, false); + } + + /** + * 注册日志写入事件监听 + * @param $listener + * @return Event + */ + public function listen($listener) + { + return $this->app->event->listen(LogWrite::class, $listener); + } + + /** + * 记录日志信息 + * @access public + * @param string $level 日志级别 + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function log($level, $message, array $context = []): void + { + $this->record($message, $level, $context); + } + + /** + * 记录emergency信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function emergency($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录警报信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function alert($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录紧急情况 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function critical($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录错误信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function error($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录warning信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function warning($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录notice信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function notice($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录一般信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function info($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录调试信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function debug($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + /** + * 记录sql信息 + * @access public + * @param mixed $message 日志信息 + * @param array $context 替换内容 + * @return void + */ + public function sql($message, array $context = []): void + { + $this->log(__FUNCTION__, $message, $context); + } + + public function __call($method, $parameters) + { + $this->log($method, ...$parameters); + } +} diff --git a/vendor/topthink/framework/src/think/Manager.php b/vendor/topthink/framework/src/think/Manager.php new file mode 100644 index 0000000..ca3f6a5 --- /dev/null +++ b/vendor/topthink/framework/src/think/Manager.php @@ -0,0 +1,177 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use InvalidArgumentException; +use think\helper\Str; + +abstract class Manager +{ + /** @var App */ + protected $app; + + /** + * 驱动 + * @var array + */ + protected $drivers = []; + + /** + * 驱动的命名空间 + * @var string + */ + protected $namespace = null; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * 获取驱动实例 + * @param null|string $name + * @return mixed + */ + protected function driver(string $name = null) + { + $name = $name ?: $this->getDefaultDriver(); + + if (is_null($name)) { + throw new InvalidArgumentException(sprintf( + 'Unable to resolve NULL driver for [%s].', + static::class + )); + } + + return $this->drivers[$name] = $this->getDriver($name); + } + + /** + * 获取驱动实例 + * @param string $name + * @return mixed + */ + protected function getDriver(string $name) + { + return $this->drivers[$name] ?? $this->createDriver($name); + } + + /** + * 获取驱动类型 + * @param string $name + * @return mixed + */ + protected function resolveType(string $name) + { + return $name; + } + + /** + * 获取驱动配置 + * @param string $name + * @return mixed + */ + protected function resolveConfig(string $name) + { + return $name; + } + + /** + * 获取驱动类 + * @param string $type + * @return string + */ + protected function resolveClass(string $type): string + { + if ($this->namespace || false !== strpos($type, '\\')) { + $class = false !== strpos($type, '\\') ? $type : $this->namespace . Str::studly($type); + + if (class_exists($class)) { + return $class; + } + } + + throw new InvalidArgumentException("Driver [$type] not supported."); + } + + /** + * 获取驱动参数 + * @param $name + * @return array + */ + protected function resolveParams($name): array + { + $config = $this->resolveConfig($name); + return [$config]; + } + + /** + * 创建驱动 + * + * @param string $name + * @return mixed + * + */ + protected function createDriver(string $name) + { + $type = $this->resolveType($name); + + $method = 'create' . Str::studly($type) . 'Driver'; + + $params = $this->resolveParams($name); + + if (method_exists($this, $method)) { + return $this->$method(...$params); + } + + $class = $this->resolveClass($type); + + return $this->app->invokeClass($class, $params); + } + + /** + * 移除一个驱动实例 + * + * @param array|string|null $name + * @return $this + */ + public function forgetDriver($name = null) + { + $name = $name ?? $this->getDefaultDriver(); + + foreach ((array) $name as $cacheName) { + if (isset($this->drivers[$cacheName])) { + unset($this->drivers[$cacheName]); + } + } + + return $this; + } + + /** + * 默认驱动 + * @return string|null + */ + abstract public function getDefaultDriver(); + + /** + * 动态调用 + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->driver()->$method(...$parameters); + } +} diff --git a/vendor/topthink/framework/src/think/Middleware.php b/vendor/topthink/framework/src/think/Middleware.php new file mode 100644 index 0000000..a3db0f2 --- /dev/null +++ b/vendor/topthink/framework/src/think/Middleware.php @@ -0,0 +1,257 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use Closure; +use InvalidArgumentException; +use LogicException; +use think\exception\Handle; +use Throwable; + +/** + * 中间件管理类 + * @package think + */ +class Middleware +{ + /** + * 中间件执行队列 + * @var array + */ + protected $queue = []; + + /** + * 应用对象 + * @var App + */ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * 导入中间件 + * @access public + * @param array $middlewares + * @param string $type 中间件类型 + * @return void + */ + public function import(array $middlewares = [], string $type = 'global'): void + { + foreach ($middlewares as $middleware) { + $this->add($middleware, $type); + } + } + + /** + * 注册中间件 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + * @return void + */ + public function add($middleware, string $type = 'global'): void + { + $middleware = $this->buildMiddleware($middleware, $type); + + if (!empty($middleware)) { + $this->queue[$type][] = $middleware; + $this->queue[$type] = array_unique($this->queue[$type], SORT_REGULAR); + } + } + + /** + * 注册路由中间件 + * @access public + * @param mixed $middleware + * @return void + */ + public function route($middleware): void + { + $this->add($middleware, 'route'); + } + + /** + * 注册控制器中间件 + * @access public + * @param mixed $middleware + * @return void + */ + public function controller($middleware): void + { + $this->add($middleware, 'controller'); + } + + /** + * 注册中间件到开始位置 + * @access public + * @param mixed $middleware + * @param string $type 中间件类型 + */ + public function unshift($middleware, string $type = 'global') + { + $middleware = $this->buildMiddleware($middleware, $type); + + if (!empty($middleware)) { + if (!isset($this->queue[$type])) { + $this->queue[$type] = []; + } + + array_unshift($this->queue[$type], $middleware); + } + } + + /** + * 获取注册的中间件 + * @access public + * @param string $type 中间件类型 + * @return array + */ + public function all(string $type = 'global'): array + { + return $this->queue[$type] ?? []; + } + + /** + * 调度管道 + * @access public + * @param string $type 中间件类型 + * @return Pipeline + */ + public function pipeline(string $type = 'global') + { + return (new Pipeline()) + ->through(array_map(function ($middleware) { + return function ($request, $next) use ($middleware) { + [$call, $params] = $middleware; + if (is_array($call) && is_string($call[0])) { + $call = [$this->app->make($call[0]), $call[1]]; + } + $response = call_user_func($call, $request, $next, ...$params); + + if (!$response instanceof Response) { + throw new LogicException('The middleware must return Response instance'); + } + return $response; + }; + }, $this->sortMiddleware($this->queue[$type] ?? []))) + ->whenException([$this, 'handleException']); + } + + /** + * 结束调度 + * @param Response $response + */ + public function end(Response $response) + { + foreach ($this->queue as $queue) { + foreach ($queue as $middleware) { + [$call] = $middleware; + if (is_array($call) && is_string($call[0])) { + $instance = $this->app->make($call[0]); + if (method_exists($instance, 'end')) { + $instance->end($response); + } + } + } + } + } + + /** + * 异常处理 + * @param Request $passable + * @param Throwable $e + * @return Response + */ + public function handleException($passable, Throwable $e) + { + /** @var Handle $handler */ + $handler = $this->app->make(Handle::class); + + $handler->report($e); + + return $handler->render($passable, $e); + } + + /** + * 解析中间件 + * @access protected + * @param mixed $middleware + * @param string $type 中间件类型 + * @return array + */ + protected function buildMiddleware($middleware, string $type): array + { + if (is_array($middleware)) { + [$middleware, $params] = $middleware; + } + + if ($middleware instanceof Closure) { + return [$middleware, $params ?? []]; + } + + if (!is_string($middleware)) { + throw new InvalidArgumentException('The middleware is invalid'); + } + + //中间件别名检查 + $alias = $this->app->config->get('middleware.alias', []); + + if (isset($alias[$middleware])) { + $middleware = $alias[$middleware]; + } + + if (is_array($middleware)) { + $this->import($middleware, $type); + return []; + } + + return [[$middleware, 'handle'], $params ?? []]; + } + + /** + * 中间件排序 + * @param array $middlewares + * @return array + */ + protected function sortMiddleware(array $middlewares) + { + $priority = $this->app->config->get('middleware.priority', []); + uasort($middlewares, function ($a, $b) use ($priority) { + $aPriority = $this->getMiddlewarePriority($priority, $a); + $bPriority = $this->getMiddlewarePriority($priority, $b); + return $bPriority - $aPriority; + }); + + return $middlewares; + } + + /** + * 获取中间件优先级 + * @param $priority + * @param $middleware + * @return int + */ + protected function getMiddlewarePriority($priority, $middleware) + { + [$call] = $middleware; + if (is_array($call) && is_string($call[0])) { + $index = array_search($call[0], array_reverse($priority)); + return false === $index ? -1 : $index; + } + return -1; + } + +} diff --git a/vendor/topthink/framework/src/think/cache/Driver.php b/vendor/topthink/framework/src/think/cache/Driver.php new file mode 100644 index 0000000..7dc0d30 --- /dev/null +++ b/vendor/topthink/framework/src/think/cache/Driver.php @@ -0,0 +1,359 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\cache; + +use Closure; +use DateInterval; +use DateTime; +use DateTimeInterface; +use Exception; +use Psr\SimpleCache\CacheInterface; +use think\Container; +use think\contract\CacheHandlerInterface; +use think\exception\InvalidArgumentException; +use throwable; + +/** + * 缓存基础类 + */ +abstract class Driver implements CacheInterface, CacheHandlerInterface +{ + /** + * 驱动句柄 + * @var object + */ + protected $handler = null; + + /** + * 缓存读取次数 + * @var integer + */ + protected $readTimes = 0; + + /** + * 缓存写入次数 + * @var integer + */ + protected $writeTimes = 0; + + /** + * 缓存参数 + * @var array + */ + protected $options = []; + + /** + * 缓存标签 + * @var array + */ + protected $tag = []; + + /** + * 获取有效期 + * @access protected + * @param integer|DateTimeInterface|DateInterval $expire 有效期 + * @return int + */ + protected function getExpireTime($expire): int + { + if ($expire instanceof DateTimeInterface) { + $expire = $expire->getTimestamp() - time(); + } elseif ($expire instanceof DateInterval) { + $expire = DateTime::createFromFormat('U', (string) time()) + ->add($expire) + ->format('U') - time(); + } + + return (int) $expire; + } + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + public function getCacheKey(string $name): string + { + return $this->options['prefix'] . $name; + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function pull(string $name) + { + $result = $this->get($name, false); + + if ($result) { + $this->delete($name); + return $result; + } + } + + /** + * 追加(数组)缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @return void + */ + public function push(string $name, $value): void + { + $item = $this->get($name, []); + + if (!is_array($item)) { + throw new InvalidArgumentException('only array cache can be push'); + } + + $item[] = $value; + + if (count($item) > 1000) { + array_shift($item); + } + + $item = array_unique($item); + + $this->set($name, $item); + } + + /** + * 追加TagSet数据 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @return void + */ + public function append(string $name, $value): void + { + $this->push($name, $value); + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember(string $name, $value, $expire = null) + { + if ($this->has($name)) { + if (($hit = $this->get($name)) !== null) { + return $hit; + } + } + + $time = time(); + + while ($time + 5 > time() && $this->has($name . '_lock')) { + // 存在锁定则等待 + usleep(200000); + } + + try { + // 锁定 + $this->set($name . '_lock', true); + + if ($value instanceof Closure) { + // 获取缓存数据 + $value = Container::getInstance()->invokeFunction($value); + } + + // 缓存数据 + $this->set($name, $value, $expire); + + // 解锁 + $this->delete($name . '_lock'); + } catch (Exception | throwable $e) { + $this->delete($name . '_lock'); + throw $e; + } + + return $value; + } + + /** + * 缓存标签 + * @access public + * @param string|array $name 标签名 + * @return TagSet + */ + public function tag($name): TagSet + { + $name = (array) $name; + $key = implode('-', $name); + + if (!isset($this->tag[$key])) { + $this->tag[$key] = new TagSet($name, $this); + } + + return $this->tag[$key]; + } + + /** + * 获取标签包含的缓存标识 + * @access public + * @param string $tag 标签标识 + * @return array + */ + public function getTagItems(string $tag): array + { + $name = $this->getTagKey($tag); + return $this->get($name, []); + } + + /** + * 获取实际标签名 + * @access public + * @param string $tag 标签名 + * @return string + */ + public function getTagKey(string $tag): string + { + return $this->options['tag_prefix'] . md5($tag); + } + + /** + * 序列化数据 + * @access protected + * @param mixed $data 缓存数据 + * @return string + */ + protected function serialize($data): string + { + if (is_numeric($data)) { + return (string) $data; + } + + $serialize = $this->options['serialize'][0] ?? "serialize"; + + return $serialize($data); + } + + /** + * 反序列化数据 + * @access protected + * @param string $data 缓存数据 + * @return mixed + */ + protected function unserialize($data) + { + if (is_numeric($data)) { + return $data; + } + + $unserialize = $this->options['serialize'][1] ?? "unserialize"; + + return $unserialize($data); + } + + /** + * 返回句柄对象,可执行其它高级方法 + * + * @access public + * @return object + */ + public function handler() + { + return $this->handler; + } + + /** + * 返回缓存读取次数 + * @access public + * @return int + */ + public function getReadTimes(): int + { + return $this->readTimes; + } + + /** + * 返回缓存写入次数 + * @access public + * @return int + */ + public function getWriteTimes(): int + { + return $this->writeTimes; + } + + /** + * 读取缓存 + * @access public + * @param iterable $keys 缓存变量名 + * @param mixed $default 默认值 + * @return iterable + * @throws InvalidArgumentException + */ + public function getMultiple($keys, $default = null): iterable + { + $result = []; + + foreach ($keys as $key) { + $result[$key] = $this->get($key, $default); + } + + return $result; + } + + /** + * 写入缓存 + * @access public + * @param iterable $values 缓存数据 + * @param null|int|\DateInterval $ttl 有效时间 0为永久 + * @return bool + */ + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $val) { + $result = $this->set($key, $val, $ttl); + + if (false === $result) { + return false; + } + } + + return true; + } + + /** + * 删除缓存 + * @access public + * @param iterable $keys 缓存变量名 + * @return bool + * @throws InvalidArgumentException + */ + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $result = $this->delete($key); + + if (false === $result) { + return false; + } + } + + return true; + } + + public function __call($method, $args) + { + return call_user_func_array([$this->handler, $method], $args); + } +} diff --git a/vendor/topthink/framework/src/think/cache/driver/File.php b/vendor/topthink/framework/src/think/cache/driver/File.php new file mode 100644 index 0000000..b36b069 --- /dev/null +++ b/vendor/topthink/framework/src/think/cache/driver/File.php @@ -0,0 +1,304 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\cache\driver; + +use FilesystemIterator; +use think\App; +use think\cache\Driver; + +/** + * 文件缓存类 + */ +class File extends Driver +{ + /** + * 配置参数 + * @var array + */ + protected $options = [ + 'expire' => 0, + 'cache_subdir' => true, + 'prefix' => '', + 'path' => '', + 'hash_type' => 'md5', + 'data_compress' => false, + 'tag_prefix' => 'tag:', + 'serialize' => [], + ]; + + /** + * 架构函数 + * @param App $app + * @param array $options 参数 + */ + public function __construct(App $app, array $options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + if (empty($this->options['path'])) { + $this->options['path'] = $app->getRuntimePath() . 'cache'; + } + + if (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { + $this->options['path'] .= DIRECTORY_SEPARATOR; + } + } + + /** + * 取得变量的存储文件名 + * @access public + * @param string $name 缓存变量名 + * @return string + */ + public function getCacheKey(string $name): string + { + $name = hash($this->options['hash_type'], $name); + + if ($this->options['cache_subdir']) { + // 使用子目录 + $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2); + } + + if ($this->options['prefix']) { + $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name; + } + + return $this->options['path'] . $name . '.php'; + } + + /** + * 获取缓存数据 + * @param string $name 缓存标识名 + * @return array|null + */ + protected function getRaw(string $name) + { + $filename = $this->getCacheKey($name); + + if (!is_file($filename)) { + return; + } + + $content = @file_get_contents($filename); + + if (false !== $content) { + $expire = (int) substr($content, 8, 12); + if (0 != $expire && time() - $expire > filemtime($filename)) { + //缓存过期删除缓存文件 + $this->unlink($filename); + return; + } + + $content = substr($content, 32); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + + return is_string($content) ? ['content' => $content, 'expire' => $expire] : null; + } + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name): bool + { + return $this->getRaw($name) !== null; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = null) + { + $this->readTimes++; + + $raw = $this->getRaw($name); + + return is_null($raw) ? $default : $this->unserialize($raw['content']); + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间 0为永久 + * @return bool + */ + public function set($name, $value, $expire = null): bool + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $expire = $this->getExpireTime($expire); + $filename = $this->getCacheKey($name); + + $dir = dirname($filename); + + if (!is_dir($dir)) { + try { + mkdir($dir, 0755, true); + } catch (\Exception $e) { + // 创建失败 + } + } + + $data = $this->serialize($value); + + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + + $data = "\n" . $data; + $result = file_put_contents($filename, $data); + + if ($result) { + clearstatcache(); + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc(string $name, int $step = 1) + { + if ($raw = $this->getRaw($name)) { + $value = $this->unserialize($raw['content']) + $step; + $expire = $raw['expire']; + } else { + $value = $step; + $expire = 0; + } + + return $this->set($name, $value, $expire) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec(string $name, int $step = 1) + { + return $this->inc($name, -$step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function delete($name): bool + { + $this->writeTimes++; + + return $this->unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @return bool + */ + public function clear(): bool + { + $this->writeTimes++; + + $dirname = $this->options['path'] . $this->options['prefix']; + + $this->rmdir($dirname); + + return true; + } + + /** + * 删除缓存标签 + * @access public + * @param array $keys 缓存标识列表 + * @return void + */ + public function clearTag(array $keys): void + { + foreach ($keys as $key) { + $this->unlink($key); + } + } + + /** + * 判断文件是否存在后,删除 + * @access private + * @param string $path + * @return bool + */ + private function unlink(string $path): bool + { + try { + return is_file($path) && unlink($path); + } catch (\Exception $e) { + return false; + } + } + + /** + * 删除文件夹 + * @param $dirname + * @return bool + */ + private function rmdir($dirname) + { + if (!is_dir($dirname)) { + return false; + } + + $items = new FilesystemIterator($dirname); + + foreach ($items as $item) { + if ($item->isDir() && !$item->isLink()) { + $this->rmdir($item->getPathname()); + } else { + $this->unlink($item->getPathname()); + } + } + + @rmdir($dirname); + + return true; + } + +} diff --git a/vendor/topthink/framework/src/think/cache/driver/Memcache.php b/vendor/topthink/framework/src/think/cache/driver/Memcache.php new file mode 100644 index 0000000..2fbbb9c --- /dev/null +++ b/vendor/topthink/framework/src/think/cache/driver/Memcache.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Memcache缓存类 + */ +class Memcache extends Driver +{ + /** + * 配置参数 + * @var array + */ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'persistent' => true, + 'prefix' => '', + 'tag_prefix' => 'tag:', + 'serialize' => [], + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + */ + public function __construct(array $options = []) + { + if (!extension_loaded('memcache')) { + throw new \BadFunctionCallException('not support: memcache'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcache; + + // 支持集群 + $hosts = (array) $this->options['host']; + $ports = (array) $this->options['port']; + + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + foreach ($hosts as $i => $host) { + $port = $ports[$i] ?? $ports[0]; + $this->options['timeout'] > 0 ? + $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1, (int) $this->options['timeout']) : + $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name): bool + { + $key = $this->getCacheKey($name); + + return false !== $this->handler->get($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = null) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int|\DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null): bool + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, 0, $expire)) { + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc(string $name, int $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec(string $name, int $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function delete($name, $ttl = false): bool + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @return bool + */ + public function clear(): bool + { + $this->writeTimes++; + + return $this->handler->flush(); + } + + /** + * 删除缓存标签 + * @access public + * @param array $keys 缓存标识列表 + * @return void + */ + public function clearTag(array $keys): void + { + foreach ($keys as $key) { + $this->handler->delete($key); + } + } + +} diff --git a/vendor/topthink/framework/src/think/cache/driver/Memcached.php b/vendor/topthink/framework/src/think/cache/driver/Memcached.php new file mode 100644 index 0000000..71edb05 --- /dev/null +++ b/vendor/topthink/framework/src/think/cache/driver/Memcached.php @@ -0,0 +1,221 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Memcached缓存类 + */ +class Memcached extends Driver +{ + /** + * 配置参数 + * @var array + */ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 + 'option' => [], + 'tag_prefix' => 'tag:', + 'serialize' => [], + ]; + + /** + * 架构函数 + * @access public + * @param array $options 缓存参数 + */ + public function __construct(array $options = []) + { + if (!extension_loaded('memcached')) { + throw new \BadFunctionCallException('not support: memcached'); + } + + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + + $this->handler = new \Memcached; + + if (!empty($this->options['option'])) { + $this->handler->setOptions($this->options['option']); + } + + // 设置连接超时时间(单位:毫秒) + if ($this->options['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']); + } + + // 支持集群 + $hosts = (array) $this->options['host']; + $ports = (array) $this->options['port']; + if (empty($ports[0])) { + $ports[0] = 11211; + } + + // 建立连接 + $servers = []; + foreach ($hosts as $i => $host) { + $servers[] = [$host, $ports[$i] ?? $ports[0], 1]; + } + + $this->handler->addServers($servers); + + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name): bool + { + $key = $this->getCacheKey($name); + + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = null) + { + $this->readTimes++; + + $result = $this->handler->get($this->getCacheKey($name)); + + return false !== $result ? $this->unserialize($result) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null): bool + { + $this->writeTimes++; + + if (is_null($expire)) { + $expire = $this->options['expire']; + } + + $key = $this->getCacheKey($name); + $expire = $this->getExpireTime($expire); + $value = $this->serialize($value); + + if ($this->handler->set($key, $value, $expire)) { + return true; + } + + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc(string $name, int $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + + return $this->handler->set($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec(string $name, int $step = 1) + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + + return !$res ? false : $value; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function delete($name, $ttl = false): bool + { + $this->writeTimes++; + + $key = $this->getCacheKey($name); + + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @return bool + */ + public function clear(): bool + { + $this->writeTimes++; + + return $this->handler->flush(); + } + + /** + * 删除缓存标签 + * @access public + * @param array $keys 缓存标识列表 + * @return void + */ + public function clearTag(array $keys): void + { + $this->handler->deleteMulti($keys); + } + +} diff --git a/vendor/topthink/framework/src/think/console/Input.php b/vendor/topthink/framework/src/think/console/Input.php new file mode 100644 index 0000000..9ae9077 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/Input.php @@ -0,0 +1,465 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition): void + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse(): void + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption(string $token): void + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet(string $name): void + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption(string $token): void + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument(string $token): void + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption(string $shortcut, $value): void + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption(string $name, $value): void + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values): bool + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive(): bool + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive(bool $interactive): void + { + $this->interactive = $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments(): array + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument(string $name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return $this->arguments[$name] ?? $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument(string $name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name): bool + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions(): array + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption(string $name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return $this->options[$name] ?? $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption(string $name, $value): void + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption(string $name): bool + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken(string $token): string + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/vendor/topthink/framework/src/think/console/LICENSE b/vendor/topthink/framework/src/think/console/LICENSE new file mode 100644 index 0000000..0abe056 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/topthink/framework/src/think/console/Output.php b/vendor/topthink/framework/src/think/console/Output.php new file mode 100644 index 0000000..294c4b8 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/Output.php @@ -0,0 +1,231 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; +use Throwable; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + // 不显示信息(静默) + const VERBOSITY_QUIET = 0; + // 正常信息 + const VERBOSITY_NORMAL = 1; + // 详细信息 + const VERBOSITY_VERBOSE = 2; + // 非常详细的信息 + const VERBOSITY_VERY_VERBOSE = 3; + // 调试信息 + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + // 输出信息级别 + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning', + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block(string $style, string $message): void + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine(int $count = 1): void + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln(string $messages, int $type = 0): void + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write(string $messages, bool $newline = false, int $type = 0): void + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(Throwable $e): void + { + $this->handle->renderException($e); + } + + /** + * 设置输出信息级别 + * @param int $level 输出信息级别 + */ + public function setVerbosity(int $level) + { + $this->verbosity = $level; + } + + /** + * 获取输出信息级别 + * @return int + */ + public function getVerbosity(): int + { + return $this->verbosity; + } + + public function isQuiet(): bool + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose(): bool + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose(): bool + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug(): bool + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []): void + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/vendor/topthink/framework/src/think/console/bin/hiddeninput.exe b/vendor/topthink/framework/src/think/console/bin/hiddeninput.exe new file mode 100644 index 0000000..c8cf65e Binary files /dev/null and b/vendor/topthink/framework/src/think/console/bin/hiddeninput.exe differ diff --git a/vendor/topthink/framework/src/think/console/command/Help.php b/vendor/topthink/framework/src/think/console/command/Help.php new file mode 100644 index 0000000..2e4f2ca --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/Help.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Help extends Command +{ + + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp( + <<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command): void + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} diff --git a/vendor/topthink/framework/src/think/console/command/Lists.php b/vendor/topthink/framework/src/think/console/command/Lists.php new file mode 100644 index 0000000..d20fc75 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/Lists.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Lists extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp( + <<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition(): InputDefinition + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition(): InputDefinition + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + ]); + } +} diff --git a/vendor/topthink/framework/src/think/console/command/Make.php b/vendor/topthink/framework/src/think/console/command/Make.php new file mode 100644 index 0000000..a74e9e8 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/Make.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +abstract class Make extends Command +{ + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ':' . $classname . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(dirname($pathname), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ':' . $classname . ' created successfully.'); + } + + protected function buildClass(string $name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + $this->app->config->get('route.action_suffix'), + $namespace, + $this->app->getNamespace(), + ], $stub); + } + + protected function getPathName(string $name): string + { + $name = substr($name, 4); + + return $this->app->getBasePath() . ltrim(str_replace('\\', '/', $name), '/') . '.php'; + } + + protected function getClassName(string $name): string + { + if (strpos($name, '\\') !== false) { + return $name; + } + + if (strpos($name, '@')) { + [$app, $name] = explode('@', $name); + } else { + $app = ''; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($app) . '\\' . $name; + } + + protected function getNamespace(string $app): string + { + return 'app' . ($app ? '\\' . $app : ''); + } + +} diff --git a/vendor/topthink/framework/src/think/console/command/make/Event.php b/vendor/topthink/framework/src/think/console/command/make/Event.php new file mode 100644 index 0000000..6b16689 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/make/Event.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\make; + +use think\console\command\Make; + +class Event extends Make +{ + protected $type = "Event"; + + protected function configure() + { + parent::configure(); + $this->setName('make:event') + ->setDescription('Create a new event class'); + } + + protected function getStub(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'event.stub'; + } + + protected function getNamespace(string $app): string + { + return parent::getNamespace($app) . '\\event'; + } +} diff --git a/vendor/topthink/framework/src/think/console/command/make/Listener.php b/vendor/topthink/framework/src/think/console/command/make/Listener.php new file mode 100644 index 0000000..5c92673 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/make/Listener.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\make; + +use think\console\command\Make; + +class Listener extends Make +{ + protected $type = "Listener"; + + protected function configure() + { + parent::configure(); + $this->setName('make:listener') + ->setDescription('Create a new listener class'); + } + + protected function getStub(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'listener.stub'; + } + + protected function getNamespace(string $app): string + { + return parent::getNamespace($app) . '\\listener'; + } +} diff --git a/vendor/topthink/framework/src/think/console/command/make/Middleware.php b/vendor/topthink/framework/src/think/console/command/make/Middleware.php new file mode 100644 index 0000000..3b68b4a --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/make/Middleware.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Middleware extends Make +{ + protected $type = "Middleware"; + + protected function configure() + { + parent::configure(); + $this->setName('make:middleware') + ->setDescription('Create a new middleware class'); + } + + protected function getStub(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'middleware.stub'; + } + + protected function getNamespace(string $app): string + { + return parent::getNamespace($app) . '\\middleware'; + } +} diff --git a/vendor/topthink/framework/src/think/console/command/make/Model.php b/vendor/topthink/framework/src/think/console/command/make/Model.php new file mode 100644 index 0000000..cb7a23c --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'model.stub'; + } + + protected function getNamespace(string $app): string + { + return parent::getNamespace($app) . '\\model'; + } +} diff --git a/vendor/topthink/framework/src/think/console/command/make/stubs/event.stub b/vendor/topthink/framework/src/think/console/command/make/stubs/event.stub new file mode 100644 index 0000000..a173089 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/command/make/stubs/event.stub @@ -0,0 +1,8 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +/** + * 命令行选项 + * @package think\console\input + */ +class Option +{ + // 无需传值 + const VALUE_NONE = 1; + // 必须传值 + const VALUE_REQUIRED = 2; + // 可选传值 + const VALUE_OPTIONAL = 4; + // 传数组值 + const VALUE_IS_ARRAY = 8; + + /** + * 选项名 + * @var string + */ + private $name = ''; + + /** + * 选项短名称 + * @var string + */ + private $shortcut = ''; + + /** + * 选项类型 + * @var int + */ + private $mode; + + /** + * 选项默认值 + * @var mixed + */ + private $default; + + /** + * 选项描述 + * @var string + */ + private $description = ''; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = ''; + } + + if ('' !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut(): string + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue(): bool + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired(): bool + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional(): bool + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray(): bool + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option): bool + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/vendor/topthink/framework/src/think/console/output/Formatter.php b/vendor/topthink/framework/src/think/console/output/Formatter.php new file mode 100644 index 0000000..1b97ca3 --- /dev/null +++ b/vendor/topthink/framework/src/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = $matches[3][$i][0] ?? ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/vendor/topthink/framework/src/think/console/output/driver/Nothing.php b/vendor/topthink/framework/src/think/console/output/driver/Nothing.php new file mode 100644 index 0000000..a7cc49e --- /dev/null +++ b/vendor/topthink/framework/src/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, bool $newline = false, int $options = 0) + { + // do nothing + } + + public function renderException(\Throwable $e) + { + // do nothing + } +} diff --git a/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php b/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php new file mode 100644 index 0000000..896ac29 --- /dev/null +++ b/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php @@ -0,0 +1,28 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\contract; + +/** + * 日志驱动接口 + */ +interface LogHandlerInterface +{ + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log): bool; + +} diff --git a/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php b/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php new file mode 100644 index 0000000..1f6f994 --- /dev/null +++ b/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\contract; + +use Closure; +use think\Collection; +use think\db\Query; +use think\Model; + +/** + * 模型关联接口 + */ +interface ModelRelationInterface +{ + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联 + * @param Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation(array $subRelation = [], Closure $closure = null): Collection; + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包条件 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null): void; + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包条件 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null): void; + + /** + * 关联统计 + * @access public + * @param Model $result 模型对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount(Model $result, Closure $closure, string $aggregate = 'count', string $field = '*', string &$name = null); + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string; + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER'): Query; + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = ''): Query; +} diff --git a/vendor/topthink/framework/src/think/event/HttpEnd.php b/vendor/topthink/framework/src/think/event/HttpEnd.php new file mode 100644 index 0000000..c40da57 --- /dev/null +++ b/vendor/topthink/framework/src/think/event/HttpEnd.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\event; + +/** + * HttpEnd事件类 + */ +class HttpEnd +{} diff --git a/vendor/topthink/framework/src/think/event/HttpRun.php b/vendor/topthink/framework/src/think/event/HttpRun.php new file mode 100644 index 0000000..ce67e93 --- /dev/null +++ b/vendor/topthink/framework/src/think/event/HttpRun.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\event; + +/** + * HttpRun事件类 + */ +class HttpRun +{} diff --git a/vendor/topthink/framework/src/think/event/LogRecord.php b/vendor/topthink/framework/src/think/event/LogRecord.php new file mode 100644 index 0000000..237468d --- /dev/null +++ b/vendor/topthink/framework/src/think/event/LogRecord.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- +namespace think\event; + +/** + * LogRecord事件类 + */ +class LogRecord +{ + /** @var string */ + public $type; + + /** @var string */ + public $message; + + public function __construct($type, $message) + { + $this->type = $type; + $this->message = $message; + } +} diff --git a/vendor/topthink/framework/src/think/event/LogWrite.php b/vendor/topthink/framework/src/think/event/LogWrite.php new file mode 100644 index 0000000..a787301 --- /dev/null +++ b/vendor/topthink/framework/src/think/event/LogWrite.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\event; + +/** + * LogWrite事件类 + */ +class LogWrite +{ + /** @var string */ + public $channel; + + /** @var array */ + public $log; + + public function __construct($channel, $log) + { + $this->channel = $channel; + $this->log = $log; + } +} diff --git a/vendor/topthink/framework/src/think/exception/ErrorException.php b/vendor/topthink/framework/src/think/exception/ErrorException.php new file mode 100644 index 0000000..d1a2378 --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/ErrorException.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\exception; + +use think\Exception; + +/** + * ThinkPHP错误异常 + * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 + * 除开从 think\Exception 继承的功能 + * 其他和PHP系统\ErrorException功能基本一样 + */ +class ErrorException extends Exception +{ + /** + * 用于保存错误级别 + * @var integer + */ + protected $severity; + + /** + * 错误异常构造函数 + * @access public + * @param integer $severity 错误级别 + * @param string $message 错误详细信息 + * @param string $file 出错文件路径 + * @param integer $line 出错行号 + */ + public function __construct(int $severity, string $message, string $file, int $line) + { + $this->severity = $severity; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->code = 0; + } + + /** + * 获取错误级别 + * @access public + * @return integer 错误级别 + */ + final public function getSeverity() + { + return $this->severity; + } +} diff --git a/vendor/topthink/framework/src/think/exception/FileException.php b/vendor/topthink/framework/src/think/exception/FileException.php new file mode 100644 index 0000000..228a189 --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/FileException.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\exception; + +class FileException extends \RuntimeException +{ +} diff --git a/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php b/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php new file mode 100644 index 0000000..ee2bcad --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php @@ -0,0 +1,30 @@ +message = $message; + $this->func = $func; + + parent::__construct($message, 0, $previous); + } + + /** + * 获取方法名 + * @access public + * @return string + */ + public function getFunc() + { + return $this->func; + } +} diff --git a/vendor/topthink/framework/src/think/exception/Handle.php b/vendor/topthink/framework/src/think/exception/Handle.php new file mode 100644 index 0000000..1f783bc --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/Handle.php @@ -0,0 +1,332 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\exception; + +use Exception; +use think\App; +use think\console\Output; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; +use think\Request; +use think\Response; +use Throwable; + +/** + * 系统异常处理类 + */ +class Handle +{ + /** @var App */ + protected $app; + + protected $ignoreReport = [ + HttpException::class, + HttpResponseException::class, + ModelNotFoundException::class, + DataNotFoundException::class, + ValidateException::class, + ]; + + protected $isJson = false; + + public function __construct(App $app) + { + $this->app = $app; + } + + /** + * Report or log an exception. + * + * @access public + * @param Throwable $exception + * @return void + */ + public function report(Throwable $exception): void + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if ($this->app->isDebug()) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + if ($this->app->config->get('log.record_trace')) { + $log .= PHP_EOL . $exception->getTraceAsString(); + } + + try { + $this->app->log->record($log, 'error'); + } catch (Exception $e) {} + } + } + + protected function isIgnoreReport(Throwable $exception): bool + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @access public + * @param Request $request + * @param Throwable $e + * @return Response + */ + public function render($request, Throwable $e): Response + { + $this->isJson = $request->isJson(); + if ($e instanceof HttpResponseException) { + return $e->getResponse(); + } elseif ($e instanceof HttpException) { + return $this->renderHttpException($e); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @access public + * @param Output $output + * @param Throwable $e + */ + public function renderForConsole(Output $output, Throwable $e): void + { + if ($this->app->isDebug()) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + + $output->renderException($e); + } + + /** + * @access protected + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e): Response + { + $status = $e->getStatusCode(); + $template = $this->app->config->get('app.http_exception_template'); + + if (!$this->app->isDebug() && !empty($template[$status])) { + return Response::create($template[$status], 'view', $status)->assign(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * 收集异常数据 + * @param Throwable $exception + * @return array + */ + protected function convertExceptionToArray(Throwable $exception): array + { + if ($this->app->isDebug()) { + // 调试模式,获取详细的错误信息 + $traces = []; + $nextException = $exception; + do { + $traces[] = [ + 'name' => get_class($nextException), + 'file' => $nextException->getFile(), + 'line' => $nextException->getLine(), + 'code' => $this->getCode($nextException), + 'message' => $this->getMessage($nextException), + 'trace' => $nextException->getTrace(), + 'source' => $this->getSourceCode($nextException), + ]; + } while ($nextException = $nextException->getPrevious()); + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + 'traces' => $traces, + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $this->app->request->get(), + 'POST Data' => $this->app->request->post(), + 'Files' => $this->app->request->file(), + 'Cookies' => $this->app->request->cookie(), + 'Session' => $this->app->exists('session') ? $this->app->session->all() : [], + 'Server/Request Data' => $this->app->request->server(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!$this->app->config->get('app.show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = $this->app->config->get('app.error_message'); + } + } + + return $data; + } + + /** + * @access protected + * @param Throwable $exception + * @return Response + */ + protected function convertExceptionToResponse(Throwable $exception): Response + { + if (!$this->isJson) { + $response = Response::create($this->renderExceptionContent($exception)); + } else { + $response = Response::create($this->convertExceptionToArray($exception), 'json'); + } + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + return $response->code($statusCode ?? 500); + } + + protected function renderExceptionContent(Throwable $exception): string + { + ob_start(); + $data = $this->convertExceptionToArray($exception); + extract($data); + include $this->app->config->get('app.exception_tmpl') ?: __DIR__ . '/../../tpl/think_exception.tpl'; + + return ob_get_clean(); + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param Throwable $exception + * @return integer 错误编码 + */ + protected function getCode(Throwable $exception) + { + $code = $exception->getCode(); + + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @access protected + * @param Throwable $exception + * @return string 错误信息 + */ + protected function getMessage(Throwable $exception): string + { + $message = $exception->getMessage(); + + if ($this->app->runningInConsole()) { + return $message; + } + + $lang = $this->app->lang; + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message; + } elseif (strpos($message, ',')) { + $name = strstr($message, ',', true); + $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message; + } elseif ($lang->has($message)) { + $message = $lang->get($message); + } + + return $message; + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @access protected + * @param Throwable $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Throwable $exception): array + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()) ?: []; + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @access protected + * @param Throwable $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Throwable $exception): array + { + $data = []; + + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + + return $data; + } + + /** + * 获取常量列表 + * @access protected + * @return array 常量列表 + */ + protected function getConst(): array + { + $const = get_defined_constants(true); + + return $const['user'] ?? []; + } +} diff --git a/vendor/topthink/framework/src/think/exception/HttpException.php b/vendor/topthink/framework/src/think/exception/HttpException.php new file mode 100644 index 0000000..45302e5 --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/HttpException.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\exception; + +use Exception; + +/** + * HTTP异常 + */ +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct(int $statusCode, string $message = '', Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} diff --git a/vendor/topthink/framework/src/think/exception/HttpResponseException.php b/vendor/topthink/framework/src/think/exception/HttpResponseException.php new file mode 100644 index 0000000..607813d --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/HttpResponseException.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\exception; + +use think\Response; + +/** + * HTTP响应异常 + */ +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + +} diff --git a/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php b/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php new file mode 100644 index 0000000..8ccd6f6 --- /dev/null +++ b/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); +namespace think\exception; + +use Psr\Cache\InvalidArgumentException as Psr6CacheInvalidArgumentInterface; +use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface; + +/** + * 非法数据异常 + */ +class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInvalidArgumentInterface, SimpleCacheInvalidArgumentInterface +{ +} diff --git a/vendor/topthink/framework/src/think/facade/Env.php b/vendor/topthink/framework/src/think/facade/Env.php new file mode 100644 index 0000000..bed2538 --- /dev/null +++ b/vendor/topthink/framework/src/think/facade/Env.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Env + * @package think\facade + * @mixin \think\Env + * @method static void load(string $file) 读取环境变量定义文件 + * @method static mixed get(string $name = null, mixed $default = null) 获取环境变量值 + * @method static void set(string|array $env, mixed $value = null) 设置环境变量值 + * @method static bool has(string $name) 检测是否存在环境变量 + * @method static void __set(string $name, mixed $value) 设置环境变量 + * @method static mixed __get(string $name) 获取环境变量 + * @method static bool __isset(string $name) 检测是否存在环境变量 + * @method static void offsetSet($name, $value) + * @method static bool offsetExists($name) + * @method static mixed offsetUnset($name) + * @method static mixed offsetGet($name) + */ +class Env extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'env'; + } +} diff --git a/vendor/topthink/framework/src/think/facade/Event.php b/vendor/topthink/framework/src/think/facade/Event.php new file mode 100644 index 0000000..c09d816 --- /dev/null +++ b/vendor/topthink/framework/src/think/facade/Event.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Event + * @package think\facade + * @mixin \think\Event + * @method static \think\Event listenEvents(array $events) 批量注册事件监听 + * @method static \think\Event listen(string $event, mixed $listener, bool $first = false) 注册事件监听 + * @method static bool hasListener(string $event) 是否存在事件监听 + * @method static void remove(string $event) 移除事件监听 + * @method static \think\Event bind(array $events) 指定事件别名标识 便于调用 + * @method static \think\Event subscribe(mixed $subscriber) 注册事件订阅者 + * @method static \think\Event observe(string|object $observer, null|string $prefix = '') 自动注册事件观察者 + * @method static mixed trigger(string|object $event, mixed $params = null, bool $once = false) 触发事件 + * @method static mixed until($event, $params = null) 触发事件(只获取一个有效返回值) + */ +class Event extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'event'; + } +} diff --git a/vendor/topthink/framework/src/think/facade/Lang.php b/vendor/topthink/framework/src/think/facade/Lang.php new file mode 100644 index 0000000..b460fe2 --- /dev/null +++ b/vendor/topthink/framework/src/think/facade/Lang.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Lang + * @package think\facade + * @mixin \think\Lang + * @method static void setLangSet(string $lang) 设置当前语言 + * @method static string getLangSet() 获取当前语言 + * @method static string defaultLangSet() 获取默认语言 + * @method static array load(string|array $file, string $range = '') 加载语言定义(不区分大小写) + * @method static bool has(string|null $name, string $range = '') 判断是否存在语言定义(不区分大小写) + * @method static mixed get(string|null $name = null, array $vars = [], string $range = '') 获取语言定义(不区分大小写) + * @method static string detect(\think\Request $request) 自动侦测设置获取语言选择 + * @method static void saveToCookie(\think\Cookie $cookie) 保存当前语言到Cookie + */ +class Lang extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'lang'; + } +} diff --git a/vendor/topthink/framework/src/think/facade/Log.php b/vendor/topthink/framework/src/think/facade/Log.php new file mode 100644 index 0000000..7c43d37 --- /dev/null +++ b/vendor/topthink/framework/src/think/facade/Log.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; +use think\log\Channel; +use think\log\ChannelSet; + +/** + * @see \think\Log + * @package think\facade + * @mixin \think\Log + * @method static string|null getDefaultDriver() 默认驱动 + * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取日志配置 + * @method static array getChannelConfig(string $channel, null $name = null, null $default = null) 获取渠道配置 + * @method static Channel|ChannelSet channel(string|array $name = null) driver() 的别名 + * @method static mixed createDriver(string $name) + * @method static \think\Log clear(string|array $channel = '*') 清空日志信息 + * @method static \think\Log close(string|array $channel = '*') 关闭本次请求日志写入 + * @method static array getLog(string $channel = null) 获取日志信息 + * @method static bool save() 保存日志信息 + * @method static \think\Log record(mixed $msg, string $type = 'info', array $context = [], bool $lazy = true) 记录日志信息 + * @method static \think\Log write(mixed $msg, string $type = 'info', array $context = []) 实时写入日志信息 + * @method static Event listen($listener) 注册日志写入事件监听 + * @method static void log(string $level, mixed $message, array $context = []) 记录日志信息 + * @method static void emergency(mixed $message, array $context = []) 记录emergency信息 + * @method static void alert(mixed $message, array $context = []) 记录警报信息 + * @method static void critical(mixed $message, array $context = []) 记录紧急情况 + * @method static void error(mixed $message, array $context = []) 记录错误信息 + * @method static void warning(mixed $message, array $context = []) 记录warning信息 + * @method static void notice(mixed $message, array $context = []) 记录notice信息 + * @method static void info(mixed $message, array $context = []) 记录一般信息 + * @method static void debug(mixed $message, array $context = []) 记录调试信息 + * @method static void sql(mixed $message, array $context = []) 记录sql信息 + * @method static mixed __call($method, $parameters) + */ +class Log extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'log'; + } +} diff --git a/vendor/topthink/framework/src/think/facade/Middleware.php b/vendor/topthink/framework/src/think/facade/Middleware.php new file mode 100644 index 0000000..4203f82 --- /dev/null +++ b/vendor/topthink/framework/src/think/facade/Middleware.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; + +/** + * @see \think\Middleware + * @package think\facade + * @mixin \think\Middleware + * @method static void import(array $middlewares = [], string $type = 'global') 导入中间件 + * @method static void add(mixed $middleware, string $type = 'global') 注册中间件 + * @method static void route(mixed $middleware) 注册路由中间件 + * @method static void controller(mixed $middleware) 注册控制器中间件 + * @method static mixed unshift(mixed $middleware, string $type = 'global') 注册中间件到开始位置 + * @method static array all(string $type = 'global') 获取注册的中间件 + * @method static Pipeline pipeline(string $type = 'global') 调度管道 + * @method static mixed end(\think\Response $response) 结束调度 + * @method static \think\Response handleException(\think\Request $passable, \Throwable $e) 异常处理 + */ +class Middleware extends Facade +{ + /** + * 获取当前Facade对应类名(或者已经绑定的容器对象标识) + * @access protected + * @return string + */ + protected static function getFacadeClass() + { + return 'middleware'; + } +} diff --git a/vendor/topthink/framework/src/think/initializer/Error.php b/vendor/topthink/framework/src/think/initializer/Error.php new file mode 100644 index 0000000..201d947 --- /dev/null +++ b/vendor/topthink/framework/src/think/initializer/Error.php @@ -0,0 +1,117 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\initializer; + +use think\App; +use think\console\Output as ConsoleOutput; +use think\exception\ErrorException; +use think\exception\Handle; +use Throwable; + +/** + * 错误和异常处理 + */ +class Error +{ + /** @var App */ + protected $app; + + /** + * 注册异常处理 + * @access public + * @param App $app + * @return void + */ + public function init(App $app) + { + $this->app = $app; + error_reporting(E_ALL); + set_error_handler([$this, 'appError']); + set_exception_handler([$this, 'appException']); + register_shutdown_function([$this, 'appShutdown']); + } + + /** + * Exception Handler + * @access public + * @param \Throwable $e + */ + public function appException(Throwable $e): void + { + $handler = $this->getExceptionHandler(); + + $handler->report($e); + + if ($this->app->runningInConsole()) { + $handler->renderForConsole(new ConsoleOutput, $e); + } else { + $handler->render($this->app->request, $e)->send(); + } + } + + /** + * Error Handler + * @access public + * @param integer $errno 错误编号 + * @param string $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @throws ErrorException + */ + public function appError(int $errno, string $errstr, string $errfile = '', int $errline = 0): void + { + $exception = new ErrorException($errno, $errstr, $errfile, $errline); + + if (error_reporting() & $errno) { + // 将错误信息托管至 think\exception\ErrorException + throw $exception; + } + } + + /** + * Shutdown Handler + * @access public + */ + public function appShutdown(): void + { + if (!is_null($error = error_get_last()) && $this->isFatal($error['type'])) { + // 将错误信息托管至think\ErrorException + $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']); + + $this->appException($exception); + } + } + + /** + * 确定错误类型是否致命 + * + * @access protected + * @param int $type + * @return bool + */ + protected function isFatal(int $type): bool + { + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); + } + + /** + * Get an instance of the exception handler. + * + * @access protected + * @return Handle + */ + protected function getExceptionHandler() + { + return $this->app->make(Handle::class); + } +} diff --git a/vendor/topthink/framework/src/think/log/driver/File.php b/vendor/topthink/framework/src/think/log/driver/File.php new file mode 100644 index 0000000..1f726b0 --- /dev/null +++ b/vendor/topthink/framework/src/think/log/driver/File.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\log\driver; + +use think\App; +use think\contract\LogHandlerInterface; + +/** + * 本地化调试输出到文件 + */ +class File implements LogHandlerInterface +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + 'time_format' => 'c', + 'single' => false, + 'file_size' => 2097152, + 'path' => '', + 'apart_level' => [], + 'max_files' => 0, + 'json' => false, + 'json_options' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, + 'format' => '[%s][%s] %s', + ]; + + // 实例化并传入参数 + public function __construct(App $app, $config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + + if (empty($this->config['format'])) { + $this->config['format'] = '[%s][%s] %s'; + } + + if (empty($this->config['path'])) { + $this->config['path'] = $app->getRuntimePath() . 'log'; + } + + if (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) { + $this->config['path'] .= DIRECTORY_SEPARATOR; + } + } + + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log): bool + { + $destination = $this->getMasterLogFile(); + + $path = dirname($destination); + !is_dir($path) && mkdir($path, 0755, true); + + $info = []; + + // 日志信息封装 + $time = \DateTime::createFromFormat('0.u00 U', microtime())->setTimezone(new \DateTimeZone(date_default_timezone_get()))->format($this->config['time_format']); + + foreach ($log as $type => $val) { + $message = []; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + + $message[] = $this->config['json'] ? + json_encode(['time' => $time, 'type' => $type, 'msg' => $msg], $this->config['json_options']) : + sprintf($this->config['format'], $time, $type, $msg); + } + + if (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level'])) { + // 独立记录的日志级别 + $filename = $this->getApartLevelFile($path, $type); + $this->write($message, $filename); + continue; + } + + $info[$type] = $message; + } + + if ($info) { + return $this->write($info, $destination); + } + + return true; + } + + /** + * 日志写入 + * @access protected + * @param array $message 日志信息 + * @param string $destination 日志文件 + * @return bool + */ + protected function write(array $message, string $destination): bool + { + // 检测日志文件大小,超过配置大小则备份日志文件重新生成 + $this->checkLogSize($destination); + + $info = []; + + foreach ($message as $type => $msg) { + $info[$type] = is_array($msg) ? implode(PHP_EOL, $msg) : $msg; + } + + $message = implode(PHP_EOL, $info) . PHP_EOL; + + return error_log($message, 3, $destination); + } + + /** + * 获取主日志文件名 + * @access public + * @return string + */ + protected function getMasterLogFile(): string + { + + if ($this->config['max_files']) { + $files = glob($this->config['path'] . '*.log'); + + try { + if (count($files) > $this->config['max_files']) { + set_error_handler(function ($errno, $errstr, $errfile, $errline) {}); + unlink($files[0]); + restore_error_handler(); + } + } catch (\Exception $e) { + // + } + } + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + $destination = $this->config['path'] . $name . '.log'; + } else { + + if ($this->config['max_files']) { + $filename = date('Ymd') . '.log'; + } else { + $filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . '.log'; + } + + $destination = $this->config['path'] . $filename; + } + + return $destination; + } + + /** + * 获取独立日志文件名 + * @access public + * @param string $path 日志目录 + * @param string $type 日志类型 + * @return string + */ + protected function getApartLevelFile(string $path, string $type): string + { + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + + $name .= '_' . $type; + } elseif ($this->config['max_files']) { + $name = date('Ymd') . '_' . $type; + } else { + $name = date('d') . '_' . $type; + } + + return $path . DIRECTORY_SEPARATOR . $name . '.log'; + } + + /** + * 检查日志文件大小并自动生成备份文件 + * @access protected + * @param string $destination 日志文件 + * @return void + */ + protected function checkLogSize(string $destination): void + { + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + try { + rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination)); + } catch (\Exception $e) { + // + } + } + } +} diff --git a/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php b/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php new file mode 100644 index 0000000..efbb77b --- /dev/null +++ b/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\middleware; + +use Closure; +use think\exception\ValidateException; +use think\Request; +use think\Response; + +/** + * 表单令牌支持 + */ +class FormTokenCheck +{ + + /** + * 表单令牌检测 + * @access public + * @param Request $request + * @param Closure $next + * @param string $token 表单令牌Token名称 + * @return Response + */ + public function handle(Request $request, Closure $next, string $token = null) + { + $check = $request->checkToken($token ?: '__token__'); + + if (false === $check) { + throw new ValidateException('invalid token'); + } + + return $next($request); + } + +} diff --git a/vendor/topthink/framework/src/think/middleware/LoadLangPack.php b/vendor/topthink/framework/src/think/middleware/LoadLangPack.php new file mode 100644 index 0000000..af0324b --- /dev/null +++ b/vendor/topthink/framework/src/think/middleware/LoadLangPack.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\middleware; + +use Closure; +use think\App; +use think\Config; +use think\Cookie; +use think\Lang; +use think\Request; +use think\Response; + +/** + * 多语言加载 + */ +class LoadLangPack +{ + protected $app; + protected $lang; + protected $config; + + public function __construct(App $app, Lang $lang, Config $config) + { + $this->app = $app; + $this->lang = $lang; + $this->config = $lang->getConfig(); + } + + /** + * 路由初始化(路由规则注册) + * @access public + * @param Request $request + * @param Closure $next + * @return Response + */ + public function handle($request, Closure $next) + { + // 自动侦测当前语言 + $langset = $this->detect($request); + + if ($this->lang->defaultLangSet() != $langset) { + $this->lang->switchLangSet($langset); + } + + $this->saveToCookie($this->app->cookie, $langset); + + return $next($request); + } + + /** + * 自动侦测设置获取语言选择 + * @access protected + * @param Request $request + * @return string + */ + protected function detect(Request $request): string + { + // 自动侦测设置获取语言选择 + $langSet = ''; + + if ($request->get($this->config['detect_var'])) { + // url中设置了语言变量 + $langSet = $request->get($this->config['detect_var']); + } elseif ($request->header($this->config['header_var'])) { + // Header中设置了语言变量 + $langSet = $request->header($this->config['header_var']); + } elseif ($request->cookie($this->config['cookie_var'])) { + // Cookie中设置了语言变量 + $langSet = $request->cookie($this->config['cookie_var']); + } elseif ($request->server('HTTP_ACCEPT_LANGUAGE')) { + // 自动侦测浏览器语言 + $langSet = $request->server('HTTP_ACCEPT_LANGUAGE'); + } + + if (preg_match('/^([a-z\d\-]+)/i', $langSet, $matches)) { + $langSet = strtolower($matches[1]); + if (isset($this->config['accept_language'][$langSet])) { + $langSet = $this->config['accept_language'][$langSet]; + } + } else { + $langSet = $this->lang->getLangSet(); + } + + if (empty($this->config['allow_lang_list']) || in_array($langSet, $this->config['allow_lang_list'])) { + // 合法的语言 + $this->lang->setLangSet($langSet); + } else { + $langSet = $this->lang->getLangSet(); + } + + return $langSet; + } + + /** + * 保存当前语言到Cookie + * @access protected + * @param Cookie $cookie Cookie对象 + * @param string $langSet 语言 + * @return void + */ + protected function saveToCookie(Cookie $cookie, string $langSet) + { + if ($this->config['use_cookie']) { + $cookie->set($this->config['cookie_var'], $langSet); + } + } + +} diff --git a/vendor/topthink/framework/src/think/response/File.php b/vendor/topthink/framework/src/think/response/File.php new file mode 100644 index 0000000..1e45f2f --- /dev/null +++ b/vendor/topthink/framework/src/think/response/File.php @@ -0,0 +1,160 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\response; + +use think\Exception; +use think\Response; + +/** + * File Response + */ +class File extends Response +{ + protected $expire = 360; + protected $name; + protected $mimeType; + protected $isContent = false; + protected $force = true; + + public function __construct($data = '', int $code = 200) + { + $this->init($data, $code); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + if (!$this->isContent && !is_file($data)) { + throw new Exception('file not exists:' . $data); + } + + while (ob_get_level() > 0) { + ob_end_clean(); + } + + if (!empty($this->name)) { + $name = $this->name; + } else { + $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : ''; + } + + if ($this->isContent) { + $mimeType = $this->mimeType; + $size = strlen($data); + } else { + $mimeType = $this->getMimeType($data); + $size = filesize($data); + } + + $this->header['Pragma'] = 'public'; + $this->header['Content-Type'] = $mimeType ?: 'application/octet-stream'; + $this->header['Cache-control'] = 'max-age=' . $this->expire; + $this->header['Content-Disposition'] = ($this->force ? 'attachment; ' : '') . 'filename="' . $name . '"'; + $this->header['Content-Length'] = $size; + $this->header['Content-Transfer-Encoding'] = 'binary'; + $this->header['Expires'] = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT'; + + $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT'); + + return $this->isContent ? $data : file_get_contents($data); + } + + /** + * 设置是否为内容 必须配合mimeType方法使用 + * @access public + * @param bool $content + * @return $this + */ + public function isContent(bool $content = true) + { + $this->isContent = $content; + return $this; + } + + /** + * 设置有效期 + * @access public + * @param integer $expire 有效期 + * @return $this + */ + public function expire(int $expire) + { + $this->expire = $expire; + return $this; + } + + /** + * 设置文件类型 + * @access public + * @param string $filename 文件名 + * @return $this + */ + public function mimeType(string $mimeType) + { + $this->mimeType = $mimeType; + return $this; + } + + /** + * 设置文件强制下载 + * @access public + * @param bool $force 强制浏览器下载 + * @return $this + */ + public function force(bool $force) + { + $this->force = $force; + return $this; + } + + /** + * 获取文件类型信息 + * @access public + * @param string $filename 文件名 + * @return string + */ + protected function getMimeType(string $filename): string + { + if (!empty($this->mimeType)) { + return $this->mimeType; + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + + return finfo_file($finfo, $filename); + } + + /** + * 设置下载文件的显示名称 + * @access public + * @param string $filename 文件名 + * @param bool $extension 后缀自动识别 + * @return $this + */ + public function name(string $filename, bool $extension = true) + { + $this->name = $filename; + + if ($extension && false === strpos($filename, '.')) { + $this->name .= '.' . pathinfo($this->data, PATHINFO_EXTENSION); + } + + return $this; + } +} diff --git a/vendor/topthink/framework/src/think/response/Html.php b/vendor/topthink/framework/src/think/response/Html.php new file mode 100644 index 0000000..c158f78 --- /dev/null +++ b/vendor/topthink/framework/src/think/response/Html.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\response; + +use think\Cookie; +use think\Response; + +/** + * Html Response + */ +class Html extends Response +{ + /** + * 输出type + * @var string + */ + protected $contentType = 'text/html'; + + public function __construct(Cookie $cookie, $data = '', int $code = 200) + { + $this->init($data, $code); + $this->cookie = $cookie; + } +} diff --git a/vendor/topthink/framework/src/think/response/Json.php b/vendor/topthink/framework/src/think/response/Json.php new file mode 100644 index 0000000..a84501f --- /dev/null +++ b/vendor/topthink/framework/src/think/response/Json.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\response; + +use think\Cookie; +use think\Response; + +/** + * Json Response + */ +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + public function __construct(Cookie $cookie, $data = '', int $code = 200) + { + $this->init($data, $code); + $this->cookie = $cookie; + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return string + * @throws \Exception + */ + protected function output($data): string + { + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/vendor/topthink/framework/src/think/response/Jsonp.php b/vendor/topthink/framework/src/think/response/Jsonp.php new file mode 100644 index 0000000..81d3a06 --- /dev/null +++ b/vendor/topthink/framework/src/think/response/Jsonp.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\response; + +use think\Cookie; +use think\Request; +use think\Response; + +/** + * Jsonp Response + */ +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + protected $request; + + public function __construct(Cookie $cookie, Request $request, $data = '', int $code = 200) + { + $this->init($data, $code); + + $this->cookie = $cookie; + $this->request = $request; + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return string + * @throws \Exception + */ + protected function output($data): string + { + try { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $varJsonpHandler = $this->request->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($varJsonpHandler) ? $varJsonpHandler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if (false === $data) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/vendor/topthink/framework/src/think/route/Domain.php b/vendor/topthink/framework/src/think/route/Domain.php new file mode 100644 index 0000000..84f1d46 --- /dev/null +++ b/vendor/topthink/framework/src/think/route/Domain.php @@ -0,0 +1,183 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\route; + +use think\helper\Str; +use think\Request; +use think\Route; +use think\route\dispatch\Callback as CallbackDispatch; +use think\route\dispatch\Controller as ControllerDispatch; + +/** + * 域名路由 + */ +class Domain extends RuleGroup +{ + /** + * 架构函数 + * @access public + * @param Route $router 路由对象 + * @param string $name 路由域名 + * @param mixed $rule 域名路由 + */ + public function __construct(Route $router, string $name = null, $rule = null) + { + $this->router = $router; + $this->domain = $name; + $this->rule = $rule; + } + + /** + * 检测域名路由 + * @access public + * @param Request $request 请求对象 + * @param string $url 访问地址 + * @param bool $completeMatch 路由是否完全匹配 + * @return Dispatch|false + */ + public function check(Request $request, string $url, bool $completeMatch = false) + { + // 检测URL绑定 + $result = $this->checkUrlBind($request, $url); + + if (!empty($this->option['append'])) { + $request->setRoute($this->option['append']); + unset($this->option['append']); + } + + if (false !== $result) { + return $result; + } + + return parent::check($request, $url, $completeMatch); + } + + /** + * 设置路由绑定 + * @access public + * @param string $bind 绑定信息 + * @return $this + */ + public function bind(string $bind) + { + $this->router->bind($bind, $this->domain); + + return $this; + } + + /** + * 检测URL绑定 + * @access private + * @param Request $request + * @param string $url URL地址 + * @return Dispatch|false + */ + private function checkUrlBind(Request $request, string $url) + { + $bind = $this->router->getDomainBind($this->domain); + + if ($bind) { + $this->parseBindAppendParam($bind); + + // 如果有URL绑定 则进行绑定检测 + $type = substr($bind, 0, 1); + $bind = substr($bind, 1); + + $bindTo = [ + '\\' => 'bindToClass', + '@' => 'bindToController', + ':' => 'bindToNamespace', + ]; + + if (isset($bindTo[$type])) { + return $this->{$bindTo[$type]}($request, $url, $bind); + } + } + + return false; + } + + protected function parseBindAppendParam(string &$bind): void + { + if (false !== strpos($bind, '?')) { + [$bind, $query] = explode('?', $bind); + parse_str($query, $vars); + $this->append($vars); + } + } + + /** + * 绑定到类 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @return CallbackDispatch + */ + protected function bindToClass(Request $request, string $url, string $class): CallbackDispatch + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($array[1], $param); + } + + return new CallbackDispatch($request, $this, [$class, $action], $param); + } + + /** + * 绑定到命名空间 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @return CallbackDispatch + */ + protected function bindToNamespace(Request $request, string $url, string $namespace): CallbackDispatch + { + $array = explode('|', $url, 3); + $class = !empty($array[0]) ? $array[0] : $this->router->config('default_controller'); + $method = !empty($array[1]) ? $array[1] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[2])) { + $this->parseUrlParams($array[2], $param); + } + + return new CallbackDispatch($request, $this, [$namespace . '\\' . Str::studly($class), $method], $param); + } + + /** + * 绑定到控制器 + * @access protected + * @param Request $request + * @param string $url URL地址 + * @param string $controller 控制器名 + * @return ControllerDispatch + */ + protected function bindToController(Request $request, string $url, string $controller): ControllerDispatch + { + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action'); + $param = []; + + if (!empty($array[1])) { + $this->parseUrlParams($array[1], $param); + } + + return new ControllerDispatch($request, $this, $controller . '/' . $action, $param); + } + +} diff --git a/vendor/topthink/framework/src/think/service/ModelService.php b/vendor/topthink/framework/src/think/service/ModelService.php new file mode 100644 index 0000000..b517c4e --- /dev/null +++ b/vendor/topthink/framework/src/think/service/ModelService.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\service; + +use think\Model; +use think\Service; + +/** + * 模型服务类 + */ +class ModelService extends Service +{ + public function boot() + { + Model::setDb($this->app->db); + Model::setEvent($this->app->event); + Model::setInvoker([$this->app, 'invoke']); + Model::maker(function (Model $model) { + $config = $this->app->config; + + $isAutoWriteTimestamp = $model->getAutoWriteTimestamp(); + + if (is_null($isAutoWriteTimestamp)) { + // 自动写入时间戳 + $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp')); + } + + $dateFormat = $model->getDateFormat(); + + if (is_null($dateFormat)) { + // 设置时间戳格式 + $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s')); + } + + $timeField = $config->get('database.datetime_field'); + if (!empty($timeField)) { + [$createTime, $updateTime] = explode(',', $timeField); + $model->setTimeField($createTime, $updateTime); + } + + }); + } +} diff --git a/vendor/topthink/framework/src/think/service/PaginatorService.php b/vendor/topthink/framework/src/think/service/PaginatorService.php new file mode 100644 index 0000000..a01977d --- /dev/null +++ b/vendor/topthink/framework/src/think/service/PaginatorService.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\service; + +use think\Paginator; +use think\paginator\driver\Bootstrap; +use think\Service; + +/** + * 分页服务类 + */ +class PaginatorService extends Service +{ + public function register() + { + if (!$this->app->bound(Paginator::class)) { + $this->app->bind(Paginator::class, Bootstrap::class); + } + } + + public function boot() + { + Paginator::maker(function (...$args) { + return $this->app->make(Paginator::class, $args, true); + }); + + Paginator::currentPathResolver(function () { + return $this->app->request->baseUrl(); + }); + + Paginator::currentPageResolver(function ($varPage = 'page') { + + $page = $this->app->request->param($varPage); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + return (int) $page; + } + + return 1; + }); + } +} diff --git a/vendor/topthink/framework/src/think/session/driver/File.php b/vendor/topthink/framework/src/think/session/driver/File.php new file mode 100644 index 0000000..788f323 --- /dev/null +++ b/vendor/topthink/framework/src/think/session/driver/File.php @@ -0,0 +1,249 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\session\driver; + +use Closure; +use Exception; +use FilesystemIterator; +use Generator; +use SplFileInfo; +use think\App; +use think\contract\SessionHandlerInterface; + +/** + * Session 文件驱动 + */ +class File implements SessionHandlerInterface +{ + protected $config = [ + 'path' => '', + 'expire' => 1440, + 'prefix' => '', + 'data_compress' => false, + 'gc_probability' => 1, + 'gc_divisor' => 100, + ]; + + public function __construct(App $app, array $config = []) + { + $this->config = array_merge($this->config, $config); + + if (empty($this->config['path'])) { + $this->config['path'] = $app->getRuntimePath() . 'session' . DIRECTORY_SEPARATOR; + } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) { + $this->config['path'] .= DIRECTORY_SEPARATOR; + } + + $this->init(); + } + + /** + * 打开Session + * @access protected + * @throws Exception + */ + protected function init(): void + { + try { + !is_dir($this->config['path']) && mkdir($this->config['path'], 0755, true); + } catch (\Exception $e) { + // 写入失败 + } + + // 垃圾回收 + if (random_int(1, $this->config['gc_divisor']) <= $this->config['gc_probability']) { + $this->gc(); + } + } + + /** + * Session 垃圾回收 + * @access public + * @return void + */ + public function gc(): void + { + $lifetime = $this->config['expire']; + $now = time(); + + $files = $this->findFiles($this->config['path'], function (SplFileInfo $item) use ($lifetime, $now) { + return $now - $lifetime > $item->getMTime(); + }); + + foreach ($files as $file) { + $this->unlink($file->getPathname()); + } + } + + /** + * 查找文件 + * @param string $root + * @param Closure $filter + * @return Generator + */ + protected function findFiles(string $root, Closure $filter) + { + $items = new FilesystemIterator($root); + + /** @var SplFileInfo $item */ + foreach ($items as $item) { + if ($item->isDir() && !$item->isLink()) { + yield from $this->findFiles($item->getPathname(), $filter); + } else { + if ($filter($item)) { + yield $item; + } + } + } + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @param bool $auto 是否自动创建目录 + * @return string + */ + protected function getFileName(string $name, bool $auto = false): string + { + if ($this->config['prefix']) { + // 使用子目录 + $name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name; + } else { + $name = 'sess_' . $name; + } + + $filename = $this->config['path'] . $name; + $dir = dirname($filename); + + if ($auto && !is_dir($dir)) { + try { + mkdir($dir, 0755, true); + } catch (\Exception $e) { + // 创建失败 + } + } + + return $filename; + } + + /** + * 读取Session + * @access public + * @param string $sessID + * @return string + */ + public function read(string $sessID): string + { + $filename = $this->getFileName($sessID); + + if (is_file($filename) && filemtime($filename) >= time() - $this->config['expire']) { + $content = $this->readFile($filename); + + if ($this->config['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = (string) gzuncompress($content); + } + + return $content; + } + + return ''; + } + + /** + * 写文件(加锁) + * @param $path + * @param $content + * @return bool + */ + protected function writeFile($path, $content): bool + { + return (bool) file_put_contents($path, $content, LOCK_EX); + } + + /** + * 读取文件内容(加锁) + * @param $path + * @return string + */ + protected function readFile($path): string + { + $contents = ''; + + $handle = fopen($path, 'rb'); + + if ($handle) { + try { + if (flock($handle, LOCK_SH)) { + clearstatcache(true, $path); + + $contents = fread($handle, filesize($path) ?: 1); + + flock($handle, LOCK_UN); + } + } finally { + fclose($handle); + } + } + + return $contents; + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param string $sessData + * @return bool + */ + public function write(string $sessID, string $sessData): bool + { + $filename = $this->getFileName($sessID, true); + $data = $sessData; + + if ($this->config['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + + return $this->writeFile($filename, $data); + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool + */ + public function delete(string $sessID): bool + { + try { + return $this->unlink($this->getFileName($sessID)); + } catch (\Exception $e) { + return false; + } + } + + /** + * 判断文件是否存在后,删除 + * @access private + * @param string $file + * @return bool + */ + private function unlink(string $file): bool + { + return is_file($file) && unlink($file); + } + +} diff --git a/vendor/topthink/framework/tests/DispatchTest.php b/vendor/topthink/framework/tests/DispatchTest.php new file mode 100644 index 0000000..2b9e413 --- /dev/null +++ b/vendor/topthink/framework/tests/DispatchTest.php @@ -0,0 +1,32 @@ + ['tp', 'thinkphp'], 'psr' => 'psr-7'], '123'); + } + }; + + $response = $dispatch->run(); + + $this->assertInstanceOf(\think\Response::class, $response); + $this->assertEquals('123', $response->getContent()); + $this->assertEquals('tp, thinkphp', $response->getHeader('framework')); + $this->assertEquals('psr-7', $response->getHeader('psr')); + } +} diff --git a/vendor/topthink/framework/tests/EnvTest.php b/vendor/topthink/framework/tests/EnvTest.php new file mode 100644 index 0000000..9b3b615 --- /dev/null +++ b/vendor/topthink/framework/tests/EnvTest.php @@ -0,0 +1,80 @@ +setContent("key1=value1\nkey2=value2"); + $root->addChild($envFile); + + $env = new Env(); + + $env->load($envFile->url()); + + $this->assertEquals('value1', $env->get('key1')); + $this->assertEquals('value2', $env->get('key2')); + } + + public function testServerEnv() + { + $env = new Env(); + + $this->assertEquals('value2', $env->get('key2', 'value2')); + + putenv('PHP_KEY7=value7'); + putenv('PHP_KEY8=false'); + putenv('PHP_KEY9=true'); + + $this->assertEquals('value7', $env->get('key7')); + $this->assertFalse($env->get('KEY8')); + $this->assertTrue($env->get('key9')); + } + + public function testSetEnv() + { + $env = new Env(); + + $env->set([ + 'key1' => 'value1', + 'key2' => [ + 'key1' => 'value1-2', + ], + ]); + + $env->set('key3', 'value3'); + + $env->key4 = 'value4'; + + $env['key5'] = 'value5'; + + $this->assertEquals('value1', $env->get('key1')); + $this->assertEquals('value1-2', $env->get('key2.key1')); + + $this->assertEquals('value3', $env->get('key3')); + + $this->assertEquals('value4', $env->key4); + + $this->assertEquals('value5', $env['key5']); + + $this->expectException(Exception::class); + + unset($env['key5']); + } + + public function testHasEnv() + { + $env = new Env(); + $env->set(['foo' => 'bar']); + $this->assertTrue($env->has('foo')); + $this->assertTrue(isset($env->foo)); + $this->assertTrue($env->offsetExists('foo')); + } +} diff --git a/vendor/topthink/framework/tests/EventTest.php b/vendor/topthink/framework/tests/EventTest.php new file mode 100644 index 0000000..ded5a36 --- /dev/null +++ b/vendor/topthink/framework/tests/EventTest.php @@ -0,0 +1,134 @@ +app = m::mock(App::class)->makePartial(); + + Container::setInstance($this->app); + $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app); + $this->config = m::mock(Config::class)->makePartial(); + $this->app->shouldReceive('get')->with('config')->andReturn($this->config); + + $this->event = new Event($this->app); + } + + public function testBasic() + { + $this->event->bind(['foo' => 'baz']); + + $this->event->listen('foo', function ($bar) { + $this->assertEquals('bar', $bar); + }); + + $this->assertTrue($this->event->hasListener('foo')); + + $this->event->trigger('baz', 'bar'); + + $this->event->remove('foo'); + + $this->assertFalse($this->event->hasListener('foo')); + } + + public function testOnceEvent() + { + $this->event->listen('AppInit', function ($bar) { + $this->assertEquals('bar', $bar); + return 'foo'; + }); + + $this->assertEquals('foo', $this->event->trigger('AppInit', 'bar', true)); + $this->assertEquals(['foo'], $this->event->trigger('AppInit', 'bar')); + } + + public function testClassListener() + { + $listener = m::mock("overload:SomeListener", TestListener::class); + + $listener->shouldReceive('handle')->andReturnTrue(); + + $this->event->listen('some', "SomeListener"); + + $this->assertTrue($this->event->until('some')); + } + + public function testSubscribe() + { + $listener = m::mock("overload:SomeListener", TestListener::class); + + $listener->shouldReceive('subscribe')->andReturnUsing(function (Event $event) use ($listener) { + + $listener->shouldReceive('onBar')->once()->andReturnFalse(); + + $event->listenEvents(['SomeListener::onBar' => [[$listener, 'onBar']]]); + }); + + $this->event->subscribe('SomeListener'); + + $this->assertTrue($this->event->hasListener('SomeListener::onBar')); + + $this->event->trigger('SomeListener::onBar'); + } + + public function testAutoObserve() + { + $listener = m::mock("overload:SomeListener", TestListener::class); + + $listener->shouldReceive('onBar')->once(); + + $this->app->shouldReceive('make')->with('SomeListener')->andReturn($listener); + + $this->event->observe('SomeListener'); + + $this->event->trigger('bar'); + } + +} + +class TestListener +{ + public function handle() + { + + } + + public function onBar() + { + + } + + public function onFoo() + { + + } + + public function subscribe() + { + + } +} diff --git a/vendor/topthink/framework/tests/HttpTest.php b/vendor/topthink/framework/tests/HttpTest.php new file mode 100644 index 0000000..c3e0abd --- /dev/null +++ b/vendor/topthink/framework/tests/HttpTest.php @@ -0,0 +1,155 @@ +app = m::mock(App::class)->makePartial(); + + $this->http = m::mock(Http::class, [$this->app])->shouldAllowMockingProtectedMethods()->makePartial(); + } + + protected function prepareApp($request, $response) + { + $this->app->shouldReceive('instance')->once()->with('request', $request); + $this->app->shouldReceive('initialized')->once()->andReturnFalse(); + $this->app->shouldReceive('initialize')->once(); + $this->app->shouldReceive('get')->with('request')->andReturn($request); + + $route = m::mock(Route::class); + + $route->shouldReceive('dispatch')->withArgs(function ($req, $withRoute) use ($request) { + if ($withRoute) { + $withRoute(); + } + return $req === $request; + })->andReturn($response); + + $route->shouldReceive('config')->with('route_annotation')->andReturn(true); + + $this->app->shouldReceive('get')->with('route')->andReturn($route); + + $console = m::mock(Console::class); + + $console->shouldReceive('call'); + + $this->app->shouldReceive('get')->with('console')->andReturn($console); + } + + public function testRun() + { + $root = vfsStream::setup('rootDir', null, [ + 'app' => [ + 'controller' => [], + 'middleware.php' => ' [ + 'route.php' => 'app->shouldReceive('getBasePath')->andReturn($root->getChild('app')->url() . DIRECTORY_SEPARATOR); + $this->app->shouldReceive('getRootPath')->andReturn($root->url() . DIRECTORY_SEPARATOR); + + $request = m::mock(Request::class)->makePartial(); + $response = m::mock(Response::class)->makePartial(); + + $this->prepareApp($request, $response); + + $this->assertEquals($response, $this->http->run($request)); + } + + public function multiAppRunProvider() + { + $request1 = m::mock(Request::class)->makePartial(); + $request1->shouldReceive('subDomain')->andReturn('www'); + $request1->shouldReceive('host')->andReturn('www.domain.com'); + + $request2 = m::mock(Request::class)->makePartial(); + $request2->shouldReceive('subDomain')->andReturn('app2'); + $request2->shouldReceive('host')->andReturn('app2.domain.com'); + + $request3 = m::mock(Request::class)->makePartial(); + $request3->shouldReceive('pathinfo')->andReturn('some1/a/b/c'); + + $request4 = m::mock(Request::class)->makePartial(); + $request4->shouldReceive('pathinfo')->andReturn('app3/a/b/c'); + + $request5 = m::mock(Request::class)->makePartial(); + $request5->shouldReceive('pathinfo')->andReturn('some2/a/b/c'); + + return [ + [$request1, true, 'app1'], + [$request2, true, 'app2'], + [$request3, true, 'app3'], + [$request4, true, null], + [$request5, true, 'some2', 'path'], + [$request1, false, 'some3'], + ]; + } + + public function testRunWithException() + { + $request = m::mock(Request::class); + $response = m::mock(Response::class); + + $this->app->shouldReceive('instance')->once()->with('request', $request); + $this->app->shouldReceive('initialize')->once(); + + $exception = new Exception(); + + $this->http->shouldReceive('runWithRequest')->once()->with($request)->andThrow($exception); + + $handle = m::mock(Handle::class); + + $handle->shouldReceive('report')->once()->with($exception); + $handle->shouldReceive('render')->once()->with($request, $exception)->andReturn($response); + + $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle); + + $this->assertEquals($response, $this->http->run($request)); + } + + public function testEnd() + { + $response = m::mock(Response::class); + $event = m::mock(Event::class); + $event->shouldReceive('trigger')->once()->with(HttpEnd::class, $response); + $this->app->shouldReceive('get')->once()->with('event')->andReturn($event); + $log = m::mock(Log::class); + $log->shouldReceive('save')->once(); + $this->app->shouldReceive('get')->once()->with('log')->andReturn($log); + + $this->http->end($response); + } + +} diff --git a/vendor/topthink/framework/tests/InteractsWithApp.php b/vendor/topthink/framework/tests/InteractsWithApp.php new file mode 100644 index 0000000..f4fcf73 --- /dev/null +++ b/vendor/topthink/framework/tests/InteractsWithApp.php @@ -0,0 +1,30 @@ +app = m::mock(App::class)->makePartial(); + Container::setInstance($this->app); + $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app); + $this->app->shouldReceive('isDebug')->andReturnTrue(); + $this->config = m::mock(Config::class)->makePartial(); + $this->config->shouldReceive('get')->with('app.show_error_msg')->andReturnTrue(); + $this->app->shouldReceive('get')->with('config')->andReturn($this->config); + $this->app->shouldReceive('runningInConsole')->andReturn(false); + } +} diff --git a/vendor/topthink/framework/tests/LogTest.php b/vendor/topthink/framework/tests/LogTest.php new file mode 100644 index 0000000..981110f --- /dev/null +++ b/vendor/topthink/framework/tests/LogTest.php @@ -0,0 +1,130 @@ +prepareApp(); + + $this->log = new Log($this->app); + } + + public function testGetConfig() + { + $config = [ + 'default' => 'file', + ]; + + $this->config->shouldReceive('get')->with('log')->andReturn($config); + + $this->assertEquals($config, $this->log->getConfig()); + + $this->expectException(InvalidArgumentException::class); + $this->log->getChannelConfig('foo'); + } + + public function testChannel() + { + $this->assertInstanceOf(ChannelSet::class, $this->log->channel(['file', 'mail'])); + } + + public function testLogManagerInstances() + { + $this->config->shouldReceive('get')->with("log.channels.single", null)->andReturn(['type' => 'file']); + + $channel1 = $this->log->channel('single'); + $channel2 = $this->log->channel('single'); + + $this->assertSame($channel1, $channel2); + } + + public function testFileLog() + { + $root = vfsStream::setup(); + + $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file'); + + $this->config->shouldReceive('get')->with("log.channels.file", null)->andReturn(['type' => 'file', 'path' => $root->url()]); + + $this->log->info('foo'); + + $this->assertEquals($this->log->getLog(), ['info' => ['foo']]); + + $this->log->clear(); + + $this->assertEmpty($this->log->getLog()); + + $this->log->error('foo'); + $this->assertArrayHasKey('error', $this->log->getLog()); + + $this->log->emergency('foo'); + $this->assertArrayHasKey('emergency', $this->log->getLog()); + + $this->log->alert('foo'); + $this->assertArrayHasKey('alert', $this->log->getLog()); + + $this->log->critical('foo'); + $this->assertArrayHasKey('critical', $this->log->getLog()); + + $this->log->warning('foo'); + $this->assertArrayHasKey('warning', $this->log->getLog()); + + $this->log->notice('foo'); + $this->assertArrayHasKey('notice', $this->log->getLog()); + + $this->log->debug('foo'); + $this->assertArrayHasKey('debug', $this->log->getLog()); + + $this->log->sql('foo'); + $this->assertArrayHasKey('sql', $this->log->getLog()); + + $this->log->custom('foo'); + $this->assertArrayHasKey('custom', $this->log->getLog()); + + $this->log->write('foo'); + $this->assertTrue($root->hasChildren()); + $this->assertEmpty($this->log->getLog()); + + $this->log->close(); + + $this->log->info('foo'); + + $this->assertEmpty($this->log->getLog()); + } + + public function testSave() + { + $root = vfsStream::setup(); + + $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file'); + + $this->config->shouldReceive('get')->with("log.channels.file", null)->andReturn(['type' => 'file', 'path' => $root->url()]); + + $this->log->info('foo'); + + $this->log->save(); + + $this->assertTrue($root->hasChildren()); + } + +} diff --git a/vendor/topthink/framework/tests/MiddlewareTest.php b/vendor/topthink/framework/tests/MiddlewareTest.php new file mode 100644 index 0000000..aa53059 --- /dev/null +++ b/vendor/topthink/framework/tests/MiddlewareTest.php @@ -0,0 +1,108 @@ +prepareApp(); + + $this->middleware = new Middleware($this->app); + } + + public function testSetMiddleware() + { + $this->middleware->add('BarMiddleware', 'bar'); + + $this->assertEquals(1, count($this->middleware->all('bar'))); + + $this->middleware->controller('BarMiddleware'); + $this->assertEquals(1, count($this->middleware->all('controller'))); + + $this->middleware->import(['FooMiddleware']); + $this->assertEquals(1, count($this->middleware->all())); + + $this->middleware->unshift(['BazMiddleware', 'baz']); + $this->assertEquals(2, count($this->middleware->all())); + $this->assertEquals([['BazMiddleware', 'handle'], 'baz'], $this->middleware->all()[0]); + + $this->config->shouldReceive('get')->with('middleware.alias', [])->andReturn(['foo' => ['FooMiddleware', 'FarMiddleware']]); + + $this->middleware->add('foo'); + $this->assertEquals(3, count($this->middleware->all())); + $this->middleware->add(function () { + }); + $this->middleware->add(function () { + }); + $this->assertEquals(5, count($this->middleware->all())); + } + + public function testPipelineAndEnd() + { + $bar = m::mock("overload:BarMiddleware"); + $foo = m::mock("overload:FooMiddleware", Foo::class); + + $request = m::mock(Request::class); + $response = m::mock(Response::class); + + $e = new Exception(); + + $handle = m::mock(Handle::class); + $handle->shouldReceive('report')->with($e)->andReturnNull(); + $handle->shouldReceive('render')->with($request, $e)->andReturn($response); + + $foo->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) { + return $next($request); + }); + $bar->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) use ($e) { + $next($request); + throw $e; + }); + + $foo->shouldReceive('end')->once()->with($response)->andReturnNull(); + + $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle); + + $this->config->shouldReceive('get')->once()->with('middleware.priority', [])->andReturn(['FooMiddleware', 'BarMiddleware']); + + $this->middleware->import([function ($request, $next) { + return $next($request); + }, 'BarMiddleware', 'FooMiddleware']); + + $this->assertInstanceOf(Pipeline::class, $pipeline = $this->middleware->pipeline()); + + $pipeline->send($request)->then(function ($request) use ($e, $response) { + throw $e; + }); + + $this->middleware->end($response); + } +} + +class Foo +{ + public function end(Response $response) + { + } +} diff --git a/vendor/topthink/think-filesystem/src/Filesystem.php b/vendor/topthink/think-filesystem/src/Filesystem.php new file mode 100644 index 0000000..0aee929 --- /dev/null +++ b/vendor/topthink/think-filesystem/src/Filesystem.php @@ -0,0 +1,89 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use InvalidArgumentException; +use think\filesystem\Driver; +use think\filesystem\driver\Local; +use think\helper\Arr; + +/** + * Class Filesystem + * @package think + * @mixin Driver + * @mixin Local + */ +class Filesystem extends Manager +{ + protected $namespace = '\\think\\filesystem\\driver\\'; + + /** + * @param null|string $name + * @return Driver + */ + public function disk(string $name = null): Driver + { + return $this->driver($name); + } + + protected function resolveType(string $name) + { + return $this->getDiskConfig($name, 'type', 'local'); + } + + protected function resolveConfig(string $name) + { + return $this->getDiskConfig($name); + } + + /** + * 获取缓存配置 + * @access public + * @param null|string $name 名称 + * @param mixed $default 默认值 + * @return mixed + */ + public function getConfig(string $name = null, $default = null) + { + if (!is_null($name)) { + return $this->app->config->get('filesystem.' . $name, $default); + } + + return $this->app->config->get('filesystem'); + } + + /** + * 获取磁盘配置 + * @param string $disk + * @param null $name + * @param null $default + * @return array + */ + public function getDiskConfig($disk, $name = null, $default = null) + { + if ($config = $this->getConfig("disks.{$disk}")) { + return Arr::get($config, $name, $default); + } + + throw new InvalidArgumentException("Disk [$disk] not found."); + } + + /** + * 默认驱动 + * @return string|null + */ + public function getDefaultDriver() + { + return $this->getConfig('default'); + } +} diff --git a/vendor/topthink/think-filesystem/src/facade/Filesystem.php b/vendor/topthink/think-filesystem/src/facade/Filesystem.php new file mode 100644 index 0000000..0e32c2c --- /dev/null +++ b/vendor/topthink/think-filesystem/src/facade/Filesystem.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\facade; + +use think\Facade; +use think\filesystem\Driver; + +/** + * Class Filesystem + * @package think\facade + * @mixin \think\Filesystem + * @method static Driver disk(string $name = null) , null|string + * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取缓存配置 + * @method static array getDiskConfig(string $disk, null $name = null, null $default = null) 获取磁盘配置 + * @method static string|null getDefaultDriver() 默认驱动 + */ +class Filesystem extends Facade +{ + protected static function getFacadeClass() + { + return \think\Filesystem::class; + } +} diff --git a/vendor/topthink/think-filesystem/src/filesystem/Driver.php b/vendor/topthink/think-filesystem/src/filesystem/Driver.php new file mode 100644 index 0000000..e804141 --- /dev/null +++ b/vendor/topthink/think-filesystem/src/filesystem/Driver.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\filesystem; + +use League\Flysystem\Filesystem; +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\UnableToSetVisibility; +use League\Flysystem\UnableToWriteFile; +use RuntimeException; +use think\Cache; +use think\File; + +/** + * Class Driver + * @package think\filesystem + * @mixin Filesystem + */ +abstract class Driver +{ + + /** @var Cache */ + protected $cache; + + /** @var Filesystem */ + protected $filesystem; + + /** + * 配置参数 + * @var array + */ + protected $config = []; + + public function __construct(Cache $cache, array $config) + { + $this->cache = $cache; + $this->config = array_merge($this->config, $config); + + $adapter = $this->createAdapter(); + $this->filesystem = $this->createFilesystem($adapter); + } + + abstract protected function createAdapter(): FilesystemAdapter; + + protected function createFilesystem(FilesystemAdapter $adapter): Filesystem + { + $config = array_intersect_key($this->config, array_flip(['visibility', 'disable_asserts', 'url'])); + + return new Filesystem($adapter, $config); + } + + /** + * 获取文件完整路径 + * @param string $path + * @return string + */ + public function path(string $path): string + { + return $path; + } + + protected function concatPathToUrl($url, $path) + { + return rtrim($url, '/') . '/' . ltrim($path, '/'); + } + + public function url(string $path): string + { + throw new RuntimeException('This driver does not support retrieving URLs.'); + } + + /** + * 保存文件 + * @param string $path 路径 + * @param File $file 文件 + * @param null|string|\Closure $rule 文件名规则 + * @param array $options 参数 + * @return bool|string + */ + public function putFile(string $path, File $file, $rule = null, array $options = []) + { + return $this->putFileAs($path, $file, $file->hashName($rule), $options); + } + + /** + * 指定文件名保存文件 + * @param string $path 路径 + * @param File $file 文件 + * @param string $name 文件名 + * @param array $options 参数 + * @return bool|string + */ + public function putFileAs(string $path, File $file, string $name, array $options = []) + { + $stream = fopen($file->getRealPath(), 'r'); + $path = trim($path . '/' . $name, '/'); + + $result = $this->put($path, $stream, $options); + + if (is_resource($stream)) { + fclose($stream); + } + + return $result ? $path : false; + } + + protected function put(string $path, $contents, array $options = []) + { + try { + $this->writeStream($path, $contents, $options); + } catch (UnableToWriteFile|UnableToSetVisibility $e) { + return false; + } + return true; + } + + public function __call($method, $parameters) + { + return $this->filesystem->$method(...$parameters); + } +} diff --git a/vendor/topthink/think-filesystem/src/filesystem/driver/Local.php b/vendor/topthink/think-filesystem/src/filesystem/driver/Local.php new file mode 100644 index 0000000..3b1b64d --- /dev/null +++ b/vendor/topthink/think-filesystem/src/filesystem/driver/Local.php @@ -0,0 +1,98 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\filesystem\driver; + +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\Local\LocalFilesystemAdapter; +use League\Flysystem\PathNormalizer; +use League\Flysystem\PathPrefixer; +use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use League\Flysystem\Visibility; +use League\Flysystem\WhitespacePathNormalizer; +use think\filesystem\Driver; + +class Local extends Driver +{ + /** + * 配置参数 + * @var array + */ + protected $config = [ + 'root' => '', + ]; + + /** + * @var PathPrefixer + */ + protected $prefixer; + + /** + * @var PathNormalizer + */ + protected $normalizer; + + protected function createAdapter(): FilesystemAdapter + { + $visibility = PortableVisibilityConverter::fromArray( + $this->config['permissions'] ?? [], + $this->config['visibility'] ?? Visibility::PRIVATE + ); + + $links = ($this->config['links'] ?? null) === 'skip' + ? LocalFilesystemAdapter::SKIP_LINKS + : LocalFilesystemAdapter::DISALLOW_LINKS; + + return new LocalFilesystemAdapter( + $this->config['root'], + $visibility, + $this->config['lock'] ?? LOCK_EX, + $links + ); + } + + protected function prefixer() + { + if (!$this->prefixer) { + $this->prefixer = new PathPrefixer($this->config['root'], DIRECTORY_SEPARATOR); + } + return $this->prefixer; + } + + protected function normalizer() + { + if (!$this->normalizer) { + $this->normalizer = new WhitespacePathNormalizer(); + } + return $this->normalizer; + } + + /** + * 获取文件访问地址 + * @param string $path 文件路径 + * @return string + */ + public function url(string $path): string + { + $path = $this->normalizer()->normalizePath($path); + + if (isset($this->config['url'])) { + return $this->concatPathToUrl($this->config['url'], $path); + } + return parent::url($path); + } + + public function path(string $path): string + { + return $this->prefixer()->prefixPath($path); + } +} diff --git a/vendor/topthink/think-filesystem/tests/FilesystemTest.php b/vendor/topthink/think-filesystem/tests/FilesystemTest.php new file mode 100644 index 0000000..cc9d94e --- /dev/null +++ b/vendor/topthink/think-filesystem/tests/FilesystemTest.php @@ -0,0 +1,66 @@ +app = m::mock(App::class)->makePartial(); + Container::setInstance($this->app); + $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app); + $this->config = m::mock(Config::class); + $this->config->shouldReceive('get')->with('filesystem.default', null)->andReturn('local'); + $this->app->shouldReceive('get')->with('config')->andReturn($this->config); + $this->filesystem = new Filesystem($this->app); + + $this->root = vfsStream::setup('rootDir'); + } + + protected function tearDown(): void + { + m::close(); + } + + public function testDisk() + { + $this->config->shouldReceive('get')->with('filesystem.disks.local', null)->andReturn([ + 'type' => 'local', + 'root' => $this->root->url(), + ]); + + $this->config->shouldReceive('get')->with('filesystem.disks.foo', null)->andReturn([ + 'type' => 'local', + 'root' => $this->root->url(), + ]); + + $this->assertInstanceOf(Local::class, $this->filesystem->disk()); + + $this->assertInstanceOf(Local::class, $this->filesystem->disk('foo')); + } + +} + diff --git a/vendor/topthink/think-helper/LICENSE b/vendor/topthink/think-helper/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/topthink/think-helper/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/topthink/think-helper/src/contract/Jsonable.php b/vendor/topthink/think-helper/src/contract/Jsonable.php new file mode 100644 index 0000000..d86122d --- /dev/null +++ b/vendor/topthink/think-helper/src/contract/Jsonable.php @@ -0,0 +1,8 @@ + +// +---------------------------------------------------------------------- + +use think\Collection; +use think\helper\Arr; + +if (!function_exists('throw_if')) { + /** + * 按条件抛异常 + * + * @param mixed $condition + * @param Throwable|string $exception + * @param array ...$parameters + * @return mixed + * + * @throws Throwable + */ + function throw_if($condition, $exception, ...$parameters) + { + if ($condition) { + throw (is_string($exception) ? new $exception(...$parameters) : $exception); + } + + return $condition; + } +} + +if (!function_exists('throw_unless')) { + /** + * 按条件抛异常 + * + * @param mixed $condition + * @param Throwable|string $exception + * @param array ...$parameters + * @return mixed + * @throws Throwable + */ + function throw_unless($condition, $exception, ...$parameters) + { + if (!$condition) { + throw (is_string($exception) ? new $exception(...$parameters) : $exception); + } + + return $condition; + } +} + +if (!function_exists('tap')) { + /** + * 对一个值调用给定的闭包,然后返回该值 + * + * @param mixed $value + * @param callable|null $callback + * @return mixed + */ + function tap($value, $callback = null) + { + if (is_null($callback)) { + return $value; + } + + $callback($value); + + return $value; + } +} + +if (!function_exists('value')) { + /** + * Return the default value of the given value. + * + * @param mixed $value + * @return mixed + */ + function value($value) + { + return $value instanceof Closure ? $value() : $value; + } +} + +if (!function_exists('collect')) { + /** + * Create a collection from the given value. + * + * @param mixed $value + * @return Collection + */ + function collect($value = null) + { + return new Collection($value); + } +} + +if (!function_exists('data_fill')) { + /** + * Fill in data where it's missing. + * + * @param mixed $target + * @param string|array $key + * @param mixed $value + * @return mixed + */ + function data_fill(&$target, $key, $value) + { + return data_set($target, $key, $value, false); + } +} + +if (!function_exists('data_get')) { + /** + * Get an item from an array or object using "dot" notation. + * + * @param mixed $target + * @param string|array|int $key + * @param mixed $default + * @return mixed + */ + function data_get($target, $key, $default = null) + { + if (is_null($key)) { + return $target; + } + + $key = is_array($key) ? $key : explode('.', $key); + + while (!is_null($segment = array_shift($key))) { + if ('*' === $segment) { + if ($target instanceof Collection) { + $target = $target->all(); + } elseif (!is_array($target)) { + return value($default); + } + + $result = []; + + foreach ($target as $item) { + $result[] = data_get($item, $key); + } + + return in_array('*', $key) ? Arr::collapse($result) : $result; + } + + if (Arr::accessible($target) && Arr::exists($target, $segment)) { + $target = $target[$segment]; + } elseif (is_object($target) && isset($target->{$segment})) { + $target = $target->{$segment}; + } else { + return value($default); + } + } + + return $target; + } +} + +if (!function_exists('data_set')) { + /** + * Set an item on an array or object using dot notation. + * + * @param mixed $target + * @param string|array $key + * @param mixed $value + * @param bool $overwrite + * @return mixed + */ + function data_set(&$target, $key, $value, $overwrite = true) + { + $segments = is_array($key) ? $key : explode('.', $key); + + if (($segment = array_shift($segments)) === '*') { + if (!Arr::accessible($target)) { + $target = []; + } + + if ($segments) { + foreach ($target as &$inner) { + data_set($inner, $segments, $value, $overwrite); + } + } elseif ($overwrite) { + foreach ($target as &$inner) { + $inner = $value; + } + } + } elseif (Arr::accessible($target)) { + if ($segments) { + if (!Arr::exists($target, $segment)) { + $target[$segment] = []; + } + + data_set($target[$segment], $segments, $value, $overwrite); + } elseif ($overwrite || !Arr::exists($target, $segment)) { + $target[$segment] = $value; + } + } elseif (is_object($target)) { + if ($segments) { + if (!isset($target->{$segment})) { + $target->{$segment} = []; + } + + data_set($target->{$segment}, $segments, $value, $overwrite); + } elseif ($overwrite || !isset($target->{$segment})) { + $target->{$segment} = $value; + } + } else { + $target = []; + + if ($segments) { + data_set($target[$segment], $segments, $value, $overwrite); + } elseif ($overwrite) { + $target[$segment] = $value; + } + } + + return $target; + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * 获取一个trait里所有引用到的trait + * + * @param string $trait Trait + * @return array + */ + function trait_uses_recursive(string $trait): array + { + $traits = class_uses($trait); + foreach ($traits as $trait) { + $traits += trait_uses_recursive($trait); + } + + return $traits; + } +} + +if (!function_exists('class_basename')) { + /** + * 获取类名(不包含命名空间) + * + * @param mixed $class 类名 + * @return string + */ + function class_basename($class): string + { + $class = is_object($class) ? get_class($class) : $class; + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('class_uses_recursive')) { + /** + *获取一个类里所有用到的trait,包括父类的 + * + * @param mixed $class 类名 + * @return array + */ + function class_uses_recursive($class): array + { + if (is_object($class)) { + $class = get_class($class); + } + + $results = []; + $classes = array_merge([$class => $class], class_parents($class)); + foreach ($classes as $class) { + $results += trait_uses_recursive($class); + } + + return array_unique($results); + } +} diff --git a/vendor/topthink/think-multi-app/LICENSE b/vendor/topthink/think-multi-app/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/topthink/think-multi-app/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/topthink/think-multi-app/src/MultiApp.php b/vendor/topthink/think-multi-app/src/MultiApp.php new file mode 100644 index 0000000..b0ac260 --- /dev/null +++ b/vendor/topthink/think-multi-app/src/MultiApp.php @@ -0,0 +1,245 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\app; + +use Closure; +use think\App; +use think\exception\HttpException; +use think\Request; +use think\Response; + +/** + * 多应用模式支持 + */ +class MultiApp +{ + + /** @var App */ + protected $app; + + /** + * 应用名称 + * @var string + */ + protected $name; + + /** + * 应用名称 + * @var string + */ + protected $appName; + + /** + * 应用路径 + * @var string + */ + protected $path; + + public function __construct(App $app) + { + $this->app = $app; + $this->name = $this->app->http->getName(); + $this->path = $this->app->http->getPath(); + } + + /** + * 多应用解析 + * @access public + * @param Request $request + * @param Closure $next + * @return Response + */ + public function handle($request, Closure $next) + { + if (!$this->parseMultiApp()) { + return $next($request); + } + + return $this->app->middleware->pipeline('app') + ->send($request) + ->then(function ($request) use ($next) { + return $next($request); + }); + } + + /** + * 获取路由目录 + * @access protected + * @return string + */ + protected function getRoutePath(): string + { + return $this->app->getAppPath() . 'route' . DIRECTORY_SEPARATOR; + } + + /** + * 解析多应用 + * @return bool + */ + protected function parseMultiApp(): bool + { + $scriptName = $this->getScriptName(); + $defaultApp = $this->app->config->get('app.default_app') ?: 'index'; + + if ($this->name || ($scriptName && !in_array($scriptName, ['index', 'router', 'think']))) { + $appName = $this->name ?: $scriptName; + $this->app->http->setBind(); + } else { + // 自动多应用识别 + $this->app->http->setBind(false); + $appName = null; + $this->appName = ''; + + $bind = $this->app->config->get('app.domain_bind', []); + + if (!empty($bind)) { + // 获取当前子域名 + $subDomain = $this->app->request->subDomain(); + $domain = $this->app->request->host(true); + + if (isset($bind[$domain])) { + $appName = $bind[$domain]; + $this->app->http->setBind(); + } elseif (isset($bind[$subDomain])) { + $appName = $bind[$subDomain]; + $this->app->http->setBind(); + } elseif (isset($bind['*'])) { + $appName = $bind['*']; + $this->app->http->setBind(); + } + } + + if (!$this->app->http->isBind()) { + $path = $this->app->request->pathinfo(); + $map = $this->app->config->get('app.app_map', []); + $deny = $this->app->config->get('app.deny_app_list', []); + $name = current(explode('/', $path)); + + if (strpos($name, '.')) { + $name = strstr($name, '.', true); + } + + if (isset($map[$name])) { + if ($map[$name] instanceof Closure) { + $result = call_user_func_array($map[$name], [$this->app]); + $appName = $result ?: $name; + } else { + $appName = $map[$name]; + } + } elseif ($name && (false !== array_search($name, $map) || in_array($name, $deny))) { + throw new HttpException(404, 'app not exists:' . $name); + } elseif ($name && isset($map['*'])) { + $appName = $map['*']; + } else { + $appName = $name ?: $defaultApp; + $appPath = $this->path ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR; + + if (!is_dir($appPath)) { + $express = $this->app->config->get('app.app_express', false); + if ($express) { + $this->setApp($defaultApp); + return true; + } else { + return false; + } + } + } + + if ($name) { + $this->app->request->setRoot('/' . $name); + $this->app->request->setPathinfo(strpos($path, '/') ? ltrim(strstr($path, '/'), '/') : ''); + } + } + } + + $this->setApp($appName ?: $defaultApp); + return true; + } + + /** + * 获取当前运行入口名称 + * @access protected + * @codeCoverageIgnore + * @return string + */ + protected function getScriptName(): string + { + if (isset($_SERVER['SCRIPT_FILENAME'])) { + $file = $_SERVER['SCRIPT_FILENAME']; + } elseif (isset($_SERVER['argv'][0])) { + $file = realpath($_SERVER['argv'][0]); + } + + return isset($file) ? pathinfo($file, PATHINFO_FILENAME) : ''; + } + + /** + * 设置应用 + * @param string $appName + */ + protected function setApp(string $appName): void + { + $this->appName = $appName; + $this->app->http->name($appName); + + $appPath = $this->path ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR; + + $this->app->setAppPath($appPath); + // 设置应用命名空间 + $this->app->setNamespace($this->app->config->get('app.app_namespace') ?: 'app\\' . $appName); + + if (is_dir($appPath)) { + $this->app->setRuntimePath($this->app->getRuntimePath() . $appName . DIRECTORY_SEPARATOR); + $this->app->http->setRoutePath($this->getRoutePath()); + + //加载应用 + $this->loadApp($appName, $appPath); + } + } + + /** + * 加载应用文件 + * @param string $appName 应用名 + * @return void + */ + protected function loadApp(string $appName, string $appPath): void + { + if (is_file($appPath . 'common.php')) { + include_once $appPath . 'common.php'; + } + + $files = []; + + $files = array_merge($files, glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $this->app->getConfigExt())); + + foreach ($files as $file) { + $this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME)); + } + + if (is_file($appPath . 'event.php')) { + $this->app->loadEvent(include $appPath . 'event.php'); + } + + if (is_file($appPath . 'middleware.php')) { + $this->app->middleware->import(include $appPath . 'middleware.php', 'app'); + } + + if (is_file($appPath . 'provider.php')) { + $this->app->bind(include $appPath . 'provider.php'); + } + + // 加载应用默认语言包 + $this->app->loadLangPack($this->app->lang->defaultLangSet()); + } + +} diff --git a/vendor/topthink/think-orm/LICENSE b/vendor/topthink/think-orm/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/topthink/think-orm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/topthink/think-orm/src/Model.php b/vendor/topthink/think-orm/src/Model.php new file mode 100644 index 0000000..761f780 --- /dev/null +++ b/vendor/topthink/think-orm/src/Model.php @@ -0,0 +1,1069 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use ArrayAccess; +use Closure; +use JsonSerializable; +use think\contract\Arrayable; +use think\contract\Jsonable; +use think\db\BaseQuery as Query; + +/** + * Class Model + * @package think + * @mixin Query + * @method void onAfterRead(Model $model) static after_read事件定义 + * @method mixed onBeforeInsert(Model $model) static before_insert事件定义 + * @method void onAfterInsert(Model $model) static after_insert事件定义 + * @method mixed onBeforeUpdate(Model $model) static before_update事件定义 + * @method void onAfterUpdate(Model $model) static after_update事件定义 + * @method mixed onBeforeWrite(Model $model) static before_write事件定义 + * @method void onAfterWrite(Model $model) static after_write事件定义 + * @method mixed onBeforeDelete(Model $model) static before_write事件定义 + * @method void onAfterDelete(Model $model) static after_delete事件定义 + * @method void onBeforeRestore(Model $model) static before_restore事件定义 + * @method void onAfterRestore(Model $model) static after_restore事件定义 + */ +abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable +{ + use model\concern\Attribute; + use model\concern\RelationShip; + use model\concern\ModelEvent; + use model\concern\TimeStamp; + use model\concern\Conversion; + + /** + * 数据是否存在 + * @var bool + */ + private $exists = false; + + /** + * 是否强制更新所有数据 + * @var bool + */ + private $force = false; + + /** + * 是否Replace + * @var bool + */ + private $replace = false; + + /** + * 数据表后缀 + * @var string + */ + protected $suffix; + + /** + * 更新条件 + * @var array + */ + private $updateWhere; + + /** + * 数据库配置 + * @var string + */ + protected $connection; + + /** + * 模型名称 + * @var string + */ + protected $name; + + /** + * 主键值 + * @var string + */ + protected $key; + + /** + * 数据表名称 + * @var string + */ + protected $table; + + /** + * 初始化过的模型. + * @var array + */ + protected static $initialized = []; + + /** + * 软删除字段默认值 + * @var mixed + */ + protected $defaultSoftDelete; + + /** + * 全局查询范围 + * @var array + */ + protected $globalScope = []; + + /** + * 延迟保存信息 + * @var bool + */ + private $lazySave = false; + + /** + * Db对象 + * @var DbManager + */ + protected static $db; + + /** + * 容器对象的依赖注入方法 + * @var callable + */ + protected static $invoker; + + /** + * 服务注入 + * @var Closure[] + */ + protected static $maker = []; + + /** + * 方法注入 + * @var Closure[][] + */ + protected static $macro = []; + + /** + * 设置服务注入 + * @access public + * @param Closure $maker + * @return void + */ + public static function maker(Closure $maker) + { + static::$maker[] = $maker; + } + + /** + * 设置方法注入 + * @access public + * @param string $method + * @param Closure $closure + * @return void + */ + public static function macro(string $method, Closure $closure) + { + if (!isset(static::$macro[static::class])) { + static::$macro[static::class] = []; + } + static::$macro[static::class][$method] = $closure; + } + + /** + * 设置Db对象 + * @access public + * @param DbManager $db Db对象 + * @return void + */ + public static function setDb(DbManager $db) + { + self::$db = $db; + } + + /** + * 设置容器对象的依赖注入方法 + * @access public + * @param callable $callable 依赖注入方法 + * @return void + */ + public static function setInvoker(callable $callable): void + { + self::$invoker = $callable; + } + + /** + * 调用反射执行模型方法 支持参数绑定 + * @access public + * @param mixed $method + * @param array $vars 参数 + * @return mixed + */ + public function invoke($method, array $vars = []) + { + if (self::$invoker) { + $call = self::$invoker; + return $call($method instanceof Closure ? $method : Closure::fromCallable([$this, $method]), $vars); + } + + return call_user_func_array($method instanceof Closure ? $method : [$this, $method], $vars); + } + + /** + * 架构函数 + * @access public + * @param array $data 数据 + */ + public function __construct(array $data = []) + { + $this->data = $data; + + if (!empty($this->data)) { + // 废弃字段 + foreach ((array) $this->disuse as $key) { + if (array_key_exists($key, $this->data)) { + unset($this->data[$key]); + } + } + } + + // 记录原始数据 + $this->origin = $this->data; + + if (empty($this->name)) { + // 当前模型名 + $name = str_replace('\\', '/', static::class); + $this->name = basename($name); + } + + if (!empty(static::$maker)) { + foreach (static::$maker as $maker) { + call_user_func($maker, $this); + } + } + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 获取当前模型名称 + * @access public + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * 创建新的模型实例 + * @access public + * @param array $data 数据 + * @param mixed $where 更新条件 + * @param array $options 参数 + * @return Model + */ + public function newInstance(array $data = [], $where = null, array $options = []): Model + { + $model = new static($data); + + if ($this->connection) { + $model->setConnection($this->connection); + } + + if ($this->suffix) { + $model->setSuffix($this->suffix); + } + + if (empty($data)) { + return $model; + } + + $model->exists(true); + + $model->setUpdateWhere($where); + + $model->trigger('AfterRead'); + + return $model; + } + + /** + * 设置模型的更新条件 + * @access protected + * @param mixed $where 更新条件 + * @return void + */ + protected function setUpdateWhere($where): void + { + $this->updateWhere = $where; + } + + /** + * 设置当前模型的数据库连接 + * @access public + * @param string $connection 数据表连接标识 + * @return $this + */ + public function setConnection(string $connection) + { + $this->connection = $connection; + return $this; + } + + /** + * 获取当前模型的数据库连接标识 + * @access public + * @return string + */ + public function getConnection(): string + { + return $this->connection ?: ''; + } + + /** + * 设置当前模型数据表的后缀 + * @access public + * @param string $suffix 数据表后缀 + * @return $this + */ + public function setSuffix(string $suffix) + { + $this->suffix = $suffix; + return $this; + } + + /** + * 获取当前模型的数据表后缀 + * @access public + * @return string + */ + public function getSuffix(): string + { + return $this->suffix ?: ''; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param array $scope 设置不使用的全局查询范围 + * @return Query + */ + public function db($scope = []): Query + { + /** @var Query $query */ + $query = self::$db->connect($this->connection) + ->name($this->name . $this->suffix) + ->pk($this->pk); + + if (!empty($this->table)) { + $query->table($this->table . $this->suffix); + } + + $query->model($this) + ->json($this->json, $this->jsonAssoc) + ->setFieldType(array_merge($this->schema, $this->jsonType)); + + // 软删除 + if (property_exists($this, 'withTrashed') && !$this->withTrashed) { + $this->withNoTrashed($query); + } + + // 全局作用域 + if (is_array($scope)) { + $globalScope = array_diff($this->globalScope, $scope); + $query->scope($globalScope); + } + + // 返回当前模型的数据库查询对象 + return $query; + } + + /** + * 初始化模型 + * @access private + * @return void + */ + private function initialize(): void + { + if (!isset(static::$initialized[static::class])) { + static::$initialized[static::class] = true; + static::init(); + } + } + + /** + * 初始化处理 + * @access protected + * @return void + */ + protected static function init() + { + } + + protected function checkData(): void + { + } + + protected function checkResult($result): void + { + } + + /** + * 更新是否强制写入数据 而不做比较(亦可用于软删除的强制删除) + * @access public + * @param bool $force + * @return $this + */ + public function force(bool $force = true) + { + $this->force = $force; + return $this; + } + + /** + * 判断force + * @access public + * @return bool + */ + public function isForce(): bool + { + return $this->force; + } + + /** + * 新增数据是否使用Replace + * @access public + * @param bool $replace + * @return $this + */ + public function replace(bool $replace = true) + { + $this->replace = $replace; + return $this; + } + + /** + * 刷新模型数据 + * @access public + * @param bool $relation 是否刷新关联数据 + * @return $this + */ + public function refresh(bool $relation = false) + { + if ($this->exists) { + $this->data = $this->db()->find($this->getKey())->getData(); + $this->origin = $this->data; + $this->get = []; + + if ($relation) { + $this->relation = []; + } + } + + return $this; + } + + /** + * 设置数据是否存在 + * @access public + * @param bool $exists + * @return $this + */ + public function exists(bool $exists = true) + { + $this->exists = $exists; + return $this; + } + + /** + * 判断数据是否存在数据库 + * @access public + * @return bool + */ + public function isExists(): bool + { + return $this->exists; + } + + /** + * 判断模型是否为空 + * @access public + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->data); + } + + /** + * 延迟保存当前数据对象 + * @access public + * @param array|bool $data 数据 + * @return void + */ + public function lazySave($data = []): void + { + if (false === $data) { + $this->lazySave = false; + } else { + if (is_array($data)) { + $this->setAttrs($data); + } + + $this->lazySave = true; + } + } + + /** + * 保存当前数据对象 + * @access public + * @param array $data 数据 + * @param string $sequence 自增序列名 + * @return bool + */ + public function save(array $data = [], string $sequence = null): bool + { + // 数据对象赋值 + $this->setAttrs($data); + + if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { + return false; + } + + $result = $this->exists ? $this->updateData() : $this->insertData($sequence); + + if (false === $result) { + return false; + } + + // 写入回调 + $this->trigger('AfterWrite'); + + // 重新记录原始数据 + $this->origin = $this->data; + $this->get = []; + $this->lazySave = false; + + return true; + } + + /** + * 检查数据是否允许写入 + * @access protected + * @return array + */ + protected function checkAllowFields(): array + { + // 检测字段 + if (empty($this->field)) { + if (!empty($this->schema)) { + $this->field = array_keys(array_merge($this->schema, $this->jsonType)); + } else { + $query = $this->db(); + $table = $this->table ? $this->table . $this->suffix : $query->getTable(); + + $this->field = $query->getConnection()->getTableFields($table); + } + + return $this->field; + } + + $field = $this->field; + + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); + } + + if (!empty($this->disuse)) { + // 废弃字段 + $field = array_diff($field, $this->disuse); + } + + return $field; + } + + /** + * 保存写入数据 + * @access protected + * @return bool + */ + protected function updateData(): bool + { + // 事件回调 + if (false === $this->trigger('BeforeUpdate')) { + return false; + } + + $this->checkData(); + + // 获取有更新的数据 + $data = $this->getChangedData(); + + if (empty($data)) { + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + + return true; + } + + if ($this->autoWriteTimestamp && $this->updateTime) { + // 自动写入更新时间 + $data[$this->updateTime] = $this->autoWriteTimestamp(); + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(); + + foreach ($this->relationWrite as $name => $val) { + if (!is_array($val)) { + continue; + } + + foreach ($val as $key) { + if (isset($data[$key])) { + unset($data[$key]); + } + } + } + + // 模型更新 + $db = $this->db(); + + $db->transaction(function () use ($data, $allowFields, $db) { + $this->key = null; + $where = $this->getWhere(); + + $result = $db->where($where) + ->strict(false) + ->cache(true) + ->setOption('key', $this->key) + ->field($allowFields) + ->update($data); + + $this->checkResult($result); + + // 关联更新 + if (!empty($this->relationWrite)) { + $this->autoRelationUpdate(); + } + }); + + // 更新回调 + $this->trigger('AfterUpdate'); + + return true; + } + + /** + * 新增写入数据 + * @access protected + * @param string $sequence 自增名 + * @return bool + */ + protected function insertData(string $sequence = null): bool + { + if (false === $this->trigger('BeforeInsert')) { + return false; + } + + $this->checkData(); + $data = $this->data; + + // 时间戳自动写入 + if ($this->autoWriteTimestamp) { + if ($this->createTime && !isset($data[$this->createTime])) { + $data[$this->createTime] = $this->autoWriteTimestamp(); + $this->data[$this->createTime] = $data[$this->createTime]; + } + + if ($this->updateTime && !isset($data[$this->updateTime])) { + $data[$this->updateTime] = $this->autoWriteTimestamp(); + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + } + + // 检查允许字段 + $allowFields = $this->checkAllowFields(); + + $db = $this->db(); + + $db->transaction(function () use ($data, $sequence, $allowFields, $db) { + $result = $db->strict(false) + ->field($allowFields) + ->replace($this->replace) + ->sequence($sequence) + ->insert($data, true); + + // 获取自动增长主键 + if ($result) { + $pk = $this->getPk(); + + if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) { + unset($this->get[$pk]); + $this->data[$pk] = $result; + } + } + + // 关联写入 + if (!empty($this->relationWrite)) { + $this->autoRelationInsert(); + } + }); + + // 标记数据已经存在 + $this->exists = true; + $this->origin = $this->data; + + // 新增回调 + $this->trigger('AfterInsert'); + + return true; + } + + /** + * 获取当前的更新条件 + * @access public + * @return mixed + */ + public function getWhere() + { + $pk = $this->getPk(); + + if (is_string($pk) && isset($this->origin[$pk])) { + $where = [[$pk, '=', $this->origin[$pk]]]; + $this->key = $this->origin[$pk]; + } elseif (is_array($pk)) { + foreach ($pk as $field) { + if (isset($this->origin[$field])) { + $where[] = [$field, '=', $this->origin[$field]]; + } + } + } + + if (empty($where)) { + $where = empty($this->updateWhere) ? null : $this->updateWhere; + } + + return $where; + } + + /** + * 保存多个数据到当前数据对象 + * @access public + * @param iterable $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 + * @return Collection + * @throws \Exception + */ + public function saveAll(iterable $dataSet, bool $replace = true): Collection + { + $db = $this->db(); + + $result = $db->transaction(function () use ($replace, $dataSet) { + + $pk = $this->getPk(); + + if (is_string($pk) && $replace) { + $auto = true; + } + + $result = []; + + $suffix = $this->getSuffix(); + + foreach ($dataSet as $key => $data) { + if ($this->exists || (!empty($auto) && isset($data[$pk]))) { + $result[$key] = static::update($data, [], [], $suffix); + } else { + $result[$key] = static::create($data, $this->field, $this->replace, $suffix); + } + } + + return $result; + }); + + return $this->toCollection($result); + } + + /** + * 删除当前的记录 + * @access public + * @return bool + */ + public function delete(): bool + { + if (!$this->exists || $this->isEmpty() || false === $this->trigger('BeforeDelete')) { + return false; + } + + // 读取更新条件 + $where = $this->getWhere(); + + $db = $this->db(); + + $db->transaction(function () use ($where, $db) { + // 删除当前模型数据 + $db->where($where)->delete(); + + // 关联删除 + if (!empty($this->relationWrite)) { + $this->autoRelationDelete(); + } + }); + + $this->trigger('AfterDelete'); + + $this->exists = false; + $this->lazySave = false; + + return true; + } + + /** + * 写入数据 + * @access public + * @param array $data 数据数组 + * @param array $allowField 允许字段 + * @param bool $replace 使用Replace + * @param string $suffix 数据表后缀 + * @return static + */ + public static function create(array $data, array $allowField = [], bool $replace = false, string $suffix = ''): Model + { + $model = new static(); + + if (!empty($allowField)) { + $model->allowField($allowField); + } + + if (!empty($suffix)) { + $model->setSuffix($suffix); + } + + $model->replace($replace)->save($data); + + return $model; + } + + /** + * 更新数据 + * @access public + * @param array $data 数据数组 + * @param mixed $where 更新条件 + * @param array $allowField 允许字段 + * @param string $suffix 数据表后缀 + * @return static + */ + public static function update(array $data, $where = [], array $allowField = [], string $suffix = '') + { + $model = new static(); + + if (!empty($allowField)) { + $model->allowField($allowField); + } + + if (!empty($where)) { + $model->setUpdateWhere($where); + } + + if (!empty($suffix)) { + $model->setSuffix($suffix); + } + + $model->exists(true)->save($data); + + return $model; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @param bool $force 是否强制删除 + * @return bool + */ + public static function destroy($data, bool $force = false): bool + { + if (empty($data) && 0 !== $data) { + return false; + } + + $model = new static(); + + $query = $model->db(); + + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + $data($query); + $data = null; + } + + $resultSet = $query->select($data); + + foreach ($resultSet as $result) { + $result->force($force)->delete(); + } + + return true; + } + + /** + * 解序列化后处理 + */ + public function __wakeup() + { + $this->initialize(); + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set(string $name, $value): void + { + $this->setAttr($name, $value); + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get(string $name) + { + return $this->getAttr($name); + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return bool + */ + public function __isset(string $name): bool + { + return !is_null($this->getAttr($name)); + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset(string $name): void + { + unset($this->data[$name], + $this->get[$name], + $this->relation[$name]); + } + + // ArrayAccess + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + $this->setAttr($name, $value); + } + + #[\ReturnTypeWillChange] + public function offsetExists($name): bool + { + return $this->__isset($name); + } + + #[\ReturnTypeWillChange] + public function offsetUnset($name) + { + $this->__unset($name); + } + + #[\ReturnTypeWillChange] + public function offsetGet($name) + { + return $this->getAttr($name); + } + + /** + * 设置不使用的全局查询范围 + * @access public + * @param array $scope 不启用的全局查询范围 + * @return Query + */ + public static function withoutGlobalScope(array $scope = null) + { + $model = new static(); + + return $model->db($scope); + } + + /** + * 切换后缀进行查询 + * @access public + * @param string $suffix 切换的表后缀 + * @return Model + */ + public static function suffix(string $suffix) + { + $model = new static(); + $model->setSuffix($suffix); + + return $model; + } + + /** + * 切换数据库连接进行查询 + * @access public + * @param string $connection 数据库连接标识 + * @return Model + */ + public static function connect(string $connection) + { + $model = new static(); + $model->setConnection($connection); + + return $model; + } + + public function __call($method, $args) + { + if (isset(static::$macro[static::class][$method])) { + return call_user_func_array(static::$macro[static::class][$method]->bindTo($this, static::class), $args); + } + + return call_user_func_array([$this->db(), $method], $args); + } + + public static function __callStatic($method, $args) + { + if (isset(static::$macro[static::class][$method])) { + return call_user_func_array(static::$macro[static::class][$method]->bindTo(null, static::class), $args); + } + + $model = new static(); + + return call_user_func_array([$model->db(), $method], $args); + } + + /** + * 析构方法 + * @access public + */ + public function __destruct() + { + if ($this->lazySave) { + $this->save(); + } + } +} diff --git a/vendor/topthink/think-orm/src/Paginator.php b/vendor/topthink/think-orm/src/Paginator.php new file mode 100644 index 0000000..eace6ba --- /dev/null +++ b/vendor/topthink/think-orm/src/Paginator.php @@ -0,0 +1,537 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Closure; +use Countable; +use DomainException; +use IteratorAggregate; +use JsonSerializable; +use think\paginator\driver\Bootstrap; +use Traversable; + +/** + * 分页基础类 + * @mixin Collection + */ +abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + /** + * 是否简洁模式 + * @var bool + */ + protected $simple = false; + + /** + * 数据集 + * @var Collection + */ + protected $items; + + /** + * 当前页 + * @var int + */ + protected $currentPage; + + /** + * 最后一页 + * @var int + */ + protected $lastPage; + + /** + * 数据总数 + * @var integer|null + */ + protected $total; + + /** + * 每页数量 + * @var int + */ + protected $listRows; + + /** + * 是否有下一页 + * @var bool + */ + protected $hasMore; + + /** + * 分页配置 + * @var array + */ + protected $options = [ + 'var_page' => 'page', + 'path' => '/', + 'query' => [], + 'fragment' => '', + ]; + + /** + * 获取当前页码 + * @var Closure + */ + protected static $currentPageResolver; + + /** + * 获取当前路径 + * @var Closure + */ + protected static $currentPathResolver; + + /** + * @var Closure + */ + protected static $maker; + + public function __construct($items, int $listRows, int $currentPage = 1, int $total = null, bool $simple = false, array $options = []) + { + $this->options = array_merge($this->options, $options); + + $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path']; + + $this->simple = $simple; + $this->listRows = $listRows; + + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + + if ($simple) { + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); + } else { + $this->total = $total; + $this->lastPage = (int) ceil($total / $listRows); + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = $this->currentPage < $this->lastPage; + } + $this->items = $items; + } + + /** + * @access public + * @param mixed $items + * @param int $listRows + * @param int $currentPage + * @param int $total + * @param bool $simple + * @param array $options + * @return Paginator + */ + public static function make($items, int $listRows, int $currentPage = 1, int $total = null, bool $simple = false, array $options = []) + { + if (isset(static::$maker)) { + return call_user_func(static::$maker, $items, $listRows, $currentPage, $total, $simple, $options); + } + + return new Bootstrap($items, $listRows, $currentPage, $total, $simple, $options); + } + + public static function maker(Closure $resolver) + { + static::$maker = $resolver; + } + + protected function setCurrentPage(int $currentPage): int + { + if (!$this->simple && $currentPage > $this->lastPage) { + return $this->lastPage > 0 ? $this->lastPage : 1; + } + + return $currentPage; + } + + /** + * 获取页码对应的链接 + * + * @access protected + * @param int $page + * @return string + */ + protected function url(int $page): string + { + if ($page <= 0) { + $page = 1; + } + + if (strpos($this->options['path'], '[PAGE]') === false) { + $parameters = [$this->options['var_page'] => $page]; + $path = $this->options['path']; + } else { + $parameters = []; + $path = str_replace('[PAGE]', (string) $page, $this->options['path']); + } + + if (count($this->options['query']) > 0) { + $parameters = array_merge($this->options['query'], $parameters); + } + + $url = $path; + if (!empty($parameters)) { + $url .= '?' . http_build_query($parameters, '', '&'); + } + + return $url . $this->buildFragment(); + } + + /** + * 自动获取当前页码 + * @access public + * @param string $varPage + * @param int $default + * @return int + */ + public static function getCurrentPage(string $varPage = 'page', int $default = 1): int + { + if (isset(static::$currentPageResolver)) { + return call_user_func(static::$currentPageResolver, $varPage); + } + + return $default; + } + + /** + * 设置获取当前页码闭包 + * @param Closure $resolver + */ + public static function currentPageResolver(Closure $resolver) + { + static::$currentPageResolver = $resolver; + } + + /** + * 自动获取当前的path + * @access public + * @param string $default + * @return string + */ + public static function getCurrentPath($default = '/'): string + { + if (isset(static::$currentPathResolver)) { + return call_user_func(static::$currentPathResolver); + } + + return $default; + } + + /** + * 设置获取当前路径闭包 + * @param Closure $resolver + */ + public static function currentPathResolver(Closure $resolver) + { + static::$currentPathResolver = $resolver; + } + + /** + * 获取数据总条数 + * @return int + */ + public function total(): int + { + if ($this->simple) { + throw new DomainException('not support total'); + } + + return $this->total; + } + + /** + * 获取每页数量 + * @return int + */ + public function listRows(): int + { + return $this->listRows; + } + + /** + * 获取当前页页码 + * @return int + */ + public function currentPage(): int + { + return $this->currentPage; + } + + /** + * 获取最后一页页码 + * @return int + */ + public function lastPage(): int + { + if ($this->simple) { + throw new DomainException('not support last'); + } + + return $this->lastPage; + } + + /** + * 数据是否足够分页 + * @access public + * @return bool + */ + public function hasPages(): bool + { + return !(1 == $this->currentPage && !$this->hasMore); + } + + /** + * 创建一组分页链接 + * + * @access public + * @param int $start + * @param int $end + * @return array + */ + public function getUrlRange(int $start, int $end): array + { + $urls = []; + + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * 设置URL锚点 + * + * @access public + * @param string|null $fragment + * @return $this + */ + public function fragment(string $fragment = null) + { + $this->options['fragment'] = $fragment; + + return $this; + } + + /** + * 添加URL参数 + * + * @access public + * @param array $append + * @return $this + */ + public function appends(array $append) + { + foreach ($append as $k => $v) { + if ($k !== $this->options['var_page']) { + $this->options['query'][$k] = $v; + } + } + + return $this; + } + + /** + * 构造锚点字符串 + * + * @access public + * @return string + */ + protected function buildFragment(): string + { + return $this->options['fragment'] ? '#' . $this->options['fragment'] : ''; + } + + /** + * 渲染分页html + * @access public + * @return mixed + */ + abstract public function render(); + + public function items() + { + return $this->items->all(); + } + + /** + * 获取数据集 + * + * @return Collection|\think\model\Collection + */ + public function getCollection() + { + return $this->items; + } + + /** + * 设置数据集 + * + * @param Collection $items + * @return $this + */ + public function setCollection(Collection $items) + { + $this->items = $items; + + return $this; + } + + public function isEmpty(): bool + { + return $this->items->isEmpty(); + } + + /** + * 给每个元素执行个回调 + * + * @access public + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * Retrieve an external iterator + * @access public + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + #[\ReturnTypeWillChange] + public function getIterator(): Traversable + { + return new ArrayIterator($this->items->all()); + } + + /** + * Whether a offset exists + * @access public + * @param mixed $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset): bool + { + return $this->items->offsetExists($offset); + } + + /** + * Offset to retrieve + * @access public + * @param mixed $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->items->offsetGet($offset); + } + + /** + * Offset to set + * @access public + * @param mixed $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->items->offsetSet($offset, $value); + } + + /** + * Offset to unset + * @access public + * @param mixed $offset + * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $this->items->offsetUnset($offset); + } + + /** + * 统计数据集条数 + * @return int + */ + public function count(): int + { + return $this->items->count(); + } + + public function __toString() + { + return (string) $this->render(); + } + + /** + * 转换为数组 + * @return array + */ + public function toArray(): array + { + try { + $total = $this->total(); + } catch (DomainException $e) { + $total = null; + } + + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; + } + + /** + * Specify data which should be serialized to JSON + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->toArray(); + } + + public function __call($name, $arguments) + { + $result = call_user_func_array([$this->items, $name], $arguments); + + if ($result instanceof Collection) { + $this->items = $result; + return $this; + } + + return $result; + } + +} diff --git a/vendor/topthink/think-orm/src/db/Fetch.php b/vendor/topthink/think-orm/src/db/Fetch.php new file mode 100644 index 0000000..a997a85 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/Fetch.php @@ -0,0 +1,492 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db; + +use think\db\exception\DbException as Exception; +use think\helper\Str; + +/** + * SQL获取类 + */ +class Fetch +{ + /** + * 查询对象 + * @var Query + */ + protected $query; + + /** + * Connection对象 + * @var Connection + */ + protected $connection; + + /** + * Builder对象 + * @var Builder + */ + protected $builder; + + /** + * 创建一个查询SQL获取对象 + * + * @param Query $query 查询对象 + */ + public function __construct(Query $query) + { + $this->query = $query; + $this->connection = $query->getConnection(); + $this->builder = $this->connection->getBuilder(); + } + + /** + * 聚合查询 + * @access protected + * @param string $aggregate 聚合方法 + * @param string $field 字段名 + * @return string + */ + protected function aggregate(string $aggregate, string $field): string + { + $this->query->parseOptions(); + + $field = $aggregate . '(' . $this->builder->parseKey($this->query, $field) . ') AS think_' . strtolower($aggregate); + + return $this->value($field, 0, false); + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @param bool $one + * @return string + */ + public function value(string $field, $default = null, bool $one = true): string + { + $options = $this->query->parseOptions(); + + if (isset($options['field'])) { + $this->query->removeOption('field'); + } + + $this->query->setOption('field', (array) $field); + + // 生成查询SQL + $sql = $this->builder->select($this->query, $one); + + if (isset($options['field'])) { + $this->query->setOption('field', $options['field']); + } else { + $this->query->removeOption('field'); + } + + return $this->fetch($sql); + } + + /** + * 得到某个列的数组 + * @access public + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return string + */ + public function column(string $field, string $key = ''): string + { + $options = $this->query->parseOptions(); + + if (isset($options['field'])) { + $this->query->removeOption('field'); + } + + if ($key && '*' != $field) { + $field = $key . ',' . $field; + } + + $field = array_map('trim', explode(',', $field)); + + $this->query->setOption('field', $field); + + // 生成查询SQL + $sql = $this->builder->select($this->query); + + if (isset($options['field'])) { + $this->query->setOption('field', $options['field']); + } else { + $this->query->removeOption('field'); + } + + return $this->fetch($sql); + } + + /** + * 插入记录 + * @access public + * @param array $data 数据 + * @return string + */ + public function insert(array $data = []): string + { + $options = $this->query->parseOptions(); + + if (!empty($data)) { + $this->query->setOption('data', $data); + } + + $sql = $this->builder->insert($this->query); + + return $this->fetch($sql); + } + + /** + * 插入记录并获取自增ID + * @access public + * @param array $data 数据 + * @return string + */ + public function insertGetId(array $data = []): string + { + return $this->insert($data); + } + + /** + * 保存数据 自动判断insert或者update + * @access public + * @param array $data 数据 + * @param bool $forceInsert 是否强制insert + * @return string + */ + public function save(array $data = [], bool $forceInsert = false): string + { + if ($forceInsert) { + return $this->insert($data); + } + + $data = array_merge($this->query->getOptions('data') ?: [], $data); + + $this->query->setOption('data', $data); + + if ($this->query->getOptions('where')) { + $isUpdate = true; + } else { + $isUpdate = $this->query->parseUpdateData($data); + } + + return $isUpdate ? $this->update() : $this->insert(); + } + + /** + * 批量插入记录 + * @access public + * @param array $dataSet 数据集 + * @param integer $limit 每次写入数据限制 + * @return string + */ + public function insertAll(array $dataSet = [], int $limit = null): string + { + $options = $this->query->parseOptions(); + + if (empty($dataSet)) { + $dataSet = $options['data']; + } + + if (empty($limit) && !empty($options['limit'])) { + $limit = $options['limit']; + } + + if ($limit) { + $array = array_chunk($dataSet, $limit, true); + $fetchSql = []; + foreach ($array as $item) { + $sql = $this->builder->insertAll($this->query, $item); + $bind = $this->query->getBind(); + + $fetchSql[] = $this->connection->getRealSql($sql, $bind); + } + + return implode(';', $fetchSql); + } + + $sql = $this->builder->insertAll($this->query, $dataSet); + + return $this->fetch($sql); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param array $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return string + */ + public function selectInsert(array $fields, string $table): string + { + $this->query->parseOptions(); + + $sql = $this->builder->selectInsert($this->query, $fields, $table); + + return $this->fetch($sql); + } + + /** + * 更新记录 + * @access public + * @param mixed $data 数据 + * @return string + */ + public function update(array $data = []): string + { + $options = $this->query->parseOptions(); + + $data = !empty($data) ? $data : $options['data']; + + $pk = $this->query->getPk(); + + if (empty($options['where'])) { + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $this->query->where($pk, '=', $data[$pk]); + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $this->query->where($field, '=', $data[$field]); + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss complex primary data'); + } + unset($data[$field]); + } + } + + if (empty($this->query->getOptions('where'))) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } + } + + // 更新数据 + $this->query->setOption('data', $data); + + // 生成UPDATE SQL语句 + $sql = $this->builder->update($this->query); + + return $this->fetch($sql); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return string + */ + public function delete($data = null): string + { + $options = $this->query->parseOptions(); + + if (!is_null($data) && true !== $data) { + // AR模式分析主键条件 + $this->query->parsePkWhere($data); + } + + if (!empty($options['soft_delete'])) { + // 软删除 + [$field, $condition] = $options['soft_delete']; + if ($condition) { + $this->query->setOption('soft_delete', null); + $this->query->setOption('data', [$field => $condition]); + // 生成删除SQL语句 + $sql = $this->builder->delete($this->query); + return $this->fetch($sql); + } + } + + // 生成删除SQL语句 + $sql = $this->builder->delete($this->query); + + return $this->fetch($sql); + } + + /** + * 查找记录 返回SQL + * @access public + * @param mixed $data + * @return string + */ + public function select($data = null): string + { + $this->query->parseOptions(); + + if (!is_null($data)) { + // 主键条件分析 + $this->query->parsePkWhere($data); + } + + // 生成查询SQL + $sql = $this->builder->select($this->query); + + return $this->fetch($sql); + } + + /** + * 查找单条记录 返回SQL语句 + * @access public + * @param mixed $data + * @return string + */ + public function find($data = null): string + { + $this->query->parseOptions(); + + if (!is_null($data)) { + // AR模式分析主键条件 + $this->query->parsePkWhere($data); + } + + // 生成查询SQL + $sql = $this->builder->select($this->query, true); + + // 获取实际执行的SQL语句 + return $this->fetch($sql); + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param mixed $data + * @return string + */ + public function selectOrFail($data = null): string + { + return $this->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param mixed $data + * @return string + */ + public function findOrFail($data = null): string + { + return $this->find($data); + } + + /** + * 查找单条记录 不存在返回空数据(或者空模型) + * @access public + * @param mixed $data 数据 + * @return string + */ + public function findOrEmpty($data = null) + { + return $this->find($data); + } + + /** + * 获取实际的SQL语句 + * @access public + * @param string $sql + * @return string + */ + public function fetch(string $sql): string + { + $bind = $this->query->getBind(); + + return $this->connection->getRealSql($sql, $bind); + } + + /** + * COUNT查询 + * @access public + * @param string $field 字段名 + * @return string + */ + public function count(string $field = '*'): string + { + $options = $this->query->parseOptions(); + + if (!empty($options['group'])) { + // 支持GROUP + $subSql = $this->query->field('count(' . $field . ') AS think_count')->buildSql(); + $query = $this->query->newQuery()->table([$subSql => '_group_count_']); + + return $query->fetchsql()->aggregate('COUNT', '*'); + } else { + return $this->aggregate('COUNT', $field); + } + } + + /** + * SUM查询 + * @access public + * @param string $field 字段名 + * @return string + */ + public function sum(string $field): string + { + return $this->aggregate('SUM', $field); + } + + /** + * MIN查询 + * @access public + * @param string $field 字段名 + * @return string + */ + public function min(string $field): string + { + return $this->aggregate('MIN', $field); + } + + /** + * MAX查询 + * @access public + * @param string $field 字段名 + * @return string + */ + public function max(string $field): string + { + return $this->aggregate('MAX', $field); + } + + /** + * AVG查询 + * @access public + * @param string $field 字段名 + * @return string + */ + public function avg(string $field): string + { + return $this->aggregate('AVG', $field); + } + + public function __call($method, $args) + { + if (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Str::snake(substr($method, 5)); + return $this->where($field, '=', $args[0])->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Str::snake(substr($method, 10)); + return $this->where($name, '=', $args[0])->value($args[1]); + } + + $result = call_user_func_array([$this->query, $method], $args); + return $result === $this->query ? $this : $result; + } +} diff --git a/vendor/topthink/think-orm/src/db/Mongo.php b/vendor/topthink/think-orm/src/db/Mongo.php new file mode 100644 index 0000000..cf6e9c4 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/Mongo.php @@ -0,0 +1,708 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); +namespace think\db; + +use MongoDB\Driver\Command; +use MongoDB\Driver\Cursor; +use MongoDB\Driver\Exception\AuthenticationException; +use MongoDB\Driver\Exception\ConnectionException; +use MongoDB\Driver\Exception\InvalidArgumentException; +use MongoDB\Driver\Exception\RuntimeException; +use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; +use think\db\exception\DbException as Exception; +use think\Paginator; + +class Mongo extends BaseQuery +{ + /** + * 当前数据库连接对象 + * @var \think\db\connector\Mongo + */ + protected $connection; + + /** + * 执行指令 返回数据集 + * @access public + * @param Command $command 指令 + * @param string $dbName + * @param ReadPreference $readPreference readPreference + * @param string|array $typeMap 指定返回的typeMap + * @return mixed + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null) + { + return $this->connection->command($command, $dbName, $readPreference, $typeMap); + } + + /** + * 执行command + * @access public + * @param string|array|object $command 指令 + * @param mixed $extra 额外参数 + * @param string $db 数据库名 + * @return array + */ + public function cmd($command, $extra = null, string $db = ''): array + { + $this->parseOptions(); + return $this->connection->cmd($this, $command, $extra, $db); + } + + /** + * 指定distinct查询 + * @access public + * @param string $field 字段名 + * @return array + */ + public function getDistinct(string $field) + { + $result = $this->cmd('distinct', $field); + return $result[0]['values']; + } + + /** + * 获取数据库的所有collection + * @access public + * @param string $db 数据库名称 留空为当前数据库 + * @throws Exception + */ + public function listCollections(string $db = '') + { + $cursor = $this->cmd('listCollections', null, $db); + $result = []; + foreach ($cursor as $collection) { + $result[] = $collection['name']; + } + + return $result; + } + + /** + * COUNT查询 + * @access public + * @param string $field 字段名 + * @return integer + */ + public function count(string $field = null): int + { + $result = $this->cmd('count'); + + return $result[0]['n']; + } + + /** + * 聚合查询 + * @access public + * @param string $aggregate 聚合指令 + * @param string $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate(string $aggregate, $field, bool $force = false) + { + $result = $this->cmd('aggregate', [strtolower($aggregate), $field]); + $value = $result[0]['aggregate'] ?? 0; + + if ($force) { + $value += 0; + } + + return $value; + } + + /** + * 多聚合操作 + * + * @param array $aggregate 聚合指令, 可以聚合多个参数, 如 ['sum' => 'field1', 'avg' => 'field2'] + * @param array $groupBy 类似mysql里面的group字段, 可以传入多个字段, 如 ['field_a', 'field_b', 'field_c'] + * @return array 查询结果 + */ + public function multiAggregate(array $aggregate, array $groupBy): array + { + $result = $this->cmd('multiAggregate', [$aggregate, $groupBy]); + + foreach ($result as &$row) { + if (isset($row['_id']) && !empty($row['_id'])) { + foreach ($row['_id'] as $k => $v) { + $row[$k] = $v; + } + unset($row['_id']); + } + } + + return $result; + } + + /** + * 字段值增长 + * @access public + * @param string $field 字段名 + * @param float $step 增长值 + * @return $this + */ + public function inc(string $field, float $step = 1) + { + $this->options['data'][$field] = ['$inc', $step]; + + return $this; + } + + /** + * 字段值减少 + * @access public + * @param string $field 字段名 + * @param float $step 减少值 + * @return $this + */ + public function dec(string $field, float $step = 1) + { + return $this->inc($field, -1 * $step); + } + + /** + * 指定当前操作的Collection + * @access public + * @param string $table 表名 + * @return $this + */ + public function table($table) + { + $this->options['table'] = $table; + + return $this; + } + + /** + * table方法的别名 + * @access public + * @param string $collection + * @return $this + */ + public function collection(string $collection) + { + return $this->table($collection); + } + + /** + * 设置typeMap + * @access public + * @param string|array $typeMap + * @return $this + */ + public function typeMap($typeMap) + { + $this->options['typeMap'] = $typeMap; + return $this; + } + + /** + * awaitData + * @access public + * @param bool $awaitData + * @return $this + */ + public function awaitData(bool $awaitData) + { + $this->options['awaitData'] = $awaitData; + return $this; + } + + /** + * batchSize + * @access public + * @param integer $batchSize + * @return $this + */ + public function batchSize(int $batchSize) + { + $this->options['batchSize'] = $batchSize; + return $this; + } + + /** + * exhaust + * @access public + * @param bool $exhaust + * @return $this + */ + public function exhaust(bool $exhaust) + { + $this->options['exhaust'] = $exhaust; + return $this; + } + + /** + * 设置modifiers + * @access public + * @param array $modifiers + * @return $this + */ + public function modifiers(array $modifiers) + { + $this->options['modifiers'] = $modifiers; + return $this; + } + + /** + * 设置noCursorTimeout + * @access public + * @param bool $noCursorTimeout + * @return $this + */ + public function noCursorTimeout(bool $noCursorTimeout) + { + $this->options['noCursorTimeout'] = $noCursorTimeout; + return $this; + } + + /** + * 设置oplogReplay + * @access public + * @param bool $oplogReplay + * @return $this + */ + public function oplogReplay(bool $oplogReplay) + { + $this->options['oplogReplay'] = $oplogReplay; + return $this; + } + + /** + * 设置partial + * @access public + * @param bool $partial + * @return $this + */ + public function partial(bool $partial) + { + $this->options['partial'] = $partial; + return $this; + } + + /** + * maxTimeMS + * @access public + * @param string $maxTimeMS + * @return $this + */ + public function maxTimeMS(string $maxTimeMS) + { + $this->options['maxTimeMS'] = $maxTimeMS; + return $this; + } + + /** + * collation + * @access public + * @param array $collation + * @return $this + */ + public function collation(array $collation) + { + $this->options['collation'] = $collation; + return $this; + } + + /** + * 设置是否REPLACE + * @access public + * @param bool $replace 是否使用REPLACE写入数据 + * @return $this + */ + public function replace(bool $replace = true) + { + return $this; + } + + /** + * 设置返回字段 + * @access public + * @param mixed $field 字段信息 + * @return $this + */ + public function field($field) + { + if (empty($field) || '*' == $field) { + return $this; + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $projection = []; + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $projection[$val] = 1; + } else { + $projection[$key] = $val; + } + } + + $this->options['projection'] = $projection; + + return $this; + } + + /** + * 指定要排除的查询字段 + * @access public + * @param array|string $field 要排除的字段 + * @return $this + */ + public function withoutField($field) + { + if (empty($field) || '*' == $field) { + return $this; + } + + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + + $projection = []; + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $projection[$val] = 0; + } else { + $projection[$key] = $val; + } + } + + $this->options['projection'] = $projection; + return $this; + } + + /** + * 设置skip + * @access public + * @param integer $skip + * @return $this + */ + public function skip(int $skip) + { + $this->options['skip'] = $skip; + return $this; + } + + /** + * 设置slaveOk + * @access public + * @param bool $slaveOk + * @return $this + */ + public function slaveOk(bool $slaveOk) + { + $this->options['slaveOk'] = $slaveOk; + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param int $offset 起始位置 + * @param int $length 查询数量 + * @return $this + */ + public function limit(int $offset, int $length = null) + { + if (is_null($length)) { + $length = $offset; + $offset = 0; + } + + $this->options['skip'] = $offset; + $this->options['limit'] = $length; + + return $this; + } + + /** + * 设置sort + * @access public + * @param array|string $field + * @param string $order + * @return $this + */ + public function order($field, string $order = '') + { + if (is_array($field)) { + $this->options['sort'] = $field; + } else { + $this->options['sort'][$field] = 'asc' == strtolower($order) ? 1 : -1; + } + return $this; + } + + /** + * 设置tailable + * @access public + * @param bool $tailable + * @return $this + */ + public function tailable(bool $tailable) + { + $this->options['tailable'] = $tailable; + return $this; + } + + /** + * 设置writeConcern对象 + * @access public + * @param WriteConcern $writeConcern + * @return $this + */ + public function writeConcern(WriteConcern $writeConcern) + { + $this->options['writeConcern'] = $writeConcern; + return $this; + } + + /** + * 获取当前数据表的主键 + * @access public + * @return string|array + */ + public function getPk() + { + return $this->pk ?: $this->connection->getConfig('pk'); + } + + /** + * 执行查询但只返回Cursor对象 + * @access public + * @return Cursor + */ + public function getCursor(): Cursor + { + $this->parseOptions(); + + return $this->connection->getCursor($this); + } + + /** + * 获取当前的查询标识 + * @access public + * @param mixed $data 要序列化的数据 + * @return string + */ + public function getQueryGuid($data = null): string + { + return md5($this->getConfig('database') . serialize(var_export($data ?: $this->options, true))); + } + + /** + * 分页查询 + * @access public + * @param int|array $listRows 每页数量 数组表示配置参数 + * @param int|bool $simple 是否简洁模式或者总记录数 + * @return Paginator + * @throws Exception + */ + public function paginate($listRows = null, $simple = false): Paginator + { + if (is_int($simple)) { + $total = $simple; + $simple = false; + } + + $defaultConfig = [ + 'query' => [], //url额外参数 + 'fragment' => '', //url锚点 + 'var_page' => 'page', //分页变量 + 'list_rows' => 15, //每页数量 + ]; + + if (is_array($listRows)) { + $config = array_merge($defaultConfig, $listRows); + $listRows = intval($config['list_rows']); + } else { + $config = $defaultConfig; + $listRows = intval($listRows ?: $config['list_rows']); + } + + $page = isset($config['page']) ? (int) $config['page'] : Paginator::getCurrentPage($config['var_page']); + + $page = $page < 1 ? 1 : $page; + + $config['path'] = $config['path'] ?? Paginator::getCurrentPath(); + + if (!isset($total) && !$simple) { + $options = $this->getOptions(); + + unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']); + + $total = $this->count(); + $results = $this->options($options)->page($page, $listRows)->select(); + } elseif ($simple) { + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; + } else { + $results = $this->page($page, $listRows)->select(); + } + + $this->removeOption('limit'); + $this->removeOption('page'); + + return Paginator::make($results, $listRows, $page, $total, $simple, $config); + } + + /** + * 分批数据返回处理 + * @access public + * @param integer $count 每次处理的数据数量 + * @param callable $callback 处理回调方法 + * @param string|array $column 分批处理的字段名 + * @param string $order 字段排序 + * @return bool + * @throws Exception + */ + public function chunk(int $count, callable $callback, $column = null, string $order = 'asc'): bool + { + $options = $this->getOptions(); + $column = $column ?: $this->getPk(); + + if (isset($options['order'])) { + unset($options['order']); + } + + if (is_array($column)) { + $times = 1; + $query = $this->options($options)->page($times, $count); + } else { + $query = $this->options($options)->limit($count); + + if (strpos($column, '.')) { + [$alias, $key] = explode('.', $column); + } else { + $key = $column; + } + } + + $resultSet = $query->order($column, $order)->select(); + + while (count($resultSet) > 0) { + if (false === call_user_func($callback, $resultSet)) { + return false; + } + + if (isset($times)) { + $times++; + $query = $this->options($options)->page($times, $count); + } else { + $end = $resultSet->pop(); + $lastId = is_array($end) ? $end[$key] : $end->getData($key); + + $query = $this->options($options) + ->limit($count) + ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId); + } + + $resultSet = $query->order($column, $order)->select(); + } + + return true; + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access public + * @return array + */ + public function parseOptions(): array + { + $options = $this->options; + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + + foreach (['where', 'data', 'projection', 'filter', 'json', 'with_attr', 'with_relation_attr'] as $name) { + if (!isset($options[$name])) { + $options[$name] = []; + } + } + + $modifiers = empty($options['modifiers']) ? [] : $options['modifiers']; + if (isset($options['comment'])) { + $modifiers['$comment'] = $options['comment']; + } + + if (isset($options['maxTimeMS'])) { + $modifiers['$maxTimeMS'] = $options['maxTimeMS']; + } + + if (!empty($modifiers)) { + $options['modifiers'] = $modifiers; + } + + if (!isset($options['typeMap'])) { + $options['typeMap'] = $this->getConfig('type_map'); + } + + if (!isset($options['limit'])) { + $options['limit'] = 0; + } + + foreach (['master', 'fetch_sql', 'fetch_cursor'] as $name) { + if (!isset($options[$name])) { + $options[$name] = false; + } + } + + if (isset($options['page'])) { + // 根据页数计算limit + [$page, $listRows] = $options['page']; + + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['skip'] = intval($offset); + $options['limit'] = intval($listRows); + } + + $this->options = $options; + + return $options; + } + + /** + * 获取字段类型信息 + * @access public + * @return array + */ + public function getFieldsType(): array + { + if (!empty($this->options['field_type'])) { + return $this->options['field_type']; + } + + return []; + } + + /** + * 获取字段类型信息 + * @access public + * @param string $field 字段名 + * @return string|null + */ + public function getFieldType(string $field) + { + $fieldType = $this->getFieldsType(); + + return $fieldType[$field] ?? null; + } +} diff --git a/vendor/topthink/think-orm/src/db/PDOConnection.php b/vendor/topthink/think-orm/src/db/PDOConnection.php new file mode 100644 index 0000000..2755040 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/PDOConnection.php @@ -0,0 +1,1868 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace think\db; + +use Closure; +use PDO; +use PDOStatement; +use think\db\exception\BindParamException; +use think\db\exception\DbEventException; +use think\db\exception\DbException; +use think\db\exception\PDOException; +use think\Model; + +/** + * 数据库连接基础类 + * @property PDO[] $links + * @property PDO $linkID + * @property PDO $linkRead + * @property PDO $linkWrite + */ +abstract class PDOConnection extends Connection +{ + const PARAM_FLOAT = 21; + + /** + * 数据库连接参数配置 + * @var array + */ + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 模型写入后自动读取主服务器 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 开启字段缓存 + 'fields_cache' => false, + // 监听SQL + 'trigger_sql' => true, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '', + // 是否需要断线重连 + 'break_reconnect' => false, + // 断线标识字符串 + 'break_match_str' => [], + // 自动参数绑定 + 'auto_param_bind' => true, + ]; + + /** + * PDO操作实例 + * @var PDOStatement + */ + protected $PDOStatement; + + /** + * 当前SQL指令 + * @var string + */ + protected $queryStr = ''; + + /** + * 事务指令数 + * @var int + */ + protected $transTimes = 0; + + /** + * 重连次数 + * @var int + */ + protected $reConnectTimes = 0; + + /** + * 查询结果类型 + * @var int + */ + protected $fetchType = PDO::FETCH_ASSOC; + + /** + * 字段属性大小写 + * @var int + */ + protected $attrCase = PDO::CASE_LOWER; + + /** + * 数据表信息 + * @var array + */ + protected $info = []; + + /** + * 查询开始时间 + * @var float + */ + protected $queryStartTime; + + /** + * PDO连接参数 + * @var array + */ + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + /** + * 参数绑定类型映射 + * @var array + */ + protected $bindType = [ + 'string' => PDO::PARAM_STR, + 'str' => PDO::PARAM_STR, + 'integer' => PDO::PARAM_INT, + 'int' => PDO::PARAM_INT, + 'boolean' => PDO::PARAM_BOOL, + 'bool' => PDO::PARAM_BOOL, + 'float' => self::PARAM_FLOAT, + 'datetime' => PDO::PARAM_STR, + 'timestamp' => PDO::PARAM_STR, + ]; + + /** + * 服务器断线标识字符 + * @var array + */ + protected $breakMatchStr = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'failed with errno', + 'child connection forced to terminate due to client_idle_limit', + 'query_wait_timeout', + 'reset by peer', + 'Physical connection is not usable', + 'TCP Provider: Error code 0x68', + 'ORA-03114', + 'Packets out of order. Expected', + 'Adaptive Server connection failed', + 'Communication link failure', + 'connection is no longer usable', + 'Login timeout expired', + 'SQLSTATE[HY000] [2002] Connection refused', + 'running with the --read-only option so it cannot execute this statement', + 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', + 'SQLSTATE[HY000] [2002] Connection timed out', + 'SSL: Connection timed out', + 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', + ]; + + /** + * 绑定参数 + * @var array + */ + protected $bind = []; + + /** + * 获取当前连接器类对应的Query类 + * @access public + * @return string + */ + public function getQueryClass(): string + { + return $this->getConfig('query') ?: Query::class; + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilderClass(): string + { + return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type')); + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + abstract protected function parseDsn(array $config): string; + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName 数据表名称 + * @return array + */ + abstract public function getFields(string $tableName): array; + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName 数据库名称 + * @return array + */ + abstract public function getTables(string $dbName = ''): array; + + /** + * 对返数据表字段信息进行大小写转换出来 + * @access public + * @param array $info 字段信息 + * @return array + */ + public function fieldCase(array $info): array + { + // 字段大小写转换 + switch ($this->attrCase) { + case PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case PDO::CASE_NATURAL: + default: + // 不做转换 + } + + return $info; + } + + /** + * 获取字段类型 + * @access protected + * @param string $type 字段类型 + * @return string + */ + protected function getFieldType(string $type): string + { + if (0 === stripos($type, 'set') || 0 === stripos($type, 'enum')) { + $result = 'string'; + } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) { + $result = 'float'; + } elseif (preg_match('/(int|serial|bit)/is', $type)) { + $result = 'int'; + } elseif (preg_match('/bool/is', $type)) { + $result = 'bool'; + } elseif (0 === stripos($type, 'timestamp')) { + $result = 'timestamp'; + } elseif (0 === stripos($type, 'datetime')) { + $result = 'datetime'; + } elseif (0 === stripos($type, 'date')) { + $result = 'date'; + } else { + $result = 'string'; + } + + return $result; + } + + /** + * 获取字段绑定类型 + * @access public + * @param string $type 字段类型 + * @return integer + */ + public function getFieldBindType(string $type): int + { + if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) { + $bind = $this->bindType[$type]; + } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $bind = PDO::PARAM_STR; + } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) { + $bind = self::PARAM_FLOAT; + } elseif (preg_match('/(int|serial|bit)/is', $type)) { + $bind = PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $type)) { + $bind = PDO::PARAM_BOOL; + } else { + $bind = PDO::PARAM_STR; + } + + return $bind; + } + + /** + * 获取数据表信息缓存key + * @access protected + * @param string $schema 数据表名称 + * @return string + */ + protected function getSchemaCacheKey(string $schema): string + { + return $this->getConfig('hostname') . ':' . $this->getConfig('hostport') . '@' . $schema; + } + + /** + * @param string $tableName 数据表名称 + * @param bool $force 强制从数据库获取 + * @return array + */ + public function getSchemaInfo(string $tableName, $force = false) + { + if (!strpos($tableName, '.')) { + $schema = $this->getConfig('database') . '.' . $tableName; + } else { + $schema = $tableName; + } + + if (!isset($this->info[$schema]) || $force) { + // 读取字段缓存 + $cacheKey = $this->getSchemaCacheKey($schema); + $cacheField = $this->config['fields_cache'] && !empty($this->cache); + + if ($cacheField && !$force) { + $info = $this->cache->get($cacheKey); + } + + if (empty($info)) { + $info = $this->getTableFieldsInfo($tableName); + if ($cacheField) { + $this->cache->set($cacheKey, $info); + } + } + + $pk = $info['_pk'] ?? null; + $autoinc = $info['_autoinc'] ?? null; + unset($info['_pk'], $info['_autoinc']); + + $bind = []; + foreach ($info as $name => $val) { + $bind[$name] = $this->getFieldBindType($val); + } + + $this->info[$schema] = [ + 'fields' => array_keys($info), + 'type' => $info, + 'bind' => $bind, + 'pk' => $pk, + 'autoinc' => $autoinc, + ]; + } + + return $this->info[$schema]; + } + + /** + * 获取数据表信息 + * @access public + * @param mixed $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @return mixed + */ + public function getTableInfo($tableName, string $fetch = '') + { + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + + if (strpos($tableName, ',') || strpos($tableName, ')')) { + // 多表不获取字段信息 + return []; + } + + [$tableName] = explode(' ', $tableName); + + $info = $this->getSchemaInfo($tableName); + + return $fetch ? $info[$fetch] : $info; + } + + /** + * 获取数据表的字段信息 + * @access public + * @param string $tableName 数据表名 + * @return array + */ + public function getTableFieldsInfo(string $tableName): array + { + $fields = $this->getFields($tableName); + $info = []; + + foreach ($fields as $key => $val) { + // 记录字段类型 + $info[$key] = $this->getFieldType($val['type']); + + if (!empty($val['primary'])) { + $pk[] = $key; + } + + if (!empty($val['autoinc'])) { + $autoinc = $key; + } + } + + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + $info['_pk'] = $pk; + } + + if (isset($autoinc)) { + $info['_autoinc'] = $autoinc; + } + + return $info; + } + + /** + * 获取数据表的主键 + * @access public + * @param mixed $tableName 数据表名 + * @return string|array + */ + public function getPk($tableName) + { + return $this->getTableInfo($tableName, 'pk'); + } + + /** + * 获取数据表的自增主键 + * @access public + * @param mixed $tableName 数据表名 + * @return string + */ + public function getAutoInc($tableName) + { + return $this->getTableInfo($tableName, 'autoinc'); + } + + /** + * 获取数据表字段信息 + * @access public + * @param mixed $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName): array + { + return $this->getTableInfo($tableName, 'fields'); + } + + /** + * 获取数据表字段类型 + * @access public + * @param mixed $tableName 数据表名 + * @param string $field 字段名 + * @return array|string + */ + public function getFieldsType($tableName, string $field = null) + { + $result = $this->getTableInfo($tableName, 'type'); + + if ($field && isset($result[$field])) { + return $result[$field]; + } + + return $result; + } + + /** + * 获取数据表绑定信息 + * @access public + * @param mixed $tableName 数据表名 + * @return array + */ + public function getFieldsBind($tableName): array + { + return $this->getTableInfo($tableName, 'bind'); + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @return PDO + * @throws PDOException + */ + public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO + { + if (isset($this->links[$linkNum])) { + return $this->links[$linkNum]; + } + + if (empty($config)) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + + // 连接参数 + if (isset($config['params']) && is_array($config['params'])) { + $params = $config['params'] + $this->params; + } else { + $params = $this->params; + } + + // 记录当前字段属性大小写设置 + $this->attrCase = $params[PDO::ATTR_CASE]; + + if (!empty($config['break_match_str'])) { + $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']); + } + + try { + if (empty($config['dsn'])) { + $config['dsn'] = $this->parseDsn($config); + } + + $startTime = microtime(true); + + $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params); + + // SQL监控 + if (!empty($config['trigger_sql'])) { + $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']); + } + + return $this->links[$linkNum]; + } catch (\PDOException $e) { + if ($autoConnection) { + $this->db->log($e->getMessage(), 'error'); + return $this->connect($autoConnection, $linkNum); + } else { + throw $e; + } + } + } + + /** + * 视图查询 + * @access public + * @param array $args + * @return BaseQuery + */ + public function view(...$args) + { + return $this->newQuery()->view(...$args); + } + + /** + * 创建PDO实例 + * @param $dsn + * @param $username + * @param $password + * @param $params + * @return PDO + */ + protected function createPdo($dsn, $username, $password, $params) + { + return new PDO($dsn, $username, $password, $params); + } + + /** + * 释放查询结果 + * @access public + */ + public function free(): void + { + $this->PDOStatement = null; + } + + /** + * 获取PDO对象 + * @access public + * @return PDO|false + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } + + return $this->linkID; + } + + /** + * 执行查询 使用生成器返回数据 + * @access public + * @param BaseQuery $query 查询对象 + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param Model|null $model 模型对象实例 + * @param null $condition 查询条件 + * @return \Generator + * @throws DbException + */ + public function getCursor(BaseQuery $query, string $sql, array $bind = [], $model = null, $condition = null) + { + $this->queryPDOStatement($query, $sql, $bind); + + // 返回结果集 + while ($result = $this->PDOStatement->fetch($this->fetchType)) { + if ($model) { + yield $model->newInstance($result, $condition); + } else { + yield $result; + } + } + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 主库读取 + * @return array + * @throws DbException + */ + public function query(string $sql, array $bind = [], bool $master = false): array + { + return $this->pdoQuery($this->newQuery(), $sql, $bind, $master); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws DbException + */ + public function execute(string $sql, array $bind = []): int + { + return $this->pdoExecute($this->newQuery(), $sql, $bind, true); + } + + /** + * 执行查询 返回数据集 + * @access protected + * @param BaseQuery $query 查询对象 + * @param mixed $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 主库读取 + * @return array + * @throws DbException + */ + protected function pdoQuery(BaseQuery $query, $sql, array $bind = [], bool $master = null): array + { + // 分析查询表达式 + $query->parseOptions(); + + if ($query->getOptions('cache')) { + // 检查查询缓存 + $cacheItem = $this->parseCache($query, $query->getOptions('cache')); + $key = $cacheItem->getKey(); + + $data = $this->cache->get($key); + + if (null !== $data) { + return $data; + } + } + + if ($sql instanceof Closure) { + $sql = $sql($query); + $bind = $query->getBind(); + } + + if (!isset($master)) { + $master = $query->getOptions('master') ? true : false; + } + + $procedure = $query->getOptions('procedure') ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + $this->getPDOStatement($sql, $bind, $master, $procedure); + + $resultSet = $this->getResult($procedure); + $requireCache = $query->getOptions('cache_always') || !empty($resultSet); + + if (isset($cacheItem) && $requireCache) { + // 缓存数据集 + $cacheItem->set($resultSet); + $this->cacheData($cacheItem); + } + + return $resultSet; + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @param BaseQuery $query 查询对象 + * @return \PDOStatement + * @throws DbException + */ + public function pdo(BaseQuery $query): PDOStatement + { + $bind = $query->getBind(); + // 生成查询SQL + $sql = $this->builder->select($query); + + return $this->queryPDOStatement($query, $sql, $bind); + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $master 是否在主服务器读操作 + * @param bool $procedure 是否为存储过程调用 + * @return PDOStatement + * @throws DbException + */ + public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement + { + try { + $this->initConnect($this->readMaster ?: $master); + // 记录SQL语句 + $this->queryStr = $sql; + $this->bind = $bind; + + $this->db->updateQueryTimes(); + $this->queryStartTime = microtime(true); + + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } + + // 执行查询 + $this->PDOStatement->execute(); + + // SQL监控 + if (!empty($this->config['trigger_sql'])) { + $this->trigger('', $master); + } + + $this->reConnectTimes = 0; + + return $this->PDOStatement; + } catch (\Throwable | \Exception $e) { + if ($this->transTimes > 0) { + // 事务活动中时不应该进行重试,应直接中断执行,防止造成污染。 + if ($this->isBreak($e)) { + // 尝试对事务计数进行重置 + $this->transTimes = 0; + } + } else { + if ($this->reConnectTimes < 4 && $this->isBreak($e)) { + ++$this->reConnectTimes; + return $this->close()->getPDOStatement($sql, $bind, $master, $procedure); + } + } + + if ($e instanceof \PDOException) { + throw new PDOException($e, $this->config, $this->getLastsql()); + } else { + throw $e; + } + } + } + + /** + * 执行语句 + * @access protected + * @param BaseQuery $query 查询对象 + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param bool $origin 是否原生查询 + * @return int + * @throws DbException + */ + protected function pdoExecute(BaseQuery $query, string $sql, array $bind = [], bool $origin = false): int + { + if ($origin) { + $query->parseOptions(); + } + + $this->queryPDOStatement($query->master(true), $sql, $bind); + + if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) { + $this->readMaster = true; + } + + $this->numRows = $this->PDOStatement->rowCount(); + + if ($query->getOptions('cache')) { + // 清理缓存数据 + $cacheItem = $this->parseCache($query, $query->getOptions('cache')); + $key = $cacheItem->getKey(); + $tag = $cacheItem->getTag(); + + if (isset($key) && $this->cache->has($key)) { + $this->cache->delete($key); + } elseif (!empty($tag) && method_exists($this->cache, 'tag')) { + $this->cache->tag($tag)->clear(); + } + } + + return $this->numRows; + } + + /** + * @param BaseQuery $query + * @param string $sql + * @param array $bind + * @return PDOStatement + * @throws DbException + */ + protected function queryPDOStatement(BaseQuery $query, string $sql, array $bind = []): PDOStatement + { + $options = $query->getOptions(); + $master = !empty($options['master']) ? true : false; + $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + + return $this->getPDOStatement($sql, $bind, $master, $procedure); + } + + /** + * 查找单条记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return array + * @throws DbException + */ + public function find(BaseQuery $query): array + { + // 事件回调 + try { + $this->db->trigger('before_find', $query); + } catch (DbEventException $e) { + return []; + } + + // 执行查询 + $resultSet = $this->pdoQuery($query, function ($query) { + return $this->builder->select($query, true); + }); + + return $resultSet[0] ?? []; + } + + /** + * 使用游标查询记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return \Generator + */ + public function cursor(BaseQuery $query) + { + // 分析查询表达式 + $options = $query->parseOptions(); + + // 生成查询SQL + $sql = $this->builder->select($query); + + $condition = $options['where']['AND'] ?? null; + + // 执行查询操作 + return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition); + } + + /** + * 查找记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return array + * @throws DbException + */ + public function select(BaseQuery $query): array + { + try { + $this->db->trigger('before_select', $query); + } catch (DbEventException $e) { + return []; + } + + // 执行查询操作 + return $this->pdoQuery($query, function ($query) { + return $this->builder->select($query); + }); + } + + /** + * 插入记录 + * @access public + * @param BaseQuery $query 查询对象 + * @param boolean $getLastInsID 返回自增主键 + * @return mixed + */ + public function insert(BaseQuery $query, bool $getLastInsID = false) + { + // 分析查询表达式 + $options = $query->parseOptions(); + + // 生成SQL语句 + $sql = $this->builder->insert($query); + + // 执行操作 + $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind()); + + if ($result) { + $sequence = $options['sequence'] ?? null; + $lastInsId = $this->getLastInsID($query, $sequence); + + $data = $options['data']; + + if ($lastInsId) { + $pk = $query->getAutoInc(); + if ($pk) { + $data[$pk] = $lastInsId; + } + } + + $query->setOption('data', $data); + + $this->db->trigger('after_insert', $query); + + if ($getLastInsID && $lastInsId) { + return $lastInsId; + } + } + + return $result; + } + + /** + * 批量插入记录 + * @access public + * @param BaseQuery $query 查询对象 + * @param mixed $dataSet 数据集 + * @param integer $limit 每次写入数据限制 + * @return integer + * @throws \Exception + * @throws \Throwable + */ + public function insertAll(BaseQuery $query, array $dataSet = [], int $limit = 0): int + { + if (!is_array(reset($dataSet))) { + return 0; + } + + $options = $query->parseOptions(); + $replace = !empty($options['replace']); + + if (0 === $limit && count($dataSet) >= 5000) { + $limit = 1000; + } + + if ($limit) { + // 分批写入 自动启动事务支持 + $this->startTrans(); + + try { + $array = array_chunk($dataSet, $limit, true); + $count = 0; + + foreach ($array as $item) { + $sql = $this->builder->insertAll($query, $item, $replace); + $count += $this->pdoExecute($query, $sql, $query->getBind()); + } + + // 提交事务 + $this->commit(); + } catch (\Exception | \Throwable $e) { + $this->rollback(); + throw $e; + } + + return $count; + } + + $sql = $this->builder->insertAll($query, $dataSet, $replace); + + return $this->pdoExecute($query, $sql, $query->getBind()); + } + + /** + * 通过Select方式插入记录 + * @access public + * @param BaseQuery $query 查询对象 + * @param array $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer + * @throws PDOException + */ + public function selectInsert(BaseQuery $query, array $fields, string $table): int + { + // 分析查询表达式 + $query->parseOptions(); + + $sql = $this->builder->selectInsert($query, $fields, $table); + + return $this->pdoExecute($query, $sql, $query->getBind()); + } + + /** + * 更新记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return integer + * @throws PDOException + */ + public function update(BaseQuery $query): int + { + $query->parseOptions(); + + // 生成UPDATE SQL语句 + $sql = $this->builder->update($query); + + // 执行操作 + $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind()); + + if ($result) { + $this->db->trigger('after_update', $query); + } + + return $result; + } + + /** + * 删除记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return int + * @throws PDOException + */ + public function delete(BaseQuery $query): int + { + // 分析查询表达式 + $query->parseOptions(); + + // 生成删除SQL语句 + $sql = $this->builder->delete($query); + + // 执行操作 + $result = $this->pdoExecute($query, $sql, $query->getBind()); + + if ($result) { + $this->db->trigger('after_delete', $query); + } + + return $result; + } + + /** + * 得到某个字段的值 + * @access public + * @param BaseQuery $query 查询对象 + * @param string $field 字段名 + * @param mixed $default 默认值 + * @param bool $one 返回一个值 + * @return mixed + */ + public function value(BaseQuery $query, string $field, $default = null, bool $one = true) + { + $options = $query->parseOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (isset($options['group'])) { + $query->group(''); + } + + $query->setOption('field', (array) $field); + + if (!empty($options['cache'])) { + $cacheItem = $this->parseCache($query, $options['cache'], 'value'); + $key = $cacheItem->getKey(); + + if ($this->cache->has($key)) { + return $this->cache->get($key); + } + } + + // 生成查询SQL + $sql = $this->builder->select($query, $one); + + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + if (isset($options['group'])) { + $query->setOption('group', $options['group']); + } + + // 执行查询操作 + $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']); + + $result = $pdo->fetchColumn(); + + if (isset($cacheItem)) { + // 缓存数据 + $cacheItem->set($result); + $this->cacheData($cacheItem); + } + + return false !== $result ? $result : $default; + } + + /** + * 得到某个字段的值 + * @access public + * @param BaseQuery $query 查询对象 + * @param string $aggregate 聚合方法 + * @param mixed $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate(BaseQuery $query, string $aggregate, $field, bool $force = false) + { + if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) { + [$distinct, $field] = explode(' ', $field); + } + + $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS think_' . strtolower($aggregate); + + $result = $this->value($query, $field, 0); + + return $force ? (float) $result : $result; + } + + /** + * 得到某个列的数组 + * @access public + * @param BaseQuery $query 查询对象 + * @param string|array $column 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(BaseQuery $query, $column, string $key = ''): array + { + $options = $query->parseOptions(); + + if (isset($options['field'])) { + $query->removeOption('field'); + } + + if (empty($key) || trim($key) === '') { + $key = null; + } + + if (is_string($column)) { + $column = trim($column); + if ('*' !== $column) { + $column = array_map('trim', explode(',', $column)); + } + } elseif (is_array($column)) { + if (in_array('*', $column)) { + $column = '*'; + } + } else { + throw new DbException('not support type'); + } + + $field = $column; + if ('*' !== $column && $key && !in_array($key, $column)) { + $field[] = $key; + } + + $query->setOption('field', $field); + + if (!empty($options['cache'])) { + // 判断查询缓存 + $cacheItem = $this->parseCache($query, $options['cache'], 'column'); + $name = $cacheItem->getKey(); + + if ($this->cache->has($name)) { + return $this->cache->get($name); + } + } + + // 生成查询SQL + $sql = $this->builder->select($query); + + if (isset($options['field'])) { + $query->setOption('field', $options['field']); + } else { + $query->removeOption('field'); + } + + // 执行查询操作 + $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']); + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + + if (is_string($key) && strpos($key, '.')) { + [$alias, $key] = explode('.', $key); + } + + if (empty($resultSet)) { + $result = []; + } elseif ('*' !== $column && count($column) === 1) { + $column = array_shift($column); + if (strpos($column, ' ')) { + $column = substr(strrchr(trim($column), ' '), 1); + } + + if (strpos($column, '.')) { + [$alias, $column] = explode('.', $column); + } + + if (strpos($column, '->')) { + $column = $this->builder->parseKey($query, $column); + } + + $result = array_column($resultSet, $column, $key); + } elseif ($key) { + $result = array_column($resultSet, null, $key); + } else { + $result = $resultSet; + } + + if (isset($cacheItem)) { + // 缓存数据 + $cacheItem->set($result); + $this->cacheData($cacheItem); + } + + return $result; + } + + /** + * 根据参数绑定组装最终的SQL语句 便于调试 + * @access public + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 + * @return string + */ + public function getRealSql(string $sql, array $bind = []): string + { + foreach ($bind as $key => $val) { + $value = strval(is_array($val) ? $val[0] : $val); + $type = is_array($val) ? $val[1] : PDO::PARAM_STR; + + if (self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) { + $value = '\'' . addslashes($value) . '\''; + } elseif (PDO::PARAM_INT == $type && '' === $value) { + $value = '0'; + } + + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key)); + } + + return rtrim($sql); + } + + /** + * 参数绑定 + * 支持 ['name'=>'value','id'=>123] 对应命名占位符 + * 或者 ['value',123] 对应问号占位符 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindValue(array $bind = []): void + { + foreach ($bind as $key => $val) { + // 占位符 + $param = is_numeric($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } elseif (self::PARAM_FLOAT == $val[1]) { + $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0]; + $val[1] = PDO::PARAM_STR; + } + + $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 存储过程的输入输出参数绑定 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindParam(array $bind): void + { + foreach ($bind as $key => $val) { + $param = is_numeric($key) ? $key + 1 : ':' . $key; + + if (is_array($val)) { + array_unshift($val, $param); + $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + + if (!$result) { + $param = array_shift($val); + + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 获得数据集数组 + * @access protected + * @param bool $procedure 是否存储过程 + * @return array + */ + protected function getResult(bool $procedure = false): array + { + if ($procedure) { + // 存储过程返回结果 + return $this->procedure(); + } + + $result = $this->PDOStatement->fetchAll($this->fetchType); + + $this->numRows = count($result); + + return $result; + } + + /** + * 获得存储过程数据集 + * @access protected + * @return array + */ + protected function procedure(): array + { + $item = []; + + do { + $result = $this->getResult(); + if (!empty($result)) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + + $this->numRows = count($item); + + return $item; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction(callable $callback) + { + $this->startTrans(); + + try { + $result = null; + if (is_callable($callback)) { + $result = $callback($this); + } + + $this->commit(); + return $result; + } catch (\Exception | \Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动事务 + * @access public + * @return void + * @throws \PDOException + * @throws \Exception + */ + public function startTrans(): void + { + try { + $this->initConnect(true); + + ++$this->transTimes; + + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint() && $this->linkID->inTransaction()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + $this->reConnectTimes = 0; + } catch (\Throwable | \Exception $e) { + if (1 === $this->transTimes && $this->reConnectTimes < 4 && $this->isBreak($e)) { + --$this->transTimes; + ++$this->reConnectTimes; + $this->close()->startTrans(); + } else { + if ($this->isBreak($e)) { + // 尝试对事务计数进行重置 + $this->transTimes = 0; + } + throw $e; + } + } + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws \PDOException + */ + public function commit(): void + { + $this->initConnect(true); + + if (1 == $this->transTimes && $this->linkID->inTransaction()) { + $this->linkID->commit(); + } + + --$this->transTimes; + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws \PDOException + */ + public function rollback(): void + { + $this->initConnect(true); + + if ($this->linkID->inTransaction()) { + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); + } + } + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint(): bool + { + return false; + } + + /** + * 生成定义保存点的SQL + * @access protected + * @param string $name 标识 + * @return string + */ + protected function parseSavepoint(string $name): string + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @access protected + * @param string $name 标识 + * @return string + */ + protected function parseSavepointRollBack(string $name): string + { + return 'ROLLBACK TO SAVEPOINT ' . $name; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param BaseQuery $query 查询对象 + * @param array $sqlArray SQL批处理指令 + * @param array $bind 参数绑定 + * @return bool + */ + public function batchQuery(BaseQuery $query, array $sqlArray = [], array $bind = []): bool + { + // 自动启动事务支持 + $this->startTrans(); + + try { + foreach ($sqlArray as $sql) { + $this->pdoExecute($query, $sql, $bind); + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + + return true; + } + + /** + * 关闭数据库(或者重新连接) + * @access public + * @return $this + */ + public function close() + { + $this->linkID = null; + $this->linkWrite = null; + $this->linkRead = null; + $this->links = []; + $this->transTimes = 0; + + $this->free(); + + return $this; + } + + /** + * 是否断线 + * @access protected + * @param \PDOException|\Exception $e 异常对象 + * @return bool + */ + protected function isBreak($e): bool + { + if (!$this->config['break_reconnect']) { + return false; + } + + $error = $e->getMessage(); + + foreach ($this->breakMatchStr as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + + return false; + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql(): string + { + return $this->getRealSql($this->queryStr, $this->bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param BaseQuery $query 查询对象 + * @param string $sequence 自增序列名 + * @return mixed + */ + public function getLastInsID(BaseQuery $query, string $sequence = null) + { + try { + $insertId = $this->linkID->lastInsertId($sequence); + } catch (\Exception $e) { + $insertId = ''; + } + + return $this->autoInsIDType($query, $insertId); + } + + /** + * 获取最近插入的ID + * @access public + * @param BaseQuery $query 查询对象 + * @param string $insertId 自增ID + * @return mixed + */ + protected function autoInsIDType(BaseQuery $query, string $insertId) + { + $pk = $query->getAutoInc(); + + if ($pk) { + $type = $this->getFieldBindType($pk); + + if (PDO::PARAM_INT == $type) { + $insertId = (int) $insertId; + } elseif (self::PARAM_FLOAT == $type) { + $insertId = (float) $insertId; + } + } + + return $insertId; + } + + /** + * 获取最近的错误信息 + * @access public + * @return string + */ + public function getError(): string + { + if ($this->PDOStatement) { + $error = $this->PDOStatement->errorInfo(); + $error = $error[1] . ':' . $error[2]; + } else { + $error = ''; + } + + if ('' != $this->queryStr) { + $error .= "\n [ SQL语句 ] : " . $this->getLastsql(); + } + + return $error; + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect(bool $master = true): void + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master || $this->transTimes) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + + $this->linkID = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + + $this->linkID = $this->linkRead; + } + } elseif (!$this->linkID) { + // 默认单数据库 + $this->linkID = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return PDO + */ + protected function multiConnect(bool $master = false): PDO + { + $config = []; + + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name]; + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + $r = $m; + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($config['hostname']) - 1)); + } + $dbMaster = false; + + if ($m != $r) { + $dbMaster = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0]; + } + } + + $dbConfig = []; + + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0]; + } + + return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); + } + + /** + * 获取数据库的唯一标识 + * @access public + * @param string $suffix 标识后缀 + * @return string + */ + public function getUniqueXid(string $suffix = ''): string + { + return $this->config['hostname'] . '_' . $this->config['database'] . $suffix; + } + + /** + * 执行数据库Xa事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @param array $dbs 多个查询对象或者连接对象 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transactionXa(callable $callback, array $dbs = []) + { + $xid = uniqid('xa'); + + if (empty($dbs)) { + $dbs[] = $this; + } + + foreach ($dbs as $key => $db) { + if ($db instanceof BaseQuery) { + $db = $db->getConnection(); + + $dbs[$key] = $db; + } + + $db->startTransXa($db->getUniqueXid('_' . $xid) ); + } + + try { + $result = null; + if (is_callable($callback)) { + $result = $callback($this); + } + + foreach ($dbs as $db) { + $db->prepareXa($db->getUniqueXid('_' . $xid)); + } + + foreach ($dbs as $db) { + $db->commitXa($db->getUniqueXid('_' . $xid) ); + } + + return $result; + } catch (\Exception | \Throwable $e) { + foreach ($dbs as $db) { + $db->rollbackXa($db->getUniqueXid('_' . $xid) ); + } + throw $e; + } + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa(string $xid): void + { + } + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa(string $xid): void + { + } + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa(string $xid): void + { + } + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa(string $xid): void + { + } +} diff --git a/vendor/topthink/think-orm/src/db/builder/Mongo.php b/vendor/topthink/think-orm/src/db/builder/Mongo.php new file mode 100644 index 0000000..823156b --- /dev/null +++ b/vendor/topthink/think-orm/src/db/builder/Mongo.php @@ -0,0 +1,675 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); +namespace think\db\builder; + +use MongoDB\BSON\Javascript; +use MongoDB\BSON\ObjectID; +use MongoDB\BSON\Regex; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Command; +use MongoDB\Driver\Exception\InvalidArgumentException; +use MongoDB\Driver\Query as MongoQuery; +use think\db\connector\Mongo as Connection; +use think\db\exception\DbException as Exception; +use think\db\Mongo as Query; + +class Mongo +{ + // connection对象实例 + protected $connection; + // 最后插入ID + protected $insertId = []; + // 查询表达式 + protected $exp = ['<>' => 'ne', '=' => 'eq', '>' => 'gt', '>=' => 'gte', '<' => 'lt', '<=' => 'lte', 'in' => 'in', 'not in' => 'nin', 'nin' => 'nin', 'mod' => 'mod', 'exists' => 'exists', 'null' => 'null', 'notnull' => 'not null', 'not null' => 'not null', 'regex' => 'regex', 'type' => 'type', 'all' => 'all', '> time' => '> time', '< time' => '< time', 'between' => 'between', 'not between' => 'not between', 'between time' => 'between time', 'not between time' => 'not between time', 'notbetween time' => 'not between time', 'like' => 'like', 'near' => 'near', 'size' => 'size']; + + /** + * 架构函数 + * @access public + * @param Connection $connection 数据库连接对象实例 + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * 获取当前的连接对象实例 + * @access public + * @return Connection + */ + public function getConnection(): Connection + { + return $this->connection; + } + + /** + * key分析 + * @access protected + * @param string $key + * @return string + */ + protected function parseKey(Query $query, string $key): string + { + if (0 === strpos($key, '__TABLE__.')) { + [$collection, $key] = explode('.', $key, 2); + } + + if ('id' == $key && $this->connection->getConfig('pk_convert_id')) { + $key = '_id'; + } + + return trim($key); + } + + /** + * value分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $value + * @param string $field + * @return string + */ + protected function parseValue(Query $query, $value, $field = '') + { + if ('_id' == $field && 'ObjectID' == $this->connection->getConfig('pk_type') && is_string($value)) { + try { + return new ObjectID($value); + } catch (InvalidArgumentException $e) { + return new ObjectID(); + } + } + + return $value; + } + + /** + * insert数据分析 + * @access protected + * @param Query $query 查询对象 + * @param array $data 数据 + * @return array + */ + protected function parseData(Query $query, array $data): array + { + if (empty($data)) { + return []; + } + + $result = []; + + foreach ($data as $key => $val) { + $item = $this->parseKey($query, $key); + + if (is_object($val)) { + $result[$item] = $val; + } elseif (isset($val[0]) && 'exp' == $val[0]) { + $result[$item] = $val[1]; + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } else { + $result[$item] = $this->parseValue($query, $val, $key); + } + } + + return $result; + } + + /** + * Set数据分析 + * @access protected + * @param Query $query 查询对象 + * @param array $data 数据 + * @return array + */ + protected function parseSet(Query $query, array $data): array + { + if (empty($data)) { + return []; + } + + $result = []; + + foreach ($data as $key => $val) { + $item = $this->parseKey($query, $key); + + if (is_array($val) && isset($val[0]) && is_string($val[0]) && 0 === strpos($val[0], '$')) { + $result[$val[0]][$item] = $this->parseValue($query, $val[1], $key); + } else { + $result['$set'][$item] = $this->parseValue($query, $val, $key); + } + } + + return $result; + } + + /** + * 生成查询过滤条件 + * @access public + * @param Query $query 查询对象 + * @param mixed $where + * @return array + */ + public function parseWhere(Query $query, array $where): array + { + if (empty($where)) { + $where = []; + } + + $filter = []; + foreach ($where as $logic => $val) { + $logic = '$' . strtolower($logic); + foreach ($val as $field => $value) { + if (is_array($value)) { + if (key($value) !== 0) { + throw new Exception('where express error:' . var_export($value, true)); + } + $field = array_shift($value); + } elseif (!($value instanceof \Closure)) { + throw new Exception('where express error:' . var_export($value, true)); + } + + if ($value instanceof \Closure) { + // 使用闭包查询 + $query = new Query($this->connection); + call_user_func_array($value, [ & $query]); + $filter[$logic][] = $this->parseWhere($query, $query->getOptions('where')); + } else { + if (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + foreach ($array as $k) { + $filter['$or'][] = $this->parseWhereItem($query, $k, $value); + } + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + foreach ($array as $k) { + $filter['$and'][] = $this->parseWhereItem($query, $k, $value); + } + } else { + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $filter[$logic][] = $this->parseWhereItem($query, $field, $value); + } + } + } + } + + $options = $query->getOptions(); + if (!empty($options['soft_delete'])) { + // 附加软删除条件 + [$field, $condition] = $options['soft_delete']; + $filter['$and'][] = $this->parseWhereItem($query, $field, $condition); + } + + return $filter; + } + + // where子单元分析 + protected function parseWhereItem(Query $query, $field, $val): array + { + $key = $field ? $this->parseKey($query, $field) : ''; + // 查询规则和条件 + if (!is_array($val)) { + $val = ['=', $val]; + } + [$exp, $value] = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + $data = []; + foreach ($val as $value) { + $exp = $value[0]; + $value = $value[1]; + if (!in_array($exp, $this->exp)) { + $exp = strtolower($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } + } + $k = '$' . $exp; + $data[$k] = $value; + } + $result[$key] = $data; + return $result; + } elseif (!in_array($exp, $this->exp)) { + $exp = strtolower($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } else { + throw new Exception('where express error:' . $exp); + } + } + + $result = []; + if ('=' == $exp) { + // 普通查询 + $result[$key] = $this->parseValue($query, $value, $key); + } elseif (in_array($exp, ['neq', 'ne', 'gt', 'egt', 'gte', 'lt', 'lte', 'elt', 'mod'])) { + // 比较运算 + $k = '$' . $exp; + $result[$key] = [$k => $this->parseValue($query, $value, $key)]; + } elseif ('null' == $exp) { + // NULL 查询 + $result[$key] = null; + } elseif ('not null' == $exp) { + $result[$key] = ['$ne' => null]; + } elseif ('all' == $exp) { + // 满足所有指定条件 + $result[$key] = ['$all', $this->parseValue($query, $value, $key)]; + } elseif ('between' == $exp) { + // 区间查询 + $value = is_array($value) ? $value : explode(',', $value); + $result[$key] = ['$gte' => $this->parseValue($query, $value[0], $key), '$lte' => $this->parseValue($query, $value[1], $key)]; + } elseif ('not between' == $exp) { + // 范围查询 + $value = is_array($value) ? $value : explode(',', $value); + $result[$key] = ['$lt' => $this->parseValue($query, $value[0], $key), '$gt' => $this->parseValue($query, $value[1], $key)]; + } elseif ('exists' == $exp) { + // 字段是否存在 + $result[$key] = ['$exists' => (bool) $value]; + } elseif ('type' == $exp) { + // 类型查询 + $result[$key] = ['$type' => intval($value)]; + } elseif ('exp' == $exp) { + // 表达式查询 + $result['$where'] = $value instanceof Javascript ? $value : new Javascript($value); + } elseif ('like' == $exp) { + // 模糊查询 采用正则方式 + $result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i'); + } elseif (in_array($exp, ['nin', 'in'])) { + // IN 查询 + $value = is_array($value) ? $value : explode(',', $value); + foreach ($value as $k => $val) { + $value[$k] = $this->parseValue($query, $val, $key); + } + $result[$key] = ['$' . $exp => $value]; + } elseif ('regex' == $exp) { + $result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i'); + } elseif ('< time' == $exp) { + $result[$key] = ['$lt' => $this->parseDateTime($query, $value, $field)]; + } elseif ('> time' == $exp) { + $result[$key] = ['$gt' => $this->parseDateTime($query, $value, $field)]; + } elseif ('between time' == $exp) { + // 区间查询 + $value = is_array($value) ? $value : explode(',', $value); + $result[$key] = ['$gte' => $this->parseDateTime($query, $value[0], $field), '$lte' => $this->parseDateTime($query, $value[1], $field)]; + } elseif ('not between time' == $exp) { + // 范围查询 + $value = is_array($value) ? $value : explode(',', $value); + $result[$key] = ['$lt' => $this->parseDateTime($query, $value[0], $field), '$gt' => $this->parseDateTime($query, $value[1], $field)]; + } elseif ('near' == $exp) { + // 经纬度查询 + $result[$key] = ['$near' => $this->parseValue($query, $value, $key)]; + } elseif ('size' == $exp) { + // 元素长度查询 + $result[$key] = ['$size' => intval($value)]; + } else { + // 普通查询 + $result[$key] = $this->parseValue($query, $value, $key); + } + + return $result; + } + + /** + * 日期时间条件解析 + * @access protected + * @param Query $query 查询对象 + * @param string $value + * @param string $key + * @return string + */ + protected function parseDateTime(Query $query, $value, $key) + { + // 获取时间字段类型 + $type = $query->getFieldType($key); + + if ($type) { + if (is_string($value)) { + $value = strtotime($value) ?: $value; + } + + if (is_int($value)) { + if (preg_match('/(datetime|timestamp)/is', $type)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $type)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + } + + return $value; + } + + /** + * 获取最后写入的ID 如果是insertAll方法的话 返回所有写入的ID + * @access public + * @return mixed + */ + public function getLastInsID() + { + return $this->insertId; + } + + /** + * 生成insert BulkWrite对象 + * @access public + * @param Query $query 查询对象 + * @return BulkWrite + */ + public function insert(Query $query): BulkWrite + { + // 分析并处理数据 + $options = $query->getOptions(); + + $data = $this->parseData($query, $options['data']); + + $bulk = new BulkWrite; + + if ($insertId = $bulk->insert($data)) { + $this->insertId = $insertId; + } + + $this->log('insert', $data, $options); + + return $bulk; + } + + /** + * 生成insertall BulkWrite对象 + * @access public + * @param Query $query 查询对象 + * @param array $dataSet 数据集 + * @return BulkWrite + */ + public function insertAll(Query $query, array $dataSet): BulkWrite + { + $bulk = new BulkWrite; + $options = $query->getOptions(); + + $this->insertId = []; + foreach ($dataSet as $data) { + // 分析并处理数据 + $data = $this->parseData($query, $data); + if ($insertId = $bulk->insert($data)) { + $this->insertId[] = $insertId; + } + } + + $this->log('insert', $dataSet, $options); + + return $bulk; + } + + /** + * 生成update BulkWrite对象 + * @access public + * @param Query $query 查询对象 + * @return BulkWrite + */ + public function update(Query $query): BulkWrite + { + $options = $query->getOptions(); + + $data = $this->parseSet($query, $options['data']); + $where = $this->parseWhere($query, $options['where']); + + if (1 == $options['limit']) { + $updateOptions = ['multi' => false]; + } else { + $updateOptions = ['multi' => true]; + } + + $bulk = new BulkWrite; + + $bulk->update($where, $data, $updateOptions); + + $this->log('update', $data, $where); + + return $bulk; + } + + /** + * 生成delete BulkWrite对象 + * @access public + * @param Query $query 查询对象 + * @return BulkWrite + */ + public function delete(Query $query): BulkWrite + { + $options = $query->getOptions(); + $where = $this->parseWhere($query, $options['where']); + + $bulk = new BulkWrite; + + if (1 == $options['limit']) { + $deleteOptions = ['limit' => 1]; + } else { + $deleteOptions = ['limit' => 0]; + } + + $bulk->delete($where, $deleteOptions); + + $this->log('remove', $where, $deleteOptions); + + return $bulk; + } + + /** + * 生成Mongo查询对象 + * @access public + * @param Query $query 查询对象 + * @param bool $one 是否仅获取一个记录 + * @return MongoQuery + */ + public function select(Query $query, bool $one = false): MongoQuery + { + $options = $query->getOptions(); + + $where = $this->parseWhere($query, $options['where']); + + if ($one) { + $options['limit'] = 1; + } + + $query = new MongoQuery($where, $options); + + $this->log('find', $where, $options); + + return $query; + } + + /** + * 生成Count命令 + * @access public + * @param Query $query 查询对象 + * @return Command + */ + public function count(Query $query): Command + { + $options = $query->getOptions(); + + $cmd['count'] = $options['table']; + $cmd['query'] = (object) $this->parseWhere($query, $options['where']); + + foreach (['hint', 'limit', 'maxTimeMS', 'skip'] as $option) { + if (isset($options[$option])) { + $cmd[$option] = $options[$option]; + } + } + + $command = new Command($cmd); + $this->log('cmd', 'count', $cmd); + + return $command; + } + + /** + * 聚合查询命令 + * @access public + * @param Query $query 查询对象 + * @param array $extra 指令和字段 + * @return Command + */ + public function aggregate(Query $query, array $extra): Command + { + $options = $query->getOptions(); + [$fun, $field] = $extra; + + if ('id' == $field && $this->connection->getConfig('pk_convert_id')) { + $field = '_id'; + } + + $group = isset($options['group']) ? '$' . $options['group'] : null; + + $pipeline = [ + ['$match' => (object) $this->parseWhere($query, $options['where'])], + ['$group' => ['_id' => $group, 'aggregate' => ['$' . $fun => '$' . $field]]], + ]; + + $cmd = [ + 'aggregate' => $options['table'], + 'allowDiskUse' => true, + 'pipeline' => $pipeline, + 'cursor' => new \stdClass, + ]; + + foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) { + if (isset($options[$option])) { + $cmd[$option] = $options[$option]; + } + } + + $command = new Command($cmd); + + $this->log('aggregate', $cmd); + + return $command; + } + + /** + * 多聚合查询命令, 可以对多个字段进行 group by 操作 + * + * @param Query $query 查询对象 + * @param array $extra 指令和字段 + * @return Command + */ + public function multiAggregate(Query $query, $extra): Command + { + $options = $query->getOptions(); + + [$aggregate, $groupBy] = $extra; + + $groups = ['_id' => []]; + + foreach ($groupBy as $field) { + $groups['_id'][$field] = '$' . $field; + } + + foreach ($aggregate as $fun => $field) { + $groups[$field . '_' . $fun] = ['$' . $fun => '$' . $field]; + } + + $pipeline = [ + ['$match' => (object) $this->parseWhere($query, $options['where'])], + ['$group' => $groups], + ]; + + $cmd = [ + 'aggregate' => $options['table'], + 'allowDiskUse' => true, + 'pipeline' => $pipeline, + 'cursor' => new \stdClass, + ]; + + foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) { + if (isset($options[$option])) { + $cmd[$option] = $options[$option]; + } + } + + $command = new Command($cmd); + $this->log('group', $cmd); + + return $command; + } + + /** + * 生成distinct命令 + * @access public + * @param Query $query 查询对象 + * @param string $field 字段名 + * @return Command + */ + public function distinct(Query $query, $field): Command + { + $options = $query->getOptions(); + + $cmd = [ + 'distinct' => $options['table'], + 'key' => $field, + ]; + + if (!empty($options['where'])) { + $cmd['query'] = (object) $this->parseWhere($query, $options['where']); + } + + if (isset($options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $options['maxTimeMS']; + } + + $command = new Command($cmd); + + $this->log('cmd', 'distinct', $cmd); + + return $command; + } + + /** + * 查询所有的collection + * @access public + * @return Command + */ + public function listcollections(): Command + { + $cmd = ['listCollections' => 1]; + $command = new Command($cmd); + + $this->log('cmd', 'listCollections', $cmd); + + return $command; + } + + /** + * 查询数据表的状态信息 + * @access public + * @param Query $query 查询对象 + * @return Command + */ + public function collStats(Query $query): Command + { + $options = $query->getOptions(); + + $cmd = ['collStats' => $options['table']]; + $command = new Command($cmd); + + $this->log('cmd', 'collStats', $cmd); + + return $command; + } + + protected function log($type, $data, $options = []) + { + $this->connection->mongoLog($type, $data, $options); + } +} diff --git a/vendor/topthink/think-orm/src/db/builder/Mysql.php b/vendor/topthink/think-orm/src/db/builder/Mysql.php new file mode 100644 index 0000000..d70fb90 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/builder/Mysql.php @@ -0,0 +1,432 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\builder; + +use think\db\Builder; +use think\db\exception\DbException as Exception; +use think\db\Query; +use think\db\Raw; + +/** + * mysql数据库驱动 + */ +class Mysql extends Builder +{ + /** + * 查询表达式解析 + * @var array + */ + protected $parser = [ + 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='], + 'parseLike' => ['LIKE', 'NOT LIKE'], + 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'], + 'parseIn' => ['NOT IN', 'IN'], + 'parseExp' => ['EXP'], + 'parseRegexp' => ['REGEXP', 'NOT REGEXP'], + 'parseNull' => ['NOT NULL', 'NULL'], + 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'], + 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'], + 'parseExists' => ['NOT EXISTS', 'EXISTS'], + 'parseColumn' => ['COLUMN'], + 'parseFindInSet' => ['FIND IN SET'], + ]; + + /** + * SELECT SQL表达式 + * @var string + */ + protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%PARTITION%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * INSERT SQL表达式 + * @var string + */ + protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% SET %SET% %DUPLICATE%%COMMENT%'; + + /** + * INSERT ALL SQL表达式 + * @var string + */ + protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% (%FIELD%) VALUES %DATA% %DUPLICATE%%COMMENT%'; + + /** + * UPDATE SQL表达式 + * @var string + */ + protected $updateSql = 'UPDATE%EXTRA% %TABLE%%PARTITION% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * DELETE SQL表达式 + * @var string + */ + protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%PARTITION%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 生成查询SQL + * @access public + * @param Query $query 查询对象 + * @param bool $one 是否仅获取一个记录 + * @return string + */ + public function select(Query $query, bool $one = false): string + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%PARTITION%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], + [ + $this->parseTable($query, $options['table']), + $this->parsePartition($query, $options['partition']), + $this->parseDistinct($query, $options['distinct']), + $this->parseExtra($query, $options['extra']), + $this->parseField($query, $options['field'] ?? '*'), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseGroup($query, $options['group']), + $this->parseHaving($query, $options['having']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $one ? '1' : $options['limit']), + $this->parseUnion($query, $options['union']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + $this->parseForce($query, $options['force']), + ], + $this->selectSql + ); + } + + /** + * 生成Insert SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function insert(Query $query): string + { + $options = $query->getOptions(); + + // 分析并处理数据 + $data = $this->parseData($query, $options['data']); + if (empty($data)) { + return ''; + } + + $set = []; + foreach ($data as $key => $val) { + $set[] = $key . ' = ' . $val; + } + + return str_replace( + ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%SET%', '%DUPLICATE%', '%COMMENT%'], + [ + !empty($options['replace']) ? 'REPLACE' : 'INSERT', + $this->parseExtra($query, $options['extra']), + $this->parseTable($query, $options['table']), + $this->parsePartition($query, $options['partition']), + implode(' , ', $set), + $this->parseDuplicate($query, $options['duplicate']), + $this->parseComment($query, $options['comment']), + ], + $this->insertSql + ); + } + + /** + * 生成insertall SQL + * @access public + * @param Query $query 查询对象 + * @param array $dataSet 数据集 + * @param bool $replace 是否replace + * @return string + */ + public function insertAll(Query $query, array $dataSet, bool $replace = false): string + { + $options = $query->getOptions(); + + // 获取绑定信息 + $bind = $query->getFieldsBindType(); + + // 获取合法的字段 + if (empty($options['field']) || '*' == $options['field']) { + $allowFields = array_keys($bind); + } else { + $allowFields = $options['field']; + } + + $fields = []; + $values = []; + + foreach ($dataSet as $data) { + $data = $this->parseData($query, $data, $allowFields, $bind); + + $values[] = '( ' . implode(',', array_values($data)) . ' )'; + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($query, $field); + } + + return str_replace( + ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%FIELD%', '%DATA%', '%DUPLICATE%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseExtra($query, $options['extra']), + $this->parseTable($query, $options['table']), + $this->parsePartition($query, $options['partition']), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseDuplicate($query, $options['duplicate']), + $this->parseComment($query, $options['comment']), + ], + $this->insertAllSql + ); + } + + /** + * 生成update SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function update(Query $query): string + { + $options = $query->getOptions(); + + $data = $this->parseData($query, $options['data']); + + if (empty($data)) { + return ''; + } + + $set = []; + foreach ($data as $key => $val) { + $set[] = (strpos($key, '->') ? strstr($key, '->', true) : $key) . ' = ' . $val; + } + + return str_replace( + ['%TABLE%', '%PARTITION%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + $this->parsePartition($query, $options['partition']), + $this->parseExtra($query, $options['extra']), + implode(' , ', $set), + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->updateSql + ); + } + + /** + * 生成delete SQL + * @access public + * @param Query $query 查询对象 + * @return string + */ + public function delete(Query $query): string + { + $options = $query->getOptions(); + + return str_replace( + ['%TABLE%', '%PARTITION%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($query, $options['table']), + $this->parsePartition($query, $options['partition']), + $this->parseExtra($query, $options['extra']), + !empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '', + $this->parseJoin($query, $options['join']), + $this->parseWhere($query, $options['where']), + $this->parseOrder($query, $options['order']), + $this->parseLimit($query, $options['limit']), + $this->parseLock($query, $options['lock']), + $this->parseComment($query, $options['comment']), + ], + $this->deleteSql + ); + } + + /** + * 正则查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @return string + */ + protected function parseRegexp(Query $query, string $key, string $exp, $value, string $field): string + { + if ($value instanceof Raw) { + $value = $this->parseRaw($query, $value); + } + + return $key . ' ' . $exp . ' ' . $value; + } + + /** + * FIND_IN_SET 查询 + * @access protected + * @param Query $query 查询对象 + * @param string $key + * @param string $exp + * @param mixed $value + * @param string $field + * @return string + */ + protected function parseFindInSet(Query $query, string $key, string $exp, $value, string $field): string + { + if ($value instanceof Raw) { + $value = $this->parseRaw($query, $value); + } + + return 'FIND_IN_SET(' . $value . ', ' . $key . ')'; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param mixed $key 字段名 + * @param bool $strict 严格检测 + * @return string + */ + public function parseKey(Query $query, $key, bool $strict = false): string + { + if (is_int($key)) { + return (string) $key; + } elseif ($key instanceof Raw) { + return $this->parseRaw($query, $key); + } + + $key = trim($key); + + if (strpos($key, '->>') && false === strpos($key, '(')) { + // JSON字段支持 + [$field, $name] = explode('->>', $key, 2); + + return $this->parseKey($query, $field, true) . '->>\'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->>', '.', $name) . '\''; + } elseif (strpos($key, '->') && false === strpos($key, '(')) { + // JSON字段支持 + [$field, $name] = explode('->', $key, 2); + return 'json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + [$table, $key] = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + $key = '`' . $key . '`'; + } + + if (isset($table)) { + if (strpos($table, '.')) { + $table = str_replace('.', '`.`', $table); + } + + $key = '`' . $table . '`.' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query): string + { + return 'rand()'; + } + + /** + * Partition 分析 + * @access protected + * @param Query $query 查询对象 + * @param string|array $partition 分区 + * @return string + */ + protected function parsePartition(Query $query, $partition): string + { + if ('' == $partition) { + return ''; + } + + if (is_string($partition)) { + $partition = explode(',', $partition); + } + + return ' PARTITION (' . implode(' , ', $partition) . ') '; + } + + /** + * ON DUPLICATE KEY UPDATE 分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $duplicate + * @return string + */ + protected function parseDuplicate(Query $query, $duplicate): string + { + if ('' == $duplicate) { + return ''; + } + + if ($duplicate instanceof Raw) { + return ' ON DUPLICATE KEY UPDATE ' . $this->parseRaw($query, $duplicate) . ' '; + } + + if (is_string($duplicate)) { + $duplicate = explode(',', $duplicate); + } + + $updates = []; + foreach ($duplicate as $key => $val) { + if (is_numeric($key)) { + $val = $this->parseKey($query, $val); + $updates[] = $val . ' = VALUES(' . $val . ')'; + } elseif ($val instanceof Raw) { + $updates[] = $this->parseKey($query, $key) . " = " . $this->parseRaw($query, $val); + } else { + $name = $query->bindValue($val, $query->getConnection()->getFieldBindType($key)); + $updates[] = $this->parseKey($query, $key) . " = :" . $name; + } + } + + return ' ON DUPLICATE KEY UPDATE ' . implode(' , ', $updates) . ' '; + } +} diff --git a/vendor/topthink/think-orm/src/db/builder/Oracle.php b/vendor/topthink/think-orm/src/db/builder/Oracle.php new file mode 100644 index 0000000..38feb5d --- /dev/null +++ b/vendor/topthink/think-orm/src/db/builder/Oracle.php @@ -0,0 +1,129 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\builder; + +use think\db\Builder; +use think\db\Query; +use think\db\exception\DbException as Exception; +use think\db\Raw; + +/** + * Oracle数据库驱动 + */ +class Oracle extends Builder +{ + protected $selectSql = 'SELECT * FROM (SELECT thinkphp.*, rownum AS numrow FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%) thinkphp ) %LIMIT%%COMMENT%'; + + /** + * limit分析 + * @access protected + * @param Query $query 查询对象 + * @param mixed $limit + * @return string + */ + protected function parseLimit(Query $query, string $limit): string + { + $limitStr = ''; + + if (!empty($limit)) { + $limit = explode(',', $limit); + + if (count($limit) > 1) { + $limitStr = "(numrow>" . $limit[0] . ") AND (numrow<=" . ($limit[0] + $limit[1]) . ")"; + } else { + $limitStr = "(numrow>0 AND numrow<=" . $limit[0] . ")"; + } + + } + + return $limitStr ? ' WHERE ' . $limitStr : ''; + } + + /** + * 设置锁机制 + * @access protected + * @param Query $query 查询对象 + * @param bool|false $lock + * @return string + */ + protected function parseLock(Query $query, $lock = false): string + { + if (!$lock) { + return ''; + } + + return ' FOR UPDATE NOWAIT '; + } + + /** + * 字段和表名处理 + * @access public + * @param Query $query 查询对象 + * @param string $key + * @param bool $strict + * @return string + * @throws Exception + */ + public function parseKey(Query $query, $key, bool $strict = false): string + { + if (is_int($key)) { + return (string) $key; + } elseif ($key instanceof Raw) { + return $this->parseRaw($query, $key); + } + + $key = trim($key); + + if (strpos($key, '->') && false === strpos($key, '(')) { + // JSON字段支持 + [$field, $name] = explode($key, '->'); + $key = $field . '."' . $name . '"'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) { + [$table, $key] = explode('.', $key, 2); + + $alias = $query->getOptions('alias'); + + if ('__TABLE__' == $table) { + $table = $query->getOptions('table'); + $table = is_array($table) ? array_shift($table) : $table; + } + + if (isset($alias[$table])) { + $table = $alias[$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + + if ('*' != $key && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + $key = '"' . $key . '"'; + } + + if (isset($table)) { + $key = '"' . $table . '".' . $key; + } + + return $key; + } + + /** + * 随机排序 + * @access protected + * @param Query $query 查询对象 + * @return string + */ + protected function parseRand(Query $query): string + { + return 'DBMS_RANDOM.value'; + } +} diff --git a/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php b/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php new file mode 100644 index 0000000..2828949 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\concern; + +use think\db\Raw; + +/** + * JOIN和VIEW查询 + */ +trait JoinAndViewQuery +{ + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @param array $bind 参数绑定 + * @return $this + */ + public function join($join, string $condition = null, string $type = 'INNER', array $bind = []) + { + $table = $this->getJoinTable($join); + + if (!empty($bind) && $condition) { + $this->bindParams($condition, $bind); + } + + $this->options['join'][] = [$table, strtoupper($type), $condition]; + + return $this; + } + + /** + * LEFT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function leftJoin($join, string $condition = null, array $bind = []) + { + return $this->join($join, $condition, 'LEFT', $bind); + } + + /** + * RIGHT JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function rightJoin($join, string $condition = null, array $bind = []) + { + return $this->join($join, $condition, 'RIGHT', $bind); + } + + /** + * FULL JOIN + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function fullJoin($join, string $condition = null, array $bind = []) + { + return $this->join($join, $condition, 'FULL'); + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'table alias' + * @access protected + * @param array|string|Raw $join JION表名 + * @param string $alias 别名 + * @return string|array + */ + protected function getJoinTable($join, &$alias = null) + { + if (is_array($join)) { + $table = $join; + $alias = array_shift($join); + return $table; + } elseif ($join instanceof Raw) { + return $join; + } + + $join = trim($join); + + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; + } else { + // 使用别名 + if (strpos($join, ' ')) { + // 使用别名 + [$table, $alias] = explode(' ', $join); + } else { + $table = $join; + if (false === strpos($join, '.')) { + $alias = $join; + } + } + + if ($this->prefix && false === strpos($table, '.') && 0 !== strpos($table, $this->prefix)) { + $table = $this->getTable($table); + } + } + + if (!empty($alias) && $table != $alias) { + $table = [$table => $alias]; + } + + return $table; + } + + /** + * 指定JOIN查询字段 + * @access public + * @param string|array $join 数据表 + * @param string|array $field 查询字段 + * @param string $on JOIN条件 + * @param string $type JOIN类型 + * @param array $bind 参数绑定 + * @return $this + */ + public function view($join, $field = true, $on = null, string $type = 'INNER', array $bind = []) + { + $this->options['view'] = true; + + $fields = []; + $table = $this->getJoinTable($join, $alias); + + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + + $fields[] = $name . ' AS ' . $val; + + $this->options['map'][$val] = $name; + } + } + } + + $this->field($fields); + + if ($on) { + $this->join($table, $on, $type, $bind); + } else { + $this->table($table); + } + + return $this; + } + + /** + * 视图查询处理 + * @access protected + * @param array $options 查询参数 + * @return void + */ + protected function parseView(array &$options): void + { + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + array_shift($val); + array_unshift($val, $options['map'][$key]); + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + foreach ($options['order'] as $key => $val) { + if (is_numeric($key) && is_string($val)) { + if (strpos($val, ' ')) { + [$field, $sort] = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } + } + +} diff --git a/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php b/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php new file mode 100644 index 0000000..cabd65b --- /dev/null +++ b/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php @@ -0,0 +1,562 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\concern; + +use Closure; +use think\helper\Str; +use think\Model; +use think\model\Collection as ModelCollection; + +/** + * 模型及关联查询 + */ +trait ModelRelationQuery +{ + + /** + * 当前模型对象 + * @var Model + */ + protected $model; + + /** + * 指定模型 + * @access public + * @param Model $model 模型对象实例 + * @return $this + */ + public function model(Model $model) + { + $this->model = $model; + return $this; + } + + /** + * 获取当前的模型对象 + * @access public + * @return Model|null + */ + public function getModel() + { + return $this->model; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @return $this + */ + public function hidden(array $hidden = []) + { + $this->options['hidden'] = $hidden; + + return $this; + } + + /** + * 设置需要输出的属性 + * @access public + * @param array $visible + * @return $this + */ + public function visible(array $visible = []) + { + $this->options['visible'] = $visible; + + return $this; + } + + /** + * 设置需要附加的输出属性 + * @access public + * @param array $append 属性列表 + * @return $this + */ + public function append(array $append = []) + { + $this->options['append'] = $append; + + return $this; + } + + /** + * 添加查询范围 + * @access public + * @param array|string|Closure $scope 查询范围定义 + * @param array $args 参数 + * @return $this + */ + public function scope($scope, ...$args) + { + // 查询范围的第一个参数始终是当前查询对象 + array_unshift($args, $this); + + if ($scope instanceof Closure) { + call_user_func_array($scope, $args); + return $this; + } + + if (is_string($scope)) { + $scope = explode(',', $scope); + } + + if ($this->model) { + // 检查模型类的查询范围方法 + foreach ($scope as $name) { + $method = 'scope' . trim($name); + + if (method_exists($this->model, $method)) { + call_user_func_array([$this->model, $method], $args); + } + } + } + + return $this; + } + + /** + * 设置关联查询 + * @access public + * @param array $relation 关联名称 + * @return $this + */ + public function relation(array $relation) + { + if (empty($this->model) || empty($relation)) { + return $this; + } + + $this->options['relation'] = $relation; + return $this; + } + + /** + * 使用搜索器条件搜索字段 + * @access public + * @param string|array $fields 搜索字段 + * @param mixed $data 搜索数据 + * @param string $prefix 字段前缀标识 + * @return $this + */ + public function withSearch($fields, $data = [], string $prefix = '') + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + $likeFields = $this->getConfig('match_like_fields') ?: []; + + foreach ($fields as $key => $field) { + if ($field instanceof Closure) { + $field($this, $data[$key] ?? null, $data, $prefix); + } elseif ($this->model) { + // 检测搜索器 + $fieldName = is_numeric($key) ? $field : $key; + $method = 'search' . Str::studly($fieldName) . 'Attr'; + + if (method_exists($this->model, $method)) { + $this->model->$method($this, $data[$field] ?? null, $data, $prefix); + } elseif (isset($data[$field])) { + $this->where($fieldName, in_array($fieldName, $likeFields) ? 'like' : '=', in_array($fieldName, $likeFields) ? '%' . $data[$field] . '%' : $data[$field]); + } + } + } + + return $this; + } + + /** + * 设置数据字段获取器 + * @access public + * @param string|array $name 字段名 + * @param callable $callback 闭包获取器 + * @return $this + */ + public function withAttr($name, callable $callback = null) + { + if (is_array($name)) { + foreach ($name as $key => $val) { + $this->withAttr($key, $val); + } + return $this; + } + + $this->options['with_attr'][$name] = $callback; + + if (strpos($name, '.')) { + [$relation, $field] = explode('.', $name); + + if (!empty($this->options['json']) && in_array($relation, $this->options['json'])) { + + } else { + $this->options['with_relation_attr'][$relation][$field] = $callback; + unset($this->options['with_attr'][$name]); + } + } + + return $this; + } + + /** + * 关联预载入 In方式 + * @access public + * @param array|string $with 关联方法名称 + * @return $this + */ + public function with($with) + { + if (empty($this->model) || empty($with)) { + return $this; + } + + $this->options['with'] = (array) $with; + return $this; + } + + /** + * 关联预载入 JOIN方式 + * @access protected + * @param array|string $with 关联方法名 + * @param string $joinType JOIN方式 + * @return $this + */ + public function withJoin($with, string $joinType = '') + { + if (empty($this->model) || empty($with)) { + return $this; + } + + $with = (array) $with; + $first = true; + + foreach ($with as $key => $relation) { + $closure = null; + $field = true; + + if ($relation instanceof Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } elseif (is_array($relation)) { + $field = $relation; + $relation = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + $relation = strstr($relation, '.', true); + } + + $result = $this->model->eagerly($this, $relation, $field, $joinType, $closure, $first); + + if (!$result) { + unset($with[$key]); + } else { + $first = false; + } + } + + $this->via(); + $this->options['with_join'] = $with; + + return $this; + } + + /** + * 关联统计 + * @access protected + * @param array|string $relations 关联方法名 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + protected function withAggregate($relations, string $aggregate = 'count', $field = '*', bool $subQuery = true) + { + if (empty($this->model)) { + return $this; + } + + if (!$subQuery) { + $this->options['with_aggregate'][] = [(array) $relations, $aggregate, $field]; + return $this; + } + + if (!isset($this->options['field'])) { + $this->field('*'); + } + + $this->model->relationCount($this, (array) $relations, $aggregate, $field, true); + return $this; + } + + /** + * 关联缓存 + * @access public + * @param string|array|bool $relation 关联方法名 + * @param mixed $key 缓存key + * @param integer|\DateTime $expire 缓存有效期 + * @param string $tag 缓存标签 + * @return $this + */ + public function withCache($relation = true, $key = true, $expire = null, string $tag = null) + { + if (empty($this->model)) { + return $this; + } + + if (false === $relation || false === $key || !$this->getConnection()->getCache()) { + return $this; + } + + if ($key instanceof \DateTimeInterface || $key instanceof \DateInterval || (is_int($key) && is_null($expire))) { + $expire = $key; + $key = true; + } + + if (true === $relation || is_numeric($relation)) { + $this->options['with_cache'] = $relation; + return $this; + } + + $relations = (array) $relation; + foreach ($relations as $name => $relation) { + if (!is_numeric($name)) { + $this->options['with_cache'][$name] = is_array($relation) ? $relation : [$key, $relation, $tag]; + } else { + $this->options['with_cache'][$relation] = [$key, $expire, $tag]; + } + } + + return $this; + } + + /** + * 关联统计 + * @access public + * @param string|array $relation 关联方法名 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withCount($relation, bool $subQuery = true) + { + return $this->withAggregate($relation, 'count', '*', $subQuery); + } + + /** + * 关联统计Sum + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withSum($relation, string $field, bool $subQuery = true) + { + return $this->withAggregate($relation, 'sum', $field, $subQuery); + } + + /** + * 关联统计Max + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMax($relation, string $field, bool $subQuery = true) + { + return $this->withAggregate($relation, 'max', $field, $subQuery); + } + + /** + * 关联统计Min + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withMin($relation, string $field, bool $subQuery = true) + { + return $this->withAggregate($relation, 'min', $field, $subQuery); + } + + /** + * 关联统计Avg + * @access public + * @param string|array $relation 关联方法名 + * @param string $field 字段 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withAvg($relation, string $field, bool $subQuery = true) + { + return $this->withAggregate($relation, 'avg', $field, $subQuery); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return $this + */ + public function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '') + { + return $this->model->has($relation, $operator, $count, $id, $joinType, $this); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @return $this + */ + public function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '') + { + return $this->model->hasWhere($relation, $where, $fields, $joinType, $this); + } + + /** + * JSON字段数据转换 + * @access protected + * @param array $result 查询数据 + * @return void + */ + protected function jsonModelResult(array &$result): void + { + $withAttr = $this->options['with_attr']; + foreach ($this->options['json'] as $name) { + if (!isset($result[$name])) { + continue; + } + + $jsonData = json_decode($result[$name], true); + + if (isset($withAttr[$name])) { + foreach ($withAttr[$name] as $key => $closure) { + $jsonData[$key] = $closure($jsonData[$key] ?? null, $jsonData); + } + } + + $result[$name] = !$this->options['json_assoc'] ? (object) $jsonData : $jsonData; + } + } + + /** + * 查询数据转换为模型数据集对象 + * @access protected + * @param array $resultSet 数据集 + * @return ModelCollection + */ + protected function resultSetToModelCollection(array $resultSet): ModelCollection + { + if (empty($resultSet)) { + return $this->model->toCollection(); + } + + $this->options['is_resultSet'] = true; + + foreach ($resultSet as $key => &$result) { + // 数据转换为模型对象 + $this->resultToModel($result); + } + + foreach (['with', 'with_join'] as $with) { + // 关联预载入 + if (!empty($this->options[$with])) { + $result->eagerlyResultSet( + $resultSet, + $this->options[$with], + $this->options['with_relation_attr'], + 'with_join' == $with ? true : false, + $this->options['with_cache'] ?? false + ); + } + } + + // 模型数据集转换 + return $this->model->toCollection($resultSet); + } + + /** + * 查询数据转换为模型对象 + * @access protected + * @param array $result 查询数据 + * @return void + */ + protected function resultToModel(array &$result): void + { + // JSON数据处理 + if (!empty($this->options['json'])) { + $this->jsonModelResult($result); + } + + $result = $this->model->newInstance( + $result, + !empty($this->options['is_resultSet']) ? null : $this->getModelUpdateCondition($this->options), + $this->options + ); + + // 模型数据处理 + foreach ($this->options['filter'] as $filter) { + call_user_func_array($filter, [$result, $this->options]); + } + + // 关联查询 + if (!empty($this->options['relation'])) { + $result->relationQuery($this->options['relation'], $this->options['with_relation_attr']); + } + + // 关联预载入查询 + if (empty($this->options['is_resultSet'])) { + foreach (['with', 'with_join'] as $with) { + if (!empty($this->options[$with])) { + $result->eagerlyResult( + $this->options[$with], + $this->options['with_relation_attr'], + 'with_join' == $with ? true : false, + $this->options['with_cache'] ?? false + ); + } + } + } + + // 关联统计查询 + if (!empty($this->options['with_aggregate'])) { + foreach ($this->options['with_aggregate'] as $val) { + $result->relationCount($this, $val[0], $val[1], $val[2], false); + } + } + + // 动态获取器 + if (!empty($this->options['with_attr'])) { + $result->withAttr($this->options['with_attr']); + } + + foreach (['hidden', 'visible', 'append'] as $name) { + if (!empty($this->options[$name])) { + $result->$name($this->options[$name]); + } + } + + // 刷新原始数据 + $result->refreshOrigin(); + } + +} diff --git a/vendor/topthink/think-orm/src/db/concern/ParamsBind.php b/vendor/topthink/think-orm/src/db/concern/ParamsBind.php new file mode 100644 index 0000000..88293fa --- /dev/null +++ b/vendor/topthink/think-orm/src/db/concern/ParamsBind.php @@ -0,0 +1,133 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\concern; + +use PDO; + +/** + * 参数绑定支持 + */ +trait ParamsBind +{ + /** + * 当前参数绑定 + * @var array + */ + protected $bind = []; + + /** + * 批量参数绑定 + * @access public + * @param array $value 绑定变量值 + * @return $this + */ + public function bind(array $value) + { + $this->bind = array_merge($this->bind, $value); + return $this; + } + + /** + * 单个参数绑定 + * @access public + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @param string $name 绑定标识 + * @return string + */ + public function bindValue($value, int $type = null, string $name = null) + { + $name = $name ?: 'ThinkBind_' . (count($this->bind) + 1) . '_' . mt_rand() . '_'; + + $this->bind[$name] = [$value, $type ?: PDO::PARAM_STR]; + return $name; + } + + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ + public function isBind($key) + { + return isset($this->bind[$key]); + } + + /** + * 设置自动参数绑定 + * @access public + * @param bool $bind 是否自动参数绑定 + * @return $this + */ + public function autoBind(bool $bind) + { + $this->options['auto_bind'] = $bind; + return $this; + } + + /** + * 检测是否开启自动参数绑定 + * @access public + * @return bool + */ + public function isAutoBind(): bool + { + $autoBind = $this->getConfig('auto_param_bind'); + if (null !== $this->getOptions('auto_bind')) { + $autoBind = $this->getOptions('auto_bind'); + } + + return (bool) $autoBind; + } + + /** + * 参数绑定 + * @access public + * @param string $sql 绑定的sql表达式 + * @param array $bind 参数绑定 + * @return void + */ + public function bindParams(string &$sql, array $bind = []): void + { + foreach ($bind as $key => $value) { + if (is_array($value)) { + $name = $this->bindValue($value[0], $value[1], $value[2] ?? null); + } else { + $name = $this->bindValue($value); + } + + if (is_numeric($key)) { + $sql = substr_replace($sql, ':' . $name, strpos($sql, '?'), 1); + } else { + $sql = str_replace(':' . $key, ':' . $name, $sql); + } + } + } + + /** + * 获取绑定的参数 并清空 + * @access public + * @param bool $clear 是否清空绑定数据 + * @return array + */ + public function getBind(bool $clear = true): array + { + $bind = $this->bind; + if ($clear) { + $this->bind = []; + } + + return $bind; + } +} diff --git a/vendor/topthink/think-orm/src/db/connector/Mongo.php b/vendor/topthink/think-orm/src/db/connector/Mongo.php new file mode 100644 index 0000000..418dc50 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/connector/Mongo.php @@ -0,0 +1,1175 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\connector; + +use Closure; +use MongoDB\BSON\ObjectID; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Command; +use MongoDB\Driver\Cursor; +use MongoDB\Driver\Exception\AuthenticationException; +use MongoDB\Driver\Exception\BulkWriteException; +use MongoDB\Driver\Exception\ConnectionException; +use MongoDB\Driver\Exception\InvalidArgumentException; +use MongoDB\Driver\Exception\RuntimeException; +use MongoDB\Driver\Manager; +use MongoDB\Driver\Query as MongoQuery; +use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; +use think\db\BaseQuery; +use think\db\builder\Mongo as Builder; +use think\db\Connection; +use think\db\exception\DbEventException; +use think\db\exception\DbException as Exception; +use think\db\Mongo as Query; + +/** + * Mongo数据库驱动 + * @property Manager[] $links + * @property Manager $linkRead + * @property Manager $linkWrite + */ +class Mongo extends Connection +{ + + // 查询数据类型 + protected $dbName = ''; + protected $typeMap = 'array'; + protected $mongo; // MongoDb Object + protected $cursor; // MongoCursor Object + protected $session_uuid; // sessions会话列表当前会话数组key 随机生成 + protected $sessions = []; // 会话列表 + + /** @var Builder */ + protected $builder; + + // 数据库连接参数配置 + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 是否是复制集 + 'is_replica_set' => false, + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 主键名 + 'pk' => '_id', + // 主键类型 + 'pk_type' => 'ObjectID', + // 数据库表前缀 + 'prefix' => '', + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 开启字段缓存 + 'fields_cache' => false, + // 监听SQL + 'trigger_sql' => true, + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否_id转换为id + 'pk_convert_id' => false, + // typeMap + 'type_map' => ['root' => 'array', 'document' => 'array'], + ]; + + /** + * 获取当前连接器类对应的Query类 + * @access public + * @return string + */ + public function getQueryClass(): string + { + return Query::class; + } + + /** + * 获取当前的builder实例对象 + * @access public + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilderClass(): string + { + return Builder::class; + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @return Manager + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function connect(array $config = [], $linkNum = 0) + { + if (!isset($this->links[$linkNum])) { + if (empty($config)) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + + $this->dbName = $config['database']; + $this->typeMap = $config['type_map']; + + if ($config['pk_convert_id'] && '_id' == $config['pk']) { + $this->config['pk'] = 'id'; + } + + if (empty($config['dsn'])) { + $config['dsn'] = 'mongodb://' . ($config['username'] ? "{$config['username']}" : '') . ($config['password'] ? ":{$config['password']}@" : '') . $config['hostname'] . ($config['hostport'] ? ":{$config['hostport']}" : ''); + } + + $startTime = microtime(true); + + $this->links[$linkNum] = new Manager($config['dsn'], $config['params']); + + if (!empty($config['trigger_sql'])) { + // 记录数据库连接信息 + $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']); + } + + } + + return $this->links[$linkNum]; + } + + /** + * 获取Mongo Manager对象 + * @access public + * @return Manager|null + */ + public function getMongo() + { + return $this->mongo ?: null; + } + + /** + * 设置/获取当前操作的database + * @access public + * @param string $db db + * @return string + */ + public function db(string $db = null) + { + if (is_null($db)) { + return $this->dbName; + } else { + $this->dbName = $db; + } + } + + /** + * 执行查询但只返回Cursor对象 + * @access public + * @param Query $query 查询对象 + * @return Cursor + */ + public function cursor($query) + { + // 分析查询表达式 + $options = $query->parseOptions(); + + // 生成MongoQuery对象 + $mongoQuery = $this->builder->select($query); + + $master = $query->getOptions('master') ? true : false; + + // 执行查询操作 + return $this->getCursor($query, $mongoQuery, $master); + } + + /** + * 执行查询并返回Cursor对象 + * @access public + * @param BaseQuery $query 查询对象 + * @param MongoQuery|Closure $mongoQuery Mongo查询对象 + * @param bool $master 是否主库操作 + * @return Cursor + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function getCursor(BaseQuery $query, $mongoQuery, bool $master = false): Cursor + { + $this->initConnect($master); + $this->db->updateQueryTimes(); + + $options = $query->getOptions(); + $namespace = $options['table']; + + if (false === strpos($namespace, '.')) { + $namespace = $this->dbName . '.' . $namespace; + } + + if (!empty($this->queryStr)) { + // 记录执行指令 + $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr; + } + + if ($mongoQuery instanceof Closure) { + $mongoQuery = $mongoQuery($query); + } + + $readPreference = $options['readPreference'] ?? null; + $this->queryStartTime = microtime(true); + + if ($session = $this->getSession()) { + $this->cursor = $this->mongo->executeQuery($namespace, $query, [ + 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference, + 'session' => $session, + ]); + } else { + $this->cursor = $this->mongo->executeQuery($namespace, $mongoQuery, $readPreference); + } + + // SQL监控 + if (!empty($this->config['trigger_sql'])) { + $this->trigger('', $master); + } + + return $this->cursor; + } + + /** + * 执行查询 返回数据集 + * @access public + * @param MongoQuery $query 查询对象 + * @return mixed + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function query(MongoQuery $query) + { + return $this->mongoQuery($this->newQuery(), $query); + } + + /** + * 执行语句 + * @access public + * @param BulkWrite $bulk + * @return int + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + public function execute(BulkWrite $bulk) + { + return $this->mongoExecute($this->newQuery(), $bulk); + } + + /** + * 执行查询 + * @access protected + * @param BaseQuery $query 查询对象 + * @param MongoQuery|Closure $mongoQuery Mongo查询对象 + * @return array + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + protected function mongoQuery(BaseQuery $query, $mongoQuery): array + { + $options = $query->parseOptions(); + + if ($query->getOptions('cache')) { + // 检查查询缓存 + $cacheItem = $this->parseCache($query, $query->getOptions('cache')); + $key = $cacheItem->getKey(); + + if ($this->cache->has($key)) { + return $this->cache->get($key); + } + } + + if ($mongoQuery instanceof Closure) { + $mongoQuery = $mongoQuery($query); + } + + $master = $query->getOptions('master') ? true : false; + $this->getCursor($query, $mongoQuery, $master); + + $resultSet = $this->getResult($options['typeMap']); + + if (isset($cacheItem) && $resultSet) { + // 缓存数据集 + $cacheItem->set($resultSet); + $this->cacheData($cacheItem); + } + + return $resultSet; + } + + /** + * 执行写操作 + * @access protected + * @param BaseQuery $query + * @param BulkWrite $bulk + * + * @return WriteResult + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + protected function mongoExecute(BaseQuery $query, BulkWrite $bulk) + { + $this->initConnect(true); + $this->db->updateQueryTimes(); + + $options = $query->getOptions(); + + $namespace = $options['table']; + if (false === strpos($namespace, '.')) { + $namespace = $this->dbName . '.' . $namespace; + } + + if (!empty($this->queryStr)) { + // 记录执行指令 + $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr; + } + + $writeConcern = $options['writeConcern'] ?? null; + $this->queryStartTime = microtime(true); + + if ($session = $this->getSession()) { + $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, [ + 'session' => $session, + 'writeConcern' => is_null($writeConcern) ? new WriteConcern(1) : $writeConcern, + ]); + } else { + $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern); + } + + // SQL监控 + if (!empty($this->config['trigger_sql'])) { + $this->trigger(); + } + + $this->numRows = $writeResult->getMatchedCount(); + + if ($query->getOptions('cache')) { + // 清理缓存数据 + $cacheItem = $this->parseCache($query, $query->getOptions('cache')); + $key = $cacheItem->getKey(); + $tag = $cacheItem->getTag(); + + if (isset($key) && $this->cache->has($key)) { + $this->cache->delete($key); + } elseif (!empty($tag) && method_exists($this->cache, 'tag')) { + $this->cache->tag($tag)->clear(); + } + } + + return $writeResult; + } + + /** + * 执行指令 + * @access public + * @param Command $command 指令 + * @param string $dbName 当前数据库名 + * @param ReadPreference $readPreference readPreference + * @param string|array $typeMap 指定返回的typeMap + * @param bool $master 是否主库操作 + * @return array + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null, bool $master = false): array + { + $this->initConnect($master); + $this->db->updateQueryTimes(); + + $this->queryStartTime = microtime(true); + + $dbName = $dbName ?: $this->dbName; + + if (!empty($this->queryStr)) { + $this->queryStr = 'db.' . $this->queryStr; + } + + if ($session = $this->getSession()) { + $this->cursor = $this->mongo->executeCommand($dbName, $command, [ + 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference, + 'session' => $session, + ]); + } else { + $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference); + } + + // SQL监控 + if (!empty($this->config['trigger_sql'])) { + $this->trigger('', $master); + } + + return $this->getResult($typeMap); + } + + /** + * 获得数据集 + * @access protected + * @param string|array $typeMap 指定返回的typeMap + * @return mixed + */ + protected function getResult($typeMap = null): array + { + // 设置结果数据类型 + if (is_null($typeMap)) { + $typeMap = $this->typeMap; + } + + $typeMap = is_string($typeMap) ? ['root' => $typeMap] : $typeMap; + + $this->cursor->setTypeMap($typeMap); + + // 获取数据集 + $result = $this->cursor->toArray(); + + if ($this->getConfig('pk_convert_id')) { + // 转换ObjectID 字段 + foreach ($result as &$data) { + $this->convertObjectID($data); + } + } + + $this->numRows = count($result); + + return $result; + } + + /** + * ObjectID处理 + * @access protected + * @param array $data 数据 + * @return void + */ + protected function convertObjectID(array &$data): void + { + if (isset($data['_id']) && is_object($data['_id'])) { + $data['id'] = $data['_id']->__toString(); + unset($data['_id']); + } + } + + /** + * 数据库日志记录(仅供参考) + * @access public + * @param string $type 类型 + * @param mixed $data 数据 + * @param array $options 参数 + * @return void + */ + public function mongoLog(string $type, $data, array $options = []) + { + if (!$this->config['trigger_sql']) { + return; + } + + if (is_array($data)) { + array_walk_recursive($data, function (&$value) { + if ($value instanceof ObjectID) { + $value = $value->__toString(); + } + }); + } + + switch (strtolower($type)) { + case 'aggregate': + $this->queryStr = 'runCommand(' . ($data ? json_encode($data) : '') . ');'; + break; + case 'find': + $this->queryStr = $type . '(' . ($data ? json_encode($data) : '') . ')'; + + if (isset($options['sort'])) { + $this->queryStr .= '.sort(' . json_encode($options['sort']) . ')'; + } + + if (isset($options['skip'])) { + $this->queryStr .= '.skip(' . $options['skip'] . ')'; + } + + if (isset($options['limit'])) { + $this->queryStr .= '.limit(' . $options['limit'] . ')'; + } + + $this->queryStr .= ';'; + break; + case 'insert': + case 'remove': + $this->queryStr = $type . '(' . ($data ? json_encode($data) : '') . ');'; + break; + case 'update': + $this->queryStr = $type . '(' . json_encode($options) . ',' . json_encode($data) . ');'; + break; + case 'cmd': + $this->queryStr = $data . '(' . json_encode($options) . ');'; + break; + } + + $this->options = $options; + } + + /** + * 获取最近执行的指令 + * @access public + * @return string + */ + public function getLastSql(): string + { + return $this->queryStr; + } + + /** + * 关闭数据库 + * @access public + */ + public function close() + { + $this->mongo = null; + $this->cursor = null; + $this->linkRead = null; + $this->linkWrite = null; + $this->links = []; + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect(bool $master = true): void + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + + $this->mongo = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + + $this->mongo = $this->linkRead; + } + } elseif (!$this->mongo) { + // 默认单数据库 + $this->mongo = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return Manager + */ + protected function multiConnect(bool $master = false): Manager + { + $config = []; + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) { + $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name]; + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + if ($this->config['is_replica_set']) { + return $this->replicaSetConnect(); + } else { + $r = $m; + } + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($config['hostname']) - 1)); + } + + $dbConfig = []; + + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) { + $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0]; + } + + return $this->connect($dbConfig, $r); + } + + /** + * 创建基于复制集的连接 + * @return Manager + */ + public function replicaSetConnect(): Manager + { + $this->dbName = $this->config['database']; + $this->typeMap = $this->config['type_map']; + + $startTime = microtime(true); + + $this->config['params']['replicaSet'] = $this->config['database']; + + $manager = new Manager($this->buildUrl(), $this->config['params']); + + // 记录数据库连接信息 + if (!empty($config['trigger_sql'])) { + $this->trigger('CONNECT:ReplicaSet[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $this->config['dsn']); + } + + return $manager; + } + + /** + * 根据配置信息 生成适用于连接复制集的 URL + * @return string + */ + private function buildUrl(): string + { + $url = 'mongodb://' . ($this->config['username'] ? "{$this->config['username']}" : '') . ($this->config['password'] ? ":{$this->config['password']}@" : ''); + + $hostList = is_string($this->config['hostname']) ? explode(',', $this->config['hostname']) : $this->config['hostname']; + $portList = is_string($this->config['hostport']) ? explode(',', $this->config['hostport']) : $this->config['hostport']; + + for ($i = 0; $i < count($hostList); $i++) { + $url = $url . $hostList[$i] . ':' . $portList[0] . ','; + } + + return rtrim($url, ",") . '/'; + } + + /** + * 插入记录 + * @access public + * @param BaseQuery $query 查询对象 + * @param boolean $getLastInsID 返回自增主键 + * @return mixed + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + public function insert(BaseQuery $query, bool $getLastInsID = false) + { + // 分析查询表达式 + $options = $query->parseOptions(); + + if (empty($options['data'])) { + throw new Exception('miss data to insert'); + } + + // 生成bulk对象 + $bulk = $this->builder->insert($query); + + $writeResult = $this->mongoExecute($query, $bulk); + $result = $writeResult->getInsertedCount(); + + if ($result) { + $data = $options['data']; + $lastInsId = $this->getLastInsID($query); + + if ($lastInsId) { + $pk = $query->getPk(); + $data[$pk] = $lastInsId; + } + + $query->setOption('data', $data); + + $this->db->trigger('after_insert', $query); + + if ($getLastInsID) { + return $lastInsId; + } + } + + return $result; + } + + /** + * 获取最近插入的ID + * @access public + * @param BaseQuery $query 查询对象 + * @return mixed + */ + public function getLastInsID(BaseQuery $query) + { + $id = $this->builder->getLastInsID(); + + if (is_array($id)) { + array_walk($id, function (&$item, $key) { + if ($item instanceof ObjectID) { + $item = $item->__toString(); + } + }); + } elseif ($id instanceof ObjectID) { + $id = $id->__toString(); + } + + return $id; + } + + /** + * 批量插入记录 + * @access public + * @param BaseQuery $query 查询对象 + * @param array $dataSet 数据集 + * @return integer + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + public function insertAll(BaseQuery $query, array $dataSet = []): int + { + // 分析查询表达式 + $query->parseOptions(); + + if (!is_array(reset($dataSet))) { + return 0; + } + + // 生成bulkWrite对象 + $bulk = $this->builder->insertAll($query, $dataSet); + + $writeResult = $this->mongoExecute($query, $bulk); + + return $writeResult->getInsertedCount(); + } + + /** + * 更新记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return int + * @throws Exception + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + public function update(BaseQuery $query): int + { + $query->parseOptions(); + + // 生成bulkWrite对象 + $bulk = $this->builder->update($query); + + $writeResult = $this->mongoExecute($query, $bulk); + + $result = $writeResult->getModifiedCount(); + + if ($result) { + $this->db->trigger('after_update', $query); + } + + return $result; + } + + /** + * 删除记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return int + * @throws Exception + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + * @throws BulkWriteException + */ + public function delete(BaseQuery $query): int + { + // 分析查询表达式 + $query->parseOptions(); + + // 生成bulkWrite对象 + $bulk = $this->builder->delete($query); + + // 执行操作 + $writeResult = $this->mongoExecute($query, $bulk); + + $result = $writeResult->getDeletedCount(); + + if ($result) { + $this->db->trigger('after_delete', $query); + } + + return $result; + } + + /** + * 查找记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return array + * @throws ModelNotFoundException + * @throws DataNotFoundException + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function select(BaseQuery $query): array + { + try { + $this->db->trigger('before_select', $query); + } catch (DbEventException $e) { + return []; + } + + return $this->mongoQuery($query, function ($query) { + return $this->builder->select($query); + }); + } + + /** + * 查找单条记录 + * @access public + * @param BaseQuery $query 查询对象 + * @return array + * @throws ModelNotFoundException + * @throws DataNotFoundException + * @throws AuthenticationException + * @throws InvalidArgumentException + * @throws ConnectionException + * @throws RuntimeException + */ + public function find(BaseQuery $query): array + { + // 事件回调 + try { + $this->db->trigger('before_find', $query); + } catch (DbEventException $e) { + return []; + } + + // 执行查询 + $resultSet = $this->mongoQuery($query, function ($query) { + return $this->builder->select($query, true); + }); + + return $resultSet[0] ?? []; + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @return mixed + */ + public function value(BaseQuery $query, string $field, $default = null) + { + $options = $query->parseOptions(); + + if (isset($options['projection'])) { + $query->removeOption('projection'); + } + + $query->setOption('projection', (array) $field); + + if (!empty($options['cache'])) { + $cacheItem = $this->parseCache($query, $options['cache']); + $key = $cacheItem->getKey(); + + if ($this->cache->has($key)) { + return $this->cache->get($key); + } + } + + $mongoQuery = $this->builder->select($query, true); + + if (isset($options['projection'])) { + $query->setOption('projection', $options['projection']); + } else { + $query->removeOption('projection'); + } + + // 执行查询操作 + $resultSet = $this->mongoQuery($query, $mongoQuery); + + if (!empty($resultSet)) { + $data = array_shift($resultSet); + $result = $data[$field]; + } else { + $result = false; + } + + if (isset($cacheItem) && false !== $result) { + // 缓存数据 + $cacheItem->set($result); + $this->cacheData($cacheItem); + } + + return false !== $result ? $result : $default; + } + + /** + * 得到某个列的数组 + * @access public + * @param BaseQuery $query + * @param string|array $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column(BaseQuery $query, $field, string $key = ''): array + { + $options = $query->parseOptions(); + + if (isset($options['projection'])) { + $query->removeOption('projection'); + } + + if (is_array($field)) { + $field = implode(',', $field); + } + if ($key && '*' != $field) { + $projection = $key . ',' . $field; + } else { + $projection = $field; + } + + $query->field($projection); + + if (!empty($options['cache'])) { + // 判断查询缓存 + $cacheItem = $this->parseCache($query, $options['cache']); + $key = $cacheItem->getKey(); + + if ($this->cache->has($key)) { + return $this->cache->get($key); + } + } + + $mongoQuery = $this->builder->select($query); + + if (isset($options['projection'])) { + $query->setOption('projection', $options['projection']); + } else { + $query->removeOption('projection'); + } + + // 执行查询操作 + $resultSet = $this->mongoQuery($query, $mongoQuery); + + if (('*' == $field || strpos($field, ',')) && $key) { + $result = array_column($resultSet, null, $key); + } elseif (!empty($resultSet)) { + $result = array_column($resultSet, $field, $key); + } else { + $result = []; + } + + if (isset($cacheItem)) { + // 缓存数据 + $cacheItem->set($result); + $this->cacheData($cacheItem); + } + + return $result; + } + + /** + * 执行command + * @access public + * @param BaseQuery $query 查询对象 + * @param string|array|object $command 指令 + * @param mixed $extra 额外参数 + * @param string $db 数据库名 + * @return array + */ + public function cmd(BaseQuery $query, $command, $extra = null, string $db = ''): array + { + if (is_array($command) || is_object($command)) { + + $this->mongoLog('cmd', 'cmd', $command); + + // 直接创建Command对象 + $command = new Command($command); + } else { + // 调用Builder封装的Command对象 + $command = $this->builder->$command($query, $extra); + } + + return $this->command($command, $db); + } + + /** + * 获取数据库字段 + * @access public + * @param mixed $tableName 数据表名 + * @return array + */ + public function getTableFields($tableName): array + { + return []; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction(callable $callback) + { + $this->startTrans(); + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动事务 + * @access public + * @return void + * @throws \PDOException + * @throws \Exception + */ + public function startTrans() + { + $this->initConnect(true); + $this->session_uuid = uniqid(); + $this->sessions[$this->session_uuid] = $this->getMongo()->startSession(); + + $this->sessions[$this->session_uuid]->startTransaction([]); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + if ($session = $this->getSession()) { + $session->commitTransaction(); + $this->setLastSession(); + } + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + if ($session = $this->getSession()) { + $session->abortTransaction(); + $this->setLastSession(); + } + } + + /** + * 结束当前会话,设置上一个会话为当前会话 + * @author klinson + */ + protected function setLastSession() + { + if ($session = $this->getSession()) { + $session->endSession(); + unset($this->sessions[$this->session_uuid]); + if (empty($this->sessions)) { + $this->session_uuid = null; + } else { + end($this->sessions); + $this->session_uuid = key($this->sessions); + } + } + } + + /** + * 获取当前会话 + * @return \MongoDB\Driver\Session|null + * @author klinson + */ + public function getSession() + { + return ($this->session_uuid && isset($this->sessions[$this->session_uuid])) + ? $this->sessions[$this->session_uuid] + : null; + } +} diff --git a/vendor/topthink/think-orm/src/db/connector/Mysql.php b/vendor/topthink/think-orm/src/db/connector/Mysql.php new file mode 100644 index 0000000..fd9e63a --- /dev/null +++ b/vendor/topthink/think-orm/src/db/connector/Mysql.php @@ -0,0 +1,162 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\connector; + +use PDO; +use think\db\PDOConnection; + +/** + * mysql数据库驱动 + */ +class Mysql extends PDOConnection +{ + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn(array $config): string + { + if (!empty($config['socket'])) { + $dsn = 'mysql:unix_socket=' . $config['socket']; + } elseif (!empty($config['hostport'])) { + $dsn = 'mysql:host=' . $config['hostname'] . ';port=' . $config['hostport']; + } else { + $dsn = 'mysql:host=' . $config['hostname']; + } + $dsn .= ';dbname=' . $config['database']; + + if (!empty($config['charset'])) { + $dsn .= ';charset=' . $config['charset']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields(string $tableName): array + { + [$tableName] = explode(' ', $tableName); + + if (false === strpos($tableName, '`')) { + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $tableName = '`' . $tableName . '`'; + } + + $sql = 'SHOW FULL COLUMNS FROM ' . $tableName; + $pdo = $this->getPDOStatement($sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if (!empty($result)) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => 'NO' == $val['null'], + 'default' => $val['default'], + 'primary' => strtolower($val['key']) == 'pri', + 'autoinc' => strtolower($val['extra']) == 'auto_increment', + 'comment' => $val['comment'], + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables(string $dbName = ''): array + { + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + $pdo = $this->getPDOStatement($sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + protected function supportSavepoint(): bool + { + return true; + } + + /** + * 启动XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function startTransXa(string $xid): void + { + $this->initConnect(true); + $this->linkID->exec("XA START '$xid'"); + } + + /** + * 预编译XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function prepareXa(string $xid): void + { + $this->initConnect(true); + $this->linkID->exec("XA END '$xid'"); + $this->linkID->exec("XA PREPARE '$xid'"); + } + + /** + * 提交XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function commitXa(string $xid): void + { + $this->initConnect(true); + $this->linkID->exec("XA COMMIT '$xid'"); + } + + /** + * 回滚XA事务 + * @access public + * @param string $xid XA事务id + * @return void + */ + public function rollbackXa(string $xid): void + { + $this->initConnect(true); + $this->linkID->exec("XA ROLLBACK '$xid'"); + } +} diff --git a/vendor/topthink/think-orm/src/db/connector/Oracle.php b/vendor/topthink/think-orm/src/db/connector/Oracle.php new file mode 100644 index 0000000..c8e957a --- /dev/null +++ b/vendor/topthink/think-orm/src/db/connector/Oracle.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\BaseQuery; +use think\db\PDOConnection; + +/** + * Oracle数据库驱动 + */ +class Oracle extends PDOConnection +{ + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn(array $config): string + { + $dsn = 'oci:dbname='; + + if (!empty($config['hostname'])) { + // Oracle Instant Client + $dsn .= '//' . $config['hostname'] . ($config['hostport'] ? ':' . $config['hostport'] : '') . '/'; + } + + $dsn .= $config['database']; + + if (!empty($config['charset'])) { + $dsn .= ';charset=' . $config['charset']; + } + + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields(string $tableName): array + { + [$tableName] = explode(' ', $tableName); + $sql = "select a.column_name,data_type,DECODE (nullable, 'Y', 0, 1) notnull,data_default, DECODE (A .column_name,b.column_name,1,0) pk from all_tab_columns a,(select column_name from all_constraints c, all_cons_columns col where c.constraint_name = col.constraint_name and c.constraint_type = 'P' and c.table_name = '" . $tableName . "' ) b where table_name = '" . $tableName . "' and a.column_name = b.column_name (+)"; + + $pdo = $this->getPDOStatement($sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + + $info[$val['column_name']] = [ + 'name' => $val['column_name'], + 'type' => $val['data_type'], + 'notnull' => $val['notnull'], + 'default' => $val['data_default'], + 'primary' => $val['pk'], + 'autoinc' => $val['pk'], + ]; + } + } + + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息(暂时实现取得用户表信息) + * @access public + * @param string $dbName + * @return array + */ + public function getTables(string $dbName = ''): array + { + $sql = 'select table_name from all_tables'; + $pdo = $this->getPDOStatement($sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + + return $info; + } + + /** + * 获取最近插入的ID + * @access public + * @param BaseQuery $query 查询对象 + * @param string|null $sequence 自增序列名 + * @return mixed + */ + public function getLastInsID(BaseQuery $query, string $sequence = null) + { + if(!is_null($sequence)) { + $pdo = $this->linkID->query("select {$sequence}.currval as id from dual"); + $result = $pdo->fetchColumn(); + } + + return $result ?? null; + } + + protected function supportSavepoint(): bool + { + return true; + } +} diff --git a/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php b/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php new file mode 100644 index 0000000..047e45e --- /dev/null +++ b/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); +namespace think\db\exception; + +use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface; + +/** + * 非法数据异常 + */ +class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentInterface +{ +} diff --git a/vendor/topthink/think-orm/src/db/exception/ModelEventException.php b/vendor/topthink/think-orm/src/db/exception/ModelEventException.php new file mode 100644 index 0000000..767bc1a --- /dev/null +++ b/vendor/topthink/think-orm/src/db/exception/ModelEventException.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +/** + * 模型事件异常 + */ +class ModelEventException extends DbException +{ +} diff --git a/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php b/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php new file mode 100644 index 0000000..84a1525 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\exception; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @access public + * @param string $message + * @param string $model + * @param array $config + */ + public function __construct(string $message, string $model = '', array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/vendor/topthink/think-orm/src/db/exception/PDOException.php b/vendor/topthink/think-orm/src/db/exception/PDOException.php new file mode 100644 index 0000000..efe78b9 --- /dev/null +++ b/vendor/topthink/think-orm/src/db/exception/PDOException.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\db\exception; + +/** + * PDO异常处理类 + * 重新封装了系统的\PDOException类 + */ +class PDOException extends DbException +{ + /** + * PDOException constructor. + * @access public + * @param \PDOException $exception + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct(\PDOException $exception, array $config = [], string $sql = '', int $code = 10501) + { + $error = $exception->errorInfo; + $message = $exception->getMessage(); + + if (!empty($error)) { + $this->setData('PDO Error Info', [ + 'SQLSTATE' => $error[0], + 'Driver Error Code' => isset($error[1]) ? $error[1] : 0, + 'Driver Error Message' => isset($error[2]) ? $error[2] : '', + ]); + } + + parent::__construct($message, $config, $sql, $code); + } +} diff --git a/vendor/topthink/think-orm/src/model/concern/ModelEvent.php b/vendor/topthink/think-orm/src/model/concern/ModelEvent.php new file mode 100644 index 0000000..d2388ab --- /dev/null +++ b/vendor/topthink/think-orm/src/model/concern/ModelEvent.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\model\concern; + +use think\db\exception\ModelEventException; +use think\helper\Str; + +/** + * 模型事件处理 + */ +trait ModelEvent +{ + + /** + * Event对象 + * @var object + */ + protected static $event; + + /** + * 是否需要事件响应 + * @var bool + */ + protected $withEvent = true; + + /** + * 设置Event对象 + * @access public + * @param object $event Event对象 + * @return void + */ + public static function setEvent($event) + { + self::$event = $event; + } + + /** + * 当前操作的事件响应 + * @access protected + * @param bool $event 是否需要事件响应 + * @return $this + */ + public function withEvent(bool $event) + { + $this->withEvent = $event; + return $this; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @return bool + */ + protected function trigger(string $event): bool + { + if (!$this->withEvent) { + return true; + } + + $call = 'on' . Str::studly($event); + + try { + if (method_exists(static::class, $call)) { + $result = call_user_func([static::class, $call], $this); + } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) { + $result = self::$event->trigger('model.' . static::class . '.' . $event, $this); + $result = empty($result) ? true : end($result); + } else { + $result = true; + } + + return false === $result ? false : true; + } catch (ModelEventException $e) { + return false; + } + } +} diff --git a/vendor/topthink/think-orm/src/model/concern/OptimLock.php b/vendor/topthink/think-orm/src/model/concern/OptimLock.php new file mode 100644 index 0000000..5e61318 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/concern/OptimLock.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\model\concern; + +use think\db\exception\DbException as Exception; + +/** + * 乐观锁 + */ +trait OptimLock +{ + protected function getOptimLockField() + { + return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'lock_version'; + } + + /** + * 数据检查 + * @access protected + * @return void + */ + protected function checkData(): void + { + $this->isExists() ? $this->updateLockVersion() : $this->recordLockVersion(); + } + + /** + * 记录乐观锁 + * @access protected + * @return void + */ + protected function recordLockVersion(): void + { + $optimLock = $this->getOptimLockField(); + + if ($optimLock) { + $this->set($optimLock, 0); + } + } + + /** + * 更新乐观锁 + * @access protected + * @return void + */ + protected function updateLockVersion(): void + { + $optimLock = $this->getOptimLockField(); + + if ($optimLock && $lockVer = $this->getOrigin($optimLock)) { + // 更新乐观锁 + $this->set($optimLock, $lockVer + 1); + } + } + + public function getWhere() + { + $where = parent::getWhere(); + $optimLock = $this->getOptimLockField(); + + if ($optimLock && $lockVer = $this->getOrigin($optimLock)) { + $where[] = [$optimLock, '=', $lockVer]; + } + + return $where; + } + + protected function checkResult($result): void + { + if (!$result) { + throw new Exception('record has update'); + } + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/HasMany.php b/vendor/topthink/think-orm/src/model/relation/HasMany.php new file mode 100644 index 0000000..4624955 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/HasMany.php @@ -0,0 +1,376 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\model\relation; + +use Closure; +use think\Collection; +use think\db\BaseQuery as Query; +use think\Model; +use think\model\Relation; + +/** + * 一对多关联类 + */ +class HasMany extends Relation +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, string $model, string $foreignKey, string $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation(array $subRelation = [], Closure $closure = null): Collection + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + return $this->query + ->where($this->foreignKey, $this->parent->{$this->localKey}) + ->relation($subRelation) + ->select() + ->setParent(clone $this->parent); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $data = $this->eagerlyOneToMany([ + [$this->foreignKey, 'in', $range], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + + if (isset($result->$localKey)) { + $pk = $result->$localKey; + $data = $this->eagerlyOneToMany([ + [$this->foreignKey, '=', $pk], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null) + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query->alias($aggregate . '_table') + ->whereExp($aggregate . '_table.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param array $subRelation 子关联 + * @param Closure $closure + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyOneToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($this->foreignKey); + + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + $this->baseQuery = true; + $closure($this->getClosureType($closure)); + } + + if ($this->withoutField) { + $this->query->withoutField($this->withoutField); + } + + $withLimit = $this->query->getOptions('limit'); + if ($withLimit) { + $this->query->removeOption('limit'); + } + + $list = $this->query + ->where($where) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->with($subRelation) + ->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $key = $set->$foreignKey; + + if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) { + continue; + } + + $data[$key][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 + * @param boolean $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, bool $replace = true) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = $this->make(); + + return $model->replace($replace)->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array|Model $data + * @return Model + */ + public function make($data = []): Model + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param iterable $dataSet 数据集 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + */ + public function saveAll(iterable $dataSet, bool $replace = true) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data, $replace); + } + + return empty($result) ? false : $result; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', Query $query = null): Query + { + $table = $this->query->getTable(); + + $model = class_basename($this->parent); + $relation = class_basename($this->model); + + if ('*' != $id) { + $id = $relation . '.' . (new $this->model)->getPk(); + } + + $softDelete = $this->query->getOptions('soft_delete'); + $query = $query ?: $this->parent->db()->alias($model); + + return $query->field($model . '.*') + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relation . '.' . $this->foreignKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query + { + $table = $this->query->getTable(); + $model = class_basename($this->parent); + $relation = class_basename($this->model); + + if (is_array($where)) { + $this->getQueryWhere($where, $relation); + } elseif ($where instanceof Query) { + $where->via($relation); + } elseif ($where instanceof Closure) { + $where($this->query->via($relation)); + $where = $this->query; + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + $query = $query ?: $this->parent->db(); + + return $query->alias($model) + ->group($model . '.' . $this->localKey) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey}); + } + + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/HasManyThrough.php b/vendor/topthink/think-orm/src/model/relation/HasManyThrough.php new file mode 100644 index 0000000..4f14399 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/HasManyThrough.php @@ -0,0 +1,395 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Collection; +use think\db\BaseQuery as Query; +use think\helper\Str; +use think\Model; +use think\model\Relation; + +/** + * 远程一对多关联类 + */ +class HasManyThrough extends Relation +{ + /** + * 中间关联表外键 + * @var string + */ + protected $throughKey; + + /** + * 中间主键 + * @var string + */ + protected $throughPk; + + /** + * 中间表查询对象 + * @var Query + */ + protected $through; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 关联模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 中间关联外键 + * @param string $localKey 当前模型主键 + * @param string $throughPk 中间模型主键 + */ + public function __construct(Model $parent, string $model, string $through, string $foreignKey, string $throughKey, string $localKey, string $throughPk) + { + $this->parent = $parent; + $this->model = $model; + $this->through = (new $through)->db(); + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->localKey = $localKey; + $this->throughPk = $throughPk; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation(array $subRelation = [], Closure $closure = null) + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $this->baseQuery(); + + return $this->query->relation($subRelation) + ->select() + ->setParent(clone $this->parent); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query + { + $model = Str::snake(class_basename($this->parent)); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $relation = new $this->model; + $relationTable = $relation->getTable(); + $softDelete = $this->query->getOptions('soft_delete'); + + if ('*' != $id) { + $id = $relationTable . '.' . $relation->getPk(); + } + $query = $query ?: $this->parent->db()->alias($model); + + return $query->field($model . '.*') + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($relationTable, $relationTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk) + ->when($softDelete, function ($query) use ($softDelete, $relationTable) { + $query->where($relationTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($relationTable . '.' . $this->throughKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, $joinType = '', Query $query = null): Query + { + $model = Str::snake(class_basename($this->parent)); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = (new $this->model)->getTable(); + + if (is_array($where)) { + $this->getQueryWhere($where, $modelTable); + } elseif ($where instanceof Query) { + $where->via($modelTable); + } elseif ($where instanceof Closure) { + $where($this->query->via($modelTable)); + $where = $this->query; + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + $query = $query ?: $this->parent->db(); + + return $query->alias($model) + ->join($throughTable, $throughTable . '.' . $this->foreignKey . '=' . $model . '.' . $this->localKey) + ->join($modelTable, $modelTable . '.' . $throughKey . '=' . $throughTable . '.' . $this->throughPk, $joinType) + ->when($softDelete, function ($query) use ($softDelete, $modelTable) { + $query->where($modelTable . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->group($modelTable . '.' . $this->throughKey) + ->where($where) + ->field($fields); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$this->foreignKey, 'in', $range], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + $pk = $result->$localKey; + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + // 设置关联属性 + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $pk = $result->$localKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $pk], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + + /** + * 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param array $subRelation 子关联 + * @param Closure $closure + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + // 预载入关联查询 支持嵌套预载入 + $throughList = $this->through->where($where)->select(); + $keys = $throughList->column($this->throughPk, $this->throughPk); + + if ($closure) { + $this->baseQuery = true; + $closure($this->getClosureType($closure)); + } + + $throughKey = $this->throughKey; + + if ($this->baseQuery) { + $throughKey = Str::snake(class_basename($this->model)) . "." . $this->throughKey; + } + + $withLimit = $this->query->getOptions('limit'); + if ($withLimit) { + $this->query->removeOption('limit'); + } + + $list = $this->query + ->where($throughKey, 'in', $keys) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + + // 组装模型数据 + $data = []; + $keys = $throughList->column($this->foreignKey, $this->throughPk); + + foreach ($list as $set) { + $key = $keys[$set->{$this->throughKey}]; + + if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) { + continue; + } + + $data[$key][] = $set; + } + + return $data; + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return mixed + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null) + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + $alias = Str::snake(class_basename($this->model)); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $result->$localKey) + ->$aggregate($field); + } + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + $alias = Str::snake(class_basename($this->model)); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if (false === strpos($field, '.')) { + $field = $alias . '.' . $field; + } + + return $this->query + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->whereExp($throughTable . '.' . $this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $alias = Str::snake(class_basename($this->model)); + $throughTable = $this->through->getTable(); + $pk = $this->throughPk; + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + + if ($this->withoutField) { + $this->query->withoutField($this->withoutField); + } + + $fields = $this->getQueryFields($alias); + + $this->query + ->field($fields) + ->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/HasOne.php b/vendor/topthink/think-orm/src/model/relation/HasOne.php new file mode 100644 index 0000000..b941f6e --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/HasOne.php @@ -0,0 +1,304 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); + +namespace think\model\relation; + +use Closure; +use think\db\BaseQuery as Query; +use think\Model; + +/** + * HasOne 关联类 + */ +class HasOne extends OneToOne +{ + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, string $model, string $foreignKey, string $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + + if (get_class($parent) == $model) { + $this->selfRelation = true; + } + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation(array $subRelation = [], Closure $closure = null) + { + $localKey = $this->localKey; + + if ($closure) { + $closure($this->getClosureType($closure)); + } + + // 判断关联类型执行查询 + $relationModel = $this->query + ->removeWhereField($this->foreignKey) + ->where($this->foreignKey, $this->parent->$localKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($this->parent, $relationModel); + } + + $relationModel->setParent(clone $this->parent); + } else { + $relationModel = $this->getDefaultModel(); + } + + return $relationModel; + } + + /** + * 创建关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query + ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->localKey) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null) + { + $localKey = $this->localKey; + + if (!isset($result->$localKey)) { + return 0; + } + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query + ->where($this->foreignKey, '=', $result->$localKey) + ->$aggregate($field); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query + { + $table = $this->query->getTable(); + $model = class_basename($this->parent); + $relation = class_basename($this->model); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $softDelete = $this->query->getOptions('soft_delete'); + $query = $query ?: $this->parent->db()->alias($model); + + return $query->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey, $softDelete) { + $query->table([$table => $relation]) + ->field($relation . '.' . $foreignKey) + ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null): Query + { + $table = $this->query->getTable(); + $model = class_basename($this->parent); + $relation = class_basename($this->model); + + if (is_array($where)) { + $this->getQueryWhere($where, $relation); + } elseif ($where instanceof Query) { + $where->via($relation); + } elseif ($where instanceof Closure) { + $where($this->query->via($relation)); + $where = $this->query; + } + + $fields = $this->getRelationQueryFields($fields, $model); + $softDelete = $this->query->getOptions('soft_delete'); + $query = $query ?: $this->parent->db(); + + return $query->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType ?: $this->joinType) + ->when($softDelete, function ($query) use ($softDelete, $relation) { + $query->where($relation . strstr($softDelete[0], '.'), '=' == $softDelete[1][0] ? $softDelete[1][1] : null); + }) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, 'in', $range], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = $this->getDefaultModel(); + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + // 设置关联属性 + $result->setRelation($relation, $relationModel); + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($result, $relationModel); + $result->hidden([$relation], true); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $result->$localKey], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = $this->getDefaultModel(); + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + // 设置关联属性 + $result->setRelation($relation, $relationModel); + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($result, $relationModel); + $result->hidden([$relation], true); + } + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey}); + } + + $this->baseQuery = true; + } + } +} diff --git a/vendor/topthink/think-orm/src/model/relation/HasOneThrough.php b/vendor/topthink/think-orm/src/model/relation/HasOneThrough.php new file mode 100644 index 0000000..2734c13 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/HasOneThrough.php @@ -0,0 +1,164 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Model; + +/** + * 远程一对一关联类 + */ +class HasOneThrough extends HasManyThrough +{ + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation(array $subRelation = [], Closure $closure = null) + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $this->baseQuery(); + + $relationModel = $this->query->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } else { + $relationModel = $this->getDefaultModel(); + } + + return $relationModel; + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$this->foreignKey, 'in', $range], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = $this->getDefaultModel(); + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + // 设置关联属性 + $result->setRelation($relation, $relationModel); + } + } + } + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $this->query->removeWhereField($foreignKey); + + $data = $this->eagerlyWhere([ + [$foreignKey, '=', $result->$localKey], + ], $foreignKey, $subRelation, $closure, $cache); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = $this->getDefaultModel(); + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + $result->setRelation($relation, $relationModel); + } + + /** + * 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param array $subRelation 子关联 + * @param Closure $closure + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + // 预载入关联查询 支持嵌套预载入 + $keys = $this->through->where($where)->column($this->throughPk, $this->foreignKey); + + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $list = $this->query + ->where($this->throughKey, 'in', $keys) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + + // 组装模型数据 + $data = []; + $keys = array_flip($keys); + + foreach ($list as $set) { + $data[$keys[$set->{$this->throughKey}]] = $set; + } + + return $data; + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/MorphMany.php b/vendor/topthink/think-orm/src/model/relation/MorphMany.php new file mode 100644 index 0000000..ed0dd40 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/MorphMany.php @@ -0,0 +1,385 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\Collection; +use think\db\BaseQuery as Query; +use think\db\exception\DbException as Exception; +use think\Model; +use think\model\Relation; + +/** + * 多态一对多关联 + */ +class MorphMany extends Relation +{ + + /** + * 多态关联外键 + * @var string + */ + protected $morphKey; + + /** + * 多态字段名 + * @var string + */ + protected $morphType; + + /** + * 多态类型 + * @var string + */ + protected $type; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Collection + */ + public function getRelation(array $subRelation = [], Closure $closure = null): Collection + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $this->baseQuery(); + + return $this->query->relation($subRelation) + ->select() + ->setParent(clone $this->parent); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null) + { + throw new Exception('relation not support: has'); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $where = [ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ]; + $data = $this->eagerlyMorphToMany($where, $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent)); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $key = $result->$pk; + $data = $this->eagerlyMorphToMany([ + [$this->morphKey, '=', $key], + [$this->morphType, '=', $this->type], + ], $subRelation, $closure, $cache); + + if (!isset($data[$key])) { + $data[$key] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$key], clone $this->parent)); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return mixed + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null) + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query + ->where([ + [$this->morphKey, '=', $result->$pk], + [$this->morphType, '=', $this->type], + ]) + ->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->query + ->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk()) + ->where($this->morphType, '=', $this->type) + ->fetchSql() + ->$aggregate($field); + } + + /** + * 多态一对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param array $subRelation 子关联 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyMorphToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + // 预载入关联查询 支持嵌套预载入 + $this->query->removeOption('where'); + + if ($closure) { + $this->baseQuery = true; + $closure($this->getClosureType($closure)); + } + + $withLimit = $this->query->getOptions('limit'); + if ($withLimit) { + $this->query->removeOption('limit'); + } + + $list = $this->query + ->where($where) + ->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $key = $set->$morphKey; + + if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) { + continue; + } + + $data[$key][] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 + * @param bool $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, bool $replace = true) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = $this->make(); + + return $model->replace($replace)->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array|Model $data + * @return Model + */ + public function make($data = []): Model + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param iterable $dataSet 数据集 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + */ + public function saveAll(iterable $dataSet, bool $replace = true) + { + $result = []; + + foreach ($dataSet as $key => $data) { + $result[] = $this->save($data, $replace); + } + + return empty($result) ? false : $result; + } + + /** + * 获取多态关联外键 + * @return string + */ + public function getMorphKey() + { + return $this->morphKey; + } + + /** + * 获取多态字段名 + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + + /** + * 获取多态类型 + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + + $this->baseQuery = true; + } + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/MorphOne.php b/vendor/topthink/think-orm/src/model/relation/MorphOne.php new file mode 100644 index 0000000..d63966f --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/MorphOne.php @@ -0,0 +1,352 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\BaseQuery as Query; +use think\db\exception\DbException as Exception; +use think\Model; +use think\model\Relation; + +/** + * 多态一对一关联类 + */ +class MorphOne extends Relation +{ + /** + * 多态关联外键 + * @var string + */ + protected $morphKey; + + /** + * 多态字段 + * @var string + */ + protected $morphType; + + /** + * 多态类型 + * @var string + */ + protected $type; + + /** + * 绑定的关联属性 + * @var array + */ + protected $bindAttr = []; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation(array $subRelation = [], Closure $closure = null) + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $this->baseQuery(); + + $relationModel = $this->query->relation($subRelation)->find(); + + if ($relationModel) { + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($this->parent, $relationModel); + } + + $relationModel->setParent(clone $this->parent); + } else { + $relationModel = $this->getDefaultModel(); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null) + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToOne([ + [$morphKey, 'in', $range], + [$morphType, '=', $type], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $relationModel = $this->getDefaultModel(); + } else { + $relationModel = $data[$result->$pk]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($result, $relationModel); + } else { + // 设置关联属性 + $result->setRelation($relation, $relationModel); + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + $data = $this->eagerlyMorphToOne([ + [$this->morphKey, '=', $pk], + [$this->morphType, '=', $this->type], + ], $subRelation, $closure, $cache); + + if (isset($data[$pk])) { + $relationModel = $data[$pk]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } else { + $relationModel = $this->getDefaultModel(); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($result, $relationModel); + } else { + // 设置关联属性 + $result->setRelation($relation, $relationModel); + } + } + } + + /** + * 多态一对一 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param array $subRelation 子关联 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyMorphToOne(array $where, array $subRelation = [], $closure = null, array $cache = []): array + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + $this->baseQuery = true; + $closure($this->getClosureType($closure)); + } + + $list = $this->query + ->where($where) + ->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + $morphKey = $this->morphKey; + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + $data[$set->$morphKey] = $set; + } + + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 + * @param boolean $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, bool $replace = true) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = $this->make(); + return $model->replace($replace)->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array|Model $data + * @return Model + */ + public function make($data = []): Model + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + + $this->query->where([ + [$this->morphKey, '=', $this->parent->$pk], + [$this->morphType, '=', $this->type], + ]); + $this->baseQuery = true; + } + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param array $attr 要绑定的属性列表 + * @return $this + */ + public function bind(array $attr) + { + $this->bindAttr = $attr; + + return $this; + } + + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr(): array + { + return $this->bindAttr; + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $result 父模型对象 + * @param Model $model 关联模型对象 + * @return void + * @throws Exception + */ + protected function bindAttr(Model $result, Model $model = null): void + { + foreach ($this->bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $result->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $result->setAttr($key, $model ? $model->$attr : null); + } + } +} diff --git a/vendor/topthink/think-orm/src/model/relation/MorphTo.php b/vendor/topthink/think-orm/src/model/relation/MorphTo.php new file mode 100644 index 0000000..d24c082 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/MorphTo.php @@ -0,0 +1,382 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\exception\DbException as Exception; +use think\db\Query; +use think\helper\Str; +use think\Model; +use think\model\Relation; + +/** + * 多态关联类 + */ +class MorphTo extends Relation +{ + /** + * 多态关联外键 + * @var string + */ + protected $morphKey; + + /** + * 多态字段 + * @var string + */ + protected $morphType; + + /** + * 多态别名 + * @var array + */ + protected $alias = []; + + /** + * 关联名 + * @var string + */ + protected $relation; + + protected $queryCaller = []; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $morphType 多态字段名 + * @param string $morphKey 外键名 + * @param array $alias 多态别名定义 + * @param ?string $relation 关联名 + */ + public function __construct(Model $parent, string $morphType, string $morphKey, array $alias = [], string $relation = null) + { + $this->parent = $parent; + $this->morphType = $morphType; + $this->morphKey = $morphKey; + $this->alias = $alias; + $this->relation = $relation; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel(): Model + { + $morphType = $this->morphType; + $model = $this->parseModel($this->parent->$morphType); + + return (new $model); + } + + /** + * 延迟获取关联数据 + * @access public + * @param array $subRelation 子关联名 + * @param ?Closure $closure 闭包查询条件 + * @return Model + */ + public function getRelation(array $subRelation = [], Closure $closure = null) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + // 多态模型 + $model = $this->parseModel($this->parent->$morphType); + + // 主键数据 + $pk = $this->parent->$morphKey; + + $relationModel = $this->buildQuery((new $model)->relation($subRelation))->find($pk); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @param Query $query Query对象 + * @return Query + */ + public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null) + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @param string $joinType JOIN类型 + * @param ?Query $query Query对象 + * @return Query + */ + public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null) + { + $alias = class_basename($this->parent); + + $types = $this->parent->distinct()->column($this->morphType); + + $query = $query ?: $this->parent->db(); + + return $query->alias($alias) + ->where(function (Query $query) use ($types, $where, $alias) { + foreach ($types as $type) { + if ($type) { + $query->whereExists(function (Query $query) use ($type, $where, $alias) { + $class = $this->parseModel($type); + /** @var Model $model */ + $model = new $class(); + + $table = $model->getTable(); + $query + ->table($table) + ->where($alias . '.' . $this->morphType, $type) + ->whereRaw("`{$alias}`.`{$this->morphKey}`=`{$table}`.`{$model->getPk()}`") + ->where($where); + }, 'OR'); + } + } + }); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return Model + */ + protected function parseModel(string $model): string + { + if (isset($this->alias[$model])) { + $model = $this->alias[$model]; + } + + if (false === strpos($model, '\\')) { + $path = explode('\\', get_class($this->parent)); + array_pop($path); + array_push($path, Str::studly($model)); + $model = implode('\\', $path); + } + + return $model; + } + + /** + * 设置多态别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias(array $alias) + { + $this->alias = $alias; + + return $this; + } + + /** + * 移除关联查询参数 + * @access public + * @return $this + */ + public function removeOption(string $option = '') + { + return $this; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param ?Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + * @throws Exception + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (!empty($result->$morphKey)) { + $range[$result->$morphType][] = $result->$morphKey; + } + } + + if (!empty($range)) { + + foreach ($range as $key => $val) { + // 多态类型映射 + $model = $this->parseModel($key); + $obj = new $model; + if (!\is_null($closure)) { + $obj = $closure($obj); + } + $pk = $obj->getPk(); + $list = $obj->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select($val); + $data = []; + + foreach ($list as $k => $vo) { + $data[$vo->$pk] = $vo; + } + + foreach ($resultSet as $result) { + if ($key == $result->$morphType) { + // 关联模型 + if (!isset($data[$result->$morphKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$morphKey]; + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + $result->setRelation($relation, $relationModel); + } + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param ?Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void + { + // 多态类型映射 + $model = $this->parseModel($result->{$this->morphType}); + + $this->eagerlyMorphToOne($model, $relation, $result, $subRelation, $cache); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param ?Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @return integer + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*') + { + } + + /** + * 多态MorphTo 关联模型预查询 + * @access protected + * @param string $model 关联模型对象 + * @param string $relation 关联名 + * @param Model $result + * @param array $subRelation 子关联 + * @param array $cache 关联缓存 + * @return void + */ + protected function eagerlyMorphToOne(string $model, string $relation, Model $result, array $subRelation = [], array $cache = []): void + { + // 预载入关联查询 支持嵌套预载入 + $pk = $this->parent->{$this->morphKey}; + + $data = null; + + if (\class_exists($model)) { + $data = (new $model)->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->find($pk); + + if ($data) { + $data->setParent(clone $result); + $data->exists(true); + } + } + + $result->setRelation($relation, $data ?: null); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @param string $type 多态类型 + * @return Model + */ + public function associate(Model $model, string $type = ''): Model + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $pk = $model->getPk(); + + $this->parent->setAttr($morphKey, $model->$pk); + $this->parent->setAttr($morphType, $type ?: get_class($model)); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate(): Model + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + $this->parent->setAttr($morphKey, null); + $this->parent->setAttr($morphType, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + + protected function buildQuery(Query $query) + { + foreach ($this->queryCaller as $caller) { + call_user_func_array([$query, $caller[0]], $caller[1]); + } + + return $query; + } + + public function __call($method, $args) + { + $this->queryCaller[] = [$method, $args]; + return $this; + } +} diff --git a/vendor/topthink/think-orm/src/model/relation/MorphToMany.php b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php new file mode 100644 index 0000000..c9d8583 --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php @@ -0,0 +1,490 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use Exception; +use think\db\BaseQuery as Query; +use think\db\Raw; +use think\Model; +use think\model\Pivot; + +/** + * 多态多对多关联 + */ +class MorphToMany extends BelongsToMany +{ + + /** + * 多态关系的模型名映射别名的数组 + * + * @var array + */ + protected static $morphMap = []; + + /** + * 多态字段名 + * @var string + */ + protected $morphType; + + /** + * 多态模型名 + * @var string + */ + protected $morphClass; + + /** + * 是否反向关联 + * @var bool + */ + protected $inverse; + + /** + * 架构函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $middle 中间表名/模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $localKey 当前模型关联键 + * @param bool $inverse 反向关联 + */ + public function __construct(Model $parent, string $model, string $middle, string $morphType, string $morphKey, string $localKey, bool $inverse = false) + { + $this->morphType = $morphType; + $this->inverse = $inverse; + $this->morphClass = $inverse ? $model : get_class($parent); + if (isset(static::$morphMap[$this->morphClass])) { + $this->morphClass = static::$morphMap[$this->morphClass]; + } + + $foreignKey = $inverse ? $morphKey : $localKey; + $localKey = $inverse ? $localKey : $morphKey; + + parent::__construct($parent, $model, $middle, $foreignKey, $localKey); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $pk = $resultSet[0]->getPk(); + $range = []; + + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->localKey, 'in', $range], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$result->$pk], clone $this->parent)); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null, array $cache = []): void + { + $pk = $result->getPk(); + + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany([ + ['pivot.' . $this->localKey, '=', $pk], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ], $subRelation, $closure, $cache); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + + $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent)); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return integer + */ + public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float + { + $pk = $result->getPk(); + + if (!isset($result->$pk)) { + return 0; + } + + $pk = $result->$pk; + + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + ['pivot.' . $this->localKey, '=', $pk], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ])->$aggregate($field); + } + + /** + * 获取关联统计子查询 + * @access public + * @param Closure $closure 闭包 + * @param string $aggregate 聚合查询方法 + * @param string $field 字段 + * @param string $name 统计字段别名 + * @return string + */ + public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string + { + if ($closure) { + $closure($this->getClosureType($closure), $name); + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + ['pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk())], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ])->fetchSql()->$aggregate($field); + } + + /** + * BELONGS TO MANY 关联查询 + * @access protected + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->db()->getTable(); + + if ($this->withoutField) { + $this->query->withoutField($this->withoutField); + } + + $fields = $this->getQueryFields($tableName); + $query = $this->query + ->field($fields) + ->tableField(true, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + + return $query; + } + + /** + * 多对多 关联模型预查询 + * @access protected + * @param array $where 关联预查询条件 + * @param array $subRelation 子关联 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyManyToMany(array $where, array $subRelation = [], Closure $closure = null, array $cache = []): array + { + if ($closure) { + $closure($this->getClosureType($closure)); + } + + $withLimit = $this->query->getOptions('limit'); + if ($withLimit) { + $this->query->removeOption('limit'); + } + + // 预载入关联查询 支持嵌套预载入 + $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where) + ->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + [$name, $attr] = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + + $key = $pivot[$this->localKey]; + + if ($withLimit && isset($data[$key]) && count($data[$key]) >= $withLimit) { + continue; + } + + $set->setRelation($this->pivotDataName, $this->newPivot($pivot)); + + $data[$key][] = $set; + } + + return $data; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + */ + public function attach($data, array $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $id = $model->insertGetId($data); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $id = $data->getKey(); + } + + if (!empty($id)) { + // 保存中间表数据 + $pivot[$this->localKey] = $this->parent->getKey(); + $pivot[$this->morphType] = $this->morphClass; + $ids = (array) $id; + + $result = []; + + foreach ($ids as $id) { + $pivot[$this->foreignKey] = $id; + + $this->pivot->replace() + ->exists(false) + ->data([]) + ->save($pivot); + $result[] = $this->newPivot($pivot); + } + + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot|false + */ + public function attached($data) + { + if ($data instanceof Model) { + $id = $data->getKey(); + } else { + $id = $data; + } + + $pivot = $this->pivot + ->where($this->localKey, $this->parent->getKey()) + ->where($this->morphType, $this->morphClass) + ->where($this->foreignKey, $id) + ->find(); + + return $pivot ?: false; + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, bool $relationDel = false): int + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $id = $data->getKey(); + } + + // 删除中间表数据 + $pivot = [ + [$this->localKey, '=', $this->parent->getKey()], + [$this->morphType, '=', $this->morphClass], + ]; + + if (isset($id)) { + $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id]; + } + + $result = $this->pivot->where($pivot)->delete(); + + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + + return $result; + } + + /** + * 数据同步 + * @access public + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync(array $ids, bool $detaching = true): array + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + + $current = $this->pivot + ->where($this->localKey, $this->parent->getKey()) + ->where($this->morphType, $this->morphClass) + ->column($this->foreignKey); + + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) { + $changes['updated'][] = $id; + } + } + + return $changes; + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery(): void + { + if (empty($this->baseQuery)) { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + + // 关联查询 + $this->belongsToManyQuery($foreignKey, $localKey, [ + ['pivot.' . $localKey, '=', $this->parent->getKey()], + ['pivot.' . $this->morphType, '=', $this->morphClass], + ]); + + $this->baseQuery = true; + } + } + + /** + * 设置或获取多态关系的模型名映射别名的数组 + * + * @param array|null $map + * @param bool $merge + * @return array + */ + public static function morphMap(array $map = null, $merge = true): array + { + if (is_array($map)) { + static::$morphMap = $merge && static::$morphMap + ? $map + static::$morphMap : $map; + } + + return static::$morphMap; + } + +} diff --git a/vendor/topthink/think-orm/src/model/relation/OneToOne.php b/vendor/topthink/think-orm/src/model/relation/OneToOne.php new file mode 100644 index 0000000..d62692a --- /dev/null +++ b/vendor/topthink/think-orm/src/model/relation/OneToOne.php @@ -0,0 +1,359 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use Closure; +use think\db\BaseQuery as Query; +use think\db\exception\DbException as Exception; +use think\helper\Str; +use think\Model; +use think\model\Relation; + +/** + * 一对一关联基础类 + * @package think\model\relation + */ +abstract class OneToOne extends Relation +{ + /** + * JOIN类型 + * @var string + */ + protected $joinType = 'INNER'; + + /** + * 绑定的关联属性 + * @var array + */ + protected $bindAttr = []; + + /** + * 关联名 + * @var string + */ + protected $relation; + + /** + * 设置join类型 + * @access public + * @param string $type JOIN类型 + * @return $this + */ + public function joinType(string $type) + { + $this->joinType = $type; + return $this; + } + + /** + * 预载入关联查询(JOIN方式) + * @access public + * @param Query $query 查询对象 + * @param string $relation 关联名 + * @param mixed $field 关联字段 + * @param string $joinType JOIN方式 + * @param Closure $closure 闭包条件 + * @param bool $first + * @return void + */ + public function eagerly(Query $query, string $relation, $field = true, string $joinType = '', Closure $closure = null, bool $first = false): void + { + $name = Str::snake(class_basename($this->parent)); + + if ($first) { + $table = $query->getTable(); + $query->table([$table => $name]); + + if ($query->getOptions('field')) { + $masterField = $query->getOptions('field'); + $query->removeOption('field'); + } else { + $masterField = true; + } + + $query->tableField($masterField, $table, $name); + } + + // 预载入封装 + $joinTable = $this->query->getTable(); + $joinAlias = $relation; + $joinType = $joinType ?: $this->joinType; + + $query->via($joinAlias); + + if ($this instanceof BelongsTo) { + + $foreignKeyExp = $this->foreignKey; + + if (strpos($foreignKeyExp, '.') === false) { + $foreignKeyExp = $name . '.' . $this->foreignKey; + } + + $joinOn = $foreignKeyExp . '=' . $joinAlias . '.' . $this->localKey; + } else { + + $foreignKeyExp = $this->foreignKey; + + if (strpos($foreignKeyExp, '.') === false) { + $foreignKeyExp = $joinAlias . '.' . $this->foreignKey; + } + + $joinOn = $name . '.' . $this->localKey . '=' . $foreignKeyExp; + } + + if ($closure) { + // 执行闭包查询 + $closure($this->getClosureType($closure, $query)); + + // 使用withField指定获取关联的字段 + if ($this->withField) { + $field = $this->withField; + } + } + + $query->join([$joinTable => $joinAlias], $joinOn, $joinType) + ->tableField($field, $joinTable, $joinAlias, $relation . '__'); + } + + /** + * 预载入关联查询(数据集) + * @access protected + * @param array $resultSet + * @param string $relation + * @param array $subRelation + * @param Closure $closure + * @return mixed + */ + abstract protected function eagerlySet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null); + + /** + * 预载入关联查询(数据) + * @access protected + * @param Model $result + * @param string $relation + * @param array $subRelation + * @param Closure $closure + * @return mixed + */ + abstract protected function eagerlyOne(Model $result, string $relation, array $subRelation = [], Closure $closure = null); + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void + { + if ($join) { + // 模型JOIN关联组装 + foreach ($resultSet as $result) { + $this->match($this->model, $relation, $result); + } + } else { + // IN查询 + $this->eagerlySet($resultSet, $relation, $subRelation, $closure, $cache); + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param array $subRelation 子关联名 + * @param Closure $closure 闭包 + * @param array $cache 关联缓存 + * @param bool $join 是否为JOIN方式 + * @return void + */ + public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = [], bool $join = false): void + { + if ($join) { + // 模型JOIN关联组装 + $this->match($this->model, $relation, $result); + } else { + // IN查询 + $this->eagerlyOne($result, $relation, $subRelation, $closure, $cache); + } + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 + * @param boolean $replace 是否自动识别更新和写入 + * @return Model|false + */ + public function save($data, bool $replace = true) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + $model = $this->make(); + + return $model->replace($replace)->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array|Model $data + * @return Model + */ + public function make($data = []): Model + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return new $this->model($data); + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param array $attr 要绑定的属性列表 + * @return $this + */ + public function bind(array $attr) + { + $this->bindAttr = $attr; + + return $this; + } + + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr(): array + { + return $this->bindAttr; + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match(string $model, string $relation, Model $result): void + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + [$name, $attr] = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + if (isset($list[$relation])) { + $array = array_unique($list[$relation]); + + if (count($array) == 1 && null === current($array)) { + $relationModel = null; + } else { + $relationModel = new $model($list[$relation]); + $relationModel->setParent(clone $result); + $relationModel->exists(true); + } + + if (!empty($this->bindAttr)) { + $this->bindAttr($result, $relationModel); + } + } else { + $relationModel = null; + } + + $result->setRelation($relation, $relationModel); + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $result 父模型对象 + * @param Model $model 关联模型对象 + * @return void + * @throws Exception + */ + protected function bindAttr(Model $result, Model $model = null): void + { + foreach ($this->bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + $value = $result->getOrigin($key); + + if (!is_null($value)) { + throw new Exception('bind attr has exists:' . $key); + } + + $result->setAttr($key, $model ? $model->$attr : null); + } + } + + /** + * 一对一 关联模型预查询(IN方式) + * @access public + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param array $subRelation 子关联 + * @param Closure $closure + * @param array $cache 关联缓存 + * @return array + */ + protected function eagerlyWhere(array $where, string $key, array $subRelation = [], Closure $closure = null, array $cache = []) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + $this->baseQuery = true; + $closure($this->getClosureType($closure)); + } + + if ($this->withField) { + $this->query->field($this->withField); + } elseif ($this->withoutField) { + $this->query->withoutField($this->withoutField); + } + + $list = $this->query + ->where($where) + ->with($subRelation) + ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null) + ->select(); + + // 组装模型数据 + $data = []; + + foreach ($list as $set) { + if (!isset($data[$set->$key])) { + $data[$set->$key] = $set; + } + } + + return $data; + } + +} diff --git a/vendor/topthink/think-orm/stubs/Exception.php b/vendor/topthink/think-orm/stubs/Exception.php new file mode 100644 index 0000000..0fdba9c --- /dev/null +++ b/vendor/topthink/think-orm/stubs/Exception.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace think; + +/** + * 异常基础类 + * @package think + */ +class Exception extends \Exception +{ + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData(string $label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @access public + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } +} diff --git a/vendor/topthink/think-orm/stubs/Facade.php b/vendor/topthink/think-orm/stubs/Facade.php new file mode 100644 index 0000000..d801d8b --- /dev/null +++ b/vendor/topthink/think-orm/stubs/Facade.php @@ -0,0 +1,65 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace think; + +class Facade +{ + /** + * 始终创建新的对象实例 + * @var bool + */ + protected static $alwaysNewInstance; + + protected static $instance; + + /** + * 获取当前Facade对应类名 + * @access protected + * @return string + */ + protected static function getFacadeClass() + {} + + /** + * 创建Facade实例 + * @static + * @access protected + * @param bool $newInstance 是否每次创建新的实例 + * @return object + */ + protected static function createFacade(bool $newInstance = false) + { + $class = static::getFacadeClass() ?: 'think\DbManager'; + + if (static::$alwaysNewInstance) { + $newInstance = true; + } + + if ($newInstance) { + return new $class(); + } + + if (!self::$instance) { + self::$instance = new $class(); + } + + return self::$instance; + + } + + // 调用实际类的方法 + public static function __callStatic($method, $params) + { + return call_user_func_array([static::createFacade(), $method], $params); + } +} diff --git a/vendor/topthink/think-orm/stubs/load_stubs.php b/vendor/topthink/think-orm/stubs/load_stubs.php new file mode 100644 index 0000000..734507e --- /dev/null +++ b/vendor/topthink/think-orm/stubs/load_stubs.php @@ -0,0 +1,9 @@ + $key) { + if (is_callable($key)) { + $item[$index] = call_user_func($key, $val); + } elseif (is_int($index)) { + $item[$key] = $val[$key]; + } else { + $item[$key] = $val[$index]; + } + } + return $item; + }, $arr); + + if (!empty($key)) { + $result = array_combine(array_column($arr, 'id'), $result); + } + + return $result; +} + +function array_value_sort(array $arr) +{ + foreach ($arr as &$value) { + sort($value); + } +} + +function query_mysql_connection_id(ConnectionInterface $connect): int +{ + $cid = $connect->query('SELECT CONNECTION_ID() as cid')[0]['cid']; + return (int) $cid; +} + +function mysql_kill_connection(string $name, $cid) +{ + Db::connect($name)->execute("KILL {$cid}"); +} diff --git a/vendor/topthink/think-trace/LICENSE b/vendor/topthink/think-trace/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/topthink/think-trace/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/topthink/think-trace/src/Html.php b/vendor/topthink/think-trace/src/Html.php new file mode 100644 index 0000000..35b3146 --- /dev/null +++ b/vendor/topthink/think-trace/src/Html.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- +declare (strict_types = 1); +namespace think\trace; + +use think\App; +use think\Response; + +/** + * 页面Trace调试 + */ +class Html +{ + protected $config = [ + 'file' => '', + 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct(array $config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 调试输出接口 + * @access public + * @param App $app 应用实例 + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool|string + */ + public function output(App $app, Response $response, array $log = []) + { + $request = $app->request; + $contentType = $response->getHeader('Content-Type'); + + if ($request->isJson() || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } elseif ($response->getCode() == 204) { + return false; + } + + // 获取基本信息 + $runtime = number_format(microtime(true) - $app->getBeginTime(), 10, '.', ''); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - $app->getBeginMem()) / 1024, 2); + + // 页面Trace信息 + if ($request->host()) { + $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true); + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + $base = [ + '请求信息' => date('Y-m-d H:i:s', $request->time() ?: time()) . ' ' . $uri, + '运行时间' => number_format((float) $runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => $app->db->getQueryTimes() . ' queries', + '缓存信息' => $app->cache->getReadTimes() . ' reads,' . $app->cache->getWriteTimes() . ' writes', + ]; + + if (isset($app->session)) { + $base['会话信息'] = 'SESSION_ID=' . $app->session->getId(); + } + + $info = $this->getFileInfo(); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $item) { + $result = array_merge($result, $log[$item] ?? []); + } + $trace[$title] = $result; + } else { + $trace[$title] = $log[$name] ?? ''; + } + } + } + // 调用Trace页面模板 + ob_start(); + include $this->config['file'] ?: __DIR__ . '/tpl/page_trace.tpl'; + return ob_get_clean(); + } + + /** + * 获取文件加载信息 + * @access protected + * @return integer|array + */ + protected function getFileInfo() + { + $files = get_included_files(); + $info = []; + + foreach ($files as $key => $file) { + $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; + } + + return $info; + } +} diff --git a/vendor/topthink/think-trace/src/tpl/page_trace.tpl b/vendor/topthink/think-trace/src/tpl/page_trace.tpl new file mode 100644 index 0000000..6b1b9a1 --- /dev/null +++ b/vendor/topthink/think-trace/src/tpl/page_trace.tpl @@ -0,0 +1,71 @@ +
+ + +
+
+
+ +
+ + diff --git a/vendor/topthink/think-worker/LICENSE b/vendor/topthink/think-worker/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/topthink/think-worker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/topthink/think-worker/src/Events.php b/vendor/topthink/think-worker/src/Events.php new file mode 100644 index 0000000..7aecf99 --- /dev/null +++ b/vendor/topthink/think-worker/src/Events.php @@ -0,0 +1,97 @@ + +// +---------------------------------------------------------------------- +namespace think\worker; + +use GatewayWorker\Lib\Gateway; +use Workerman\Worker; + +/** + * Worker 命令行服务类 + */ +class Events +{ + /** + * onWorkerStart 事件回调 + * 当businessWorker进程启动时触发。每个进程生命周期内都只会触发一次 + * + * @access public + * @param \Workerman\Worker $businessWorker + * @return void + */ + public static function onWorkerStart(Worker $businessWorker) + { + $app = new Application; + $app->initialize(); + } + + /** + * onConnect 事件回调 + * 当客户端连接上gateway进程时(TCP三次握手完毕时)触发 + * + * @access public + * @param int $client_id + * @return void + */ + public static function onConnect($client_id) + { + Gateway::sendToCurrentClient("Your client_id is $client_id"); + } + + /** + * onWebSocketConnect 事件回调 + * 当客户端连接上gateway完成websocket握手时触发 + * + * @param integer $client_id 断开连接的客户端client_id + * @param mixed $data + * @return void + */ + public static function onWebSocketConnect($client_id, $data) + { + var_export($data); + } + + /** + * onMessage 事件回调 + * 当客户端发来数据(Gateway进程收到数据)后触发 + * + * @access public + * @param int $client_id + * @param mixed $data + * @return void + */ + public static function onMessage($client_id, $data) + { + Gateway::sendToAll($data); + } + + /** + * onClose 事件回调 当用户断开连接时触发的方法 + * + * @param integer $client_id 断开连接的客户端client_id + * @return void + */ + public static function onClose($client_id) + { + GateWay::sendToAll("client[$client_id] logout\n"); + } + + /** + * onWorkerStop 事件回调 + * 当businessWorker进程退出时触发。每个进程生命周期内都只会触发一次。 + * + * @param \Workerman\Worker $businessWorker + * @return void + */ + public static function onWorkerStop(Worker $businessWorker) + { + echo "WorkerStop\n"; + } +} diff --git a/vendor/topthink/think-worker/src/Http.php b/vendor/topthink/think-worker/src/Http.php new file mode 100644 index 0000000..cccc50d --- /dev/null +++ b/vendor/topthink/think-worker/src/Http.php @@ -0,0 +1,290 @@ + +// +---------------------------------------------------------------------- +namespace think\worker; + +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Workerman\Connection\TcpConnection; +use Workerman\Lib\Timer; +use Workerman\Protocols\Http as WorkerHttp; +use Workerman\Worker; + +/** + * Worker http server 命令行服务类 + */ +class Http extends Server +{ + protected $app; + protected $rootPath; + protected $root; + protected $appInit; + protected $monitor; + protected $lastMtime; + /** @var array Mime mapping. */ + protected static $mimeTypeMap = []; + + /** + * 架构函数 + * @access public + * @param string $host 监听地址 + * @param int $port 监听端口 + * @param array $context 参数 + */ + public function __construct($host, $port, $context = []) + { + $this->worker = new Worker('http://' . $host . ':' . $port, $context); + + // 设置回调 + foreach ($this->event as $event) { + if (method_exists($this, $event)) { + $this->worker->$event = [$this, $event]; + } + } + } + + public function setRootPath($path) + { + $this->rootPath = $path; + } + + public function appInit(\Closure $closure) + { + $this->appInit = $closure; + } + + public function setRoot($path) + { + $this->root = $path; + } + + public function setStaticOption($name, $value) + { + Worker::${$name} = $value; + } + + public function setMonitor($interval = 2, $path = []) + { + $this->monitor['interval'] = $interval; + $this->monitor['path'] = (array) $path; + } + + /** + * 设置参数 + * @access public + * @param array $option 参数 + * @return void + */ + public function option(array $option) + { + // 设置参数 + if (!empty($option)) { + foreach ($option as $key => $val) { + $this->worker->$key = $val; + } + } + } + + /** + * onWorkerStart 事件回调 + * @access public + * @param \Workerman\Worker $worker + * @return void + */ + public function onWorkerStart($worker) + { + $this->initMimeTypeMap(); + $this->app = new Application($this->rootPath); + + if ($this->appInit) { + call_user_func_array($this->appInit, [$this->app]); + } + + $this->app->initialize(); + + $this->lastMtime = time(); + + $this->app->workerman = $worker; + + $this->app->bind([ + 'think\Cookie' => Cookie::class, + ]); + + if (0 == $worker->id && $this->monitor) { + $paths = $this->monitor['path']; + $timer = $this->monitor['interval'] ?: 2; + + Timer::add($timer, function () use ($paths) { + foreach ($paths as $path) { + $dir = new RecursiveDirectoryIterator($path); + $iterator = new RecursiveIteratorIterator($dir); + + foreach ($iterator as $file) { + if (pathinfo($file, PATHINFO_EXTENSION) != 'php') { + continue; + } + + if ($this->lastMtime < $file->getMTime()) { + echo '[update]' . $file . "\n"; + posix_kill(posix_getppid(), SIGUSR1); + $this->lastMtime = $file->getMTime(); + return; + } + } + } + }); + } + } + + /** + * onMessage 事件回调 + * @access public + * @param TcpConnection $connection + * @param mixed $data + * @return void + */ + public function onMessage($connection, $data) + { + $uri = parse_url($_SERVER['REQUEST_URI']); + $path = $uri['path'] ?? '/'; + + $file = $this->root . $path; + + if (!is_file($file)) { + $this->app->worker($connection, $data); + } else { + $this->sendFile($connection, $file); + } + } + + /** + * 访问资源文件 + * @access protected + * @param TcpConnection $connection + * @param string $file 文件名 + * @return string + */ + protected function sendFile($connection, $file) + { + $info = stat($file); + $modifiyTime = $info ? date('D, d M Y H:i:s', $info['mtime']) . ' ' . date_default_timezone_get() : ''; + + if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $info) { + // Http 304. + if ($modifiyTime === $_SERVER['HTTP_IF_MODIFIED_SINCE']) { + // 304 + WorkerHttp::header('HTTP/1.1 304 Not Modified'); + // Send nothing but http headers.. + return $connection->close(''); + } + } + + $mimeType = $this->getMimeType($file); + + WorkerHttp::header('HTTP/1.1 200 OK'); + WorkerHttp::header('Connection: keep-alive'); + + if ($mimeType) { + WorkerHttp::header('Content-Type: ' . $mimeType); + } else { + WorkerHttp::header('Content-Type: application/octet-stream'); + $fileinfo = pathinfo($file); + $filename = $fileinfo['filename'] ?? ''; + WorkerHttp::header('Content-Disposition: attachment; filename="' . $filename . '"'); + } + + if ($modifiyTime) { + WorkerHttp::header('Last-Modified: ' . $modifiyTime); + } + + WorkerHttp::header('Content-Length: ' . filesize($file)); + + ob_start(); + readfile($file); + $content = ob_get_clean(); + + return $connection->send($content); + } + + /** + * 获取文件类型信息 + * @access protected + * @param string $filename 文件名 + * @return string + */ + protected function getMimeType(string $filename) + { + $file_info = pathinfo($filename); + $extension = $file_info['extension'] ?? ''; + + if (isset(self::$mimeTypeMap[$extension])) { + $mime = self::$mimeTypeMap[$extension]; + } else { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $filename); + } + + return $mime; + } + + /** + * Init mime map. + * + * @return void + * @throws \Exception + */ + protected function initMimeTypeMap() + { + $mime_file = WorkerHttp::getMimeTypesFile(); + + if (!is_file($mime_file)) { + Worker::log("$mime_file mime.type file not fond"); + return; + } + + $items = file($mime_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if (!is_array($items)) { + Worker::log("get $mime_file mime.type content fail"); + return; + } + + foreach ($items as $content) { + if (preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) { + $mime_type = $match[1]; + $workerman_file_extension_var = $match[2]; + $workerman_file_extension_array = explode(' ', substr($workerman_file_extension_var, 0, -1)); + foreach ($workerman_file_extension_array as $workerman_file_extension) { + self::$mimeTypeMap[$workerman_file_extension] = $mime_type; + } + } + } + } + + /** + * 启动 + * @access public + * @return void + */ + public function start() + { + Worker::runAll(); + } + + /** + * 停止 + * @access public + * @return void + */ + public function stop() + { + Worker::stopAll(); + } +} diff --git a/vendor/topthink/think-worker/src/command/GatewayWorker.php b/vendor/topthink/think-worker/src/command/GatewayWorker.php new file mode 100644 index 0000000..3fca135 --- /dev/null +++ b/vendor/topthink/think-worker/src/command/GatewayWorker.php @@ -0,0 +1,201 @@ + +// +---------------------------------------------------------------------- + +namespace think\worker\command; + +use GatewayWorker\BusinessWorker; +use GatewayWorker\Gateway; +use GatewayWorker\Register; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\input\Option; +use think\console\Output; +use think\facade\Config; +use Workerman\Worker; + +/** + * Worker 命令行类 + */ +class GatewayWorker extends Command +{ + public function configure() + { + $this->setName('worker:gateway') + ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start') + ->addOption('host', 'H', Option::VALUE_OPTIONAL, 'the host of workerman server.', null) + ->addOption('port', 'p', Option::VALUE_OPTIONAL, 'the port of workerman server.', null) + ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the workerman server in daemon mode.') + ->setDescription('GatewayWorker Server for ThinkPHP'); + } + + public function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + + if (DIRECTORY_SEPARATOR !== '\\') { + if (!in_array($action, ['start', 'stop', 'reload', 'restart', 'status', 'connections'])) { + $output->writeln("Invalid argument action:{$action}, Expected start|stop|restart|reload|status|connections ."); + exit(1); + } + + global $argv; + array_shift($argv); + array_shift($argv); + array_unshift($argv, 'think', $action); + } else { + $output->writeln("GatewayWorker Not Support On Windows."); + exit(1); + } + + if ('start' == $action) { + $output->writeln('Starting GatewayWorker server...'); + } + + $option = Config::get('gateway_worker'); + + if ($input->hasOption('host')) { + $host = $input->getOption('host'); + } else { + $host = !empty($option['host']) ? $option['host'] : '0.0.0.0'; + } + + if ($input->hasOption('port')) { + $port = $input->getOption('port'); + } else { + $port = !empty($option['port']) ? $option['port'] : '2347'; + } + + $this->start($host, (int) $port, $option); + } + + /** + * 启动 + * @access public + * @param string $host 监听地址 + * @param integer $port 监听端口 + * @param array $option 参数 + * @return void + */ + public function start(string $host, int $port, array $option = []) + { + $registerAddress = !empty($option['registerAddress']) ? $option['registerAddress'] : '127.0.0.1:1236'; + + if (!empty($option['register_deploy'])) { + // 分布式部署的时候其它服务器可以关闭register服务 + // 注意需要设置不同的lanIp + $this->register($registerAddress); + } + + // 启动businessWorker + if (!empty($option['businessWorker_deploy'])) { + $this->businessWorker($registerAddress, $option['businessWorker'] ?? []); + } + + // 启动gateway + if (!empty($option['gateway_deploy'])) { + $this->gateway($registerAddress, $host, $port, $option); + } + + Worker::runAll(); + } + + /** + * 启动register + * @access public + * @param string $registerAddress + * @return void + */ + public function register(string $registerAddress) + { + // 初始化register + new Register('text://' . $registerAddress); + } + + /** + * 启动businessWorker + * @access public + * @param string $registerAddress registerAddress + * @param array $option 参数 + * @return void + */ + public function businessWorker(string $registerAddress, array $option = []) + { + // 初始化 bussinessWorker 进程 + $worker = new BusinessWorker(); + + $this->option($worker, $option); + + $worker->registerAddress = $registerAddress; + } + + /** + * 启动gateway + * @access public + * @param string $registerAddress registerAddress + * @param string $host 服务地址 + * @param integer $port 监听端口 + * @param array $option 参数 + * @return void + */ + public function gateway(string $registerAddress, string $host, int $port, array $option = []) + { + // 初始化 gateway 进程 + if (!empty($option['socket'])) { + $socket = $option['socket']; + unset($option['socket']); + } else { + $protocol = !empty($option['protocol']) ? $option['protocol'] : 'websocket'; + $socket = $protocol . '://' . $host . ':' . $port; + unset($option['host'], $option['port'], $option['protocol']); + } + + $gateway = new Gateway($socket, $option['context'] ?? []); + + // 以下设置参数都可以在配置文件中重新定义覆盖 + $gateway->name = 'Gateway'; + $gateway->count = 4; + $gateway->lanIp = '127.0.0.1'; + $gateway->startPort = 2000; + $gateway->pingInterval = 30; + $gateway->pingNotResponseLimit = 0; + $gateway->pingData = '{"type":"ping"}'; + $gateway->registerAddress = $registerAddress; + + // 全局静态属性设置 + foreach ($option as $name => $val) { + if (in_array($name, ['stdoutFile', 'daemonize', 'pidFile', 'logFile'])) { + Worker::${$name} = $val; + unset($option[$name]); + } + } + + $this->option($gateway, $option); + } + + /** + * 设置参数 + * @access protected + * @param Worker $worker Worker对象 + * @param array $option 参数 + * @return void + */ + protected function option(Worker $worker, array $option = []) + { + // 设置参数 + if (!empty($option)) { + foreach ($option as $key => $val) { + $worker->$key = $val; + } + } + } + +} diff --git a/vendor/topthink/think-worker/src/config/gateway.php b/vendor/topthink/think-worker/src/config/gateway.php new file mode 100644 index 0000000..21d1ebe --- /dev/null +++ b/vendor/topthink/think-worker/src/config/gateway.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- +// +---------------------------------------------------------------------- +// | Workerman设置 仅对 php think worker:gateway 指令有效 +// +---------------------------------------------------------------------- +return [ + // 扩展自身需要的配置 + 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text + 'host' => '0.0.0.0', // 监听地址 + 'port' => 2348, // 监听端口 + 'socket' => '', // 完整监听地址 + 'context' => [], // socket 上下文选项 + 'register_deploy' => true, // 是否需要部署register + 'businessWorker_deploy' => true, // 是否需要部署businessWorker + 'gateway_deploy' => true, // 是否需要部署gateway + + // Register配置 + 'registerAddress' => '127.0.0.1:1236', + + // Gateway配置 + 'name' => 'thinkphp', + 'count' => 1, + 'lanIp' => '127.0.0.1', + 'startPort' => 2000, + 'daemonize' => false, + 'pingInterval' => 30, + 'pingNotResponseLimit' => 0, + 'pingData' => '{"type":"ping"}', + + // BusinsessWorker配置 + 'businessWorker' => [ + 'name' => 'BusinessWorker', + 'count' => 1, + 'eventHandler' => '\think\worker\Events', + ], + +];
element because TCPDF + // does not recognize e.g.
element because TCPDF + // does not recognize e.g.