, ...].
+ *
+ * @param array $pValue
+ */
+ public function setIDCLs($pValue): void
+ {
+ $this->IDCLs = $pValue;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php
new file mode 100644
index 0000000..b07786f
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php
@@ -0,0 +1,34 @@
+BSECollection[] = $BSE;
+ $BSE->setParent($this);
+ }
+
+ /**
+ * Get the collection of BLIP Store Entries.
+ *
+ * @return BstoreContainer\BSE[]
+ */
+ public function getBSECollection()
+ {
+ return $this->BSECollection;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php
new file mode 100644
index 0000000..e885146
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php
@@ -0,0 +1,89 @@
+parent = $parent;
+ }
+
+ /**
+ * Get the BLIP.
+ *
+ * @return BSE\Blip
+ */
+ public function getBlip()
+ {
+ return $this->blip;
+ }
+
+ /**
+ * Set the BLIP.
+ *
+ * @param BSE\Blip $blip
+ */
+ public function setBlip($blip): void
+ {
+ $this->blip = $blip;
+ $blip->setParent($this);
+ }
+
+ /**
+ * Get the BLIP type.
+ *
+ * @return int
+ */
+ public function getBlipType()
+ {
+ return $this->blipType;
+ }
+
+ /**
+ * Set the BLIP type.
+ *
+ * @param int $blipType
+ */
+ public function setBlipType($blipType): void
+ {
+ $this->blipType = $blipType;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php
new file mode 100644
index 0000000..500d7ea
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php
@@ -0,0 +1,60 @@
+data;
+ }
+
+ /**
+ * Set the raw image data.
+ *
+ * @param string $data
+ */
+ public function setData($data): void
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * Set parent BSE.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent
+ */
+ public function setParent($parent): void
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Get parent BSE.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+}
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..7991ed4
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php
@@ -0,0 +1,153 @@
+open($zipFile) === true) {
+ $returnValue = ($zip->getFromName($archiveFile) !== false);
+ $zip->close();
+
+ return $returnValue;
+ }
+
+ return false;
+ }
+
+ return file_exists($pFilename);
+ }
+
+ /**
+ * Returns canonicalized absolute pathname, also for ZIP archives.
+ *
+ * @param string $pFilename
+ *
+ * @return string
+ */
+ public static function realpath($pFilename)
+ {
+ // Returnvalue
+ $returnValue = '';
+
+ // Try using realpath()
+ if (file_exists($pFilename)) {
+ $returnValue = realpath($pFilename);
+ }
+
+ // Found something?
+ if ($returnValue == '' || ($returnValue === null)) {
+ $pathArray = explode('/', $pFilename);
+ 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.
+ *
+ * @return string
+ */
+ public static function sysGetTempDir()
+ {
+ 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)) {
+ return realpath($temp);
+ }
+ }
+ }
+ }
+
+ return realpath(sys_get_temp_dir());
+ }
+
+ 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.
+ *
+ * @param string $filename
+ */
+ public static function assertFile($filename): void
+ {
+ if (!is_file($filename)) {
+ throw new InvalidArgumentException('File "' . $filename . '" does not exist.');
+ }
+
+ if (!is_readable($filename)) {
+ throw new InvalidArgumentException('Could not open "' . $filename . '" for reading.');
+ }
+ }
+}
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..00629e6
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php
@@ -0,0 +1,759 @@
+ [
+ 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 $pValue see self::AUTOSIZE_METHOD_*
+ *
+ * @return bool Success or failure
+ */
+ public static function setAutoSizeMethod($pValue)
+ {
+ if (!in_array($pValue, self::$autoSizeMethods)) {
+ return false;
+ }
+ self::$autoSizeMethod = $pValue;
+
+ 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:
+ *
+ * C:/Windows/Fonts/
+ * /usr/share/fonts/truetype/
+ * ~/.fonts/
+ * .
+ *
+ * @param string $pValue
+ */
+ public static function setTrueTypeFontPath($pValue): void
+ {
+ self::$trueTypeFontPath = $pValue;
+ }
+
+ /**
+ * 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 \PhpOffice\PhpSpreadsheet\Style\Font $font Font object
+ * @param RichText|string $cellText Text to calculate width
+ * @param int $rotation Rotation angle
+ * @param null|\PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Font object
+ *
+ * @return int Column width
+ */
+ public static function calculateColumnWidth(\PhpOffice\PhpSpreadsheet\Style\Font $font, $cellText = '', $rotation = 0, ?\PhpOffice\PhpSpreadsheet\Style\Font $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($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, \PhpOffice\PhpSpreadsheet\Style\Font $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, \PhpOffice\PhpSpreadsheet\Style\Font $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.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
+ *
+ * @return string Path to TrueType font file
+ */
+ public static function getTrueTypeFontFileFromFont($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 $name Font name
+ *
+ * @return int Character set code
+ */
+ public static function getCharsetFromFontName($name)
+ {
+ switch ($name) {
+ // 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 \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
+ * @param bool $pPixels true = return column width in pixels, false = return in OOXML units
+ *
+ * @return mixed Column width
+ */
+ public static function getDefaultColumnWidthByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font, $pPixels = false)
+ {
+ if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) {
+ // Exact width can be determined
+ $columnWidth = $pPixels ?
+ 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 = $pPixels ?
+ self::$defaultColumnWidths['Calibri'][11]['px']
+ : self::$defaultColumnWidths['Calibri'][11]['width'];
+ $columnWidth = $columnWidth * $font->getSize() / 11;
+
+ // Round pixels to closest integer
+ if ($pPixels) {
+ $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 \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
+ *
+ * @return float Row height in points
+ */
+ public static function getDefaultRowHeightByFont(\PhpOffice\PhpSpreadsheet\Style\Font $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 @@
+L = $A->getArray();
+ $this->m = $A->getRowDimension();
+
+ for ($i = 0; $i < $this->m; ++$i) {
+ for ($j = $i; $j < $this->m; ++$j) {
+ for ($sum = $this->L[$i][$j], $k = $i - 1; $k >= 0; --$k) {
+ $sum -= $this->L[$i][$k] * $this->L[$j][$k];
+ }
+ if ($i == $j) {
+ if ($sum >= 0) {
+ $this->L[$i][$i] = sqrt($sum);
+ } else {
+ $this->isspd = false;
+ }
+ } else {
+ if ($this->L[$i][$i] != 0) {
+ $this->L[$j][$i] = $sum / $this->L[$i][$i];
+ }
+ }
+ }
+
+ for ($k = $i + 1; $k < $this->m; ++$k) {
+ $this->L[$i][$k] = 0.0;
+ }
+ }
+ }
+
+ /**
+ * Is the matrix symmetric and positive definite?
+ *
+ * @return bool
+ */
+ public function isSPD()
+ {
+ return $this->isspd;
+ }
+
+ /**
+ * getL.
+ *
+ * Return triangular factor.
+ *
+ * @return Matrix Lower triangular matrix
+ */
+ public function getL()
+ {
+ return new Matrix($this->L);
+ }
+
+ /**
+ * Solve A*X = B.
+ *
+ * @param Matrix $B Row-equal matrix
+ *
+ * @return Matrix L * L' * X = B
+ */
+ public function solve(Matrix $B)
+ {
+ if ($B->getRowDimension() == $this->m) {
+ if ($this->isspd) {
+ $X = $B->getArray();
+ $nx = $B->getColumnDimension();
+
+ for ($k = 0; $k < $this->m; ++$k) {
+ for ($i = $k + 1; $i < $this->m; ++$i) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$i][$j] -= $X[$k][$j] * $this->L[$i][$k];
+ }
+ }
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$k][$j] /= $this->L[$k][$k];
+ }
+ }
+
+ for ($k = $this->m - 1; $k >= 0; --$k) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$k][$j] /= $this->L[$k][$k];
+ }
+ for ($i = 0; $i < $k; ++$i) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$i][$j] -= $X[$k][$j] * $this->L[$k][$i];
+ }
+ }
+ }
+
+ return new Matrix($X, $this->m, $nx);
+ }
+
+ throw new CalculationException(Matrix::MATRIX_SPD_EXCEPTION);
+ }
+
+ throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php
new file mode 100644
index 0000000..5c6ccfd
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php
@@ -0,0 +1,870 @@
+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 < $low) || ($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 < $low | $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/QRDecomposition.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php
new file mode 100644
index 0000000..9b51f41
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php
@@ -0,0 +1,245 @@
+= n, the QR decomposition is an m-by-n
+ * orthogonal matrix Q and an n-by-n upper triangular matrix R so that
+ * A = Q*R.
+ *
+ * The QR decompostion always exists, even if the matrix does not have
+ * full rank, so the constructor will never fail. The primary use of the
+ * QR decomposition is in the least squares solution of nonsquare systems
+ * of simultaneous linear equations. This will fail if isFullRank()
+ * returns false.
+ *
+ * @author Paul Meagher
+ *
+ * @version 1.1
+ */
+class QRDecomposition
+{
+ const MATRIX_RANK_EXCEPTION = 'Can only perform operation on full-rank matrix.';
+
+ /**
+ * Array for internal storage of decomposition.
+ *
+ * @var array
+ */
+ private $QR = [];
+
+ /**
+ * Row dimension.
+ *
+ * @var int
+ */
+ private $m;
+
+ /**
+ * Column dimension.
+ *
+ * @var int
+ */
+ private $n;
+
+ /**
+ * Array for internal storage of diagonal of R.
+ *
+ * @var array
+ */
+ private $Rdiag = [];
+
+ /**
+ * QR Decomposition computed by Householder reflections.
+ *
+ * @param Matrix $A Rectangular matrix
+ */
+ public function __construct(Matrix $A)
+ {
+ // Initialize.
+ $this->QR = $A->getArray();
+ $this->m = $A->getRowDimension();
+ $this->n = $A->getColumnDimension();
+ // Main loop.
+ for ($k = 0; $k < $this->n; ++$k) {
+ // Compute 2-norm of k-th column without under/overflow.
+ $nrm = 0.0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $nrm = hypo($nrm, $this->QR[$i][$k]);
+ }
+ if ($nrm != 0.0) {
+ // Form k-th Householder vector.
+ if ($this->QR[$k][$k] < 0) {
+ $nrm = -$nrm;
+ }
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->QR[$i][$k] /= $nrm;
+ }
+ $this->QR[$k][$k] += 1.0;
+ // Apply transformation to remaining columns.
+ for ($j = $k + 1; $j < $this->n; ++$j) {
+ $s = 0.0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $s += $this->QR[$i][$k] * $this->QR[$i][$j];
+ }
+ $s = -$s / $this->QR[$k][$k];
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->QR[$i][$j] += $s * $this->QR[$i][$k];
+ }
+ }
+ }
+ $this->Rdiag[$k] = -$nrm;
+ }
+ }
+
+ // function __construct()
+
+ /**
+ * Is the matrix full rank?
+ *
+ * @return bool true if R, and hence A, has full rank, else false
+ */
+ public function isFullRank()
+ {
+ for ($j = 0; $j < $this->n; ++$j) {
+ if ($this->Rdiag[$j] == 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // function isFullRank()
+
+ /**
+ * Return the Householder vectors.
+ *
+ * @return Matrix Lower trapezoidal matrix whose columns define the reflections
+ */
+ public function getH()
+ {
+ $H = [];
+ for ($i = 0; $i < $this->m; ++$i) {
+ for ($j = 0; $j < $this->n; ++$j) {
+ if ($i >= $j) {
+ $H[$i][$j] = $this->QR[$i][$j];
+ } else {
+ $H[$i][$j] = 0.0;
+ }
+ }
+ }
+
+ return new Matrix($H);
+ }
+
+ // function getH()
+
+ /**
+ * Return the upper triangular factor.
+ *
+ * @return Matrix upper triangular factor
+ */
+ public function getR()
+ {
+ $R = [];
+ for ($i = 0; $i < $this->n; ++$i) {
+ for ($j = 0; $j < $this->n; ++$j) {
+ if ($i < $j) {
+ $R[$i][$j] = $this->QR[$i][$j];
+ } elseif ($i == $j) {
+ $R[$i][$j] = $this->Rdiag[$i];
+ } else {
+ $R[$i][$j] = 0.0;
+ }
+ }
+ }
+
+ return new Matrix($R);
+ }
+
+ // function getR()
+
+ /**
+ * Generate and return the (economy-sized) orthogonal factor.
+ *
+ * @return Matrix orthogonal factor
+ */
+ public function getQ()
+ {
+ $Q = [];
+ for ($k = $this->n - 1; $k >= 0; --$k) {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $Q[$i][$k] = 0.0;
+ }
+ $Q[$k][$k] = 1.0;
+ for ($j = $k; $j < $this->n; ++$j) {
+ if ($this->QR[$k][$k] != 0) {
+ $s = 0.0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $s += $this->QR[$i][$k] * $Q[$i][$j];
+ }
+ $s = -$s / $this->QR[$k][$k];
+ for ($i = $k; $i < $this->m; ++$i) {
+ $Q[$i][$j] += $s * $this->QR[$i][$k];
+ }
+ }
+ }
+ }
+
+ return new Matrix($Q);
+ }
+
+ // function getQ()
+
+ /**
+ * Least squares solution of A*X = B.
+ *
+ * @param Matrix $B a Matrix with as many rows as A and any number of columns
+ *
+ * @return Matrix matrix that minimizes the two norm of Q*R*X-B
+ */
+ public function solve(Matrix $B)
+ {
+ if ($B->getRowDimension() == $this->m) {
+ if ($this->isFullRank()) {
+ // Copy right hand side
+ $nx = $B->getColumnDimension();
+ $X = $B->getArray();
+ // Compute Y = transpose(Q)*B
+ for ($k = 0; $k < $this->n; ++$k) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $s = 0.0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $s += $this->QR[$i][$k] * $X[$i][$j];
+ }
+ $s = -$s / $this->QR[$k][$k];
+ for ($i = $k; $i < $this->m; ++$i) {
+ $X[$i][$j] += $s * $this->QR[$i][$k];
+ }
+ }
+ }
+ // Solve R*X = Y;
+ for ($k = $this->n - 1; $k >= 0; --$k) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$k][$j] /= $this->Rdiag[$k];
+ }
+ for ($i = 0; $i < $k; ++$i) {
+ for ($j = 0; $j < $nx; ++$j) {
+ $X[$i][$j] -= $X[$k][$j] * $this->QR[$i][$k];
+ }
+ }
+ }
+ $X = new Matrix($X);
+
+ return $X->getMatrix(0, $this->n - 1, 0, $nx);
+ }
+
+ throw new CalculationException(self::MATRIX_RANK_EXCEPTION);
+ }
+
+ throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php
new file mode 100644
index 0000000..6c8999d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php
@@ -0,0 +1,529 @@
+= n, the singular value decomposition is
+ * an m-by-n orthogonal matrix U, an n-by-n diagonal matrix S, and
+ * an n-by-n orthogonal matrix V so that A = U*S*V'.
+ *
+ * The singular values, sigma[$k] = S[$k][$k], are ordered so that
+ * sigma[0] >= sigma[1] >= ... >= sigma[n-1].
+ *
+ * The singular value decompostion always exists, so the constructor will
+ * never fail. The matrix condition number and the effective numerical
+ * rank can be computed from this decomposition.
+ *
+ * @author Paul Meagher
+ *
+ * @version 1.1
+ */
+class SingularValueDecomposition
+{
+ /**
+ * Internal storage of U.
+ *
+ * @var array
+ */
+ private $U = [];
+
+ /**
+ * Internal storage of V.
+ *
+ * @var array
+ */
+ private $V = [];
+
+ /**
+ * Internal storage of singular values.
+ *
+ * @var array
+ */
+ private $s = [];
+
+ /**
+ * Row dimension.
+ *
+ * @var int
+ */
+ private $m;
+
+ /**
+ * Column dimension.
+ *
+ * @var int
+ */
+ private $n;
+
+ /**
+ * Construct the singular value decomposition.
+ *
+ * Derived from LINPACK code.
+ *
+ * @param mixed $Arg Rectangular matrix
+ */
+ public function __construct($Arg)
+ {
+ // Initialize.
+ $A = $Arg->getArray();
+ $this->m = $Arg->getRowDimension();
+ $this->n = $Arg->getColumnDimension();
+ $nu = min($this->m, $this->n);
+ $e = [];
+ $work = [];
+ $wantu = true;
+ $wantv = true;
+ $nct = min($this->m - 1, $this->n);
+ $nrt = max(0, min($this->n - 2, $this->m));
+
+ // Reduce A to bidiagonal form, storing the diagonal elements
+ // in s and the super-diagonal elements in e.
+ $kMax = max($nct, $nrt);
+ for ($k = 0; $k < $kMax; ++$k) {
+ if ($k < $nct) {
+ // Compute the transformation for the k-th column and
+ // place the k-th diagonal in s[$k].
+ // Compute 2-norm of k-th column without under/overflow.
+ $this->s[$k] = 0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->s[$k] = hypo($this->s[$k], $A[$i][$k]);
+ }
+ if ($this->s[$k] != 0.0) {
+ if ($A[$k][$k] < 0.0) {
+ $this->s[$k] = -$this->s[$k];
+ }
+ for ($i = $k; $i < $this->m; ++$i) {
+ $A[$i][$k] /= $this->s[$k];
+ }
+ $A[$k][$k] += 1.0;
+ }
+ $this->s[$k] = -$this->s[$k];
+ }
+
+ for ($j = $k + 1; $j < $this->n; ++$j) {
+ if (($k < $nct) & ($this->s[$k] != 0.0)) {
+ // Apply the transformation.
+ $t = 0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $t += $A[$i][$k] * $A[$i][$j];
+ }
+ $t = -$t / $A[$k][$k];
+ for ($i = $k; $i < $this->m; ++$i) {
+ $A[$i][$j] += $t * $A[$i][$k];
+ }
+ // Place the k-th row of A into e for the
+ // subsequent calculation of the row transformation.
+ $e[$j] = $A[$k][$j];
+ }
+ }
+
+ if ($wantu && ($k < $nct)) {
+ // Place the transformation in U for subsequent back
+ // multiplication.
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->U[$i][$k] = $A[$i][$k];
+ }
+ }
+
+ if ($k < $nrt) {
+ // Compute the k-th row transformation and place the
+ // k-th super-diagonal in e[$k].
+ // Compute 2-norm without under/overflow.
+ $e[$k] = 0;
+ for ($i = $k + 1; $i < $this->n; ++$i) {
+ $e[$k] = hypo($e[$k], $e[$i]);
+ }
+ if ($e[$k] != 0.0) {
+ if ($e[$k + 1] < 0.0) {
+ $e[$k] = -$e[$k];
+ }
+ for ($i = $k + 1; $i < $this->n; ++$i) {
+ $e[$i] /= $e[$k];
+ }
+ $e[$k + 1] += 1.0;
+ }
+ $e[$k] = -$e[$k];
+ if (($k + 1 < $this->m) && ($e[$k] != 0.0)) {
+ // Apply the transformation.
+ for ($i = $k + 1; $i < $this->m; ++$i) {
+ $work[$i] = 0.0;
+ }
+ for ($j = $k + 1; $j < $this->n; ++$j) {
+ for ($i = $k + 1; $i < $this->m; ++$i) {
+ $work[$i] += $e[$j] * $A[$i][$j];
+ }
+ }
+ for ($j = $k + 1; $j < $this->n; ++$j) {
+ $t = -$e[$j] / $e[$k + 1];
+ for ($i = $k + 1; $i < $this->m; ++$i) {
+ $A[$i][$j] += $t * $work[$i];
+ }
+ }
+ }
+ if ($wantv) {
+ // Place the transformation in V for subsequent
+ // back multiplication.
+ for ($i = $k + 1; $i < $this->n; ++$i) {
+ $this->V[$i][$k] = $e[$i];
+ }
+ }
+ }
+ }
+
+ // Set up the final bidiagonal matrix or order p.
+ $p = min($this->n, $this->m + 1);
+ if ($nct < $this->n) {
+ $this->s[$nct] = $A[$nct][$nct];
+ }
+ if ($this->m < $p) {
+ $this->s[$p - 1] = 0.0;
+ }
+ if ($nrt + 1 < $p) {
+ $e[$nrt] = $A[$nrt][$p - 1];
+ }
+ $e[$p - 1] = 0.0;
+ // If required, generate U.
+ if ($wantu) {
+ for ($j = $nct; $j < $nu; ++$j) {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $this->U[$i][$j] = 0.0;
+ }
+ $this->U[$j][$j] = 1.0;
+ }
+ for ($k = $nct - 1; $k >= 0; --$k) {
+ if ($this->s[$k] != 0.0) {
+ for ($j = $k + 1; $j < $nu; ++$j) {
+ $t = 0;
+ for ($i = $k; $i < $this->m; ++$i) {
+ $t += $this->U[$i][$k] * $this->U[$i][$j];
+ }
+ $t = -$t / $this->U[$k][$k];
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->U[$i][$j] += $t * $this->U[$i][$k];
+ }
+ }
+ for ($i = $k; $i < $this->m; ++$i) {
+ $this->U[$i][$k] = -$this->U[$i][$k];
+ }
+ $this->U[$k][$k] = 1.0 + $this->U[$k][$k];
+ for ($i = 0; $i < $k - 1; ++$i) {
+ $this->U[$i][$k] = 0.0;
+ }
+ } else {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $this->U[$i][$k] = 0.0;
+ }
+ $this->U[$k][$k] = 1.0;
+ }
+ }
+ }
+
+ // If required, generate V.
+ if ($wantv) {
+ for ($k = $this->n - 1; $k >= 0; --$k) {
+ if (($k < $nrt) && ($e[$k] != 0.0)) {
+ for ($j = $k + 1; $j < $nu; ++$j) {
+ $t = 0;
+ for ($i = $k + 1; $i < $this->n; ++$i) {
+ $t += $this->V[$i][$k] * $this->V[$i][$j];
+ }
+ $t = -$t / $this->V[$k + 1][$k];
+ for ($i = $k + 1; $i < $this->n; ++$i) {
+ $this->V[$i][$j] += $t * $this->V[$i][$k];
+ }
+ }
+ }
+ for ($i = 0; $i < $this->n; ++$i) {
+ $this->V[$i][$k] = 0.0;
+ }
+ $this->V[$k][$k] = 1.0;
+ }
+ }
+
+ // Main iteration loop for the singular values.
+ $pp = $p - 1;
+ $iter = 0;
+ $eps = 2.0 ** (-52.0);
+
+ while ($p > 0) {
+ // Here is where a test for too many iterations would go.
+ // This section of the program inspects for negligible
+ // elements in the s and e arrays. On completion the
+ // variables kase and k are set as follows:
+ // kase = 1 if s(p) and e[k-1] are negligible and k= -1; --$k) {
+ if ($k == -1) {
+ break;
+ }
+ if (abs($e[$k]) <= $eps * (abs($this->s[$k]) + abs($this->s[$k + 1]))) {
+ $e[$k] = 0.0;
+
+ break;
+ }
+ }
+ if ($k == $p - 2) {
+ $kase = 4;
+ } else {
+ for ($ks = $p - 1; $ks >= $k; --$ks) {
+ if ($ks == $k) {
+ break;
+ }
+ $t = ($ks != $p ? abs($e[$ks]) : 0.) + ($ks != $k + 1 ? abs($e[$ks - 1]) : 0.);
+ if (abs($this->s[$ks]) <= $eps * $t) {
+ $this->s[$ks] = 0.0;
+
+ break;
+ }
+ }
+ if ($ks == $k) {
+ $kase = 3;
+ } elseif ($ks == $p - 1) {
+ $kase = 1;
+ } else {
+ $kase = 2;
+ $k = $ks;
+ }
+ }
+ ++$k;
+
+ // Perform the task indicated by kase.
+ switch ($kase) {
+ // Deflate negligible s(p).
+ case 1:
+ $f = $e[$p - 2];
+ $e[$p - 2] = 0.0;
+ for ($j = $p - 2; $j >= $k; --$j) {
+ $t = hypo($this->s[$j], $f);
+ $cs = $this->s[$j] / $t;
+ $sn = $f / $t;
+ $this->s[$j] = $t;
+ if ($j != $k) {
+ $f = -$sn * $e[$j - 1];
+ $e[$j - 1] = $cs * $e[$j - 1];
+ }
+ if ($wantv) {
+ for ($i = 0; $i < $this->n; ++$i) {
+ $t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$p - 1];
+ $this->V[$i][$p - 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$p - 1];
+ $this->V[$i][$j] = $t;
+ }
+ }
+ }
+
+ break;
+ // Split at negligible s(k).
+ case 2:
+ $f = $e[$k - 1];
+ $e[$k - 1] = 0.0;
+ for ($j = $k; $j < $p; ++$j) {
+ $t = hypo($this->s[$j], $f);
+ $cs = $this->s[$j] / $t;
+ $sn = $f / $t;
+ $this->s[$j] = $t;
+ $f = -$sn * $e[$j];
+ $e[$j] = $cs * $e[$j];
+ if ($wantu) {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$k - 1];
+ $this->U[$i][$k - 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$k - 1];
+ $this->U[$i][$j] = $t;
+ }
+ }
+ }
+
+ break;
+ // Perform one qr step.
+ case 3:
+ // Calculate the shift.
+ $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k]));
+ $sp = $this->s[$p - 1] / $scale;
+ $spm1 = $this->s[$p - 2] / $scale;
+ $epm1 = $e[$p - 2] / $scale;
+ $sk = $this->s[$k] / $scale;
+ $ek = $e[$k] / $scale;
+ $b = (($spm1 + $sp) * ($spm1 - $sp) + $epm1 * $epm1) / 2.0;
+ $c = ($sp * $epm1) * ($sp * $epm1);
+ $shift = 0.0;
+ if (($b != 0.0) || ($c != 0.0)) {
+ $shift = sqrt($b * $b + $c);
+ if ($b < 0.0) {
+ $shift = -$shift;
+ }
+ $shift = $c / ($b + $shift);
+ }
+ $f = ($sk + $sp) * ($sk - $sp) + $shift;
+ $g = $sk * $ek;
+ // Chase zeros.
+ for ($j = $k; $j < $p - 1; ++$j) {
+ $t = hypo($f, $g);
+ $cs = $f / $t;
+ $sn = $g / $t;
+ if ($j != $k) {
+ $e[$j - 1] = $t;
+ }
+ $f = $cs * $this->s[$j] + $sn * $e[$j];
+ $e[$j] = $cs * $e[$j] - $sn * $this->s[$j];
+ $g = $sn * $this->s[$j + 1];
+ $this->s[$j + 1] = $cs * $this->s[$j + 1];
+ if ($wantv) {
+ for ($i = 0; $i < $this->n; ++$i) {
+ $t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$j + 1];
+ $this->V[$i][$j + 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$j + 1];
+ $this->V[$i][$j] = $t;
+ }
+ }
+ $t = hypo($f, $g);
+ $cs = $f / $t;
+ $sn = $g / $t;
+ $this->s[$j] = $t;
+ $f = $cs * $e[$j] + $sn * $this->s[$j + 1];
+ $this->s[$j + 1] = -$sn * $e[$j] + $cs * $this->s[$j + 1];
+ $g = $sn * $e[$j + 1];
+ $e[$j + 1] = $cs * $e[$j + 1];
+ if ($wantu && ($j < $this->m - 1)) {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$j + 1];
+ $this->U[$i][$j + 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$j + 1];
+ $this->U[$i][$j] = $t;
+ }
+ }
+ }
+ $e[$p - 2] = $f;
+ $iter = $iter + 1;
+
+ break;
+ // Convergence.
+ case 4:
+ // Make the singular values positive.
+ if ($this->s[$k] <= 0.0) {
+ $this->s[$k] = ($this->s[$k] < 0.0 ? -$this->s[$k] : 0.0);
+ if ($wantv) {
+ for ($i = 0; $i <= $pp; ++$i) {
+ $this->V[$i][$k] = -$this->V[$i][$k];
+ }
+ }
+ }
+ // Order the singular values.
+ while ($k < $pp) {
+ if ($this->s[$k] >= $this->s[$k + 1]) {
+ break;
+ }
+ $t = $this->s[$k];
+ $this->s[$k] = $this->s[$k + 1];
+ $this->s[$k + 1] = $t;
+ if ($wantv && ($k < $this->n - 1)) {
+ for ($i = 0; $i < $this->n; ++$i) {
+ $t = $this->V[$i][$k + 1];
+ $this->V[$i][$k + 1] = $this->V[$i][$k];
+ $this->V[$i][$k] = $t;
+ }
+ }
+ if ($wantu && ($k < $this->m - 1)) {
+ for ($i = 0; $i < $this->m; ++$i) {
+ $t = $this->U[$i][$k + 1];
+ $this->U[$i][$k + 1] = $this->U[$i][$k];
+ $this->U[$i][$k] = $t;
+ }
+ }
+ ++$k;
+ }
+ $iter = 0;
+ --$p;
+
+ break;
+ } // end switch
+ } // end while
+ }
+
+ /**
+ * Return the left singular vectors.
+ *
+ * @return Matrix U
+ */
+ public function getU()
+ {
+ return new Matrix($this->U, $this->m, min($this->m + 1, $this->n));
+ }
+
+ /**
+ * Return the right singular vectors.
+ *
+ * @return Matrix V
+ */
+ public function getV()
+ {
+ return new Matrix($this->V);
+ }
+
+ /**
+ * Return the one-dimensional array of singular values.
+ *
+ * @return array diagonal of S
+ */
+ public function getSingularValues()
+ {
+ return $this->s;
+ }
+
+ /**
+ * Return the diagonal matrix of singular values.
+ *
+ * @return Matrix S
+ */
+ public function getS()
+ {
+ $S = [];
+ for ($i = 0; $i < $this->n; ++$i) {
+ for ($j = 0; $j < $this->n; ++$j) {
+ $S[$i][$j] = 0.0;
+ }
+ $S[$i][$i] = $this->s[$i];
+ }
+
+ return new Matrix($S);
+ }
+
+ /**
+ * Two norm.
+ *
+ * @return float max(S)
+ */
+ public function norm2()
+ {
+ return $this->s[0];
+ }
+
+ /**
+ * Two norm condition number.
+ *
+ * @return float max(S)/min(S)
+ */
+ public function cond()
+ {
+ return $this->s[0] / $this->s[min($this->m, $this->n) - 1];
+ }
+
+ /**
+ * Effective numerical matrix rank.
+ *
+ * @return int Number of nonnegligible singular values
+ */
+ public function rank()
+ {
+ $eps = 2.0 ** (-52.0);
+ $tol = max($this->m, $this->n) * $this->s[0] * $eps;
+ $r = 0;
+ $iMax = count($this->s);
+ for ($i = 0; $i < $iMax; ++$i) {
+ if ($this->s[$i] > $tol) {
+ ++$r;
+ }
+ }
+
+ return $r;
+ }
+}
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..8ecfc6b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE.php
@@ -0,0 +1,566 @@
+ |
+// | 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 $file
+ *
+ * @return bool true on success, PEAR_Error on failure
+ */
+ public function read($file)
+ {
+ $fh = fopen($file, 'rb');
+ if (!$fh) {
+ throw new ReaderException("Can't open file $file");
+ }
+ $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 $fh file handle
+ *
+ * @return int
+ */
+ private static function readInt1($fh)
+ {
+ [, $tmp] = unpack('c', fread($fh, 1));
+
+ return $tmp;
+ }
+
+ /**
+ * Reads an unsigned short (2 octets).
+ *
+ * @param resource $fh file handle
+ *
+ * @return int
+ */
+ private static function readInt2($fh)
+ {
+ [, $tmp] = unpack('v', fread($fh, 2));
+
+ return $tmp;
+ }
+
+ /**
+ * Reads an unsigned long (4 octets).
+ *
+ * @param resource $fh file handle
+ *
+ * @return int
+ */
+ private static function readInt4($fh)
+ {
+ [, $tmp] = unpack('V', fread($fh, 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");
+
+ // factor used for separating numbers into 4 bytes parts
+ $factor = 2 ** 32;
+
+ // 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;
+
+ $high_part = floor($big_date / $factor);
+ // lower 4 bytes
+ $low_part = floor((($big_date / $factor) - $high_part) * $factor);
+
+ // Make HEX string
+ $res = '';
+
+ for ($i = 0; $i < 4; ++$i) {
+ $hex = $low_part % 0x100;
+ $res .= pack('c', $hex);
+ $low_part /= 0x100;
+ }
+ for ($i = 0; $i < 4; ++$i) {
+ $hex = $high_part % 0x100;
+ $res .= pack('c', $hex);
+ $high_part /= 0x100;
+ }
+
+ 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/ChainedBlockStream.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php
new file mode 100644
index 0000000..43e4804
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php
@@ -0,0 +1,196 @@
+params);
+ if (!isset($this->params['oleInstanceId'], $this->params['blockId'], $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']])) {
+ if ($options & STREAM_REPORT_ERRORS) {
+ trigger_error('OLE stream not found', E_USER_WARNING);
+ }
+
+ return false;
+ }
+ $this->ole = $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']];
+
+ $blockId = $this->params['blockId'];
+ $this->data = '';
+ if (isset($this->params['size']) && $this->params['size'] < $this->ole->bigBlockThreshold && $blockId != $this->ole->root->startBlock) {
+ // Block id refers to small blocks
+ $rootPos = $this->ole->getBlockOffset($this->ole->root->startBlock);
+ while ($blockId != -2) {
+ $pos = $rootPos + $blockId * $this->ole->bigBlockSize;
+ $blockId = $this->ole->sbat[$blockId];
+ fseek($this->ole->_file_handle, $pos);
+ $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize);
+ }
+ } else {
+ // Block id refers to big blocks
+ while ($blockId != -2) {
+ $pos = $this->ole->getBlockOffset($blockId);
+ fseek($this->ole->_file_handle, $pos);
+ $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize);
+ $blockId = $this->ole->bbat[$blockId];
+ }
+ }
+ if (isset($this->params['size'])) {
+ $this->data = substr($this->data, 0, $this->params['size']);
+ }
+
+ if ($options & STREAM_USE_PATH) {
+ $openedPath = $path;
+ }
+
+ return true;
+ }
+
+ /**
+ * Implements support for fclose().
+ */
+ public function stream_close(): void // @codingStandardsIgnoreLine
+ {
+ $this->ole = null;
+ unset($GLOBALS['_OLE_INSTANCES']);
+ }
+
+ /**
+ * Implements support for fread(), fgets() etc.
+ *
+ * @param int $count maximum number of bytes to read
+ *
+ * @return false|string
+ */
+ public function stream_read($count) // @codingStandardsIgnoreLine
+ {
+ if ($this->stream_eof()) {
+ return false;
+ }
+ $s = substr($this->data, $this->pos, $count);
+ $this->pos += $count;
+
+ return $s;
+ }
+
+ /**
+ * Implements support for feof().
+ *
+ * @return bool TRUE if the file pointer is at EOF; otherwise FALSE
+ */
+ public function stream_eof() // @codingStandardsIgnoreLine
+ {
+ return $this->pos >= strlen($this->data);
+ }
+
+ /**
+ * Returns the position of the file pointer, i.e. its offset into the file
+ * stream. Implements support for ftell().
+ *
+ * @return int
+ */
+ public function stream_tell() // @codingStandardsIgnoreLine
+ {
+ return $this->pos;
+ }
+
+ /**
+ * Implements support for fseek().
+ *
+ * @param int $offset byte offset
+ * @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
+ *
+ * @return bool
+ */
+ public function stream_seek($offset, $whence) // @codingStandardsIgnoreLine
+ {
+ if ($whence == SEEK_SET && $offset >= 0) {
+ $this->pos = $offset;
+ } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) {
+ $this->pos += $offset;
+ } elseif ($whence == SEEK_END && -$offset <= count($this->data)) {
+ $this->pos = strlen($this->data) + $offset;
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Implements support for fstat(). Currently the only supported field is
+ * "size".
+ *
+ * @return array
+ */
+ public function stream_stat() // @codingStandardsIgnoreLine
+ {
+ return [
+ 'size' => strlen($this->data),
+ ];
+ }
+
+ // Methods used by stream_wrapper_register() that are not implemented:
+ // bool stream_flush ( void )
+ // int stream_write ( string data )
+ // bool rename ( string path_from, string path_to )
+ // bool mkdir ( string path, int mode, int options )
+ // bool rmdir ( string path, int options )
+ // bool dir_opendir ( string path, int options )
+ // array url_stat ( string path, int flags )
+ // string dir_readdir ( void )
+ // bool dir_rewinddir ( void )
+ // bool dir_closedir ( void )
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS.php
new file mode 100644
index 0000000..8b9e92a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS.php
@@ -0,0 +1,237 @@
+ |
+// | Based on OLE::Storage_Lite by Kawai, Takanori |
+// +----------------------------------------------------------------------+
+//
+use PhpOffice\PhpSpreadsheet\Shared\OLE;
+
+/**
+ * Class for creating PPS's for OLE containers.
+ *
+ * @author Xavier Noguer
+ */
+class PPS
+{
+ /**
+ * The PPS index.
+ *
+ * @var int
+ */
+ public $No;
+
+ /**
+ * The PPS name (in Unicode).
+ *
+ * @var string
+ */
+ public $Name;
+
+ /**
+ * The PPS type. Dir, Root or File.
+ *
+ * @var int
+ */
+ public $Type;
+
+ /**
+ * The index of the previous PPS.
+ *
+ * @var int
+ */
+ public $PrevPps;
+
+ /**
+ * The index of the next PPS.
+ *
+ * @var int
+ */
+ public $NextPps;
+
+ /**
+ * The index of it's first child if this is a Dir or Root PPS.
+ *
+ * @var int
+ */
+ public $DirPps;
+
+ /**
+ * A timestamp.
+ *
+ * @var float|int
+ */
+ public $Time1st;
+
+ /**
+ * A timestamp.
+ *
+ * @var float|int
+ */
+ public $Time2nd;
+
+ /**
+ * Starting block (small or big) for this PPS's data inside the container.
+ *
+ * @var int
+ */
+ public $startBlock;
+
+ /**
+ * The size of the PPS's data (in bytes).
+ *
+ * @var int
+ */
+ public $Size;
+
+ /**
+ * The PPS's data (only used if it's not using a temporary file).
+ *
+ * @var string
+ */
+ public $_data;
+
+ /**
+ * Array of child PPS's (only used by Root and Dir PPS's).
+ *
+ * @var array
+ */
+ public $children = [];
+
+ /**
+ * Pointer to OLE container.
+ *
+ * @var OLE
+ */
+ public $ole;
+
+ /**
+ * The constructor.
+ *
+ * @param int $No The PPS index
+ * @param string $name The PPS name
+ * @param int $type The PPS type. Dir, Root or File
+ * @param int $prev The index of the previous PPS
+ * @param int $next The index of the next PPS
+ * @param int $dir The index of it's first child if this is a Dir or Root PPS
+ * @param null|float|int $time_1st A timestamp
+ * @param null|float|int $time_2nd A timestamp
+ * @param string $data The (usually binary) source data of the PPS
+ * @param array $children Array containing children PPS for this PPS
+ */
+ public function __construct($No, $name, $type, $prev, $next, $dir, $time_1st, $time_2nd, $data, $children)
+ {
+ $this->No = $No;
+ $this->Name = $name;
+ $this->Type = $type;
+ $this->PrevPps = $prev;
+ $this->NextPps = $next;
+ $this->DirPps = $dir;
+ $this->Time1st = $time_1st ?? 0;
+ $this->Time2nd = $time_2nd ?? 0;
+ $this->_data = $data;
+ $this->children = $children;
+ if ($data != '') {
+ $this->Size = strlen($data);
+ } else {
+ $this->Size = 0;
+ }
+ }
+
+ /**
+ * Returns the amount of data saved for this PPS.
+ *
+ * @return int The amount of data (in bytes)
+ */
+ public function getDataLen()
+ {
+ if (!isset($this->_data)) {
+ return 0;
+ }
+
+ return strlen($this->_data);
+ }
+
+ /**
+ * Returns a string with the PPS's WK (What is a WK?).
+ *
+ * @return string The binary string
+ */
+ public function getPpsWk()
+ {
+ $ret = str_pad($this->Name, 64, "\x00");
+
+ $ret .= pack('v', strlen($this->Name) + 2) // 66
+ . pack('c', $this->Type) // 67
+ . pack('c', 0x00) //UK // 68
+ . pack('V', $this->PrevPps) //Prev // 72
+ . pack('V', $this->NextPps) //Next // 76
+ . pack('V', $this->DirPps) //Dir // 80
+ . "\x00\x09\x02\x00" // 84
+ . "\x00\x00\x00\x00" // 88
+ . "\xc0\x00\x00\x00" // 92
+ . "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root
+ . "\x00\x00\x00\x00" // 100
+ . OLE::localDateToOLE($this->Time1st) // 108
+ . OLE::localDateToOLE($this->Time2nd) // 116
+ . pack('V', $this->startBlock ?? 0) // 120
+ . pack('V', $this->Size) // 124
+ . pack('V', 0); // 128
+
+ return $ret;
+ }
+
+ /**
+ * Updates index and pointers to previous, next and children PPS's for this
+ * PPS. I don't think it'll work with Dir PPS's.
+ *
+ * @param array $raList Reference to the array of PPS's for the whole OLE
+ * container
+ * @param mixed $to_save
+ * @param mixed $depth
+ *
+ * @return int The index for this PPS
+ */
+ public static function savePpsSetPnt(&$raList, $to_save, $depth = 0)
+ {
+ if (!is_array($to_save) || (empty($to_save))) {
+ return 0xFFFFFFFF;
+ } elseif (count($to_save) == 1) {
+ $cnt = count($raList);
+ // If the first entry, it's the root... Don't clone it!
+ $raList[$cnt] = ($depth == 0) ? $to_save[0] : clone $to_save[0];
+ $raList[$cnt]->No = $cnt;
+ $raList[$cnt]->PrevPps = 0xFFFFFFFF;
+ $raList[$cnt]->NextPps = 0xFFFFFFFF;
+ $raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++);
+ } else {
+ $iPos = floor(count($to_save) / 2);
+ $aPrev = array_slice($to_save, 0, $iPos);
+ $aNext = array_slice($to_save, $iPos + 1);
+ $cnt = count($raList);
+ // If the first entry, it's the root... Don't clone it!
+ $raList[$cnt] = ($depth == 0) ? $to_save[$iPos] : clone $to_save[$iPos];
+ $raList[$cnt]->No = $cnt;
+ $raList[$cnt]->PrevPps = self::savePpsSetPnt($raList, $aPrev, $depth++);
+ $raList[$cnt]->NextPps = self::savePpsSetPnt($raList, $aNext, $depth++);
+ $raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++);
+ }
+
+ return $cnt;
+ }
+}
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/OLE/PPS/Root.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php
new file mode 100644
index 0000000..c9b99a8
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php
@@ -0,0 +1,426 @@
+ |
+// | Based on OLE::Storage_Lite by Kawai, Takanori |
+// +----------------------------------------------------------------------+
+//
+use PhpOffice\PhpSpreadsheet\Shared\OLE;
+use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;
+
+/**
+ * Class for creating Root PPS's for OLE containers.
+ *
+ * @author Xavier Noguer
+ */
+class Root extends PPS
+{
+ /**
+ * @var resource
+ */
+ private $fileHandle;
+
+ /**
+ * @var int
+ */
+ private $smallBlockSize;
+
+ /**
+ * @var int
+ */
+ private $bigBlockSize;
+
+ /**
+ * @param null|float|int $time_1st A timestamp
+ * @param null|float|int $time_2nd A timestamp
+ * @param File[] $raChild
+ */
+ public function __construct($time_1st, $time_2nd, $raChild)
+ {
+ parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild);
+ }
+
+ /**
+ * Method for saving the whole OLE container (including files).
+ * In fact, if called with an empty argument (or '-'), it saves to a
+ * temporary file and then outputs it's contents to stdout.
+ * If a resource pointer to a stream created by fopen() is passed
+ * it will be used, but you have to close such stream by yourself.
+ *
+ * @param resource $fileHandle the name of the file or stream where to save the OLE container
+ *
+ * @return bool true on success
+ */
+ public function save($fileHandle)
+ {
+ $this->fileHandle = $fileHandle;
+
+ // Initial Setting for saving
+ $this->bigBlockSize = 2 ** (
+ (isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9
+ );
+ $this->smallBlockSize = 2 ** (
+ (isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6
+ );
+
+ // Make an array of PPS's (for Save)
+ $aList = [];
+ PPS::savePpsSetPnt($aList, [$this]);
+ // calculate values for header
+ [$iSBDcnt, $iBBcnt, $iPPScnt] = $this->calcSize($aList); //, $rhInfo);
+ // Save Header
+ $this->saveHeader($iSBDcnt, $iBBcnt, $iPPScnt);
+
+ // Make Small Data string (write SBD)
+ $this->_data = $this->makeSmallData($aList);
+
+ // Write BB
+ $this->saveBigData($iSBDcnt, $aList);
+ // Write PPS
+ $this->savePps($aList);
+ // Write Big Block Depot and BDList and Adding Header informations
+ $this->saveBbd($iSBDcnt, $iBBcnt, $iPPScnt);
+
+ return true;
+ }
+
+ /**
+ * Calculate some numbers.
+ *
+ * @param array $raList Reference to an array of PPS's
+ *
+ * @return float[] The array of numbers
+ */
+ private function calcSize(&$raList)
+ {
+ // Calculate Basic Setting
+ [$iSBDcnt, $iBBcnt, $iPPScnt] = [0, 0, 0];
+ $iSmallLen = 0;
+ $iSBcnt = 0;
+ $iCount = count($raList);
+ for ($i = 0; $i < $iCount; ++$i) {
+ if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) {
+ $raList[$i]->Size = $raList[$i]->getDataLen();
+ if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) {
+ $iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize)
+ + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0);
+ } else {
+ $iBBcnt += (floor($raList[$i]->Size / $this->bigBlockSize) +
+ (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0));
+ }
+ }
+ }
+ $iSmallLen = $iSBcnt * $this->smallBlockSize;
+ $iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE);
+ $iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0);
+ $iBBcnt += (floor($iSmallLen / $this->bigBlockSize) +
+ (($iSmallLen % $this->bigBlockSize) ? 1 : 0));
+ $iCnt = count($raList);
+ $iBdCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE;
+ $iPPScnt = (floor($iCnt / $iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0));
+
+ return [$iSBDcnt, $iBBcnt, $iPPScnt];
+ }
+
+ /**
+ * Helper function for caculating a magic value for block sizes.
+ *
+ * @param int $i2 The argument
+ *
+ * @return float
+ *
+ * @see save()
+ */
+ private static function adjust2($i2)
+ {
+ $iWk = log($i2) / log(2);
+
+ return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk;
+ }
+
+ /**
+ * Save OLE header.
+ *
+ * @param int $iSBDcnt
+ * @param int $iBBcnt
+ * @param int $iPPScnt
+ */
+ private function saveHeader($iSBDcnt, $iBBcnt, $iPPScnt): void
+ {
+ $FILE = $this->fileHandle;
+
+ // Calculate Basic Setting
+ $iBlCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE;
+ $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE;
+
+ $iBdExL = 0;
+ $iAll = $iBBcnt + $iPPScnt + $iSBDcnt;
+ $iAllW = $iAll;
+ $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
+ $iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0);
+
+ // Calculate BD count
+ if ($iBdCnt > $i1stBdL) {
+ while (1) {
+ ++$iBdExL;
+ ++$iAllW;
+ $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
+ $iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0);
+ if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) {
+ break;
+ }
+ }
+ }
+
+ // Save Header
+ fwrite(
+ $FILE,
+ "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"
+ . "\x00\x00\x00\x00"
+ . "\x00\x00\x00\x00"
+ . "\x00\x00\x00\x00"
+ . "\x00\x00\x00\x00"
+ . pack('v', 0x3b)
+ . pack('v', 0x03)
+ . pack('v', -2)
+ . pack('v', 9)
+ . pack('v', 6)
+ . pack('v', 0)
+ . "\x00\x00\x00\x00"
+ . "\x00\x00\x00\x00"
+ . pack('V', $iBdCnt)
+ . pack('V', $iBBcnt + $iSBDcnt) //ROOT START
+ . pack('V', 0)
+ . pack('V', 0x1000)
+ . pack('V', $iSBDcnt ? 0 : -2) //Small Block Depot
+ . pack('V', $iSBDcnt)
+ );
+ // Extra BDList Start, Count
+ if ($iBdCnt < $i1stBdL) {
+ fwrite(
+ $FILE,
+ pack('V', -2) // Extra BDList Start
+ . pack('V', 0)// Extra BDList Count
+ );
+ } else {
+ fwrite($FILE, pack('V', $iAll + $iBdCnt) . pack('V', $iBdExL));
+ }
+
+ // BDList
+ for ($i = 0; $i < $i1stBdL && $i < $iBdCnt; ++$i) {
+ fwrite($FILE, pack('V', $iAll + $i));
+ }
+ if ($i < $i1stBdL) {
+ $jB = $i1stBdL - $i;
+ for ($j = 0; $j < $jB; ++$j) {
+ fwrite($FILE, (pack('V', -1)));
+ }
+ }
+ }
+
+ /**
+ * Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL).
+ *
+ * @param int $iStBlk
+ * @param array $raList Reference to array of PPS's
+ */
+ private function saveBigData($iStBlk, &$raList): void
+ {
+ $FILE = $this->fileHandle;
+
+ // cycle through PPS's
+ $iCount = count($raList);
+ for ($i = 0; $i < $iCount; ++$i) {
+ if ($raList[$i]->Type != OLE::OLE_PPS_TYPE_DIR) {
+ $raList[$i]->Size = $raList[$i]->getDataLen();
+ if (($raList[$i]->Size >= OLE::OLE_DATA_SIZE_SMALL) || (($raList[$i]->Type == OLE::OLE_PPS_TYPE_ROOT) && isset($raList[$i]->_data))) {
+ fwrite($FILE, $raList[$i]->_data);
+
+ if ($raList[$i]->Size % $this->bigBlockSize) {
+ fwrite($FILE, str_repeat("\x00", $this->bigBlockSize - ($raList[$i]->Size % $this->bigBlockSize)));
+ }
+ // Set For PPS
+ $raList[$i]->startBlock = $iStBlk;
+ $iStBlk +=
+ (floor($raList[$i]->Size / $this->bigBlockSize) +
+ (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0));
+ }
+ }
+ }
+ }
+
+ /**
+ * get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL).
+ *
+ * @param array $raList Reference to array of PPS's
+ *
+ * @return string
+ */
+ private function makeSmallData(&$raList)
+ {
+ $sRes = '';
+ $FILE = $this->fileHandle;
+ $iSmBlk = 0;
+
+ $iCount = count($raList);
+ for ($i = 0; $i < $iCount; ++$i) {
+ // Make SBD, small data string
+ if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) {
+ if ($raList[$i]->Size <= 0) {
+ continue;
+ }
+ if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) {
+ $iSmbCnt = floor($raList[$i]->Size / $this->smallBlockSize)
+ + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0);
+ // Add to SBD
+ $jB = $iSmbCnt - 1;
+ for ($j = 0; $j < $jB; ++$j) {
+ fwrite($FILE, pack('V', $j + $iSmBlk + 1));
+ }
+ fwrite($FILE, pack('V', -2));
+
+ // Add to Data String(this will be written for RootEntry)
+ $sRes .= $raList[$i]->_data;
+ if ($raList[$i]->Size % $this->smallBlockSize) {
+ $sRes .= str_repeat("\x00", $this->smallBlockSize - ($raList[$i]->Size % $this->smallBlockSize));
+ }
+ // Set for PPS
+ $raList[$i]->startBlock = $iSmBlk;
+ $iSmBlk += $iSmbCnt;
+ }
+ }
+ }
+ $iSbCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE);
+ if ($iSmBlk % $iSbCnt) {
+ $iB = $iSbCnt - ($iSmBlk % $iSbCnt);
+ for ($i = 0; $i < $iB; ++$i) {
+ fwrite($FILE, pack('V', -1));
+ }
+ }
+
+ return $sRes;
+ }
+
+ /**
+ * Saves all the PPS's WKs.
+ *
+ * @param array $raList Reference to an array with all PPS's
+ */
+ private function savePps(&$raList): void
+ {
+ // Save each PPS WK
+ $iC = count($raList);
+ for ($i = 0; $i < $iC; ++$i) {
+ fwrite($this->fileHandle, $raList[$i]->getPpsWk());
+ }
+ // Adjust for Block
+ $iCnt = count($raList);
+ $iBCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE;
+ if ($iCnt % $iBCnt) {
+ fwrite($this->fileHandle, str_repeat("\x00", ($iBCnt - ($iCnt % $iBCnt)) * OLE::OLE_PPS_SIZE));
+ }
+ }
+
+ /**
+ * Saving Big Block Depot.
+ *
+ * @param int $iSbdSize
+ * @param int $iBsize
+ * @param int $iPpsCnt
+ */
+ private function saveBbd($iSbdSize, $iBsize, $iPpsCnt): void
+ {
+ $FILE = $this->fileHandle;
+ // Calculate Basic Setting
+ $iBbCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE;
+ $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE;
+
+ $iBdExL = 0;
+ $iAll = $iBsize + $iPpsCnt + $iSbdSize;
+ $iAllW = $iAll;
+ $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
+ $iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0);
+ // Calculate BD count
+ if ($iBdCnt > $i1stBdL) {
+ while (1) {
+ ++$iBdExL;
+ ++$iAllW;
+ $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
+ $iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0);
+ if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) {
+ break;
+ }
+ }
+ }
+
+ // Making BD
+ // Set for SBD
+ if ($iSbdSize > 0) {
+ for ($i = 0; $i < ($iSbdSize - 1); ++$i) {
+ fwrite($FILE, pack('V', $i + 1));
+ }
+ fwrite($FILE, pack('V', -2));
+ }
+ // Set for B
+ for ($i = 0; $i < ($iBsize - 1); ++$i) {
+ fwrite($FILE, pack('V', $i + $iSbdSize + 1));
+ }
+ fwrite($FILE, pack('V', -2));
+
+ // Set for PPS
+ for ($i = 0; $i < ($iPpsCnt - 1); ++$i) {
+ fwrite($FILE, pack('V', $i + $iSbdSize + $iBsize + 1));
+ }
+ fwrite($FILE, pack('V', -2));
+ // Set for BBD itself ( 0xFFFFFFFD : BBD)
+ for ($i = 0; $i < $iBdCnt; ++$i) {
+ fwrite($FILE, pack('V', 0xFFFFFFFD));
+ }
+ // Set for ExtraBDList
+ for ($i = 0; $i < $iBdExL; ++$i) {
+ fwrite($FILE, pack('V', 0xFFFFFFFC));
+ }
+ // Adjust for Block
+ if (($iAllW + $iBdCnt) % $iBbCnt) {
+ $iBlock = ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt));
+ for ($i = 0; $i < $iBlock; ++$i) {
+ fwrite($FILE, pack('V', -1));
+ }
+ }
+ // Extra BDList
+ if ($iBdCnt > $i1stBdL) {
+ $iN = 0;
+ $iNb = 0;
+ for ($i = $i1stBdL; $i < $iBdCnt; $i++, ++$iN) {
+ if ($iN >= ($iBbCnt - 1)) {
+ $iN = 0;
+ ++$iNb;
+ fwrite($FILE, pack('V', $iAll + $iBdCnt + $iNb));
+ }
+ fwrite($FILE, pack('V', $iBsize + $iSbdSize + $iPpsCnt + $i));
+ }
+ if (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)) {
+ $iB = ($iBbCnt - 1) - (($iBdCnt - $i1stBdL) % ($iBbCnt - 1));
+ for ($i = 0; $i < $iB; ++$i) {
+ fwrite($FILE, pack('V', -1));
+ }
+ }
+ fwrite($FILE, pack('V', -2));
+ }
+ }
+}
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..c4dc572
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLERead.php
@@ -0,0 +1,348 @@
+data = file_get_contents($pFilename, 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 ' . $pFilename . ' is not recognised as an OLE file');
+ }
+
+ // Get the file data
+ $this->data = file_get_contents($pFilename);
+
+ // 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 $bl Sector ID where the stream starts
+ *
+ * @return string Data for standard stream
+ */
+ private function readData($bl)
+ {
+ $block = $bl;
+ $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..9fefe88
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/PasswordHasher.php
@@ -0,0 +1,100 @@
+ '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 Exception('Unsupported password algorithm: ' . $algorithmName);
+ }
+
+ /**
+ * Create a password hash from a given string.
+ *
+ * This method is based on the algorithm provided by
+ * Daniel Rentz of OpenOffice and the PEAR package
+ * Spreadsheet_Excel_Writer by Xavier Noguer .
+ *
+ * @param string $pPassword Password to hash
+ */
+ private static function defaultHashPassword(string $pPassword): string
+ {
+ $password = 0x0000;
+ $charPos = 1; // char position
+
+ // split the plain text password in its component characters
+ $chars = preg_split('//', $pPassword, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($chars as $char) {
+ $value = ord($char) << $charPos++; // shifted ASCII value
+ $rotated_bits = $value >> 15; // rotated bits beyond bit 15
+ $value &= 0x7fff; // first 15 bits
+ $password ^= ($value | $rotated_bits);
+ }
+
+ $password ^= strlen($pPassword);
+ $password ^= 0xCE4B;
+
+ return strtoupper(dechex($password));
+ }
+
+ /**
+ * 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
+ {
+ $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/StringHelper.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php
new file mode 100644
index 0000000..e85ce55
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php
@@ -0,0 +1,722 @@
+ chr(0),
+ "\x1B 1" => chr(1),
+ "\x1B 2" => chr(2),
+ "\x1B 3" => chr(3),
+ "\x1B 4" => chr(4),
+ "\x1B 5" => chr(5),
+ "\x1B 6" => chr(6),
+ "\x1B 7" => chr(7),
+ "\x1B 8" => chr(8),
+ "\x1B 9" => chr(9),
+ "\x1B :" => chr(10),
+ "\x1B ;" => chr(11),
+ "\x1B <" => chr(12),
+ "\x1B =" => chr(13),
+ "\x1B >" => chr(14),
+ "\x1B ?" => chr(15),
+ "\x1B!0" => chr(16),
+ "\x1B!1" => chr(17),
+ "\x1B!2" => chr(18),
+ "\x1B!3" => chr(19),
+ "\x1B!4" => chr(20),
+ "\x1B!5" => chr(21),
+ "\x1B!6" => chr(22),
+ "\x1B!7" => chr(23),
+ "\x1B!8" => chr(24),
+ "\x1B!9" => chr(25),
+ "\x1B!:" => chr(26),
+ "\x1B!;" => chr(27),
+ "\x1B!<" => chr(28),
+ "\x1B!=" => chr(29),
+ "\x1B!>" => chr(30),
+ "\x1B!?" => chr(31),
+ "\x1B'?" => chr(127),
+ "\x1B(0" => '€', // 128 in CP1252
+ "\x1B(2" => '‚', // 130 in CP1252
+ "\x1B(3" => 'ƒ', // 131 in CP1252
+ "\x1B(4" => '„', // 132 in CP1252
+ "\x1B(5" => '…', // 133 in CP1252
+ "\x1B(6" => '†', // 134 in CP1252
+ "\x1B(7" => '‡', // 135 in CP1252
+ "\x1B(8" => 'ˆ', // 136 in CP1252
+ "\x1B(9" => '‰', // 137 in CP1252
+ "\x1B(:" => 'Š', // 138 in CP1252
+ "\x1B(;" => '‹', // 139 in CP1252
+ "\x1BNj" => 'Œ', // 140 in CP1252
+ "\x1B(>" => 'Ž', // 142 in CP1252
+ "\x1B)1" => '‘', // 145 in CP1252
+ "\x1B)2" => '’', // 146 in CP1252
+ "\x1B)3" => '“', // 147 in CP1252
+ "\x1B)4" => '”', // 148 in CP1252
+ "\x1B)5" => '•', // 149 in CP1252
+ "\x1B)6" => '–', // 150 in CP1252
+ "\x1B)7" => '—', // 151 in CP1252
+ "\x1B)8" => '˜', // 152 in CP1252
+ "\x1B)9" => '™', // 153 in CP1252
+ "\x1B):" => 'š', // 154 in CP1252
+ "\x1B);" => '›', // 155 in CP1252
+ "\x1BNz" => 'œ', // 156 in CP1252
+ "\x1B)>" => 'ž', // 158 in CP1252
+ "\x1B)?" => 'Ÿ', // 159 in CP1252
+ "\x1B*0" => ' ', // 160 in CP1252
+ "\x1BN!" => '¡', // 161 in CP1252
+ "\x1BN\"" => '¢', // 162 in CP1252
+ "\x1BN#" => '£', // 163 in CP1252
+ "\x1BN(" => '¤', // 164 in CP1252
+ "\x1BN%" => '¥', // 165 in CP1252
+ "\x1B*6" => '¦', // 166 in CP1252
+ "\x1BN'" => '§', // 167 in CP1252
+ "\x1BNH " => '¨', // 168 in CP1252
+ "\x1BNS" => '©', // 169 in CP1252
+ "\x1BNc" => 'ª', // 170 in CP1252
+ "\x1BN+" => '«', // 171 in CP1252
+ "\x1B*<" => '¬', // 172 in CP1252
+ "\x1B*=" => '', // 173 in CP1252
+ "\x1BNR" => '®', // 174 in CP1252
+ "\x1B*?" => '¯', // 175 in CP1252
+ "\x1BN0" => '°', // 176 in CP1252
+ "\x1BN1" => '±', // 177 in CP1252
+ "\x1BN2" => '²', // 178 in CP1252
+ "\x1BN3" => '³', // 179 in CP1252
+ "\x1BNB " => '´', // 180 in CP1252
+ "\x1BN5" => 'µ', // 181 in CP1252
+ "\x1BN6" => '¶', // 182 in CP1252
+ "\x1BN7" => '·', // 183 in CP1252
+ "\x1B+8" => '¸', // 184 in CP1252
+ "\x1BNQ" => '¹', // 185 in CP1252
+ "\x1BNk" => 'º', // 186 in CP1252
+ "\x1BN;" => '»', // 187 in CP1252
+ "\x1BN<" => '¼', // 188 in CP1252
+ "\x1BN=" => '½', // 189 in CP1252
+ "\x1BN>" => '¾', // 190 in CP1252
+ "\x1BN?" => '¿', // 191 in CP1252
+ "\x1BNAA" => 'À', // 192 in CP1252
+ "\x1BNBA" => 'Á', // 193 in CP1252
+ "\x1BNCA" => 'Â', // 194 in CP1252
+ "\x1BNDA" => 'Ã', // 195 in CP1252
+ "\x1BNHA" => 'Ä', // 196 in CP1252
+ "\x1BNJA" => 'Å', // 197 in CP1252
+ "\x1BNa" => 'Æ', // 198 in CP1252
+ "\x1BNKC" => 'Ç', // 199 in CP1252
+ "\x1BNAE" => 'È', // 200 in CP1252
+ "\x1BNBE" => 'É', // 201 in CP1252
+ "\x1BNCE" => 'Ê', // 202 in CP1252
+ "\x1BNHE" => 'Ë', // 203 in CP1252
+ "\x1BNAI" => 'Ì', // 204 in CP1252
+ "\x1BNBI" => 'Í', // 205 in CP1252
+ "\x1BNCI" => 'Î', // 206 in CP1252
+ "\x1BNHI" => 'Ï', // 207 in CP1252
+ "\x1BNb" => 'Ð', // 208 in CP1252
+ "\x1BNDN" => 'Ñ', // 209 in CP1252
+ "\x1BNAO" => 'Ò', // 210 in CP1252
+ "\x1BNBO" => 'Ó', // 211 in CP1252
+ "\x1BNCO" => 'Ô', // 212 in CP1252
+ "\x1BNDO" => 'Õ', // 213 in CP1252
+ "\x1BNHO" => 'Ö', // 214 in CP1252
+ "\x1B-7" => '×', // 215 in CP1252
+ "\x1BNi" => 'Ø', // 216 in CP1252
+ "\x1BNAU" => 'Ù', // 217 in CP1252
+ "\x1BNBU" => 'Ú', // 218 in CP1252
+ "\x1BNCU" => 'Û', // 219 in CP1252
+ "\x1BNHU" => 'Ü', // 220 in CP1252
+ "\x1B-=" => 'Ý', // 221 in CP1252
+ "\x1BNl" => 'Þ', // 222 in CP1252
+ "\x1BN{" => 'ß', // 223 in CP1252
+ "\x1BNAa" => 'à', // 224 in CP1252
+ "\x1BNBa" => 'á', // 225 in CP1252
+ "\x1BNCa" => 'â', // 226 in CP1252
+ "\x1BNDa" => 'ã', // 227 in CP1252
+ "\x1BNHa" => 'ä', // 228 in CP1252
+ "\x1BNJa" => 'å', // 229 in CP1252
+ "\x1BNq" => 'æ', // 230 in CP1252
+ "\x1BNKc" => 'ç', // 231 in CP1252
+ "\x1BNAe" => 'è', // 232 in CP1252
+ "\x1BNBe" => 'é', // 233 in CP1252
+ "\x1BNCe" => 'ê', // 234 in CP1252
+ "\x1BNHe" => 'ë', // 235 in CP1252
+ "\x1BNAi" => 'ì', // 236 in CP1252
+ "\x1BNBi" => 'í', // 237 in CP1252
+ "\x1BNCi" => 'î', // 238 in CP1252
+ "\x1BNHi" => 'ï', // 239 in CP1252
+ "\x1BNs" => 'ð', // 240 in CP1252
+ "\x1BNDn" => 'ñ', // 241 in CP1252
+ "\x1BNAo" => 'ò', // 242 in CP1252
+ "\x1BNBo" => 'ó', // 243 in CP1252
+ "\x1BNCo" => 'ô', // 244 in CP1252
+ "\x1BNDo" => 'õ', // 245 in CP1252
+ "\x1BNHo" => 'ö', // 246 in CP1252
+ "\x1B/7" => '÷', // 247 in CP1252
+ "\x1BNy" => 'ø', // 248 in CP1252
+ "\x1BNAu" => 'ù', // 249 in CP1252
+ "\x1BNBu" => 'ú', // 250 in CP1252
+ "\x1BNCu" => 'û', // 251 in CP1252
+ "\x1BNHu" => 'ü', // 252 in CP1252
+ "\x1B/=" => 'ý', // 253 in CP1252
+ "\x1BN|" => 'þ', // 254 in CP1252
+ "\x1BNHy" => 'ÿ', // 255 in CP1252
+ ];
+ }
+
+ /**
+ * Get whether iconv extension is available.
+ *
+ * @return bool
+ */
+ public static function getIsIconvEnabled()
+ {
+ if (isset(self::$isIconvEnabled)) {
+ return self::$isIconvEnabled;
+ }
+
+ // Assume no problems with iconv
+ self::$isIconvEnabled = true;
+
+ // Fail if iconv doesn't exist
+ if (!function_exists('iconv')) {
+ self::$isIconvEnabled = false;
+ } elseif (!@iconv('UTF-8', 'UTF-16LE', 'x')) {
+ // Sometimes iconv is not working, and e.g. iconv('UTF-8', 'UTF-16LE', 'x') just returns false,
+ self::$isIconvEnabled = false;
+ } elseif (defined('PHP_OS') && @stristr(PHP_OS, 'AIX') && defined('ICONV_IMPL') && (@strcasecmp(ICONV_IMPL, 'unknown') == 0) && defined('ICONV_VERSION') && (@strcasecmp(ICONV_VERSION, 'unknown') == 0)) {
+ // CUSTOM: IBM AIX iconv() does not work
+ self::$isIconvEnabled = false;
+ }
+
+ // Deactivate iconv default options if they fail (as seen on IMB i)
+ if (self::$isIconvEnabled && !@iconv('UTF-8', 'UTF-16LE' . self::$iconvOptions, 'x')) {
+ self::$iconvOptions = '';
+ }
+
+ return self::$isIconvEnabled;
+ }
+
+ private static function buildCharacterSets(): void
+ {
+ if (empty(self::$controlCharacters)) {
+ self::buildControlCharacters();
+ }
+
+ if (empty(self::$SYLKCharacters)) {
+ self::buildSYLKCharacters();
+ }
+ }
+
+ /**
+ * Convert from OpenXML escaped control character to PHP control character.
+ *
+ * Excel 2007 team:
+ * ----------------
+ * That's correct, control characters are stored directly in the shared-strings table.
+ * We do encode characters that cannot be represented in XML using the following escape sequence:
+ * _xHHHH_ where H represents a hexadecimal character in the character's value...
+ * So you could end up with something like _x0008_ in a string (either in a cell value ()
+ * element or in the shared string element.
+ *
+ * @param string $value Value to unescape
+ *
+ * @return string
+ */
+ public static function controlCharacterOOXML2PHP($value)
+ {
+ self::buildCharacterSets();
+
+ return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $value);
+ }
+
+ /**
+ * Convert from PHP control character to OpenXML escaped control character.
+ *
+ * Excel 2007 team:
+ * ----------------
+ * That's correct, control characters are stored directly in the shared-strings table.
+ * We do encode characters that cannot be represented in XML using the following escape sequence:
+ * _xHHHH_ where H represents a hexadecimal character in the character's value...
+ * So you could end up with something like _x0008_ in a string (either in a cell value ()
+ * element or in the shared string element.
+ *
+ * @param string $value Value to escape
+ *
+ * @return string
+ */
+ public static function controlCharacterPHP2OOXML($value)
+ {
+ self::buildCharacterSets();
+
+ return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $value);
+ }
+
+ /**
+ * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public static function sanitizeUTF8($value)
+ {
+ if (self::getIsIconvEnabled()) {
+ $value = @iconv('UTF-8', 'UTF-8', $value);
+
+ return $value;
+ }
+
+ $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
+
+ return $value;
+ }
+
+ /**
+ * Check if a string contains UTF8 data.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public static function isUTF8($value)
+ {
+ return $value === '' || preg_match('/^./su', $value) === 1;
+ }
+
+ /**
+ * Formats a numeric value as a string for output in various output writers forcing
+ * point as decimal separator in case locale is other than English.
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public static function formatNumber($value)
+ {
+ if (is_float($value)) {
+ return str_replace(',', '.', $value);
+ }
+
+ return (string) $value;
+ }
+
+ /**
+ * Converts a UTF-8 string into BIFF8 Unicode string data (8-bit string length)
+ * Writes the string using uncompressed notation, no rich text, no Asian phonetics
+ * If mbstring extension is not available, ASCII is assumed, and compressed notation is used
+ * although this will give wrong results for non-ASCII strings
+ * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3.
+ *
+ * @param string $value UTF-8 encoded string
+ * @param mixed[] $arrcRuns Details of rich text runs in $value
+ *
+ * @return string
+ */
+ public static function UTF8toBIFF8UnicodeShort($value, $arrcRuns = [])
+ {
+ // character count
+ $ln = self::countCharacters($value, 'UTF-8');
+ // option flags
+ if (empty($arrcRuns)) {
+ $data = pack('CC', $ln, 0x0001);
+ // characters
+ $data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
+ } else {
+ $data = pack('vC', $ln, 0x09);
+ $data .= pack('v', count($arrcRuns));
+ // characters
+ $data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
+ foreach ($arrcRuns as $cRun) {
+ $data .= pack('v', $cRun['strlen']);
+ $data .= pack('v', $cRun['fontidx']);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Converts a UTF-8 string into BIFF8 Unicode string data (16-bit string length)
+ * Writes the string using uncompressed notation, no rich text, no Asian phonetics
+ * If mbstring extension is not available, ASCII is assumed, and compressed notation is used
+ * although this will give wrong results for non-ASCII strings
+ * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3.
+ *
+ * @param string $value UTF-8 encoded string
+ *
+ * @return string
+ */
+ public static function UTF8toBIFF8UnicodeLong($value)
+ {
+ // character count
+ $ln = self::countCharacters($value, 'UTF-8');
+
+ // characters
+ $chars = self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
+
+ return pack('vC', $ln, 0x0001) . $chars;
+ }
+
+ /**
+ * Convert string from one encoding to another.
+ *
+ * @param string $value
+ * @param string $to Encoding to convert to, e.g. 'UTF-8'
+ * @param string $from Encoding to convert from, e.g. 'UTF-16LE'
+ *
+ * @return string
+ */
+ public static function convertEncoding($value, $to, $from)
+ {
+ if (self::getIsIconvEnabled()) {
+ $result = iconv($from, $to . self::$iconvOptions, $value);
+ if (false !== $result) {
+ return $result;
+ }
+ }
+
+ return mb_convert_encoding($value, $to, $from);
+ }
+
+ /**
+ * Get character count.
+ *
+ * @param string $value
+ * @param string $enc Encoding
+ *
+ * @return int Character count
+ */
+ public static function countCharacters($value, $enc = 'UTF-8')
+ {
+ return mb_strlen($value ?? '', $enc);
+ }
+
+ /**
+ * Get a substring of a UTF-8 encoded string.
+ *
+ * @param string $pValue UTF-8 encoded string
+ * @param int $pStart Start offset
+ * @param int $pLength Maximum number of characters in substring
+ *
+ * @return string
+ */
+ public static function substring($pValue, $pStart, $pLength = 0)
+ {
+ return mb_substr($pValue, $pStart, $pLength, 'UTF-8');
+ }
+
+ /**
+ * Convert a UTF-8 encoded string to upper case.
+ *
+ * @param string $pValue UTF-8 encoded string
+ *
+ * @return string
+ */
+ public static function strToUpper($pValue)
+ {
+ return mb_convert_case($pValue, MB_CASE_UPPER, 'UTF-8');
+ }
+
+ /**
+ * Convert a UTF-8 encoded string to lower case.
+ *
+ * @param string $pValue UTF-8 encoded string
+ *
+ * @return string
+ */
+ public static function strToLower($pValue)
+ {
+ return mb_convert_case($pValue, MB_CASE_LOWER, 'UTF-8');
+ }
+
+ /**
+ * Convert a UTF-8 encoded string to title/proper case
+ * (uppercase every first character in each word, lower case all other characters).
+ *
+ * @param string $pValue UTF-8 encoded string
+ *
+ * @return string
+ */
+ public static function strToTitle($pValue)
+ {
+ return mb_convert_case($pValue, MB_CASE_TITLE, 'UTF-8');
+ }
+
+ public static function mbIsUpper($char)
+ {
+ return mb_strtolower($char, 'UTF-8') != $char;
+ }
+
+ public static function mbStrSplit($string)
+ {
+ // Split at all position not after the start: ^
+ // and not before the end: $
+ return preg_split('/(?_calculateFormulaValue($fractionFormula);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // function convertToNumberIfFraction()
+
+ /**
+ * Get the decimal separator. If it has not yet been set explicitly, try to obtain number
+ * formatting information from locale.
+ *
+ * @return string
+ */
+ public static function getDecimalSeparator()
+ {
+ if (!isset(self::$decimalSeparator)) {
+ $localeconv = localeconv();
+ self::$decimalSeparator = ($localeconv['decimal_point'] != '')
+ ? $localeconv['decimal_point'] : $localeconv['mon_decimal_point'];
+
+ if (self::$decimalSeparator == '') {
+ // Default to .
+ self::$decimalSeparator = '.';
+ }
+ }
+
+ return self::$decimalSeparator;
+ }
+
+ /**
+ * Set the decimal separator. Only used by NumberFormat::toFormattedString()
+ * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
+ *
+ * @param string $pValue Character for decimal separator
+ */
+ public static function setDecimalSeparator($pValue): void
+ {
+ self::$decimalSeparator = $pValue;
+ }
+
+ /**
+ * Get the thousands separator. If it has not yet been set explicitly, try to obtain number
+ * formatting information from locale.
+ *
+ * @return string
+ */
+ public static function getThousandsSeparator()
+ {
+ if (!isset(self::$thousandsSeparator)) {
+ $localeconv = localeconv();
+ self::$thousandsSeparator = ($localeconv['thousands_sep'] != '')
+ ? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep'];
+
+ if (self::$thousandsSeparator == '') {
+ // Default to .
+ self::$thousandsSeparator = ',';
+ }
+ }
+
+ return self::$thousandsSeparator;
+ }
+
+ /**
+ * Set the thousands separator. Only used by NumberFormat::toFormattedString()
+ * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
+ *
+ * @param string $pValue Character for thousands separator
+ */
+ public static function setThousandsSeparator($pValue): void
+ {
+ self::$thousandsSeparator = $pValue;
+ }
+
+ /**
+ * Get the currency code. If it has not yet been set explicitly, try to obtain the
+ * symbol information from locale.
+ *
+ * @return string
+ */
+ public static function getCurrencyCode()
+ {
+ if (!empty(self::$currencyCode)) {
+ return self::$currencyCode;
+ }
+ self::$currencyCode = '$';
+ $localeconv = localeconv();
+ if (!empty($localeconv['currency_symbol'])) {
+ self::$currencyCode = $localeconv['currency_symbol'];
+
+ return self::$currencyCode;
+ }
+ if (!empty($localeconv['int_curr_symbol'])) {
+ self::$currencyCode = $localeconv['int_curr_symbol'];
+
+ return self::$currencyCode;
+ }
+
+ return self::$currencyCode;
+ }
+
+ /**
+ * Set the currency code. Only used by NumberFormat::toFormattedString()
+ * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
+ *
+ * @param string $pValue Character for currency code
+ */
+ public static function setCurrencyCode($pValue): void
+ {
+ self::$currencyCode = $pValue;
+ }
+
+ /**
+ * Convert SYLK encoded string to UTF-8.
+ *
+ * @param string $pValue
+ *
+ * @return string UTF-8 encoded string
+ */
+ public static function SYLKtoUTF8($pValue)
+ {
+ self::buildCharacterSets();
+
+ // If there is no escape character in the string there is nothing to do
+ if (strpos($pValue, '') === false) {
+ return $pValue;
+ }
+
+ foreach (self::$SYLKCharacters as $k => $v) {
+ $pValue = str_replace($k, $v, $pValue);
+ }
+
+ return $pValue;
+ }
+
+ /**
+ * Retrieve any leading numeric part of a string, or return the full string if no leading numeric
+ * (handles basic integer or float, but not exponent or non decimal).
+ *
+ * @param string $value
+ *
+ * @return mixed string or only the leading numeric part of the string
+ */
+ public static function testStringAsNumeric($value)
+ {
+ if (is_numeric($value)) {
+ return $value;
+ }
+ $v = (float) $value;
+
+ return (is_numeric(substr($value, 0, strlen($v)))) ? $v : $value;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php
new file mode 100644
index 0000000..a6f9850
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php
@@ -0,0 +1,77 @@
+setTimeZone(new DateTimeZone($timezone));
+
+ return $dtobj->getOffset();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php
new file mode 100644
index 0000000..7df4895
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php
@@ -0,0 +1,461 @@
+error;
+ }
+
+ public function getBestFitType()
+ {
+ return $this->bestFitType;
+ }
+
+ /**
+ * Return the Y-Value for a specified value of X.
+ *
+ * @param float $xValue X-Value
+ *
+ * @return float Y-Value
+ */
+ abstract public function getValueOfYForX($xValue);
+
+ /**
+ * Return the X-Value for a specified value of Y.
+ *
+ * @param float $yValue Y-Value
+ *
+ * @return float X-Value
+ */
+ abstract public function getValueOfXForY($yValue);
+
+ /**
+ * Return the original set of X-Values.
+ *
+ * @return float[] X-Values
+ */
+ public function getXValues()
+ {
+ return $this->xValues;
+ }
+
+ /**
+ * Return the Equation of the best-fit line.
+ *
+ * @param int $dp Number of places of decimal precision to display
+ *
+ * @return string
+ */
+ abstract public function getEquation($dp = 0);
+
+ /**
+ * 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($this->slope, $dp);
+ }
+
+ return $this->slope;
+ }
+
+ /**
+ * Return the standard error of the Slope.
+ *
+ * @param int $dp Number of places of decimal precision to display
+ *
+ * @return float
+ */
+ public function getSlopeSE($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->slopeSE, $dp);
+ }
+
+ return $this->slopeSE;
+ }
+
+ /**
+ * 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($this->intersect, $dp);
+ }
+
+ return $this->intersect;
+ }
+
+ /**
+ * Return the standard error of the Intersect.
+ *
+ * @param int $dp Number of places of decimal precision to display
+ *
+ * @return float
+ */
+ public function getIntersectSE($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->intersectSE, $dp);
+ }
+
+ return $this->intersectSE;
+ }
+
+ /**
+ * Return the goodness of fit for this regression.
+ *
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getGoodnessOfFit($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->goodnessOfFit, $dp);
+ }
+
+ return $this->goodnessOfFit;
+ }
+
+ /**
+ * Return the goodness of fit for this regression.
+ *
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getGoodnessOfFitPercent($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->goodnessOfFit * 100, $dp);
+ }
+
+ return $this->goodnessOfFit * 100;
+ }
+
+ /**
+ * Return the standard deviation of the residuals for this regression.
+ *
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getStdevOfResiduals($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->stdevOfResiduals, $dp);
+ }
+
+ return $this->stdevOfResiduals;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getSSRegression($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->SSRegression, $dp);
+ }
+
+ return $this->SSRegression;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getSSResiduals($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->SSResiduals, $dp);
+ }
+
+ return $this->SSResiduals;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getDFResiduals($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->DFResiduals, $dp);
+ }
+
+ return $this->DFResiduals;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getF($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->f, $dp);
+ }
+
+ return $this->f;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getCovariance($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->covariance, $dp);
+ }
+
+ return $this->covariance;
+ }
+
+ /**
+ * @param int $dp Number of places of decimal precision to return
+ *
+ * @return float
+ */
+ public function getCorrelation($dp = 0)
+ {
+ if ($dp != 0) {
+ return round($this->correlation, $dp);
+ }
+
+ return $this->correlation;
+ }
+
+ /**
+ * @return float[]
+ */
+ public function getYBestFitValues()
+ {
+ return $this->yBestFitValues;
+ }
+
+ protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
+ {
+ $SSres = $SScov = $SScor = $SStot = $SSsex = 0.0;
+ foreach ($this->xValues as $xKey => $xValue) {
+ $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
+
+ $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
+ if ($const === true) {
+ $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
+ } else {
+ $SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
+ }
+ $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
+ if ($const === true) {
+ $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
+ } else {
+ $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
+ }
+ }
+
+ $this->SSResiduals = $SSres;
+ $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
+
+ if ($this->DFResiduals == 0.0) {
+ $this->stdevOfResiduals = 0.0;
+ } else {
+ $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
+ }
+ if (($SStot == 0.0) || ($SSres == $SStot)) {
+ $this->goodnessOfFit = 1;
+ } else {
+ $this->goodnessOfFit = 1 - ($SSres / $SStot);
+ }
+
+ $this->SSRegression = $this->goodnessOfFit * $SStot;
+ $this->covariance = $SScov / $this->valueCount;
+ $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2));
+ $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
+ $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
+ if ($this->SSResiduals != 0.0) {
+ if ($this->DFResiduals == 0.0) {
+ $this->f = 0.0;
+ } else {
+ $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
+ }
+ } else {
+ if ($this->DFResiduals == 0.0) {
+ $this->f = 0.0;
+ } else {
+ $this->f = $this->SSRegression / $this->DFResiduals;
+ }
+ }
+ }
+
+ private function sumSquares(array $values)
+ {
+ return array_sum(
+ array_map(
+ function ($value) {
+ return $value ** 2;
+ },
+ $values
+ )
+ );
+ }
+
+ /**
+ * @param float[] $yValues
+ * @param float[] $xValues
+ */
+ protected function leastSquareFit(array $yValues, array $xValues, bool $const): void
+ {
+ // calculate sums
+ $sumValuesX = array_sum($xValues);
+ $sumValuesY = array_sum($yValues);
+ $meanValueX = $sumValuesX / $this->valueCount;
+ $meanValueY = $sumValuesY / $this->valueCount;
+ $sumSquaresX = $this->sumSquares($xValues);
+ $sumSquaresY = $this->sumSquares($yValues);
+ $mBase = $mDivisor = 0.0;
+ $xy_sum = 0.0;
+ for ($i = 0; $i < $this->valueCount; ++$i) {
+ $xy_sum += $xValues[$i] * $yValues[$i];
+
+ if ($const === true) {
+ $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY);
+ $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX);
+ } else {
+ $mBase += $xValues[$i] * $yValues[$i];
+ $mDivisor += $xValues[$i] * $xValues[$i];
+ }
+ }
+
+ // calculate slope
+ $this->slope = $mBase / $mDivisor;
+
+ // calculate intersect
+ $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;
+
+ $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
+ }
+
+ /**
+ * Define the regression.
+ *
+ * @param float[] $yValues The set of Y-values for this regression
+ * @param float[] $xValues The set of X-values for this regression
+ */
+ public function __construct($yValues, $xValues = [])
+ {
+ // Calculate number of points
+ $yValueCount = count($yValues);
+ $xValueCount = count($xValues);
+
+ // Define X Values if necessary
+ if ($xValueCount === 0) {
+ $xValues = range(1, $yValueCount);
+ } elseif ($yValueCount !== $xValueCount) {
+ // Ensure both arrays of points are the same size
+ $this->error = true;
+ }
+
+ $this->valueCount = $yValueCount;
+ $this->xValues = $xValues;
+ $this->yValues = $yValues;
+ }
+}
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/Shared/Trend/PolynomialBestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
new file mode 100644
index 0000000..2c8eea5
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
@@ -0,0 +1,202 @@
+order;
+ }
+
+ /**
+ * Return the Y-Value for a specified value of X.
+ *
+ * @param float $xValue X-Value
+ *
+ * @return float Y-Value
+ */
+ public function getValueOfYForX($xValue)
+ {
+ $retVal = $this->getIntersect();
+ $slope = $this->getSlope();
+ // @phpstan-ignore-next-line
+ foreach ($slope as $key => $value) {
+ if ($value != 0.0) {
+ $retVal += $value * $xValue ** ($key + 1);
+ }
+ }
+
+ return $retVal;
+ }
+
+ /**
+ * 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);
+
+ $equation = 'Y = ' . $intersect;
+ // @phpstan-ignore-next-line
+ foreach ($slope as $key => $value) {
+ if ($value != 0.0) {
+ $equation .= ' + ' . $value . ' * X';
+ if ($key > 0) {
+ $equation .= '^' . ($key + 1);
+ }
+ }
+ }
+
+ return $equation;
+ }
+
+ /**
+ * 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) {
+ $coefficients = [];
+ foreach ($this->slope as $coefficient) {
+ $coefficients[] = round($coefficient, $dp);
+ }
+
+ // @phpstan-ignore-next-line
+ return $coefficients;
+ }
+
+ return $this->slope;
+ }
+
+ public function getCoefficients($dp = 0)
+ {
+ return array_merge([$this->getIntersect($dp)], $this->getSlope($dp));
+ }
+
+ /**
+ * Execute the regression and calculate the goodness of fit for a set of X and Y data values.
+ *
+ * @param int $order Order of Polynomial for this regression
+ * @param float[] $yValues The set of Y-values for this regression
+ * @param float[] $xValues The set of X-values for this regression
+ */
+ private function polynomialRegression($order, $yValues, $xValues): void
+ {
+ // calculate sums
+ $x_sum = array_sum($xValues);
+ $y_sum = array_sum($yValues);
+ $xx_sum = $xy_sum = $yy_sum = 0;
+ for ($i = 0; $i < $this->valueCount; ++$i) {
+ $xy_sum += $xValues[$i] * $yValues[$i];
+ $xx_sum += $xValues[$i] * $xValues[$i];
+ $yy_sum += $yValues[$i] * $yValues[$i];
+ }
+ /*
+ * This routine uses logic from the PHP port of polyfit version 0.1
+ * written by Michael Bommarito and Paul Meagher
+ *
+ * The function fits a polynomial function of order $order through
+ * a series of x-y data points using least squares.
+ *
+ */
+ $A = [];
+ $B = [];
+ for ($i = 0; $i < $this->valueCount; ++$i) {
+ for ($j = 0; $j <= $order; ++$j) {
+ $A[$i][$j] = $xValues[$i] ** $j;
+ }
+ }
+ for ($i = 0; $i < $this->valueCount; ++$i) {
+ $B[$i] = [$yValues[$i]];
+ }
+ $matrixA = new Matrix($A);
+ $matrixB = new Matrix($B);
+ $C = $matrixA->solve($matrixB);
+
+ $coefficients = [];
+ for ($i = 0; $i < $C->getRowDimension(); ++$i) {
+ $r = $C->get($i, 0);
+ if (abs($r) <= 10 ** (-9)) {
+ $r = 0;
+ }
+ $coefficients[] = $r;
+ }
+
+ $this->intersect = array_shift($coefficients);
+ $this->slope = $coefficients;
+
+ $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
+ foreach ($this->xValues as $xKey => $xValue) {
+ $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
+ }
+ }
+
+ /**
+ * Define the regression and calculate the goodness of fit for a set of X and Y data values.
+ *
+ * @param int $order Order of Polynomial for this regression
+ * @param float[] $yValues The set of Y-values for this regression
+ * @param float[] $xValues The set of X-values for this regression
+ */
+ public function __construct($order, $yValues, $xValues = [])
+ {
+ parent::__construct($yValues, $xValues);
+
+ if (!$this->error) {
+ if ($order < $this->valueCount) {
+ $this->bestFitType .= '_' . $order;
+ $this->order = $order;
+ $this->polynomialRegression($order, $yValues, $xValues);
+ if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) {
+ $this->error = true;
+ }
+ } else {
+ $this->error = true;
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php
new file mode 100644
index 0000000..cafd011
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php
@@ -0,0 +1,109 @@
+getIntersect() * ($xValue - $this->xOffset) ** $this->getSlope();
+ }
+
+ /**
+ * 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->yOffset) / $this->getIntersect()) ** (1 / $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 . ' * X^' . $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 powerRegression(array $yValues, array $xValues, bool $const): void
+ {
+ $adjustedYValues = array_map(
+ function ($value) {
+ return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
+ },
+ $yValues
+ );
+ $adjustedXValues = array_map(
+ function ($value) {
+ return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
+ },
+ $xValues
+ );
+
+ $this->leastSquareFit($adjustedYValues, $adjustedXValues, $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->powerRegression($yValues, $xValues, (bool) $const);
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php
new file mode 100644
index 0000000..61d1183
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php
@@ -0,0 +1,121 @@
+getGoodnessOfFit();
+ }
+ if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
+ foreach (self::$trendTypePolynomialOrders as $trendMethod) {
+ $order = substr($trendMethod, -1);
+ $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
+ if ($bestFit[$trendMethod]->getError()) {
+ unset($bestFit[$trendMethod]);
+ } else {
+ $bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
+ }
+ }
+ }
+ // Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class
+ arsort($bestFitValue);
+ $bestFitType = key($bestFitValue);
+
+ return $bestFit[$bestFitType];
+ default:
+ return false;
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php
new file mode 100644
index 0000000..c35b782
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php
@@ -0,0 +1,92 @@
+openMemory();
+ } else {
+ // Create temporary filename
+ if ($pTemporaryStorageFolder === null) {
+ $pTemporaryStorageFolder = File::sysGetTempDir();
+ }
+ $this->tempFileName = @tempnam($pTemporaryStorageFolder, 'xml');
+
+ // Open storage
+ if ($this->openUri($this->tempFileName) === false) {
+ // Fallback to memory...
+ $this->openMemory();
+ }
+ }
+
+ // Set default values
+ if (self::$debugEnabled) {
+ $this->setIndent(true);
+ }
+ }
+
+ /**
+ * Destructor.
+ */
+ public function __destruct()
+ {
+ // Unlink temporary files
+ if ($this->tempFileName != '') {
+ @unlink($this->tempFileName);
+ }
+ }
+
+ /**
+ * Get written data.
+ *
+ * @return string
+ */
+ public function getData()
+ {
+ if ($this->tempFileName == '') {
+ return $this->outputMemory(true);
+ }
+ $this->flush();
+
+ return file_get_contents($this->tempFileName);
+ }
+
+ /**
+ * Wrapper method for writeRaw.
+ *
+ * @param string|string[] $text
+ *
+ * @return bool
+ */
+ public function writeRawData($text)
+ {
+ if (is_array($text)) {
+ $text = implode("\n", $text);
+ }
+
+ return $this->writeRaw(htmlspecialchars($text ?? ''));
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Xls.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Xls.php
new file mode 100644
index 0000000..26035ec
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Xls.php
@@ -0,0 +1,278 @@
+getParent()->getDefaultStyle()->getFont();
+
+ $columnDimensions = $sheet->getColumnDimensions();
+
+ // first find the true column width in pixels (uncollapsed and unhidden)
+ if (isset($columnDimensions[$col]) && $columnDimensions[$col]->getWidth() != -1) {
+ // then we have column dimension with explicit width
+ $columnDimension = $columnDimensions[$col];
+ $width = $columnDimension->getWidth();
+ $pixelWidth = Drawing::cellDimensionToPixels($width, $font);
+ } elseif ($sheet->getDefaultColumnDimension()->getWidth() != -1) {
+ // then we have default column dimension with explicit width
+ $defaultColumnDimension = $sheet->getDefaultColumnDimension();
+ $width = $defaultColumnDimension->getWidth();
+ $pixelWidth = Drawing::cellDimensionToPixels($width, $font);
+ } else {
+ // we don't even have any default column dimension. Width depends on default font
+ $pixelWidth = Font::getDefaultColumnWidthByFont($font, true);
+ }
+
+ // now find the effective column width in pixels
+ if (isset($columnDimensions[$col]) && !$columnDimensions[$col]->getVisible()) {
+ $effectivePixelWidth = 0;
+ } else {
+ $effectivePixelWidth = $pixelWidth;
+ }
+
+ return $effectivePixelWidth;
+ }
+
+ /**
+ * Convert the height of a cell from user's units to pixels. By interpolation
+ * the relationship is: y = 4/3x. If the height hasn't been set by the user we
+ * use the default value. If the row is hidden we use a value of zero.
+ *
+ * @param Worksheet $sheet The sheet
+ * @param int $row The row index (1-based)
+ *
+ * @return int The width in pixels
+ */
+ public static function sizeRow($sheet, $row = 1)
+ {
+ // default font of the workbook
+ $font = $sheet->getParent()->getDefaultStyle()->getFont();
+
+ $rowDimensions = $sheet->getRowDimensions();
+
+ // first find the true row height in pixels (uncollapsed and unhidden)
+ if (isset($rowDimensions[$row]) && $rowDimensions[$row]->getRowHeight() != -1) {
+ // then we have a row dimension
+ $rowDimension = $rowDimensions[$row];
+ $rowHeight = $rowDimension->getRowHeight();
+ $pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10
+ } elseif ($sheet->getDefaultRowDimension()->getRowHeight() != -1) {
+ // then we have a default row dimension with explicit height
+ $defaultRowDimension = $sheet->getDefaultRowDimension();
+ $rowHeight = $defaultRowDimension->getRowHeight();
+ $pixelRowHeight = Drawing::pointsToPixels($rowHeight);
+ } else {
+ // we don't even have any default row dimension. Height depends on default font
+ $pointRowHeight = Font::getDefaultRowHeightByFont($font);
+ $pixelRowHeight = Font::fontSizeToPixels($pointRowHeight);
+ }
+
+ // now find the effective row height in pixels
+ if (isset($rowDimensions[$row]) && !$rowDimensions[$row]->getVisible()) {
+ $effectivePixelRowHeight = 0;
+ } else {
+ $effectivePixelRowHeight = $pixelRowHeight;
+ }
+
+ return $effectivePixelRowHeight;
+ }
+
+ /**
+ * Get the horizontal distance in pixels between two anchors
+ * The distanceX is found as sum of all the spanning columns widths minus correction for the two offsets.
+ *
+ * @param string $startColumn
+ * @param int $startOffsetX Offset within start cell measured in 1/1024 of the cell width
+ * @param string $endColumn
+ * @param int $endOffsetX Offset within end cell measured in 1/1024 of the cell width
+ *
+ * @return int Horizontal measured in pixels
+ */
+ public static function getDistanceX(Worksheet $sheet, $startColumn = 'A', $startOffsetX = 0, $endColumn = 'A', $endOffsetX = 0)
+ {
+ $distanceX = 0;
+
+ // add the widths of the spanning columns
+ $startColumnIndex = Coordinate::columnIndexFromString($startColumn);
+ $endColumnIndex = Coordinate::columnIndexFromString($endColumn);
+ for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) {
+ $distanceX += self::sizeCol($sheet, Coordinate::stringFromColumnIndex($i));
+ }
+
+ // correct for offsetX in startcell
+ $distanceX -= (int) floor(self::sizeCol($sheet, $startColumn) * $startOffsetX / 1024);
+
+ // correct for offsetX in endcell
+ $distanceX -= (int) floor(self::sizeCol($sheet, $endColumn) * (1 - $endOffsetX / 1024));
+
+ return $distanceX;
+ }
+
+ /**
+ * Get the vertical distance in pixels between two anchors
+ * The distanceY is found as sum of all the spanning rows minus two offsets.
+ *
+ * @param int $startRow (1-based)
+ * @param int $startOffsetY Offset within start cell measured in 1/256 of the cell height
+ * @param int $endRow (1-based)
+ * @param int $endOffsetY Offset within end cell measured in 1/256 of the cell height
+ *
+ * @return int Vertical distance measured in pixels
+ */
+ public static function getDistanceY(Worksheet $sheet, $startRow = 1, $startOffsetY = 0, $endRow = 1, $endOffsetY = 0)
+ {
+ $distanceY = 0;
+
+ // add the widths of the spanning rows
+ for ($row = $startRow; $row <= $endRow; ++$row) {
+ $distanceY += self::sizeRow($sheet, $row);
+ }
+
+ // correct for offsetX in startcell
+ $distanceY -= (int) floor(self::sizeRow($sheet, $startRow) * $startOffsetY / 256);
+
+ // correct for offsetX in endcell
+ $distanceY -= (int) floor(self::sizeRow($sheet, $endRow) * (1 - $endOffsetY / 256));
+
+ return $distanceY;
+ }
+
+ /**
+ * Convert 1-cell anchor coordinates to 2-cell anchor coordinates
+ * This function is ported from PEAR Spreadsheet_Writer_Excel with small modifications.
+ *
+ * Calculate the vertices that define the position of the image as required by
+ * the OBJ record.
+ *
+ * +------------+------------+
+ * | A | B |
+ * +-----+------------+------------+
+ * | |(x1,y1) | |
+ * | 1 |(A1)._______|______ |
+ * | | | | |
+ * | | | | |
+ * +-----+----| BITMAP |-----+
+ * | | | | |
+ * | 2 | |______________. |
+ * | | | (B2)|
+ * | | | (x2,y2)|
+ * +---- +------------+------------+
+ *
+ * Example of a bitmap that covers some of the area from cell A1 to cell B2.
+ *
+ * Based on the width and height of the bitmap we need to calculate 8 vars:
+ * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
+ * The width and height of the cells are also variable and have to be taken into
+ * account.
+ * The values of $col_start and $row_start are passed in from the calling
+ * function. The values of $col_end and $row_end are calculated by subtracting
+ * the width and height of the bitmap from the width and height of the
+ * underlying cells.
+ * The vertices are expressed as a percentage of the underlying cell width as
+ * follows (rhs values are in pixels):
+ *
+ * x1 = X / W *1024
+ * y1 = Y / H *256
+ * x2 = (X-1) / W *1024
+ * y2 = (Y-1) / H *256
+ *
+ * Where: X is distance from the left side of the underlying cell
+ * Y is distance from the top of the underlying cell
+ * W is the width of the cell
+ * H is the height of the cell
+ *
+ * @param Worksheet $sheet
+ * @param string $coordinates E.g. 'A1'
+ * @param int $offsetX Horizontal offset in pixels
+ * @param int $offsetY Vertical offset in pixels
+ * @param int $width Width in pixels
+ * @param int $height Height in pixels
+ *
+ * @return null|array
+ */
+ public static function oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height)
+ {
+ [$col_start, $row] = Coordinate::indexesFromString($coordinates);
+ $row_start = $row - 1;
+
+ $x1 = $offsetX;
+ $y1 = $offsetY;
+
+ // Initialise end cell to the same as the start cell
+ $col_end = $col_start; // Col containing lower right corner of object
+ $row_end = $row_start; // Row containing bottom right corner of object
+
+ // Zero the specified offset if greater than the cell dimensions
+ if ($x1 >= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start))) {
+ $x1 = 0;
+ }
+ if ($y1 >= self::sizeRow($sheet, $row_start + 1)) {
+ $y1 = 0;
+ }
+
+ $width = $width + $x1 - 1;
+ $height = $height + $y1 - 1;
+
+ // Subtract the underlying cell widths to find the end cell of the image
+ while ($width >= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end))) {
+ $width -= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end));
+ ++$col_end;
+ }
+
+ // Subtract the underlying cell heights to find the end cell of the image
+ while ($height >= self::sizeRow($sheet, $row_end + 1)) {
+ $height -= self::sizeRow($sheet, $row_end + 1);
+ ++$row_end;
+ }
+
+ // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
+ // with zero height or width.
+ if (self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start)) == 0) {
+ return null;
+ }
+ if (self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) == 0) {
+ return null;
+ }
+ if (self::sizeRow($sheet, $row_start + 1) == 0) {
+ return null;
+ }
+ if (self::sizeRow($sheet, $row_end + 1) == 0) {
+ return null;
+ }
+
+ // Convert the pixel values to the percentage value expected by Excel
+ $x1 = $x1 / self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_start)) * 1024;
+ $y1 = $y1 / self::sizeRow($sheet, $row_start + 1) * 256;
+ $x2 = ($width + 1) / self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object
+ $y2 = ($height + 1) / self::sizeRow($sheet, $row_end + 1) * 256; // Distance to bottom of object
+
+ $startCoordinates = Coordinate::stringFromColumnIndex($col_start) . ($row_start + 1);
+ $endCoordinates = Coordinate::stringFromColumnIndex($col_end) . ($row_end + 1);
+
+ return [
+ 'startCoordinates' => $startCoordinates,
+ 'startOffsetX' => $x1,
+ 'startOffsetY' => $y1,
+ 'endCoordinates' => $endCoordinates,
+ 'endOffsetX' => $x2,
+ 'endOffsetY' => $y2,
+ ];
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php
new file mode 100644
index 0000000..db617ef
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php
@@ -0,0 +1,1593 @@
+hasMacros;
+ }
+
+ /**
+ * Define if a workbook has macros.
+ *
+ * @param bool $hasMacros true|false
+ */
+ public function setHasMacros($hasMacros): void
+ {
+ $this->hasMacros = (bool) $hasMacros;
+ }
+
+ /**
+ * Set the macros code.
+ *
+ * @param string $macroCode string|null
+ */
+ public function setMacrosCode($macroCode): void
+ {
+ $this->macrosCode = $macroCode;
+ $this->setHasMacros($macroCode !== null);
+ }
+
+ /**
+ * Return the macros code.
+ *
+ * @return null|string
+ */
+ public function getMacrosCode()
+ {
+ return $this->macrosCode;
+ }
+
+ /**
+ * Set the macros certificate.
+ *
+ * @param null|string $certificate
+ */
+ public function setMacrosCertificate($certificate): void
+ {
+ $this->macrosCertificate = $certificate;
+ }
+
+ /**
+ * Is the project signed ?
+ *
+ * @return bool true|false
+ */
+ public function hasMacrosCertificate()
+ {
+ return $this->macrosCertificate !== null;
+ }
+
+ /**
+ * Return the macros certificate.
+ *
+ * @return null|string
+ */
+ public function getMacrosCertificate()
+ {
+ return $this->macrosCertificate;
+ }
+
+ /**
+ * Remove all macros, certificate from spreadsheet.
+ */
+ public function discardMacros(): void
+ {
+ $this->hasMacros = false;
+ $this->macrosCode = null;
+ $this->macrosCertificate = null;
+ }
+
+ /**
+ * set ribbon XML data.
+ *
+ * @param null|mixed $target
+ * @param null|mixed $xmlData
+ */
+ public function setRibbonXMLData($target, $xmlData): void
+ {
+ if ($target !== null && $xmlData !== null) {
+ $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData];
+ } else {
+ $this->ribbonXMLData = null;
+ }
+ }
+
+ /**
+ * retrieve ribbon XML Data.
+ *
+ * @param string $what
+ *
+ * @return null|array|string
+ */
+ public function getRibbonXMLData($what = 'all') //we need some constants here...
+ {
+ $returnData = null;
+ $what = strtolower($what);
+ switch ($what) {
+ case 'all':
+ $returnData = $this->ribbonXMLData;
+
+ break;
+ case 'target':
+ case 'data':
+ if (is_array($this->ribbonXMLData) && isset($this->ribbonXMLData[$what])) {
+ $returnData = $this->ribbonXMLData[$what];
+ }
+
+ break;
+ }
+
+ return $returnData;
+ }
+
+ /**
+ * store binaries ribbon objects (pictures).
+ *
+ * @param null|mixed $BinObjectsNames
+ * @param null|mixed $BinObjectsData
+ */
+ public function setRibbonBinObjects($BinObjectsNames, $BinObjectsData): void
+ {
+ if ($BinObjectsNames !== null && $BinObjectsData !== null) {
+ $this->ribbonBinObjects = ['names' => $BinObjectsNames, 'data' => $BinObjectsData];
+ } else {
+ $this->ribbonBinObjects = null;
+ }
+ }
+
+ /**
+ * List of unparsed loaded data for export to same format with better compatibility.
+ * It has to be minimized when the library start to support currently unparsed data.
+ *
+ * @internal
+ *
+ * @return array
+ */
+ public function getUnparsedLoadedData()
+ {
+ return $this->unparsedLoadedData;
+ }
+
+ /**
+ * List of unparsed loaded data for export to same format with better compatibility.
+ * It has to be minimized when the library start to support currently unparsed data.
+ *
+ * @internal
+ */
+ public function setUnparsedLoadedData(array $unparsedLoadedData): void
+ {
+ $this->unparsedLoadedData = $unparsedLoadedData;
+ }
+
+ /**
+ * return the extension of a filename. Internal use for a array_map callback (php<5.3 don't like lambda function).
+ *
+ * @param mixed $path
+ *
+ * @return string
+ */
+ private function getExtensionOnly($path)
+ {
+ $extension = pathinfo($path, PATHINFO_EXTENSION);
+
+ return is_array($extension) ? '' : $extension;
+ }
+
+ /**
+ * retrieve Binaries Ribbon Objects.
+ *
+ * @param string $what
+ *
+ * @return null|array
+ */
+ public function getRibbonBinObjects($what = 'all')
+ {
+ $ReturnData = null;
+ $what = strtolower($what);
+ switch ($what) {
+ case 'all':
+ return $this->ribbonBinObjects;
+
+ break;
+ case 'names':
+ case 'data':
+ if (is_array($this->ribbonBinObjects) && isset($this->ribbonBinObjects[$what])) {
+ $ReturnData = $this->ribbonBinObjects[$what];
+ }
+
+ break;
+ case 'types':
+ if (
+ is_array($this->ribbonBinObjects) &&
+ isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data'])
+ ) {
+ $tmpTypes = array_keys($this->ribbonBinObjects['data']);
+ $ReturnData = array_unique(array_map([$this, 'getExtensionOnly'], $tmpTypes));
+ } else {
+ $ReturnData = []; // the caller want an array... not null if empty
+ }
+
+ break;
+ }
+
+ return $ReturnData;
+ }
+
+ /**
+ * This workbook have a custom UI ?
+ *
+ * @return bool
+ */
+ public function hasRibbon()
+ {
+ return $this->ribbonXMLData !== null;
+ }
+
+ /**
+ * This workbook have additionnal object for the ribbon ?
+ *
+ * @return bool
+ */
+ public function hasRibbonBinObjects()
+ {
+ return $this->ribbonBinObjects !== null;
+ }
+
+ /**
+ * Check if a sheet with a specified code name already exists.
+ *
+ * @param string $pSheetCodeName Name of the worksheet to check
+ *
+ * @return bool
+ */
+ public function sheetCodeNameExists($pSheetCodeName)
+ {
+ return $this->getSheetByCodeName($pSheetCodeName) !== null;
+ }
+
+ /**
+ * Get sheet by code name. Warning : sheet don't have always a code name !
+ *
+ * @param string $pName Sheet name
+ *
+ * @return null|Worksheet
+ */
+ public function getSheetByCodeName($pName)
+ {
+ $worksheetCount = count($this->workSheetCollection);
+ for ($i = 0; $i < $worksheetCount; ++$i) {
+ if ($this->workSheetCollection[$i]->getCodeName() == $pName) {
+ return $this->workSheetCollection[$i];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Create a new PhpSpreadsheet with one Worksheet.
+ */
+ public function __construct()
+ {
+ $this->uniqueID = uniqid('', true);
+ $this->calculationEngine = new Calculation($this);
+
+ // Initialise worksheet collection and add one worksheet
+ $this->workSheetCollection = [];
+ $this->workSheetCollection[] = new Worksheet($this);
+ $this->activeSheetIndex = 0;
+
+ // Create document properties
+ $this->properties = new Document\Properties();
+
+ // Create document security
+ $this->security = new Document\Security();
+
+ // Set defined names
+ $this->definedNames = [];
+
+ // Create the cellXf supervisor
+ $this->cellXfSupervisor = new Style(true);
+ $this->cellXfSupervisor->bindParent($this);
+
+ // Create the default style
+ $this->addCellXf(new Style());
+ $this->addCellStyleXf(new Style());
+ }
+
+ /**
+ * Code to execute when this worksheet is unset().
+ */
+ public function __destruct()
+ {
+ $this->disconnectWorksheets();
+ $this->calculationEngine = null;
+ $this->cellXfCollection = [];
+ $this->cellStyleXfCollection = [];
+ }
+
+ /**
+ * Disconnect all worksheets from this PhpSpreadsheet workbook object,
+ * typically so that the PhpSpreadsheet object can be unset.
+ */
+ public function disconnectWorksheets(): void
+ {
+ foreach ($this->workSheetCollection as $worksheet) {
+ $worksheet->disconnectCells();
+ unset($worksheet);
+ }
+ $this->workSheetCollection = [];
+ }
+
+ /**
+ * Return the calculation engine for this worksheet.
+ *
+ * @return null|Calculation
+ */
+ public function getCalculationEngine()
+ {
+ return $this->calculationEngine;
+ }
+
+ /**
+ * Get properties.
+ *
+ * @return Document\Properties
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * Set properties.
+ */
+ public function setProperties(Document\Properties $pValue): void
+ {
+ $this->properties = $pValue;
+ }
+
+ /**
+ * Get security.
+ *
+ * @return Document\Security
+ */
+ public function getSecurity()
+ {
+ return $this->security;
+ }
+
+ /**
+ * Set security.
+ */
+ public function setSecurity(Document\Security $pValue): void
+ {
+ $this->security = $pValue;
+ }
+
+ /**
+ * Get active sheet.
+ *
+ * @return Worksheet
+ */
+ public function getActiveSheet()
+ {
+ return $this->getSheet($this->activeSheetIndex);
+ }
+
+ /**
+ * Create sheet and add it to this workbook.
+ *
+ * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
+ *
+ * @return Worksheet
+ */
+ public function createSheet($sheetIndex = null)
+ {
+ $newSheet = new Worksheet($this);
+ $this->addSheet($newSheet, $sheetIndex);
+
+ return $newSheet;
+ }
+
+ /**
+ * Check if a sheet with a specified name already exists.
+ *
+ * @param string $pSheetName Name of the worksheet to check
+ *
+ * @return bool
+ */
+ public function sheetNameExists($pSheetName)
+ {
+ return $this->getSheetByName($pSheetName) !== null;
+ }
+
+ /**
+ * Add sheet.
+ *
+ * @param null|int $iSheetIndex Index where sheet should go (0,1,..., or null for last)
+ *
+ * @return Worksheet
+ */
+ public function addSheet(Worksheet $pSheet, $iSheetIndex = null)
+ {
+ if ($this->sheetNameExists($pSheet->getTitle())) {
+ throw new Exception(
+ "Workbook already contains a worksheet named '{$pSheet->getTitle()}'. Rename this worksheet first."
+ );
+ }
+
+ if ($iSheetIndex === null) {
+ if ($this->activeSheetIndex < 0) {
+ $this->activeSheetIndex = 0;
+ }
+ $this->workSheetCollection[] = $pSheet;
+ } else {
+ // Insert the sheet at the requested index
+ array_splice(
+ $this->workSheetCollection,
+ $iSheetIndex,
+ 0,
+ [$pSheet]
+ );
+
+ // Adjust active sheet index if necessary
+ if ($this->activeSheetIndex >= $iSheetIndex) {
+ ++$this->activeSheetIndex;
+ }
+ }
+
+ if ($pSheet->getParent() === null) {
+ $pSheet->rebindParent($this);
+ }
+
+ return $pSheet;
+ }
+
+ /**
+ * Remove sheet by index.
+ *
+ * @param int $pIndex Active sheet index
+ */
+ public function removeSheetByIndex($pIndex): void
+ {
+ $numSheets = count($this->workSheetCollection);
+ if ($pIndex > $numSheets - 1) {
+ throw new Exception(
+ "You tried to remove a sheet by the out of bounds index: {$pIndex}. The actual number of sheets is {$numSheets}."
+ );
+ }
+ array_splice($this->workSheetCollection, $pIndex, 1);
+
+ // Adjust active sheet index if necessary
+ if (
+ ($this->activeSheetIndex >= $pIndex) &&
+ ($this->activeSheetIndex > 0 || $numSheets <= 1)
+ ) {
+ --$this->activeSheetIndex;
+ }
+ }
+
+ /**
+ * Get sheet by index.
+ *
+ * @param int $pIndex Sheet index
+ *
+ * @return Worksheet
+ */
+ public function getSheet($pIndex)
+ {
+ if (!isset($this->workSheetCollection[$pIndex])) {
+ $numSheets = $this->getSheetCount();
+
+ throw new Exception(
+ "Your requested sheet index: {$pIndex} is out of bounds. The actual number of sheets is {$numSheets}."
+ );
+ }
+
+ return $this->workSheetCollection[$pIndex];
+ }
+
+ /**
+ * Get all sheets.
+ *
+ * @return Worksheet[]
+ */
+ public function getAllSheets()
+ {
+ return $this->workSheetCollection;
+ }
+
+ /**
+ * Get sheet by name.
+ *
+ * @param string $pName Sheet name
+ *
+ * @return null|Worksheet
+ */
+ public function getSheetByName($pName)
+ {
+ $worksheetCount = count($this->workSheetCollection);
+ for ($i = 0; $i < $worksheetCount; ++$i) {
+ if ($this->workSheetCollection[$i]->getTitle() === trim($pName, "'")) {
+ return $this->workSheetCollection[$i];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get index for sheet.
+ *
+ * @return int index
+ */
+ public function getIndex(Worksheet $pSheet)
+ {
+ foreach ($this->workSheetCollection as $key => $value) {
+ if ($value->getHashCode() === $pSheet->getHashCode()) {
+ return $key;
+ }
+ }
+
+ throw new Exception('Sheet does not exist.');
+ }
+
+ /**
+ * Set index for sheet by sheet name.
+ *
+ * @param string $sheetName Sheet name to modify index for
+ * @param int $newIndex New index for the sheet
+ *
+ * @return int New sheet index
+ */
+ public function setIndexByName($sheetName, $newIndex)
+ {
+ $oldIndex = $this->getIndex($this->getSheetByName($sheetName));
+ $pSheet = array_splice(
+ $this->workSheetCollection,
+ $oldIndex,
+ 1
+ );
+ array_splice(
+ $this->workSheetCollection,
+ $newIndex,
+ 0,
+ $pSheet
+ );
+
+ return $newIndex;
+ }
+
+ /**
+ * Get sheet count.
+ *
+ * @return int
+ */
+ public function getSheetCount()
+ {
+ return count($this->workSheetCollection);
+ }
+
+ /**
+ * Get active sheet index.
+ *
+ * @return int Active sheet index
+ */
+ public function getActiveSheetIndex()
+ {
+ return $this->activeSheetIndex;
+ }
+
+ /**
+ * Set active sheet index.
+ *
+ * @param int $pIndex Active sheet index
+ *
+ * @return Worksheet
+ */
+ public function setActiveSheetIndex($pIndex)
+ {
+ $numSheets = count($this->workSheetCollection);
+
+ if ($pIndex > $numSheets - 1) {
+ throw new Exception(
+ "You tried to set a sheet active by the out of bounds index: {$pIndex}. The actual number of sheets is {$numSheets}."
+ );
+ }
+ $this->activeSheetIndex = $pIndex;
+
+ return $this->getActiveSheet();
+ }
+
+ /**
+ * Set active sheet index by name.
+ *
+ * @param string $pValue Sheet title
+ *
+ * @return Worksheet
+ */
+ public function setActiveSheetIndexByName($pValue)
+ {
+ if (($worksheet = $this->getSheetByName($pValue)) instanceof Worksheet) {
+ $this->setActiveSheetIndex($this->getIndex($worksheet));
+
+ return $worksheet;
+ }
+
+ throw new Exception('Workbook does not contain sheet:' . $pValue);
+ }
+
+ /**
+ * Get sheet names.
+ *
+ * @return string[]
+ */
+ public function getSheetNames()
+ {
+ $returnValue = [];
+ $worksheetCount = $this->getSheetCount();
+ for ($i = 0; $i < $worksheetCount; ++$i) {
+ $returnValue[] = $this->getSheet($i)->getTitle();
+ }
+
+ return $returnValue;
+ }
+
+ /**
+ * Add external sheet.
+ *
+ * @param Worksheet $pSheet External sheet to add
+ * @param null|int $iSheetIndex Index where sheet should go (0,1,..., or null for last)
+ *
+ * @return Worksheet
+ */
+ public function addExternalSheet(Worksheet $pSheet, $iSheetIndex = null)
+ {
+ if ($this->sheetNameExists($pSheet->getTitle())) {
+ throw new Exception("Workbook already contains a worksheet named '{$pSheet->getTitle()}'. Rename the external sheet first.");
+ }
+
+ // count how many cellXfs there are in this workbook currently, we will need this below
+ $countCellXfs = count($this->cellXfCollection);
+
+ // copy all the shared cellXfs from the external workbook and append them to the current
+ foreach ($pSheet->getParent()->getCellXfCollection() as $cellXf) {
+ $this->addCellXf(clone $cellXf);
+ }
+
+ // move sheet to this workbook
+ $pSheet->rebindParent($this);
+
+ // update the cellXfs
+ foreach ($pSheet->getCoordinates(false) as $coordinate) {
+ $cell = $pSheet->getCell($coordinate);
+ $cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
+ }
+
+ return $this->addSheet($pSheet, $iSheetIndex);
+ }
+
+ /**
+ * Get an array of all Named Ranges.
+ *
+ * @return DefinedName[]
+ */
+ public function getNamedRanges(): array
+ {
+ return array_filter(
+ $this->definedNames,
+ function (DefinedName $definedName) {
+ return $definedName->isFormula() === self::DEFINED_NAME_IS_RANGE;
+ }
+ );
+ }
+
+ /**
+ * Get an array of all Named Formulae.
+ *
+ * @return DefinedName[]
+ */
+ public function getNamedFormulae(): array
+ {
+ return array_filter(
+ $this->definedNames,
+ function (DefinedName $definedName) {
+ return $definedName->isFormula() === self::DEFINED_NAME_IS_FORMULA;
+ }
+ );
+ }
+
+ /**
+ * Get an array of all Defined Names (both named ranges and named formulae).
+ *
+ * @return DefinedName[]
+ */
+ public function getDefinedNames(): array
+ {
+ return $this->definedNames;
+ }
+
+ /**
+ * Add a named range.
+ * If a named range with this name already exists, then this will replace the existing value.
+ */
+ public function addNamedRange(NamedRange $namedRange): void
+ {
+ $this->addDefinedName($namedRange);
+ }
+
+ /**
+ * Add a named formula.
+ * If a named formula with this name already exists, then this will replace the existing value.
+ */
+ public function addNamedFormula(NamedFormula $namedFormula): void
+ {
+ $this->addDefinedName($namedFormula);
+ }
+
+ /**
+ * Add a defined name (either a named range or a named formula).
+ * If a defined named with this name already exists, then this will replace the existing value.
+ */
+ public function addDefinedName(DefinedName $definedName): void
+ {
+ $upperCaseName = StringHelper::strToUpper($definedName->getName());
+ if ($definedName->getScope() == null) {
+ // global scope
+ $this->definedNames[$upperCaseName] = $definedName;
+ } else {
+ // local scope
+ $this->definedNames[$definedName->getScope()->getTitle() . '!' . $upperCaseName] = $definedName;
+ }
+ }
+
+ /**
+ * Get named range.
+ *
+ * @param null|Worksheet $pSheet Scope. Use null for global scope
+ */
+ public function getNamedRange(string $namedRange, ?Worksheet $pSheet = null): ?NamedRange
+ {
+ $returnValue = null;
+
+ if ($namedRange !== '') {
+ $namedRange = StringHelper::strToUpper($namedRange);
+ // first look for global named range
+ $returnValue = $this->getGlobalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE);
+ // then look for local named range (has priority over global named range if both names exist)
+ $returnValue = $this->getLocalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE, $pSheet) ?: $returnValue;
+ }
+
+ return $returnValue instanceof NamedRange ? $returnValue : null;
+ }
+
+ /**
+ * Get named formula.
+ *
+ * @param null|Worksheet $pSheet Scope. Use null for global scope
+ */
+ public function getNamedFormula(string $namedFormula, ?Worksheet $pSheet = null): ?NamedFormula
+ {
+ $returnValue = null;
+
+ if ($namedFormula !== '') {
+ $namedFormula = StringHelper::strToUpper($namedFormula);
+ // first look for global named formula
+ $returnValue = $this->getGlobalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA);
+ // then look for local named formula (has priority over global named formula if both names exist)
+ $returnValue = $this->getLocalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA, $pSheet) ?: $returnValue;
+ }
+
+ return $returnValue instanceof NamedFormula ? $returnValue : null;
+ }
+
+ private function getGlobalDefinedNameByType(string $name, bool $type): ?DefinedName
+ {
+ if (isset($this->definedNames[$name]) && $this->definedNames[$name]->isFormula() === $type) {
+ return $this->definedNames[$name];
+ }
+
+ return null;
+ }
+
+ private function getLocalDefinedNameByType(string $name, bool $type, ?Worksheet $pSheet = null): ?DefinedName
+ {
+ if (
+ ($pSheet !== null) && isset($this->definedNames[$pSheet->getTitle() . '!' . $name])
+ && $this->definedNames[$pSheet->getTitle() . '!' . $name]->isFormula() === $type
+ ) {
+ return $this->definedNames[$pSheet->getTitle() . '!' . $name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get named range.
+ *
+ * @param null|Worksheet $pSheet Scope. Use null for global scope
+ */
+ public function getDefinedName(string $definedName, ?Worksheet $pSheet = null): ?DefinedName
+ {
+ $returnValue = null;
+
+ if ($definedName !== '') {
+ $definedName = StringHelper::strToUpper($definedName);
+ // first look for global defined name
+ if (isset($this->definedNames[$definedName])) {
+ $returnValue = $this->definedNames[$definedName];
+ }
+
+ // then look for local defined name (has priority over global defined name if both names exist)
+ if (($pSheet !== null) && isset($this->definedNames[$pSheet->getTitle() . '!' . $definedName])) {
+ $returnValue = $this->definedNames[$pSheet->getTitle() . '!' . $definedName];
+ }
+ }
+
+ return $returnValue;
+ }
+
+ /**
+ * Remove named range.
+ *
+ * @param null|Worksheet $pSheet scope: use null for global scope
+ *
+ * @return $this
+ */
+ public function removeNamedRange(string $namedRange, ?Worksheet $pSheet = null): self
+ {
+ if ($this->getNamedRange($namedRange, $pSheet) === null) {
+ return $this;
+ }
+
+ return $this->removeDefinedName($namedRange, $pSheet);
+ }
+
+ /**
+ * Remove named formula.
+ *
+ * @param null|Worksheet $pSheet scope: use null for global scope
+ *
+ * @return $this
+ */
+ public function removeNamedFormula(string $namedFormula, ?Worksheet $pSheet = null): self
+ {
+ if ($this->getNamedFormula($namedFormula, $pSheet) === null) {
+ return $this;
+ }
+
+ return $this->removeDefinedName($namedFormula, $pSheet);
+ }
+
+ /**
+ * Remove defined name.
+ *
+ * @param null|Worksheet $pSheet scope: use null for global scope
+ *
+ * @return $this
+ */
+ public function removeDefinedName(string $definedName, ?Worksheet $pSheet = null): self
+ {
+ $definedName = StringHelper::strToUpper($definedName);
+
+ if ($pSheet === null) {
+ if (isset($this->definedNames[$definedName])) {
+ unset($this->definedNames[$definedName]);
+ }
+ } else {
+ if (isset($this->definedNames[$pSheet->getTitle() . '!' . $definedName])) {
+ unset($this->definedNames[$pSheet->getTitle() . '!' . $definedName]);
+ } elseif (isset($this->definedNames[$definedName])) {
+ unset($this->definedNames[$definedName]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get worksheet iterator.
+ *
+ * @return Iterator
+ */
+ public function getWorksheetIterator()
+ {
+ return new Iterator($this);
+ }
+
+ /**
+ * Copy workbook (!= clone!).
+ *
+ * @return Spreadsheet
+ */
+ public function copy()
+ {
+ $copied = clone $this;
+
+ $worksheetCount = count($this->workSheetCollection);
+ for ($i = 0; $i < $worksheetCount; ++$i) {
+ $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy();
+ $this->workSheetCollection[$i]->rebindParent($this);
+ }
+
+ return $copied;
+ }
+
+ /**
+ * Implement PHP __clone to create a deep clone, not just a shallow copy.
+ */
+ public function __clone()
+ {
+ // @phpstan-ignore-next-line
+ foreach ($this as $key => $val) {
+ if (is_object($val) || (is_array($val))) {
+ $this->{$key} = unserialize(serialize($val));
+ }
+ }
+ }
+
+ /**
+ * Get the workbook collection of cellXfs.
+ *
+ * @return Style[]
+ */
+ public function getCellXfCollection()
+ {
+ return $this->cellXfCollection;
+ }
+
+ /**
+ * Get cellXf by index.
+ *
+ * @param int $pIndex
+ *
+ * @return Style
+ */
+ public function getCellXfByIndex($pIndex)
+ {
+ return $this->cellXfCollection[$pIndex];
+ }
+
+ /**
+ * Get cellXf by hash code.
+ *
+ * @param string $pValue
+ *
+ * @return false|Style
+ */
+ public function getCellXfByHashCode($pValue)
+ {
+ foreach ($this->cellXfCollection as $cellXf) {
+ if ($cellXf->getHashCode() === $pValue) {
+ return $cellXf;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if style exists in style collection.
+ *
+ * @param Style $pCellStyle
+ *
+ * @return bool
+ */
+ public function cellXfExists($pCellStyle)
+ {
+ return in_array($pCellStyle, $this->cellXfCollection, true);
+ }
+
+ /**
+ * Get default style.
+ *
+ * @return Style
+ */
+ public function getDefaultStyle()
+ {
+ if (isset($this->cellXfCollection[0])) {
+ return $this->cellXfCollection[0];
+ }
+
+ throw new Exception('No default style found for this workbook');
+ }
+
+ /**
+ * Add a cellXf to the workbook.
+ */
+ public function addCellXf(Style $style): void
+ {
+ $this->cellXfCollection[] = $style;
+ $style->setIndex(count($this->cellXfCollection) - 1);
+ }
+
+ /**
+ * Remove cellXf by index. It is ensured that all cells get their xf index updated.
+ *
+ * @param int $pIndex Index to cellXf
+ */
+ public function removeCellXfByIndex($pIndex): void
+ {
+ if ($pIndex > count($this->cellXfCollection) - 1) {
+ throw new Exception('CellXf index is out of bounds.');
+ }
+
+ // first remove the cellXf
+ array_splice($this->cellXfCollection, $pIndex, 1);
+
+ // then update cellXf indexes for cells
+ foreach ($this->workSheetCollection as $worksheet) {
+ foreach ($worksheet->getCoordinates(false) as $coordinate) {
+ $cell = $worksheet->getCell($coordinate);
+ $xfIndex = $cell->getXfIndex();
+ if ($xfIndex > $pIndex) {
+ // decrease xf index by 1
+ $cell->setXfIndex($xfIndex - 1);
+ } elseif ($xfIndex == $pIndex) {
+ // set to default xf index 0
+ $cell->setXfIndex(0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the cellXf supervisor.
+ *
+ * @return Style
+ */
+ public function getCellXfSupervisor()
+ {
+ return $this->cellXfSupervisor;
+ }
+
+ /**
+ * Get the workbook collection of cellStyleXfs.
+ *
+ * @return Style[]
+ */
+ public function getCellStyleXfCollection()
+ {
+ return $this->cellStyleXfCollection;
+ }
+
+ /**
+ * Get cellStyleXf by index.
+ *
+ * @param int $pIndex Index to cellXf
+ *
+ * @return Style
+ */
+ public function getCellStyleXfByIndex($pIndex)
+ {
+ return $this->cellStyleXfCollection[$pIndex];
+ }
+
+ /**
+ * Get cellStyleXf by hash code.
+ *
+ * @param string $pValue
+ *
+ * @return false|Style
+ */
+ public function getCellStyleXfByHashCode($pValue)
+ {
+ foreach ($this->cellStyleXfCollection as $cellStyleXf) {
+ if ($cellStyleXf->getHashCode() === $pValue) {
+ return $cellStyleXf;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add a cellStyleXf to the workbook.
+ */
+ public function addCellStyleXf(Style $pStyle): void
+ {
+ $this->cellStyleXfCollection[] = $pStyle;
+ $pStyle->setIndex(count($this->cellStyleXfCollection) - 1);
+ }
+
+ /**
+ * Remove cellStyleXf by index.
+ *
+ * @param int $pIndex Index to cellXf
+ */
+ public function removeCellStyleXfByIndex($pIndex): void
+ {
+ if ($pIndex > count($this->cellStyleXfCollection) - 1) {
+ throw new Exception('CellStyleXf index is out of bounds.');
+ }
+ array_splice($this->cellStyleXfCollection, $pIndex, 1);
+ }
+
+ /**
+ * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells
+ * and columns in the workbook.
+ */
+ public function garbageCollect(): void
+ {
+ // how many references are there to each cellXf ?
+ $countReferencesCellXf = [];
+ foreach ($this->cellXfCollection as $index => $cellXf) {
+ $countReferencesCellXf[$index] = 0;
+ }
+
+ foreach ($this->getWorksheetIterator() as $sheet) {
+ // from cells
+ foreach ($sheet->getCoordinates(false) as $coordinate) {
+ $cell = $sheet->getCell($coordinate);
+ ++$countReferencesCellXf[$cell->getXfIndex()];
+ }
+
+ // from row dimensions
+ foreach ($sheet->getRowDimensions() as $rowDimension) {
+ if ($rowDimension->getXfIndex() !== null) {
+ ++$countReferencesCellXf[$rowDimension->getXfIndex()];
+ }
+ }
+
+ // from column dimensions
+ foreach ($sheet->getColumnDimensions() as $columnDimension) {
+ ++$countReferencesCellXf[$columnDimension->getXfIndex()];
+ }
+ }
+
+ // remove cellXfs without references and create mapping so we can update xfIndex
+ // for all cells and columns
+ $countNeededCellXfs = 0;
+ $map = [];
+ foreach ($this->cellXfCollection as $index => $cellXf) {
+ if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf
+ ++$countNeededCellXfs;
+ } else {
+ unset($this->cellXfCollection[$index]);
+ }
+ $map[$index] = $countNeededCellXfs - 1;
+ }
+ $this->cellXfCollection = array_values($this->cellXfCollection);
+
+ // update the index for all cellXfs
+ foreach ($this->cellXfCollection as $i => $cellXf) {
+ $cellXf->setIndex($i);
+ }
+
+ // make sure there is always at least one cellXf (there should be)
+ if (empty($this->cellXfCollection)) {
+ $this->cellXfCollection[] = new Style();
+ }
+
+ // update the xfIndex for all cells, row dimensions, column dimensions
+ foreach ($this->getWorksheetIterator() as $sheet) {
+ // for all cells
+ foreach ($sheet->getCoordinates(false) as $coordinate) {
+ $cell = $sheet->getCell($coordinate);
+ $cell->setXfIndex($map[$cell->getXfIndex()]);
+ }
+
+ // for all row dimensions
+ foreach ($sheet->getRowDimensions() as $rowDimension) {
+ if ($rowDimension->getXfIndex() !== null) {
+ $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]);
+ }
+ }
+
+ // for all column dimensions
+ foreach ($sheet->getColumnDimensions() as $columnDimension) {
+ $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]);
+ }
+
+ // also do garbage collection for all the sheets
+ $sheet->garbageCollect();
+ }
+ }
+
+ /**
+ * Return the unique ID value assigned to this spreadsheet workbook.
+ *
+ * @return string
+ */
+ public function getID()
+ {
+ return $this->uniqueID;
+ }
+
+ /**
+ * Get the visibility of the horizonal scroll bar in the application.
+ *
+ * @return bool True if horizonal scroll bar is visible
+ */
+ public function getShowHorizontalScroll()
+ {
+ return $this->showHorizontalScroll;
+ }
+
+ /**
+ * Set the visibility of the horizonal scroll bar in the application.
+ *
+ * @param bool $showHorizontalScroll True if horizonal scroll bar is visible
+ */
+ public function setShowHorizontalScroll($showHorizontalScroll): void
+ {
+ $this->showHorizontalScroll = (bool) $showHorizontalScroll;
+ }
+
+ /**
+ * Get the visibility of the vertical scroll bar in the application.
+ *
+ * @return bool True if vertical scroll bar is visible
+ */
+ public function getShowVerticalScroll()
+ {
+ return $this->showVerticalScroll;
+ }
+
+ /**
+ * Set the visibility of the vertical scroll bar in the application.
+ *
+ * @param bool $showVerticalScroll True if vertical scroll bar is visible
+ */
+ public function setShowVerticalScroll($showVerticalScroll): void
+ {
+ $this->showVerticalScroll = (bool) $showVerticalScroll;
+ }
+
+ /**
+ * Get the visibility of the sheet tabs in the application.
+ *
+ * @return bool True if the sheet tabs are visible
+ */
+ public function getShowSheetTabs()
+ {
+ return $this->showSheetTabs;
+ }
+
+ /**
+ * Set the visibility of the sheet tabs in the application.
+ *
+ * @param bool $showSheetTabs True if sheet tabs are visible
+ */
+ public function setShowSheetTabs($showSheetTabs): void
+ {
+ $this->showSheetTabs = (bool) $showSheetTabs;
+ }
+
+ /**
+ * Return whether the workbook window is minimized.
+ *
+ * @return bool true if workbook window is minimized
+ */
+ public function getMinimized()
+ {
+ return $this->minimized;
+ }
+
+ /**
+ * Set whether the workbook window is minimized.
+ *
+ * @param bool $minimized true if workbook window is minimized
+ */
+ public function setMinimized($minimized): void
+ {
+ $this->minimized = (bool) $minimized;
+ }
+
+ /**
+ * Return whether to group dates when presenting the user with
+ * filtering optiomd in the user interface.
+ *
+ * @return bool true if workbook window is minimized
+ */
+ public function getAutoFilterDateGrouping()
+ {
+ return $this->autoFilterDateGrouping;
+ }
+
+ /**
+ * Set whether to group dates when presenting the user with
+ * filtering optiomd in the user interface.
+ *
+ * @param bool $autoFilterDateGrouping true if workbook window is minimized
+ */
+ public function setAutoFilterDateGrouping($autoFilterDateGrouping): void
+ {
+ $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping;
+ }
+
+ /**
+ * Return the first sheet in the book view.
+ *
+ * @return int First sheet in book view
+ */
+ public function getFirstSheetIndex()
+ {
+ return $this->firstSheetIndex;
+ }
+
+ /**
+ * Set the first sheet in the book view.
+ *
+ * @param int $firstSheetIndex First sheet in book view
+ */
+ public function setFirstSheetIndex($firstSheetIndex): void
+ {
+ if ($firstSheetIndex >= 0) {
+ $this->firstSheetIndex = (int) $firstSheetIndex;
+ } else {
+ throw new Exception('First sheet index must be a positive integer.');
+ }
+ }
+
+ /**
+ * Return the visibility status of the workbook.
+ *
+ * This may be one of the following three values:
+ * - visibile
+ *
+ * @return string Visible status
+ */
+ public function getVisibility()
+ {
+ return $this->visibility;
+ }
+
+ /**
+ * Set the visibility status of the workbook.
+ *
+ * Valid values are:
+ * - 'visible' (self::VISIBILITY_VISIBLE):
+ * Workbook window is visible
+ * - 'hidden' (self::VISIBILITY_HIDDEN):
+ * Workbook window is hidden, but can be shown by the user
+ * via the user interface
+ * - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN):
+ * Workbook window is hidden and cannot be shown in the
+ * user interface.
+ *
+ * @param string $visibility visibility status of the workbook
+ */
+ public function setVisibility($visibility): void
+ {
+ if ($visibility === null) {
+ $visibility = self::VISIBILITY_VISIBLE;
+ }
+
+ if (in_array($visibility, self::$workbookViewVisibilityValues)) {
+ $this->visibility = $visibility;
+ } else {
+ throw new Exception('Invalid visibility value.');
+ }
+ }
+
+ /**
+ * Get the ratio between the workbook tabs bar and the horizontal scroll bar.
+ * TabRatio is assumed to be out of 1000 of the horizontal window width.
+ *
+ * @return int Ratio between the workbook tabs bar and the horizontal scroll bar
+ */
+ public function getTabRatio()
+ {
+ return $this->tabRatio;
+ }
+
+ /**
+ * Set the ratio between the workbook tabs bar and the horizontal scroll bar
+ * TabRatio is assumed to be out of 1000 of the horizontal window width.
+ *
+ * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar
+ */
+ public function setTabRatio($tabRatio): void
+ {
+ if ($tabRatio >= 0 || $tabRatio <= 1000) {
+ $this->tabRatio = (int) $tabRatio;
+ } else {
+ throw new Exception('Tab ratio must be between 0 and 1000.');
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php
new file mode 100644
index 0000000..e072c52
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php
@@ -0,0 +1,483 @@
+horizontal = null;
+ $this->vertical = null;
+ $this->textRotation = null;
+ }
+ }
+
+ /**
+ * Get the shared style component for the currently active cell in currently active sheet.
+ * Only used for style supervisor.
+ *
+ * @return Alignment
+ */
+ public function getSharedComponent()
+ {
+ return $this->parent->getSharedComponent()->getAlignment();
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return ['alignment' => $array];
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getAlignment()->applyFromArray(
+ * [
+ * 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
+ * 'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER,
+ * 'textRotation' => 0,
+ * 'wrapText' => TRUE
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())
+ ->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['horizontal'])) {
+ $this->setHorizontal($pStyles['horizontal']);
+ }
+ if (isset($pStyles['vertical'])) {
+ $this->setVertical($pStyles['vertical']);
+ }
+ if (isset($pStyles['textRotation'])) {
+ $this->setTextRotation($pStyles['textRotation']);
+ }
+ if (isset($pStyles['wrapText'])) {
+ $this->setWrapText($pStyles['wrapText']);
+ }
+ if (isset($pStyles['shrinkToFit'])) {
+ $this->setShrinkToFit($pStyles['shrinkToFit']);
+ }
+ if (isset($pStyles['indent'])) {
+ $this->setIndent($pStyles['indent']);
+ }
+ if (isset($pStyles['readOrder'])) {
+ $this->setReadOrder($pStyles['readOrder']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Horizontal.
+ *
+ * @return null|string
+ */
+ public function getHorizontal()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHorizontal();
+ }
+
+ return $this->horizontal;
+ }
+
+ /**
+ * Set Horizontal.
+ *
+ * @param string $pValue see self::HORIZONTAL_*
+ *
+ * @return $this
+ */
+ public function setHorizontal($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = self::HORIZONTAL_GENERAL;
+ }
+
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['horizontal' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->horizontal = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Vertical.
+ *
+ * @return null|string
+ */
+ public function getVertical()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getVertical();
+ }
+
+ return $this->vertical;
+ }
+
+ /**
+ * Set Vertical.
+ *
+ * @param string $pValue see self::VERTICAL_*
+ *
+ * @return $this
+ */
+ public function setVertical($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = self::VERTICAL_BOTTOM;
+ }
+
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['vertical' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->vertical = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get TextRotation.
+ *
+ * @return null|int
+ */
+ public function getTextRotation()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getTextRotation();
+ }
+
+ return $this->textRotation;
+ }
+
+ /**
+ * Set TextRotation.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setTextRotation($pValue)
+ {
+ // Excel2007 value 255 => PhpSpreadsheet value -165
+ if ($pValue == self::TEXTROTATION_STACK_EXCEL) {
+ $pValue = self::TEXTROTATION_STACK_PHPSPREADSHEET;
+ }
+
+ // Set rotation
+ if (($pValue >= -90 && $pValue <= 90) || $pValue == self::TEXTROTATION_STACK_PHPSPREADSHEET) {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['textRotation' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->textRotation = $pValue;
+ }
+ } else {
+ throw new PhpSpreadsheetException('Text rotation should be a value between -90 and 90.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Wrap Text.
+ *
+ * @return bool
+ */
+ public function getWrapText()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getWrapText();
+ }
+
+ return $this->wrapText;
+ }
+
+ /**
+ * Set Wrap Text.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setWrapText($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['wrapText' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->wrapText = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Shrink to fit.
+ *
+ * @return bool
+ */
+ public function getShrinkToFit()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getShrinkToFit();
+ }
+
+ return $this->shrinkToFit;
+ }
+
+ /**
+ * Set Shrink to fit.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setShrinkToFit($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['shrinkToFit' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->shrinkToFit = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get indent.
+ *
+ * @return int
+ */
+ public function getIndent()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getIndent();
+ }
+
+ return $this->indent;
+ }
+
+ /**
+ * Set indent.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setIndent($pValue)
+ {
+ if ($pValue > 0) {
+ if (
+ $this->getHorizontal() != self::HORIZONTAL_GENERAL &&
+ $this->getHorizontal() != self::HORIZONTAL_LEFT &&
+ $this->getHorizontal() != self::HORIZONTAL_RIGHT &&
+ $this->getHorizontal() != self::HORIZONTAL_DISTRIBUTED
+ ) {
+ $pValue = 0; // indent not supported
+ }
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['indent' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->indent = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get read order.
+ *
+ * @return int
+ */
+ public function getReadOrder()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getReadOrder();
+ }
+
+ return $this->readOrder;
+ }
+
+ /**
+ * Set read order.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setReadOrder($pValue)
+ {
+ if ($pValue < 0 || $pValue > 2) {
+ $pValue = 0;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['readOrder' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->readOrder = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHashCode();
+ }
+
+ return md5(
+ $this->horizontal .
+ $this->vertical .
+ $this->textRotation .
+ ($this->wrapText ? 't' : 'f') .
+ ($this->shrinkToFit ? 't' : 'f') .
+ $this->indent .
+ $this->readOrder .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'horizontal', $this->getHorizontal());
+ $this->exportArray2($exportedArray, 'indent', $this->getIndent());
+ $this->exportArray2($exportedArray, 'readOrder', $this->getReadOrder());
+ $this->exportArray2($exportedArray, 'shrinkToFit', $this->getShrinkToFit());
+ $this->exportArray2($exportedArray, 'textRotation', $this->getTextRotation());
+ $this->exportArray2($exportedArray, 'vertical', $this->getVertical());
+ $this->exportArray2($exportedArray, 'wrapText', $this->getWrapText());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Border.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Border.php
new file mode 100644
index 0000000..a8af9d9
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Border.php
@@ -0,0 +1,233 @@
+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 Border
+ */
+ public function getSharedComponent()
+ {
+ /** @var Borders $sharedComponent */
+ $sharedComponent = $this->parent->getSharedComponent();
+ switch ($this->parentPropertyName) {
+ case 'bottom':
+ return $sharedComponent->getBottom();
+ case 'diagonal':
+ return $sharedComponent->getDiagonal();
+ case 'left':
+ return $sharedComponent->getLeft();
+ case 'right':
+ return $sharedComponent->getRight();
+ case 'top':
+ return $sharedComponent->getTop();
+ }
+
+ throw new PhpSpreadsheetException('Cannot get shared component for a pseudo-border.');
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return $this->parent->getStyleArray([$this->parentPropertyName => $array]);
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->getTop()->applyFromArray(
+ * [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['borderStyle'])) {
+ $this->setBorderStyle($pStyles['borderStyle']);
+ }
+ if (isset($pStyles['color'])) {
+ $this->getColor()->applyFromArray($pStyles['color']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Border style.
+ *
+ * @return string
+ */
+ public function getBorderStyle()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getBorderStyle();
+ }
+
+ return $this->borderStyle;
+ }
+
+ /**
+ * Set Border style.
+ *
+ * @param bool|string $pValue
+ * When passing a boolean, FALSE equates Border::BORDER_NONE
+ * and TRUE to Border::BORDER_MEDIUM
+ *
+ * @return $this
+ */
+ public function setBorderStyle($pValue)
+ {
+ if (empty($pValue)) {
+ $pValue = self::BORDER_NONE;
+ } elseif (is_bool($pValue) && $pValue) {
+ $pValue = self::BORDER_MEDIUM;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['borderStyle' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->borderStyle = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Border Color.
+ *
+ * @return Color
+ */
+ public function getColor()
+ {
+ return $this->color;
+ }
+
+ /**
+ * Set Border Color.
+ *
+ * @return $this
+ */
+ public function setColor(Color $pValue)
+ {
+ // make sure parameter is a real color and not a supervisor
+ $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue;
+
+ 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->borderStyle .
+ $this->color->getHashCode() .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'borderStyle', $this->getBorderStyle());
+ $this->exportArray2($exportedArray, 'color', $this->getColor());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Borders.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Borders.php
new file mode 100644
index 0000000..eeb4932
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Borders.php
@@ -0,0 +1,421 @@
+left = new Border($isSupervisor);
+ $this->right = new Border($isSupervisor);
+ $this->top = new Border($isSupervisor);
+ $this->bottom = new Border($isSupervisor);
+ $this->diagonal = new Border($isSupervisor);
+ $this->diagonalDirection = self::DIAGONAL_NONE;
+
+ // Specially for supervisor
+ if ($isSupervisor) {
+ // Initialize pseudo-borders
+ $this->allBorders = new Border(true);
+ $this->outline = new Border(true);
+ $this->inside = new Border(true);
+ $this->vertical = new Border(true);
+ $this->horizontal = new Border(true);
+
+ // bind parent if we are a supervisor
+ $this->left->bindParent($this, 'left');
+ $this->right->bindParent($this, 'right');
+ $this->top->bindParent($this, 'top');
+ $this->bottom->bindParent($this, 'bottom');
+ $this->diagonal->bindParent($this, 'diagonal');
+ $this->allBorders->bindParent($this, 'allBorders');
+ $this->outline->bindParent($this, 'outline');
+ $this->inside->bindParent($this, 'inside');
+ $this->vertical->bindParent($this, 'vertical');
+ $this->horizontal->bindParent($this, 'horizontal');
+ }
+ }
+
+ /**
+ * Get the shared style component for the currently active cell in currently active sheet.
+ * Only used for style supervisor.
+ *
+ * @return Borders
+ */
+ public function getSharedComponent()
+ {
+ return $this->parent->getSharedComponent()->getBorders();
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return ['borders' => $array];
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray(
+ * [
+ * 'bottom' => [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ],
+ * 'top' => [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ]
+ * ]
+ * );
+ *
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray(
+ * [
+ * 'allBorders' => [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ]
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['left'])) {
+ $this->getLeft()->applyFromArray($pStyles['left']);
+ }
+ if (isset($pStyles['right'])) {
+ $this->getRight()->applyFromArray($pStyles['right']);
+ }
+ if (isset($pStyles['top'])) {
+ $this->getTop()->applyFromArray($pStyles['top']);
+ }
+ if (isset($pStyles['bottom'])) {
+ $this->getBottom()->applyFromArray($pStyles['bottom']);
+ }
+ if (isset($pStyles['diagonal'])) {
+ $this->getDiagonal()->applyFromArray($pStyles['diagonal']);
+ }
+ if (isset($pStyles['diagonalDirection'])) {
+ $this->setDiagonalDirection($pStyles['diagonalDirection']);
+ }
+ if (isset($pStyles['allBorders'])) {
+ $this->getLeft()->applyFromArray($pStyles['allBorders']);
+ $this->getRight()->applyFromArray($pStyles['allBorders']);
+ $this->getTop()->applyFromArray($pStyles['allBorders']);
+ $this->getBottom()->applyFromArray($pStyles['allBorders']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Left.
+ *
+ * @return Border
+ */
+ public function getLeft()
+ {
+ return $this->left;
+ }
+
+ /**
+ * Get Right.
+ *
+ * @return Border
+ */
+ public function getRight()
+ {
+ return $this->right;
+ }
+
+ /**
+ * Get Top.
+ *
+ * @return Border
+ */
+ public function getTop()
+ {
+ return $this->top;
+ }
+
+ /**
+ * Get Bottom.
+ *
+ * @return Border
+ */
+ public function getBottom()
+ {
+ return $this->bottom;
+ }
+
+ /**
+ * Get Diagonal.
+ *
+ * @return Border
+ */
+ public function getDiagonal()
+ {
+ return $this->diagonal;
+ }
+
+ /**
+ * Get AllBorders (pseudo-border). Only applies to supervisor.
+ *
+ * @return Border
+ */
+ public function getAllBorders()
+ {
+ if (!$this->isSupervisor) {
+ throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.');
+ }
+
+ return $this->allBorders;
+ }
+
+ /**
+ * Get Outline (pseudo-border). Only applies to supervisor.
+ *
+ * @return Border
+ */
+ public function getOutline()
+ {
+ if (!$this->isSupervisor) {
+ throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.');
+ }
+
+ return $this->outline;
+ }
+
+ /**
+ * Get Inside (pseudo-border). Only applies to supervisor.
+ *
+ * @return Border
+ */
+ public function getInside()
+ {
+ if (!$this->isSupervisor) {
+ throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.');
+ }
+
+ return $this->inside;
+ }
+
+ /**
+ * Get Vertical (pseudo-border). Only applies to supervisor.
+ *
+ * @return Border
+ */
+ public function getVertical()
+ {
+ if (!$this->isSupervisor) {
+ throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.');
+ }
+
+ return $this->vertical;
+ }
+
+ /**
+ * Get Horizontal (pseudo-border). Only applies to supervisor.
+ *
+ * @return Border
+ */
+ public function getHorizontal()
+ {
+ if (!$this->isSupervisor) {
+ throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.');
+ }
+
+ return $this->horizontal;
+ }
+
+ /**
+ * Get DiagonalDirection.
+ *
+ * @return int
+ */
+ public function getDiagonalDirection()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getDiagonalDirection();
+ }
+
+ return $this->diagonalDirection;
+ }
+
+ /**
+ * Set DiagonalDirection.
+ *
+ * @param int $pValue see self::DIAGONAL_*
+ *
+ * @return $this
+ */
+ public function setDiagonalDirection($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = self::DIAGONAL_NONE;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['diagonalDirection' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->diagonalDirection = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHashcode();
+ }
+
+ return md5(
+ $this->getLeft()->getHashCode() .
+ $this->getRight()->getHashCode() .
+ $this->getTop()->getHashCode() .
+ $this->getBottom()->getHashCode() .
+ $this->getDiagonal()->getHashCode() .
+ $this->getDiagonalDirection() .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'bottom', $this->getBottom());
+ $this->exportArray2($exportedArray, 'diagonal', $this->getDiagonal());
+ $this->exportArray2($exportedArray, 'diagonalDirection', $this->getDiagonalDirection());
+ $this->exportArray2($exportedArray, 'left', $this->getLeft());
+ $this->exportArray2($exportedArray, 'right', $this->getRight());
+ $this->exportArray2($exportedArray, 'top', $this->getTop());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php
new file mode 100644
index 0000000..bf5d093
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php
@@ -0,0 +1,405 @@
+argb = $pARGB;
+ }
+ }
+
+ /**
+ * Get the shared style component for the currently active cell in currently active sheet.
+ * Only used for style supervisor.
+ *
+ * @return Color
+ */
+ public function getSharedComponent()
+ {
+ /** @var Border|Fill $sharedComponent */
+ $sharedComponent = $this->parent->getSharedComponent();
+ if ($this->parentPropertyName === 'endColor') {
+ return $sharedComponent->getEndColor();
+ }
+ if ($this->parentPropertyName === 'startColor') {
+ return $sharedComponent->getStartColor();
+ }
+
+ return $sharedComponent->getColor();
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return $this->parent->getStyleArray([$this->parentPropertyName => $array]);
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->getColor()->applyFromArray(['rgb' => '808080']);
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['rgb'])) {
+ $this->setRGB($pStyles['rgb']);
+ }
+ if (isset($pStyles['argb'])) {
+ $this->setARGB($pStyles['argb']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get ARGB.
+ *
+ * @return string
+ */
+ public function getARGB()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getARGB();
+ }
+
+ return $this->argb;
+ }
+
+ /**
+ * Set ARGB.
+ *
+ * @param string $pValue see self::COLOR_*
+ *
+ * @return $this
+ */
+ public function setARGB($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = self::COLOR_BLACK;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['argb' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->argb = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get RGB.
+ *
+ * @return string
+ */
+ public function getRGB()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getRGB();
+ }
+
+ return substr($this->argb ?? '', 2);
+ }
+
+ /**
+ * Set RGB.
+ *
+ * @param string $pValue RGB value
+ *
+ * @return $this
+ */
+ public function setRGB($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = '000000';
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['argb' => 'FF' . $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->argb = 'FF' . $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get a specified colour component of an RGB value.
+ *
+ * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE
+ * @param int $offset Position within the RGB value to extract
+ * @param bool $hex Flag indicating whether the component should be returned as a hex or a
+ * decimal value
+ *
+ * @return int|string The extracted colour component
+ */
+ private static function getColourComponent($RGB, $offset, $hex = true)
+ {
+ $colour = substr($RGB, $offset, 2);
+
+ return ($hex) ? $colour : hexdec($colour);
+ }
+
+ /**
+ * Get the red colour component of an RGB value.
+ *
+ * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE
+ * @param bool $hex Flag indicating whether the component should be returned as a hex or a
+ * decimal value
+ *
+ * @return int|string The red colour component
+ */
+ public static function getRed($RGB, $hex = true)
+ {
+ return self::getColourComponent($RGB, strlen($RGB) - 6, $hex);
+ }
+
+ /**
+ * Get the green colour component of an RGB value.
+ *
+ * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE
+ * @param bool $hex Flag indicating whether the component should be returned as a hex or a
+ * decimal value
+ *
+ * @return int|string The green colour component
+ */
+ public static function getGreen($RGB, $hex = true)
+ {
+ return self::getColourComponent($RGB, strlen($RGB) - 4, $hex);
+ }
+
+ /**
+ * Get the blue colour component of an RGB value.
+ *
+ * @param string $RGB The colour as an RGB value (e.g. FF00CCCC or CCDDEE
+ * @param bool $hex Flag indicating whether the component should be returned as a hex or a
+ * decimal value
+ *
+ * @return int|string The blue colour component
+ */
+ public static function getBlue($RGB, $hex = true)
+ {
+ return self::getColourComponent($RGB, strlen($RGB) - 2, $hex);
+ }
+
+ /**
+ * Adjust the brightness of a color.
+ *
+ * @param string $hex The colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE)
+ * @param float $adjustPercentage The percentage by which to adjust the colour as a float from -1 to 1
+ *
+ * @return string The adjusted colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE)
+ */
+ public static function changeBrightness($hex, $adjustPercentage)
+ {
+ $rgba = (strlen($hex) === 8);
+ $adjustPercentage = max(-1.0, min(1.0, $adjustPercentage));
+
+ /** @var int $red */
+ $red = self::getRed($hex, false);
+ /** @var int $green */
+ $green = self::getGreen($hex, false);
+ /** @var int $blue */
+ $blue = self::getBlue($hex, false);
+ if ($adjustPercentage > 0) {
+ $red += (255 - $red) * $adjustPercentage;
+ $green += (255 - $green) * $adjustPercentage;
+ $blue += (255 - $blue) * $adjustPercentage;
+ } else {
+ $red += $red * $adjustPercentage;
+ $green += $green * $adjustPercentage;
+ $blue += $blue * $adjustPercentage;
+ }
+
+ $rgb = strtoupper(
+ str_pad(dechex((int) $red), 2, '0', 0) .
+ str_pad(dechex((int) $green), 2, '0', 0) .
+ str_pad(dechex((int) $blue), 2, '0', 0)
+ );
+
+ return (($rgba) ? 'FF' : '') . $rgb;
+ }
+
+ /**
+ * Get indexed color.
+ *
+ * @param int $pIndex Index entry point into the colour array
+ * @param bool $background Flag to indicate whether default background or foreground colour
+ * should be returned if the indexed colour doesn't exist
+ *
+ * @return self
+ */
+ public static function indexedColor($pIndex, $background = false)
+ {
+ // Clean parameter
+ $pIndex = (int) $pIndex;
+
+ // Indexed colors
+ if (self::$indexedColors === null) {
+ self::$indexedColors = [
+ 1 => 'FF000000', // System Colour #1 - Black
+ 2 => 'FFFFFFFF', // System Colour #2 - White
+ 3 => 'FFFF0000', // System Colour #3 - Red
+ 4 => 'FF00FF00', // System Colour #4 - Green
+ 5 => 'FF0000FF', // System Colour #5 - Blue
+ 6 => 'FFFFFF00', // System Colour #6 - Yellow
+ 7 => 'FFFF00FF', // System Colour #7- Magenta
+ 8 => 'FF00FFFF', // System Colour #8- Cyan
+ 9 => 'FF800000', // Standard Colour #9
+ 10 => 'FF008000', // Standard Colour #10
+ 11 => 'FF000080', // Standard Colour #11
+ 12 => 'FF808000', // Standard Colour #12
+ 13 => 'FF800080', // Standard Colour #13
+ 14 => 'FF008080', // Standard Colour #14
+ 15 => 'FFC0C0C0', // Standard Colour #15
+ 16 => 'FF808080', // Standard Colour #16
+ 17 => 'FF9999FF', // Chart Fill Colour #17
+ 18 => 'FF993366', // Chart Fill Colour #18
+ 19 => 'FFFFFFCC', // Chart Fill Colour #19
+ 20 => 'FFCCFFFF', // Chart Fill Colour #20
+ 21 => 'FF660066', // Chart Fill Colour #21
+ 22 => 'FFFF8080', // Chart Fill Colour #22
+ 23 => 'FF0066CC', // Chart Fill Colour #23
+ 24 => 'FFCCCCFF', // Chart Fill Colour #24
+ 25 => 'FF000080', // Chart Line Colour #25
+ 26 => 'FFFF00FF', // Chart Line Colour #26
+ 27 => 'FFFFFF00', // Chart Line Colour #27
+ 28 => 'FF00FFFF', // Chart Line Colour #28
+ 29 => 'FF800080', // Chart Line Colour #29
+ 30 => 'FF800000', // Chart Line Colour #30
+ 31 => 'FF008080', // Chart Line Colour #31
+ 32 => 'FF0000FF', // Chart Line Colour #32
+ 33 => 'FF00CCFF', // Standard Colour #33
+ 34 => 'FFCCFFFF', // Standard Colour #34
+ 35 => 'FFCCFFCC', // Standard Colour #35
+ 36 => 'FFFFFF99', // Standard Colour #36
+ 37 => 'FF99CCFF', // Standard Colour #37
+ 38 => 'FFFF99CC', // Standard Colour #38
+ 39 => 'FFCC99FF', // Standard Colour #39
+ 40 => 'FFFFCC99', // Standard Colour #40
+ 41 => 'FF3366FF', // Standard Colour #41
+ 42 => 'FF33CCCC', // Standard Colour #42
+ 43 => 'FF99CC00', // Standard Colour #43
+ 44 => 'FFFFCC00', // Standard Colour #44
+ 45 => 'FFFF9900', // Standard Colour #45
+ 46 => 'FFFF6600', // Standard Colour #46
+ 47 => 'FF666699', // Standard Colour #47
+ 48 => 'FF969696', // Standard Colour #48
+ 49 => 'FF003366', // Standard Colour #49
+ 50 => 'FF339966', // Standard Colour #50
+ 51 => 'FF003300', // Standard Colour #51
+ 52 => 'FF333300', // Standard Colour #52
+ 53 => 'FF993300', // Standard Colour #53
+ 54 => 'FF993366', // Standard Colour #54
+ 55 => 'FF333399', // Standard Colour #55
+ 56 => 'FF333333', // Standard Colour #56
+ ];
+ }
+
+ if (isset(self::$indexedColors[$pIndex])) {
+ return new self(self::$indexedColors[$pIndex]);
+ }
+
+ if ($background) {
+ return new self(self::COLOR_WHITE);
+ }
+
+ return new self(self::COLOR_BLACK);
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHashCode();
+ }
+
+ return md5(
+ $this->argb .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'argb', $this->getARGB());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php
new file mode 100644
index 0000000..4cbc274
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php
@@ -0,0 +1,323 @@
+style = new Style(false, true);
+ }
+
+ /**
+ * Get Condition type.
+ *
+ * @return string
+ */
+ public function getConditionType()
+ {
+ return $this->conditionType;
+ }
+
+ /**
+ * Set Condition type.
+ *
+ * @param string $pValue Condition type, see self::CONDITION_*
+ *
+ * @return $this
+ */
+ public function setConditionType($pValue)
+ {
+ $this->conditionType = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Operator type.
+ *
+ * @return string
+ */
+ public function getOperatorType()
+ {
+ return $this->operatorType;
+ }
+
+ /**
+ * Set Operator type.
+ *
+ * @param string $pValue Conditional operator type, see self::OPERATOR_*
+ *
+ * @return $this
+ */
+ public function setOperatorType($pValue)
+ {
+ $this->operatorType = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get text.
+ *
+ * @return string
+ */
+ public function getText()
+ {
+ return $this->text;
+ }
+
+ /**
+ * Set text.
+ *
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setText($value)
+ {
+ $this->text = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get StopIfTrue.
+ *
+ * @return bool
+ */
+ public function getStopIfTrue()
+ {
+ return $this->stopIfTrue;
+ }
+
+ /**
+ * Set StopIfTrue.
+ *
+ * @param bool $value
+ *
+ * @return $this
+ */
+ public function setStopIfTrue($value)
+ {
+ $this->stopIfTrue = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get Conditions.
+ *
+ * @return string[]
+ */
+ public function getConditions()
+ {
+ return $this->condition;
+ }
+
+ /**
+ * Set Conditions.
+ *
+ * @param bool|float|int|string|string[] $pValue Condition
+ *
+ * @return $this
+ */
+ public function setConditions($pValue)
+ {
+ if (!is_array($pValue)) {
+ $pValue = [$pValue];
+ }
+ $this->condition = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Add Condition.
+ *
+ * @param string $pValue Condition
+ *
+ * @return $this
+ */
+ public function addCondition($pValue)
+ {
+ $this->condition[] = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Style.
+ *
+ * @return Style
+ */
+ public function getStyle()
+ {
+ return $this->style;
+ }
+
+ /**
+ * Set Style.
+ *
+ * @param Style $pValue
+ *
+ * @return $this
+ */
+ public function setStyle(?Style $pValue = null)
+ {
+ $this->style = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * get DataBar.
+ *
+ * @return ConditionalDataBar | null
+ */
+ public function getDataBar()
+ {
+ return $this->dataBar;
+ }
+
+ /**
+ * set DataBar.
+ *
+ * @return $this
+ */
+ public function setDataBar(ConditionalDataBar $dataBar)
+ {
+ $this->dataBar = $dataBar;
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ return md5(
+ $this->conditionType .
+ $this->operatorType .
+ implode(';', $this->condition) .
+ $this->style->getHashCode() .
+ __CLASS__
+ );
+ }
+
+ /**
+ * 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;
+ }
+ }
+ }
+
+ /**
+ * Verify if param is valid condition type.
+ */
+ public static function isValidConditionType(string $type): bool
+ {
+ return in_array($type, self::CONDITION_TYPES);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php
new file mode 100644
index 0000000..5451367
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php
@@ -0,0 +1,102 @@
+ attribute */
+
+ /** @var null|bool */
+ private $showValue;
+
+ /** children */
+
+ /** @var ConditionalFormatValueObject */
+ private $minimumConditionalFormatValueObject;
+
+ /** @var ConditionalFormatValueObject */
+ private $maximumConditionalFormatValueObject;
+
+ /** @var string */
+ private $color;
+
+ /** */
+
+ /** @var ConditionalFormattingRuleExtension */
+ private $conditionalFormattingRuleExt;
+
+ /**
+ * @return null|bool
+ */
+ public function getShowValue()
+ {
+ return $this->showValue;
+ }
+
+ /**
+ * @param bool $showValue
+ */
+ public function setShowValue($showValue)
+ {
+ $this->showValue = $showValue;
+
+ return $this;
+ }
+
+ /**
+ * @return ConditionalFormatValueObject
+ */
+ public function getMinimumConditionalFormatValueObject()
+ {
+ return $this->minimumConditionalFormatValueObject;
+ }
+
+ public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject)
+ {
+ $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject;
+
+ return $this;
+ }
+
+ /**
+ * @return ConditionalFormatValueObject
+ */
+ public function getMaximumConditionalFormatValueObject()
+ {
+ return $this->maximumConditionalFormatValueObject;
+ }
+
+ public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject)
+ {
+ $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject;
+
+ return $this;
+ }
+
+ public function getColor(): string
+ {
+ return $this->color;
+ }
+
+ public function setColor(string $color): self
+ {
+ $this->color = $color;
+
+ return $this;
+ }
+
+ /**
+ * @return ConditionalFormattingRuleExtension
+ */
+ public function getConditionalFormattingRuleExt()
+ {
+ return $this->conditionalFormattingRuleExt;
+ }
+
+ public function setConditionalFormattingRuleExt(ConditionalFormattingRuleExtension $conditionalFormattingRuleExt)
+ {
+ $this->conditionalFormattingRuleExt = $conditionalFormattingRuleExt;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php
new file mode 100644
index 0000000..c709cf3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php
@@ -0,0 +1,290 @@
+ attributes */
+
+ /** @var int */
+ private $minLength;
+
+ /** @var int */
+ private $maxLength;
+
+ /** @var null|bool */
+ private $border;
+
+ /** @var null|bool */
+ private $gradient;
+
+ /** @var string */
+ private $direction;
+
+ /** @var null|bool */
+ private $negativeBarBorderColorSameAsPositive;
+
+ /** @var string */
+ private $axisPosition;
+
+ // children
+
+ /** @var ConditionalFormatValueObject */
+ private $maximumConditionalFormatValueObject;
+
+ /** @var ConditionalFormatValueObject */
+ private $minimumConditionalFormatValueObject;
+
+ /** @var string */
+ private $borderColor;
+
+ /** @var string */
+ private $negativeFillColor;
+
+ /** @var string */
+ private $negativeBorderColor;
+
+ /** @var array */
+ private $axisColor = [
+ 'rgb' => null,
+ 'theme' => null,
+ 'tint' => null,
+ ];
+
+ public function getXmlAttributes()
+ {
+ $ret = [];
+ foreach (['minLength', 'maxLength', 'direction', 'axisPosition'] as $attrKey) {
+ if (null !== $this->{$attrKey}) {
+ $ret[$attrKey] = $this->{$attrKey};
+ }
+ }
+ foreach (['border', 'gradient', 'negativeBarBorderColorSameAsPositive'] as $attrKey) {
+ if (null !== $this->{$attrKey}) {
+ $ret[$attrKey] = $this->{$attrKey} ? '1' : '0';
+ }
+ }
+
+ return $ret;
+ }
+
+ public function getXmlElements()
+ {
+ $ret = [];
+ $elms = ['borderColor', 'negativeFillColor', 'negativeBorderColor'];
+ foreach ($elms as $elmKey) {
+ if (null !== $this->{$elmKey}) {
+ $ret[$elmKey] = ['rgb' => $this->{$elmKey}];
+ }
+ }
+ foreach (array_filter($this->axisColor) as $attrKey => $axisColorAttr) {
+ if (!isset($ret['axisColor'])) {
+ $ret['axisColor'] = [];
+ }
+ $ret['axisColor'][$attrKey] = $axisColorAttr;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @return int
+ */
+ public function getMinLength()
+ {
+ return $this->minLength;
+ }
+
+ public function setMinLength(int $minLength): self
+ {
+ $this->minLength = $minLength;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getMaxLength()
+ {
+ return $this->maxLength;
+ }
+
+ public function setMaxLength(int $maxLength): self
+ {
+ $this->maxLength = $maxLength;
+
+ return $this;
+ }
+
+ /**
+ * @return null|bool
+ */
+ public function getBorder()
+ {
+ return $this->border;
+ }
+
+ public function setBorder(bool $border): self
+ {
+ $this->border = $border;
+
+ return $this;
+ }
+
+ /**
+ * @return null|bool
+ */
+ public function getGradient()
+ {
+ return $this->gradient;
+ }
+
+ public function setGradient(bool $gradient): self
+ {
+ $this->gradient = $gradient;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDirection()
+ {
+ return $this->direction;
+ }
+
+ public function setDirection(string $direction): self
+ {
+ $this->direction = $direction;
+
+ return $this;
+ }
+
+ /**
+ * @return null|bool
+ */
+ public function getNegativeBarBorderColorSameAsPositive()
+ {
+ return $this->negativeBarBorderColorSameAsPositive;
+ }
+
+ public function setNegativeBarBorderColorSameAsPositive(bool $negativeBarBorderColorSameAsPositive): self
+ {
+ $this->negativeBarBorderColorSameAsPositive = $negativeBarBorderColorSameAsPositive;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAxisPosition()
+ {
+ return $this->axisPosition;
+ }
+
+ public function setAxisPosition(string $axisPosition): self
+ {
+ $this->axisPosition = $axisPosition;
+
+ return $this;
+ }
+
+ /**
+ * @return ConditionalFormatValueObject
+ */
+ public function getMaximumConditionalFormatValueObject()
+ {
+ return $this->maximumConditionalFormatValueObject;
+ }
+
+ public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject)
+ {
+ $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject;
+
+ return $this;
+ }
+
+ /**
+ * @return ConditionalFormatValueObject
+ */
+ public function getMinimumConditionalFormatValueObject()
+ {
+ return $this->minimumConditionalFormatValueObject;
+ }
+
+ public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject)
+ {
+ $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBorderColor()
+ {
+ return $this->borderColor;
+ }
+
+ public function setBorderColor(string $borderColor): self
+ {
+ $this->borderColor = $borderColor;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNegativeFillColor()
+ {
+ return $this->negativeFillColor;
+ }
+
+ public function setNegativeFillColor(string $negativeFillColor): self
+ {
+ $this->negativeFillColor = $negativeFillColor;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNegativeBorderColor()
+ {
+ return $this->negativeBorderColor;
+ }
+
+ public function setNegativeBorderColor(string $negativeBorderColor): self
+ {
+ $this->negativeBorderColor = $negativeBorderColor;
+
+ return $this;
+ }
+
+ public function getAxisColor(): array
+ {
+ return $this->axisColor;
+ }
+
+ /**
+ * @param mixed $rgb
+ * @param null|mixed $theme
+ * @param null|mixed $tint
+ */
+ public function setAxisColor($rgb, $theme = null, $tint = null): self
+ {
+ $this->axisColor = [
+ 'rgb' => $rgb,
+ 'theme' => $theme,
+ 'tint' => $tint,
+ ];
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php
new file mode 100644
index 0000000..107969b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php
@@ -0,0 +1,78 @@
+type = $type;
+ $this->value = $value;
+ $this->cellFormula = $cellFormula;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param mixed $type
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @param mixed $value
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getCellFormula()
+ {
+ return $this->cellFormula;
+ }
+
+ /**
+ * @param mixed $cellFormula
+ */
+ public function setCellFormula($cellFormula)
+ {
+ $this->cellFormula = $cellFormula;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php
new file mode 100644
index 0000000..0797c2e
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php
@@ -0,0 +1,202 @@
+ attributes */
+ private $id;
+
+ /** @var string Conditional Formatting Rule */
+ private $cfRule;
+
+ /** children */
+
+ /** @var ConditionalDataBarExtension */
+ private $dataBar;
+
+ /** @var string Sequence of References */
+ private $sqref;
+
+ /**
+ * ConditionalFormattingRuleExtension constructor.
+ */
+ public function __construct($id = null, string $cfRule = self::CONDITION_EXTENSION_DATABAR)
+ {
+ if (null === $id) {
+ $this->id = '{' . $this->generateUuid() . '}';
+ } else {
+ $this->id = $id;
+ }
+ $this->cfRule = $cfRule;
+ }
+
+ private function generateUuid()
+ {
+ $chars = str_split('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx');
+
+ foreach ($chars as $i => $char) {
+ if ($char === 'x') {
+ $chars[$i] = dechex(random_int(0, 15));
+ } elseif ($char === 'y') {
+ $chars[$i] = dechex(random_int(8, 11));
+ }
+ }
+
+ return implode('', $chars);
+ }
+
+ public static function parseExtLstXml($extLstXml)
+ {
+ $conditionalFormattingRuleExtensions = [];
+ $conditionalFormattingRuleExtensionXml = null;
+ if ($extLstXml instanceof SimpleXMLElement) {
+ foreach ((count($extLstXml) > 0 ? $extLstXml : [$extLstXml]) as $extLst) {
+ //this uri is conditionalFormattings
+ //https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
+ if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') {
+ $conditionalFormattingRuleExtensionXml = $extLst->ext;
+ }
+ }
+
+ if ($conditionalFormattingRuleExtensionXml) {
+ $ns = $conditionalFormattingRuleExtensionXml->getNamespaces(true);
+ $extFormattingsXml = $conditionalFormattingRuleExtensionXml->children($ns['x14']);
+
+ foreach ($extFormattingsXml->children($ns['x14']) as $extFormattingXml) {
+ $extCfRuleXml = $extFormattingXml->cfRule;
+ if (((string) $extCfRuleXml->attributes()->type) !== Conditional::CONDITION_DATABAR) {
+ continue;
+ }
+
+ $extFormattingRuleObj = new self((string) $extCfRuleXml->attributes()->id);
+ $extFormattingRuleObj->setSqref((string) $extFormattingXml->children($ns['xm'])->sqref);
+ $conditionalFormattingRuleExtensions[$extFormattingRuleObj->getId()] = $extFormattingRuleObj;
+
+ $extDataBarObj = new ConditionalDataBarExtension();
+ $extFormattingRuleObj->setDataBarExt($extDataBarObj);
+ $dataBarXml = $extCfRuleXml->dataBar;
+ self::parseExtDataBarAttributesFromXml($extDataBarObj, $dataBarXml);
+ self::parseExtDataBarElementChildrenFromXml($extDataBarObj, $dataBarXml, $ns);
+ }
+ }
+ }
+
+ return $conditionalFormattingRuleExtensions;
+ }
+
+ private static function parseExtDataBarAttributesFromXml(
+ ConditionalDataBarExtension $extDataBarObj,
+ SimpleXMLElement $dataBarXml
+ ): void {
+ $dataBarAttribute = $dataBarXml->attributes();
+ if ($dataBarAttribute->minLength) {
+ $extDataBarObj->setMinLength((int) $dataBarAttribute->minLength);
+ }
+ if ($dataBarAttribute->maxLength) {
+ $extDataBarObj->setMaxLength((int) $dataBarAttribute->maxLength);
+ }
+ if ($dataBarAttribute->border) {
+ $extDataBarObj->setBorder((bool) (string) $dataBarAttribute->border);
+ }
+ if ($dataBarAttribute->gradient) {
+ $extDataBarObj->setGradient((bool) (string) $dataBarAttribute->gradient);
+ }
+ if ($dataBarAttribute->direction) {
+ $extDataBarObj->setDirection((string) $dataBarAttribute->direction);
+ }
+ if ($dataBarAttribute->negativeBarBorderColorSameAsPositive) {
+ $extDataBarObj->setNegativeBarBorderColorSameAsPositive((bool) (string) $dataBarAttribute->negativeBarBorderColorSameAsPositive);
+ }
+ if ($dataBarAttribute->axisPosition) {
+ $extDataBarObj->setAxisPosition((string) $dataBarAttribute->axisPosition);
+ }
+ }
+
+ private static function parseExtDataBarElementChildrenFromXml(ConditionalDataBarExtension $extDataBarObj, SimpleXMLElement $dataBarXml, $ns): void
+ {
+ if ($dataBarXml->borderColor) {
+ $extDataBarObj->setBorderColor((string) $dataBarXml->borderColor->attributes()['rgb']);
+ }
+ if ($dataBarXml->negativeFillColor) {
+ $extDataBarObj->setNegativeFillColor((string) $dataBarXml->negativeFillColor->attributes()['rgb']);
+ }
+ if ($dataBarXml->negativeBorderColor) {
+ $extDataBarObj->setNegativeBorderColor((string) $dataBarXml->negativeBorderColor->attributes()['rgb']);
+ }
+ if ($dataBarXml->axisColor) {
+ $axisColorAttr = $dataBarXml->axisColor->attributes();
+ $extDataBarObj->setAxisColor((string) $axisColorAttr['rgb'], (string) $axisColorAttr['theme'], (string) $axisColorAttr['tint']);
+ }
+ $cfvoIndex = 0;
+ foreach ($dataBarXml->cfvo as $cfvo) {
+ $f = (string) $cfvo->children($ns['xm'])->f;
+ if ($cfvoIndex === 0) {
+ $extDataBarObj->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo->attributes()['type'], null, (empty($f) ? null : $f)));
+ }
+ if ($cfvoIndex === 1) {
+ $extDataBarObj->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo->attributes()['type'], null, (empty($f) ? null : $f)));
+ }
+ ++$cfvoIndex;
+ }
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param mixed $id
+ */
+ public function setId($id): self
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function getCfRule(): string
+ {
+ return $this->cfRule;
+ }
+
+ public function setCfRule(string $cfRule): self
+ {
+ $this->cfRule = $cfRule;
+
+ return $this;
+ }
+
+ public function getDataBarExt(): ConditionalDataBarExtension
+ {
+ return $this->dataBar;
+ }
+
+ public function setDataBarExt(ConditionalDataBarExtension $dataBar): self
+ {
+ $this->dataBar = $dataBar;
+
+ return $this;
+ }
+
+ public function getSqref(): string
+ {
+ return $this->sqref;
+ }
+
+ public function setSqref(string $sqref): self
+ {
+ $this->sqref = $sqref;
+
+ 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..e793f1e
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php
@@ -0,0 +1,325 @@
+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()
+ {
+ return $this->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,
+ * 'startColor' => [
+ * 'rgb' => '000000'
+ * ],
+ * 'endColor' => [
+ * 'argb' => 'FFFFFFFF'
+ * ]
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['fillType'])) {
+ $this->setFillType($pStyles['fillType']);
+ }
+ if (isset($pStyles['rotation'])) {
+ $this->setRotation($pStyles['rotation']);
+ }
+ if (isset($pStyles['startColor'])) {
+ $this->getStartColor()->applyFromArray($pStyles['startColor']);
+ }
+ if (isset($pStyles['endColor'])) {
+ $this->getEndColor()->applyFromArray($pStyles['endColor']);
+ }
+ if (isset($pStyles['color'])) {
+ $this->getStartColor()->applyFromArray($pStyles['color']);
+ $this->getEndColor()->applyFromArray($pStyles['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 $pValue Fill type, see self::FILL_*
+ *
+ * @return $this
+ */
+ public function setFillType($pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['fillType' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->fillType = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Rotation.
+ *
+ * @return float
+ */
+ public function getRotation()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getRotation();
+ }
+
+ return $this->rotation;
+ }
+
+ /**
+ * Set Rotation.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setRotation($pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['rotation' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->rotation = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Start Color.
+ *
+ * @return Color
+ */
+ public function getStartColor()
+ {
+ return $this->startColor;
+ }
+
+ /**
+ * Set Start Color.
+ *
+ * @return $this
+ */
+ public function setStartColor(Color $pValue)
+ {
+ // make sure parameter is a real color and not a supervisor
+ $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue;
+
+ 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 $pValue)
+ {
+ // make sure parameter is a real color and not a supervisor
+ $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue;
+
+ if ($this->isSupervisor) {
+ $styleArray = $this->getEndColor()->getStyleArray(['argb' => $color->getARGB()]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->endColor = $color;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 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() : '') .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'endColor', $this->getEndColor());
+ $this->exportArray2($exportedArray, 'fillType', $this->getFillType());
+ $this->exportArray2($exportedArray, 'rotation', $this->getRotation());
+ $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..5f73c1d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Font.php
@@ -0,0 +1,565 @@
+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()
+ {
+ return $this->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 $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['name'])) {
+ $this->setName($pStyles['name']);
+ }
+ if (isset($pStyles['bold'])) {
+ $this->setBold($pStyles['bold']);
+ }
+ if (isset($pStyles['italic'])) {
+ $this->setItalic($pStyles['italic']);
+ }
+ if (isset($pStyles['superscript'])) {
+ $this->setSuperscript($pStyles['superscript']);
+ }
+ if (isset($pStyles['subscript'])) {
+ $this->setSubscript($pStyles['subscript']);
+ }
+ if (isset($pStyles['underline'])) {
+ $this->setUnderline($pStyles['underline']);
+ }
+ if (isset($pStyles['strikethrough'])) {
+ $this->setStrikethrough($pStyles['strikethrough']);
+ }
+ if (isset($pStyles['color'])) {
+ $this->getColor()->applyFromArray($pStyles['color']);
+ }
+ if (isset($pStyles['size'])) {
+ $this->setSize($pStyles['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 $pValue
+ *
+ * @return $this
+ */
+ public function setBold($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['bold' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->bold = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Italic.
+ *
+ * @return null|bool
+ */
+ public function getItalic()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getItalic();
+ }
+
+ return $this->italic;
+ }
+
+ /**
+ * Set Italic.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setItalic($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['italic' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->italic = $pValue;
+ }
+
+ 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 $pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['superscript' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->superscript = $pValue;
+ 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 $pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['subscript' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->subscript = $pValue;
+ 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 $pValue \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($pValue)
+ {
+ if (is_bool($pValue)) {
+ $pValue = ($pValue) ? self::UNDERLINE_SINGLE : self::UNDERLINE_NONE;
+ } elseif ($pValue == '') {
+ $pValue = self::UNDERLINE_NONE;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['underline' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->underline = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Strikethrough.
+ *
+ * @return null|bool
+ */
+ public function getStrikethrough()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getStrikethrough();
+ }
+
+ return $this->strikethrough;
+ }
+
+ /**
+ * Set Strikethrough.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setStrikethrough($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['strikethrough' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->strikethrough = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Color.
+ *
+ * @return Color
+ */
+ public function getColor()
+ {
+ return $this->color;
+ }
+
+ /**
+ * Set Color.
+ *
+ * @return $this
+ */
+ public function setColor(Color $pValue)
+ {
+ // make sure parameter is a real color and not a supervisor
+ $color = $pValue->getIsSupervisor() ? $pValue->getSharedComponent() : $pValue;
+
+ 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..6235d86
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php
@@ -0,0 +1,409 @@
+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()
+ {
+ return $this->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 $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['formatCode'])) {
+ $this->setFormatCode($pStyles['formatCode']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Format Code.
+ *
+ * @return null|string
+ */
+ public function getFormatCode()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getFormatCode();
+ }
+ if ($this->builtInFormatCode !== false) {
+ return self::builtInFormatCode($this->builtInFormatCode);
+ }
+
+ return $this->formatCode;
+ }
+
+ /**
+ * Set Format Code.
+ *
+ * @param string $pValue see self::FORMAT_*
+ *
+ * @return $this
+ */
+ public function setFormatCode($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = self::FORMAT_GENERAL;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->formatCode = $pValue;
+ $this->builtInFormatCode = self::builtInFormatCodeIndex($pValue);
+ }
+
+ 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 $pValue
+ *
+ * @return $this
+ */
+ public function setBuiltInFormatCode($pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($pValue)]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->builtInFormatCode = $pValue;
+ $this->formatCode = self::builtInFormatCode($pValue);
+ }
+
+ 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 $pIndex
+ *
+ * @return string
+ */
+ public static function builtInFormatCode($pIndex)
+ {
+ // Clean parameter
+ $pIndex = (int) $pIndex;
+
+ // Ensure built-in format codes are available
+ self::fillBuiltInFormatCodes();
+
+ // Lookup format code
+ if (isset(self::$builtInFormats[$pIndex])) {
+ return self::$builtInFormats[$pIndex];
+ }
+
+ return '';
+ }
+
+ /**
+ * Get built-in format code index.
+ *
+ * @param string $formatCode
+ *
+ * @return bool|int
+ */
+ public static function builtInFormatCodeIndex($formatCode)
+ {
+ // Ensure built-in format codes are available
+ self::fillBuiltInFormatCodes();
+
+ // Lookup format code
+ if (array_key_exists($formatCode, self::$flippedBuiltInFormats)) {
+ return self::$flippedBuiltInFormats[$formatCode];
+ }
+
+ 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/BaseFormatter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php
new file mode 100644
index 0000000..7988143
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php
@@ -0,0 +1,12 @@
+ '',
+ // 12-hour suffix
+ 'am/pm' => 'A',
+ // 4-digit year
+ 'e' => 'Y',
+ 'yyyy' => 'Y',
+ // 2-digit year
+ 'yy' => 'y',
+ // first letter of month - no php equivalent
+ 'mmmmm' => 'M',
+ // full month name
+ 'mmmm' => 'F',
+ // short month name
+ 'mmm' => 'M',
+ // mm is minutes if time, but can also be month w/leading zero
+ // so we try to identify times be the inclusion of a : separator in the mask
+ // It isn't perfect, but the best way I know how
+ ':mm' => ':i',
+ 'mm:' => 'i:',
+ // month leading zero
+ 'mm' => 'm',
+ // month no leading zero
+ 'm' => 'n',
+ // full day of week name
+ 'dddd' => 'l',
+ // short day of week name
+ 'ddd' => 'D',
+ // days leading zero
+ 'dd' => 'd',
+ // days no leading zero
+ 'd' => 'j',
+ // seconds
+ 'ss' => 's',
+ // fractional seconds - no php equivalent
+ '.s' => '',
+ ];
+
+ /**
+ * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
+ *
+ * @var array
+ */
+ private static $dateFormatReplacements24 = [
+ 'hh' => 'H',
+ 'h' => 'G',
+ ];
+
+ /**
+ * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
+ *
+ * @var array
+ */
+ private static $dateFormatReplacements12 = [
+ 'hh' => 'h',
+ 'h' => 'g',
+ ];
+
+ public static function format($value, string $format): string
+ {
+ // strip off first part containing e.g. [$-F800] or [$USD-409]
+ // general syntax: [$-]
+ // language info is in hexadecimal
+ // strip off chinese part like [DBNum1][$-804]
+ $format = preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format);
+
+ // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
+ // but we don't want to change any quoted strings
+ $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
+
+ // Only process the non-quoted blocks for date format characters
+ $blocks = explode('"', $format);
+ foreach ($blocks as $key => &$block) {
+ if ($key % 2 == 0) {
+ $block = strtr($block, self::$dateFormatReplacements);
+ if (!strpos($block, 'A')) {
+ // 24-hour time format
+ // when [h]:mm format, the [h] should replace to the hours of the value * 24
+ if (false !== strpos($block, '[h]')) {
+ $hours = (int) ($value * 24);
+ $block = str_replace('[h]', $hours, $block);
+
+ continue;
+ }
+ $block = strtr($block, self::$dateFormatReplacements24);
+ } else {
+ // 12-hour time format
+ $block = strtr($block, self::$dateFormatReplacements12);
+ }
+ }
+ }
+ $format = implode('"', $blocks);
+
+ // escape any quoted characters so that DateTime format() will render them correctly
+ $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
+
+ $dateObj = Date::excelToDateTimeObject($value);
+ // If the colon preceding minute had been quoted, as happens in
+ // Excel 2003 XML formats, m will not have been changed to i above.
+ // Change it now.
+ $format = \preg_replace('/\\\\:m/', ':i', $format);
+
+ return $dateObj->format($format);
+ }
+
+ private static function setLowercaseCallback($matches): string
+ {
+ return mb_strtolower($matches[0]);
+ }
+
+ private static function escapeQuotesCallback($matches): string
+ {
+ return '\\' . implode('\\', str_split($matches[1]));
+ }
+}
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..48d927f
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php
@@ -0,0 +1,45 @@
+ 0);
+
+ return [
+ implode('.', $masks),
+ implode('.', array_reverse($postDecimalMasks)),
+ ];
+ }
+
+ private static function processComplexNumberFormatMask($number, $mask): 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];
+
+ $blockValue = sprintf("%0{$size}d", fmod($number, $divisor));
+ $number = floor($number / $divisor);
+ $mask = substr_replace($mask, $blockValue, $offset, $size);
+ }
+ if ($number > 0) {
+ $mask = substr_replace($mask, $number, $offset, 0);
+ }
+ $result = $mask;
+ }
+
+ return $result;
+ }
+
+ private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true): string
+ {
+ $sign = ($number < 0.0) ? '-' : '';
+ $number = abs($number);
+
+ 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}";
+ }
+
+ private static function formatStraightNumericValue($value, $format, array $matches, $useThousands): string
+ {
+ $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(
+ $value,
+ strlen($right),
+ StringHelper::getDecimalSeparator(),
+ StringHelper::getThousandsSeparator()
+ );
+
+ return preg_replace(self::NUMBER_REGEX, $value, $format);
+ }
+
+ if (preg_match('/[0#]E[+-]0/i', $format)) {
+ // Scientific format
+ return sprintf('%5.2E', $value);
+ } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
+ if ($value == (int) $value && substr_count($format, '.') === 1) {
+ $value *= 10 ** strlen(explode('.', $format)[1]);
+ }
+
+ return self::complexNumberFormatMask($value, $format);
+ }
+
+ $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
+ $value = sprintf($sprintf_pattern, $value);
+
+ return preg_replace(self::NUMBER_REGEX, $value, $format);
+ }
+
+ public static function format($value, $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 = 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 = preg_match('/(#,#|0,0)/', $format);
+ if ($useThousands) {
+ $format = preg_replace('/0,0/', '00', $format);
+ $format = preg_replace('/#,#/', '##', $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 = preg_replace('/0,+/', '0', $format);
+ $format = preg_replace('/#,+/', '#', $format);
+ }
+ if (preg_match('/#?.*\?\/\?/', $format, $m)) {
+ if ($value != (int) $value) {
+ $value = FractionFormatter::format($value, $format);
+ }
+ } else {
+ // Handle the number itself
+
+ // scale number
+ $value = $value / $scale;
+ // Strip #
+ $format = preg_replace('/\\#/', '0', $format);
+ // Remove locale code [$-###]
+ $format = preg_replace('/\[\$\-.*\]/', '', $format);
+
+ $n = '/\\[[^\\]]+\\]/';
+ $m = preg_replace($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 = str_replace('?', '', $format ?? '');
+ }
+ }
+
+ if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
+ // Currency or Accounting
+ $currencyCode = $m[1];
+ [$currencyCode] = explode('-', $currencyCode);
+ if ($currencyCode == '') {
+ $currencyCode = StringHelper::getCurrencyCode();
+ }
+ $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
+ }
+
+ return $value;
+ }
+}
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..cf1731e
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php
@@ -0,0 +1,42 @@
+locked = self::PROTECTION_INHERIT;
+ $this->hidden = self::PROTECTION_INHERIT;
+ }
+ }
+
+ /**
+ * Get the shared style component for the currently active cell in currently active sheet.
+ * Only used for style supervisor.
+ *
+ * @return Protection
+ */
+ public function getSharedComponent()
+ {
+ return $this->parent->getSharedComponent()->getProtection();
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return ['protection' => $array];
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->getLocked()->applyFromArray(
+ * [
+ * 'locked' => TRUE,
+ * 'hidden' => FALSE
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles)
+ {
+ if ($this->isSupervisor) {
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
+ } else {
+ if (isset($pStyles['locked'])) {
+ $this->setLocked($pStyles['locked']);
+ }
+ if (isset($pStyles['hidden'])) {
+ $this->setHidden($pStyles['hidden']);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get locked.
+ *
+ * @return string
+ */
+ public function getLocked()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getLocked();
+ }
+
+ return $this->locked;
+ }
+
+ /**
+ * Set locked.
+ *
+ * @param string $pValue see self::PROTECTION_*
+ *
+ * @return $this
+ */
+ public function setLocked($pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['locked' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->locked = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hidden.
+ *
+ * @return string
+ */
+ public function getHidden()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHidden();
+ }
+
+ return $this->hidden;
+ }
+
+ /**
+ * Set hidden.
+ *
+ * @param string $pValue see self::PROTECTION_*
+ *
+ * @return $this
+ */
+ public function setHidden($pValue)
+ {
+ if ($this->isSupervisor) {
+ $styleArray = $this->getStyleArray(['hidden' => $pValue]);
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->hidden = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getHashCode();
+ }
+
+ return md5(
+ $this->locked .
+ $this->hidden .
+ __CLASS__
+ );
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'locked', $this->getLocked());
+ $this->exportArray2($exportedArray, 'hidden', $this->getHidden());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php
new file mode 100644
index 0000000..224c0fe
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php
@@ -0,0 +1,664 @@
+font = new Font($isSupervisor, $isConditional);
+ $this->fill = new Fill($isSupervisor, $isConditional);
+ $this->borders = new Borders($isSupervisor);
+ $this->alignment = new Alignment($isSupervisor, $isConditional);
+ $this->numberFormat = new NumberFormat($isSupervisor, $isConditional);
+ $this->protection = new Protection($isSupervisor, $isConditional);
+
+ // bind parent if we are a supervisor
+ if ($isSupervisor) {
+ $this->font->bindParent($this);
+ $this->fill->bindParent($this);
+ $this->borders->bindParent($this);
+ $this->alignment->bindParent($this);
+ $this->numberFormat->bindParent($this);
+ $this->protection->bindParent($this);
+ }
+ }
+
+ /**
+ * Get the shared style component for the currently active cell in currently active sheet.
+ * Only used for style supervisor.
+ *
+ * @return Style
+ */
+ public function getSharedComponent()
+ {
+ $activeSheet = $this->getActiveSheet();
+ $selectedCell = $this->getActiveCell(); // e.g. 'A1'
+
+ if ($activeSheet->cellExists($selectedCell)) {
+ $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex();
+ } else {
+ $xfIndex = 0;
+ }
+
+ return $this->parent->getCellXfByIndex($xfIndex);
+ }
+
+ /**
+ * Get parent. Only used for style supervisor.
+ *
+ * @return Spreadsheet
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Build style array from subcomponents.
+ *
+ * @param array $array
+ *
+ * @return array
+ */
+ public function getStyleArray($array)
+ {
+ return ['quotePrefix' => $array];
+ }
+
+ /**
+ * Apply styles from array.
+ *
+ *
+ * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray(
+ * [
+ * 'font' => [
+ * 'name' => 'Arial',
+ * 'bold' => true,
+ * 'italic' => false,
+ * 'underline' => Font::UNDERLINE_DOUBLE,
+ * 'strikethrough' => false,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ],
+ * 'borders' => [
+ * 'bottom' => [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ],
+ * 'top' => [
+ * 'borderStyle' => Border::BORDER_DASHDOT,
+ * 'color' => [
+ * 'rgb' => '808080'
+ * ]
+ * ]
+ * ],
+ * 'alignment' => [
+ * 'horizontal' => Alignment::HORIZONTAL_CENTER,
+ * 'vertical' => Alignment::VERTICAL_CENTER,
+ * 'wrapText' => true,
+ * ],
+ * 'quotePrefix' => true
+ * ]
+ * );
+ *
+ *
+ * @param array $pStyles Array containing style information
+ * @param bool $pAdvanced advanced mode for setting borders
+ *
+ * @return $this
+ */
+ public function applyFromArray(array $pStyles, $pAdvanced = true)
+ {
+ if ($this->isSupervisor) {
+ $pRange = $this->getSelectedCells();
+
+ // Uppercase coordinate
+ $pRange = strtoupper($pRange);
+
+ // Is it a cell range or a single cell?
+ if (strpos($pRange, ':') === false) {
+ $rangeA = $pRange;
+ $rangeB = $pRange;
+ } else {
+ [$rangeA, $rangeB] = explode(':', $pRange);
+ }
+
+ // Calculate range outer borders
+ $rangeStart = Coordinate::coordinateFromString($rangeA);
+ $rangeEnd = Coordinate::coordinateFromString($rangeB);
+ $rangeStartIndexes = Coordinate::indexesFromString($rangeA);
+ $rangeEndIndexes = Coordinate::indexesFromString($rangeB);
+
+ $columnStart = $rangeStart[0];
+ $columnEnd = $rangeEnd[0];
+
+ // Make sure we can loop upwards on rows and columns
+ if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) {
+ $tmp = $rangeStartIndexes;
+ $rangeStartIndexes = $rangeEndIndexes;
+ $rangeEndIndexes = $tmp;
+ }
+
+ // ADVANCED MODE:
+ if ($pAdvanced && isset($pStyles['borders'])) {
+ // 'allBorders' is a shorthand property for 'outline' and 'inside' and
+ // it applies to components that have not been set explicitly
+ if (isset($pStyles['borders']['allBorders'])) {
+ foreach (['outline', 'inside'] as $component) {
+ if (!isset($pStyles['borders'][$component])) {
+ $pStyles['borders'][$component] = $pStyles['borders']['allBorders'];
+ }
+ }
+ unset($pStyles['borders']['allBorders']); // not needed any more
+ }
+ // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left'
+ // it applies to components that have not been set explicitly
+ if (isset($pStyles['borders']['outline'])) {
+ foreach (['top', 'right', 'bottom', 'left'] as $component) {
+ if (!isset($pStyles['borders'][$component])) {
+ $pStyles['borders'][$component] = $pStyles['borders']['outline'];
+ }
+ }
+ unset($pStyles['borders']['outline']); // not needed any more
+ }
+ // 'inside' is a shorthand property for 'vertical' and 'horizontal'
+ // it applies to components that have not been set explicitly
+ if (isset($pStyles['borders']['inside'])) {
+ foreach (['vertical', 'horizontal'] as $component) {
+ if (!isset($pStyles['borders'][$component])) {
+ $pStyles['borders'][$component] = $pStyles['borders']['inside'];
+ }
+ }
+ unset($pStyles['borders']['inside']); // not needed any more
+ }
+ // width and height characteristics of selection, 1, 2, or 3 (for 3 or more)
+ $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3);
+ $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3);
+
+ // loop through up to 3 x 3 = 9 regions
+ for ($x = 1; $x <= $xMax; ++$x) {
+ // start column index for region
+ $colStart = ($x == 3) ?
+ Coordinate::stringFromColumnIndex($rangeEndIndexes[0])
+ : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1);
+ // end column index for region
+ $colEnd = ($x == 1) ?
+ Coordinate::stringFromColumnIndex($rangeStartIndexes[0])
+ : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x);
+
+ for ($y = 1; $y <= $yMax; ++$y) {
+ // which edges are touching the region
+ $edges = [];
+ if ($x == 1) {
+ // are we at left edge
+ $edges[] = 'left';
+ }
+ if ($x == $xMax) {
+ // are we at right edge
+ $edges[] = 'right';
+ }
+ if ($y == 1) {
+ // are we at top edge?
+ $edges[] = 'top';
+ }
+ if ($y == $yMax) {
+ // are we at bottom edge?
+ $edges[] = 'bottom';
+ }
+
+ // start row index for region
+ $rowStart = ($y == 3) ?
+ $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1;
+
+ // end row index for region
+ $rowEnd = ($y == 1) ?
+ $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y;
+
+ // build range for region
+ $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd;
+
+ // retrieve relevant style array for region
+ $regionStyles = $pStyles;
+ unset($regionStyles['borders']['inside']);
+
+ // what are the inner edges of the region when looking at the selection
+ $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges);
+
+ // inner edges that are not touching the region should take the 'inside' border properties if they have been set
+ foreach ($innerEdges as $innerEdge) {
+ switch ($innerEdge) {
+ case 'top':
+ case 'bottom':
+ // should pick up 'horizontal' border property if set
+ if (isset($pStyles['borders']['horizontal'])) {
+ $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal'];
+ } else {
+ unset($regionStyles['borders'][$innerEdge]);
+ }
+
+ break;
+ case 'left':
+ case 'right':
+ // should pick up 'vertical' border property if set
+ if (isset($pStyles['borders']['vertical'])) {
+ $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical'];
+ } else {
+ unset($regionStyles['borders'][$innerEdge]);
+ }
+
+ break;
+ }
+ }
+
+ // apply region style to region by calling applyFromArray() in simple mode
+ $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false);
+ }
+ }
+
+ // restore initial cell selection range
+ $this->getActiveSheet()->getStyle($pRange);
+
+ return $this;
+ }
+
+ // SIMPLE MODE:
+ // Selection type, inspect
+ if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) {
+ $selectionType = 'COLUMN';
+ } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) {
+ $selectionType = 'ROW';
+ } else {
+ $selectionType = 'CELL';
+ }
+
+ // First loop through columns, rows, or cells to find out which styles are affected by this operation
+ $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $pStyles);
+
+ // clone each of the affected styles, apply the style array, and add the new styles to the workbook
+ $workbook = $this->getActiveSheet()->getParent();
+ $newXfIndexes = [];
+ foreach ($oldXfIndexes as $oldXfIndex => $dummy) {
+ $style = $workbook->getCellXfByIndex($oldXfIndex);
+ $newStyle = clone $style;
+ $newStyle->applyFromArray($pStyles);
+
+ if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) {
+ // there is already such cell Xf in our collection
+ $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex();
+ } else {
+ // we don't have such a cell Xf, need to add
+ $workbook->addCellXf($newStyle);
+ $newXfIndexes[$oldXfIndex] = $newStyle->getIndex();
+ }
+ }
+
+ // Loop through columns, rows, or cells again and update the XF index
+ switch ($selectionType) {
+ case 'COLUMN':
+ for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) {
+ $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col);
+ $oldXfIndex = $columnDimension->getXfIndex();
+ $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
+ }
+
+ break;
+ case 'ROW':
+ for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) {
+ $rowDimension = $this->getActiveSheet()->getRowDimension($row);
+ // row without explicit style should be formatted based on default style
+ $oldXfIndex = $rowDimension->getXfIndex() ?? 0;
+ $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
+ }
+
+ break;
+ case 'CELL':
+ for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) {
+ for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) {
+ $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row);
+ $oldXfIndex = $cell->getXfIndex();
+ $cell->setXfIndex($newXfIndexes[$oldXfIndex]);
+ }
+ }
+
+ break;
+ }
+ } else {
+ // not a supervisor, just apply the style array directly on style object
+ if (isset($pStyles['fill'])) {
+ $this->getFill()->applyFromArray($pStyles['fill']);
+ }
+ if (isset($pStyles['font'])) {
+ $this->getFont()->applyFromArray($pStyles['font']);
+ }
+ if (isset($pStyles['borders'])) {
+ $this->getBorders()->applyFromArray($pStyles['borders']);
+ }
+ if (isset($pStyles['alignment'])) {
+ $this->getAlignment()->applyFromArray($pStyles['alignment']);
+ }
+ if (isset($pStyles['numberFormat'])) {
+ $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']);
+ }
+ if (isset($pStyles['protection'])) {
+ $this->getProtection()->applyFromArray($pStyles['protection']);
+ }
+ if (isset($pStyles['quotePrefix'])) {
+ $this->quotePrefix = $pStyles['quotePrefix'];
+ }
+ }
+
+ return $this;
+ }
+
+ private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $pStyles): array
+ {
+ $oldXfIndexes = [];
+ switch ($selectionType) {
+ case 'COLUMN':
+ for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+ $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true;
+ }
+ foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) {
+ $cellIterator = $columnIterator->getCellIterator();
+ $cellIterator->setIterateOnlyExistingCells(true);
+ foreach ($cellIterator as $columnCell) {
+ if ($columnCell !== null) {
+ $columnCell->getStyle()->applyFromArray($pStyles);
+ }
+ }
+ }
+
+ break;
+ case 'ROW':
+ for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+ if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) {
+ $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style
+ } else {
+ $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true;
+ }
+ }
+ foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) {
+ $cellIterator = $rowIterator->getCellIterator();
+ $cellIterator->setIterateOnlyExistingCells(true);
+ foreach ($cellIterator as $rowCell) {
+ if ($rowCell !== null) {
+ $rowCell->getStyle()->applyFromArray($pStyles);
+ }
+ }
+ }
+
+ break;
+ case 'CELL':
+ for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+ for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+ $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true;
+ }
+ }
+
+ break;
+ }
+
+ return $oldXfIndexes;
+ }
+
+ /**
+ * Get Fill.
+ *
+ * @return Fill
+ */
+ public function getFill()
+ {
+ return $this->fill;
+ }
+
+ /**
+ * Get Font.
+ *
+ * @return Font
+ */
+ public function getFont()
+ {
+ return $this->font;
+ }
+
+ /**
+ * Set font.
+ *
+ * @return $this
+ */
+ public function setFont(Font $font)
+ {
+ $this->font = $font;
+
+ return $this;
+ }
+
+ /**
+ * Get Borders.
+ *
+ * @return Borders
+ */
+ public function getBorders()
+ {
+ return $this->borders;
+ }
+
+ /**
+ * Get Alignment.
+ *
+ * @return Alignment
+ */
+ public function getAlignment()
+ {
+ return $this->alignment;
+ }
+
+ /**
+ * Get Number Format.
+ *
+ * @return NumberFormat
+ */
+ public function getNumberFormat()
+ {
+ return $this->numberFormat;
+ }
+
+ /**
+ * Get Conditional Styles. Only used on supervisor.
+ *
+ * @return Conditional[]
+ */
+ public function getConditionalStyles()
+ {
+ return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell());
+ }
+
+ /**
+ * Set Conditional Styles. Only used on supervisor.
+ *
+ * @param Conditional[] $pValue Array of conditional styles
+ *
+ * @return $this
+ */
+ public function setConditionalStyles(array $pValue)
+ {
+ $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue);
+
+ return $this;
+ }
+
+ /**
+ * Get Protection.
+ *
+ * @return Protection
+ */
+ public function getProtection()
+ {
+ return $this->protection;
+ }
+
+ /**
+ * Get quote prefix.
+ *
+ * @return bool
+ */
+ public function getQuotePrefix()
+ {
+ if ($this->isSupervisor) {
+ return $this->getSharedComponent()->getQuotePrefix();
+ }
+
+ return $this->quotePrefix;
+ }
+
+ /**
+ * Set quote prefix.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setQuotePrefix($pValue)
+ {
+ if ($pValue == '') {
+ $pValue = false;
+ }
+ if ($this->isSupervisor) {
+ $styleArray = ['quotePrefix' => $pValue];
+ $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+ } else {
+ $this->quotePrefix = (bool) $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ return md5(
+ $this->fill->getHashCode() .
+ $this->font->getHashCode() .
+ $this->borders->getHashCode() .
+ $this->alignment->getHashCode() .
+ $this->numberFormat->getHashCode() .
+ $this->protection->getHashCode() .
+ ($this->quotePrefix ? 't' : 'f') .
+ __CLASS__
+ );
+ }
+
+ /**
+ * Get own index in style collection.
+ *
+ * @return int
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * Set own index in style collection.
+ *
+ * @param int $pValue
+ */
+ public function setIndex($pValue): void
+ {
+ $this->index = $pValue;
+ }
+
+ protected function exportArray1(): array
+ {
+ $exportedArray = [];
+ $this->exportArray2($exportedArray, 'alignment', $this->getAlignment());
+ $this->exportArray2($exportedArray, 'borders', $this->getBorders());
+ $this->exportArray2($exportedArray, 'fill', $this->getFill());
+ $this->exportArray2($exportedArray, 'font', $this->getFont());
+ $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat());
+ $this->exportArray2($exportedArray, 'protection', $this->getProtection());
+ $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix());
+
+ return $exportedArray;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php
new file mode 100644
index 0000000..7f655be
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php
@@ -0,0 +1,158 @@
+isSupervisor = $isSupervisor;
+ }
+
+ /**
+ * Bind parent. Only used for supervisor.
+ *
+ * @param Spreadsheet|Style $parent
+ * @param null|string $parentPropertyName
+ *
+ * @return $this
+ */
+ public function bindParent($parent, $parentPropertyName = null)
+ {
+ $this->parent = $parent;
+ $this->parentPropertyName = $parentPropertyName;
+
+ return $this;
+ }
+
+ /**
+ * Is this a supervisor or a cell style component?
+ *
+ * @return bool
+ */
+ public function getIsSupervisor()
+ {
+ return $this->isSupervisor;
+ }
+
+ /**
+ * Get the currently active sheet. Only used for supervisor.
+ *
+ * @return Worksheet
+ */
+ public function getActiveSheet()
+ {
+ return $this->parent->getActiveSheet();
+ }
+
+ /**
+ * Get the currently active cell coordinate in currently active sheet.
+ * Only used for supervisor.
+ *
+ * @return string E.g. 'A1'
+ */
+ public function getSelectedCells()
+ {
+ return $this->getActiveSheet()->getSelectedCells();
+ }
+
+ /**
+ * Get the currently active cell coordinate in currently active sheet.
+ * Only used for supervisor.
+ *
+ * @return string E.g. 'A1'
+ */
+ public function getActiveCell()
+ {
+ return $this->getActiveSheet()->getActiveCell();
+ }
+
+ /**
+ * 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)) && ($key != 'parent')) {
+ $this->$key = clone $value;
+ } else {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ /**
+ * Export style as array.
+ *
+ * Available to anything which extends this class:
+ * Alignment, Border, Borders, Color, Fill, Font,
+ * NumberFormat, Protection, and Style.
+ */
+ final public function exportArray(): array
+ {
+ return $this->exportArray1();
+ }
+
+ /**
+ * Abstract method to be implemented in anything which
+ * extends this class.
+ *
+ * This method invokes exportArray2 with the names and values
+ * of all properties to be included in output array,
+ * returning that array to exportArray, then to caller.
+ */
+ abstract protected function exportArray1(): array;
+
+ /**
+ * Populate array from exportArray1.
+ * This method is available to anything which extends this class.
+ * The parameter index is the key to be added to the array.
+ * The parameter objOrValue is either a primitive type,
+ * which is the value added to the array,
+ * or a Style object to be recursively added via exportArray.
+ *
+ * @param mixed $objOrValue
+ */
+ final protected function exportArray2(array &$exportedArray, string $index, $objOrValue): void
+ {
+ if ($objOrValue instanceof self) {
+ $exportedArray[$index] = $objOrValue->exportArray();
+ } else {
+ $exportedArray[$index] = $objOrValue;
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php
new file mode 100644
index 0000000..b846f7b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php
@@ -0,0 +1,876 @@
+range = $pRange;
+ $this->workSheet = $pSheet;
+ }
+
+ /**
+ * Get AutoFilter Parent Worksheet.
+ *
+ * @return null|Worksheet
+ */
+ public function getParent()
+ {
+ return $this->workSheet;
+ }
+
+ /**
+ * Set AutoFilter Parent Worksheet.
+ *
+ * @param Worksheet $pSheet
+ *
+ * @return $this
+ */
+ public function setParent(?Worksheet $pSheet = null)
+ {
+ $this->workSheet = $pSheet;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Range.
+ *
+ * @return string
+ */
+ public function getRange()
+ {
+ return $this->range;
+ }
+
+ /**
+ * Set AutoFilter Range.
+ *
+ * @param string $pRange Cell range (i.e. A1:E10)
+ *
+ * @return $this
+ */
+ public function setRange($pRange)
+ {
+ // extract coordinate
+ [$worksheet, $pRange] = Worksheet::extractSheetTitle($pRange, true);
+
+ if (strpos($pRange, ':') !== false) {
+ $this->range = $pRange;
+ } elseif (empty($pRange)) {
+ $this->range = '';
+ } else {
+ throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.');
+ }
+
+ if (empty($pRange)) {
+ // Discard all column rules
+ $this->columns = [];
+ } else {
+ // Discard any column rules that are no longer valid within this range
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+ foreach ($this->columns as $key => $value) {
+ $colIndex = Coordinate::columnIndexFromString($key);
+ if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
+ unset($this->columns[$key]);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get all AutoFilter Columns.
+ *
+ * @return AutoFilter\Column[]
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Validate that the specified column is in the AutoFilter range.
+ *
+ * @param string $column Column name (e.g. A)
+ *
+ * @return int The column offset within the autofilter range
+ */
+ public function testColumnInRange($column)
+ {
+ if (empty($this->range)) {
+ throw new PhpSpreadsheetException('No autofilter range is defined.');
+ }
+
+ $columnIndex = Coordinate::columnIndexFromString($column);
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+ if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
+ throw new PhpSpreadsheetException('Column is outside of current autofilter range.');
+ }
+
+ return $columnIndex - $rangeStart[0];
+ }
+
+ /**
+ * Get a specified AutoFilter Column Offset within the defined AutoFilter range.
+ *
+ * @param string $pColumn Column name (e.g. A)
+ *
+ * @return int The offset of the specified column within the autofilter range
+ */
+ public function getColumnOffset($pColumn)
+ {
+ return $this->testColumnInRange($pColumn);
+ }
+
+ /**
+ * Get a specified AutoFilter Column.
+ *
+ * @param string $pColumn Column name (e.g. A)
+ *
+ * @return AutoFilter\Column
+ */
+ public function getColumn($pColumn)
+ {
+ $this->testColumnInRange($pColumn);
+
+ if (!isset($this->columns[$pColumn])) {
+ $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
+ }
+
+ return $this->columns[$pColumn];
+ }
+
+ /**
+ * Get a specified AutoFilter Column by it's offset.
+ *
+ * @param int $pColumnOffset Column offset within range (starting from 0)
+ *
+ * @return AutoFilter\Column
+ */
+ public function getColumnByOffset($pColumnOffset)
+ {
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+ $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $pColumnOffset);
+
+ return $this->getColumn($pColumn);
+ }
+
+ /**
+ * Set AutoFilter.
+ *
+ * @param AutoFilter\Column|string $pColumn
+ * A simple string containing a Column ID like 'A' is permitted
+ *
+ * @return $this
+ */
+ public function setColumn($pColumn)
+ {
+ if ((is_string($pColumn)) && (!empty($pColumn))) {
+ $column = $pColumn;
+ } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
+ $column = $pColumn->getColumnIndex();
+ } else {
+ throw new PhpSpreadsheetException('Column is not within the autofilter range.');
+ }
+ $this->testColumnInRange($column);
+
+ if (is_string($pColumn)) {
+ $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
+ } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
+ $pColumn->setParent($this);
+ $this->columns[$column] = $pColumn;
+ }
+ ksort($this->columns);
+
+ return $this;
+ }
+
+ /**
+ * Clear a specified AutoFilter Column.
+ *
+ * @param string $pColumn Column name (e.g. A)
+ *
+ * @return $this
+ */
+ public function clearColumn($pColumn)
+ {
+ $this->testColumnInRange($pColumn);
+
+ if (isset($this->columns[$pColumn])) {
+ unset($this->columns[$pColumn]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Shift an AutoFilter Column Rule to a different column.
+ *
+ * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.
+ * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
+ * Use with caution.
+ *
+ * @param string $fromColumn Column name (e.g. A)
+ * @param string $toColumn Column name (e.g. B)
+ *
+ * @return $this
+ */
+ public function shiftColumn($fromColumn, $toColumn)
+ {
+ $fromColumn = strtoupper($fromColumn);
+ $toColumn = strtoupper($toColumn);
+
+ if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
+ $this->columns[$fromColumn]->setParent();
+ $this->columns[$fromColumn]->setColumnIndex($toColumn);
+ $this->columns[$toColumn] = $this->columns[$fromColumn];
+ $this->columns[$toColumn]->setParent($this);
+ unset($this->columns[$fromColumn]);
+
+ ksort($this->columns);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Test if cell value is in the defined set of values.
+ *
+ * @param mixed $cellValue
+ * @param mixed[] $dataSet
+ *
+ * @return bool
+ */
+ private static function filterTestInSimpleDataSet($cellValue, $dataSet)
+ {
+ $dataSetValues = $dataSet['filterValues'];
+ $blanks = $dataSet['blanks'];
+ if (($cellValue == '') || ($cellValue === null)) {
+ return $blanks;
+ }
+
+ return in_array($cellValue, $dataSetValues);
+ }
+
+ /**
+ * Test if cell value is in the defined set of Excel date values.
+ *
+ * @param mixed $cellValue
+ * @param mixed[] $dataSet
+ *
+ * @return bool
+ */
+ private static function filterTestInDateGroupSet($cellValue, $dataSet)
+ {
+ $dateSet = $dataSet['filterValues'];
+ $blanks = $dataSet['blanks'];
+ if (($cellValue == '') || ($cellValue === null)) {
+ return $blanks;
+ }
+
+ if (is_numeric($cellValue)) {
+ $dateValue = Date::excelToTimestamp($cellValue);
+ if ($cellValue < 1) {
+ // Just the time part
+ $dtVal = date('His', $dateValue);
+ $dateSet = $dateSet['time'];
+ } elseif ($cellValue == floor($cellValue)) {
+ // Just the date part
+ $dtVal = date('Ymd', $dateValue);
+ $dateSet = $dateSet['date'];
+ } else {
+ // date and time parts
+ $dtVal = date('YmdHis', $dateValue);
+ $dateSet = $dateSet['dateTime'];
+ }
+ foreach ($dateSet as $dateValue) {
+ // Use of substr to extract value at the appropriate group level
+ if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Test if cell value is within a set of values defined by a ruleset.
+ *
+ * @param mixed $cellValue
+ * @param mixed[] $ruleSet
+ *
+ * @return bool
+ */
+ private static function filterTestInCustomDataSet($cellValue, $ruleSet)
+ {
+ $dataSet = $ruleSet['filterRules'];
+ $join = $ruleSet['join'];
+ $customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;
+
+ if (!$customRuleForBlanks) {
+ // Blank cells are always ignored, so return a FALSE
+ if (($cellValue == '') || ($cellValue === null)) {
+ return false;
+ }
+ }
+ $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
+ foreach ($dataSet as $rule) {
+ $retVal = false;
+
+ if (is_numeric($rule['value'])) {
+ // Numeric values are tested using the appropriate operator
+ switch ($rule['operator']) {
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
+ $retVal = ($cellValue == $rule['value']);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
+ $retVal = ($cellValue != $rule['value']);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
+ $retVal = ($cellValue > $rule['value']);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
+ $retVal = ($cellValue >= $rule['value']);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
+ $retVal = ($cellValue < $rule['value']);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
+ $retVal = ($cellValue <= $rule['value']);
+
+ break;
+ }
+ } elseif ($rule['value'] == '') {
+ switch ($rule['operator']) {
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
+ $retVal = (($cellValue == '') || ($cellValue === null));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
+ $retVal = (($cellValue != '') && ($cellValue !== null));
+
+ break;
+ default:
+ $retVal = true;
+
+ break;
+ }
+ } else {
+ // String values are always tested for equality, factoring in for wildcards (hence a regexp test)
+ $retVal = preg_match('/^' . $rule['value'] . '$/i', $cellValue);
+ }
+ // If there are multiple conditions, then we need to test both using the appropriate join operator
+ switch ($join) {
+ case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
+ $returnVal = $returnVal || $retVal;
+ // Break as soon as we have a TRUE match for OR joins,
+ // to avoid unnecessary additional code execution
+ if ($returnVal) {
+ return $returnVal;
+ }
+
+ break;
+ case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
+ $returnVal = $returnVal && $retVal;
+
+ break;
+ }
+ }
+
+ return $returnVal;
+ }
+
+ /**
+ * Test if cell date value is matches a set of values defined by a set of months.
+ *
+ * @param mixed $cellValue
+ * @param mixed[] $monthSet
+ *
+ * @return bool
+ */
+ private static function filterTestInPeriodDateSet($cellValue, $monthSet)
+ {
+ // Blank cells are always ignored, so return a FALSE
+ if (($cellValue == '') || ($cellValue === null)) {
+ return false;
+ }
+
+ if (is_numeric($cellValue)) {
+ $dateValue = date('m', Date::excelToTimestamp($cellValue));
+ if (in_array($dateValue, $monthSet)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Search/Replace arrays to convert Excel wildcard syntax to a regexp syntax for preg_matching.
+ *
+ * @var array
+ */
+ private static $fromReplace = ['\*', '\?', '~~', '~.*', '~.?'];
+
+ private static $toReplace = ['.*', '.', '~', '\*', '\?'];
+
+ /**
+ * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.
+ *
+ * @param string $dynamicRuleType
+ * @param AutoFilter\Column $filterColumn
+ *
+ * @return mixed[]
+ */
+ private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn)
+ {
+ $rDateType = Functions::getReturnDateType();
+ Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC);
+ $val = $maxVal = null;
+
+ $ruleValues = [];
+ $baseDate = DateTimeExcel\Current::now();
+ // Calculate start/end dates for the required date range based on current date
+ switch ($dynamicRuleType) {
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
+ $baseDate = strtotime('-7 days', $baseDate);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
+ $baseDate = strtotime('-7 days', $baseDate);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
+ $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
+ $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
+ $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
+ $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
+ $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
+ $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ }
+
+ switch ($dynamicRuleType) {
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
+ $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
+ $val = (int) Date::PHPToExcel($baseDate);
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE:
+ $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
+ $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
+ $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate)));
+ ++$maxVal;
+ $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
+ $thisMonth = date('m', $baseDate);
+ $thisQuarter = floor(--$thisMonth / 3);
+ $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate)));
+ ++$maxVal;
+ $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
+ $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate)));
+ ++$maxVal;
+ $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
+ $dayOfWeek = date('w', $baseDate);
+ $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek;
+ $maxVal = $val + 7;
+
+ break;
+ }
+
+ switch ($dynamicRuleType) {
+ // Adjust Today dates for Yesterday and Tomorrow
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
+ --$maxVal;
+ --$val;
+
+ break;
+ case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
+ ++$maxVal;
+ ++$val;
+
+ break;
+ }
+
+ // Set the filter column rule attributes ready for writing
+ $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxVal]);
+
+ // Set the rules for identifying rows for hide/show
+ $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
+ $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal];
+ Functions::setReturnDateType($rDateType);
+
+ return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
+ }
+
+ private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue)
+ {
+ $range = $columnID . $startRow . ':' . $columnID . $endRow;
+ $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
+
+ $dataValues = array_filter($dataValues);
+ if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
+ rsort($dataValues);
+ } else {
+ sort($dataValues);
+ }
+
+ $slice = array_slice($dataValues, 0, $ruleValue);
+
+ return array_pop($slice);
+ }
+
+ /**
+ * Apply the AutoFilter rules to the AutoFilter Range.
+ *
+ * @return $this
+ */
+ public function showHideRows()
+ {
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+
+ // The heading row should always be visible
+ $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
+
+ $columnFilterTests = [];
+ foreach ($this->columns as $columnID => $filterColumn) {
+ $rules = $filterColumn->getRules();
+ switch ($filterColumn->getFilterType()) {
+ case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
+ $ruleType = null;
+ $ruleValues = [];
+ // Build a list of the filter value selections
+ foreach ($rules as $rule) {
+ $ruleType = $rule->getRuleType();
+ $ruleValues[] = $rule->getValue();
+ }
+ // Test if we want to include blanks in our filter criteria
+ $blanks = false;
+ $ruleDataSet = array_filter($ruleValues);
+ if (count($ruleValues) != count($ruleDataSet)) {
+ $blanks = true;
+ }
+ if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) {
+ // Filter on absolute values
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInSimpleDataSet',
+ 'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
+ ];
+ } else {
+ // Filter on date group values
+ $arguments = [
+ 'date' => [],
+ 'time' => [],
+ 'dateTime' => [],
+ ];
+ foreach ($ruleDataSet as $ruleValue) {
+ $date = $time = '';
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')
+ ) {
+ $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
+ }
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')
+ ) {
+ $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
+ }
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')
+ ) {
+ $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
+ }
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')
+ ) {
+ $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
+ }
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')
+ ) {
+ $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
+ }
+ if (
+ (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) &&
+ ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')
+ ) {
+ $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
+ }
+ $dateTime = $date . $time;
+ $arguments['date'][] = $date;
+ $arguments['time'][] = $time;
+ $arguments['dateTime'][] = $dateTime;
+ }
+ // Remove empty elements
+ $arguments['date'] = array_filter($arguments['date']);
+ $arguments['time'] = array_filter($arguments['time']);
+ $arguments['dateTime'] = array_filter($arguments['dateTime']);
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInDateGroupSet',
+ 'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
+ ];
+ }
+
+ break;
+ case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
+ $customRuleForBlanks = false;
+ $ruleValues = [];
+ // Build a list of the filter value selections
+ foreach ($rules as $rule) {
+ $ruleValue = $rule->getValue();
+ if (!is_numeric($ruleValue)) {
+ // Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
+ $ruleValue = preg_quote($ruleValue);
+ $ruleValue = str_replace(self::$fromReplace, self::$toReplace, $ruleValue);
+ if (trim($ruleValue) == '') {
+ $customRuleForBlanks = true;
+ $ruleValue = trim($ruleValue);
+ }
+ }
+ $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
+ }
+ $join = $filterColumn->getJoin();
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInCustomDataSet',
+ 'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
+ ];
+
+ break;
+ case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
+ $ruleValues = [];
+ foreach ($rules as $rule) {
+ // We should only ever have one Dynamic Filter Rule anyway
+ $dynamicRuleType = $rule->getGrouping();
+ if (
+ ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ||
+ ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)
+ ) {
+ // Number (Average) based
+ // Calculate the average
+ $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
+ $average = Calculation::getInstance()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
+ // Set above/below rule based on greaterThan or LessTan
+ $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
+ ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
+ : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
+ $ruleValues[] = [
+ 'operator' => $operator,
+ 'value' => $average,
+ ];
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInCustomDataSet',
+ 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
+ ];
+ } else {
+ // Date based
+ if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
+ $periodType = '';
+ $period = 0;
+ // Month or Quarter
+ sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
+ if ($periodType == 'M') {
+ $ruleValues = [$period];
+ } else {
+ --$period;
+ $periodEnd = (1 + $period) * 3;
+ $periodStart = 1 + $period * 3;
+ $ruleValues = range($periodStart, $periodEnd);
+ }
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInPeriodDateSet',
+ 'arguments' => $ruleValues,
+ ];
+ $filterColumn->setAttributes([]);
+ } else {
+ // Date Range
+ $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);
+
+ break;
+ }
+ }
+ }
+
+ break;
+ case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
+ $ruleValues = [];
+ $dataRowCount = $rangeEnd[1] - $rangeStart[1];
+ $toptenRuleType = null;
+ $ruleValue = 0;
+ $ruleOperator = null;
+ foreach ($rules as $rule) {
+ // We should only ever have one Dynamic Filter Rule anyway
+ $toptenRuleType = $rule->getGrouping();
+ $ruleValue = $rule->getValue();
+ $ruleOperator = $rule->getOperator();
+ }
+ if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
+ $ruleValue = floor($ruleValue * ($dataRowCount / 100));
+ }
+ if (!is_array($ruleValue) && $ruleValue < 1) {
+ $ruleValue = 1;
+ }
+ if (!is_array($ruleValue) && $ruleValue > 500) {
+ $ruleValue = 500;
+ }
+
+ $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue);
+
+ $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
+ ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
+ : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
+ $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
+ $columnFilterTests[$columnID] = [
+ 'method' => 'filterTestInCustomDataSet',
+ 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
+ ];
+ $filterColumn->setAttributes(['maxVal' => $maxVal]);
+
+ break;
+ }
+ }
+
+ // Execute the column tests for each row in the autoFilter range to determine show/hide,
+ for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
+ $result = true;
+ foreach ($columnFilterTests as $columnID => $columnFilterTest) {
+ $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
+ // Execute the filter test
+ $result = $result &&
+ call_user_func_array(
+ [self::class, $columnFilterTest['method']],
+ [$cellValue, $columnFilterTest['arguments']]
+ );
+ // If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests
+ if (!$result) {
+ break;
+ }
+ }
+ // Set show/hide for the row based on the result of the autoFilter result
+ $this->workSheet->getRowDimension($row)->setVisible($result);
+ }
+
+ 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)) {
+ if ($key === 'workSheet') {
+ // Detach from worksheet
+ $this->{$key} = null;
+ } else {
+ $this->{$key} = clone $value;
+ }
+ } elseif ((is_array($value)) && ($key == 'columns')) {
+ // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
+ $this->{$key} = [];
+ foreach ($value as $k => $v) {
+ $this->{$key}[$k] = clone $v;
+ // attach the new cloned Column to this new cloned Autofilter object
+ $this->{$key}[$k]->setParent($this);
+ }
+ } else {
+ $this->{$key} = $value;
+ }
+ }
+ }
+
+ /**
+ * toString method replicates previous behavior by returning the range if object is
+ * referenced as a property of its parent.
+ */
+ public function __toString()
+ {
+ return (string) $this->range;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php
new file mode 100644
index 0000000..8ec0ca3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php
@@ -0,0 +1,380 @@
+columnIndex = $pColumn;
+ $this->parent = $pParent;
+ }
+
+ /**
+ * Get AutoFilter column index as string eg: 'A'.
+ *
+ * @return string
+ */
+ public function getColumnIndex()
+ {
+ return $this->columnIndex;
+ }
+
+ /**
+ * Set AutoFilter column index as string eg: 'A'.
+ *
+ * @param string $pColumn Column (e.g. A)
+ *
+ * @return $this
+ */
+ public function setColumnIndex($pColumn)
+ {
+ // Uppercase coordinate
+ $pColumn = strtoupper($pColumn);
+ if ($this->parent !== null) {
+ $this->parent->testColumnInRange($pColumn);
+ }
+
+ $this->columnIndex = $pColumn;
+
+ return $this;
+ }
+
+ /**
+ * Get this Column's AutoFilter Parent.
+ *
+ * @return null|AutoFilter
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Set this Column's AutoFilter Parent.
+ *
+ * @param AutoFilter $pParent
+ *
+ * @return $this
+ */
+ public function setParent(?AutoFilter $pParent = null)
+ {
+ $this->parent = $pParent;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Type.
+ *
+ * @return string
+ */
+ public function getFilterType()
+ {
+ return $this->filterType;
+ }
+
+ /**
+ * Set AutoFilter Type.
+ *
+ * @param string $pFilterType
+ *
+ * @return $this
+ */
+ public function setFilterType($pFilterType)
+ {
+ if (!in_array($pFilterType, self::$filterTypes)) {
+ throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.');
+ }
+
+ $this->filterType = $pFilterType;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Multiple Rules And/Or Join.
+ *
+ * @return string
+ */
+ public function getJoin()
+ {
+ return $this->join;
+ }
+
+ /**
+ * Set AutoFilter Multiple Rules And/Or.
+ *
+ * @param string $pJoin And/Or
+ *
+ * @return $this
+ */
+ public function setJoin($pJoin)
+ {
+ // Lowercase And/Or
+ $pJoin = strtolower($pJoin);
+ if (!in_array($pJoin, self::$ruleJoins)) {
+ throw new PhpSpreadsheetException('Invalid rule connection for column AutoFilter.');
+ }
+
+ $this->join = $pJoin;
+
+ return $this;
+ }
+
+ /**
+ * Set AutoFilter Attributes.
+ *
+ * @param string[] $attributes
+ *
+ * @return $this
+ */
+ public function setAttributes(array $attributes)
+ {
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+
+ /**
+ * Set An AutoFilter Attribute.
+ *
+ * @param string $pName Attribute Name
+ * @param string $pValue Attribute Value
+ *
+ * @return $this
+ */
+ public function setAttribute($pName, $pValue)
+ {
+ $this->attributes[$pName] = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Column Attributes.
+ *
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Get specific AutoFilter Column Attribute.
+ *
+ * @param string $pName Attribute Name
+ *
+ * @return null|string
+ */
+ public function getAttribute($pName)
+ {
+ if (isset($this->attributes[$pName])) {
+ return $this->attributes[$pName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get all AutoFilter Column Rules.
+ *
+ * @return Column\Rule[]
+ */
+ public function getRules()
+ {
+ return $this->ruleset;
+ }
+
+ /**
+ * Get a specified AutoFilter Column Rule.
+ *
+ * @param int $pIndex Rule index in the ruleset array
+ *
+ * @return Column\Rule
+ */
+ public function getRule($pIndex)
+ {
+ if (!isset($this->ruleset[$pIndex])) {
+ $this->ruleset[$pIndex] = new Column\Rule($this);
+ }
+
+ return $this->ruleset[$pIndex];
+ }
+
+ /**
+ * Create a new AutoFilter Column Rule in the ruleset.
+ *
+ * @return Column\Rule
+ */
+ public function createRule()
+ {
+ $this->ruleset[] = new Column\Rule($this);
+
+ return end($this->ruleset);
+ }
+
+ /**
+ * Add a new AutoFilter Column Rule to the ruleset.
+ *
+ * @return $this
+ */
+ public function addRule(Column\Rule $pRule)
+ {
+ $pRule->setParent($this);
+ $this->ruleset[] = $pRule;
+
+ return $this;
+ }
+
+ /**
+ * Delete a specified AutoFilter Column Rule
+ * If the number of rules is reduced to 1, then we reset And/Or logic to Or.
+ *
+ * @param int $pIndex Rule index in the ruleset array
+ *
+ * @return $this
+ */
+ public function deleteRule($pIndex)
+ {
+ if (isset($this->ruleset[$pIndex])) {
+ unset($this->ruleset[$pIndex]);
+ // If we've just deleted down to a single rule, then reset And/Or joining to Or
+ if (count($this->ruleset) <= 1) {
+ $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Delete all AutoFilter Column Rules.
+ *
+ * @return $this
+ */
+ public function clearRules()
+ {
+ $this->ruleset = [];
+ $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
+
+ 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 ($key === 'parent') {
+ // Detach from autofilter parent
+ $this->parent = null;
+ } elseif ($key === 'ruleset') {
+ // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
+ $this->ruleset = [];
+ foreach ($value as $k => $v) {
+ $cloned = clone $v;
+ $cloned->setParent($this); // attach the new cloned Rule to this new cloned Autofilter Cloned object
+ $this->ruleset[$k] = $cloned;
+ }
+ } elseif (is_object($value)) {
+ $this->$key = clone $value;
+ } else {
+ $this->$key = $value;
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php
new file mode 100644
index 0000000..330a164
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php
@@ -0,0 +1,449 @@
+
+ *
+ *
+ *
+ *
+ *
+ */
+ const AUTOFILTER_COLUMN_RULE_EQUAL = 'equal';
+ const AUTOFILTER_COLUMN_RULE_NOTEQUAL = 'notEqual';
+ const AUTOFILTER_COLUMN_RULE_GREATERTHAN = 'greaterThan';
+ const AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL = 'greaterThanOrEqual';
+ const AUTOFILTER_COLUMN_RULE_LESSTHAN = 'lessThan';
+ const AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL = 'lessThanOrEqual';
+
+ private static $operators = [
+ self::AUTOFILTER_COLUMN_RULE_EQUAL,
+ self::AUTOFILTER_COLUMN_RULE_NOTEQUAL,
+ self::AUTOFILTER_COLUMN_RULE_GREATERTHAN,
+ self::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL,
+ self::AUTOFILTER_COLUMN_RULE_LESSTHAN,
+ self::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL,
+ ];
+
+ const AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE = 'byValue';
+ const AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT = 'byPercent';
+
+ private static $topTenValue = [
+ self::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE,
+ self::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT,
+ ];
+
+ const AUTOFILTER_COLUMN_RULE_TOPTEN_TOP = 'top';
+ const AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM = 'bottom';
+
+ private static $topTenType = [
+ self::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP,
+ self::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM,
+ ];
+
+ // Rule Operators (Numeric, Boolean etc)
+// const AUTOFILTER_COLUMN_RULE_BETWEEN = 'between'; // greaterThanOrEqual 1 && lessThanOrEqual 2
+ // Rule Operators (Numeric Special) which are translated to standard numeric operators with calculated values
+// const AUTOFILTER_COLUMN_RULE_TOPTEN = 'topTen'; // greaterThan calculated value
+// const AUTOFILTER_COLUMN_RULE_TOPTENPERCENT = 'topTenPercent'; // greaterThan calculated value
+// const AUTOFILTER_COLUMN_RULE_ABOVEAVERAGE = 'aboveAverage'; // Value is calculated as the average
+// const AUTOFILTER_COLUMN_RULE_BELOWAVERAGE = 'belowAverage'; // Value is calculated as the average
+ // Rule Operators (String) which are set as wild-carded values
+// const AUTOFILTER_COLUMN_RULE_BEGINSWITH = 'beginsWith'; // A*
+// const AUTOFILTER_COLUMN_RULE_ENDSWITH = 'endsWith'; // *Z
+// const AUTOFILTER_COLUMN_RULE_CONTAINS = 'contains'; // *B*
+// const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN = 'notEqual'; // notEqual *B*
+ // Rule Operators (Date Special) which are translated to standard numeric operators with calculated values
+// const AUTOFILTER_COLUMN_RULE_BEFORE = 'lessThan';
+// const AUTOFILTER_COLUMN_RULE_AFTER = 'greaterThan';
+// const AUTOFILTER_COLUMN_RULE_YESTERDAY = 'yesterday';
+// const AUTOFILTER_COLUMN_RULE_TODAY = 'today';
+// const AUTOFILTER_COLUMN_RULE_TOMORROW = 'tomorrow';
+// const AUTOFILTER_COLUMN_RULE_LASTWEEK = 'lastWeek';
+// const AUTOFILTER_COLUMN_RULE_THISWEEK = 'thisWeek';
+// const AUTOFILTER_COLUMN_RULE_NEXTWEEK = 'nextWeek';
+// const AUTOFILTER_COLUMN_RULE_LASTMONTH = 'lastMonth';
+// const AUTOFILTER_COLUMN_RULE_THISMONTH = 'thisMonth';
+// const AUTOFILTER_COLUMN_RULE_NEXTMONTH = 'nextMonth';
+// const AUTOFILTER_COLUMN_RULE_LASTQUARTER = 'lastQuarter';
+// const AUTOFILTER_COLUMN_RULE_THISQUARTER = 'thisQuarter';
+// const AUTOFILTER_COLUMN_RULE_NEXTQUARTER = 'nextQuarter';
+// const AUTOFILTER_COLUMN_RULE_LASTYEAR = 'lastYear';
+// const AUTOFILTER_COLUMN_RULE_THISYEAR = 'thisYear';
+// const AUTOFILTER_COLUMN_RULE_NEXTYEAR = 'nextYear';
+// const AUTOFILTER_COLUMN_RULE_YEARTODATE = 'yearToDate'; //
+// const AUTOFILTER_COLUMN_RULE_ALLDATESINMONTH = 'allDatesInMonth'; // for Month/February
+// const AUTOFILTER_COLUMN_RULE_ALLDATESINQUARTER = 'allDatesInQuarter'; // for Quarter 2
+
+ /**
+ * Autofilter Column.
+ *
+ * @var Column
+ */
+ private $parent;
+
+ /**
+ * Autofilter Rule Type.
+ *
+ * @var string
+ */
+ private $ruleType = self::AUTOFILTER_RULETYPE_FILTER;
+
+ /**
+ * Autofilter Rule Value.
+ *
+ * @var string|string[]
+ */
+ private $value = '';
+
+ /**
+ * Autofilter Rule Operator.
+ *
+ * @var string
+ */
+ private $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
+
+ /**
+ * DateTimeGrouping Group Value.
+ *
+ * @var string
+ */
+ private $grouping = '';
+
+ /**
+ * Create a new Rule.
+ *
+ * @param Column $pParent
+ */
+ public function __construct(?Column $pParent = null)
+ {
+ $this->parent = $pParent;
+ }
+
+ /**
+ * Get AutoFilter Rule Type.
+ *
+ * @return string
+ */
+ public function getRuleType()
+ {
+ return $this->ruleType;
+ }
+
+ /**
+ * Set AutoFilter Rule Type.
+ *
+ * @param string $pRuleType see self::AUTOFILTER_RULETYPE_*
+ *
+ * @return $this
+ */
+ public function setRuleType($pRuleType)
+ {
+ if (!in_array($pRuleType, self::$ruleTypes)) {
+ throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
+ }
+
+ $this->ruleType = $pRuleType;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Rule Value.
+ *
+ * @return string|string[]
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set AutoFilter Rule Value.
+ *
+ * @param string|string[] $pValue
+ *
+ * @return $this
+ */
+ public function setValue($pValue)
+ {
+ if (is_array($pValue)) {
+ $grouping = -1;
+ foreach ($pValue as $key => $value) {
+ // Validate array entries
+ if (!in_array($key, self::$dateTimeGroups)) {
+ // Remove any invalid entries from the value array
+ unset($pValue[$key]);
+ } else {
+ // Work out what the dateTime grouping will be
+ $grouping = max($grouping, array_search($key, self::$dateTimeGroups));
+ }
+ }
+ if (count($pValue) == 0) {
+ throw new PhpSpreadsheetException('Invalid rule value for column AutoFilter Rule.');
+ }
+ // Set the dateTime grouping that we've anticipated
+ $this->setGrouping(self::$dateTimeGroups[$grouping]);
+ }
+ $this->value = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Rule Operator.
+ *
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * Set AutoFilter Rule Operator.
+ *
+ * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_*
+ *
+ * @return $this
+ */
+ public function setOperator($pOperator)
+ {
+ if (empty($pOperator)) {
+ $pOperator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
+ }
+ if (
+ (!in_array($pOperator, self::$operators)) &&
+ (!in_array($pOperator, self::$topTenValue))
+ ) {
+ throw new PhpSpreadsheetException('Invalid operator for column AutoFilter Rule.');
+ }
+ $this->operator = $pOperator;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter Rule Grouping.
+ *
+ * @return string
+ */
+ public function getGrouping()
+ {
+ return $this->grouping;
+ }
+
+ /**
+ * Set AutoFilter Rule Grouping.
+ *
+ * @param string $pGrouping
+ *
+ * @return $this
+ */
+ public function setGrouping($pGrouping)
+ {
+ if (
+ ($pGrouping !== null) &&
+ (!in_array($pGrouping, self::$dateTimeGroups)) &&
+ (!in_array($pGrouping, self::$dynamicTypes)) &&
+ (!in_array($pGrouping, self::$topTenType))
+ ) {
+ throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
+ }
+ $this->grouping = $pGrouping;
+
+ return $this;
+ }
+
+ /**
+ * Set AutoFilter Rule.
+ *
+ * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_*
+ * @param string|string[] $pValue
+ * @param string $pGrouping
+ *
+ * @return $this
+ */
+ public function setRule($pOperator, $pValue, $pGrouping = null)
+ {
+ $this->setOperator($pOperator);
+ $this->setValue($pValue);
+ // Only set grouping if it's been passed in as a user-supplied argument,
+ // otherwise we're calculating it when we setValue() and don't want to overwrite that
+ // If the user supplies an argumnet for grouping, then on their own head be it
+ if ($pGrouping !== null) {
+ $this->setGrouping($pGrouping);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get this Rule's AutoFilter Column Parent.
+ *
+ * @return Column
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Set this Rule's AutoFilter Column Parent.
+ *
+ * @param Column $pParent
+ *
+ * @return $this
+ */
+ public function setParent(?Column $pParent = null)
+ {
+ $this->parent = $pParent;
+
+ 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)) {
+ if ($key == 'parent') {
+ // Detach from autofilter column parent
+ $this->$key = null;
+ } else {
+ $this->$key = clone $value;
+ }
+ } else {
+ $this->$key = $value;
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
new file mode 100644
index 0000000..5829671
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
@@ -0,0 +1,532 @@
+name = '';
+ $this->description = '';
+ $this->worksheet = null;
+ $this->coordinates = 'A1';
+ $this->offsetX = 0;
+ $this->offsetY = 0;
+ $this->width = 0;
+ $this->height = 0;
+ $this->resizeProportional = true;
+ $this->rotation = 0;
+ $this->shadow = new Drawing\Shadow();
+
+ // Set image index
+ ++self::$imageCounter;
+ $this->imageIndex = self::$imageCounter;
+ }
+
+ /**
+ * Get image index.
+ *
+ * @return int
+ */
+ public function getImageIndex()
+ {
+ return $this->imageIndex;
+ }
+
+ /**
+ * Get Name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set Name.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setName($pValue)
+ {
+ $this->name = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Description.
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * Set Description.
+ *
+ * @param string $description
+ *
+ * @return $this
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * Get Worksheet.
+ *
+ * @return null|Worksheet
+ */
+ public function getWorksheet()
+ {
+ return $this->worksheet;
+ }
+
+ /**
+ * Set Worksheet.
+ *
+ * @param Worksheet $pValue
+ * @param bool $pOverrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet?
+ *
+ * @return $this
+ */
+ public function setWorksheet(?Worksheet $pValue = null, $pOverrideOld = false)
+ {
+ if ($this->worksheet === null) {
+ // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ $this->worksheet = $pValue;
+ $this->worksheet->getCell($this->coordinates);
+ $this->worksheet->getDrawingCollection()->append($this);
+ } else {
+ if ($pOverrideOld) {
+ // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ $iterator = $this->worksheet->getDrawingCollection()->getIterator();
+
+ while ($iterator->valid()) {
+ if ($iterator->current()->getHashCode() === $this->getHashCode()) {
+ $this->worksheet->getDrawingCollection()->offsetUnset($iterator->key());
+ $this->worksheet = null;
+
+ break;
+ }
+ }
+
+ // Set new \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ $this->setWorksheet($pValue);
+ } else {
+ throw new PhpSpreadsheetException('A Worksheet has already been assigned. Drawings can only exist on one \\PhpOffice\\PhpSpreadsheet\\Worksheet.');
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get Coordinates.
+ *
+ * @return string
+ */
+ public function getCoordinates()
+ {
+ return $this->coordinates;
+ }
+
+ /**
+ * Set Coordinates.
+ *
+ * @param string $pValue eg: 'A1'
+ *
+ * @return $this
+ */
+ public function setCoordinates($pValue)
+ {
+ $this->coordinates = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get OffsetX.
+ *
+ * @return int
+ */
+ public function getOffsetX()
+ {
+ return $this->offsetX;
+ }
+
+ /**
+ * Set OffsetX.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setOffsetX($pValue)
+ {
+ $this->offsetX = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get OffsetY.
+ *
+ * @return int
+ */
+ public function getOffsetY()
+ {
+ return $this->offsetY;
+ }
+
+ /**
+ * Set OffsetY.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setOffsetY($pValue)
+ {
+ $this->offsetY = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Width.
+ *
+ * @return int
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * Set Width.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setWidth($pValue)
+ {
+ // Resize proportional?
+ if ($this->resizeProportional && $pValue != 0) {
+ $ratio = $this->height / ($this->width != 0 ? $this->width : 1);
+ $this->height = (int) round($ratio * $pValue);
+ }
+
+ // Set width
+ $this->width = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Height.
+ *
+ * @return int
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Set Height.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setHeight($pValue)
+ {
+ // Resize proportional?
+ if ($this->resizeProportional && $pValue != 0) {
+ $ratio = $this->width / ($this->height != 0 ? $this->height : 1);
+ $this->width = (int) round($ratio * $pValue);
+ }
+
+ // Set height
+ $this->height = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Set width and height with proportional resize.
+ *
+ * Example:
+ *
+ * $objDrawing->setResizeProportional(true);
+ * $objDrawing->setWidthAndHeight(160,120);
+ *
+ *
+ * @author Vincent@luo MSN:kele_100@hotmail.com
+ *
+ * @param int $width
+ * @param int $height
+ *
+ * @return $this
+ */
+ public function setWidthAndHeight($width, $height)
+ {
+ $xratio = $width / ($this->width != 0 ? $this->width : 1);
+ $yratio = $height / ($this->height != 0 ? $this->height : 1);
+ if ($this->resizeProportional && !($width == 0 || $height == 0)) {
+ if (($xratio * $this->height) < $height) {
+ $this->height = (int) ceil($xratio * $this->height);
+ $this->width = $width;
+ } else {
+ $this->width = (int) ceil($yratio * $this->width);
+ $this->height = $height;
+ }
+ } else {
+ $this->width = $width;
+ $this->height = $height;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get ResizeProportional.
+ *
+ * @return bool
+ */
+ public function getResizeProportional()
+ {
+ return $this->resizeProportional;
+ }
+
+ /**
+ * Set ResizeProportional.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setResizeProportional($pValue)
+ {
+ $this->resizeProportional = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Rotation.
+ *
+ * @return int
+ */
+ public function getRotation()
+ {
+ return $this->rotation;
+ }
+
+ /**
+ * Set Rotation.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setRotation($pValue)
+ {
+ $this->rotation = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Shadow.
+ *
+ * @return Drawing\Shadow
+ */
+ public function getShadow()
+ {
+ return $this->shadow;
+ }
+
+ /**
+ * Set Shadow.
+ *
+ * @param Drawing\Shadow $pValue
+ *
+ * @return $this
+ */
+ public function setShadow(?Drawing\Shadow $pValue = null)
+ {
+ $this->shadow = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ return md5(
+ $this->name .
+ $this->description .
+ $this->worksheet->getHashCode() .
+ $this->coordinates .
+ $this->offsetX .
+ $this->offsetY .
+ $this->width .
+ $this->height .
+ $this->rotation .
+ $this->shadow->getHashCode() .
+ __CLASS__
+ );
+ }
+
+ /**
+ * 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 ($key == 'worksheet') {
+ $this->worksheet = null;
+ } elseif (is_object($value)) {
+ $this->$key = clone $value;
+ } else {
+ $this->$key = $value;
+ }
+ }
+ }
+
+ public function setHyperlink(?Hyperlink $pHyperlink = null): void
+ {
+ $this->hyperlink = $pHyperlink;
+ }
+
+ /**
+ * @return null|Hyperlink
+ */
+ public function getHyperlink()
+ {
+ return $this->hyperlink;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php
new file mode 100644
index 0000000..31f6ae6
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php
@@ -0,0 +1,54 @@
+worksheet = null;
+ }
+
+ /**
+ * Get loop only existing cells.
+ */
+ public function getIterateOnlyExistingCells(): bool
+ {
+ return $this->onlyExistingCells;
+ }
+
+ /**
+ * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary.
+ */
+ abstract protected function adjustForExistingOnlyRange();
+
+ /**
+ * Set the iterator to loop only existing cells.
+ */
+ public function setIterateOnlyExistingCells(bool $value): void
+ {
+ $this->onlyExistingCells = (bool) $value;
+
+ $this->adjustForExistingOnlyRange();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php
new file mode 100644
index 0000000..8abbabe
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php
@@ -0,0 +1,63 @@
+parent = $parent;
+ $this->columnIndex = $columnIndex;
+ }
+
+ /**
+ * Destructor.
+ */
+ public function __destruct()
+ {
+ // @phpstan-ignore-next-line
+ $this->parent = null;
+ }
+
+ /**
+ * Get column index as string eg: 'A'.
+ */
+ public function getColumnIndex(): string
+ {
+ return $this->columnIndex;
+ }
+
+ /**
+ * Get cell iterator.
+ *
+ * @param int $startRow The row number at which to start iterating
+ * @param int $endRow Optionally, the row number at which to stop iterating
+ *
+ * @return ColumnCellIterator
+ */
+ public function getCellIterator($startRow = 1, $endRow = null)
+ {
+ return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php
new file mode 100644
index 0000000..6577211
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php
@@ -0,0 +1,187 @@
+worksheet = $subject;
+ $this->columnIndex = Coordinate::columnIndexFromString($columnIndex);
+ $this->resetEnd($endRow);
+ $this->resetStart($startRow);
+ }
+
+ /**
+ * (Re)Set the start row and the current row pointer.
+ *
+ * @param int $startRow The row number at which to start iterating
+ *
+ * @return $this
+ */
+ public function resetStart(int $startRow = 1)
+ {
+ $this->startRow = $startRow;
+ $this->adjustForExistingOnlyRange();
+ $this->seek($startRow);
+
+ return $this;
+ }
+
+ /**
+ * (Re)Set the end row.
+ *
+ * @param int $endRow The row number at which to stop iterating
+ *
+ * @return $this
+ */
+ public function resetEnd($endRow = null)
+ {
+ $this->endRow = $endRow ?: $this->worksheet->getHighestRow();
+ $this->adjustForExistingOnlyRange();
+
+ return $this;
+ }
+
+ /**
+ * Set the row pointer to the selected row.
+ *
+ * @param int $row The row number to set the current pointer at
+ *
+ * @return $this
+ */
+ public function seek(int $row = 1)
+ {
+ if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $row))) {
+ throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist');
+ }
+ if (($row < $this->startRow) || ($row > $this->endRow)) {
+ throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})");
+ }
+ $this->currentRow = $row;
+
+ return $this;
+ }
+
+ /**
+ * Rewind the iterator to the starting row.
+ */
+ public function rewind(): void
+ {
+ $this->currentRow = $this->startRow;
+ }
+
+ /**
+ * Return the current cell in this worksheet column.
+ */
+ public function current(): ?Cell
+ {
+ return $this->worksheet->getCellByColumnAndRow($this->columnIndex, $this->currentRow);
+ }
+
+ /**
+ * Return the current iterator key.
+ */
+ public function key(): int
+ {
+ return $this->currentRow;
+ }
+
+ /**
+ * Set the iterator to its next value.
+ */
+ public function next(): void
+ {
+ do {
+ ++$this->currentRow;
+ } while (
+ ($this->onlyExistingCells) &&
+ (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) &&
+ ($this->currentRow <= $this->endRow)
+ );
+ }
+
+ /**
+ * Set the iterator to its previous value.
+ */
+ public function prev(): void
+ {
+ do {
+ --$this->currentRow;
+ } while (
+ ($this->onlyExistingCells) &&
+ (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) &&
+ ($this->currentRow >= $this->startRow)
+ );
+ }
+
+ /**
+ * Indicate if more rows exist in the worksheet range of rows that we're iterating.
+ */
+ public function valid(): bool
+ {
+ return $this->currentRow <= $this->endRow && $this->currentRow >= $this->startRow;
+ }
+
+ /**
+ * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary.
+ */
+ protected function adjustForExistingOnlyRange(): void
+ {
+ if ($this->onlyExistingCells) {
+ while (
+ (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->startRow)) &&
+ ($this->startRow <= $this->endRow)
+ ) {
+ ++$this->startRow;
+ }
+ while (
+ (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->endRow)) &&
+ ($this->endRow >= $this->startRow)
+ ) {
+ --$this->endRow;
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php
new file mode 100644
index 0000000..12b1efd
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php
@@ -0,0 +1,103 @@
+columnIndex = $pIndex;
+
+ // set dimension as unformatted by default
+ parent::__construct(0);
+ }
+
+ /**
+ * Get column index as string eg: 'A'.
+ */
+ public function getColumnIndex(): string
+ {
+ return $this->columnIndex;
+ }
+
+ /**
+ * Set column index as string eg: 'A'.
+ *
+ * @return $this
+ */
+ public function setColumnIndex(string $index)
+ {
+ $this->columnIndex = $index;
+
+ return $this;
+ }
+
+ /**
+ * Get Width.
+ */
+ public function getWidth(): float
+ {
+ return $this->width;
+ }
+
+ /**
+ * Set Width.
+ *
+ * @return $this
+ */
+ public function setWidth(float $width)
+ {
+ $this->width = $width;
+
+ return $this;
+ }
+
+ /**
+ * Get Auto Size.
+ */
+ public function getAutoSize(): bool
+ {
+ return $this->autoSize;
+ }
+
+ /**
+ * Set Auto Size.
+ *
+ * @return $this
+ */
+ public function setAutoSize(bool $autosizeEnabled)
+ {
+ $this->autoSize = $autosizeEnabled;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnIterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnIterator.php
new file mode 100644
index 0000000..0651a7a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnIterator.php
@@ -0,0 +1,173 @@
+worksheet = $worksheet;
+ $this->resetEnd($endColumn);
+ $this->resetStart($startColumn);
+ }
+
+ /**
+ * Destructor.
+ */
+ public function __destruct()
+ {
+ // @phpstan-ignore-next-line
+ $this->worksheet = null;
+ }
+
+ /**
+ * (Re)Set the start column and the current column pointer.
+ *
+ * @param string $startColumn The column address at which to start iterating
+ *
+ * @return $this
+ */
+ public function resetStart(string $startColumn = 'A')
+ {
+ $startColumnIndex = Coordinate::columnIndexFromString($startColumn);
+ if ($startColumnIndex > Coordinate::columnIndexFromString($this->worksheet->getHighestColumn())) {
+ throw new Exception(
+ "Start column ({$startColumn}) is beyond highest column ({$this->worksheet->getHighestColumn()})"
+ );
+ }
+
+ $this->startColumnIndex = $startColumnIndex;
+ if ($this->endColumnIndex < $this->startColumnIndex) {
+ $this->endColumnIndex = $this->startColumnIndex;
+ }
+ $this->seek($startColumn);
+
+ return $this;
+ }
+
+ /**
+ * (Re)Set the end column.
+ *
+ * @param string $endColumn The column address at which to stop iterating
+ *
+ * @return $this
+ */
+ public function resetEnd($endColumn = null)
+ {
+ $endColumn = $endColumn ?: $this->worksheet->getHighestColumn();
+ $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn);
+
+ return $this;
+ }
+
+ /**
+ * Set the column pointer to the selected column.
+ *
+ * @param string $column The column address to set the current pointer at
+ *
+ * @return $this
+ */
+ public function seek(string $column = 'A')
+ {
+ $column = Coordinate::columnIndexFromString($column);
+ if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) {
+ throw new PhpSpreadsheetException(
+ "Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"
+ );
+ }
+ $this->currentColumnIndex = $column;
+
+ return $this;
+ }
+
+ /**
+ * Rewind the iterator to the starting column.
+ */
+ public function rewind(): void
+ {
+ $this->currentColumnIndex = $this->startColumnIndex;
+ }
+
+ /**
+ * Return the current column in this worksheet.
+ *
+ * @return Column
+ */
+ public function current()
+ {
+ return new Column($this->worksheet, Coordinate::stringFromColumnIndex($this->currentColumnIndex));
+ }
+
+ /**
+ * Return the current iterator key.
+ */
+ public function key(): string
+ {
+ return Coordinate::stringFromColumnIndex($this->currentColumnIndex);
+ }
+
+ /**
+ * Set the iterator to its next value.
+ */
+ public function next(): void
+ {
+ ++$this->currentColumnIndex;
+ }
+
+ /**
+ * Set the iterator to its previous value.
+ */
+ public function prev(): void
+ {
+ --$this->currentColumnIndex;
+ }
+
+ /**
+ * Indicate if more columns exist in the worksheet range of columns that we're iterating.
+ */
+ public function valid(): bool
+ {
+ return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php
new file mode 100644
index 0000000..4b3a0da
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php
@@ -0,0 +1,149 @@
+xfIndex = $initialValue;
+ }
+
+ /**
+ * Get Visible.
+ */
+ public function getVisible(): bool
+ {
+ return $this->visible;
+ }
+
+ /**
+ * Set Visible.
+ *
+ * @return $this
+ */
+ public function setVisible(bool $visible)
+ {
+ $this->visible = $visible;
+
+ return $this;
+ }
+
+ /**
+ * Get Outline Level.
+ */
+ public function getOutlineLevel(): int
+ {
+ return $this->outlineLevel;
+ }
+
+ /**
+ * Set Outline Level.
+ * Value must be between 0 and 7.
+ *
+ * @return $this
+ */
+ public function setOutlineLevel(int $level)
+ {
+ if ($level < 0 || $level > 7) {
+ throw new PhpSpreadsheetException('Outline level must range between 0 and 7.');
+ }
+
+ $this->outlineLevel = $level;
+
+ return $this;
+ }
+
+ /**
+ * Get Collapsed.
+ */
+ public function getCollapsed(): bool
+ {
+ return $this->collapsed;
+ }
+
+ /**
+ * Set Collapsed.
+ *
+ * @return $this
+ */
+ public function setCollapsed(bool $collapsed)
+ {
+ $this->collapsed = $collapsed;
+
+ return $this;
+ }
+
+ /**
+ * Get index to cellXf.
+ *
+ * @return int
+ */
+ public function getXfIndex(): ?int
+ {
+ return $this->xfIndex;
+ }
+
+ /**
+ * Set index to cellXf.
+ *
+ * @return $this
+ */
+ public function setXfIndex(int $pValue)
+ {
+ $this->xfIndex = $pValue;
+
+ 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/Worksheet/Drawing.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php
new file mode 100644
index 0000000..1f1dae9
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php
@@ -0,0 +1,114 @@
+path = '';
+
+ // Initialize parent
+ parent::__construct();
+ }
+
+ /**
+ * Get Filename.
+ *
+ * @return string
+ */
+ public function getFilename()
+ {
+ return basename($this->path);
+ }
+
+ /**
+ * Get indexed filename (using image index).
+ *
+ * @return string
+ */
+ public function getIndexedFilename()
+ {
+ $fileName = $this->getFilename();
+ $fileName = str_replace(' ', '_', $fileName);
+
+ return str_replace('.' . $this->getExtension(), '', $fileName) . $this->getImageIndex() . '.' . $this->getExtension();
+ }
+
+ /**
+ * Get Extension.
+ *
+ * @return string
+ */
+ public function getExtension()
+ {
+ $exploded = explode('.', basename($this->path));
+
+ return $exploded[count($exploded) - 1];
+ }
+
+ /**
+ * Get Path.
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Set Path.
+ *
+ * @param string $pValue File path
+ * @param bool $pVerifyFile Verify file
+ *
+ * @return $this
+ */
+ public function setPath($pValue, $pVerifyFile = true)
+ {
+ if ($pVerifyFile) {
+ if (file_exists($pValue)) {
+ $this->path = $pValue;
+
+ if ($this->width == 0 && $this->height == 0) {
+ // Get width/height
+ [$this->width, $this->height] = getimagesize($pValue);
+ }
+ } else {
+ throw new PhpSpreadsheetException("File $pValue not found!");
+ }
+ } else {
+ $this->path = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ return md5(
+ $this->path .
+ parent::getHashCode() .
+ __CLASS__
+ );
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing/Shadow.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing/Shadow.php
new file mode 100644
index 0000000..0557c83
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing/Shadow.php
@@ -0,0 +1,289 @@
+visible = false;
+ $this->blurRadius = 6;
+ $this->distance = 2;
+ $this->direction = 0;
+ $this->alignment = self::SHADOW_BOTTOM_RIGHT;
+ $this->color = new Color(Color::COLOR_BLACK);
+ $this->alpha = 50;
+ }
+
+ /**
+ * Get Visible.
+ *
+ * @return bool
+ */
+ public function getVisible()
+ {
+ return $this->visible;
+ }
+
+ /**
+ * Set Visible.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setVisible($pValue)
+ {
+ $this->visible = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Blur radius.
+ *
+ * @return int
+ */
+ public function getBlurRadius()
+ {
+ return $this->blurRadius;
+ }
+
+ /**
+ * Set Blur radius.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setBlurRadius($pValue)
+ {
+ $this->blurRadius = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Shadow distance.
+ *
+ * @return int
+ */
+ public function getDistance()
+ {
+ return $this->distance;
+ }
+
+ /**
+ * Set Shadow distance.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setDistance($pValue)
+ {
+ $this->distance = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Shadow direction (in degrees).
+ *
+ * @return int
+ */
+ public function getDirection()
+ {
+ return $this->direction;
+ }
+
+ /**
+ * Set Shadow direction (in degrees).
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setDirection($pValue)
+ {
+ $this->direction = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Shadow alignment.
+ *
+ * @return string
+ */
+ public function getAlignment()
+ {
+ return $this->alignment;
+ }
+
+ /**
+ * Set Shadow alignment.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setAlignment($pValue)
+ {
+ $this->alignment = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Color.
+ *
+ * @return Color
+ */
+ public function getColor()
+ {
+ return $this->color;
+ }
+
+ /**
+ * Set Color.
+ *
+ * @param Color $pValue
+ *
+ * @return $this
+ */
+ public function setColor(?Color $pValue = null)
+ {
+ $this->color = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Alpha.
+ *
+ * @return int
+ */
+ public function getAlpha()
+ {
+ return $this->alpha;
+ }
+
+ /**
+ * Set Alpha.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setAlpha($pValue)
+ {
+ $this->alpha = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ return md5(
+ ($this->visible ? 't' : 'f') .
+ $this->blurRadius .
+ $this->distance .
+ $this->direction .
+ $this->alignment .
+ $this->color->getHashCode() .
+ $this->alpha .
+ __CLASS__
+ );
+ }
+
+ /**
+ * 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/HeaderFooter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php
new file mode 100644
index 0000000..cc37e7f
--- /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 $pValue
+ *
+ * @return $this
+ */
+ public function setOddHeader($pValue)
+ {
+ $this->oddHeader = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get OddFooter.
+ *
+ * @return string
+ */
+ public function getOddFooter()
+ {
+ return $this->oddFooter;
+ }
+
+ /**
+ * Set OddFooter.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setOddFooter($pValue)
+ {
+ $this->oddFooter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get EvenHeader.
+ *
+ * @return string
+ */
+ public function getEvenHeader()
+ {
+ return $this->evenHeader;
+ }
+
+ /**
+ * Set EvenHeader.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setEvenHeader($pValue)
+ {
+ $this->evenHeader = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get EvenFooter.
+ *
+ * @return string
+ */
+ public function getEvenFooter()
+ {
+ return $this->evenFooter;
+ }
+
+ /**
+ * Set EvenFooter.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setEvenFooter($pValue)
+ {
+ $this->evenFooter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get FirstHeader.
+ *
+ * @return string
+ */
+ public function getFirstHeader()
+ {
+ return $this->firstHeader;
+ }
+
+ /**
+ * Set FirstHeader.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setFirstHeader($pValue)
+ {
+ $this->firstHeader = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get FirstFooter.
+ *
+ * @return string
+ */
+ public function getFirstFooter()
+ {
+ return $this->firstFooter;
+ }
+
+ /**
+ * Set FirstFooter.
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setFirstFooter($pValue)
+ {
+ $this->firstFooter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get DifferentOddEven.
+ *
+ * @return bool
+ */
+ public function getDifferentOddEven()
+ {
+ return $this->differentOddEven;
+ }
+
+ /**
+ * Set DifferentOddEven.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setDifferentOddEven($pValue)
+ {
+ $this->differentOddEven = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get DifferentFirst.
+ *
+ * @return bool
+ */
+ public function getDifferentFirst()
+ {
+ return $this->differentFirst;
+ }
+
+ /**
+ * Set DifferentFirst.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setDifferentFirst($pValue)
+ {
+ $this->differentFirst = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get ScaleWithDocument.
+ *
+ * @return bool
+ */
+ public function getScaleWithDocument()
+ {
+ return $this->scaleWithDocument;
+ }
+
+ /**
+ * Set ScaleWithDocument.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setScaleWithDocument($pValue)
+ {
+ $this->scaleWithDocument = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get AlignWithMargins.
+ *
+ * @return bool
+ */
+ public function getAlignWithMargins()
+ {
+ return $this->alignWithMargins;
+ }
+
+ /**
+ * Set AlignWithMargins.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setAlignWithMargins($pValue)
+ {
+ $this->alignWithMargins = $pValue;
+
+ 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..134b619
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php
@@ -0,0 +1,73 @@
+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?
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ 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..7789543
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php
@@ -0,0 +1,232 @@
+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 ($rgb === false) {
+ 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).
+ *
+ * @return string
+ */
+ public function getIndexedFilename()
+ {
+ $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..a829793
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php
@@ -0,0 +1,244 @@
+left;
+ }
+
+ /**
+ * Set Left.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setLeft($pValue)
+ {
+ $this->left = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Right.
+ *
+ * @return float
+ */
+ public function getRight()
+ {
+ return $this->right;
+ }
+
+ /**
+ * Set Right.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setRight($pValue)
+ {
+ $this->right = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Top.
+ *
+ * @return float
+ */
+ public function getTop()
+ {
+ return $this->top;
+ }
+
+ /**
+ * Set Top.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setTop($pValue)
+ {
+ $this->top = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Bottom.
+ *
+ * @return float
+ */
+ public function getBottom()
+ {
+ return $this->bottom;
+ }
+
+ /**
+ * Set Bottom.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setBottom($pValue)
+ {
+ $this->bottom = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Header.
+ *
+ * @return float
+ */
+ public function getHeader()
+ {
+ return $this->header;
+ }
+
+ /**
+ * Set Header.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setHeader($pValue)
+ {
+ $this->header = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Footer.
+ *
+ * @return float
+ */
+ public function getFooter()
+ {
+ return $this->footer;
+ }
+
+ /**
+ * Set Footer.
+ *
+ * @param float $pValue
+ *
+ * @return $this
+ */
+ public function setFooter($pValue)
+ {
+ $this->footer = $pValue;
+
+ 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..7f3f71d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php
@@ -0,0 +1,857 @@
+
+ * 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.
+ *
+ * @var int
+ */
+ private $paperSize = self::PAPERSIZE_LETTER;
+
+ /**
+ * Orientation.
+ *
+ * @var string
+ */
+ private $orientation = self::ORIENTATION_DEFAULT;
+
+ /**
+ * 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()
+ {
+ }
+
+ /**
+ * Get Paper Size.
+ *
+ * @return int
+ */
+ public function getPaperSize()
+ {
+ return $this->paperSize;
+ }
+
+ /**
+ * Set Paper Size.
+ *
+ * @param int $pValue see self::PAPERSIZE_*
+ *
+ * @return $this
+ */
+ public function setPaperSize($pValue)
+ {
+ $this->paperSize = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Orientation.
+ *
+ * @return string
+ */
+ public function getOrientation()
+ {
+ return $this->orientation;
+ }
+
+ /**
+ * Set Orientation.
+ *
+ * @param string $pValue see self::ORIENTATION_*
+ *
+ * @return $this
+ */
+ public function setOrientation($pValue)
+ {
+ $this->orientation = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * 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 $pValue
+ * @param bool $pUpdate Update fitToPage so scaling applies rather than fitToHeight / fitToWidth
+ *
+ * @return $this
+ */
+ public function setScale($pValue, $pUpdate = 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 (($pValue >= 0) || $pValue === null) {
+ $this->scale = $pValue;
+ if ($pUpdate) {
+ $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 $pValue
+ *
+ * @return $this
+ */
+ public function setFitToPage($pValue)
+ {
+ $this->fitToPage = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Fit To Height.
+ *
+ * @return null|int
+ */
+ public function getFitToHeight()
+ {
+ return $this->fitToHeight;
+ }
+
+ /**
+ * Set Fit To Height.
+ *
+ * @param null|int $pValue
+ * @param bool $pUpdate Update fitToPage so it applies rather than scaling
+ *
+ * @return $this
+ */
+ public function setFitToHeight($pValue, $pUpdate = true)
+ {
+ $this->fitToHeight = $pValue;
+ if ($pUpdate) {
+ $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 $pValue
+ * @param bool $pUpdate Update fitToPage so it applies rather than scaling
+ *
+ * @return $this
+ */
+ public function setFitToWidth($pValue, $pUpdate = true)
+ {
+ $this->fitToWidth = $pValue;
+ if ($pUpdate) {
+ $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 $pValue Containing start column and end column, empty array if option unset
+ *
+ * @return $this
+ */
+ public function setColumnsToRepeatAtLeft(array $pValue)
+ {
+ $this->columnsToRepeatAtLeft = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Set Columns to repeat at left by start and end.
+ *
+ * @param string $pStart eg: 'A'
+ * @param string $pEnd eg: 'B'
+ *
+ * @return $this
+ */
+ public function setColumnsToRepeatAtLeftByStartAndEnd($pStart, $pEnd)
+ {
+ $this->columnsToRepeatAtLeft = [$pStart, $pEnd];
+
+ 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 $pValue Containing start column and end column, empty array if option unset
+ *
+ * @return $this
+ */
+ public function setRowsToRepeatAtTop(array $pValue)
+ {
+ $this->rowsToRepeatAtTop = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Set Rows to repeat at top by start and end.
+ *
+ * @param int $pStart eg: 1
+ * @param int $pEnd eg: 1
+ *
+ * @return $this
+ */
+ public function setRowsToRepeatAtTopByStartAndEnd($pStart, $pEnd)
+ {
+ $this->rowsToRepeatAtTop = [$pStart, $pEnd];
+
+ 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/Worksheet/Protection.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Protection.php
new file mode 100644
index 0000000..ba3af0a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Protection.php
@@ -0,0 +1,691 @@
+sheet ||
+ $this->objects ||
+ $this->scenarios ||
+ $this->formatCells ||
+ $this->formatColumns ||
+ $this->formatRows ||
+ $this->insertColumns ||
+ $this->insertRows ||
+ $this->insertHyperlinks ||
+ $this->deleteColumns ||
+ $this->deleteRows ||
+ $this->selectLockedCells ||
+ $this->sort ||
+ $this->autoFilter ||
+ $this->pivotTables ||
+ $this->selectUnlockedCells;
+ }
+
+ /**
+ * Get Sheet.
+ *
+ * @return bool
+ */
+ public function getSheet()
+ {
+ return $this->sheet;
+ }
+
+ /**
+ * Set Sheet.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setSheet($pValue)
+ {
+ $this->sheet = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Objects.
+ *
+ * @return bool
+ */
+ public function getObjects()
+ {
+ return $this->objects;
+ }
+
+ /**
+ * Set Objects.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setObjects($pValue)
+ {
+ $this->objects = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Scenarios.
+ *
+ * @return bool
+ */
+ public function getScenarios()
+ {
+ return $this->scenarios;
+ }
+
+ /**
+ * Set Scenarios.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setScenarios($pValue)
+ {
+ $this->scenarios = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get FormatCells.
+ *
+ * @return bool
+ */
+ public function getFormatCells()
+ {
+ return $this->formatCells;
+ }
+
+ /**
+ * Set FormatCells.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setFormatCells($pValue)
+ {
+ $this->formatCells = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get FormatColumns.
+ *
+ * @return bool
+ */
+ public function getFormatColumns()
+ {
+ return $this->formatColumns;
+ }
+
+ /**
+ * Set FormatColumns.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setFormatColumns($pValue)
+ {
+ $this->formatColumns = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get FormatRows.
+ *
+ * @return bool
+ */
+ public function getFormatRows()
+ {
+ return $this->formatRows;
+ }
+
+ /**
+ * Set FormatRows.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setFormatRows($pValue)
+ {
+ $this->formatRows = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get InsertColumns.
+ *
+ * @return bool
+ */
+ public function getInsertColumns()
+ {
+ return $this->insertColumns;
+ }
+
+ /**
+ * Set InsertColumns.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setInsertColumns($pValue)
+ {
+ $this->insertColumns = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get InsertRows.
+ *
+ * @return bool
+ */
+ public function getInsertRows()
+ {
+ return $this->insertRows;
+ }
+
+ /**
+ * Set InsertRows.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setInsertRows($pValue)
+ {
+ $this->insertRows = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get InsertHyperlinks.
+ *
+ * @return bool
+ */
+ public function getInsertHyperlinks()
+ {
+ return $this->insertHyperlinks;
+ }
+
+ /**
+ * Set InsertHyperlinks.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setInsertHyperlinks($pValue)
+ {
+ $this->insertHyperlinks = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get DeleteColumns.
+ *
+ * @return bool
+ */
+ public function getDeleteColumns()
+ {
+ return $this->deleteColumns;
+ }
+
+ /**
+ * Set DeleteColumns.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setDeleteColumns($pValue)
+ {
+ $this->deleteColumns = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get DeleteRows.
+ *
+ * @return bool
+ */
+ public function getDeleteRows()
+ {
+ return $this->deleteRows;
+ }
+
+ /**
+ * Set DeleteRows.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setDeleteRows($pValue)
+ {
+ $this->deleteRows = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get SelectLockedCells.
+ *
+ * @return bool
+ */
+ public function getSelectLockedCells()
+ {
+ return $this->selectLockedCells;
+ }
+
+ /**
+ * Set SelectLockedCells.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setSelectLockedCells($pValue)
+ {
+ $this->selectLockedCells = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Sort.
+ *
+ * @return bool
+ */
+ public function getSort()
+ {
+ return $this->sort;
+ }
+
+ /**
+ * Set Sort.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setSort($pValue)
+ {
+ $this->sort = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get AutoFilter.
+ *
+ * @return bool
+ */
+ public function getAutoFilter()
+ {
+ return $this->autoFilter;
+ }
+
+ /**
+ * Set AutoFilter.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setAutoFilter($pValue)
+ {
+ $this->autoFilter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get PivotTables.
+ *
+ * @return bool
+ */
+ public function getPivotTables()
+ {
+ return $this->pivotTables;
+ }
+
+ /**
+ * Set PivotTables.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setPivotTables($pValue)
+ {
+ $this->pivotTables = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get SelectUnlockedCells.
+ *
+ * @return bool
+ */
+ public function getSelectUnlockedCells()
+ {
+ return $this->selectUnlockedCells;
+ }
+
+ /**
+ * Set SelectUnlockedCells.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setSelectUnlockedCells($pValue)
+ {
+ $this->selectUnlockedCells = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get hashed password.
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Set Password.
+ *
+ * @param string $pValue
+ * @param bool $pAlreadyHashed If the password has already been hashed, set this to true
+ *
+ * @return $this
+ */
+ public function setPassword($pValue, $pAlreadyHashed = false)
+ {
+ if (!$pAlreadyHashed) {
+ $salt = $this->generateSalt();
+ $this->setSalt($salt);
+ $pValue = PasswordHasher::hashPassword($pValue, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
+ }
+
+ $this->password = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Create a pseudorandom string.
+ */
+ private function generateSalt(): string
+ {
+ return base64_encode(random_bytes(16));
+ }
+
+ /**
+ * Get algorithm name.
+ */
+ public function getAlgorithm(): string
+ {
+ return $this->algorithm;
+ }
+
+ /**
+ * Set algorithm name.
+ */
+ public function setAlgorithm(string $algorithm): void
+ {
+ $this->algorithm = $algorithm;
+ }
+
+ /**
+ * Get salt value.
+ */
+ public function getSalt(): string
+ {
+ return $this->salt;
+ }
+
+ /**
+ * Set salt value.
+ */
+ public function setSalt(string $salt): void
+ {
+ $this->salt = $salt;
+ }
+
+ /**
+ * Get spin count.
+ */
+ public function getSpinCount(): int
+ {
+ return $this->spinCount;
+ }
+
+ /**
+ * Set spin count.
+ */
+ public function setSpinCount(int $spinCount): void
+ {
+ $this->spinCount = $spinCount;
+ }
+
+ /**
+ * Verify that the given non-hashed password can "unlock" the protection.
+ */
+ public function verify(string $password): bool
+ {
+ if (!$this->isProtectionEnabled()) {
+ return true;
+ }
+
+ $hash = PasswordHasher::hashPassword($password, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
+
+ return $this->getPassword() === $hash;
+ }
+
+ /**
+ * 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/Row.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php
new file mode 100644
index 0000000..01053c3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php
@@ -0,0 +1,71 @@
+worksheet = $worksheet;
+ $this->rowIndex = $rowIndex;
+ }
+
+ /**
+ * Destructor.
+ */
+ public function __destruct()
+ {
+ // @phpstan-ignore-next-line
+ $this->worksheet = null;
+ }
+
+ /**
+ * Get row index.
+ */
+ public function getRowIndex(): int
+ {
+ return $this->rowIndex;
+ }
+
+ /**
+ * Get cell iterator.
+ *
+ * @param string $startColumn The column address at which to start iterating
+ * @param string $endColumn Optionally, the column address at which to stop iterating
+ *
+ * @return RowCellIterator
+ */
+ public function getCellIterator($startColumn = 'A', $endColumn = null)
+ {
+ return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn);
+ }
+
+ /**
+ * Returns bound worksheet.
+ */
+ public function getWorksheet(): Worksheet
+ {
+ return $this->worksheet;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php
new file mode 100644
index 0000000..6a96a82
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php
@@ -0,0 +1,186 @@
+worksheet = $worksheet;
+ $this->rowIndex = $rowIndex;
+ $this->resetEnd($endColumn);
+ $this->resetStart($startColumn);
+ }
+
+ /**
+ * (Re)Set the start column and the current column pointer.
+ *
+ * @param string $startColumn The column address at which to start iterating
+ *
+ * @return $this
+ */
+ public function resetStart(string $startColumn = 'A')
+ {
+ $this->startColumnIndex = Coordinate::columnIndexFromString($startColumn);
+ $this->adjustForExistingOnlyRange();
+ $this->seek(Coordinate::stringFromColumnIndex($this->startColumnIndex));
+
+ return $this;
+ }
+
+ /**
+ * (Re)Set the end column.
+ *
+ * @param string $endColumn The column address at which to stop iterating
+ *
+ * @return $this
+ */
+ public function resetEnd($endColumn = null)
+ {
+ $endColumn = $endColumn ?: $this->worksheet->getHighestColumn();
+ $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn);
+ $this->adjustForExistingOnlyRange();
+
+ return $this;
+ }
+
+ /**
+ * Set the column pointer to the selected column.
+ *
+ * @param string $column The column address to set the current pointer at
+ *
+ * @return $this
+ */
+ public function seek(string $column = 'A')
+ {
+ $columnx = $column;
+ $column = Coordinate::columnIndexFromString($column);
+ if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) {
+ throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist');
+ }
+ if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) {
+ throw new PhpSpreadsheetException("Column $columnx is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})");
+ }
+ $this->currentColumnIndex = $column;
+
+ return $this;
+ }
+
+ /**
+ * Rewind the iterator to the starting column.
+ */
+ public function rewind(): void
+ {
+ $this->currentColumnIndex = $this->startColumnIndex;
+ }
+
+ /**
+ * Return the current cell in this worksheet row.
+ */
+ public function current(): ?Cell
+ {
+ return $this->worksheet->getCellByColumnAndRow($this->currentColumnIndex, $this->rowIndex);
+ }
+
+ /**
+ * Return the current iterator key.
+ */
+ public function key(): string
+ {
+ return Coordinate::stringFromColumnIndex($this->currentColumnIndex);
+ }
+
+ /**
+ * Set the iterator to its next value.
+ */
+ public function next(): void
+ {
+ do {
+ ++$this->currentColumnIndex;
+ } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex));
+ }
+
+ /**
+ * Set the iterator to its previous value.
+ */
+ public function prev(): void
+ {
+ do {
+ --$this->currentColumnIndex;
+ } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex));
+ }
+
+ /**
+ * Indicate if more columns exist in the worksheet range of columns that we're iterating.
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex;
+ }
+
+ /**
+ * Return the current iterator position.
+ */
+ public function getCurrentColumnIndex(): int
+ {
+ return $this->currentColumnIndex;
+ }
+
+ /**
+ * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary.
+ */
+ protected function adjustForExistingOnlyRange(): void
+ {
+ if ($this->onlyExistingCells) {
+ while ((!$this->worksheet->cellExistsByColumnAndRow($this->startColumnIndex, $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) {
+ ++$this->startColumnIndex;
+ }
+ while ((!$this->worksheet->cellExistsByColumnAndRow($this->endColumnIndex, $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) {
+ --$this->endColumnIndex;
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php
new file mode 100644
index 0000000..d86dd80
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php
@@ -0,0 +1,107 @@
+rowIndex = $pIndex;
+
+ // set dimension as unformatted by default
+ parent::__construct(null);
+ }
+
+ /**
+ * Get Row Index.
+ */
+ public function getRowIndex(): int
+ {
+ return $this->rowIndex;
+ }
+
+ /**
+ * Set Row Index.
+ *
+ * @return $this
+ */
+ public function setRowIndex(int $index)
+ {
+ $this->rowIndex = $index;
+
+ return $this;
+ }
+
+ /**
+ * Get Row Height.
+ *
+ * @return float
+ */
+ public function getRowHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Set Row Height.
+ *
+ * @param float $height
+ *
+ * @return $this
+ */
+ public function setRowHeight($height)
+ {
+ $this->height = $height;
+
+ return $this;
+ }
+
+ /**
+ * Get ZeroHeight.
+ */
+ public function getZeroHeight(): bool
+ {
+ return $this->zeroHeight;
+ }
+
+ /**
+ * Set ZeroHeight.
+ *
+ * @return $this
+ */
+ public function setZeroHeight(bool $pValue)
+ {
+ $this->zeroHeight = $pValue;
+
+ return $this;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowIterator.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowIterator.php
new file mode 100644
index 0000000..7d49f1a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowIterator.php
@@ -0,0 +1,157 @@
+subject = $subject;
+ $this->resetEnd($endRow);
+ $this->resetStart($startRow);
+ }
+
+ /**
+ * (Re)Set the start row and the current row pointer.
+ *
+ * @param int $startRow The row number at which to start iterating
+ *
+ * @return $this
+ */
+ public function resetStart(int $startRow = 1)
+ {
+ if ($startRow > $this->subject->getHighestRow()) {
+ throw new PhpSpreadsheetException(
+ "Start row ({$startRow}) is beyond highest row ({$this->subject->getHighestRow()})"
+ );
+ }
+
+ $this->startRow = $startRow;
+ if ($this->endRow < $this->startRow) {
+ $this->endRow = $this->startRow;
+ }
+ $this->seek($startRow);
+
+ return $this;
+ }
+
+ /**
+ * (Re)Set the end row.
+ *
+ * @param int $endRow The row number at which to stop iterating
+ *
+ * @return $this
+ */
+ public function resetEnd($endRow = null)
+ {
+ $this->endRow = $endRow ?: $this->subject->getHighestRow();
+
+ return $this;
+ }
+
+ /**
+ * Set the row pointer to the selected row.
+ *
+ * @param int $row The row number to set the current pointer at
+ *
+ * @return $this
+ */
+ public function seek(int $row = 1)
+ {
+ if (($row < $this->startRow) || ($row > $this->endRow)) {
+ throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})");
+ }
+ $this->position = $row;
+
+ return $this;
+ }
+
+ /**
+ * Rewind the iterator to the starting row.
+ */
+ public function rewind(): void
+ {
+ $this->position = $this->startRow;
+ }
+
+ /**
+ * Return the current row in this worksheet.
+ *
+ * @return Row
+ */
+ public function current()
+ {
+ return new Row($this->subject, $this->position);
+ }
+
+ /**
+ * Return the current iterator key.
+ */
+ public function key(): int
+ {
+ return $this->position;
+ }
+
+ /**
+ * Set the iterator to its next value.
+ */
+ public function next(): void
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Set the iterator to its previous value.
+ */
+ public function prev(): void
+ {
+ --$this->position;
+ }
+
+ /**
+ * Indicate if more rows exist in the worksheet range of rows that we're iterating.
+ */
+ public function valid(): bool
+ {
+ return $this->position <= $this->endRow && $this->position >= $this->startRow;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php
new file mode 100644
index 0000000..2f7d381
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php
@@ -0,0 +1,193 @@
+zoomScale;
+ }
+
+ /**
+ * Set ZoomScale.
+ * Valid values range from 10 to 400.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setZoomScale($pValue)
+ {
+ // 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 >= 1
+ if (($pValue >= 1) || $pValue === null) {
+ $this->zoomScale = $pValue;
+ } else {
+ throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get ZoomScaleNormal.
+ *
+ * @return int
+ */
+ public function getZoomScaleNormal()
+ {
+ return $this->zoomScaleNormal;
+ }
+
+ /**
+ * Set ZoomScale.
+ * Valid values range from 10 to 400.
+ *
+ * @param int $pValue
+ *
+ * @return $this
+ */
+ public function setZoomScaleNormal($pValue)
+ {
+ if (($pValue >= 1) || $pValue === null) {
+ $this->zoomScaleNormal = $pValue;
+ } else {
+ throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set ShowZeroes setting.
+ *
+ * @param bool $pValue
+ */
+ public function setShowZeros($pValue): void
+ {
+ $this->showZeros = $pValue;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getShowZeros()
+ {
+ return $this->showZeros;
+ }
+
+ /**
+ * Get View.
+ *
+ * @return string
+ */
+ public function getView()
+ {
+ return $this->sheetviewType;
+ }
+
+ /**
+ * Set View.
+ *
+ * Valid values are
+ * 'normal' self::SHEETVIEW_NORMAL
+ * 'pageLayout' self::SHEETVIEW_PAGE_LAYOUT
+ * 'pageBreakPreview' self::SHEETVIEW_PAGE_BREAK_PREVIEW
+ *
+ * @param string $pValue
+ *
+ * @return $this
+ */
+ public function setView($pValue)
+ {
+ // MS Excel 2007 allows setting the view to 'normal', 'pageLayout' or 'pageBreakPreview' via the user interface
+ if ($pValue === null) {
+ $pValue = self::SHEETVIEW_NORMAL;
+ }
+ if (in_array($pValue, self::$sheetViewTypes)) {
+ $this->sheetviewType = $pValue;
+ } else {
+ throw new PhpSpreadsheetException('Invalid sheetview layout type.');
+ }
+
+ 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/Worksheet/Worksheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php
new file mode 100644
index 0000000..0f26029
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php
@@ -0,0 +1,3047 @@
+
+ */
+ private $drawingCollection;
+
+ /**
+ * Collection of Chart objects.
+ *
+ * @var ArrayObject
+ */
+ private $chartCollection;
+
+ /**
+ * Worksheet title.
+ *
+ * @var string
+ */
+ private $title;
+
+ /**
+ * Sheet state.
+ *
+ * @var string
+ */
+ private $sheetState;
+
+ /**
+ * Page setup.
+ *
+ * @var PageSetup
+ */
+ private $pageSetup;
+
+ /**
+ * Page margins.
+ *
+ * @var PageMargins
+ */
+ private $pageMargins;
+
+ /**
+ * Page header/footer.
+ *
+ * @var HeaderFooter
+ */
+ private $headerFooter;
+
+ /**
+ * Sheet view.
+ *
+ * @var SheetView
+ */
+ private $sheetView;
+
+ /**
+ * Protection.
+ *
+ * @var Protection
+ */
+ private $protection;
+
+ /**
+ * Collection of styles.
+ *
+ * @var Style[]
+ */
+ private $styles = [];
+
+ /**
+ * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
+ *
+ * @var array
+ */
+ private $conditionalStylesCollection = [];
+
+ /**
+ * Is the current cell collection sorted already?
+ *
+ * @var bool
+ */
+ private $cellCollectionIsSorted = false;
+
+ /**
+ * Collection of breaks.
+ *
+ * @var int[]
+ */
+ private $breaks = [];
+
+ /**
+ * Collection of merged cell ranges.
+ *
+ * @var string[]
+ */
+ private $mergeCells = [];
+
+ /**
+ * Collection of protected cell ranges.
+ *
+ * @var string[]
+ */
+ private $protectedCells = [];
+
+ /**
+ * Autofilter Range and selection.
+ *
+ * @var AutoFilter
+ */
+ private $autoFilter;
+
+ /**
+ * Freeze pane.
+ *
+ * @var null|string
+ */
+ private $freezePane;
+
+ /**
+ * Default position of the right bottom pane.
+ *
+ * @var null|string
+ */
+ private $topLeftCell;
+
+ /**
+ * Show gridlines?
+ *
+ * @var bool
+ */
+ private $showGridlines = true;
+
+ /**
+ * Print gridlines?
+ *
+ * @var bool
+ */
+ private $printGridlines = false;
+
+ /**
+ * Show row and column headers?
+ *
+ * @var bool
+ */
+ private $showRowColHeaders = true;
+
+ /**
+ * Show summary below? (Row/Column outline).
+ *
+ * @var bool
+ */
+ private $showSummaryBelow = true;
+
+ /**
+ * Show summary right? (Row/Column outline).
+ *
+ * @var bool
+ */
+ private $showSummaryRight = true;
+
+ /**
+ * Collection of comments.
+ *
+ * @var Comment[]
+ */
+ private $comments = [];
+
+ /**
+ * Active cell. (Only one!).
+ *
+ * @var string
+ */
+ private $activeCell = 'A1';
+
+ /**
+ * Selected cells.
+ *
+ * @var string
+ */
+ private $selectedCells = 'A1';
+
+ /**
+ * Cached highest column.
+ *
+ * @var int
+ */
+ private $cachedHighestColumn = 1;
+
+ /**
+ * Cached highest row.
+ *
+ * @var int
+ */
+ private $cachedHighestRow = 1;
+
+ /**
+ * Right-to-left?
+ *
+ * @var bool
+ */
+ private $rightToLeft = false;
+
+ /**
+ * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
+ *
+ * @var array
+ */
+ private $hyperlinkCollection = [];
+
+ /**
+ * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
+ *
+ * @var array
+ */
+ private $dataValidationCollection = [];
+
+ /**
+ * Tab color.
+ *
+ * @var null|Color
+ */
+ private $tabColor;
+
+ /**
+ * Dirty flag.
+ *
+ * @var bool
+ */
+ private $dirty = true;
+
+ /**
+ * Hash.
+ *
+ * @var string
+ */
+ private $hash;
+
+ /**
+ * CodeName.
+ *
+ * @var string
+ */
+ private $codeName;
+
+ /**
+ * Create a new worksheet.
+ *
+ * @param Spreadsheet $parent
+ * @param string $pTitle
+ */
+ public function __construct(?Spreadsheet $parent = null, $pTitle = 'Worksheet')
+ {
+ // Set parent and title
+ $this->parent = $parent;
+ $this->setTitle($pTitle, false);
+ // setTitle can change $pTitle
+ $this->setCodeName($this->getTitle());
+ $this->setSheetState(self::SHEETSTATE_VISIBLE);
+
+ $this->cellCollection = CellsFactory::getInstance($this);
+ // Set page setup
+ $this->pageSetup = new PageSetup();
+ // Set page margins
+ $this->pageMargins = new PageMargins();
+ // Set page header/footer
+ $this->headerFooter = new HeaderFooter();
+ // Set sheet view
+ $this->sheetView = new SheetView();
+ // Drawing collection
+ $this->drawingCollection = new ArrayObject();
+ // Chart collection
+ $this->chartCollection = new ArrayObject();
+ // Protection
+ $this->protection = new Protection();
+ // Default row dimension
+ $this->defaultRowDimension = new RowDimension(null);
+ // Default column dimension
+ $this->defaultColumnDimension = new ColumnDimension(null);
+ $this->autoFilter = new AutoFilter(null, $this);
+ }
+
+ /**
+ * Disconnect all cells from this Worksheet object,
+ * typically so that the worksheet object can be unset.
+ */
+ public function disconnectCells(): void
+ {
+ if ($this->cellCollection !== null) {
+ $this->cellCollection->unsetWorksheetCells();
+ // @phpstan-ignore-next-line
+ $this->cellCollection = null;
+ }
+ // detach ourself from the workbook, so that it can then delete this worksheet successfully
+ // @phpstan-ignore-next-line
+ $this->parent = null;
+ }
+
+ /**
+ * Code to execute when this worksheet is unset().
+ */
+ public function __destruct()
+ {
+ Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
+
+ $this->disconnectCells();
+ $this->rowDimensions = [];
+ }
+
+ /**
+ * Return the cell collection.
+ *
+ * @return Cells
+ */
+ public function getCellCollection()
+ {
+ return $this->cellCollection;
+ }
+
+ /**
+ * Get array of invalid characters for sheet title.
+ *
+ * @return array
+ */
+ public static function getInvalidCharacters()
+ {
+ return self::$invalidCharacters;
+ }
+
+ /**
+ * Check sheet code name for valid Excel syntax.
+ *
+ * @param string $pValue The string to check
+ *
+ * @return string The valid string
+ */
+ private static function checkSheetCodeName($pValue)
+ {
+ $CharCount = Shared\StringHelper::countCharacters($pValue);
+ if ($CharCount == 0) {
+ throw new Exception('Sheet code name cannot be empty.');
+ }
+ // Some of the printable ASCII characters are invalid: * : / \ ? [ ] and first and last characters cannot be a "'"
+ if (
+ (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
+ (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
+ (Shared\StringHelper::substring($pValue, 0, 1) == '\'')
+ ) {
+ throw new Exception('Invalid character found in sheet code name');
+ }
+
+ // Enforce maximum characters allowed for sheet title
+ if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
+ throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
+ }
+
+ return $pValue;
+ }
+
+ /**
+ * Check sheet title for valid Excel syntax.
+ *
+ * @param string $pValue The string to check
+ *
+ * @return string The valid string
+ */
+ private static function checkSheetTitle($pValue)
+ {
+ // Some of the printable ASCII characters are invalid: * : / \ ? [ ]
+ if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) {
+ throw new Exception('Invalid character found in sheet title');
+ }
+
+ // Enforce maximum characters allowed for sheet title
+ if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
+ throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
+ }
+
+ return $pValue;
+ }
+
+ /**
+ * Get a sorted list of all cell coordinates currently held in the collection by row and column.
+ *
+ * @param bool $sorted Also sort the cell collection?
+ *
+ * @return string[]
+ */
+ public function getCoordinates($sorted = true)
+ {
+ if ($this->cellCollection == null) {
+ return [];
+ }
+
+ if ($sorted) {
+ return $this->cellCollection->getSortedCoordinates();
+ }
+
+ return $this->cellCollection->getCoordinates();
+ }
+
+ /**
+ * Get collection of row dimensions.
+ *
+ * @return RowDimension[]
+ */
+ public function getRowDimensions()
+ {
+ return $this->rowDimensions;
+ }
+
+ /**
+ * Get default row dimension.
+ *
+ * @return RowDimension
+ */
+ public function getDefaultRowDimension()
+ {
+ return $this->defaultRowDimension;
+ }
+
+ /**
+ * Get collection of column dimensions.
+ *
+ * @return ColumnDimension[]
+ */
+ public function getColumnDimensions()
+ {
+ return $this->columnDimensions;
+ }
+
+ /**
+ * Get default column dimension.
+ *
+ * @return ColumnDimension
+ */
+ public function getDefaultColumnDimension()
+ {
+ return $this->defaultColumnDimension;
+ }
+
+ /**
+ * Get collection of drawings.
+ *
+ * @return ArrayObject
+ */
+ public function getDrawingCollection()
+ {
+ return $this->drawingCollection;
+ }
+
+ /**
+ * Get collection of charts.
+ *
+ * @return ArrayObject
+ */
+ public function getChartCollection()
+ {
+ return $this->chartCollection;
+ }
+
+ /**
+ * Add chart.
+ *
+ * @param null|int $iChartIndex Index where chart should go (0,1,..., or null for last)
+ *
+ * @return Chart
+ */
+ public function addChart(Chart $pChart, $iChartIndex = null)
+ {
+ $pChart->setWorksheet($this);
+ if ($iChartIndex === null) {
+ $this->chartCollection[] = $pChart;
+ } else {
+ // Insert the chart at the requested index
+ array_splice($this->chartCollection, $iChartIndex, 0, [$pChart]);
+ }
+
+ return $pChart;
+ }
+
+ /**
+ * Return the count of charts on this worksheet.
+ *
+ * @return int The number of charts
+ */
+ public function getChartCount()
+ {
+ return count($this->chartCollection);
+ }
+
+ /**
+ * Get a chart by its index position.
+ *
+ * @param string $index Chart index position
+ *
+ * @return Chart|false
+ */
+ public function getChartByIndex($index)
+ {
+ $chartCount = count($this->chartCollection);
+ if ($chartCount == 0) {
+ return false;
+ }
+ if ($index === null) {
+ $index = --$chartCount;
+ }
+ if (!isset($this->chartCollection[$index])) {
+ return false;
+ }
+
+ return $this->chartCollection[$index];
+ }
+
+ /**
+ * Return an array of the names of charts on this worksheet.
+ *
+ * @return string[] The names of charts
+ */
+ public function getChartNames()
+ {
+ $chartNames = [];
+ foreach ($this->chartCollection as $chart) {
+ $chartNames[] = $chart->getName();
+ }
+
+ return $chartNames;
+ }
+
+ /**
+ * Get a chart by name.
+ *
+ * @param string $chartName Chart name
+ *
+ * @return Chart|false
+ */
+ public function getChartByName($chartName)
+ {
+ $chartCount = count($this->chartCollection);
+ if ($chartCount == 0) {
+ return false;
+ }
+ foreach ($this->chartCollection as $index => $chart) {
+ if ($chart->getName() == $chartName) {
+ return $this->chartCollection[$index];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Refresh column dimensions.
+ *
+ * @return $this
+ */
+ public function refreshColumnDimensions()
+ {
+ $currentColumnDimensions = $this->getColumnDimensions();
+ $newColumnDimensions = [];
+
+ foreach ($currentColumnDimensions as $objColumnDimension) {
+ $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
+ }
+
+ $this->columnDimensions = $newColumnDimensions;
+
+ return $this;
+ }
+
+ /**
+ * Refresh row dimensions.
+ *
+ * @return $this
+ */
+ public function refreshRowDimensions()
+ {
+ $currentRowDimensions = $this->getRowDimensions();
+ $newRowDimensions = [];
+
+ foreach ($currentRowDimensions as $objRowDimension) {
+ $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
+ }
+
+ $this->rowDimensions = $newRowDimensions;
+
+ return $this;
+ }
+
+ /**
+ * Calculate worksheet dimension.
+ *
+ * @return string String containing the dimension of this worksheet
+ */
+ public function calculateWorksheetDimension()
+ {
+ // Return
+ return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
+ }
+
+ /**
+ * Calculate worksheet data dimension.
+ *
+ * @return string String containing the dimension of this worksheet that actually contain data
+ */
+ public function calculateWorksheetDataDimension()
+ {
+ // Return
+ return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
+ }
+
+ /**
+ * Calculate widths for auto-size columns.
+ *
+ * @return $this
+ */
+ public function calculateColumnWidths()
+ {
+ // initialize $autoSizes array
+ $autoSizes = [];
+ foreach ($this->getColumnDimensions() as $colDimension) {
+ if ($colDimension->getAutoSize()) {
+ $autoSizes[$colDimension->getColumnIndex()] = -1;
+ }
+ }
+
+ // There is only something to do if there are some auto-size columns
+ if (!empty($autoSizes)) {
+ // build list of cells references that participate in a merge
+ $isMergeCell = [];
+ foreach ($this->getMergeCells() as $cells) {
+ foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
+ $isMergeCell[$cellReference] = true;
+ }
+ }
+
+ // loop through all cells in the worksheet
+ foreach ($this->getCoordinates(false) as $coordinate) {
+ $cell = $this->getCellOrNull($coordinate);
+ if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
+ //Determine if cell is in merge range
+ $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
+
+ //By default merged cells should be ignored
+ $isMergedButProceed = false;
+
+ //The only exception is if it's a merge range value cell of a 'vertical' randge (1 column wide)
+ if ($isMerged && $cell->isMergeRangeValueCell()) {
+ $range = $cell->getMergeRange();
+ $rangeBoundaries = Coordinate::rangeDimension($range);
+ if ($rangeBoundaries[0] == 1) {
+ $isMergedButProceed = true;
+ }
+ }
+
+ // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range
+ if (!$isMerged || $isMergedButProceed) {
+ // Calculated value
+ // To formatted string
+ $cellValue = NumberFormat::toFormattedString(
+ $cell->getCalculatedValue(),
+ $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode()
+ );
+
+ $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
+ (float) $autoSizes[$this->cellCollection->getCurrentColumn()],
+ (float) Shared\Font::calculateColumnWidth(
+ $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
+ $cellValue,
+ $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(),
+ $this->getParent()->getDefaultStyle()->getFont()
+ )
+ );
+ }
+ }
+ }
+
+ // adjust column widths
+ foreach ($autoSizes as $columnIndex => $width) {
+ if ($width == -1) {
+ $width = $this->getDefaultColumnDimension()->getWidth();
+ }
+ $this->getColumnDimension($columnIndex)->setWidth($width);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get parent.
+ *
+ * @return Spreadsheet
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Re-bind parent.
+ *
+ * @return $this
+ */
+ public function rebindParent(Spreadsheet $parent)
+ {
+ if ($this->parent !== null) {
+ $definedNames = $this->parent->getDefinedNames();
+ foreach ($definedNames as $definedName) {
+ $parent->addDefinedName($definedName);
+ }
+
+ $this->parent->removeSheetByIndex(
+ $this->parent->getIndex($this)
+ );
+ }
+ $this->parent = $parent;
+
+ return $this;
+ }
+
+ /**
+ * Get title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set title.
+ *
+ * @param string $title String containing the dimension of this worksheet
+ * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
+ * be updated to reflect the new sheet name.
+ * This should be left as the default true, unless you are
+ * certain that no formula cells on any worksheet contain
+ * references to this worksheet
+ * @param bool $validate False to skip validation of new title. WARNING: This should only be set
+ * at parse time (by Readers), where titles can be assumed to be valid.
+ *
+ * @return $this
+ */
+ public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
+ {
+ // Is this a 'rename' or not?
+ if ($this->getTitle() == $title) {
+ return $this;
+ }
+
+ // Old title
+ $oldTitle = $this->getTitle();
+
+ if ($validate) {
+ // Syntax check
+ self::checkSheetTitle($title);
+
+ if ($this->parent) {
+ // Is there already such sheet name?
+ if ($this->parent->sheetNameExists($title)) {
+ // Use name, but append with lowest possible integer
+
+ if (Shared\StringHelper::countCharacters($title) > 29) {
+ $title = Shared\StringHelper::substring($title, 0, 29);
+ }
+ $i = 1;
+ while ($this->parent->sheetNameExists($title . ' ' . $i)) {
+ ++$i;
+ if ($i == 10) {
+ if (Shared\StringHelper::countCharacters($title) > 28) {
+ $title = Shared\StringHelper::substring($title, 0, 28);
+ }
+ } elseif ($i == 100) {
+ if (Shared\StringHelper::countCharacters($title) > 27) {
+ $title = Shared\StringHelper::substring($title, 0, 27);
+ }
+ }
+ }
+
+ $title .= " $i";
+ }
+ }
+ }
+
+ // Set title
+ $this->title = $title;
+ $this->dirty = true;
+
+ if ($this->parent && $this->parent->getCalculationEngine()) {
+ // New title
+ $newTitle = $this->getTitle();
+ $this->parent->getCalculationEngine()
+ ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
+ if ($updateFormulaCellReferences) {
+ ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get sheet state.
+ *
+ * @return string Sheet state (visible, hidden, veryHidden)
+ */
+ public function getSheetState()
+ {
+ return $this->sheetState;
+ }
+
+ /**
+ * Set sheet state.
+ *
+ * @param string $value Sheet state (visible, hidden, veryHidden)
+ *
+ * @return $this
+ */
+ public function setSheetState($value)
+ {
+ $this->sheetState = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get page setup.
+ *
+ * @return PageSetup
+ */
+ public function getPageSetup()
+ {
+ return $this->pageSetup;
+ }
+
+ /**
+ * Set page setup.
+ *
+ * @return $this
+ */
+ public function setPageSetup(PageSetup $pValue)
+ {
+ $this->pageSetup = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get page margins.
+ *
+ * @return PageMargins
+ */
+ public function getPageMargins()
+ {
+ return $this->pageMargins;
+ }
+
+ /**
+ * Set page margins.
+ *
+ * @return $this
+ */
+ public function setPageMargins(PageMargins $pValue)
+ {
+ $this->pageMargins = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get page header/footer.
+ *
+ * @return HeaderFooter
+ */
+ public function getHeaderFooter()
+ {
+ return $this->headerFooter;
+ }
+
+ /**
+ * Set page header/footer.
+ *
+ * @return $this
+ */
+ public function setHeaderFooter(HeaderFooter $pValue)
+ {
+ $this->headerFooter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get sheet view.
+ *
+ * @return SheetView
+ */
+ public function getSheetView()
+ {
+ return $this->sheetView;
+ }
+
+ /**
+ * Set sheet view.
+ *
+ * @return $this
+ */
+ public function setSheetView(SheetView $pValue)
+ {
+ $this->sheetView = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Protection.
+ *
+ * @return Protection
+ */
+ public function getProtection()
+ {
+ return $this->protection;
+ }
+
+ /**
+ * Set Protection.
+ *
+ * @return $this
+ */
+ public function setProtection(Protection $pValue)
+ {
+ $this->protection = $pValue;
+ $this->dirty = true;
+
+ return $this;
+ }
+
+ /**
+ * Get highest worksheet column.
+ *
+ * @param string $row Return the data highest column for the specified row,
+ * or the highest column of any row if no row number is passed
+ *
+ * @return string Highest column name
+ */
+ public function getHighestColumn($row = null)
+ {
+ if ($row == null) {
+ return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
+ }
+
+ return $this->getHighestDataColumn($row);
+ }
+
+ /**
+ * Get highest worksheet column that contains data.
+ *
+ * @param string $row Return the highest data column for the specified row,
+ * or the highest data column of any row if no row number is passed
+ *
+ * @return string Highest column name that contains data
+ */
+ public function getHighestDataColumn($row = null)
+ {
+ return $this->cellCollection->getHighestColumn($row);
+ }
+
+ /**
+ * Get highest worksheet row.
+ *
+ * @param string $column Return the highest data row for the specified column,
+ * or the highest row of any column if no column letter is passed
+ *
+ * @return int Highest row number
+ */
+ public function getHighestRow($column = null)
+ {
+ if ($column == null) {
+ return $this->cachedHighestRow;
+ }
+
+ return $this->getHighestDataRow($column);
+ }
+
+ /**
+ * Get highest worksheet row that contains data.
+ *
+ * @param string $column Return the highest data row for the specified column,
+ * or the highest data row of any column if no column letter is passed
+ *
+ * @return int Highest row number that contains data
+ */
+ public function getHighestDataRow($column = null)
+ {
+ return $this->cellCollection->getHighestRow($column);
+ }
+
+ /**
+ * Get highest worksheet column and highest row that have cell records.
+ *
+ * @return array Highest column name and highest row number
+ */
+ public function getHighestRowAndColumn()
+ {
+ return $this->cellCollection->getHighestRowAndColumn();
+ }
+
+ /**
+ * Set a cell value.
+ *
+ * @param string $pCoordinate Coordinate of the cell, eg: 'A1'
+ * @param mixed $pValue Value of the cell
+ *
+ * @return $this
+ */
+ public function setCellValue($pCoordinate, $pValue)
+ {
+ $this->getCell($pCoordinate)->setValue($pValue);
+
+ return $this;
+ }
+
+ /**
+ * Set a cell value by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ * @param mixed $value Value of the cell
+ *
+ * @return $this
+ */
+ public function setCellValueByColumnAndRow($columnIndex, $row, $value)
+ {
+ $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value);
+
+ return $this;
+ }
+
+ /**
+ * Set a cell value.
+ *
+ * @param string $pCoordinate Coordinate of the cell, eg: 'A1'
+ * @param mixed $pValue Value of the cell
+ * @param string $pDataType Explicit data type, see DataType::TYPE_*
+ *
+ * @return $this
+ */
+ public function setCellValueExplicit($pCoordinate, $pValue, $pDataType)
+ {
+ // Set value
+ $this->getCell($pCoordinate)->setValueExplicit($pValue, $pDataType);
+
+ return $this;
+ }
+
+ /**
+ * Set a cell value by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ * @param mixed $value Value of the cell
+ * @param string $dataType Explicit data type, see DataType::TYPE_*
+ *
+ * @return $this
+ */
+ public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
+ {
+ $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType);
+
+ return $this;
+ }
+
+ /**
+ * Get cell at a specific coordinate.
+ *
+ * @param string $coordinate Coordinate of the cell, eg: 'A1'
+ *
+ * @return Cell Cell that was found or created
+ */
+ public function getCell(string $coordinate): Cell
+ {
+ // Shortcut for increased performance for the vast majority of simple cases
+ if ($this->cellCollection->has($coordinate)) {
+ /** @var Cell $cell */
+ $cell = $this->cellCollection->get($coordinate);
+
+ return $cell;
+ }
+
+ /** @var Worksheet $sheet */
+ [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate);
+ $cell = $sheet->cellCollection->get($finalCoordinate);
+
+ return $cell ?? $sheet->createNewCell($finalCoordinate);
+ }
+
+ /**
+ * Get the correct Worksheet and coordinate from a coordinate that may
+ * contains reference to another sheet or a named range.
+ *
+ * @return array{0: Worksheet, 1: string}
+ */
+ private function getWorksheetAndCoordinate(string $pCoordinate): array
+ {
+ $sheet = null;
+ $finalCoordinate = null;
+
+ // Worksheet reference?
+ if (strpos($pCoordinate, '!') !== false) {
+ $worksheetReference = self::extractSheetTitle($pCoordinate, true);
+
+ $sheet = $this->parent->getSheetByName($worksheetReference[0]);
+ $finalCoordinate = strtoupper($worksheetReference[1]);
+
+ if (!$sheet) {
+ throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
+ }
+ } elseif (
+ !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate) &&
+ preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate)
+ ) {
+ // Named range?
+ $namedRange = $this->validateNamedRange($pCoordinate, true);
+ if ($namedRange !== null) {
+ $sheet = $namedRange->getWorksheet();
+ if (!$sheet) {
+ throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
+ }
+
+ $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
+ $finalCoordinate = str_replace('$', '', $cellCoordinate);
+ }
+ }
+
+ if (!$sheet || !$finalCoordinate) {
+ $sheet = $this;
+ $finalCoordinate = strtoupper($pCoordinate);
+ }
+
+ if (Coordinate::coordinateIsRange($finalCoordinate)) {
+ throw new Exception('Cell coordinate string can not be a range of cells.');
+ } elseif (strpos($finalCoordinate, '$') !== false) {
+ throw new Exception('Cell coordinate must not be absolute.');
+ }
+
+ return [$sheet, $finalCoordinate];
+ }
+
+ /**
+ * Get an existing cell at a specific coordinate, or null.
+ *
+ * @param string $coordinate Coordinate of the cell, eg: 'A1'
+ *
+ * @return null|Cell Cell that was found or null
+ */
+ private function getCellOrNull($coordinate): ?Cell
+ {
+ // Check cell collection
+ if ($this->cellCollection->has($coordinate)) {
+ return $this->cellCollection->get($coordinate);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get cell at a specific coordinate by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ *
+ * @return Cell Cell that was found/created or null
+ */
+ public function getCellByColumnAndRow($columnIndex, $row): Cell
+ {
+ $columnLetter = Coordinate::stringFromColumnIndex($columnIndex);
+ $coordinate = $columnLetter . $row;
+
+ if ($this->cellCollection->has($coordinate)) {
+ /** @var Cell $cell */
+ $cell = $this->cellCollection->get($coordinate);
+
+ return $cell;
+ }
+
+ // Create new cell object, if required
+ return $this->createNewCell($coordinate);
+ }
+
+ /**
+ * Create a new cell at the specified coordinate.
+ *
+ * @param string $pCoordinate Coordinate of the cell
+ *
+ * @return Cell Cell that was created
+ */
+ private function createNewCell($pCoordinate)
+ {
+ $cell = new Cell(null, DataType::TYPE_NULL, $this);
+ $this->cellCollection->add($pCoordinate, $cell);
+ $this->cellCollectionIsSorted = false;
+
+ // Coordinates
+ [$column, $row] = Coordinate::coordinateFromString($pCoordinate);
+ $aIndexes = Coordinate::indexesFromString($pCoordinate);
+ if ($this->cachedHighestColumn < $aIndexes[0]) {
+ $this->cachedHighestColumn = $aIndexes[0];
+ }
+ if ($aIndexes[1] > $this->cachedHighestRow) {
+ $this->cachedHighestRow = $aIndexes[1];
+ }
+
+ // Cell needs appropriate xfIndex from dimensions records
+ // but don't create dimension records if they don't already exist
+ $rowDimension = $this->rowDimensions[$row] ?? null;
+ $columnDimension = $this->columnDimensions[$column] ?? null;
+
+ if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) {
+ // then there is a row dimension with explicit style, assign it to the cell
+ $cell->setXfIndex($rowDimension->getXfIndex());
+ } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) {
+ // then there is a column dimension, assign it to the cell
+ $cell->setXfIndex($columnDimension->getXfIndex());
+ }
+
+ return $cell;
+ }
+
+ /**
+ * Does the cell at a specific coordinate exist?
+ *
+ * @param string $coordinate Coordinate of the cell eg: 'A1'
+ *
+ * @return bool
+ */
+ public function cellExists($coordinate)
+ {
+ /** @var Worksheet $sheet */
+ [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate);
+
+ return $sheet->cellCollection->has($finalCoordinate);
+ }
+
+ /**
+ * Cell at a specific coordinate by using numeric cell coordinates exists?
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ *
+ * @return bool
+ */
+ public function cellExistsByColumnAndRow($columnIndex, $row)
+ {
+ return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+ }
+
+ /**
+ * Get row dimension at a specific row.
+ *
+ * @param int $pRow Numeric index of the row
+ */
+ public function getRowDimension(int $pRow): RowDimension
+ {
+ // Get row dimension
+ if (!isset($this->rowDimensions[$pRow])) {
+ $this->rowDimensions[$pRow] = new RowDimension($pRow);
+
+ $this->cachedHighestRow = max($this->cachedHighestRow, $pRow);
+ }
+
+ return $this->rowDimensions[$pRow];
+ }
+
+ /**
+ * Get column dimension at a specific column.
+ *
+ * @param string $pColumn String index of the column eg: 'A'
+ */
+ public function getColumnDimension(string $pColumn): ColumnDimension
+ {
+ // Uppercase coordinate
+ $pColumn = strtoupper($pColumn);
+
+ // Fetch dimensions
+ if (!isset($this->columnDimensions[$pColumn])) {
+ $this->columnDimensions[$pColumn] = new ColumnDimension($pColumn);
+
+ $columnIndex = Coordinate::columnIndexFromString($pColumn);
+ if ($this->cachedHighestColumn < $columnIndex) {
+ $this->cachedHighestColumn = $columnIndex;
+ }
+ }
+
+ return $this->columnDimensions[$pColumn];
+ }
+
+ /**
+ * Get column dimension at a specific column by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ */
+ public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
+ {
+ return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
+ }
+
+ /**
+ * Get styles.
+ *
+ * @return Style[]
+ */
+ public function getStyles()
+ {
+ return $this->styles;
+ }
+
+ /**
+ * Get style for cell.
+ *
+ * @param string $pCellCoordinate Cell coordinate (or range) to get style for, eg: 'A1'
+ *
+ * @return Style
+ */
+ public function getStyle($pCellCoordinate)
+ {
+ // set this sheet as active
+ $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
+
+ // set cell coordinate as active
+ $this->setSelectedCells($pCellCoordinate);
+
+ return $this->parent->getCellXfSupervisor();
+ }
+
+ /**
+ * Get conditional styles for a cell.
+ *
+ * @param string $pCoordinate eg: 'A1'
+ *
+ * @return Conditional[]
+ */
+ public function getConditionalStyles($pCoordinate)
+ {
+ $pCoordinate = strtoupper($pCoordinate);
+ if (!isset($this->conditionalStylesCollection[$pCoordinate])) {
+ $this->conditionalStylesCollection[$pCoordinate] = [];
+ }
+
+ return $this->conditionalStylesCollection[$pCoordinate];
+ }
+
+ /**
+ * Do conditional styles exist for this cell?
+ *
+ * @param string $pCoordinate eg: 'A1'
+ *
+ * @return bool
+ */
+ public function conditionalStylesExists($pCoordinate)
+ {
+ return isset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
+ }
+
+ /**
+ * Removes conditional styles for a cell.
+ *
+ * @param string $pCoordinate eg: 'A1'
+ *
+ * @return $this
+ */
+ public function removeConditionalStyles($pCoordinate)
+ {
+ unset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
+
+ return $this;
+ }
+
+ /**
+ * Get collection of conditional styles.
+ *
+ * @return array
+ */
+ public function getConditionalStylesCollection()
+ {
+ return $this->conditionalStylesCollection;
+ }
+
+ /**
+ * Set conditional styles.
+ *
+ * @param string $pCoordinate eg: 'A1'
+ * @param Conditional[] $pValue
+ *
+ * @return $this
+ */
+ public function setConditionalStyles($pCoordinate, $pValue)
+ {
+ $this->conditionalStylesCollection[strtoupper($pCoordinate)] = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get style for cell by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the cell
+ * @param int $row1 Numeric row coordinate of the cell
+ * @param null|int $columnIndex2 Numeric column coordinate of the range cell
+ * @param null|int $row2 Numeric row coordinate of the range cell
+ *
+ * @return Style
+ */
+ public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
+ {
+ if ($columnIndex2 !== null && $row2 !== null) {
+ $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
+
+ return $this->getStyle($cellRange);
+ }
+
+ return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1);
+ }
+
+ /**
+ * Duplicate cell style to a range of cells.
+ *
+ * Please note that this will overwrite existing cell styles for cells in range!
+ *
+ * @param Style $pCellStyle Cell style to duplicate
+ * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
+ *
+ * @return $this
+ */
+ public function duplicateStyle(Style $pCellStyle, $pRange)
+ {
+ // Add the style to the workbook if necessary
+ $workbook = $this->parent;
+ if ($existingStyle = $this->parent->getCellXfByHashCode($pCellStyle->getHashCode())) {
+ // there is already such cell Xf in our collection
+ $xfIndex = $existingStyle->getIndex();
+ } else {
+ // we don't have such a cell Xf, need to add
+ $workbook->addCellXf($pCellStyle);
+ $xfIndex = $pCellStyle->getIndex();
+ }
+
+ // Calculate range outer borders
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
+
+ // Make sure we can loop upwards on rows and columns
+ if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
+ $tmp = $rangeStart;
+ $rangeStart = $rangeEnd;
+ $rangeEnd = $tmp;
+ }
+
+ // Loop through cells and apply styles
+ for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+ for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+ $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Duplicate conditional style to a range of cells.
+ *
+ * Please note that this will overwrite existing cell styles for cells in range!
+ *
+ * @param Conditional[] $pCellStyle Cell style to duplicate
+ * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
+ *
+ * @return $this
+ */
+ public function duplicateConditionalStyle(array $pCellStyle, $pRange = '')
+ {
+ foreach ($pCellStyle as $cellStyle) {
+ if (!($cellStyle instanceof Conditional)) {
+ throw new Exception('Style is not a conditional style');
+ }
+ }
+
+ // Calculate range outer borders
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
+
+ // Make sure we can loop upwards on rows and columns
+ if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
+ $tmp = $rangeStart;
+ $rangeStart = $rangeEnd;
+ $rangeEnd = $tmp;
+ }
+
+ // Loop through cells and apply styles
+ for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+ for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+ $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $pCellStyle);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set break on a cell.
+ *
+ * @param string $pCoordinate Cell coordinate (e.g. A1)
+ * @param int $pBreak Break type (type of Worksheet::BREAK_*)
+ *
+ * @return $this
+ */
+ public function setBreak($pCoordinate, $pBreak)
+ {
+ // Uppercase coordinate
+ $pCoordinate = strtoupper($pCoordinate);
+
+ if ($pCoordinate != '') {
+ if ($pBreak == self::BREAK_NONE) {
+ if (isset($this->breaks[$pCoordinate])) {
+ unset($this->breaks[$pCoordinate]);
+ }
+ } else {
+ $this->breaks[$pCoordinate] = $pBreak;
+ }
+ } else {
+ throw new Exception('No cell coordinate specified.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set break on a cell by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ * @param int $break Break type (type of Worksheet::BREAK_*)
+ *
+ * @return $this
+ */
+ public function setBreakByColumnAndRow($columnIndex, $row, $break)
+ {
+ return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
+ }
+
+ /**
+ * Get breaks.
+ *
+ * @return int[]
+ */
+ public function getBreaks()
+ {
+ return $this->breaks;
+ }
+
+ /**
+ * Set merge on a cell range.
+ *
+ * @param string $pRange Cell range (e.g. A1:E1)
+ *
+ * @return $this
+ */
+ public function mergeCells($pRange)
+ {
+ // Uppercase coordinate
+ $pRange = strtoupper($pRange);
+
+ if (strpos($pRange, ':') !== false) {
+ $this->mergeCells[$pRange] = $pRange;
+
+ // make sure cells are created
+
+ // get the cells in the range
+ $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
+
+ // create upper left cell if it does not already exist
+ $upperLeft = $aReferences[0];
+ if (!$this->cellExists($upperLeft)) {
+ $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
+ }
+
+ // Blank out the rest of the cells in the range (if they exist)
+ $count = count($aReferences);
+ for ($i = 1; $i < $count; ++$i) {
+ if ($this->cellExists($aReferences[$i])) {
+ $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL);
+ }
+ }
+ } else {
+ throw new Exception('Merge must be set on a range of cells.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set merge on a cell range by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the first cell
+ * @param int $row1 Numeric row coordinate of the first cell
+ * @param int $columnIndex2 Numeric column coordinate of the last cell
+ * @param int $row2 Numeric row coordinate of the last cell
+ *
+ * @return $this
+ */
+ public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+ {
+ $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
+
+ return $this->mergeCells($cellRange);
+ }
+
+ /**
+ * Remove merge on a cell range.
+ *
+ * @param string $pRange Cell range (e.g. A1:E1)
+ *
+ * @return $this
+ */
+ public function unmergeCells($pRange)
+ {
+ // Uppercase coordinate
+ $pRange = strtoupper($pRange);
+
+ if (strpos($pRange, ':') !== false) {
+ if (isset($this->mergeCells[$pRange])) {
+ unset($this->mergeCells[$pRange]);
+ } else {
+ throw new Exception('Cell range ' . $pRange . ' not known as merged.');
+ }
+ } else {
+ throw new Exception('Merge can only be removed from a range of cells.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove merge on a cell range by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the first cell
+ * @param int $row1 Numeric row coordinate of the first cell
+ * @param int $columnIndex2 Numeric column coordinate of the last cell
+ * @param int $row2 Numeric row coordinate of the last cell
+ *
+ * @return $this
+ */
+ public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+ {
+ $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
+
+ return $this->unmergeCells($cellRange);
+ }
+
+ /**
+ * Get merge cells array.
+ *
+ * @return string[]
+ */
+ public function getMergeCells()
+ {
+ return $this->mergeCells;
+ }
+
+ /**
+ * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
+ * a single cell range.
+ *
+ * @param string[] $pValue
+ *
+ * @return $this
+ */
+ public function setMergeCells(array $pValue)
+ {
+ $this->mergeCells = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Set protection on a cell range.
+ *
+ * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
+ * @param string $pPassword Password to unlock the protection
+ * @param bool $pAlreadyHashed If the password has already been hashed, set this to true
+ *
+ * @return $this
+ */
+ public function protectCells($pRange, $pPassword, $pAlreadyHashed = false)
+ {
+ // Uppercase coordinate
+ $pRange = strtoupper($pRange);
+
+ if (!$pAlreadyHashed) {
+ $pPassword = Shared\PasswordHasher::hashPassword($pPassword);
+ }
+ $this->protectedCells[$pRange] = $pPassword;
+
+ return $this;
+ }
+
+ /**
+ * Set protection on a cell range by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the first cell
+ * @param int $row1 Numeric row coordinate of the first cell
+ * @param int $columnIndex2 Numeric column coordinate of the last cell
+ * @param int $row2 Numeric row coordinate of the last cell
+ * @param string $password Password to unlock the protection
+ * @param bool $alreadyHashed If the password has already been hashed, set this to true
+ *
+ * @return $this
+ */
+ public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
+ {
+ $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
+
+ return $this->protectCells($cellRange, $password, $alreadyHashed);
+ }
+
+ /**
+ * Remove protection on a cell range.
+ *
+ * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
+ *
+ * @return $this
+ */
+ public function unprotectCells($pRange)
+ {
+ // Uppercase coordinate
+ $pRange = strtoupper($pRange);
+
+ if (isset($this->protectedCells[$pRange])) {
+ unset($this->protectedCells[$pRange]);
+ } else {
+ throw new Exception('Cell range ' . $pRange . ' not known as protected.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove protection on a cell range by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the first cell
+ * @param int $row1 Numeric row coordinate of the first cell
+ * @param int $columnIndex2 Numeric column coordinate of the last cell
+ * @param int $row2 Numeric row coordinate of the last cell
+ *
+ * @return $this
+ */
+ public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+ {
+ $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
+
+ return $this->unprotectCells($cellRange);
+ }
+
+ /**
+ * Get protected cells.
+ *
+ * @return string[]
+ */
+ public function getProtectedCells()
+ {
+ return $this->protectedCells;
+ }
+
+ /**
+ * Get Autofilter.
+ *
+ * @return AutoFilter
+ */
+ public function getAutoFilter()
+ {
+ return $this->autoFilter;
+ }
+
+ /**
+ * Set AutoFilter.
+ *
+ * @param AutoFilter|string $pValue
+ * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
+ *
+ * @return $this
+ */
+ public function setAutoFilter($pValue)
+ {
+ if (is_string($pValue)) {
+ $this->autoFilter->setRange($pValue);
+ } elseif (is_object($pValue) && ($pValue instanceof AutoFilter)) {
+ $this->autoFilter = $pValue;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set Autofilter Range by using numeric cell coordinates.
+ *
+ * @param int $columnIndex1 Numeric column coordinate of the first cell
+ * @param int $row1 Numeric row coordinate of the first cell
+ * @param int $columnIndex2 Numeric column coordinate of the second cell
+ * @param int $row2 Numeric row coordinate of the second cell
+ *
+ * @return $this
+ */
+ public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+ {
+ return $this->setAutoFilter(
+ Coordinate::stringFromColumnIndex($columnIndex1) . $row1
+ . ':' .
+ Coordinate::stringFromColumnIndex($columnIndex2) . $row2
+ );
+ }
+
+ /**
+ * Remove autofilter.
+ *
+ * @return $this
+ */
+ public function removeAutoFilter()
+ {
+ $this->autoFilter->setRange(null);
+
+ return $this;
+ }
+
+ /**
+ * Get Freeze Pane.
+ *
+ * @return string
+ */
+ public function getFreezePane()
+ {
+ return $this->freezePane;
+ }
+
+ /**
+ * Freeze Pane.
+ *
+ * Examples:
+ *
+ * - A2 will freeze the rows above cell A2 (i.e row 1)
+ * - B1 will freeze the columns to the left of cell B1 (i.e column A)
+ * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
+ *
+ * @param null|string $cell Position of the split
+ * @param null|string $topLeftCell default position of the right bottom pane
+ *
+ * @return $this
+ */
+ public function freezePane($cell, $topLeftCell = null)
+ {
+ if (is_string($cell) && Coordinate::coordinateIsRange($cell)) {
+ throw new Exception('Freeze pane can not be set on a range of cells.');
+ }
+
+ if ($cell !== null && $topLeftCell === null) {
+ $coordinate = Coordinate::coordinateFromString($cell);
+ $topLeftCell = $coordinate[0] . $coordinate[1];
+ }
+
+ $this->freezePane = $cell;
+ $this->topLeftCell = $topLeftCell;
+
+ return $this;
+ }
+
+ /**
+ * Freeze Pane by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ *
+ * @return $this
+ */
+ public function freezePaneByColumnAndRow($columnIndex, $row)
+ {
+ return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+ }
+
+ /**
+ * Unfreeze Pane.
+ *
+ * @return $this
+ */
+ public function unfreezePane()
+ {
+ return $this->freezePane(null);
+ }
+
+ /**
+ * Get the default position of the right bottom pane.
+ *
+ * @return null|string
+ */
+ public function getTopLeftCell()
+ {
+ return $this->topLeftCell;
+ }
+
+ /**
+ * Insert a new row, updating all possible related data.
+ *
+ * @param int $pBefore Insert before this one
+ * @param int $pNumRows Number of rows to insert
+ *
+ * @return $this
+ */
+ public function insertNewRowBefore($pBefore, $pNumRows = 1)
+ {
+ if ($pBefore >= 1) {
+ $objReferenceHelper = ReferenceHelper::getInstance();
+ $objReferenceHelper->insertNewBefore('A' . $pBefore, 0, $pNumRows, $this);
+ } else {
+ throw new Exception('Rows can only be inserted before at least row 1.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Insert a new column, updating all possible related data.
+ *
+ * @param string $pBefore Insert before this one, eg: 'A'
+ * @param int $pNumCols Number of columns to insert
+ *
+ * @return $this
+ */
+ public function insertNewColumnBefore($pBefore, $pNumCols = 1)
+ {
+ if (!is_numeric($pBefore)) {
+ $objReferenceHelper = ReferenceHelper::getInstance();
+ $objReferenceHelper->insertNewBefore($pBefore . '1', $pNumCols, 0, $this);
+ } else {
+ throw new Exception('Column references should not be numeric.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Insert a new column, updating all possible related data.
+ *
+ * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
+ * @param int $pNumCols Number of columns to insert
+ *
+ * @return $this
+ */
+ public function insertNewColumnBeforeByIndex($beforeColumnIndex, $pNumCols = 1)
+ {
+ if ($beforeColumnIndex >= 1) {
+ return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $pNumCols);
+ }
+
+ throw new Exception('Columns can only be inserted before at least column A (1).');
+ }
+
+ /**
+ * Delete a row, updating all possible related data.
+ *
+ * @param int $pRow Remove starting with this one
+ * @param int $pNumRows Number of rows to remove
+ *
+ * @return $this
+ */
+ public function removeRow($pRow, $pNumRows = 1)
+ {
+ if ($pRow < 1) {
+ throw new Exception('Rows to be deleted should at least start from row 1.');
+ }
+
+ $highestRow = $this->getHighestDataRow();
+ $removedRowsCounter = 0;
+
+ for ($r = 0; $r < $pNumRows; ++$r) {
+ if ($pRow + $r <= $highestRow) {
+ $this->getCellCollection()->removeRow($pRow + $r);
+ ++$removedRowsCounter;
+ }
+ }
+
+ $objReferenceHelper = ReferenceHelper::getInstance();
+ $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);
+ for ($r = 0; $r < $removedRowsCounter; ++$r) {
+ $this->getCellCollection()->removeRow($highestRow);
+ --$highestRow;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove a column, updating all possible related data.
+ *
+ * @param string $pColumn Remove starting with this one, eg: 'A'
+ * @param int $pNumCols Number of columns to remove
+ *
+ * @return $this
+ */
+ public function removeColumn($pColumn, $pNumCols = 1)
+ {
+ if (is_numeric($pColumn)) {
+ throw new Exception('Column references should not be numeric.');
+ }
+
+ $highestColumn = $this->getHighestDataColumn();
+ $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
+ $pColumnIndex = Coordinate::columnIndexFromString($pColumn);
+
+ if ($pColumnIndex > $highestColumnIndex) {
+ return $this;
+ }
+
+ $pColumn = Coordinate::stringFromColumnIndex($pColumnIndex + $pNumCols);
+ $objReferenceHelper = ReferenceHelper::getInstance();
+ $objReferenceHelper->insertNewBefore($pColumn . '1', -$pNumCols, 0, $this);
+
+ $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
+
+ for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $pNumCols); $c < $n; ++$c) {
+ $this->getCellCollection()->removeColumn($highestColumn);
+ $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
+ }
+
+ $this->garbageCollect();
+
+ return $this;
+ }
+
+ /**
+ * Remove a column, updating all possible related data.
+ *
+ * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
+ * @param int $numColumns Number of columns to remove
+ *
+ * @return $this
+ */
+ public function removeColumnByIndex($columnIndex, $numColumns = 1)
+ {
+ if ($columnIndex >= 1) {
+ return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
+ }
+
+ throw new Exception('Columns to be deleted should at least start from column A (1)');
+ }
+
+ /**
+ * Show gridlines?
+ *
+ * @return bool
+ */
+ public function getShowGridlines()
+ {
+ return $this->showGridlines;
+ }
+
+ /**
+ * Set show gridlines.
+ *
+ * @param bool $pValue Show gridlines (true/false)
+ *
+ * @return $this
+ */
+ public function setShowGridlines($pValue)
+ {
+ $this->showGridlines = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Print gridlines?
+ *
+ * @return bool
+ */
+ public function getPrintGridlines()
+ {
+ return $this->printGridlines;
+ }
+
+ /**
+ * Set print gridlines.
+ *
+ * @param bool $pValue Print gridlines (true/false)
+ *
+ * @return $this
+ */
+ public function setPrintGridlines($pValue)
+ {
+ $this->printGridlines = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Show row and column headers?
+ *
+ * @return bool
+ */
+ public function getShowRowColHeaders()
+ {
+ return $this->showRowColHeaders;
+ }
+
+ /**
+ * Set show row and column headers.
+ *
+ * @param bool $pValue Show row and column headers (true/false)
+ *
+ * @return $this
+ */
+ public function setShowRowColHeaders($pValue)
+ {
+ $this->showRowColHeaders = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Show summary below? (Row/Column outlining).
+ *
+ * @return bool
+ */
+ public function getShowSummaryBelow()
+ {
+ return $this->showSummaryBelow;
+ }
+
+ /**
+ * Set show summary below.
+ *
+ * @param bool $pValue Show summary below (true/false)
+ *
+ * @return $this
+ */
+ public function setShowSummaryBelow($pValue)
+ {
+ $this->showSummaryBelow = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Show summary right? (Row/Column outlining).
+ *
+ * @return bool
+ */
+ public function getShowSummaryRight()
+ {
+ return $this->showSummaryRight;
+ }
+
+ /**
+ * Set show summary right.
+ *
+ * @param bool $pValue Show summary right (true/false)
+ *
+ * @return $this
+ */
+ public function setShowSummaryRight($pValue)
+ {
+ $this->showSummaryRight = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get comments.
+ *
+ * @return Comment[]
+ */
+ public function getComments()
+ {
+ return $this->comments;
+ }
+
+ /**
+ * Set comments array for the entire sheet.
+ *
+ * @param Comment[] $pValue
+ *
+ * @return $this
+ */
+ public function setComments(array $pValue)
+ {
+ $this->comments = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get comment for cell.
+ *
+ * @param string $pCellCoordinate Cell coordinate to get comment for, eg: 'A1'
+ *
+ * @return Comment
+ */
+ public function getComment($pCellCoordinate)
+ {
+ // Uppercase coordinate
+ $pCellCoordinate = strtoupper($pCellCoordinate);
+
+ if (Coordinate::coordinateIsRange($pCellCoordinate)) {
+ throw new Exception('Cell coordinate string can not be a range of cells.');
+ } elseif (strpos($pCellCoordinate, '$') !== false) {
+ throw new Exception('Cell coordinate string must not be absolute.');
+ } elseif ($pCellCoordinate == '') {
+ throw new Exception('Cell coordinate can not be zero-length string.');
+ }
+
+ // Check if we already have a comment for this cell.
+ if (isset($this->comments[$pCellCoordinate])) {
+ return $this->comments[$pCellCoordinate];
+ }
+
+ // If not, create a new comment.
+ $newComment = new Comment();
+ $this->comments[$pCellCoordinate] = $newComment;
+
+ return $newComment;
+ }
+
+ /**
+ * Get comment for cell by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ *
+ * @return Comment
+ */
+ public function getCommentByColumnAndRow($columnIndex, $row)
+ {
+ return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+ }
+
+ /**
+ * Get active cell.
+ *
+ * @return string Example: 'A1'
+ */
+ public function getActiveCell()
+ {
+ return $this->activeCell;
+ }
+
+ /**
+ * Get selected cells.
+ *
+ * @return string
+ */
+ public function getSelectedCells()
+ {
+ return $this->selectedCells;
+ }
+
+ /**
+ * Selected cell.
+ *
+ * @param string $pCoordinate Cell (i.e. A1)
+ *
+ * @return $this
+ */
+ public function setSelectedCell($pCoordinate)
+ {
+ return $this->setSelectedCells($pCoordinate);
+ }
+
+ /**
+ * Select a range of cells.
+ *
+ * @param string $pCoordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6'
+ *
+ * @return $this
+ */
+ public function setSelectedCells($pCoordinate)
+ {
+ // Uppercase coordinate
+ $pCoordinate = strtoupper($pCoordinate);
+
+ // Convert 'A' to 'A:A'
+ $pCoordinate = preg_replace('/^([A-Z]+)$/', '${1}:${1}', $pCoordinate);
+
+ // Convert '1' to '1:1'
+ $pCoordinate = preg_replace('/^(\d+)$/', '${1}:${1}', $pCoordinate);
+
+ // Convert 'A:C' to 'A1:C1048576'
+ $pCoordinate = preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $pCoordinate);
+
+ // Convert '1:3' to 'A1:XFD3'
+ $pCoordinate = preg_replace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $pCoordinate);
+
+ if (Coordinate::coordinateIsRange($pCoordinate)) {
+ [$first] = Coordinate::splitRange($pCoordinate);
+ $this->activeCell = $first[0];
+ } else {
+ $this->activeCell = $pCoordinate;
+ }
+ $this->selectedCells = $pCoordinate;
+
+ return $this;
+ }
+
+ /**
+ * Selected cell by using numeric cell coordinates.
+ *
+ * @param int $columnIndex Numeric column coordinate of the cell
+ * @param int $row Numeric row coordinate of the cell
+ *
+ * @return $this
+ */
+ public function setSelectedCellByColumnAndRow($columnIndex, $row)
+ {
+ return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+ }
+
+ /**
+ * Get right-to-left.
+ *
+ * @return bool
+ */
+ public function getRightToLeft()
+ {
+ return $this->rightToLeft;
+ }
+
+ /**
+ * Set right-to-left.
+ *
+ * @param bool $value Right-to-left true/false
+ *
+ * @return $this
+ */
+ public function setRightToLeft($value)
+ {
+ $this->rightToLeft = $value;
+
+ return $this;
+ }
+
+ /**
+ * Fill worksheet from values in array.
+ *
+ * @param array $source Source array
+ * @param mixed $nullValue Value in source array that stands for blank cell
+ * @param string $startCell Insert array starting from this cell address as the top left coordinate
+ * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
+ *
+ * @return $this
+ */
+ public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
+ {
+ // Convert a 1-D array to 2-D (for ease of looping)
+ if (!is_array(end($source))) {
+ $source = [$source];
+ }
+
+ // start coordinate
+ [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
+
+ // Loop through $source
+ foreach ($source as $rowData) {
+ $currentColumn = $startColumn;
+ foreach ($rowData as $cellValue) {
+ if ($strictNullComparison) {
+ if ($cellValue !== $nullValue) {
+ // Set cell value
+ $this->getCell($currentColumn . $startRow)->setValue($cellValue);
+ }
+ } else {
+ if ($cellValue != $nullValue) {
+ // Set cell value
+ $this->getCell($currentColumn . $startRow)->setValue($cellValue);
+ }
+ }
+ ++$currentColumn;
+ }
+ ++$startRow;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create array from a range of cells.
+ *
+ * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
+ * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+ * @param bool $calculateFormulas Should formulas be calculated?
+ * @param bool $formatData Should formatting be applied to cell values?
+ * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+ * True - Return rows and columns indexed by their actual row and column IDs
+ *
+ * @return array
+ */
+ public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
+ {
+ // Returnvalue
+ $returnValue = [];
+ // Identify the range that we need to extract from the worksheet
+ [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange);
+ $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
+ $minRow = $rangeStart[1];
+ $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
+ $maxRow = $rangeEnd[1];
+
+ ++$maxCol;
+ // Loop through rows
+ $r = -1;
+ for ($row = $minRow; $row <= $maxRow; ++$row) {
+ $rRef = $returnCellRef ? $row : ++$r;
+ $c = -1;
+ // Loop through columns in the current row
+ for ($col = $minCol; $col != $maxCol; ++$col) {
+ $cRef = $returnCellRef ? $col : ++$c;
+ // Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
+ // so we test and retrieve directly against cellCollection
+ if ($this->cellCollection->has($col . $row)) {
+ // Cell exists
+ $cell = $this->cellCollection->get($col . $row);
+ if ($cell->getValue() !== null) {
+ if ($cell->getValue() instanceof RichText) {
+ $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
+ } else {
+ if ($calculateFormulas) {
+ $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
+ } else {
+ $returnValue[$rRef][$cRef] = $cell->getValue();
+ }
+ }
+
+ if ($formatData) {
+ $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
+ $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
+ $returnValue[$rRef][$cRef],
+ ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
+ );
+ }
+ } else {
+ // Cell holds a NULL
+ $returnValue[$rRef][$cRef] = $nullValue;
+ }
+ } else {
+ // Cell doesn't exist
+ $returnValue[$rRef][$cRef] = $nullValue;
+ }
+ }
+ }
+
+ // Return
+ return $returnValue;
+ }
+
+ private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
+ {
+ $namedRange = DefinedName::resolveName($definedName, $this);
+ if ($namedRange === null) {
+ if ($returnNullIfInvalid) {
+ return null;
+ }
+
+ throw new Exception('Named Range ' . $definedName . ' does not exist.');
+ }
+
+ if ($namedRange->isFormula()) {
+ if ($returnNullIfInvalid) {
+ return null;
+ }
+
+ throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
+ }
+
+ if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) {
+ if ($returnNullIfInvalid) {
+ return null;
+ }
+
+ throw new Exception(
+ 'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
+ );
+ }
+
+ return $namedRange;
+ }
+
+ /**
+ * Create array from a range of cells.
+ *
+ * @param string $definedName The Named Range that should be returned
+ * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+ * @param bool $calculateFormulas Should formulas be calculated?
+ * @param bool $formatData Should formatting be applied to cell values?
+ * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+ * True - Return rows and columns indexed by their actual row and column IDs
+ *
+ * @return array
+ */
+ public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
+ {
+ $namedRange = $this->validateNamedRange($definedName);
+ $workSheet = $namedRange->getWorksheet();
+ $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
+ $cellRange = str_replace('$', '', $cellRange);
+
+ return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
+ }
+
+ /**
+ * Create array from worksheet.
+ *
+ * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+ * @param bool $calculateFormulas Should formulas be calculated?
+ * @param bool $formatData Should formatting be applied to cell values?
+ * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+ * True - Return rows and columns indexed by their actual row and column IDs
+ *
+ * @return array
+ */
+ public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
+ {
+ // Garbage collect...
+ $this->garbageCollect();
+
+ // Identify the range that we need to extract from the worksheet
+ $maxCol = $this->getHighestColumn();
+ $maxRow = $this->getHighestRow();
+
+ // Return
+ return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
+ }
+
+ /**
+ * Get row iterator.
+ *
+ * @param int $startRow The row number at which to start iterating
+ * @param int $endRow The row number at which to stop iterating
+ *
+ * @return RowIterator
+ */
+ public function getRowIterator($startRow = 1, $endRow = null)
+ {
+ return new RowIterator($this, $startRow, $endRow);
+ }
+
+ /**
+ * Get column iterator.
+ *
+ * @param string $startColumn The column address at which to start iterating
+ * @param string $endColumn The column address at which to stop iterating
+ *
+ * @return ColumnIterator
+ */
+ public function getColumnIterator($startColumn = 'A', $endColumn = null)
+ {
+ return new ColumnIterator($this, $startColumn, $endColumn);
+ }
+
+ /**
+ * Run PhpSpreadsheet garbage collector.
+ *
+ * @return $this
+ */
+ public function garbageCollect()
+ {
+ // Flush cache
+ $this->cellCollection->get('A1');
+
+ // Lookup highest column and highest row if cells are cleaned
+ $colRow = $this->cellCollection->getHighestRowAndColumn();
+ $highestRow = $colRow['row'];
+ $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
+
+ // Loop through column dimensions
+ foreach ($this->columnDimensions as $dimension) {
+ $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
+ }
+
+ // Loop through row dimensions
+ foreach ($this->rowDimensions as $dimension) {
+ $highestRow = max($highestRow, $dimension->getRowIndex());
+ }
+
+ // Cache values
+ if ($highestColumn < 1) {
+ $this->cachedHighestColumn = 1;
+ } else {
+ $this->cachedHighestColumn = $highestColumn;
+ }
+ $this->cachedHighestRow = $highestRow;
+
+ // Return
+ return $this;
+ }
+
+ /**
+ * Get hash code.
+ *
+ * @return string Hash code
+ */
+ public function getHashCode()
+ {
+ if ($this->dirty) {
+ $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
+ $this->dirty = false;
+ }
+
+ return $this->hash;
+ }
+
+ /**
+ * Extract worksheet title from range.
+ *
+ * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
+ * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
+ *
+ * @param string $pRange Range to extract title from
+ * @param bool $returnRange Return range? (see example)
+ *
+ * @return mixed
+ */
+ public static function extractSheetTitle($pRange, $returnRange = false)
+ {
+ // Sheet title included?
+ if (($sep = strrpos($pRange, '!')) === false) {
+ return $returnRange ? ['', $pRange] : '';
+ }
+
+ if ($returnRange) {
+ return [substr($pRange, 0, $sep), substr($pRange, $sep + 1)];
+ }
+
+ return substr($pRange, $sep + 1);
+ }
+
+ /**
+ * Get hyperlink.
+ *
+ * @param string $pCellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
+ *
+ * @return Hyperlink
+ */
+ public function getHyperlink($pCellCoordinate)
+ {
+ // return hyperlink if we already have one
+ if (isset($this->hyperlinkCollection[$pCellCoordinate])) {
+ return $this->hyperlinkCollection[$pCellCoordinate];
+ }
+
+ // else create hyperlink
+ $this->hyperlinkCollection[$pCellCoordinate] = new Hyperlink();
+
+ return $this->hyperlinkCollection[$pCellCoordinate];
+ }
+
+ /**
+ * Set hyperlink.
+ *
+ * @param string $pCellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
+ *
+ * @return $this
+ */
+ public function setHyperlink($pCellCoordinate, ?Hyperlink $pHyperlink = null)
+ {
+ if ($pHyperlink === null) {
+ unset($this->hyperlinkCollection[$pCellCoordinate]);
+ } else {
+ $this->hyperlinkCollection[$pCellCoordinate] = $pHyperlink;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Hyperlink at a specific coordinate exists?
+ *
+ * @param string $pCoordinate eg: 'A1'
+ *
+ * @return bool
+ */
+ public function hyperlinkExists($pCoordinate)
+ {
+ return isset($this->hyperlinkCollection[$pCoordinate]);
+ }
+
+ /**
+ * Get collection of hyperlinks.
+ *
+ * @return Hyperlink[]
+ */
+ public function getHyperlinkCollection()
+ {
+ return $this->hyperlinkCollection;
+ }
+
+ /**
+ * Get data validation.
+ *
+ * @param string $pCellCoordinate Cell coordinate to get data validation for, eg: 'A1'
+ *
+ * @return DataValidation
+ */
+ public function getDataValidation($pCellCoordinate)
+ {
+ // return data validation if we already have one
+ if (isset($this->dataValidationCollection[$pCellCoordinate])) {
+ return $this->dataValidationCollection[$pCellCoordinate];
+ }
+
+ // else create data validation
+ $this->dataValidationCollection[$pCellCoordinate] = new DataValidation();
+
+ return $this->dataValidationCollection[$pCellCoordinate];
+ }
+
+ /**
+ * Set data validation.
+ *
+ * @param string $pCellCoordinate Cell coordinate to insert data validation, eg: 'A1'
+ *
+ * @return $this
+ */
+ public function setDataValidation($pCellCoordinate, ?DataValidation $pDataValidation = null)
+ {
+ if ($pDataValidation === null) {
+ unset($this->dataValidationCollection[$pCellCoordinate]);
+ } else {
+ $this->dataValidationCollection[$pCellCoordinate] = $pDataValidation;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Data validation at a specific coordinate exists?
+ *
+ * @param string $pCoordinate eg: 'A1'
+ *
+ * @return bool
+ */
+ public function dataValidationExists($pCoordinate)
+ {
+ return isset($this->dataValidationCollection[$pCoordinate]);
+ }
+
+ /**
+ * Get collection of data validations.
+ *
+ * @return DataValidation[]
+ */
+ public function getDataValidationCollection()
+ {
+ return $this->dataValidationCollection;
+ }
+
+ /**
+ * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
+ *
+ * @param string $range
+ *
+ * @return string Adjusted range value
+ */
+ public function shrinkRangeToFit($range)
+ {
+ $maxCol = $this->getHighestColumn();
+ $maxRow = $this->getHighestRow();
+ $maxCol = Coordinate::columnIndexFromString($maxCol);
+
+ $rangeBlocks = explode(' ', $range);
+ foreach ($rangeBlocks as &$rangeSet) {
+ $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
+
+ if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
+ $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
+ }
+ if ($rangeBoundaries[0][1] > $maxRow) {
+ $rangeBoundaries[0][1] = $maxRow;
+ }
+ if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
+ $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
+ }
+ if ($rangeBoundaries[1][1] > $maxRow) {
+ $rangeBoundaries[1][1] = $maxRow;
+ }
+ $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
+ }
+ unset($rangeSet);
+
+ return implode(' ', $rangeBlocks);
+ }
+
+ /**
+ * Get tab color.
+ *
+ * @return Color
+ */
+ public function getTabColor()
+ {
+ if ($this->tabColor === null) {
+ $this->tabColor = new Color();
+ }
+
+ return $this->tabColor;
+ }
+
+ /**
+ * Reset tab color.
+ *
+ * @return $this
+ */
+ public function resetTabColor()
+ {
+ $this->tabColor = null;
+
+ return $this;
+ }
+
+ /**
+ * Tab color set?
+ *
+ * @return bool
+ */
+ public function isTabColorSet()
+ {
+ return $this->tabColor !== null;
+ }
+
+ /**
+ * Copy worksheet (!= clone!).
+ *
+ * @return static
+ */
+ public function copy()
+ {
+ return clone $this;
+ }
+
+ /**
+ * Implement PHP __clone to create a deep clone, not just a shallow copy.
+ */
+ public function __clone()
+ {
+ // @phpstan-ignore-next-line
+ foreach ($this as $key => $val) {
+ if ($key == 'parent') {
+ continue;
+ }
+
+ if (is_object($val) || (is_array($val))) {
+ if ($key == 'cellCollection') {
+ $newCollection = $this->cellCollection->cloneCellCollection($this);
+ $this->cellCollection = $newCollection;
+ } elseif ($key == 'drawingCollection') {
+ $currentCollection = $this->drawingCollection;
+ $this->drawingCollection = new ArrayObject();
+ foreach ($currentCollection as $item) {
+ if (is_object($item)) {
+ $newDrawing = clone $item;
+ $newDrawing->setWorksheet($this);
+ }
+ }
+ } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
+ $newAutoFilter = clone $this->autoFilter;
+ $this->autoFilter = $newAutoFilter;
+ $this->autoFilter->setParent($this);
+ } else {
+ $this->{$key} = unserialize(serialize($val));
+ }
+ }
+ }
+ }
+
+ /**
+ * Define the code name of the sheet.
+ *
+ * @param string $pValue Same rule as Title minus space not allowed (but, like Excel, change
+ * silently space to underscore)
+ * @param bool $validate False to skip validation of new title. WARNING: This should only be set
+ * at parse time (by Readers), where titles can be assumed to be valid.
+ *
+ * @return $this
+ */
+ public function setCodeName($pValue, $validate = true)
+ {
+ // Is this a 'rename' or not?
+ if ($this->getCodeName() == $pValue) {
+ return $this;
+ }
+
+ if ($validate) {
+ $pValue = str_replace(' ', '_', $pValue); //Excel does this automatically without flinching, we are doing the same
+
+ // Syntax check
+ // throw an exception if not valid
+ self::checkSheetCodeName($pValue);
+
+ // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
+
+ if ($this->getParent()) {
+ // Is there already such sheet name?
+ if ($this->getParent()->sheetCodeNameExists($pValue)) {
+ // Use name, but append with lowest possible integer
+
+ if (Shared\StringHelper::countCharacters($pValue) > 29) {
+ $pValue = Shared\StringHelper::substring($pValue, 0, 29);
+ }
+ $i = 1;
+ while ($this->getParent()->sheetCodeNameExists($pValue . '_' . $i)) {
+ ++$i;
+ if ($i == 10) {
+ if (Shared\StringHelper::countCharacters($pValue) > 28) {
+ $pValue = Shared\StringHelper::substring($pValue, 0, 28);
+ }
+ } elseif ($i == 100) {
+ if (Shared\StringHelper::countCharacters($pValue) > 27) {
+ $pValue = Shared\StringHelper::substring($pValue, 0, 27);
+ }
+ }
+ }
+
+ $pValue .= '_' . $i; // ok, we have a valid name
+ }
+ }
+ }
+
+ $this->codeName = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Return the code name of the sheet.
+ *
+ * @return null|string
+ */
+ public function getCodeName()
+ {
+ return $this->codeName;
+ }
+
+ /**
+ * Sheet has a code name ?
+ *
+ * @return bool
+ */
+ public function hasCodeName()
+ {
+ return $this->codeName !== null;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php
new file mode 100644
index 0000000..afda5c4
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php
@@ -0,0 +1,131 @@
+includeCharts;
+ }
+
+ public function setIncludeCharts($pValue)
+ {
+ $this->includeCharts = (bool) $pValue;
+
+ return $this;
+ }
+
+ public function getPreCalculateFormulas()
+ {
+ return $this->preCalculateFormulas;
+ }
+
+ public function setPreCalculateFormulas($pValue)
+ {
+ $this->preCalculateFormulas = (bool) $pValue;
+
+ return $this;
+ }
+
+ public function getUseDiskCaching()
+ {
+ return $this->useDiskCaching;
+ }
+
+ public function setUseDiskCaching($pValue, $pDirectory = null)
+ {
+ $this->useDiskCaching = $pValue;
+
+ if ($pDirectory !== null) {
+ if (is_dir($pDirectory)) {
+ $this->diskCachingDirectory = $pDirectory;
+ } else {
+ throw new Exception("Directory does not exist: $pDirectory");
+ }
+ }
+
+ return $this;
+ }
+
+ public function getDiskCachingDirectory()
+ {
+ return $this->diskCachingDirectory;
+ }
+
+ /**
+ * Open file handle.
+ *
+ * @param resource|string $filename
+ */
+ public function openFileHandle($filename): void
+ {
+ if (is_resource($filename)) {
+ $this->fileHandle = $filename;
+ $this->shouldCloseFile = false;
+
+ return;
+ }
+
+ $fileHandle = $filename ? fopen($filename, 'wb+') : false;
+ if ($fileHandle === false) {
+ throw new Exception('Could not open file "' . $filename . '" for writing.');
+ }
+
+ $this->fileHandle = $fileHandle;
+ $this->shouldCloseFile = true;
+ }
+
+ /**
+ * Close file handle only if we opened it ourselves.
+ */
+ protected function maybeCloseFileHandle(): void
+ {
+ if ($this->shouldCloseFile) {
+ if (!fclose($this->fileHandle)) {
+ throw new Exception('Could not close file after writing.');
+ }
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Csv.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Csv.php
new file mode 100644
index 0000000..188a83a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Csv.php
@@ -0,0 +1,386 @@
+spreadsheet = $spreadsheet;
+ }
+
+ /**
+ * Save PhpSpreadsheet to file.
+ *
+ * @param resource|string $pFilename
+ */
+ public function save($pFilename): void
+ {
+ // Fetch sheet
+ $sheet = $this->spreadsheet->getSheet($this->sheetIndex);
+
+ $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
+ Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
+ $saveArrayReturnType = Calculation::getArrayReturnType();
+ Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
+
+ // Open file
+ $this->openFileHandle($pFilename);
+
+ if ($this->excelCompatibility) {
+ $this->setUseBOM(true); // Enforce UTF-8 BOM Header
+ $this->setIncludeSeparatorLine(true); // Set separator line
+ $this->setEnclosure('"'); // Set enclosure to "
+ $this->setDelimiter(';'); // Set delimiter to a semi-colon
+ $this->setLineEnding("\r\n");
+ }
+
+ if ($this->useBOM) {
+ // Write the UTF-8 BOM code if required
+ fwrite($this->fileHandle, "\xEF\xBB\xBF");
+ }
+
+ if ($this->includeSeparatorLine) {
+ // Write the separator line if required
+ fwrite($this->fileHandle, 'sep=' . $this->getDelimiter() . $this->lineEnding);
+ }
+
+ // Identify the range that we need to extract from the worksheet
+ $maxCol = $sheet->getHighestDataColumn();
+ $maxRow = $sheet->getHighestDataRow();
+
+ // Write rows to file
+ for ($row = 1; $row <= $maxRow; ++$row) {
+ // Convert the row to an array...
+ $cellsArray = $sheet->rangeToArray('A' . $row . ':' . $maxCol . $row, '', $this->preCalculateFormulas);
+ // ... and write to the file
+ $this->writeLine($this->fileHandle, $cellsArray[0]);
+ }
+
+ $this->maybeCloseFileHandle();
+ Calculation::setArrayReturnType($saveArrayReturnType);
+ Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+ }
+
+ /**
+ * Get delimiter.
+ *
+ * @return string
+ */
+ public function getDelimiter()
+ {
+ return $this->delimiter;
+ }
+
+ /**
+ * Set delimiter.
+ *
+ * @param string $pValue Delimiter, defaults to ','
+ *
+ * @return $this
+ */
+ public function setDelimiter($pValue)
+ {
+ $this->delimiter = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get enclosure.
+ *
+ * @return string
+ */
+ public function getEnclosure()
+ {
+ return $this->enclosure;
+ }
+
+ /**
+ * Set enclosure.
+ *
+ * @param string $pValue Enclosure, defaults to "
+ *
+ * @return $this
+ */
+ public function setEnclosure($pValue = '"')
+ {
+ $this->enclosure = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get line ending.
+ *
+ * @return string
+ */
+ public function getLineEnding()
+ {
+ return $this->lineEnding;
+ }
+
+ /**
+ * Set line ending.
+ *
+ * @param string $pValue Line ending, defaults to OS line ending (PHP_EOL)
+ *
+ * @return $this
+ */
+ public function setLineEnding($pValue)
+ {
+ $this->lineEnding = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get whether BOM should be used.
+ *
+ * @return bool
+ */
+ public function getUseBOM()
+ {
+ return $this->useBOM;
+ }
+
+ /**
+ * Set whether BOM should be used.
+ *
+ * @param bool $pValue Use UTF-8 byte-order mark? Defaults to false
+ *
+ * @return $this
+ */
+ public function setUseBOM($pValue)
+ {
+ $this->useBOM = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get whether a separator line should be included.
+ *
+ * @return bool
+ */
+ public function getIncludeSeparatorLine()
+ {
+ return $this->includeSeparatorLine;
+ }
+
+ /**
+ * Set whether a separator line should be included as the first line of the file.
+ *
+ * @param bool $pValue Use separator line? Defaults to false
+ *
+ * @return $this
+ */
+ public function setIncludeSeparatorLine($pValue)
+ {
+ $this->includeSeparatorLine = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the file should be saved with full Excel Compatibility.
+ *
+ * @return bool
+ */
+ public function getExcelCompatibility()
+ {
+ return $this->excelCompatibility;
+ }
+
+ /**
+ * Set whether the file should be saved with full Excel Compatibility.
+ *
+ * @param bool $pValue Set the file to be written as a fully Excel compatible csv file
+ * Note that this overrides other settings such as useBOM, enclosure and delimiter
+ *
+ * @return $this
+ */
+ public function setExcelCompatibility($pValue)
+ {
+ $this->excelCompatibility = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get sheet index.
+ *
+ * @return int
+ */
+ public function getSheetIndex()
+ {
+ return $this->sheetIndex;
+ }
+
+ /**
+ * Set sheet index.
+ *
+ * @param int $pValue Sheet index
+ *
+ * @return $this
+ */
+ public function setSheetIndex($pValue)
+ {
+ $this->sheetIndex = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get output encoding.
+ *
+ * @return string
+ */
+ public function getOutputEncoding()
+ {
+ return $this->outputEncoding;
+ }
+
+ /**
+ * Set output encoding.
+ *
+ * @param string $pValue Output encoding
+ *
+ * @return $this
+ */
+ public function setOutputEncoding($pValue)
+ {
+ $this->outputEncoding = $pValue;
+
+ return $this;
+ }
+
+ private $enclosureRequired = true;
+
+ public function setEnclosureRequired(bool $value): self
+ {
+ $this->enclosureRequired = $value;
+
+ return $this;
+ }
+
+ public function getEnclosureRequired(): bool
+ {
+ return $this->enclosureRequired;
+ }
+
+ /**
+ * Write line to CSV file.
+ *
+ * @param resource $pFileHandle PHP filehandle
+ * @param array $pValues Array containing values in a row
+ */
+ private function writeLine($pFileHandle, array $pValues): void
+ {
+ // No leading delimiter
+ $delimiter = '';
+
+ // Build the line
+ $line = '';
+
+ foreach ($pValues as $element) {
+ // Add delimiter
+ $line .= $delimiter;
+ $delimiter = $this->delimiter;
+ // Escape enclosures
+ $enclosure = $this->enclosure;
+ if ($enclosure) {
+ // If enclosure is not required, use enclosure only if
+ // element contains newline, delimiter, or enclosure.
+ if (!$this->enclosureRequired && strpbrk($element, "$delimiter$enclosure\n") === false) {
+ $enclosure = '';
+ } else {
+ $element = str_replace($enclosure, $enclosure . $enclosure, $element);
+ }
+ }
+ // Add enclosed string
+ $line .= $enclosure . $element . $enclosure;
+ }
+
+ // Add line ending
+ $line .= $this->lineEnding;
+
+ // Write to file
+ if ($this->outputEncoding != '') {
+ $line = mb_convert_encoding($line, $this->outputEncoding);
+ }
+ fwrite($pFileHandle, $line);
+ }
+}
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 $pFilename
+ */
+ public function save($pFilename): void
+ {
+ // Open file
+ $this->openFileHandle($pFilename);
+
+ // 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.
+ *
+ * @return int
+ */
+ public function getSheetIndex()
+ {
+ return $this->sheetIndex;
+ }
+
+ /**
+ * Set sheet index.
+ *
+ * @param int $pValue Sheet index
+ *
+ * @return $this
+ */
+ public function setSheetIndex($pValue)
+ {
+ $this->sheetIndex = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get sheet index.
+ *
+ * @return bool
+ */
+ public function getGenerateSheetNavigationBlock()
+ {
+ return $this->generateSheetNavigationBlock;
+ }
+
+ /**
+ * Set sheet index.
+ *
+ * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not
+ *
+ * @return $this
+ */
+ public function setGenerateSheetNavigationBlock($pValue)
+ {
+ $this->generateSheetNavigationBlock = (bool) $pValue;
+
+ 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) : '';
+ }
+
+ /**
+ * Generate HTML header.
+ *
+ * @param bool $pIncludeStyles Include styles?
+ *
+ * @return string
+ */
+ public function generateHTMLHeader($pIncludeStyles = 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()) . ' ' . 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 .= $pIncludeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
+
+ $html .= ' ' . PHP_EOL;
+ $html .= '' . PHP_EOL;
+ $html .= ' ' . PHP_EOL;
+
+ 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;
+
+ foreach ($sheets as $sheet) {
+ $html .= ' ' . $sheet->getTitle() . ' ' . PHP_EOL;
+ ++$sheetId;
+ }
+
+ $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 Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ * @param int $row Row to check for charts
+ *
+ * @return array
+ *
+ * @codeCoverageIgnore
+ */
+ private function extendRowsForCharts(Worksheet $pSheet, int $row)
+ {
+ $rowMax = $row;
+ $colMax = 'A';
+ $anyfound = false;
+ if ($this->includeCharts) {
+ foreach ($pSheet->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 $pSheet, int $row): string
+ {
+ [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($pSheet, $row);
+
+ foreach ($pSheet->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($pSheet, $col . $row);
+ $htmlx .= $this->includeCharts ? $this->writeChartInCell($pSheet, $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 $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ * @param string $coordinates Cell coordinates
+ *
+ * @return string
+ */
+ private function writeImageInCell(Worksheet $pSheet, $coordinates)
+ {
+ // Construct HTML
+ $html = '';
+
+ // Write images
+ foreach ($pSheet->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);
+
+ $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 .= ' ';
+ } 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 .= ' ';
+ }
+ }
+ }
+
+ 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.
+ *
+ * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ * @param string $coordinates Cell coordinates
+ *
+ * @return string
+ *
+ * @codeCoverageIgnore
+ */
+ private function writeChartInCell(Worksheet $pSheet, $coordinates)
+ {
+ // Construct HTML
+ $html = '';
+
+ // Write charts
+ foreach ($pSheet->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 ? self::getChartCaption($filedesc->getCaption()) : '';
+ $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 .= ' ' . PHP_EOL;
+
+ unlink($chartFileName);
+ }
+ }
+ }
+ }
+
+ // Return
+ 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.
+ * Caption is described in documentation as fixed,
+ * but in 32_Chart it is somehow an array of RichText.
+ *
+ * @param mixed $cap
+ *
+ * @return string
+ *
+ * @codeCoverageIgnore
+ */
+ private static function getChartCaption($cap)
+ {
+ return is_array($cap) ? implode(' ', $cap) : $cap;
+ }
+
+ /**
+ * 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 $pStyle)
+ {
+ // Create CSS
+ return array_merge(
+ $this->createCSSStyleAlignment($pStyle->getAlignment()),
+ $this->createCSSStyleBorders($pStyle->getBorders()),
+ $this->createCSSStyleFont($pStyle->getFont()),
+ $this->createCSSStyleFill($pStyle->getFill())
+ );
+ }
+
+ /**
+ * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment).
+ *
+ * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment
+ *
+ * @return array
+ */
+ private function createCSSStyleAlignment(Alignment $pStyle)
+ {
+ // Construct CSS
+ $css = [];
+
+ // Create CSS
+ $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
+ $textAlign = $this->mapHAlign($pStyle->getHorizontal());
+ if ($textAlign) {
+ $css['text-align'] = $textAlign;
+ if (in_array($textAlign, ['left', 'right'])) {
+ $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
+ }
+ }
+
+ return $css;
+ }
+
+ /**
+ * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
+ *
+ * @return array
+ */
+ private function createCSSStyleFont(Font $pStyle)
+ {
+ // Construct CSS
+ $css = [];
+
+ // Create CSS
+ if ($pStyle->getBold()) {
+ $css['font-weight'] = 'bold';
+ }
+ if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
+ $css['text-decoration'] = 'underline line-through';
+ } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
+ $css['text-decoration'] = 'underline';
+ } elseif ($pStyle->getStrikethrough()) {
+ $css['text-decoration'] = 'line-through';
+ }
+ if ($pStyle->getItalic()) {
+ $css['font-style'] = 'italic';
+ }
+
+ $css['color'] = '#' . $pStyle->getColor()->getRGB();
+ $css['font-family'] = '\'' . $pStyle->getName() . '\'';
+ $css['font-size'] = $pStyle->getSize() . 'pt';
+
+ return $css;
+ }
+
+ /**
+ * Create CSS style (Borders).
+ *
+ * @param Borders $pStyle Borders
+ *
+ * @return array
+ */
+ private function createCSSStyleBorders(Borders $pStyle)
+ {
+ // Construct CSS
+ $css = [];
+
+ // Create CSS
+ $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
+ $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
+ $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
+ $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
+
+ return $css;
+ }
+
+ /**
+ * Create CSS style (Border).
+ *
+ * @param Border $pStyle Border
+ *
+ * @return string
+ */
+ private function createCSSStyleBorder(Border $pStyle)
+ {
+ // Create CSS - add !important to non-none border styles for merged cells
+ $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle());
+
+ return $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
+ }
+
+ /**
+ * Create CSS style (Fill).
+ *
+ * @param Fill $pStyle Fill
+ *
+ * @return array
+ */
+ private function createCSSStyleFill(Fill $pStyle)
+ {
+ // Construct HTML
+ $css = [];
+
+ // Create CSS
+ $value = $pStyle->getFillType() == Fill::FILL_NONE ?
+ 'white' : '#' . $pStyle->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($pSheet, $id)
+ {
+ $style = isset($this->cssStyles['table']) ?
+ $this->assembleCSS($this->cssStyles['table']) : '';
+
+ $prntgrid = $pSheet->getPrintGridlines();
+ $viewgrid = $this->isPdf ? $prntgrid : $pSheet->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($pSheet, $id, &$html, $sheetIndex): void
+ {
+ if (!$this->useInlineCss) {
+ $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
+ $gridlinesp = $pSheet->getPrintGridlines() ? ' gridlinesp' : '';
+ $html .= " " . PHP_EOL;
+ } else {
+ $html .= $this->generateTableTagInline($pSheet, $id);
+ }
+ }
+
+ /**
+ * Generate table header.
+ *
+ * @param Worksheet $pSheet 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($pSheet, $showid = true)
+ {
+ $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
+
+ // Construct HTML
+ $html = '';
+ $id = $showid ? "id='sheet$sheetIndex'" : '';
+ if ($showid) {
+ $html .= "\n";
+ } else {
+ $html .= "
\n";
+ }
+
+ $this->generateTableTag($pSheet, $id, $html, $sheetIndex);
+
+ // Write
elements
+ $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->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 Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ * @param int $sheetIndex Sheet index (0-based)
+ * @param int $pRow row number
+ *
+ * @return string
+ */
+ private function generateRowStart(Worksheet $pSheet, $sheetIndex, $pRow)
+ {
+ $html = '';
+ if (count($pSheet->getBreaks()) > 0) {
+ $breaks = $pSheet->getBreaks();
+
+ // check if a break is needed before this row
+ if (isset($breaks['A' . $pRow])) {
+ // close table:
+ $html .= $this->generateTableFooter();
+ if ($this->isPdf && $this->useInlineCss) {
+ $html .= '
';
+ }
+
+ // open table again: + etc.
+ $html .= $this->generateTableHeader($pSheet, false);
+ $html .= '' . PHP_EOL;
+ }
+ }
+
+ // Write row start
+ if (!$this->useInlineCss) {
+ $html .= ' ' . PHP_EOL;
+ } else {
+ $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
+ ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
+
+ $html .= ' ' . PHP_EOL;
+ }
+
+ return $html;
+ }
+
+ private function generateRowCellCss($pSheet, $cellAddress, $pRow, $colNum)
+ {
+ $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
+ $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
+ if (!$this->useInlineCss) {
+ $cssClass = 'column' . $colNum;
+ } 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);
+
+ $cellData .= $cellEnd;
+
+ $cellData .= ' ';
+ } else {
+ // Convert UTF8 data to PCDATA
+ $cellText = $element->getText();
+ $cellData .= htmlspecialchars($cellText);
+ }
+ }
+ }
+
+ private function generateRowCellDataValue($pSheet, $cell, &$cellData): void
+ {
+ if ($cell->getValue() instanceof RichText) {
+ $this->generateRowCellDataValueRich($cell, $cellData);
+ } else {
+ $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
+ $cellData = NumberFormat::toFormattedString(
+ $origData,
+ $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
+ [$this, 'formatColor']
+ );
+ if ($cellData === $origData) {
+ $cellData = htmlspecialchars($cellData ?? '');
+ }
+ if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
+ $cellData = '' . $cellData . ' ';
+ } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
+ $cellData = '' . $cellData . ' ';
+ }
+ }
+ }
+
+ private function generateRowCellData($pSheet, $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($pSheet);
+ //}
+ // Value
+ $this->generateRowCellDataValue($pSheet, $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 = $pSheet->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($pSheet, $coordinate)
+ {
+ return $this->includeCharts ? $this->writeChartInCell($pSheet, $coordinate) : '';
+ }
+
+ private function generateRowSpans($html, $rowSpan, $colSpan)
+ {
+ $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
+ $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
+
+ return $html;
+ }
+
+ private function generateRowWriteCell(&$html, $pSheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow): void
+ {
+ // Image?
+ $htmlx = $this->writeImageInCell($pSheet, $coordinate);
+ // Chart?
+ $htmlx .= $this->generateRowIncludeCharts($pSheet, $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 element because TCPDF
+ // does not recognize e.g.
+ 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 element because TCPDF
+ // does not recognize e.g.
+ if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
+ $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['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($pSheet, $coordinate);
+
+ // Cell data
+ $html .= $cellData;
+
+ // Column end
+ $html .= '' . $cellType . '>' . PHP_EOL;
+ }
+
+ /**
+ * Generate row.
+ *
+ * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ * @param array $pValues Array containing cells in a row
+ * @param int $pRow Row number (0-based)
+ * @param string $cellType eg: 'td'
+ *
+ * @return string
+ */
+ private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
+ {
+ // Sheet index
+ $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
+ $html = $this->generateRowStart($pSheet, $sheetIndex, $pRow);
+
+ // Write cells
+ $colNum = 0;
+ foreach ($pValues as $cellAddress) {
+ [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($pSheet, $cellAddress, $pRow, $colNum);
+
+ $colSpan = 1;
+ $rowSpan = 1;
+
+ // Cell Data
+ $cellData = $this->generateRowCellData($pSheet, $cell, $cssClass, $cellType);
+
+ // Hyperlink?
+ if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
+ $cellData = '' . $cellData . ' ';
+ }
+
+ // Should the cell be written or is it swallowed by a rowspan or colspan?
+ $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
+ && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
+
+ // Colspan and Rowspan
+ $colspan = 1;
+ $rowspan = 1;
+ if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
+ $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 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) . ($pRow + $rowSpan);
+ if (!$this->useInlineCss) {
+ $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
+ }
+ }
+
+ // Write
+ if ($writeCell) {
+ $this->generateRowWriteCell($html, $pSheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow);
+ }
+
+ // 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 $pValue = [])
+ {
+ $pairs = [];
+ foreach ($pValue 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 $pValue
+ *
+ * @return $this
+ */
+ public function setImagesRoot($pValue)
+ {
+ $this->imagesRoot = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get embed images.
+ *
+ * @return bool
+ */
+ public function getEmbedImages()
+ {
+ return $this->embedImages;
+ }
+
+ /**
+ * Set embed images.
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setEmbedImages($pValue)
+ {
+ $this->embedImages = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get use inline CSS?
+ *
+ * @return bool
+ */
+ public function getUseInlineCss()
+ {
+ return $this->useInlineCss;
+ }
+
+ /**
+ * Set use inline CSS?
+ *
+ * @param bool $pValue
+ *
+ * @return $this
+ */
+ public function setUseInlineCss($pValue)
+ {
+ $this->useInlineCss = $pValue;
+
+ 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 $pValue
+ *
+ * @return $this
+ *
+ * @codeCoverageIgnore
+ *
+ * @deprecated no longer used
+ */
+ public function setUseEmbeddedCSS($pValue)
+ {
+ $this->useEmbeddedCSS = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Add color to formatted string as inline style.
+ *
+ * @param string $pValue Plain formatted value without color
+ * @param string $pFormat Format code
+ *
+ * @return string
+ */
+ public function formatColor($pValue, $pFormat)
+ {
+ // Color information, e.g. [Red] is always at the beginning
+ $color = null; // initialize
+ $matches = [];
+
+ $color_regex = '/^\\[[a-zA-Z]+\\]/';
+ if (preg_match($color_regex, $pFormat, $matches)) {
+ $color = str_replace(['[', ']'], '', $matches[0]);
+ $color = strtolower($color);
+ }
+
+ // convert to PCDATA
+ $value = htmlspecialchars($pValue);
+
+ // color span tag
+ if ($color !== null) {
+ $value = '' . $value . ' ';
+ }
+
+ return $value;
+ }
+
+ /**
+ * 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 $pSheet, $coordinate)
+ {
+ $result = '';
+ if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) {
+ $sanitizer = new HTMLPurifier();
+ $sanitizedString = $sanitizer->purify($pSheet->getComment($coordinate)->getText()->getPlainText());
+ if ($sanitizedString !== '') {
+ $result .= '';
+ $result .= '';
+ $result .= PHP_EOL;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 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..5129d65
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php
@@ -0,0 +1,87 @@
+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 $pFilename
+ */
+ public function save($pFilename): void
+ {
+ if (!$this->spreadSheet) {
+ throw new WriterException('PhpSpreadsheet object unassigned.');
+ }
+
+ // garbage collect
+ $this->spreadSheet->garbageCollect();
+
+ $this->openFileHandle($pFilename);
+
+ $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/AutoFilters.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php
new file mode 100644
index 0000000..cf0450f
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php
@@ -0,0 +1,63 @@
+objWriter = $objWriter;
+ $this->spreadsheet = $spreadsheet;
+ }
+
+ public function write(): void
+ {
+ $wrapperWritten = false;
+ $sheetCount = $this->spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ $worksheet = $this->spreadsheet->getSheet($i);
+ $autofilter = $worksheet->getAutoFilter();
+ if ($autofilter !== null && !empty($autofilter->getRange())) {
+ if ($wrapperWritten === false) {
+ $this->objWriter->startElement('table:database-ranges');
+ $wrapperWritten = true;
+ }
+ $this->objWriter->startElement('table:database-range');
+ $this->objWriter->writeAttribute('table:orientation', 'column');
+ $this->objWriter->writeAttribute('table:display-filter-buttons', 'true');
+ $this->objWriter->writeAttribute(
+ 'table:target-range-address',
+ $this->formatRange($worksheet, $autofilter)
+ );
+ $this->objWriter->endElement();
+ }
+ }
+
+ if ($wrapperWritten === true) {
+ $this->objWriter->endElement();
+ }
+ }
+
+ protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string
+ {
+ $title = $worksheet->getTitle();
+ $range = $autofilter->getRange();
+
+ return "'{$title}'.{$range}";
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php
new file mode 100644
index 0000000..b0829bf
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php
@@ -0,0 +1,30 @@
+
+ */
+class Comment
+{
+ public static function write(XMLWriter $objWriter, Cell $cell): void
+ {
+ $comments = $cell->getWorksheet()->getComments();
+ if (!isset($comments[$cell->getCoordinate()])) {
+ return;
+ }
+ $comment = $comments[$cell->getCoordinate()];
+
+ $objWriter->startElement('office:annotation');
+ $objWriter->writeAttribute('svg:width', $comment->getWidth());
+ $objWriter->writeAttribute('svg:height', $comment->getHeight());
+ $objWriter->writeAttribute('svg:x', $comment->getMarginLeft());
+ $objWriter->writeAttribute('svg:y', $comment->getMarginTop());
+ $objWriter->writeElement('dc:creator', $comment->getAuthor());
+ $objWriter->writeElement('text:p', $comment->getText()->getPlainText());
+ $objWriter->endElement();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
new file mode 100644
index 0000000..f8aae20
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
@@ -0,0 +1,178 @@
+writer = $writer;
+ }
+
+ private function mapHorizontalAlignment(string $horizontalAlignment): string
+ {
+ switch ($horizontalAlignment) {
+ case Alignment::HORIZONTAL_CENTER:
+ case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
+ case Alignment::HORIZONTAL_DISTRIBUTED:
+ return 'center';
+ case Alignment::HORIZONTAL_RIGHT:
+ return 'end';
+ case Alignment::HORIZONTAL_FILL:
+ case Alignment::HORIZONTAL_JUSTIFY:
+ return 'justify';
+ }
+
+ return 'start';
+ }
+
+ private function mapVerticalAlignment(string $verticalAlignment): string
+ {
+ switch ($verticalAlignment) {
+ case Alignment::VERTICAL_TOP:
+ return 'top';
+ case Alignment::VERTICAL_CENTER:
+ return 'middle';
+ case Alignment::VERTICAL_DISTRIBUTED:
+ case Alignment::VERTICAL_JUSTIFY:
+ return 'automatic';
+ }
+
+ return 'bottom';
+ }
+
+ private function writeFillStyle(Fill $fill): void
+ {
+ switch ($fill->getFillType()) {
+ case Fill::FILL_SOLID:
+ $this->writer->writeAttribute('fo:background-color', sprintf(
+ '#%s',
+ strtolower($fill->getStartColor()->getRGB())
+ ));
+
+ break;
+ case Fill::FILL_GRADIENT_LINEAR:
+ case Fill::FILL_GRADIENT_PATH:
+ /// TODO :: To be implemented
+ break;
+ case Fill::FILL_NONE:
+ default:
+ }
+ }
+
+ private function writeCellProperties(CellStyle $style): void
+ {
+ // Align
+ $hAlign = $style->getAlignment()->getHorizontal();
+ $vAlign = $style->getAlignment()->getVertical();
+ $wrap = $style->getAlignment()->getWrapText();
+
+ $this->writer->startElement('style:table-cell-properties');
+ if (!empty($vAlign) || $wrap) {
+ if (!empty($vAlign)) {
+ $vAlign = $this->mapVerticalAlignment($vAlign);
+ $this->writer->writeAttribute('style:vertical-align', $vAlign);
+ }
+ if ($wrap) {
+ $this->writer->writeAttribute('fo:wrap-option', 'wrap');
+ }
+ }
+ $this->writer->writeAttribute('style:rotation-align', 'none');
+
+ // Fill
+ if ($fill = $style->getFill()) {
+ $this->writeFillStyle($fill);
+ }
+
+ $this->writer->endElement();
+
+ if (!empty($hAlign)) {
+ $hAlign = $this->mapHorizontalAlignment($hAlign);
+ $this->writer->startElement('style:paragraph-properties');
+ $this->writer->writeAttribute('fo:text-align', $hAlign);
+ $this->writer->endElement();
+ }
+ }
+
+ protected function mapUnderlineStyle(Font $font): string
+ {
+ switch ($font->getUnderline()) {
+ case Font::UNDERLINE_DOUBLE:
+ case Font::UNDERLINE_DOUBLEACCOUNTING:
+ return'double';
+ case Font::UNDERLINE_SINGLE:
+ case Font::UNDERLINE_SINGLEACCOUNTING:
+ return'single';
+ }
+
+ return 'none';
+ }
+
+ protected function writeTextProperties(CellStyle $style): void
+ {
+ // Font
+ $this->writer->startElement('style:text-properties');
+
+ $font = $style->getFont();
+
+ if ($font->getBold()) {
+ $this->writer->writeAttribute('fo:font-weight', 'bold');
+ $this->writer->writeAttribute('style:font-weight-complex', 'bold');
+ $this->writer->writeAttribute('style:font-weight-asian', 'bold');
+ }
+
+ if ($font->getItalic()) {
+ $this->writer->writeAttribute('fo:font-style', 'italic');
+ }
+
+ if ($color = $font->getColor()) {
+ $this->writer->writeAttribute('fo:color', sprintf('#%s', $color->getRGB()));
+ }
+
+ if ($family = $font->getName()) {
+ $this->writer->writeAttribute('fo:font-family', $family);
+ }
+
+ if ($size = $font->getSize()) {
+ $this->writer->writeAttribute('fo:font-size', sprintf('%.1Fpt', $size));
+ }
+
+ if ($font->getUnderline() && $font->getUnderline() !== Font::UNDERLINE_NONE) {
+ $this->writer->writeAttribute('style:text-underline-style', 'solid');
+ $this->writer->writeAttribute('style:text-underline-width', 'auto');
+ $this->writer->writeAttribute('style:text-underline-color', 'font-color');
+
+ $underline = $this->mapUnderlineStyle($font);
+ $this->writer->writeAttribute('style:text-underline-type', $underline);
+ }
+
+ $this->writer->endElement(); // Close style:text-properties
+ }
+
+ public function write(CellStyle $style): void
+ {
+ $this->writer->startElement('style:style');
+ $this->writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex());
+ $this->writer->writeAttribute('style:family', 'table-cell');
+ $this->writer->writeAttribute('style:parent-style-name', 'Default');
+
+ // Alignment, fill colour, etc
+ $this->writeCellProperties($style);
+
+ // style:text-properties
+ $this->writeTextProperties($style);
+
+ // End
+ $this->writer->endElement(); // Close style:style
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php
new file mode 100644
index 0000000..a589e54
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php
@@ -0,0 +1,302 @@
+
+ */
+class Content extends WriterPart
+{
+ const NUMBER_COLS_REPEATED_MAX = 1024;
+ const NUMBER_ROWS_REPEATED_MAX = 1048576;
+
+ private $formulaConvertor;
+
+ /**
+ * Set parent Ods writer.
+ */
+ public function __construct(Ods $writer)
+ {
+ parent::__construct($writer);
+
+ $this->formulaConvertor = new Formula($this->getParentWriter()->getSpreadsheet()->getDefinedNames());
+ }
+
+ /**
+ * Write content.xml to XML format.
+ *
+ * @return string XML Output
+ */
+ public function write(): string
+ {
+ $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');
+
+ // Content
+ $objWriter->startElement('office:document-content');
+ $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+ $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
+ $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
+ $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
+ $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
+ $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible: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:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
+ $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
+ $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
+ $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
+ $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
+ $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
+ $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
+ $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
+ $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+ $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
+ $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
+ $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
+ $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
+ $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
+ $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
+ $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
+ $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
+ $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
+ $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
+ $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
+ $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
+ $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
+ $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
+ $objWriter->writeAttribute('office:version', '1.2');
+
+ $objWriter->writeElement('office:scripts');
+ $objWriter->writeElement('office:font-face-decls');
+
+ // Styles XF
+ $objWriter->startElement('office:automatic-styles');
+ $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet());
+ $objWriter->endElement();
+
+ $objWriter->startElement('office:body');
+ $objWriter->startElement('office:spreadsheet');
+ $objWriter->writeElement('table:calculation-settings');
+
+ $this->writeSheets($objWriter);
+
+ (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write();
+ // Defined names (ranges and formulae)
+ (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write sheets.
+ */
+ private function writeSheets(XMLWriter $objWriter): void
+ {
+ $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ $objWriter->startElement('table:table');
+ $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle());
+ $objWriter->writeElement('office:forms');
+ $objWriter->startElement('table:table-column');
+ $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
+ $objWriter->endElement();
+ $this->writeRows($objWriter, $spreadsheet->getSheet($i));
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write rows of the specified sheet.
+ */
+ private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void
+ {
+ $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
+ $span_row = 0;
+ $rows = $sheet->getRowIterator();
+ while ($rows->valid()) {
+ --$numberRowsRepeated;
+ $row = $rows->current();
+ if ($row->getCellIterator()->valid()) {
+ if ($span_row) {
+ $objWriter->startElement('table:table-row');
+ if ($span_row > 1) {
+ $objWriter->writeAttribute('table:number-rows-repeated', $span_row);
+ }
+ $objWriter->startElement('table:table-cell');
+ $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $span_row = 0;
+ }
+ $objWriter->startElement('table:table-row');
+ $this->writeCells($objWriter, $row);
+ $objWriter->endElement();
+ } else {
+ ++$span_row;
+ }
+ $rows->next();
+ }
+ }
+
+ /**
+ * Write cells of the specified row.
+ */
+ private function writeCells(XMLWriter $objWriter, Row $row): void
+ {
+ $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
+ $prevColumn = -1;
+ $cells = $row->getCellIterator();
+ while ($cells->valid()) {
+ /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
+ $cell = $cells->current();
+ $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
+
+ $this->writeCellSpan($objWriter, $column, $prevColumn);
+ $objWriter->startElement('table:table-cell');
+ $this->writeCellMerge($objWriter, $cell);
+
+ // Style XF
+ $style = $cell->getXfIndex();
+ if ($style !== null) {
+ $objWriter->writeAttribute('table:style-name', Style::CELL_STYLE_PREFIX . $style);
+ }
+
+ switch ($cell->getDataType()) {
+ case DataType::TYPE_BOOL:
+ $objWriter->writeAttribute('office:value-type', 'boolean');
+ $objWriter->writeAttribute('office:value', $cell->getValue());
+ $objWriter->writeElement('text:p', $cell->getValue());
+
+ break;
+ case DataType::TYPE_ERROR:
+ $objWriter->writeAttribute('table:formula', 'of:=#NULL!');
+ $objWriter->writeAttribute('office:value-type', 'string');
+ $objWriter->writeAttribute('office:string-value', '');
+ $objWriter->writeElement('text:p', '#NULL!');
+
+ break;
+ case DataType::TYPE_FORMULA:
+ $formulaValue = $cell->getValue();
+ if ($this->getParentWriter()->getPreCalculateFormulas()) {
+ try {
+ $formulaValue = $cell->getCalculatedValue();
+ } catch (Exception $e) {
+ // don't do anything
+ }
+ }
+ $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
+ if (is_numeric($formulaValue)) {
+ $objWriter->writeAttribute('office:value-type', 'float');
+ } else {
+ $objWriter->writeAttribute('office:value-type', 'string');
+ }
+ $objWriter->writeAttribute('office:value', $formulaValue);
+ $objWriter->writeElement('text:p', $formulaValue);
+
+ break;
+ case DataType::TYPE_NUMERIC:
+ $objWriter->writeAttribute('office:value-type', 'float');
+ $objWriter->writeAttribute('office:value', $cell->getValue());
+ $objWriter->writeElement('text:p', $cell->getValue());
+
+ break;
+ case DataType::TYPE_INLINE:
+ // break intentionally omitted
+ case DataType::TYPE_STRING:
+ $objWriter->writeAttribute('office:value-type', 'string');
+ $objWriter->writeElement('text:p', $cell->getValue());
+
+ break;
+ }
+ Comment::write($objWriter, $cell);
+ $objWriter->endElement();
+ $prevColumn = $column;
+ $cells->next();
+ }
+ $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
+ if ($numberColsRepeated > 0) {
+ if ($numberColsRepeated > 1) {
+ $objWriter->startElement('table:table-cell');
+ $objWriter->writeAttribute('table:number-columns-repeated', $numberColsRepeated);
+ $objWriter->endElement();
+ } else {
+ $objWriter->writeElement('table:table-cell');
+ }
+ }
+ }
+
+ /**
+ * Write span.
+ *
+ * @param int $curColumn
+ * @param int $prevColumn
+ */
+ private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void
+ {
+ $diff = $curColumn - $prevColumn - 1;
+ if (1 === $diff) {
+ $objWriter->writeElement('table:table-cell');
+ } elseif ($diff > 1) {
+ $objWriter->startElement('table:table-cell');
+ $objWriter->writeAttribute('table:number-columns-repeated', $diff);
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write XF cell styles.
+ */
+ private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
+ {
+ $styleWriter = new Style($writer);
+ foreach ($spreadsheet->getCellXfCollection() as $style) {
+ $styleWriter->write($style);
+ }
+ }
+
+ /**
+ * Write attributes for merged cell.
+ */
+ private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
+ {
+ if (!$cell->isMergeRangeValueCell()) {
+ return;
+ }
+
+ $mergeRange = Coordinate::splitRange($cell->getMergeRange());
+ [$startCell, $endCell] = $mergeRange[0];
+ $start = Coordinate::coordinateFromString($startCell);
+ $end = Coordinate::coordinateFromString($endCell);
+ $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
+ $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1;
+
+ $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan);
+ $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan);
+ }
+}
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
+ {
+ $this->objWriter->writeAttribute('table:name', $definedName->getName());
+ $this->objWriter->writeAttribute(
+ 'table:expression',
+ $this->formulaConvertor->convertFormula($definedName->getValue(), $definedName->getWorksheet()->getTitle())
+ );
+ $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress(
+ $definedName,
+ "'" . (($definedName->getWorksheet() !== null) ? $definedName->getWorksheet()->getTitle() : $defaultWorksheet->getTitle()) . "'!\$A\$1"
+ ));
+ }
+
+ private function writeNamedRange(DefinedName $definedName): void
+ {
+ $this->objWriter->writeAttribute('table:name', $definedName->getName());
+ $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress(
+ $definedName,
+ "'" . $definedName->getWorksheet()->getTitle() . "'!\$A\$1"
+ ));
+ $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
+ $worksheet = $definedName->getWorksheet()->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/Ods/Settings.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php
new file mode 100644
index 0000000..047bd41
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php
@@ -0,0 +1,88 @@
+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');
+
+ // Settings
+ $objWriter->startElement('office:document-settings');
+ $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:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0');
+ $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+ $objWriter->writeAttribute('office:version', '1.2');
+
+ $objWriter->startElement('office:settings');
+ $objWriter->startElement('config:config-item-set');
+ $objWriter->writeAttribute('config:name', 'ooo:view-settings');
+ $objWriter->startElement('config:config-item-map-indexed');
+ $objWriter->writeAttribute('config:name', 'Views');
+ $objWriter->startElement('config:config-item-map-entry');
+ $spreadsheet = $this->getParentWriter()->getSpreadsheet();
+
+ $objWriter->startElement('config:config-item');
+ $objWriter->writeAttribute('config:name', 'ViewId');
+ $objWriter->writeAttribute('config:type', 'string');
+ $objWriter->text('view1');
+ $objWriter->endElement(); // ViewId
+ $objWriter->startElement('config:config-item-map-named');
+ $objWriter->writeAttribute('config:name', 'Tables');
+ foreach ($spreadsheet->getWorksheetIterator() as $ws) {
+ $objWriter->startElement('config:config-item-map-entry');
+ $objWriter->writeAttribute('config:name', $ws->getTitle());
+ $selected = $ws->getSelectedCells();
+ if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) {
+ $colSel = Coordinate::columnIndexFromString($matches[1]) - 1;
+ $rowSel = (int) $matches[2] - 1;
+ $objWriter->startElement('config:config-item');
+ $objWriter->writeAttribute('config:name', 'CursorPositionX');
+ $objWriter->writeAttribute('config:type', 'int');
+ $objWriter->text($colSel);
+ $objWriter->endElement();
+ $objWriter->startElement('config:config-item');
+ $objWriter->writeAttribute('config:name', 'CursorPositionY');
+ $objWriter->writeAttribute('config:type', 'int');
+ $objWriter->text($rowSel);
+ $objWriter->endElement();
+ }
+ $objWriter->endElement(); // config:config-item-map-entry
+ }
+ $objWriter->endElement(); // config:config-item-map-named
+ $wstitle = $spreadsheet->getActiveSheet()->getTitle();
+ $objWriter->startElement('config:config-item');
+ $objWriter->writeAttribute('config:name', 'ActiveTable');
+ $objWriter->writeAttribute('config:type', 'string');
+ $objWriter->text($wstitle);
+ $objWriter->endElement(); // config:config-item ActiveTable
+
+ $objWriter->endElement(); // config:config-item-map-entry
+ $objWriter->endElement(); // config:config-item-map-indexed Views
+ $objWriter->endElement(); // config:config-item-set ooo:view-settings
+ $objWriter->startElement('config:config-item-set');
+ $objWriter->writeAttribute('config:name', 'ooo:configuration-settings');
+ $objWriter->endElement(); // config:config-item-set ooo:configuration-settings
+ $objWriter->endElement(); // office:settings
+ $objWriter->endElement(); // office:document-settings
+
+ return $objWriter->getData();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Styles.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Styles.php
new file mode 100644
index 0000000..448b1ef
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Styles.php
@@ -0,0 +1,65 @@
+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');
+
+ // Content
+ $objWriter->startElement('office:document-styles');
+ $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+ $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
+ $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
+ $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
+ $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
+ $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible: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:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
+ $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
+ $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
+ $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
+ $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
+ $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
+ $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
+ $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
+ $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+ $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
+ $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
+ $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
+ $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
+ $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
+ $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
+ $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
+ $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
+ $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
+ $objWriter->writeAttribute('office:version', '1.2');
+
+ $objWriter->writeElement('office:font-face-decls');
+ $objWriter->writeElement('office:styles');
+ $objWriter->writeElement('office:automatic-styles');
+ $objWriter->writeElement('office:master-styles');
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php
new file mode 100644
index 0000000..db9579d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php
@@ -0,0 +1,16 @@
+parentWriter;
+ }
+
+ /**
+ * Set parent Ods writer.
+ */
+ public function __construct(Ods $writer)
+ {
+ $this->parentWriter = $writer;
+ }
+
+ abstract public function write(): string;
+}
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..36f3966
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php
@@ -0,0 +1,253 @@
+ '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 $pValue Paper size see PageSetup::PAPERSIZE_*
+ *
+ * @return self
+ */
+ public function setPaperSize($pValue)
+ {
+ $this->paperSize = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get Orientation.
+ *
+ * @return string
+ */
+ public function getOrientation()
+ {
+ return $this->orientation;
+ }
+
+ /**
+ * Set Orientation.
+ *
+ * @param string $pValue Page orientation see PageSetup::ORIENTATION_*
+ *
+ * @return self
+ */
+ public function setOrientation($pValue)
+ {
+ $this->orientation = $pValue;
+
+ return $this;
+ }
+
+ /**
+ * Get temporary storage directory.
+ *
+ * @return string
+ */
+ public function getTempDir()
+ {
+ return $this->tempDir;
+ }
+
+ /**
+ * Set temporary storage directory.
+ *
+ * @param string $pValue Temporary storage directory
+ *
+ * @return self
+ */
+ public function setTempDir($pValue)
+ {
+ if (is_dir($pValue)) {
+ $this->tempDir = $pValue;
+ } else {
+ throw new WriterException("Directory does not exist: $pValue");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Save Spreadsheet to PDF file, pre-save.
+ *
+ * @param string $pFilename Name of the file to save as
+ *
+ * @return resource
+ */
+ protected function prepareForSave($pFilename)
+ {
+ // Open file
+ $this->openFileHandle($pFilename);
+
+ 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..87e8eeb
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php
@@ -0,0 +1,72 @@
+getSheetIndex() === null) {
+ $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize();
+ } else {
+ $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize();
+ }
+
+ $orientation = ($orientation == 'L') ? 'landscape' : 'portrait';
+
+ // Override Page Orientation
+ if ($this->getOrientation() !== null) {
+ $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_DEFAULT)
+ ? PageSetup::ORIENTATION_PORTRAIT
+ : $this->getOrientation();
+ }
+ // Override Paper Size
+ if ($this->getPaperSize() !== null) {
+ $printPaperSize = $this->getPaperSize();
+ }
+
+ if (isset(self::$paperSizes[$printPaperSize])) {
+ $paperSize = self::$paperSizes[$printPaperSize];
+ }
+
+ // 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..56ac693
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php
@@ -0,0 +1,106 @@
+getSheetIndex()) {
+ $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize();
+ } else {
+ $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize();
+ }
+ $this->setOrientation($orientation);
+
+ // Override Page Orientation
+ if (null !== $this->getOrientation()) {
+ $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_DEFAULT)
+ ? PageSetup::ORIENTATION_PORTRAIT
+ : $this->getOrientation();
+ }
+ $orientation = strtoupper($orientation);
+
+ // Override Paper Size
+ if (null !== $this->getPaperSize()) {
+ $printPaperSize = $this->getPaperSize();
+ }
+
+ if (isset(self::$paperSizes[$printPaperSize])) {
+ $paperSize = self::$paperSizes[$printPaperSize];
+ }
+
+ // 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();
+ 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/Pdf/Tcpdf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php
new file mode 100644
index 0000000..56e917e
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php
@@ -0,0 +1,104 @@
+setUseInlineCss(true);
+ }
+
+ /**
+ * Gets the implementation of external PDF library that should be used.
+ *
+ * @param string $orientation Page orientation
+ * @param string $unit Unit measure
+ * @param array|string $paperSize Paper size
+ *
+ * @return \TCPDF implementation
+ */
+ protected function createExternalWriterInstance($orientation, $unit, $paperSize)
+ {
+ return new \TCPDF($orientation, $unit, $paperSize);
+ }
+
+ /**
+ * Save Spreadsheet to file.
+ *
+ * @param string $pFilename Name of the file to save as
+ */
+ public function save($pFilename): void
+ {
+ $fileHandle = parent::prepareForSave($pFilename);
+
+ // Default PDF paper size
+ $paperSize = 'LETTER'; // Letter (8.5 in. by 11 in.)
+
+ // Check for paper size and page orientation
+ if ($this->getSheetIndex() === null) {
+ $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize();
+ $printMargins = $this->spreadsheet->getSheet(0)->getPageMargins();
+ } else {
+ $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation()
+ == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+ $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize();
+ $printMargins = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageMargins();
+ }
+
+ // Override Page Orientation
+ if ($this->getOrientation() !== null) {
+ $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE)
+ ? 'L'
+ : 'P';
+ }
+ // Override Paper Size
+ if ($this->getPaperSize() !== null) {
+ $printPaperSize = $this->getPaperSize();
+ }
+
+ if (isset(self::$paperSizes[$printPaperSize])) {
+ $paperSize = self::$paperSizes[$printPaperSize];
+ }
+
+ // Create PDF
+ $pdf = $this->createExternalWriterInstance($orientation, 'pt', $paperSize);
+ $pdf->setFontSubsetting(false);
+ // Set margins, converting inches to points (using 72 dpi)
+ $pdf->SetMargins($printMargins->getLeft() * 72, $printMargins->getTop() * 72, $printMargins->getRight() * 72);
+ $pdf->SetAutoPageBreak(true, $printMargins->getBottom() * 72);
+
+ $pdf->setPrintHeader(false);
+ $pdf->setPrintFooter(false);
+
+ $pdf->AddPage();
+
+ // Set the appropriate font
+ $pdf->SetFont($this->getFont());
+ $pdf->writeHTML($this->generateHTMLAll());
+
+ // 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());
+
+ // Write to file
+ fwrite($fileHandle, $pdf->output($pFilename, 'S'));
+
+ parent::restoreStateAfterSave();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php
new file mode 100644
index 0000000..624dc41
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php
@@ -0,0 +1,911 @@
+spreadsheet = $spreadsheet;
+
+ $this->parser = new Xls\Parser($spreadsheet);
+ }
+
+ /**
+ * Save Spreadsheet to file.
+ *
+ * @param resource|string $pFilename
+ */
+ public function save($pFilename): void
+ {
+ // garbage collect
+ $this->spreadsheet->garbageCollect();
+
+ $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
+ Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
+ $saveDateReturnType = Functions::getReturnDateType();
+ Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
+
+ // initialize colors array
+ $this->colors = [];
+
+ // Initialise workbook writer
+ $this->writerWorkbook = new Xls\Workbook($this->spreadsheet, $this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser);
+
+ // Initialise worksheet writers
+ $countSheets = $this->spreadsheet->getSheetCount();
+ for ($i = 0; $i < $countSheets; ++$i) {
+ $this->writerWorksheets[$i] = new Xls\Worksheet($this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser, $this->preCalculateFormulas, $this->spreadsheet->getSheet($i));
+ }
+
+ // build Escher objects. Escher objects for workbooks needs to be build before Escher object for workbook.
+ $this->buildWorksheetEschers();
+ $this->buildWorkbookEscher();
+
+ // add 15 identical cell style Xfs
+ // for now, we use the first cellXf instead of cellStyleXf
+ $cellXfCollection = $this->spreadsheet->getCellXfCollection();
+ for ($i = 0; $i < 15; ++$i) {
+ $this->writerWorkbook->addXfWriter($cellXfCollection[0], true);
+ }
+
+ // add all the cell Xfs
+ foreach ($this->spreadsheet->getCellXfCollection() as $style) {
+ $this->writerWorkbook->addXfWriter($style, false);
+ }
+
+ // add fonts from rich text eleemnts
+ for ($i = 0; $i < $countSheets; ++$i) {
+ foreach ($this->writerWorksheets[$i]->phpSheet->getCoordinates() as $coordinate) {
+ $cell = $this->writerWorksheets[$i]->phpSheet->getCell($coordinate);
+ $cVal = $cell->getValue();
+ if ($cVal instanceof RichText) {
+ $elements = $cVal->getRichTextElements();
+ foreach ($elements as $element) {
+ if ($element instanceof Run) {
+ $font = $element->getFont();
+ $this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
+ }
+ }
+ }
+ }
+ }
+
+ // initialize OLE file
+ $workbookStreamName = 'Workbook';
+ $OLE = new File(OLE::ascToUcs($workbookStreamName));
+
+ // Write the worksheet streams before the global workbook stream,
+ // because the byte sizes of these are needed in the global workbook stream
+ $worksheetSizes = [];
+ for ($i = 0; $i < $countSheets; ++$i) {
+ $this->writerWorksheets[$i]->close();
+ $worksheetSizes[] = $this->writerWorksheets[$i]->_datasize;
+ }
+
+ // add binary data for global workbook stream
+ $OLE->append($this->writerWorkbook->writeWorkbook($worksheetSizes));
+
+ // add binary data for sheet streams
+ for ($i = 0; $i < $countSheets; ++$i) {
+ $OLE->append($this->writerWorksheets[$i]->getData());
+ }
+
+ $this->documentSummaryInformation = $this->writeDocumentSummaryInformation();
+ // initialize OLE Document Summary Information
+ if (isset($this->documentSummaryInformation) && !empty($this->documentSummaryInformation)) {
+ $OLE_DocumentSummaryInformation = new File(OLE::ascToUcs(chr(5) . 'DocumentSummaryInformation'));
+ $OLE_DocumentSummaryInformation->append($this->documentSummaryInformation);
+ }
+
+ $this->summaryInformation = $this->writeSummaryInformation();
+ // initialize OLE Summary Information
+ if (isset($this->summaryInformation) && !empty($this->summaryInformation)) {
+ $OLE_SummaryInformation = new File(OLE::ascToUcs(chr(5) . 'SummaryInformation'));
+ $OLE_SummaryInformation->append($this->summaryInformation);
+ }
+
+ // define OLE Parts
+ $arrRootData = [$OLE];
+ // initialize OLE Properties file
+ if (isset($OLE_SummaryInformation)) {
+ $arrRootData[] = $OLE_SummaryInformation;
+ }
+ // initialize OLE Extended Properties file
+ if (isset($OLE_DocumentSummaryInformation)) {
+ $arrRootData[] = $OLE_DocumentSummaryInformation;
+ }
+
+ $time = $this->spreadsheet->getProperties()->getModified();
+ $root = new Root($time, $time, $arrRootData);
+ // save the OLE file
+ $this->openFileHandle($pFilename);
+ $root->save($this->fileHandle);
+ $this->maybeCloseFileHandle();
+
+ Functions::setReturnDateType($saveDateReturnType);
+ Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+ }
+
+ /**
+ * Build the Worksheet Escher objects.
+ */
+ private function buildWorksheetEschers(): void
+ {
+ // 1-based index to BstoreContainer
+ $blipIndex = 0;
+ $lastReducedSpId = 0;
+ $lastSpId = 0;
+
+ foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+ // sheet index
+ $sheetIndex = $sheet->getParent()->getIndex($sheet);
+
+ $escher = null;
+
+ // check if there are any shapes for this sheet
+ $filterRange = $sheet->getAutoFilter()->getRange();
+ if (count($sheet->getDrawingCollection()) == 0 && empty($filterRange)) {
+ continue;
+ }
+
+ // create intermediate Escher object
+ $escher = new Escher();
+
+ // dgContainer
+ $dgContainer = new DgContainer();
+
+ // set the drawing index (we use sheet index + 1)
+ $dgId = $sheet->getParent()->getIndex($sheet) + 1;
+ $dgContainer->setDgId($dgId);
+ $escher->setDgContainer($dgContainer);
+
+ // spgrContainer
+ $spgrContainer = new SpgrContainer();
+ $dgContainer->setSpgrContainer($spgrContainer);
+
+ // add one shape which is the group shape
+ $spContainer = new SpContainer();
+ $spContainer->setSpgr(true);
+ $spContainer->setSpType(0);
+ $spContainer->setSpId(($sheet->getParent()->getIndex($sheet) + 1) << 10);
+ $spgrContainer->addChild($spContainer);
+
+ // add the shapes
+
+ $countShapes[$sheetIndex] = 0; // count number of shapes (minus group shape), in sheet
+
+ foreach ($sheet->getDrawingCollection() as $drawing) {
+ ++$blipIndex;
+
+ ++$countShapes[$sheetIndex];
+
+ // add the shape
+ $spContainer = new SpContainer();
+
+ // set the shape type
+ $spContainer->setSpType(0x004B);
+ // set the shape flag
+ $spContainer->setSpFlag(0x02);
+
+ // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
+ $reducedSpId = $countShapes[$sheetIndex];
+ $spId = $reducedSpId | ($sheet->getParent()->getIndex($sheet) + 1) << 10;
+ $spContainer->setSpId($spId);
+
+ // keep track of last reducedSpId
+ $lastReducedSpId = $reducedSpId;
+
+ // keep track of last spId
+ $lastSpId = $spId;
+
+ // set the BLIP index
+ $spContainer->setOPT(0x4104, $blipIndex);
+
+ // set coordinates and offsets, client anchor
+ $coordinates = $drawing->getCoordinates();
+ $offsetX = $drawing->getOffsetX();
+ $offsetY = $drawing->getOffsetY();
+ $width = $drawing->getWidth();
+ $height = $drawing->getHeight();
+
+ $twoAnchor = \PhpOffice\PhpSpreadsheet\Shared\Xls::oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height);
+
+ $spContainer->setStartCoordinates($twoAnchor['startCoordinates']);
+ $spContainer->setStartOffsetX($twoAnchor['startOffsetX']);
+ $spContainer->setStartOffsetY($twoAnchor['startOffsetY']);
+ $spContainer->setEndCoordinates($twoAnchor['endCoordinates']);
+ $spContainer->setEndOffsetX($twoAnchor['endOffsetX']);
+ $spContainer->setEndOffsetY($twoAnchor['endOffsetY']);
+
+ $spgrContainer->addChild($spContainer);
+ }
+
+ // AutoFilters
+ if (!empty($filterRange)) {
+ $rangeBounds = Coordinate::rangeBoundaries($filterRange);
+ $iNumColStart = $rangeBounds[0][0];
+ $iNumColEnd = $rangeBounds[1][0];
+
+ $iInc = $iNumColStart;
+ while ($iInc <= $iNumColEnd) {
+ ++$countShapes[$sheetIndex];
+
+ // create an Drawing Object for the dropdown
+ $oDrawing = new BaseDrawing();
+ // get the coordinates of drawing
+ $cDrawing = Coordinate::stringFromColumnIndex($iInc) . $rangeBounds[0][1];
+ $oDrawing->setCoordinates($cDrawing);
+ $oDrawing->setWorksheet($sheet);
+
+ // add the shape
+ $spContainer = new SpContainer();
+ // set the shape type
+ $spContainer->setSpType(0x00C9);
+ // set the shape flag
+ $spContainer->setSpFlag(0x01);
+
+ // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
+ $reducedSpId = $countShapes[$sheetIndex];
+ $spId = $reducedSpId | ($sheet->getParent()->getIndex($sheet) + 1) << 10;
+ $spContainer->setSpId($spId);
+
+ // keep track of last reducedSpId
+ $lastReducedSpId = $reducedSpId;
+
+ // keep track of last spId
+ $lastSpId = $spId;
+
+ $spContainer->setOPT(0x007F, 0x01040104); // Protection -> fLockAgainstGrouping
+ $spContainer->setOPT(0x00BF, 0x00080008); // Text -> fFitTextToShape
+ $spContainer->setOPT(0x01BF, 0x00010000); // Fill Style -> fNoFillHitTest
+ $spContainer->setOPT(0x01FF, 0x00080000); // Line Style -> fNoLineDrawDash
+ $spContainer->setOPT(0x03BF, 0x000A0000); // Group Shape -> fPrint
+
+ // set coordinates and offsets, client anchor
+ $endCoordinates = Coordinate::stringFromColumnIndex($iInc);
+ $endCoordinates .= $rangeBounds[0][1] + 1;
+
+ $spContainer->setStartCoordinates($cDrawing);
+ $spContainer->setStartOffsetX(0);
+ $spContainer->setStartOffsetY(0);
+ $spContainer->setEndCoordinates($endCoordinates);
+ $spContainer->setEndOffsetX(0);
+ $spContainer->setEndOffsetY(0);
+
+ $spgrContainer->addChild($spContainer);
+ ++$iInc;
+ }
+ }
+
+ // identifier clusters, used for workbook Escher object
+ $this->IDCLs[$dgId] = $lastReducedSpId;
+
+ // set last shape index
+ $dgContainer->setLastSpId($lastSpId);
+
+ // set the Escher object
+ $this->writerWorksheets[$sheetIndex]->setEscher($escher);
+ }
+ }
+
+ private function processMemoryDrawing(BstoreContainer &$bstoreContainer, MemoryDrawing $drawing, string $renderingFunctionx): void
+ {
+ switch ($renderingFunctionx) {
+ case MemoryDrawing::RENDERING_JPEG:
+ $blipType = BSE::BLIPTYPE_JPEG;
+ $renderingFunction = 'imagejpeg';
+
+ break;
+ default:
+ $blipType = BSE::BLIPTYPE_PNG;
+ $renderingFunction = 'imagepng';
+
+ break;
+ }
+
+ ob_start();
+ call_user_func($renderingFunction, $drawing->getImageResource());
+ $blipData = ob_get_contents();
+ ob_end_clean();
+
+ $blip = new Blip();
+ $blip->setData($blipData);
+
+ $BSE = new BSE();
+ $BSE->setBlipType($blipType);
+ $BSE->setBlip($blip);
+
+ $bstoreContainer->addBSE($BSE);
+ }
+
+ private function processDrawing(BstoreContainer &$bstoreContainer, Drawing $drawing): void
+ {
+ $blipType = null;
+ $blipData = '';
+ $filename = $drawing->getPath();
+
+ [$imagesx, $imagesy, $imageFormat] = getimagesize($filename);
+
+ switch ($imageFormat) {
+ case 1: // GIF, not supported by BIFF8, we convert to PNG
+ $blipType = BSE::BLIPTYPE_PNG;
+ ob_start();
+ imagepng(imagecreatefromgif($filename));
+ $blipData = ob_get_contents();
+ ob_end_clean();
+
+ break;
+ case 2: // JPEG
+ $blipType = BSE::BLIPTYPE_JPEG;
+ $blipData = file_get_contents($filename);
+
+ break;
+ case 3: // PNG
+ $blipType = BSE::BLIPTYPE_PNG;
+ $blipData = file_get_contents($filename);
+
+ break;
+ case 6: // Windows DIB (BMP), we convert to PNG
+ $blipType = BSE::BLIPTYPE_PNG;
+ ob_start();
+ imagepng(SharedDrawing::imagecreatefrombmp($filename));
+ $blipData = ob_get_contents();
+ ob_end_clean();
+
+ break;
+ }
+ if ($blipData) {
+ $blip = new Blip();
+ $blip->setData($blipData);
+
+ $BSE = new BSE();
+ $BSE->setBlipType($blipType);
+ $BSE->setBlip($blip);
+
+ $bstoreContainer->addBSE($BSE);
+ }
+ }
+
+ private function processBaseDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void
+ {
+ if ($drawing instanceof Drawing) {
+ $this->processDrawing($bstoreContainer, $drawing);
+ } elseif ($drawing instanceof MemoryDrawing) {
+ $this->processMemoryDrawing($bstoreContainer, $drawing, $drawing->getRenderingFunction());
+ }
+ }
+
+ private function checkForDrawings(): bool
+ {
+ // any drawings in this workbook?
+ $found = false;
+ foreach ($this->spreadsheet->getAllSheets() as $sheet) {
+ if (count($sheet->getDrawingCollection()) > 0) {
+ $found = true;
+
+ break;
+ }
+ }
+
+ return $found;
+ }
+
+ /**
+ * Build the Escher object corresponding to the MSODRAWINGGROUP record.
+ */
+ private function buildWorkbookEscher(): void
+ {
+ // nothing to do if there are no drawings
+ if (!$this->checkForDrawings()) {
+ return;
+ }
+
+ // if we reach here, then there are drawings in the workbook
+ $escher = new Escher();
+
+ // dggContainer
+ $dggContainer = new DggContainer();
+ $escher->setDggContainer($dggContainer);
+
+ // set IDCLs (identifier clusters)
+ $dggContainer->setIDCLs($this->IDCLs);
+
+ // this loop is for determining maximum shape identifier of all drawing
+ $spIdMax = 0;
+ $totalCountShapes = 0;
+ $countDrawings = 0;
+
+ foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+ $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet
+
+ $addCount = 0;
+ foreach ($sheet->getDrawingCollection() as $drawing) {
+ $addCount = 1;
+ ++$sheetCountShapes;
+ ++$totalCountShapes;
+
+ $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10;
+ $spIdMax = max($spId, $spIdMax);
+ }
+ $countDrawings += $addCount;
+ }
+
+ $dggContainer->setSpIdMax($spIdMax + 1);
+ $dggContainer->setCDgSaved($countDrawings);
+ $dggContainer->setCSpSaved($totalCountShapes + $countDrawings); // total number of shapes incl. one group shapes per drawing
+
+ // bstoreContainer
+ $bstoreContainer = new BstoreContainer();
+ $dggContainer->setBstoreContainer($bstoreContainer);
+
+ // the BSE's (all the images)
+ foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+ foreach ($sheet->getDrawingCollection() as $drawing) {
+ $this->processBaseDrawing($bstoreContainer, $drawing);
+ }
+ }
+
+ // Set the Escher object
+ $this->writerWorkbook->setEscher($escher);
+ }
+
+ /**
+ * Build the OLE Part for DocumentSummary Information.
+ *
+ * @return string
+ */
+ private function writeDocumentSummaryInformation()
+ {
+ // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
+ $data = pack('v', 0xFFFE);
+ // offset: 2; size: 2;
+ $data .= pack('v', 0x0000);
+ // offset: 4; size: 2; OS version
+ $data .= pack('v', 0x0106);
+ // offset: 6; size: 2; OS indicator
+ $data .= pack('v', 0x0002);
+ // offset: 8; size: 16
+ $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
+ // offset: 24; size: 4; section count
+ $data .= pack('V', 0x0001);
+
+ // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
+ $data .= pack('vvvvvvvv', 0xD502, 0xD5CD, 0x2E9C, 0x101B, 0x9793, 0x0008, 0x2C2B, 0xAEF9);
+ // offset: 44; size: 4; offset of the start
+ $data .= pack('V', 0x30);
+
+ // SECTION
+ $dataSection = [];
+ $dataSection_NumProps = 0;
+ $dataSection_Summary = '';
+ $dataSection_Content = '';
+
+ // GKPIDDSI_CODEPAGE: CodePage
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x01],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
+ 'data' => ['data' => 1252],
+ ];
+ ++$dataSection_NumProps;
+
+ // GKPIDDSI_CATEGORY : Category
+ $dataProp = $this->spreadsheet->getProperties()->getCategory();
+ if ($dataProp) {
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x02],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x1E],
+ 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+ ];
+ ++$dataSection_NumProps;
+ }
+ // GKPIDDSI_VERSION :Version of the application that wrote the property storage
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x17],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x03],
+ 'data' => ['pack' => 'V', 'data' => 0x000C0000],
+ ];
+ ++$dataSection_NumProps;
+ // GKPIDDSI_SCALE : FALSE
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x0B],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x0B],
+ 'data' => ['data' => false],
+ ];
+ ++$dataSection_NumProps;
+ // GKPIDDSI_LINKSDIRTY : True if any of the values for the linked properties have changed outside of the application
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x10],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x0B],
+ 'data' => ['data' => false],
+ ];
+ ++$dataSection_NumProps;
+ // GKPIDDSI_SHAREDOC : FALSE
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x13],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x0B],
+ 'data' => ['data' => false],
+ ];
+ ++$dataSection_NumProps;
+ // GKPIDDSI_HYPERLINKSCHANGED : True if any of the values for the _PID_LINKS (hyperlink text) have changed outside of the application
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x16],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x0B],
+ 'data' => ['data' => false],
+ ];
+ ++$dataSection_NumProps;
+
+ // GKPIDDSI_DOCSPARTS
+ // MS-OSHARED p75 (2.3.3.2.2.1)
+ // Structure is VtVecUnalignedLpstrValue (2.3.3.1.9)
+ // cElements
+ $dataProp = pack('v', 0x0001);
+ $dataProp .= pack('v', 0x0000);
+ // array of UnalignedLpstr
+ // cch
+ $dataProp .= pack('v', 0x000A);
+ $dataProp .= pack('v', 0x0000);
+ // value
+ $dataProp .= 'Worksheet' . chr(0);
+
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x0D],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x101E],
+ 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+ ];
+ ++$dataSection_NumProps;
+
+ // GKPIDDSI_HEADINGPAIR
+ // VtVecHeadingPairValue
+ // cElements
+ $dataProp = pack('v', 0x0002);
+ $dataProp .= pack('v', 0x0000);
+ // Array of vtHeadingPair
+ // vtUnalignedString - headingString
+ // stringType
+ $dataProp .= pack('v', 0x001E);
+ // padding
+ $dataProp .= pack('v', 0x0000);
+ // UnalignedLpstr
+ // cch
+ $dataProp .= pack('v', 0x0013);
+ $dataProp .= pack('v', 0x0000);
+ // value
+ $dataProp .= 'Feuilles de calcul';
+ // vtUnalignedString - headingParts
+ // wType : 0x0003 = 32 bit signed integer
+ $dataProp .= pack('v', 0x0300);
+ // padding
+ $dataProp .= pack('v', 0x0000);
+ // value
+ $dataProp .= pack('v', 0x0100);
+ $dataProp .= pack('v', 0x0000);
+ $dataProp .= pack('v', 0x0000);
+ $dataProp .= pack('v', 0x0000);
+
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x0C],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x100C],
+ 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+ ];
+ ++$dataSection_NumProps;
+
+ // 4 Section Length
+ // 4 Property count
+ // 8 * $dataSection_NumProps (8 = ID (4) + OffSet(4))
+ $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
+ foreach ($dataSection as $dataProp) {
+ // Summary
+ $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
+ // Offset
+ $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
+ // DataType
+ $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
+ // Data
+ if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
+ $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+ $dataSection_Content_Offset += 4 + 4;
+ } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
+ $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+ $dataSection_Content_Offset += 4 + 4;
+ } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean
+ $dataSection_Content .= pack('V', (int) $dataProp['data']['data']);
+ $dataSection_Content_Offset += 4 + 4;
+ } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
+ // Null-terminated string
+ $dataProp['data']['data'] .= chr(0);
+ // @phpstan-ignore-next-line
+ ++$dataProp['data']['length'];
+ // Complete the string with null string for being a %4
+ $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
+ $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
+
+ $dataSection_Content .= pack('V', $dataProp['data']['length']);
+ $dataSection_Content .= $dataProp['data']['data'];
+
+ $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
+ // Condition below can never be true
+ //} elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
+ // $dataSection_Content .= $dataProp['data']['data'];
+
+ // $dataSection_Content_Offset += 4 + 8;
+ } else {
+ $dataSection_Content .= $dataProp['data']['data'];
+
+ // @phpstan-ignore-next-line
+ $dataSection_Content_Offset += 4 + $dataProp['data']['length'];
+ }
+ }
+ // Now $dataSection_Content_Offset contains the size of the content
+
+ // section header
+ // offset: $secOffset; size: 4; section length
+ // + x Size of the content (summary + content)
+ $data .= pack('V', $dataSection_Content_Offset);
+ // offset: $secOffset+4; size: 4; property count
+ $data .= pack('V', $dataSection_NumProps);
+ // Section Summary
+ $data .= $dataSection_Summary;
+ // Section Content
+ $data .= $dataSection_Content;
+
+ return $data;
+ }
+
+ /**
+ * @param float|int $dataProp
+ */
+ private function writeSummaryPropOle($dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
+ {
+ if ($dataProp) {
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => $sumdata],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
+ 'data' => ['data' => OLE::localDateToOLE($dataProp)],
+ ];
+ ++$dataSection_NumProps;
+ }
+ }
+
+ private function writeSummaryProp(string $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
+ {
+ if ($dataProp) {
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => $sumdata],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
+ 'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+ ];
+ ++$dataSection_NumProps;
+ }
+ }
+
+ /**
+ * Build the OLE Part for Summary Information.
+ *
+ * @return string
+ */
+ private function writeSummaryInformation()
+ {
+ // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
+ $data = pack('v', 0xFFFE);
+ // offset: 2; size: 2;
+ $data .= pack('v', 0x0000);
+ // offset: 4; size: 2; OS version
+ $data .= pack('v', 0x0106);
+ // offset: 6; size: 2; OS indicator
+ $data .= pack('v', 0x0002);
+ // offset: 8; size: 16
+ $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
+ // offset: 24; size: 4; section count
+ $data .= pack('V', 0x0001);
+
+ // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
+ $data .= pack('vvvvvvvv', 0x85E0, 0xF29F, 0x4FF9, 0x1068, 0x91AB, 0x0008, 0x272B, 0xD9B3);
+ // offset: 44; size: 4; offset of the start
+ $data .= pack('V', 0x30);
+
+ // SECTION
+ $dataSection = [];
+ $dataSection_NumProps = 0;
+ $dataSection_Summary = '';
+ $dataSection_Content = '';
+
+ // CodePage : CP-1252
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x01],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
+ 'data' => ['data' => 1252],
+ ];
+ ++$dataSection_NumProps;
+
+ $props = $this->spreadsheet->getProperties();
+ $this->writeSummaryProp($props->getTitle(), $dataSection_NumProps, $dataSection, 0x02, 0x1e);
+ $this->writeSummaryProp($props->getSubject(), $dataSection_NumProps, $dataSection, 0x03, 0x1e);
+ $this->writeSummaryProp($props->getCreator(), $dataSection_NumProps, $dataSection, 0x04, 0x1e);
+ $this->writeSummaryProp($props->getKeywords(), $dataSection_NumProps, $dataSection, 0x05, 0x1e);
+ $this->writeSummaryProp($props->getDescription(), $dataSection_NumProps, $dataSection, 0x06, 0x1e);
+ $this->writeSummaryProp($props->getLastModifiedBy(), $dataSection_NumProps, $dataSection, 0x08, 0x1e);
+ $this->writeSummaryPropOle($props->getCreated(), $dataSection_NumProps, $dataSection, 0x0c, 0x40);
+ $this->writeSummaryPropOle($props->getModified(), $dataSection_NumProps, $dataSection, 0x0d, 0x40);
+
+ // Security
+ $dataSection[] = [
+ 'summary' => ['pack' => 'V', 'data' => 0x13],
+ 'offset' => ['pack' => 'V'],
+ 'type' => ['pack' => 'V', 'data' => 0x03], // 4 byte signed integer
+ 'data' => ['data' => 0x00],
+ ];
+ ++$dataSection_NumProps;
+
+ // 4 Section Length
+ // 4 Property count
+ // 8 * $dataSection_NumProps (8 = ID (4) + OffSet(4))
+ $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
+ foreach ($dataSection as $dataProp) {
+ // Summary
+ $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
+ // Offset
+ $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
+ // DataType
+ $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
+ // Data
+ if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
+ $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+ $dataSection_Content_Offset += 4 + 4;
+ } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
+ $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+ $dataSection_Content_Offset += 4 + 4;
+ } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
+ // Null-terminated string
+ $dataProp['data']['data'] .= chr(0);
+ ++$dataProp['data']['length'];
+ // Complete the string with null string for being a %4
+ $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
+ $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
+
+ $dataSection_Content .= pack('V', $dataProp['data']['length']);
+ $dataSection_Content .= $dataProp['data']['data'];
+
+ $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
+ } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
+ $dataSection_Content .= $dataProp['data']['data'];
+
+ $dataSection_Content_Offset += 4 + 8;
+ }
+ // Data Type Not Used at the moment
+ }
+ // Now $dataSection_Content_Offset contains the size of the content
+
+ // section header
+ // offset: $secOffset; size: 4; section length
+ // + x Size of the content (summary + content)
+ $data .= pack('V', $dataSection_Content_Offset);
+ // offset: $secOffset+4; size: 4; property count
+ $data .= pack('V', $dataSection_NumProps);
+ // Section Summary
+ $data .= $dataSection_Summary;
+ // Section Content
+ $data .= $dataSection_Content;
+
+ return $data;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php
new file mode 100644
index 0000000..4a7e065
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php
@@ -0,0 +1,224 @@
+
+// *
+// * The majority of this is _NOT_ my code. I simply ported it from the
+// * PERL Spreadsheet::WriteExcel module.
+// *
+// * The author of the Spreadsheet::WriteExcel module is John McNamara
+// *
+// *
+// * I _DO_ maintain this code, and John McNamara has nothing to do with the
+// * porting of this code to PHP. Any questions directly related to this
+// * class library should be directed to me.
+// *
+// * License Information:
+// *
+// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
+// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// * This library is free software; you can redistribute it and/or
+// * modify it under the terms of the GNU Lesser General Public
+// * License as published by the Free Software Foundation; either
+// * version 2.1 of the License, or (at your option) any later version.
+// *
+// * This library is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// * Lesser General Public License for more details.
+// *
+// * You should have received a copy of the GNU Lesser General Public
+// * License along with this library; if not, write to the Free Software
+// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+// */
+class BIFFwriter
+{
+ /**
+ * The byte order of this architecture. 0 => little endian, 1 => big endian.
+ *
+ * @var int
+ */
+ private static $byteOrder;
+
+ /**
+ * The string containing the data of the BIFF stream.
+ *
+ * @var null|string
+ */
+ public $_data;
+
+ /**
+ * The size of the data in bytes. Should be the same as strlen($this->_data).
+ *
+ * @var int
+ */
+ public $_datasize;
+
+ /**
+ * The maximum length for a BIFF record (excluding record header and length field). See addContinue().
+ *
+ * @var int
+ *
+ * @see addContinue()
+ */
+ private $limit = 8224;
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ $this->_data = '';
+ $this->_datasize = 0;
+ }
+
+ /**
+ * Determine the byte order and store it as class data to avoid
+ * recalculating it for each call to new().
+ *
+ * @return int
+ */
+ public static function getByteOrder()
+ {
+ if (!isset(self::$byteOrder)) {
+ // Check if "pack" gives the required IEEE 64bit float
+ $teststr = pack('d', 1.2345);
+ $number = pack('C8', 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F);
+ if ($number == $teststr) {
+ $byte_order = 0; // Little Endian
+ } elseif ($number == strrev($teststr)) {
+ $byte_order = 1; // Big Endian
+ } else {
+ // Give up. I'll fix this in a later version.
+ throw new WriterException('Required floating point format not supported on this platform.');
+ }
+ self::$byteOrder = $byte_order;
+ }
+
+ return self::$byteOrder;
+ }
+
+ /**
+ * General storage function.
+ *
+ * @param string $data binary data to append
+ */
+ protected function append($data): void
+ {
+ if (strlen($data) - 4 > $this->limit) {
+ $data = $this->addContinue($data);
+ }
+ $this->_data .= $data;
+ $this->_datasize += strlen($data);
+ }
+
+ /**
+ * General storage function like append, but returns string instead of modifying $this->_data.
+ *
+ * @param string $data binary data to write
+ *
+ * @return string
+ */
+ public function writeData($data)
+ {
+ if (strlen($data) - 4 > $this->limit) {
+ $data = $this->addContinue($data);
+ }
+ $this->_datasize += strlen($data);
+
+ return $data;
+ }
+
+ /**
+ * Writes Excel BOF record to indicate the beginning of a stream or
+ * sub-stream in the BIFF file.
+ *
+ * @param int $type type of BIFF file to write: 0x0005 Workbook,
+ * 0x0010 Worksheet
+ */
+ protected function storeBof($type): void
+ {
+ $record = 0x0809; // Record identifier (BIFF5-BIFF8)
+ $length = 0x0010;
+
+ // by inspection of real files, MS Office Excel 2007 writes the following
+ $unknown = pack('VV', 0x000100D1, 0x00000406);
+
+ $build = 0x0DBB; // Excel 97
+ $year = 0x07CC; // Excel 97
+
+ $version = 0x0600; // BIFF8
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvv', $version, $type, $build, $year);
+ $this->append($header . $data . $unknown);
+ }
+
+ /**
+ * Writes Excel EOF record to indicate the end of a BIFF stream.
+ */
+ protected function storeEof(): void
+ {
+ $record = 0x000A; // Record identifier
+ $length = 0x0000; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $this->append($header);
+ }
+
+ /**
+ * Writes Excel EOF record to indicate the end of a BIFF stream.
+ */
+ public function writeEof()
+ {
+ $record = 0x000A; // Record identifier
+ $length = 0x0000; // Number of bytes to follow
+ $header = pack('vv', $record, $length);
+
+ return $this->writeData($header);
+ }
+
+ /**
+ * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In
+ * Excel 97 the limit is 8228 bytes. Records that are longer than these limits
+ * must be split up into CONTINUE blocks.
+ *
+ * This function takes a long BIFF record and inserts CONTINUE records as
+ * necessary.
+ *
+ * @param string $data The original binary data to be written
+ *
+ * @return string A very convenient string of continue blocks
+ */
+ private function addContinue($data)
+ {
+ $limit = $this->limit;
+ $record = 0x003C; // Record identifier
+
+ // The first 2080/8224 bytes remain intact. However, we have to change
+ // the length field of the record.
+ $tmp = substr($data, 0, 2) . pack('v', $limit) . substr($data, 4, $limit);
+
+ $header = pack('vv', $record, $limit); // Headers for continue records
+
+ // Retrieve chunks of 2080/8224 bytes +4 for the header.
+ $data_length = strlen($data);
+ for ($i = $limit + 4; $i < ($data_length - $limit); $i += $limit) {
+ $tmp .= $header;
+ $tmp .= substr($data, $i, $limit);
+ }
+
+ // Retrieve the last chunk of data
+ $header = pack('vv', $record, strlen($data) - $i);
+ $tmp .= $header;
+ $tmp .= substr($data, $i);
+
+ return $tmp;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/CellDataValidation.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/CellDataValidation.php
new file mode 100644
index 0000000..7e9b3cf
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/CellDataValidation.php
@@ -0,0 +1,78 @@
+
+ */
+ protected static $validationTypeMap = [
+ DataValidation::TYPE_NONE => 0x00,
+ DataValidation::TYPE_WHOLE => 0x01,
+ DataValidation::TYPE_DECIMAL => 0x02,
+ DataValidation::TYPE_LIST => 0x03,
+ DataValidation::TYPE_DATE => 0x04,
+ DataValidation::TYPE_TIME => 0x05,
+ DataValidation::TYPE_TEXTLENGTH => 0x06,
+ DataValidation::TYPE_CUSTOM => 0x07,
+ ];
+
+ /**
+ * @var array
+ */
+ protected static $errorStyleMap = [
+ DataValidation::STYLE_STOP => 0x00,
+ DataValidation::STYLE_WARNING => 0x01,
+ DataValidation::STYLE_INFORMATION => 0x02,
+ ];
+
+ /**
+ * @var array
+ */
+ protected static $operatorMap = [
+ DataValidation::OPERATOR_BETWEEN => 0x00,
+ DataValidation::OPERATOR_NOTBETWEEN => 0x01,
+ DataValidation::OPERATOR_EQUAL => 0x02,
+ DataValidation::OPERATOR_NOTEQUAL => 0x03,
+ DataValidation::OPERATOR_GREATERTHAN => 0x04,
+ DataValidation::OPERATOR_LESSTHAN => 0x05,
+ DataValidation::OPERATOR_GREATERTHANOREQUAL => 0x06,
+ DataValidation::OPERATOR_LESSTHANOREQUAL => 0x07,
+ ];
+
+ public static function type(DataValidation $dataValidation): int
+ {
+ $validationType = $dataValidation->getType();
+
+ if (is_string($validationType) && array_key_exists($validationType, self::$validationTypeMap)) {
+ return self::$validationTypeMap[$validationType];
+ }
+
+ return self::$validationTypeMap[DataValidation::TYPE_NONE];
+ }
+
+ public static function errorStyle(DataValidation $dataValidation): int
+ {
+ $errorStyle = $dataValidation->getErrorStyle();
+
+ if (is_string($errorStyle) && array_key_exists($errorStyle, self::$errorStyleMap)) {
+ return self::$errorStyleMap[$errorStyle];
+ }
+
+ return self::$errorStyleMap[DataValidation::STYLE_STOP];
+ }
+
+ public static function operator(DataValidation $dataValidation): int
+ {
+ $operator = $dataValidation->getOperator();
+
+ if (is_string($operator) && array_key_exists($operator, self::$operatorMap)) {
+ return self::$operatorMap[$operator];
+ }
+
+ return self::$operatorMap[DataValidation::OPERATOR_BETWEEN];
+ }
+}
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/Xls/Style/CellAlignment.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellAlignment.php
new file mode 100644
index 0000000..711d88d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellAlignment.php
@@ -0,0 +1,59 @@
+
+ */
+ private static $horizontalMap = [
+ Alignment::HORIZONTAL_GENERAL => 0,
+ Alignment::HORIZONTAL_LEFT => 1,
+ Alignment::HORIZONTAL_RIGHT => 3,
+ Alignment::HORIZONTAL_CENTER => 2,
+ Alignment::HORIZONTAL_CENTER_CONTINUOUS => 6,
+ Alignment::HORIZONTAL_JUSTIFY => 5,
+ ];
+
+ /**
+ * @var array
+ */
+ private static $verticalMap = [
+ Alignment::VERTICAL_BOTTOM => 2,
+ Alignment::VERTICAL_TOP => 0,
+ Alignment::VERTICAL_CENTER => 1,
+ Alignment::VERTICAL_JUSTIFY => 3,
+ ];
+
+ public static function horizontal(Alignment $alignment): int
+ {
+ $horizontalAlignment = $alignment->getHorizontal();
+
+ if (is_string($horizontalAlignment) && array_key_exists($horizontalAlignment, self::$horizontalMap)) {
+ return self::$horizontalMap[$horizontalAlignment];
+ }
+
+ return self::$horizontalMap[Alignment::HORIZONTAL_GENERAL];
+ }
+
+ public static function wrap(Alignment $alignment): int
+ {
+ $wrap = $alignment->getWrapText();
+
+ return ($wrap === true) ? 1 : 0;
+ }
+
+ public static function vertical(Alignment $alignment): int
+ {
+ $verticalAlignment = $alignment->getVertical();
+
+ if (is_string($verticalAlignment) && array_key_exists($verticalAlignment, self::$verticalMap)) {
+ return self::$verticalMap[$verticalAlignment];
+ }
+
+ return self::$verticalMap[Alignment::VERTICAL_BOTTOM];
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php
new file mode 100644
index 0000000..8d47d6a
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php
@@ -0,0 +1,39 @@
+
+ */
+ protected static $styleMap = [
+ Border::BORDER_NONE => 0x00,
+ Border::BORDER_THIN => 0x01,
+ Border::BORDER_MEDIUM => 0x02,
+ Border::BORDER_DASHED => 0x03,
+ Border::BORDER_DOTTED => 0x04,
+ Border::BORDER_THICK => 0x05,
+ Border::BORDER_DOUBLE => 0x06,
+ Border::BORDER_HAIR => 0x07,
+ Border::BORDER_MEDIUMDASHED => 0x08,
+ Border::BORDER_DASHDOT => 0x09,
+ Border::BORDER_MEDIUMDASHDOT => 0x0A,
+ Border::BORDER_DASHDOTDOT => 0x0B,
+ Border::BORDER_MEDIUMDASHDOTDOT => 0x0C,
+ Border::BORDER_SLANTDASHDOT => 0x0D,
+ ];
+
+ public static function style(Border $border): int
+ {
+ $borderStyle = $border->getBorderStyle();
+
+ if (is_string($borderStyle) && array_key_exists($borderStyle, self::$styleMap)) {
+ return self::$styleMap[$borderStyle];
+ }
+
+ return self::$styleMap[Border::BORDER_NONE];
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellFill.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellFill.php
new file mode 100644
index 0000000..f5a8c47
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellFill.php
@@ -0,0 +1,46 @@
+
+ */
+ protected static $fillStyleMap = [
+ Fill::FILL_NONE => 0x00,
+ Fill::FILL_SOLID => 0x01,
+ Fill::FILL_PATTERN_MEDIUMGRAY => 0x02,
+ Fill::FILL_PATTERN_DARKGRAY => 0x03,
+ Fill::FILL_PATTERN_LIGHTGRAY => 0x04,
+ Fill::FILL_PATTERN_DARKHORIZONTAL => 0x05,
+ Fill::FILL_PATTERN_DARKVERTICAL => 0x06,
+ Fill::FILL_PATTERN_DARKDOWN => 0x07,
+ Fill::FILL_PATTERN_DARKUP => 0x08,
+ Fill::FILL_PATTERN_DARKGRID => 0x09,
+ Fill::FILL_PATTERN_DARKTRELLIS => 0x0A,
+ Fill::FILL_PATTERN_LIGHTHORIZONTAL => 0x0B,
+ Fill::FILL_PATTERN_LIGHTVERTICAL => 0x0C,
+ Fill::FILL_PATTERN_LIGHTDOWN => 0x0D,
+ Fill::FILL_PATTERN_LIGHTUP => 0x0E,
+ Fill::FILL_PATTERN_LIGHTGRID => 0x0F,
+ Fill::FILL_PATTERN_LIGHTTRELLIS => 0x10,
+ Fill::FILL_PATTERN_GRAY125 => 0x11,
+ Fill::FILL_PATTERN_GRAY0625 => 0x12,
+ Fill::FILL_GRADIENT_LINEAR => 0x00, // does not exist in BIFF8
+ Fill::FILL_GRADIENT_PATH => 0x00, // does not exist in BIFF8
+ ];
+
+ public static function style(Fill $fill): int
+ {
+ $fillStyle = $fill->getFillType();
+
+ if (is_string($fillStyle) && array_key_exists($fillStyle, self::$fillStyleMap)) {
+ return self::$fillStyleMap[$fillStyle];
+ }
+
+ return self::$fillStyleMap[Fill::FILL_NONE];
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php
new file mode 100644
index 0000000..caf85c0
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php
@@ -0,0 +1,90 @@
+
+ */
+ private static $colorMap = [
+ '#000000' => 0x08,
+ '#FFFFFF' => 0x09,
+ '#FF0000' => 0x0A,
+ '#00FF00' => 0x0B,
+ '#0000FF' => 0x0C,
+ '#FFFF00' => 0x0D,
+ '#FF00FF' => 0x0E,
+ '#00FFFF' => 0x0F,
+ '#800000' => 0x10,
+ '#008000' => 0x11,
+ '#000080' => 0x12,
+ '#808000' => 0x13,
+ '#800080' => 0x14,
+ '#008080' => 0x15,
+ '#C0C0C0' => 0x16,
+ '#808080' => 0x17,
+ '#9999FF' => 0x18,
+ '#993366' => 0x19,
+ '#FFFFCC' => 0x1A,
+ '#CCFFFF' => 0x1B,
+ '#660066' => 0x1C,
+ '#FF8080' => 0x1D,
+ '#0066CC' => 0x1E,
+ '#CCCCFF' => 0x1F,
+ // '#000080' => 0x20,
+ // '#FF00FF' => 0x21,
+ // '#FFFF00' => 0x22,
+ // '#00FFFF' => 0x23,
+ // '#800080' => 0x24,
+ // '#800000' => 0x25,
+ // '#008080' => 0x26,
+ // '#0000FF' => 0x27,
+ '#00CCFF' => 0x28,
+ // '#CCFFFF' => 0x29,
+ '#CCFFCC' => 0x2A,
+ '#FFFF99' => 0x2B,
+ '#99CCFF' => 0x2C,
+ '#FF99CC' => 0x2D,
+ '#CC99FF' => 0x2E,
+ '#FFCC99' => 0x2F,
+ '#3366FF' => 0x30,
+ '#33CCCC' => 0x31,
+ '#99CC00' => 0x32,
+ '#FFCC00' => 0x33,
+ '#FF9900' => 0x34,
+ '#FF6600' => 0x35,
+ '#666699' => 0x36,
+ '#969696' => 0x37,
+ '#003366' => 0x38,
+ '#339966' => 0x39,
+ '#003300' => 0x3A,
+ '#333300' => 0x3B,
+ '#993300' => 0x3C,
+ // '#993366' => 0x3D,
+ '#333399' => 0x3E,
+ '#333333' => 0x3F,
+ ];
+
+ public static function lookup(Color $color, int $defaultIndex = 0x00): int
+ {
+ $colorRgb = $color->getRGB();
+ if (is_string($colorRgb) && array_key_exists("#{$colorRgb}", self::$colorMap)) {
+ return self::$colorMap["#{$colorRgb}"];
+ }
+
+// TODO Try and map RGB value to nearest colour within the define pallette
+// $red = Color::getRed($colorRgb, false);
+// $green = Color::getGreen($colorRgb, false);
+// $blue = Color::getBlue($colorRgb, false);
+
+// $paletteSpace = 3;
+// $newColor = ($red * $paletteSpace / 256) * ($paletteSpace * $paletteSpace) +
+// ($green * $paletteSpace / 256) * $paletteSpace +
+// ($blue * $paletteSpace / 256);
+
+ return $defaultIndex;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php
new file mode 100644
index 0000000..a917185
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php
@@ -0,0 +1,1190 @@
+
+// *
+// * The majority of this is _NOT_ my code. I simply ported it from the
+// * PERL Spreadsheet::WriteExcel module.
+// *
+// * The author of the Spreadsheet::WriteExcel module is John McNamara
+// *
+// *
+// * I _DO_ maintain this code, and John McNamara has nothing to do with the
+// * porting of this code to PHP. Any questions directly related to this
+// * class library should be directed to me.
+// *
+// * License Information:
+// *
+// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
+// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// * This library is free software; you can redistribute it and/or
+// * modify it under the terms of the GNU Lesser General Public
+// * License as published by the Free Software Foundation; either
+// * version 2.1 of the License, or (at your option) any later version.
+// *
+// * This library is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// * Lesser General Public License for more details.
+// *
+// * You should have received a copy of the GNU Lesser General Public
+// * License along with this library; if not, write to the Free Software
+// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+// */
+class Workbook extends BIFFwriter
+{
+ /**
+ * Formula parser.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
+ */
+ private $parser;
+
+ /**
+ * The BIFF file size for the workbook.
+ *
+ * @var int
+ *
+ * @see calcSheetOffsets()
+ */
+ private $biffSize;
+
+ /**
+ * XF Writers.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
+ */
+ private $xfWriters = [];
+
+ /**
+ * Array containing the colour palette.
+ *
+ * @var array
+ */
+ private $palette;
+
+ /**
+ * The codepage indicates the text encoding used for strings.
+ *
+ * @var int
+ */
+ private $codepage;
+
+ /**
+ * The country code used for localization.
+ *
+ * @var int
+ */
+ private $countryCode;
+
+ /**
+ * Workbook.
+ *
+ * @var Spreadsheet
+ */
+ private $spreadsheet;
+
+ /**
+ * Fonts writers.
+ *
+ * @var Font[]
+ */
+ private $fontWriters = [];
+
+ /**
+ * Added fonts. Maps from font's hash => index in workbook.
+ *
+ * @var array
+ */
+ private $addedFonts = [];
+
+ /**
+ * Shared number formats.
+ *
+ * @var array
+ */
+ private $numberFormats = [];
+
+ /**
+ * Added number formats. Maps from numberFormat's hash => index in workbook.
+ *
+ * @var array
+ */
+ private $addedNumberFormats = [];
+
+ /**
+ * Sizes of the binary worksheet streams.
+ *
+ * @var array
+ */
+ private $worksheetSizes = [];
+
+ /**
+ * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
+ *
+ * @var array
+ */
+ private $worksheetOffsets = [];
+
+ /**
+ * Total number of shared strings in workbook.
+ *
+ * @var int
+ */
+ private $stringTotal;
+
+ /**
+ * Number of unique shared strings in workbook.
+ *
+ * @var int
+ */
+ private $stringUnique;
+
+ /**
+ * Array of unique shared strings in workbook.
+ *
+ * @var array
+ */
+ private $stringTable;
+
+ /**
+ * Color cache.
+ */
+ private $colors;
+
+ /**
+ * Escher object corresponding to MSODRAWINGGROUP.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ private $escher;
+
+ /**
+ * Class constructor.
+ *
+ * @param Spreadsheet $spreadsheet The Workbook
+ * @param int $str_total Total number of strings
+ * @param int $str_unique Total number of unique strings
+ * @param array $str_table String Table
+ * @param array $colors Colour Table
+ * @param Parser $parser The formula parser created for the Workbook
+ */
+ public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
+ {
+ // It needs to call its parent's constructor explicitly
+ parent::__construct();
+
+ $this->parser = $parser;
+ $this->biffSize = 0;
+ $this->palette = [];
+ $this->countryCode = -1;
+
+ $this->stringTotal = &$str_total;
+ $this->stringUnique = &$str_unique;
+ $this->stringTable = &$str_table;
+ $this->colors = &$colors;
+ $this->setPaletteXl97();
+
+ $this->spreadsheet = $spreadsheet;
+
+ $this->codepage = 0x04B0;
+
+ // Add empty sheets and Build color cache
+ $countSheets = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $countSheets; ++$i) {
+ $phpSheet = $spreadsheet->getSheet($i);
+
+ $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
+
+ $supbook_index = 0x00;
+ $ref = pack('vvv', $supbook_index, $i, $i);
+ $this->parser->references[] = $ref; // Register reference with parser
+
+ // Sheet tab colors?
+ if ($phpSheet->isTabColorSet()) {
+ $this->addColor($phpSheet->getTabColor()->getRGB());
+ }
+ }
+ }
+
+ /**
+ * Add a new XF writer.
+ *
+ * @param bool $isStyleXf Is it a style XF?
+ *
+ * @return int Index to XF record
+ */
+ public function addXfWriter(Style $style, $isStyleXf = false)
+ {
+ $xfWriter = new Xf($style);
+ $xfWriter->setIsStyleXf($isStyleXf);
+
+ // Add the font if not already added
+ $fontIndex = $this->addFont($style->getFont());
+
+ // Assign the font index to the xf record
+ $xfWriter->setFontIndex($fontIndex);
+
+ // Background colors, best to treat these after the font so black will come after white in custom palette
+ $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
+ $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
+ $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
+ $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
+ $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
+ $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
+ $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
+
+ // Add the number format if it is not a built-in one and not already added
+ if ($style->getNumberFormat()->getBuiltInFormatCode() === false) {
+ $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
+
+ if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
+ $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
+ } else {
+ $numberFormatIndex = 164 + count($this->numberFormats);
+ $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
+ $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
+ }
+ } else {
+ $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
+ }
+
+ // Assign the number format index to xf record
+ $xfWriter->setNumberFormatIndex($numberFormatIndex);
+
+ $this->xfWriters[] = $xfWriter;
+
+ return count($this->xfWriters) - 1;
+ }
+
+ /**
+ * Add a font to added fonts.
+ *
+ * @return int Index to FONT record
+ */
+ public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
+ {
+ $fontHashCode = $font->getHashCode();
+ if (isset($this->addedFonts[$fontHashCode])) {
+ $fontIndex = $this->addedFonts[$fontHashCode];
+ } else {
+ $countFonts = count($this->fontWriters);
+ $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
+
+ $fontWriter = new Font($font);
+ $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
+ $this->fontWriters[] = $fontWriter;
+
+ $this->addedFonts[$fontHashCode] = $fontIndex;
+ }
+
+ return $fontIndex;
+ }
+
+ /**
+ * Alter color palette adding a custom color.
+ *
+ * @param string $rgb E.g. 'FF00AA'
+ *
+ * @return int Color index
+ */
+ private function addColor($rgb)
+ {
+ if (!isset($this->colors[$rgb])) {
+ $color =
+ [
+ hexdec(substr($rgb, 0, 2)),
+ hexdec(substr($rgb, 2, 2)),
+ hexdec(substr($rgb, 4)),
+ 0,
+ ];
+ $colorIndex = array_search($color, $this->palette);
+ if ($colorIndex) {
+ $this->colors[$rgb] = $colorIndex;
+ } else {
+ if (count($this->colors) === 0) {
+ $lastColor = 7;
+ } else {
+ $lastColor = end($this->colors);
+ }
+ if ($lastColor < 57) {
+ // then we add a custom color altering the palette
+ $colorIndex = $lastColor + 1;
+ $this->palette[$colorIndex] = $color;
+ $this->colors[$rgb] = $colorIndex;
+ } else {
+ // no room for more custom colors, just map to black
+ $colorIndex = 0;
+ }
+ }
+ } else {
+ // fetch already added custom color
+ $colorIndex = $this->colors[$rgb];
+ }
+
+ return $colorIndex;
+ }
+
+ /**
+ * Sets the colour palette to the Excel 97+ default.
+ */
+ private function setPaletteXl97(): void
+ {
+ $this->palette = [
+ 0x08 => [0x00, 0x00, 0x00, 0x00],
+ 0x09 => [0xff, 0xff, 0xff, 0x00],
+ 0x0A => [0xff, 0x00, 0x00, 0x00],
+ 0x0B => [0x00, 0xff, 0x00, 0x00],
+ 0x0C => [0x00, 0x00, 0xff, 0x00],
+ 0x0D => [0xff, 0xff, 0x00, 0x00],
+ 0x0E => [0xff, 0x00, 0xff, 0x00],
+ 0x0F => [0x00, 0xff, 0xff, 0x00],
+ 0x10 => [0x80, 0x00, 0x00, 0x00],
+ 0x11 => [0x00, 0x80, 0x00, 0x00],
+ 0x12 => [0x00, 0x00, 0x80, 0x00],
+ 0x13 => [0x80, 0x80, 0x00, 0x00],
+ 0x14 => [0x80, 0x00, 0x80, 0x00],
+ 0x15 => [0x00, 0x80, 0x80, 0x00],
+ 0x16 => [0xc0, 0xc0, 0xc0, 0x00],
+ 0x17 => [0x80, 0x80, 0x80, 0x00],
+ 0x18 => [0x99, 0x99, 0xff, 0x00],
+ 0x19 => [0x99, 0x33, 0x66, 0x00],
+ 0x1A => [0xff, 0xff, 0xcc, 0x00],
+ 0x1B => [0xcc, 0xff, 0xff, 0x00],
+ 0x1C => [0x66, 0x00, 0x66, 0x00],
+ 0x1D => [0xff, 0x80, 0x80, 0x00],
+ 0x1E => [0x00, 0x66, 0xcc, 0x00],
+ 0x1F => [0xcc, 0xcc, 0xff, 0x00],
+ 0x20 => [0x00, 0x00, 0x80, 0x00],
+ 0x21 => [0xff, 0x00, 0xff, 0x00],
+ 0x22 => [0xff, 0xff, 0x00, 0x00],
+ 0x23 => [0x00, 0xff, 0xff, 0x00],
+ 0x24 => [0x80, 0x00, 0x80, 0x00],
+ 0x25 => [0x80, 0x00, 0x00, 0x00],
+ 0x26 => [0x00, 0x80, 0x80, 0x00],
+ 0x27 => [0x00, 0x00, 0xff, 0x00],
+ 0x28 => [0x00, 0xcc, 0xff, 0x00],
+ 0x29 => [0xcc, 0xff, 0xff, 0x00],
+ 0x2A => [0xcc, 0xff, 0xcc, 0x00],
+ 0x2B => [0xff, 0xff, 0x99, 0x00],
+ 0x2C => [0x99, 0xcc, 0xff, 0x00],
+ 0x2D => [0xff, 0x99, 0xcc, 0x00],
+ 0x2E => [0xcc, 0x99, 0xff, 0x00],
+ 0x2F => [0xff, 0xcc, 0x99, 0x00],
+ 0x30 => [0x33, 0x66, 0xff, 0x00],
+ 0x31 => [0x33, 0xcc, 0xcc, 0x00],
+ 0x32 => [0x99, 0xcc, 0x00, 0x00],
+ 0x33 => [0xff, 0xcc, 0x00, 0x00],
+ 0x34 => [0xff, 0x99, 0x00, 0x00],
+ 0x35 => [0xff, 0x66, 0x00, 0x00],
+ 0x36 => [0x66, 0x66, 0x99, 0x00],
+ 0x37 => [0x96, 0x96, 0x96, 0x00],
+ 0x38 => [0x00, 0x33, 0x66, 0x00],
+ 0x39 => [0x33, 0x99, 0x66, 0x00],
+ 0x3A => [0x00, 0x33, 0x00, 0x00],
+ 0x3B => [0x33, 0x33, 0x00, 0x00],
+ 0x3C => [0x99, 0x33, 0x00, 0x00],
+ 0x3D => [0x99, 0x33, 0x66, 0x00],
+ 0x3E => [0x33, 0x33, 0x99, 0x00],
+ 0x3F => [0x33, 0x33, 0x33, 0x00],
+ ];
+ }
+
+ /**
+ * Assemble worksheets into a workbook and send the BIFF data to an OLE
+ * storage.
+ *
+ * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams
+ *
+ * @return string Binary data for workbook stream
+ */
+ public function writeWorkbook(array $pWorksheetSizes)
+ {
+ $this->worksheetSizes = $pWorksheetSizes;
+
+ // Calculate the number of selected worksheet tabs and call the finalization
+ // methods for each worksheet
+ $total_worksheets = $this->spreadsheet->getSheetCount();
+
+ // Add part 1 of the Workbook globals, what goes before the SHEET records
+ $this->storeBof(0x0005);
+ $this->writeCodepage();
+ $this->writeWindow1();
+
+ $this->writeDateMode();
+ $this->writeAllFonts();
+ $this->writeAllNumberFormats();
+ $this->writeAllXfs();
+ $this->writeAllStyles();
+ $this->writePalette();
+
+ // Prepare part 3 of the workbook global stream, what goes after the SHEET records
+ $part3 = '';
+ if ($this->countryCode !== -1) {
+ $part3 .= $this->writeCountry();
+ }
+ $part3 .= $this->writeRecalcId();
+
+ $part3 .= $this->writeSupbookInternal();
+ /* TODO: store external SUPBOOK records and XCT and CRN records
+ in case of external references for BIFF8 */
+ $part3 .= $this->writeExternalsheetBiff8();
+ $part3 .= $this->writeAllDefinedNamesBiff8();
+ $part3 .= $this->writeMsoDrawingGroup();
+ $part3 .= $this->writeSharedStringsTable();
+
+ $part3 .= $this->writeEof();
+
+ // Add part 2 of the Workbook globals, the SHEET records
+ $this->calcSheetOffsets();
+ for ($i = 0; $i < $total_worksheets; ++$i) {
+ $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
+ }
+
+ // Add part 3 of the Workbook globals
+ $this->_data .= $part3;
+
+ return $this->_data;
+ }
+
+ /**
+ * Calculate offsets for Worksheet BOF records.
+ */
+ private function calcSheetOffsets(): void
+ {
+ $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
+
+ // size of Workbook globals part 1 + 3
+ $offset = $this->_datasize;
+
+ // add size of Workbook globals part 2, the length of the SHEET records
+ $total_worksheets = count($this->spreadsheet->getAllSheets());
+ foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
+ $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
+ }
+
+ // add the sizes of each of the Sheet substreams, respectively
+ for ($i = 0; $i < $total_worksheets; ++$i) {
+ $this->worksheetOffsets[$i] = $offset;
+ $offset += $this->worksheetSizes[$i];
+ }
+ $this->biffSize = $offset;
+ }
+
+ /**
+ * Store the Excel FONT records.
+ */
+ private function writeAllFonts(): void
+ {
+ foreach ($this->fontWriters as $fontWriter) {
+ $this->append($fontWriter->writeFont());
+ }
+ }
+
+ /**
+ * Store user defined numerical formats i.e. FORMAT records.
+ */
+ private function writeAllNumberFormats(): void
+ {
+ foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
+ $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
+ }
+ }
+
+ /**
+ * Write all XF records.
+ */
+ private function writeAllXfs(): void
+ {
+ foreach ($this->xfWriters as $xfWriter) {
+ $this->append($xfWriter->writeXf());
+ }
+ }
+
+ /**
+ * Write all STYLE records.
+ */
+ private function writeAllStyles(): void
+ {
+ $this->writeStyle();
+ }
+
+ private function parseDefinedNameValue(DefinedName $pDefinedName): string
+ {
+ $definedRange = $pDefinedName->getValue();
+ $splitCount = preg_match_all(
+ '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui',
+ $definedRange,
+ $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) || ($definedRange[$offset - 1] !== ':')) {
+ // We should have a worksheet
+ $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null;
+ }
+ } else {
+ $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+ }
+ if (!empty($worksheet)) {
+ $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
+ }
+
+ if (!empty($column)) {
+ $newRange .= "\${$column}";
+ }
+ if (!empty($row)) {
+ $newRange .= "\${$row}";
+ }
+
+ $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
+ }
+
+ return $definedRange;
+ }
+
+ /**
+ * Writes all the DEFINEDNAME records (BIFF8).
+ * So far this is only used for repeating rows/columns (print titles) and print areas.
+ */
+ private function writeAllDefinedNamesBiff8()
+ {
+ $chunk = '';
+
+ // Named ranges
+ $definedNames = $this->spreadsheet->getDefinedNames();
+ if (count($definedNames) > 0) {
+ // Loop named ranges
+ foreach ($definedNames as $definedName) {
+ $range = $this->parseDefinedNameValue($definedName);
+
+ // parse formula
+ try {
+ $error = $this->parser->parse($range);
+ $formulaData = $this->parser->toReversePolish();
+
+ // make sure tRef3d is of type tRef3dR (0x3A)
+ if (isset($formulaData[0]) && ($formulaData[0] == "\x7A" || $formulaData[0] == "\x5A")) {
+ $formulaData = "\x3A" . substr($formulaData, 1);
+ }
+
+ if ($definedName->getLocalOnly()) {
+ // local scope
+ $scope = $this->spreadsheet->getIndex($definedName->getScope()) + 1;
+ } else {
+ // global scope
+ $scope = 0;
+ }
+ $chunk .= $this->writeData($this->writeDefinedNameBiff8($definedName->getName(), $formulaData, $scope, false));
+ } catch (PhpSpreadsheetException $e) {
+ // do nothing
+ }
+ }
+ }
+
+ // total number of sheets
+ $total_worksheets = $this->spreadsheet->getSheetCount();
+
+ // write the print titles (repeating rows, columns), if any
+ for ($i = 0; $i < $total_worksheets; ++$i) {
+ $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
+ // simultaneous repeatColumns repeatRows
+ if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
+ $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
+ $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
+ $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
+
+ $repeat = $sheetSetup->getRowsToRepeatAtTop();
+ $rowmin = $repeat[0] - 1;
+ $rowmax = $repeat[1] - 1;
+
+ // construct formula data manually
+ $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
+ $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
+ $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
+ $formulaData .= pack('C', 0x10); // tList
+
+ // store the DEFINEDNAME record
+ $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
+
+ // (exclusive) either repeatColumns or repeatRows
+ } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
+ // Columns to repeat
+ if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
+ $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
+ $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
+ $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
+ } else {
+ $colmin = 0;
+ $colmax = 255;
+ }
+ // Rows to repeat
+ if ($sheetSetup->isRowsToRepeatAtTopSet()) {
+ $repeat = $sheetSetup->getRowsToRepeatAtTop();
+ $rowmin = $repeat[0] - 1;
+ $rowmax = $repeat[1] - 1;
+ } else {
+ $rowmin = 0;
+ $rowmax = 65535;
+ }
+
+ // construct formula data manually because parser does not recognize absolute 3d cell references
+ $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
+
+ // store the DEFINEDNAME record
+ $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
+ }
+ }
+
+ // write the print areas, if any
+ for ($i = 0; $i < $total_worksheets; ++$i) {
+ $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
+ if ($sheetSetup->isPrintAreaSet()) {
+ // Print area, e.g. A3:J6,H1:X20
+ $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
+ $countPrintArea = count($printArea);
+
+ $formulaData = '';
+ for ($j = 0; $j < $countPrintArea; ++$j) {
+ $printAreaRect = $printArea[$j]; // e.g. A3:J6
+ $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]);
+ $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]);
+
+ $print_rowmin = $printAreaRect[0][1] - 1;
+ $print_rowmax = $printAreaRect[1][1] - 1;
+ $print_colmin = $printAreaRect[0][0] - 1;
+ $print_colmax = $printAreaRect[1][0] - 1;
+
+ // construct formula data manually because parser does not recognize absolute 3d cell references
+ $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
+
+ if ($j > 0) {
+ $formulaData .= pack('C', 0x10); // list operator token ','
+ }
+ }
+
+ // store the DEFINEDNAME record
+ $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
+ }
+ }
+
+ // write autofilters, if any
+ for ($i = 0; $i < $total_worksheets; ++$i) {
+ $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
+ $autoFilterRange = $sheetAutoFilter->getRange();
+ if (!empty($autoFilterRange)) {
+ $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
+
+ //Autofilter built in name
+ $name = pack('C', 0x0D);
+
+ $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
+ }
+ }
+
+ return $chunk;
+ }
+
+ /**
+ * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
+ *
+ * @param string $name The name in UTF-8
+ * @param string $formulaData The binary formula data
+ * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
+ * @param bool $isBuiltIn Built-in name?
+ *
+ * @return string Complete binary record data
+ */
+ private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
+ {
+ $record = 0x0018;
+
+ // option flags
+ $options = $isBuiltIn ? 0x20 : 0x00;
+
+ // length of the name, character count
+ $nlen = StringHelper::countCharacters($name);
+
+ // name with stripped length field
+ $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
+
+ // size of the formula (in bytes)
+ $sz = strlen($formulaData);
+
+ // combine the parts
+ $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
+ . $name . $formulaData;
+ $length = strlen($data);
+
+ $header = pack('vv', $record, $length);
+
+ return $header . $data;
+ }
+
+ /**
+ * Write a short NAME record.
+ *
+ * @param string $name
+ * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
+ * @param int[][] $rangeBounds range boundaries
+ * @param bool $isHidden
+ *
+ * @return string Complete binary record data
+ * */
+ private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
+ {
+ $record = 0x0018;
+
+ // option flags
+ $options = ($isHidden ? 0x21 : 0x00);
+
+ $extra = pack(
+ 'Cvvvvv',
+ 0x3B,
+ $sheetIndex - 1,
+ $rangeBounds[0][1] - 1,
+ $rangeBounds[1][1] - 1,
+ $rangeBounds[0][0] - 1,
+ $rangeBounds[1][0] - 1
+ );
+
+ // size of the formula (in bytes)
+ $sz = strlen($extra);
+
+ // combine the parts
+ $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
+ . $name . $extra;
+ $length = strlen($data);
+
+ $header = pack('vv', $record, $length);
+
+ return $header . $data;
+ }
+
+ /**
+ * Stores the CODEPAGE biff record.
+ */
+ private function writeCodepage(): void
+ {
+ $record = 0x0042; // Record identifier
+ $length = 0x0002; // Number of bytes to follow
+ $cv = $this->codepage; // The code page
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $cv);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write Excel BIFF WINDOW1 record.
+ */
+ private function writeWindow1(): void
+ {
+ $record = 0x003D; // Record identifier
+ $length = 0x0012; // Number of bytes to follow
+
+ $xWn = 0x0000; // Horizontal position of window
+ $yWn = 0x0000; // Vertical position of window
+ $dxWn = 0x25BC; // Width of window
+ $dyWn = 0x1572; // Height of window
+
+ $grbit = 0x0038; // Option flags
+
+ // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
+ $ctabsel = 1; // Number of workbook tabs selected
+
+ $wTabRatio = 0x0258; // Tab to scrollbar ratio
+
+ // not supported by PhpSpreadsheet, set to 0
+ $itabFirst = 0; // 1st displayed worksheet
+ $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Writes Excel BIFF BOUNDSHEET record.
+ *
+ * @param int $offset Location of worksheet BOF
+ */
+ private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void
+ {
+ $sheetname = $sheet->getTitle();
+ $record = 0x0085; // Record identifier
+
+ // sheet state
+ switch ($sheet->getSheetState()) {
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
+ $ss = 0x00;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
+ $ss = 0x01;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
+ $ss = 0x02;
+
+ break;
+ default:
+ $ss = 0x00;
+
+ break;
+ }
+
+ // sheet type
+ $st = 0x00;
+
+ $grbit = 0x0000; // Visibility and sheet type
+
+ $data = pack('VCC', $offset, $ss, $st);
+ $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
+
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write Internal SUPBOOK record.
+ */
+ private function writeSupbookInternal()
+ {
+ $record = 0x01AE; // Record identifier
+ $length = 0x0004; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
+
+ return $this->writeData($header . $data);
+ }
+
+ /**
+ * Writes the Excel BIFF EXTERNSHEET record. These references are used by
+ * formulas.
+ */
+ private function writeExternalsheetBiff8()
+ {
+ $totalReferences = count($this->parser->references);
+ $record = 0x0017; // Record identifier
+ $length = 2 + 6 * $totalReferences; // Number of bytes to follow
+
+ $supbook_index = 0; // FIXME: only using internal SUPBOOK record
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $totalReferences);
+ for ($i = 0; $i < $totalReferences; ++$i) {
+ $data .= $this->parser->references[$i];
+ }
+
+ return $this->writeData($header . $data);
+ }
+
+ /**
+ * Write Excel BIFF STYLE records.
+ */
+ private function writeStyle(): void
+ {
+ $record = 0x0293; // Record identifier
+ $length = 0x0004; // Bytes to follow
+
+ $ixfe = 0x8000; // Index to cell style XF
+ $BuiltIn = 0x00; // Built-in style
+ $iLevel = 0xff; // Outline style level
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Writes Excel FORMAT record for non "built-in" numerical formats.
+ *
+ * @param string $format Custom format string
+ * @param int $ifmt Format index code
+ */
+ private function writeNumberFormat($format, $ifmt): void
+ {
+ $record = 0x041E; // Record identifier
+
+ $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
+ $length = 2 + strlen($numberFormatString); // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $ifmt) . $numberFormatString;
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write DATEMODE record to indicate the date system in use (1904 or 1900).
+ */
+ private function writeDateMode(): void
+ {
+ $record = 0x0022; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
+ ? 1
+ : 0; // Flag for 1904 date system
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $f1904);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Stores the COUNTRY record for localization.
+ *
+ * @return string
+ */
+ private function writeCountry()
+ {
+ $record = 0x008C; // Record identifier
+ $length = 4; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ // using the same country code always for simplicity
+ $data = pack('vv', $this->countryCode, $this->countryCode);
+
+ return $this->writeData($header . $data);
+ }
+
+ /**
+ * Write the RECALCID record.
+ *
+ * @return string
+ */
+ private function writeRecalcId()
+ {
+ $record = 0x01C1; // Record identifier
+ $length = 8; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+
+ // by inspection of real Excel files, MS Office Excel 2007 writes this
+ $data = pack('VV', 0x000001C1, 0x00001E667);
+
+ return $this->writeData($header . $data);
+ }
+
+ /**
+ * Stores the PALETTE biff record.
+ */
+ private function writePalette(): void
+ {
+ $aref = $this->palette;
+
+ $record = 0x0092; // Record identifier
+ $length = 2 + 4 * count($aref); // Number of bytes to follow
+ $ccv = count($aref); // Number of RGB values to follow
+ $data = ''; // The RGB data
+
+ // Pack the RGB data
+ foreach ($aref as $color) {
+ foreach ($color as $byte) {
+ $data .= pack('C', $byte);
+ }
+ }
+
+ $header = pack('vvv', $record, $length, $ccv);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Handling of the SST continue blocks is complicated by the need to include an
+ * additional continuation byte depending on whether the string is split between
+ * blocks or whether it starts at the beginning of the block. (There are also
+ * additional complications that will arise later when/if Rich Strings are
+ * supported).
+ *
+ * The Excel documentation says that the SST record should be followed by an
+ * EXTSST record. The EXTSST record is a hash table that is used to optimise
+ * access to SST. However, despite the documentation it doesn't seem to be
+ * required so we will ignore it.
+ *
+ * @return string Binary data
+ */
+ private function writeSharedStringsTable()
+ {
+ // maximum size of record data (excluding record header)
+ $continue_limit = 8224;
+
+ // initialize array of record data blocks
+ $recordDatas = [];
+
+ // start SST record data block with total number of strings, total number of unique strings
+ $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
+
+ // loop through all (unique) strings in shared strings table
+ foreach (array_keys($this->stringTable) as $string) {
+ // here $string is a BIFF8 encoded string
+
+ // length = character count
+ $headerinfo = unpack('vlength/Cencoding', $string);
+
+ // currently, this is always 1 = uncompressed
+ $encoding = $headerinfo['encoding'];
+
+ // initialize finished writing current $string
+ $finished = false;
+
+ while ($finished === false) {
+ // normally, there will be only one cycle, but if string cannot immediately be written as is
+ // there will be need for more than one cylcle, if string longer than one record data block, there
+ // may be need for even more cycles
+
+ if (strlen($recordData) + strlen($string) <= $continue_limit) {
+ // then we can write the string (or remainder of string) without any problems
+ $recordData .= $string;
+
+ if (strlen($recordData) + strlen($string) == $continue_limit) {
+ // we close the record data block, and initialize a new one
+ $recordDatas[] = $recordData;
+ $recordData = '';
+ }
+
+ // we are finished writing this string
+ $finished = true;
+ } else {
+ // special treatment writing the string (or remainder of the string)
+ // If the string is very long it may need to be written in more than one CONTINUE record.
+
+ // check how many bytes more there is room for in the current record
+ $space_remaining = $continue_limit - strlen($recordData);
+
+ // minimum space needed
+ // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
+ // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character
+ $min_space_needed = ($encoding == 1) ? 5 : 4;
+
+ // We have two cases
+ // 1. space remaining is less than minimum space needed
+ // here we must waste the space remaining and move to next record data block
+ // 2. space remaining is greater than or equal to minimum space needed
+ // here we write as much as we can in the current block, then move to next record data block
+
+ // 1. space remaining is less than minimum space needed
+ if ($space_remaining < $min_space_needed) {
+ // we close the block, store the block data
+ $recordDatas[] = $recordData;
+
+ // and start new record data block where we start writing the string
+ $recordData = '';
+
+ // 2. space remaining is greater than or equal to minimum space needed
+ } else {
+ // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
+ $effective_space_remaining = $space_remaining;
+
+ // for uncompressed strings, sometimes effective space remaining is reduced by 1
+ if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
+ --$effective_space_remaining;
+ }
+
+ // one block fininshed, store the block data
+ $recordData .= substr($string, 0, $effective_space_remaining);
+
+ $string = substr($string, $effective_space_remaining); // for next cycle in while loop
+ $recordDatas[] = $recordData;
+
+ // start new record data block with the repeated option flags
+ $recordData = pack('C', $encoding);
+ }
+ }
+ }
+ }
+
+ // Store the last record data block unless it is empty
+ // if there was no need for any continue records, this will be the for SST record data block itself
+ if (strlen($recordData) > 0) {
+ $recordDatas[] = $recordData;
+ }
+
+ // combine into one chunk with all the blocks SST, CONTINUE,...
+ $chunk = '';
+ foreach ($recordDatas as $i => $recordData) {
+ // first block should have the SST record header, remaing should have CONTINUE header
+ $record = ($i == 0) ? 0x00FC : 0x003C;
+
+ $header = pack('vv', $record, strlen($recordData));
+ $data = $header . $recordData;
+
+ $chunk .= $this->writeData($data);
+ }
+
+ return $chunk;
+ }
+
+ /**
+ * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
+ */
+ private function writeMsoDrawingGroup()
+ {
+ // write the Escher stream if necessary
+ if (isset($this->escher)) {
+ $writer = new Escher($this->escher);
+ $data = $writer->close();
+
+ $record = 0x00EB;
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+
+ return $this->writeData($header . $data);
+ }
+
+ return '';
+ }
+
+ /**
+ * Get Escher object.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ public function getEscher()
+ {
+ return $this->escher;
+ }
+
+ /**
+ * Set Escher object.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
+ */
+ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null): void
+ {
+ $this->escher = $pValue;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php
new file mode 100644
index 0000000..894ce03
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php
@@ -0,0 +1,3188 @@
+
+// *
+// * The majority of this is _NOT_ my code. I simply ported it from the
+// * PERL Spreadsheet::WriteExcel module.
+// *
+// * The author of the Spreadsheet::WriteExcel module is John McNamara
+// *
+// *
+// * I _DO_ maintain this code, and John McNamara has nothing to do with the
+// * porting of this code to PHP. Any questions directly related to this
+// * class library should be directed to me.
+// *
+// * License Information:
+// *
+// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
+// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// * This library is free software; you can redistribute it and/or
+// * modify it under the terms of the GNU Lesser General Public
+// * License as published by the Free Software Foundation; either
+// * version 2.1 of the License, or (at your option) any later version.
+// *
+// * This library is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// * Lesser General Public License for more details.
+// *
+// * You should have received a copy of the GNU Lesser General Public
+// * License along with this library; if not, write to the Free Software
+// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+// */
+class Worksheet extends BIFFwriter
+{
+ /**
+ * Formula parser.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
+ */
+ private $parser;
+
+ /**
+ * Maximum number of characters for a string (LABEL record in BIFF5).
+ *
+ * @var int
+ */
+ private $xlsStringMaxLength;
+
+ /**
+ * Array containing format information for columns.
+ *
+ * @var array
+ */
+ private $columnInfo;
+
+ /**
+ * Array containing the selected area for the worksheet.
+ *
+ * @var array
+ */
+ private $selection;
+
+ /**
+ * The active pane for the worksheet.
+ *
+ * @var int
+ */
+ private $activePane;
+
+ /**
+ * Whether to use outline.
+ *
+ * @var bool
+ */
+ private $outlineOn;
+
+ /**
+ * Auto outline styles.
+ *
+ * @var bool
+ */
+ private $outlineStyle;
+
+ /**
+ * Whether to have outline summary below.
+ *
+ * @var bool
+ */
+ private $outlineBelow;
+
+ /**
+ * Whether to have outline summary at the right.
+ *
+ * @var bool
+ */
+ private $outlineRight;
+
+ /**
+ * Reference to the total number of strings in the workbook.
+ *
+ * @var int
+ */
+ private $stringTotal;
+
+ /**
+ * Reference to the number of unique strings in the workbook.
+ *
+ * @var int
+ */
+ private $stringUnique;
+
+ /**
+ * Reference to the array containing all the unique strings in the workbook.
+ *
+ * @var array
+ */
+ private $stringTable;
+
+ /**
+ * Color cache.
+ */
+ private $colors;
+
+ /**
+ * Index of first used row (at least 0).
+ *
+ * @var int
+ */
+ private $firstRowIndex;
+
+ /**
+ * Index of last used row. (no used rows means -1).
+ *
+ * @var int
+ */
+ private $lastRowIndex;
+
+ /**
+ * Index of first used column (at least 0).
+ *
+ * @var int
+ */
+ private $firstColumnIndex;
+
+ /**
+ * Index of last used column (no used columns means -1).
+ *
+ * @var int
+ */
+ private $lastColumnIndex;
+
+ /**
+ * Sheet object.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+ */
+ public $phpSheet;
+
+ /**
+ * Count cell style Xfs.
+ *
+ * @var int
+ */
+ private $countCellStyleXfs;
+
+ /**
+ * Escher object corresponding to MSODRAWING.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ private $escher;
+
+ /**
+ * Array of font hashes associated to FONT records index.
+ *
+ * @var array
+ */
+ public $fontHashIndex;
+
+ /**
+ * @var bool
+ */
+ private $preCalculateFormulas;
+
+ /**
+ * @var int
+ */
+ private $printHeaders;
+
+ /**
+ * Constructor.
+ *
+ * @param int $str_total Total number of strings
+ * @param int $str_unique Total number of unique strings
+ * @param array $str_table String Table
+ * @param array $colors Colour Table
+ * @param Parser $parser The formula parser created for the Workbook
+ * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written
+ * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write
+ */
+ public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet)
+ {
+ // It needs to call its parent's constructor explicitly
+ parent::__construct();
+
+ $this->preCalculateFormulas = $preCalculateFormulas;
+ $this->stringTotal = &$str_total;
+ $this->stringUnique = &$str_unique;
+ $this->stringTable = &$str_table;
+ $this->colors = &$colors;
+ $this->parser = $parser;
+
+ $this->phpSheet = $phpSheet;
+
+ $this->xlsStringMaxLength = 255;
+ $this->columnInfo = [];
+ $this->selection = [0, 0, 0, 0];
+ $this->activePane = 3;
+
+ $this->printHeaders = 0;
+
+ $this->outlineStyle = false;
+ $this->outlineBelow = true;
+ $this->outlineRight = true;
+ $this->outlineOn = true;
+
+ $this->fontHashIndex = [];
+
+ // calculate values for DIMENSIONS record
+ $minR = 1;
+ $minC = 'A';
+
+ $maxR = $this->phpSheet->getHighestRow();
+ $maxC = $this->phpSheet->getHighestColumn();
+
+ // Determine lowest and highest column and row
+ $this->firstRowIndex = $minR;
+ $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR;
+
+ $this->firstColumnIndex = Coordinate::columnIndexFromString($minC);
+ $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC);
+
+ if ($this->lastColumnIndex > 255) {
+ $this->lastColumnIndex = 255;
+ }
+
+ $this->countCellStyleXfs = count($phpSheet->getParent()->getCellStyleXfCollection());
+ }
+
+ /**
+ * Add data to the beginning of the workbook (note the reverse order)
+ * and to the end of the workbook.
+ *
+ * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::storeWorkbook()
+ */
+ public function close(): void
+ {
+ $phpSheet = $this->phpSheet;
+
+ // Storing selected cells and active sheet because it changes while parsing cells with formulas.
+ $selectedCells = $this->phpSheet->getSelectedCells();
+ $activeSheetIndex = $this->phpSheet->getParent()->getActiveSheetIndex();
+
+ // Write BOF record
+ $this->storeBof(0x0010);
+
+ // Write PRINTHEADERS
+ $this->writePrintHeaders();
+
+ // Write PRINTGRIDLINES
+ $this->writePrintGridlines();
+
+ // Write GRIDSET
+ $this->writeGridset();
+
+ // Calculate column widths
+ $phpSheet->calculateColumnWidths();
+
+ // Column dimensions
+ if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) {
+ $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParent()->getDefaultStyle()->getFont());
+ }
+
+ $columnDimensions = $phpSheet->getColumnDimensions();
+ $maxCol = $this->lastColumnIndex - 1;
+ for ($i = 0; $i <= $maxCol; ++$i) {
+ $hidden = 0;
+ $level = 0;
+ $xfIndex = 15; // there are 15 cell style Xfs
+
+ $width = $defaultWidth;
+
+ $columnLetter = Coordinate::stringFromColumnIndex($i + 1);
+ if (isset($columnDimensions[$columnLetter])) {
+ $columnDimension = $columnDimensions[$columnLetter];
+ if ($columnDimension->getWidth() >= 0) {
+ $width = $columnDimension->getWidth();
+ }
+ $hidden = $columnDimension->getVisible() ? 0 : 1;
+ $level = $columnDimension->getOutlineLevel();
+ $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs
+ }
+
+ // Components of columnInfo:
+ // $firstcol first column on the range
+ // $lastcol last column on the range
+ // $width width to set
+ // $xfIndex The optional cell style Xf index to apply to the columns
+ // $hidden The optional hidden atribute
+ // $level The optional outline level
+ $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level];
+ }
+
+ // Write GUTS
+ $this->writeGuts();
+
+ // Write DEFAULTROWHEIGHT
+ $this->writeDefaultRowHeight();
+ // Write WSBOOL
+ $this->writeWsbool();
+ // Write horizontal and vertical page breaks
+ $this->writeBreaks();
+ // Write page header
+ $this->writeHeader();
+ // Write page footer
+ $this->writeFooter();
+ // Write page horizontal centering
+ $this->writeHcenter();
+ // Write page vertical centering
+ $this->writeVcenter();
+ // Write left margin
+ $this->writeMarginLeft();
+ // Write right margin
+ $this->writeMarginRight();
+ // Write top margin
+ $this->writeMarginTop();
+ // Write bottom margin
+ $this->writeMarginBottom();
+ // Write page setup
+ $this->writeSetup();
+ // Write sheet protection
+ $this->writeProtect();
+ // Write SCENPROTECT
+ $this->writeScenProtect();
+ // Write OBJECTPROTECT
+ $this->writeObjectProtect();
+ // Write sheet password
+ $this->writePassword();
+ // Write DEFCOLWIDTH record
+ $this->writeDefcol();
+
+ // Write the COLINFO records if they exist
+ if (!empty($this->columnInfo)) {
+ $colcount = count($this->columnInfo);
+ for ($i = 0; $i < $colcount; ++$i) {
+ $this->writeColinfo($this->columnInfo[$i]);
+ }
+ }
+ $autoFilterRange = $phpSheet->getAutoFilter()->getRange();
+ if (!empty($autoFilterRange)) {
+ // Write AUTOFILTERINFO
+ $this->writeAutoFilterInfo();
+ }
+
+ // Write sheet dimensions
+ $this->writeDimensions();
+
+ // Row dimensions
+ foreach ($phpSheet->getRowDimensions() as $rowDimension) {
+ $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs
+ $this->writeRow(
+ $rowDimension->getRowIndex() - 1,
+ (int) $rowDimension->getRowHeight(),
+ $xfIndex,
+ !$rowDimension->getVisible(),
+ $rowDimension->getOutlineLevel()
+ );
+ }
+
+ // Write Cells
+ foreach ($phpSheet->getCoordinates() as $coordinate) {
+ $cell = $phpSheet->getCell($coordinate);
+ $row = $cell->getRow() - 1;
+ $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
+
+ // Don't break Excel break the code!
+ if ($row > 65535 || $column > 255) {
+ throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.');
+ }
+
+ // Write cell value
+ $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs
+
+ $cVal = $cell->getValue();
+ if ($cVal instanceof RichText) {
+ $arrcRun = [];
+ $str_pos = 0;
+ $elements = $cVal->getRichTextElements();
+ foreach ($elements as $element) {
+ // FONT Index
+ if ($element instanceof Run) {
+ $str_fontidx = $this->fontHashIndex[$element->getFont()->getHashCode()];
+ } else {
+ $str_fontidx = 0;
+ }
+ $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx];
+ // Position FROM
+ $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8');
+ }
+ $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun);
+ } else {
+ switch ($cell->getDatatype()) {
+ case DataType::TYPE_STRING:
+ case DataType::TYPE_NULL:
+ if ($cVal === '' || $cVal === null) {
+ $this->writeBlank($row, $column, $xfIndex);
+ } else {
+ $this->writeString($row, $column, $cVal, $xfIndex);
+ }
+
+ break;
+ case DataType::TYPE_NUMERIC:
+ $this->writeNumber($row, $column, $cVal, $xfIndex);
+
+ break;
+ case DataType::TYPE_FORMULA:
+ $calculatedValue = $this->preCalculateFormulas ?
+ $cell->getCalculatedValue() : null;
+ if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue)) {
+ if ($calculatedValue === null) {
+ $calculatedValue = $cell->getCalculatedValue();
+ }
+ $calctype = gettype($calculatedValue);
+ switch ($calctype) {
+ case 'integer':
+ case 'double':
+ $this->writeNumber($row, $column, $calculatedValue, $xfIndex);
+
+ break;
+ case 'string':
+ $this->writeString($row, $column, $calculatedValue, $xfIndex);
+
+ break;
+ case 'boolean':
+ $this->writeBoolErr($row, $column, $calculatedValue, 0, $xfIndex);
+
+ break;
+ default:
+ $this->writeString($row, $column, $cVal, $xfIndex);
+ }
+ }
+
+ break;
+ case DataType::TYPE_BOOL:
+ $this->writeBoolErr($row, $column, $cVal, 0, $xfIndex);
+
+ break;
+ case DataType::TYPE_ERROR:
+ $this->writeBoolErr($row, $column, ErrorCode::error($cVal), 1, $xfIndex);
+
+ break;
+ }
+ }
+ }
+
+ // Append
+ $this->writeMsoDrawing();
+
+ // Restoring active sheet.
+ $this->phpSheet->getParent()->setActiveSheetIndex($activeSheetIndex);
+
+ // Write WINDOW2 record
+ $this->writeWindow2();
+
+ // Write PLV record
+ $this->writePageLayoutView();
+
+ // Write ZOOM record
+ $this->writeZoom();
+ if ($phpSheet->getFreezePane()) {
+ $this->writePanes();
+ }
+
+ // Restoring selected cells.
+ $this->phpSheet->setSelectedCells($selectedCells);
+
+ // Write SELECTION record
+ $this->writeSelection();
+
+ // Write MergedCellsTable Record
+ $this->writeMergedCells();
+
+ // Hyperlinks
+ foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) {
+ [$column, $row] = Coordinate::indexesFromString($coordinate);
+
+ $url = $hyperlink->getUrl();
+
+ if (strpos($url, 'sheet://') !== false) {
+ // internal to current workbook
+ $url = str_replace('sheet://', 'internal:', $url);
+ } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) {
+ // URL
+ } else {
+ // external (local file)
+ $url = 'external:' . $url;
+ }
+
+ $this->writeUrl($row - 1, $column - 1, $url);
+ }
+
+ $this->writeDataValidity();
+ $this->writeSheetLayout();
+
+ // Write SHEETPROTECTION record
+ $this->writeSheetProtection();
+ $this->writeRangeProtection();
+
+ $arrConditionalStyles = $phpSheet->getConditionalStylesCollection();
+ if (!empty($arrConditionalStyles)) {
+ $arrConditional = [];
+
+ $cfHeaderWritten = false;
+ // Write ConditionalFormattingTable records
+ foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {
+ foreach ($conditionalStyles as $conditional) {
+ /** @var Conditional $conditional */
+ if (
+ $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION ||
+ $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+ ) {
+ // Write CFHEADER record (only if there are Conditional Styles that we are able to write)
+ if ($cfHeaderWritten === false) {
+ $this->writeCFHeader();
+ $cfHeaderWritten = true;
+ }
+ if (!isset($arrConditional[$conditional->getHashCode()])) {
+ // This hash code has been handled
+ $arrConditional[$conditional->getHashCode()] = true;
+
+ // Write CFRULE record
+ $this->writeCFRule($conditional);
+ }
+ }
+ }
+ }
+ }
+
+ $this->storeEof();
+ }
+
+ /**
+ * Write a cell range address in BIFF8
+ * always fixed range
+ * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format.
+ *
+ * @param string $range E.g. 'A1' or 'A1:B6'
+ *
+ * @return string Binary data
+ */
+ private function writeBIFF8CellRangeAddressFixed($range)
+ {
+ $explodes = explode(':', $range);
+
+ // extract first cell, e.g. 'A1'
+ $firstCell = $explodes[0];
+
+ // extract last cell, e.g. 'B6'
+ if (count($explodes) == 1) {
+ $lastCell = $firstCell;
+ } else {
+ $lastCell = $explodes[1];
+ }
+
+ $firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1]
+ $lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6]
+
+ return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1);
+ }
+
+ /**
+ * Retrieves data from memory in one chunk, or from disk
+ * sized chunks.
+ *
+ * @return string The data
+ */
+ public function getData()
+ {
+ // Return data stored in memory
+ if (isset($this->_data)) {
+ $tmp = $this->_data;
+ $this->_data = null;
+
+ return $tmp;
+ }
+
+ // No data to return
+ return '';
+ }
+
+ /**
+ * Set the option to print the row and column headers on the printed page.
+ *
+ * @param int $print Whether to print the headers or not. Defaults to 1 (print).
+ */
+ public function printRowColHeaders($print = 1): void
+ {
+ $this->printHeaders = $print;
+ }
+
+ /**
+ * This method sets the properties for outlining and grouping. The defaults
+ * correspond to Excel's defaults.
+ *
+ * @param bool $visible
+ * @param bool $symbols_below
+ * @param bool $symbols_right
+ * @param bool $auto_style
+ */
+ public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false): void
+ {
+ $this->outlineOn = $visible;
+ $this->outlineBelow = $symbols_below;
+ $this->outlineRight = $symbols_right;
+ $this->outlineStyle = $auto_style;
+ }
+
+ /**
+ * Write a double to the specified row and column (zero indexed).
+ * An integer can be written as a double. Excel will display an
+ * integer. $format is optional.
+ *
+ * Returns 0 : normal termination
+ * -2 : row or column out of range
+ *
+ * @param int $row Zero indexed row
+ * @param int $col Zero indexed column
+ * @param float $num The number to write
+ * @param mixed $xfIndex The optional XF format
+ *
+ * @return int
+ */
+ private function writeNumber($row, $col, $num, $xfIndex)
+ {
+ $record = 0x0203; // Record identifier
+ $length = 0x000E; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvv', $row, $col, $xfIndex);
+ $xl_double = pack('d', $num);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $xl_double = strrev($xl_double);
+ }
+
+ $this->append($header . $data . $xl_double);
+
+ return 0;
+ }
+
+ /**
+ * Write a LABELSST record or a LABEL record. Which one depends on BIFF version.
+ *
+ * @param int $row Row index (0-based)
+ * @param int $col Column index (0-based)
+ * @param string $str The string
+ * @param int $xfIndex Index to XF record
+ */
+ private function writeString($row, $col, $str, $xfIndex): void
+ {
+ $this->writeLabelSst($row, $col, $str, $xfIndex);
+ }
+
+ /**
+ * Write a LABELSST record or a LABEL record. Which one depends on BIFF version
+ * It differs from writeString by the writing of rich text strings.
+ *
+ * @param int $row Row index (0-based)
+ * @param int $col Column index (0-based)
+ * @param string $str The string
+ * @param int $xfIndex The XF format index for the cell
+ * @param array $arrcRun Index to Font record and characters beginning
+ */
+ private function writeRichTextString($row, $col, $str, $xfIndex, $arrcRun): void
+ {
+ $record = 0x00FD; // Record identifier
+ $length = 0x000A; // Bytes to follow
+ $str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun);
+
+ // check if string is already present
+ if (!isset($this->stringTable[$str])) {
+ $this->stringTable[$str] = $this->stringUnique++;
+ }
+ ++$this->stringTotal;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write a string to the specified row and column (zero indexed).
+ * This is the BIFF8 version (no 255 chars limit).
+ * $format is optional.
+ *
+ * @param int $row Zero indexed row
+ * @param int $col Zero indexed column
+ * @param string $str The string to write
+ * @param mixed $xfIndex The XF format index for the cell
+ */
+ private function writeLabelSst($row, $col, $str, $xfIndex): void
+ {
+ $record = 0x00FD; // Record identifier
+ $length = 0x000A; // Bytes to follow
+
+ $str = StringHelper::UTF8toBIFF8UnicodeLong($str);
+
+ // check if string is already present
+ if (!isset($this->stringTable[$str])) {
+ $this->stringTable[$str] = $this->stringUnique++;
+ }
+ ++$this->stringTotal;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write a blank cell to the specified row and column (zero indexed).
+ * A blank cell is used to specify formatting without adding a string
+ * or a number.
+ *
+ * A blank cell without a format serves no purpose. Therefore, we don't write
+ * a BLANK record unless a format is specified.
+ *
+ * Returns 0 : normal termination (including no format)
+ * -1 : insufficient number of arguments
+ * -2 : row or column out of range
+ *
+ * @param int $row Zero indexed row
+ * @param int $col Zero indexed column
+ * @param mixed $xfIndex The XF format index
+ *
+ * @return int
+ */
+ public function writeBlank($row, $col, $xfIndex)
+ {
+ $record = 0x0201; // Record identifier
+ $length = 0x0006; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvv', $row, $col, $xfIndex);
+ $this->append($header . $data);
+
+ return 0;
+ }
+
+ /**
+ * Write a boolean or an error type to the specified row and column (zero indexed).
+ *
+ * @param int $row Row index (0-based)
+ * @param int $col Column index (0-based)
+ * @param int $value
+ * @param bool $isError Error or Boolean?
+ * @param int $xfIndex
+ *
+ * @return int
+ */
+ private function writeBoolErr($row, $col, $value, $isError, $xfIndex)
+ {
+ $record = 0x0205;
+ $length = 8;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError);
+ $this->append($header . $data);
+
+ return 0;
+ }
+
+ const WRITE_FORMULA_NORMAL = 0;
+ const WRITE_FORMULA_ERRORS = -1;
+ const WRITE_FORMULA_RANGE = -2;
+ const WRITE_FORMULA_EXCEPTION = -3;
+
+ /**
+ * Write a formula to the specified row and column (zero indexed).
+ * The textual representation of the formula is passed to the parser in
+ * Parser.php which returns a packed binary string.
+ *
+ * Returns 0 : WRITE_FORMULA_NORMAL normal termination
+ * -1 : WRITE_FORMULA_ERRORS formula errors (bad formula)
+ * -2 : WRITE_FORMULA_RANGE row or column out of range
+ * -3 : WRITE_FORMULA_EXCEPTION parse raised exception, probably due to definedname
+ *
+ * @param int $row Zero indexed row
+ * @param int $col Zero indexed column
+ * @param string $formula The formula text string
+ * @param mixed $xfIndex The XF format index
+ * @param mixed $calculatedValue Calculated value
+ *
+ * @return int
+ */
+ private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue)
+ {
+ $record = 0x0006; // Record identifier
+ // Initialize possible additional value for STRING record that should be written after the FORMULA record?
+ $stringValue = null;
+
+ // calculated value
+ if (isset($calculatedValue)) {
+ // Since we can't yet get the data type of the calculated value,
+ // we use best effort to determine data type
+ if (is_bool($calculatedValue)) {
+ // Boolean value
+ $num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF);
+ } elseif (is_int($calculatedValue) || is_float($calculatedValue)) {
+ // Numeric value
+ $num = pack('d', $calculatedValue);
+ } elseif (is_string($calculatedValue)) {
+ $errorCodes = DataType::getErrorCodes();
+ if (isset($errorCodes[$calculatedValue])) {
+ // Error value
+ $num = pack('CCCvCv', 0x02, 0x00, ErrorCode::error($calculatedValue), 0x00, 0x00, 0xFFFF);
+ } elseif ($calculatedValue === '') {
+ // Empty string (and BIFF8)
+ $num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
+ } else {
+ // Non-empty string value (or empty string BIFF5)
+ $stringValue = $calculatedValue;
+ $num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
+ }
+ } else {
+ // We are really not supposed to reach here
+ $num = pack('d', 0x00);
+ }
+ } else {
+ $num = pack('d', 0x00);
+ }
+
+ $grbit = 0x03; // Option flags
+ $unknown = 0x0000; // Must be zero
+
+ // Strip the '=' or '@' sign at the beginning of the formula string
+ if ($formula[0] == '=') {
+ $formula = substr($formula, 1);
+ } else {
+ // Error handling
+ $this->writeString($row, $col, 'Unrecognised character for formula', 0);
+
+ return self::WRITE_FORMULA_ERRORS;
+ }
+
+ // Parse the formula using the parser in Parser.php
+ try {
+ $this->parser->parse($formula);
+ $formula = $this->parser->toReversePolish();
+
+ $formlen = strlen($formula); // Length of the binary string
+ $length = 0x16 + $formlen; // Length of the record data
+
+ $header = pack('vv', $record, $length);
+
+ $data = pack('vvv', $row, $col, $xfIndex)
+ . $num
+ . pack('vVv', $grbit, $unknown, $formlen);
+ $this->append($header . $data . $formula);
+
+ // Append also a STRING record if necessary
+ if ($stringValue !== null) {
+ $this->writeStringRecord($stringValue);
+ }
+
+ return self::WRITE_FORMULA_NORMAL;
+ } catch (PhpSpreadsheetException $e) {
+ return self::WRITE_FORMULA_EXCEPTION;
+ }
+ }
+
+ /**
+ * Write a STRING record. This.
+ *
+ * @param string $stringValue
+ */
+ private function writeStringRecord($stringValue): void
+ {
+ $record = 0x0207; // Record identifier
+ $data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue);
+
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write a hyperlink.
+ * This is comprised of two elements: the visible label and
+ * the invisible link. The visible label is the same as the link unless an
+ * alternative string is specified. The label is written using the
+ * writeString() method. Therefore the 255 characters string limit applies.
+ * $string and $format are optional.
+ *
+ * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external
+ * directory url.
+ *
+ * @param int $row Row
+ * @param int $col Column
+ * @param string $url URL string
+ */
+ private function writeUrl($row, $col, $url): void
+ {
+ // Add start row and col to arg list
+ $this->writeUrlRange($row, $col, $row, $col, $url);
+ }
+
+ /**
+ * This is the more general form of writeUrl(). It allows a hyperlink to be
+ * written to a range of cells. This function also decides the type of hyperlink
+ * to be written. These are either, Web (http, ftp, mailto), Internal
+ * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
+ *
+ * @param int $row1 Start row
+ * @param int $col1 Start column
+ * @param int $row2 End row
+ * @param int $col2 End column
+ * @param string $url URL string
+ *
+ * @see writeUrl()
+ */
+ private function writeUrlRange($row1, $col1, $row2, $col2, $url): void
+ {
+ // Check for internal/external sheet links or default to web link
+ if (preg_match('[^internal:]', $url)) {
+ $this->writeUrlInternal($row1, $col1, $row2, $col2, $url);
+ }
+ if (preg_match('[^external:]', $url)) {
+ $this->writeUrlExternal($row1, $col1, $row2, $col2, $url);
+ }
+
+ $this->writeUrlWeb($row1, $col1, $row2, $col2, $url);
+ }
+
+ /**
+ * Used to write http, ftp and mailto hyperlinks.
+ * The link type ($options) is 0x03 is the same as absolute dir ref without
+ * sheet. However it is differentiated by the $unknown2 data stream.
+ *
+ * @param int $row1 Start row
+ * @param int $col1 Start column
+ * @param int $row2 End row
+ * @param int $col2 End column
+ * @param string $url URL string
+ *
+ * @see writeUrl()
+ */
+ public function writeUrlWeb($row1, $col1, $row2, $col2, $url): void
+ {
+ $record = 0x01B8; // Record identifier
+
+ // Pack the undocumented parts of the hyperlink stream
+ $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+ $unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B');
+
+ // Pack the option flags
+ $options = pack('V', 0x03);
+
+ // Convert URL to a null terminated wchar string
+ $url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
+ $url = $url . "\0\0\0";
+
+ // Pack the length of the URL
+ $url_len = pack('V', strlen($url));
+
+ // Calculate the data length
+ $length = 0x34 + strlen($url);
+
+ // Pack the header data
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvv', $row1, $row2, $col1, $col2);
+
+ // Write the packed data
+ $this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url);
+ }
+
+ /**
+ * Used to write internal reference hyperlinks such as "Sheet1!A1".
+ *
+ * @param int $row1 Start row
+ * @param int $col1 Start column
+ * @param int $row2 End row
+ * @param int $col2 End column
+ * @param string $url URL string
+ *
+ * @see writeUrl()
+ */
+ private function writeUrlInternal($row1, $col1, $row2, $col2, $url): void
+ {
+ $record = 0x01B8; // Record identifier
+
+ // Strip URL type
+ $url = preg_replace('/^internal:/', '', $url);
+
+ // Pack the undocumented parts of the hyperlink stream
+ $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+
+ // Pack the option flags
+ $options = pack('V', 0x08);
+
+ // Convert the URL type and to a null terminated wchar string
+ $url .= "\0";
+
+ // character count
+ $url_len = StringHelper::countCharacters($url);
+ $url_len = pack('V', $url_len);
+
+ $url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8');
+
+ // Calculate the data length
+ $length = 0x24 + strlen($url);
+
+ // Pack the header data
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvv', $row1, $row2, $col1, $col2);
+
+ // Write the packed data
+ $this->append($header . $data . $unknown1 . $options . $url_len . $url);
+ }
+
+ /**
+ * Write links to external directory names such as 'c:\foo.xls',
+ * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
+ *
+ * Note: Excel writes some relative links with the $dir_long string. We ignore
+ * these cases for the sake of simpler code.
+ *
+ * @param int $row1 Start row
+ * @param int $col1 Start column
+ * @param int $row2 End row
+ * @param int $col2 End column
+ * @param string $url URL string
+ *
+ * @see writeUrl()
+ */
+ private function writeUrlExternal($row1, $col1, $row2, $col2, $url): void
+ {
+ // Network drives are different. We will handle them separately
+ // MS/Novell network drives and shares start with \\
+ if (preg_match('[^external:\\\\]', $url)) {
+ return;
+ }
+
+ $record = 0x01B8; // Record identifier
+ $length = 0x00000; // Bytes to follow
+
+ // Strip URL type and change Unix dir separator to Dos style (if needed)
+ //
+ $url = preg_replace('/^external:/', '', $url);
+ $url = preg_replace('/\//', '\\', $url);
+
+ // Determine if the link is relative or absolute:
+ // relative if link contains no dir separator, "somefile.xls"
+ // relative if link starts with up-dir, "..\..\somefile.xls"
+ // otherwise, absolute
+
+ $absolute = 0x00; // relative path
+ if (preg_match('/^[A-Z]:/', $url)) {
+ $absolute = 0x02; // absolute path on Windows, e.g. C:\...
+ }
+ $link_type = 0x01 | $absolute;
+
+ // Determine if the link contains a sheet reference and change some of the
+ // parameters accordingly.
+ // Split the dir name and sheet name (if it exists)
+ $dir_long = $url;
+ if (preg_match('/\\#/', $url)) {
+ $link_type |= 0x08;
+ }
+
+ // Pack the link type
+ $link_type = pack('V', $link_type);
+
+ // Calculate the up-level dir count e.g.. (..\..\..\ == 3)
+ $up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless);
+ $up_count = pack('v', $up_count);
+
+ // Store the short dos dir name (null terminated)
+ $dir_short = preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0";
+
+ // Store the long dir name as a wchar string (non-null terminated)
+ $dir_long = $dir_long . "\0";
+
+ // Pack the lengths of the dir strings
+ $dir_short_len = pack('V', strlen($dir_short));
+ $dir_long_len = pack('V', strlen($dir_long));
+ $stream_len = pack('V', 0); //strlen($dir_long) + 0x06);
+
+ // Pack the undocumented parts of the hyperlink stream
+ $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+ $unknown2 = pack('H*', '0303000000000000C000000000000046');
+ $unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000');
+ $unknown4 = pack('v', 0x03);
+
+ // Pack the main data stream
+ $data = pack('vvvv', $row1, $row2, $col1, $col2) .
+ $unknown1 .
+ $link_type .
+ $unknown2 .
+ $up_count .
+ $dir_short_len .
+ $dir_short .
+ $unknown3 .
+ $stream_len; /*.
+ $dir_long_len .
+ $unknown4 .
+ $dir_long .
+ $sheet_len .
+ $sheet ;*/
+
+ // Pack the header data
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+
+ // Write the packed data
+ $this->append($header . $data);
+ }
+
+ /**
+ * This method is used to set the height and format for a row.
+ *
+ * @param int $row The row to set
+ * @param int $height Height we are giving to the row.
+ * Use null to set XF without setting height
+ * @param int $xfIndex The optional cell style Xf index to apply to the columns
+ * @param bool $hidden The optional hidden attribute
+ * @param int $level The optional outline level for row, in range [0,7]
+ */
+ private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0): void
+ {
+ $record = 0x0208; // Record identifier
+ $length = 0x0010; // Number of bytes to follow
+
+ $colMic = 0x0000; // First defined column
+ $colMac = 0x0000; // Last defined column
+ $irwMac = 0x0000; // Used by Excel to optimise loading
+ $reserved = 0x0000; // Reserved
+ $grbit = 0x0000; // Option flags
+ $ixfe = $xfIndex;
+
+ if ($height < 0) {
+ $height = null;
+ }
+
+ // Use writeRow($row, null, $XF) to set XF format without setting height
+ if ($height !== null) {
+ $miyRw = $height * 20; // row height
+ } else {
+ $miyRw = 0xff; // default row height is 256
+ }
+
+ // Set the options flags. fUnsynced is used to show that the font and row
+ // heights are not compatible. This is usually the case for WriteExcel.
+ // The collapsed flag 0x10 doesn't seem to be used to indicate that a row
+ // is collapsed. Instead it is used to indicate that the previous row is
+ // collapsed. The zero height flag, 0x20, is used to collapse a row.
+
+ $grbit |= $level;
+ if ($hidden === true) {
+ $grbit |= 0x0030;
+ }
+ if ($height !== null) {
+ $grbit |= 0x0040; // fUnsynced
+ }
+ if ($xfIndex !== 0xF) {
+ $grbit |= 0x0080;
+ }
+ $grbit |= 0x0100;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Writes Excel DIMENSIONS to define the area in which there is data.
+ */
+ private function writeDimensions(): void
+ {
+ $record = 0x0200; // Record identifier
+
+ $length = 0x000E;
+ $data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved
+
+ $header = pack('vv', $record, $length);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write BIFF record Window2.
+ */
+ private function writeWindow2(): void
+ {
+ $record = 0x023E; // Record identifier
+ $length = 0x0012;
+
+ $grbit = 0x00B6; // Option flags
+ $rwTop = 0x0000; // Top row visible in window
+ $colLeft = 0x0000; // Leftmost column visible in window
+
+ // The options flags that comprise $grbit
+ $fDspFmla = 0; // 0 - bit
+ $fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1
+ $fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2
+ $fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3
+ $fDspZeros = 1; // 4
+ $fDefaultHdr = 1; // 5
+ $fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6
+ $fDspGuts = $this->outlineOn; // 7
+ $fFrozenNoSplit = 0; // 0 - bit
+ // no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet
+ $fSelected = ($this->phpSheet === $this->phpSheet->getParent()->getActiveSheet()) ? 1 : 0;
+ $fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW;
+
+ $grbit = $fDspFmla;
+ $grbit |= $fDspGrid << 1;
+ $grbit |= $fDspRwCol << 2;
+ $grbit |= $fFrozen << 3;
+ $grbit |= $fDspZeros << 4;
+ $grbit |= $fDefaultHdr << 5;
+ $grbit |= $fArabic << 6;
+ $grbit |= $fDspGuts << 7;
+ $grbit |= $fFrozenNoSplit << 8;
+ $grbit |= $fSelected << 9; // Selected sheets.
+ $grbit |= $fSelected << 10; // Active sheet.
+ $grbit |= $fPageBreakPreview << 11;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvv', $grbit, $rwTop, $colLeft);
+
+ // FIXME !!!
+ $rgbHdr = 0x0040; // Row/column heading and gridline color index
+ $zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000);
+ $zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal();
+
+ $data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write BIFF record DEFAULTROWHEIGHT.
+ */
+ private function writeDefaultRowHeight(): void
+ {
+ $defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight();
+
+ if ($defaultRowHeight < 0) {
+ return;
+ }
+
+ // convert to twips
+ $defaultRowHeight = (int) 20 * $defaultRowHeight;
+
+ $record = 0x0225; // Record identifier
+ $length = 0x0004; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vv', 1, $defaultRowHeight);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write BIFF record DEFCOLWIDTH if COLINFO records are in use.
+ */
+ private function writeDefcol(): void
+ {
+ $defaultColWidth = 8;
+
+ $record = 0x0055; // Record identifier
+ $length = 0x0002; // Number of bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $defaultColWidth);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write BIFF record COLINFO to define column widths.
+ *
+ * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
+ * length record.
+ *
+ * @param array $col_array This is the only parameter received and is composed of the following:
+ * 0 => First formatted column,
+ * 1 => Last formatted column,
+ * 2 => Col width (8.43 is Excel default),
+ * 3 => The optional XF format of the column,
+ * 4 => Option flags.
+ * 5 => Optional outline level
+ */
+ private function writeColinfo($col_array): void
+ {
+ $colFirst = $col_array[0] ?? null;
+ $colLast = $col_array[1] ?? null;
+ $coldx = $col_array[2] ?? 8.43;
+ $xfIndex = $col_array[3] ?? 15;
+ $grbit = $col_array[4] ?? 0;
+ $level = $col_array[5] ?? 0;
+
+ $record = 0x007D; // Record identifier
+ $length = 0x000C; // Number of bytes to follow
+
+ $coldx *= 256; // Convert to units of 1/256 of a char
+
+ $ixfe = $xfIndex;
+ $reserved = 0x0000; // Reserved
+
+ $level = max(0, min($level, 7));
+ $grbit |= $level << 8;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write BIFF record SELECTION.
+ */
+ private function writeSelection(): void
+ {
+ // look up the selected cell range
+ $selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells());
+ $selectedCells = $selectedCells[0];
+ if (count($selectedCells) == 2) {
+ [$first, $last] = $selectedCells;
+ } else {
+ $first = $selectedCells[0];
+ $last = $selectedCells[0];
+ }
+
+ [$colFirst, $rwFirst] = Coordinate::coordinateFromString($first);
+ $colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index
+ --$rwFirst; // base 0 row index
+
+ [$colLast, $rwLast] = Coordinate::coordinateFromString($last);
+ $colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index
+ --$rwLast; // base 0 row index
+
+ // make sure we are not out of bounds
+ $colFirst = min($colFirst, 255);
+ $colLast = min($colLast, 255);
+
+ $rwFirst = min($rwFirst, 65535);
+ $rwLast = min($rwLast, 65535);
+
+ $record = 0x001D; // Record identifier
+ $length = 0x000F; // Number of bytes to follow
+
+ $pnn = $this->activePane; // Pane position
+ $rwAct = $rwFirst; // Active row
+ $colAct = $colFirst; // Active column
+ $irefAct = 0; // Active cell ref
+ $cref = 1; // Number of refs
+
+ // Swap last row/col for first row/col as necessary
+ if ($rwFirst > $rwLast) {
+ [$rwFirst, $rwLast] = [$rwLast, $rwFirst];
+ }
+
+ if ($colFirst > $colLast) {
+ [$colFirst, $colLast] = [$colLast, $colFirst];
+ }
+
+ $header = pack('vv', $record, $length);
+ $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the MERGEDCELLS records for all ranges of merged cells.
+ */
+ private function writeMergedCells(): void
+ {
+ $mergeCells = $this->phpSheet->getMergeCells();
+ $countMergeCells = count($mergeCells);
+
+ if ($countMergeCells == 0) {
+ return;
+ }
+
+ // maximum allowed number of merged cells per record
+ $maxCountMergeCellsPerRecord = 1027;
+
+ // record identifier
+ $record = 0x00E5;
+
+ // counter for total number of merged cells treated so far by the writer
+ $i = 0;
+
+ // counter for number of merged cells written in record currently being written
+ $j = 0;
+
+ // initialize record data
+ $recordData = '';
+
+ // loop through the merged cells
+ foreach ($mergeCells as $mergeCell) {
+ ++$i;
+ ++$j;
+
+ // extract the row and column indexes
+ $range = Coordinate::splitRange($mergeCell);
+ [$first, $last] = $range[0];
+ [$firstColumn, $firstRow] = Coordinate::indexesFromString($first);
+ [$lastColumn, $lastRow] = Coordinate::indexesFromString($last);
+
+ $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1);
+
+ // flush record if we have reached limit for number of merged cells, or reached final merged cell
+ if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) {
+ $recordData = pack('v', $j) . $recordData;
+ $length = strlen($recordData);
+ $header = pack('vv', $record, $length);
+ $this->append($header . $recordData);
+
+ // initialize for next record, if any
+ $recordData = '';
+ $j = 0;
+ }
+ }
+ }
+
+ /**
+ * Write SHEETLAYOUT record.
+ */
+ private function writeSheetLayout(): void
+ {
+ if (!$this->phpSheet->isTabColorSet()) {
+ return;
+ }
+
+ $recordData = pack(
+ 'vvVVVvv',
+ 0x0862,
+ 0x0000, // unused
+ 0x00000000, // unused
+ 0x00000000, // unused
+ 0x00000014, // size of record data
+ $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index
+ 0x0000 // unused
+ );
+
+ $length = strlen($recordData);
+
+ $record = 0x0862; // Record identifier
+ $header = pack('vv', $record, $length);
+ $this->append($header . $recordData);
+ }
+
+ /**
+ * Write SHEETPROTECTION.
+ */
+ private function writeSheetProtection(): void
+ {
+ // record identifier
+ $record = 0x0867;
+
+ // prepare options
+ $options = (int) !$this->phpSheet->getProtection()->getObjects()
+ | (int) !$this->phpSheet->getProtection()->getScenarios() << 1
+ | (int) !$this->phpSheet->getProtection()->getFormatCells() << 2
+ | (int) !$this->phpSheet->getProtection()->getFormatColumns() << 3
+ | (int) !$this->phpSheet->getProtection()->getFormatRows() << 4
+ | (int) !$this->phpSheet->getProtection()->getInsertColumns() << 5
+ | (int) !$this->phpSheet->getProtection()->getInsertRows() << 6
+ | (int) !$this->phpSheet->getProtection()->getInsertHyperlinks() << 7
+ | (int) !$this->phpSheet->getProtection()->getDeleteColumns() << 8
+ | (int) !$this->phpSheet->getProtection()->getDeleteRows() << 9
+ | (int) !$this->phpSheet->getProtection()->getSelectLockedCells() << 10
+ | (int) !$this->phpSheet->getProtection()->getSort() << 11
+ | (int) !$this->phpSheet->getProtection()->getAutoFilter() << 12
+ | (int) !$this->phpSheet->getProtection()->getPivotTables() << 13
+ | (int) !$this->phpSheet->getProtection()->getSelectUnlockedCells() << 14;
+
+ // record data
+ $recordData = pack(
+ 'vVVCVVvv',
+ 0x0867, // repeated record identifier
+ 0x0000, // not used
+ 0x0000, // not used
+ 0x00, // not used
+ 0x01000200, // unknown data
+ 0xFFFFFFFF, // unknown data
+ $options, // options
+ 0x0000 // not used
+ );
+
+ $length = strlen($recordData);
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $recordData);
+ }
+
+ /**
+ * Write BIFF record RANGEPROTECTION.
+ *
+ * Openoffice.org's Documentation of the Microsoft Excel File Format uses term RANGEPROTECTION for these records
+ * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records
+ */
+ private function writeRangeProtection(): void
+ {
+ foreach ($this->phpSheet->getProtectedCells() as $range => $password) {
+ // number of ranges, e.g. 'A1:B3 C20:D25'
+ $cellRanges = explode(' ', $range);
+ $cref = count($cellRanges);
+
+ $recordData = pack(
+ 'vvVVvCVvVv',
+ 0x0868,
+ 0x00,
+ 0x0000,
+ 0x0000,
+ 0x02,
+ 0x0,
+ 0x0000,
+ $cref,
+ 0x0000,
+ 0x00
+ );
+
+ foreach ($cellRanges as $cellRange) {
+ $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange);
+ }
+
+ // the rgbFeat structure
+ $recordData .= pack(
+ 'VV',
+ 0x0000,
+ hexdec($password)
+ );
+
+ $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData));
+
+ $length = strlen($recordData);
+
+ $record = 0x0868; // Record identifier
+ $header = pack('vv', $record, $length);
+ $this->append($header . $recordData);
+ }
+ }
+
+ /**
+ * Writes the Excel BIFF PANE record.
+ * The panes can either be frozen or thawed (unfrozen).
+ * Frozen panes are specified in terms of an integer number of rows and columns.
+ * Thawed panes are specified in terms of Excel's units for rows and columns.
+ */
+ private function writePanes(): void
+ {
+ if (!$this->phpSheet->getFreezePane()) {
+ // thaw panes
+ return;
+ }
+
+ [$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane());
+ $x = $column - 1;
+ $y = $row - 1;
+
+ [$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell());
+ //Coordinates are zero-based in xls files
+ $rwTop = $topRow - 1;
+ $colLeft = $leftMostColumn - 1;
+
+ $record = 0x0041; // Record identifier
+ $length = 0x000A; // Number of bytes to follow
+
+ // Determine which pane should be active. There is also the undocumented
+ // option to override this should it be necessary: may be removed later.
+ $pnnAct = null;
+ if ($x != 0 && $y != 0) {
+ $pnnAct = 0; // Bottom right
+ }
+ if ($x != 0 && $y == 0) {
+ $pnnAct = 1; // Top right
+ }
+ if ($x == 0 && $y != 0) {
+ $pnnAct = 2; // Bottom left
+ }
+ if ($x == 0 && $y == 0) {
+ $pnnAct = 3; // Top left
+ }
+
+ $this->activePane = $pnnAct; // Used in writeSelection
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the page setup SETUP BIFF record.
+ */
+ private function writeSetup(): void
+ {
+ $record = 0x00A1; // Record identifier
+ $length = 0x0022; // Number of bytes to follow
+
+ $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size
+ $iScale = $this->phpSheet->getPageSetup()->getScale() ?: 100; // Print scaling factor
+
+ $iPageStart = 0x01; // Starting page number
+ $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide
+ $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high
+ $grbit = 0x00; // Option flags
+ $iRes = 0x0258; // Print resolution
+ $iVRes = 0x0258; // Vertical print resolution
+
+ $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin
+
+ $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin
+ $iCopies = 0x01; // Number of copies
+
+ // Order of printing pages
+ $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER
+ ? 0x1 : 0x0;
+ // Page orientation
+ $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE)
+ ? 0x0 : 0x1;
+
+ $fNoPls = 0x0; // Setup not read from printer
+ $fNoColor = 0x0; // Print black and white
+ $fDraft = 0x0; // Print draft quality
+ $fNotes = 0x0; // Print notes
+ $fNoOrient = 0x0; // Orientation not set
+ $fUsePage = 0x0; // Use custom starting page
+
+ $grbit = $fLeftToRight;
+ $grbit |= $fLandscape << 1;
+ $grbit |= $fNoPls << 2;
+ $grbit |= $fNoColor << 3;
+ $grbit |= $fDraft << 4;
+ $grbit |= $fNotes << 5;
+ $grbit |= $fNoOrient << 6;
+ $grbit |= $fUsePage << 7;
+
+ $numHdr = pack('d', $numHdr);
+ $numFtr = pack('d', $numFtr);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $numHdr = strrev($numHdr);
+ $numFtr = strrev($numFtr);
+ }
+
+ $header = pack('vv', $record, $length);
+ $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes);
+ $data2 = $numHdr . $numFtr;
+ $data3 = pack('v', $iCopies);
+ $this->append($header . $data1 . $data2 . $data3);
+ }
+
+ /**
+ * Store the header caption BIFF record.
+ */
+ private function writeHeader(): void
+ {
+ $record = 0x0014; // Record identifier
+
+ /* removing for now
+ // need to fix character count (multibyte!)
+ if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) {
+ $str = $this->phpSheet->getHeaderFooter()->getOddHeader(); // header string
+ } else {
+ $str = '';
+ }
+ */
+
+ $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader());
+ $length = strlen($recordData);
+
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $recordData);
+ }
+
+ /**
+ * Store the footer caption BIFF record.
+ */
+ private function writeFooter(): void
+ {
+ $record = 0x0015; // Record identifier
+
+ /* removing for now
+ // need to fix character count (multibyte!)
+ if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) {
+ $str = $this->phpSheet->getHeaderFooter()->getOddFooter();
+ } else {
+ $str = '';
+ }
+ */
+
+ $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter());
+ $length = strlen($recordData);
+
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $recordData);
+ }
+
+ /**
+ * Store the horizontal centering HCENTER BIFF record.
+ */
+ private function writeHcenter(): void
+ {
+ $record = 0x0083; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fHCenter);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the vertical centering VCENTER BIFF record.
+ */
+ private function writeVcenter(): void
+ {
+ $record = 0x0084; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fVCenter);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the LEFTMARGIN BIFF record.
+ */
+ private function writeMarginLeft(): void
+ {
+ $record = 0x0026; // Record identifier
+ $length = 0x0008; // Bytes to follow
+
+ $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches
+
+ $header = pack('vv', $record, $length);
+ $data = pack('d', $margin);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $data = strrev($data);
+ }
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the RIGHTMARGIN BIFF record.
+ */
+ private function writeMarginRight(): void
+ {
+ $record = 0x0027; // Record identifier
+ $length = 0x0008; // Bytes to follow
+
+ $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches
+
+ $header = pack('vv', $record, $length);
+ $data = pack('d', $margin);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $data = strrev($data);
+ }
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the TOPMARGIN BIFF record.
+ */
+ private function writeMarginTop(): void
+ {
+ $record = 0x0028; // Record identifier
+ $length = 0x0008; // Bytes to follow
+
+ $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches
+
+ $header = pack('vv', $record, $length);
+ $data = pack('d', $margin);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $data = strrev($data);
+ }
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Store the BOTTOMMARGIN BIFF record.
+ */
+ private function writeMarginBottom(): void
+ {
+ $record = 0x0029; // Record identifier
+ $length = 0x0008; // Bytes to follow
+
+ $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches
+
+ $header = pack('vv', $record, $length);
+ $data = pack('d', $margin);
+ if (self::getByteOrder()) { // if it's Big Endian
+ $data = strrev($data);
+ }
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the PRINTHEADERS BIFF record.
+ */
+ private function writePrintHeaders(): void
+ {
+ $record = 0x002a; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fPrintRwCol = $this->printHeaders; // Boolean flag
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fPrintRwCol);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
+ * GRIDSET record.
+ */
+ private function writePrintGridlines(): void
+ {
+ $record = 0x002b; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fPrintGrid);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the GRIDSET BIFF record. Must be used in conjunction with the
+ * PRINTGRIDLINES record.
+ */
+ private function writeGridset(): void
+ {
+ $record = 0x0082; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fGridSet);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet.
+ */
+ private function writeAutoFilterInfo(): void
+ {
+ $record = 0x009D; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange());
+ $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0];
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $iNumFilters);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the GUTS BIFF record. This is used to configure the gutter margins
+ * where Excel outline symbols are displayed. The visibility of the gutters is
+ * controlled by a flag in WSBOOL.
+ *
+ * @see writeWsbool()
+ */
+ private function writeGuts(): void
+ {
+ $record = 0x0080; // Record identifier
+ $length = 0x0008; // Bytes to follow
+
+ $dxRwGut = 0x0000; // Size of row gutter
+ $dxColGut = 0x0000; // Size of col gutter
+
+ // determine maximum row outline level
+ $maxRowOutlineLevel = 0;
+ foreach ($this->phpSheet->getRowDimensions() as $rowDimension) {
+ $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel());
+ }
+
+ $col_level = 0;
+
+ // Calculate the maximum column outline level. The equivalent calculation
+ // for the row outline level is carried out in writeRow().
+ $colcount = count($this->columnInfo);
+ for ($i = 0; $i < $colcount; ++$i) {
+ $col_level = max($this->columnInfo[$i][5], $col_level);
+ }
+
+ // Set the limits for the outline levels (0 <= x <= 7).
+ $col_level = max(0, min($col_level, 7));
+
+ // The displayed level is one greater than the max outline levels
+ if ($maxRowOutlineLevel) {
+ ++$maxRowOutlineLevel;
+ }
+ if ($col_level) {
+ ++$col_level;
+ }
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
+ * with the SETUP record.
+ */
+ private function writeWsbool(): void
+ {
+ $record = 0x0081; // Record identifier
+ $length = 0x0002; // Bytes to follow
+ $grbit = 0x0000;
+
+ // The only option that is of interest is the flag for fit to page. So we
+ // set all the options in one go.
+ //
+ // Set the option flags
+ $grbit |= 0x0001; // Auto page breaks visible
+ if ($this->outlineStyle) {
+ $grbit |= 0x0020; // Auto outline styles
+ }
+ if ($this->phpSheet->getShowSummaryBelow()) {
+ $grbit |= 0x0040; // Outline summary below
+ }
+ if ($this->phpSheet->getShowSummaryRight()) {
+ $grbit |= 0x0080; // Outline summary right
+ }
+ if ($this->phpSheet->getPageSetup()->getFitToPage()) {
+ $grbit |= 0x0100; // Page setup fit to page
+ }
+ if ($this->outlineOn) {
+ $grbit |= 0x0400; // Outline symbols displayed
+ }
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $grbit);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records.
+ */
+ private function writeBreaks(): void
+ {
+ // initialize
+ $vbreaks = [];
+ $hbreaks = [];
+
+ foreach ($this->phpSheet->getBreaks() as $cell => $breakType) {
+ // Fetch coordinates
+ $coordinates = Coordinate::coordinateFromString($cell);
+
+ // Decide what to do by the type of break
+ switch ($breakType) {
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_COLUMN:
+ // Add to list of vertical breaks
+ $vbreaks[] = Coordinate::columnIndexFromString($coordinates[0]) - 1;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_ROW:
+ // Add to list of horizontal breaks
+ $hbreaks[] = $coordinates[1];
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_NONE:
+ default:
+ // Nothing to do
+ break;
+ }
+ }
+
+ //horizontal page breaks
+ if (!empty($hbreaks)) {
+ // Sort and filter array of page breaks
+ sort($hbreaks, SORT_NUMERIC);
+ if ($hbreaks[0] == 0) { // don't use first break if it's 0
+ array_shift($hbreaks);
+ }
+
+ $record = 0x001b; // Record identifier
+ $cbrk = count($hbreaks); // Number of page breaks
+ $length = 2 + 6 * $cbrk; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $cbrk);
+
+ // Append each page break
+ foreach ($hbreaks as $hbreak) {
+ $data .= pack('vvv', $hbreak, 0x0000, 0x00ff);
+ }
+
+ $this->append($header . $data);
+ }
+
+ // vertical page breaks
+ if (!empty($vbreaks)) {
+ // 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
+ // It is slightly higher in Excel 97/200, approx. 1026
+ $vbreaks = array_slice($vbreaks, 0, 1000);
+
+ // Sort and filter array of page breaks
+ sort($vbreaks, SORT_NUMERIC);
+ if ($vbreaks[0] == 0) { // don't use first break if it's 0
+ array_shift($vbreaks);
+ }
+
+ $record = 0x001a; // Record identifier
+ $cbrk = count($vbreaks); // Number of page breaks
+ $length = 2 + 6 * $cbrk; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $cbrk);
+
+ // Append each page break
+ foreach ($vbreaks as $vbreak) {
+ $data .= pack('vvv', $vbreak, 0x0000, 0xffff);
+ }
+
+ $this->append($header . $data);
+ }
+ }
+
+ /**
+ * Set the Biff PROTECT record to indicate that the worksheet is protected.
+ */
+ private function writeProtect(): void
+ {
+ // Exit unless sheet protection has been specified
+ if (!$this->phpSheet->getProtection()->getSheet()) {
+ return;
+ }
+
+ $record = 0x0012; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $fLock = 1; // Worksheet is protected
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $fLock);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write SCENPROTECT.
+ */
+ private function writeScenProtect(): void
+ {
+ // Exit if sheet protection is not active
+ if (!$this->phpSheet->getProtection()->getSheet()) {
+ return;
+ }
+
+ // Exit if scenarios are not protected
+ if (!$this->phpSheet->getProtection()->getScenarios()) {
+ return;
+ }
+
+ $record = 0x00DD; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', 1);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write OBJECTPROTECT.
+ */
+ private function writeObjectProtect(): void
+ {
+ // Exit if sheet protection is not active
+ if (!$this->phpSheet->getProtection()->getSheet()) {
+ return;
+ }
+
+ // Exit if objects are not protected
+ if (!$this->phpSheet->getProtection()->getObjects()) {
+ return;
+ }
+
+ $record = 0x0063; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', 1);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write the worksheet PASSWORD record.
+ */
+ private function writePassword(): void
+ {
+ // Exit unless sheet protection and password have been specified
+ if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword()) {
+ return;
+ }
+
+ $record = 0x0013; // Record identifier
+ $length = 0x0002; // Bytes to follow
+
+ $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password
+
+ $header = pack('vv', $record, $length);
+ $data = pack('v', $wPassword);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Insert a 24bit bitmap image in a worksheet.
+ *
+ * @param int $row The row we are going to insert the bitmap into
+ * @param int $col The column we are going to insert the bitmap into
+ * @param mixed $bitmap The bitmap filename or GD-image resource
+ * @param int $x the horizontal position (offset) of the image inside the cell
+ * @param int $y the vertical position (offset) of the image inside the cell
+ * @param float $scale_x The horizontal scale
+ * @param float $scale_y The vertical scale
+ */
+ public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void
+ {
+ $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage
+ ? $this->processBitmapGd($bitmap)
+ : $this->processBitmap($bitmap));
+ [$width, $height, $size, $data] = $bitmap_array;
+
+ // Scale the frame of the image.
+ $width *= $scale_x;
+ $height *= $scale_y;
+
+ // Calculate the vertices of the image and write the OBJ record
+ $this->positionImage($col, $row, $x, $y, $width, $height);
+
+ // Write the IMDATA record to store the bitmap data
+ $record = 0x007f;
+ $length = 8 + $size;
+ $cf = 0x09;
+ $env = 0x01;
+ $lcb = $size;
+
+ $header = pack('vvvvV', $record, $length, $cf, $env, $lcb);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Calculate the vertices that define the position of the image as required by
+ * the OBJ record.
+ *
+ * +------------+------------+
+ * | A | B |
+ * +-----+------------+------------+
+ * | |(x1,y1) | |
+ * | 1 |(A1)._______|______ |
+ * | | | | |
+ * | | | | |
+ * +-----+----| BITMAP |-----+
+ * | | | | |
+ * | 2 | |______________. |
+ * | | | (B2)|
+ * | | | (x2,y2)|
+ * +---- +------------+------------+
+ *
+ * Example of a bitmap that covers some of the area from cell A1 to cell B2.
+ *
+ * Based on the width and height of the bitmap we need to calculate 8 vars:
+ * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
+ * The width and height of the cells are also variable and have to be taken into
+ * account.
+ * The values of $col_start and $row_start are passed in from the calling
+ * function. The values of $col_end and $row_end are calculated by subtracting
+ * the width and height of the bitmap from the width and height of the
+ * underlying cells.
+ * The vertices are expressed as a percentage of the underlying cell width as
+ * follows (rhs values are in pixels):
+ *
+ * x1 = X / W *1024
+ * y1 = Y / H *256
+ * x2 = (X-1) / W *1024
+ * y2 = (Y-1) / H *256
+ *
+ * Where: X is distance from the left side of the underlying cell
+ * Y is distance from the top of the underlying cell
+ * W is the width of the cell
+ * H is the height of the cell
+ * The SDK incorrectly states that the height should be expressed as a
+ * percentage of 1024.
+ *
+ * @param int $col_start Col containing upper left corner of object
+ * @param int $row_start Row containing top left corner of object
+ * @param int $x1 Distance to left side of object
+ * @param int $y1 Distance to top of object
+ * @param int $width Width of image frame
+ * @param int $height Height of image frame
+ */
+ public function positionImage($col_start, $row_start, $x1, $y1, $width, $height): void
+ {
+ // Initialise end cell to the same as the start cell
+ $col_end = $col_start; // Col containing lower right corner of object
+ $row_end = $row_start; // Row containing bottom right corner of object
+
+ // Zero the specified offset if greater than the cell dimensions
+ if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) {
+ $x1 = 0;
+ }
+ if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) {
+ $y1 = 0;
+ }
+
+ $width = $width + $x1 - 1;
+ $height = $height + $y1 - 1;
+
+ // Subtract the underlying cell widths to find the end cell of the image
+ while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) {
+ $width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1));
+ ++$col_end;
+ }
+
+ // Subtract the underlying cell heights to find the end cell of the image
+ while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) {
+ $height -= Xls::sizeRow($this->phpSheet, $row_end + 1);
+ ++$row_end;
+ }
+
+ // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
+ // with zero eight or width.
+ //
+ if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) {
+ return;
+ }
+ if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) {
+ return;
+ }
+ if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) {
+ return;
+ }
+ if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) {
+ return;
+ }
+
+ // Convert the pixel values to the percentage value expected by Excel
+ $x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024;
+ $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256;
+ $x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object
+ $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object
+
+ $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2);
+ }
+
+ /**
+ * Store the OBJ record that precedes an IMDATA record. This could be generalise
+ * to support other Excel objects.
+ *
+ * @param int $colL Column containing upper left corner of object
+ * @param int $dxL Distance from left side of cell
+ * @param int $rwT Row containing top left corner of object
+ * @param int $dyT Distance from top of cell
+ * @param int $colR Column containing lower right corner of object
+ * @param int $dxR Distance from right of cell
+ * @param int $rwB Row containing bottom right corner of object
+ * @param int $dyB Distance from bottom of cell
+ */
+ private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dyB): void
+ {
+ $record = 0x005d; // Record identifier
+ $length = 0x003c; // Bytes to follow
+
+ $cObj = 0x0001; // Count of objects in file (set to 1)
+ $OT = 0x0008; // Object type. 8 = Picture
+ $id = 0x0001; // Object ID
+ $grbit = 0x0614; // Option flags
+
+ $cbMacro = 0x0000; // Length of FMLA structure
+ $Reserved1 = 0x0000; // Reserved
+ $Reserved2 = 0x0000; // Reserved
+
+ $icvBack = 0x09; // Background colour
+ $icvFore = 0x09; // Foreground colour
+ $fls = 0x00; // Fill pattern
+ $fAuto = 0x00; // Automatic fill
+ $icv = 0x08; // Line colour
+ $lns = 0xff; // Line style
+ $lnw = 0x01; // Line weight
+ $fAutoB = 0x00; // Automatic border
+ $frs = 0x0000; // Frame style
+ $cf = 0x0009; // Image format, 9 = bitmap
+ $Reserved3 = 0x0000; // Reserved
+ $cbPictFmla = 0x0000; // Length of FMLA structure
+ $Reserved4 = 0x0000; // Reserved
+ $grbit2 = 0x0001; // Option flags
+ $Reserved5 = 0x0000; // Reserved
+
+ $header = pack('vv', $record, $length);
+ $data = pack('V', $cObj);
+ $data .= pack('v', $OT);
+ $data .= pack('v', $id);
+ $data .= pack('v', $grbit);
+ $data .= pack('v', $colL);
+ $data .= pack('v', $dxL);
+ $data .= pack('v', $rwT);
+ $data .= pack('v', $dyT);
+ $data .= pack('v', $colR);
+ $data .= pack('v', $dxR);
+ $data .= pack('v', $rwB);
+ $data .= pack('v', $dyB);
+ $data .= pack('v', $cbMacro);
+ $data .= pack('V', $Reserved1);
+ $data .= pack('v', $Reserved2);
+ $data .= pack('C', $icvBack);
+ $data .= pack('C', $icvFore);
+ $data .= pack('C', $fls);
+ $data .= pack('C', $fAuto);
+ $data .= pack('C', $icv);
+ $data .= pack('C', $lns);
+ $data .= pack('C', $lnw);
+ $data .= pack('C', $fAutoB);
+ $data .= pack('v', $frs);
+ $data .= pack('V', $cf);
+ $data .= pack('v', $Reserved3);
+ $data .= pack('v', $cbPictFmla);
+ $data .= pack('v', $Reserved4);
+ $data .= pack('v', $grbit2);
+ $data .= pack('V', $Reserved5);
+
+ $this->append($header . $data);
+ }
+
+ /**
+ * Convert a GD-image into the internal format.
+ *
+ * @param GdImage|resource $image The image to process
+ *
+ * @return array Array with data and properties of the bitmap
+ */
+ public function processBitmapGd($image)
+ {
+ $width = imagesx($image);
+ $height = imagesy($image);
+
+ $data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18);
+ for ($j = $height; --$j;) {
+ for ($i = 0; $i < $width; ++$i) {
+ $color = imagecolorsforindex($image, imagecolorat($image, $i, $j));
+ foreach (['red', 'green', 'blue'] as $key) {
+ $color[$key] = $color[$key] + round((255 - $color[$key]) * $color['alpha'] / 127);
+ }
+ $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']);
+ }
+ if (3 * $width % 4) {
+ $data .= str_repeat("\x00", 4 - 3 * $width % 4);
+ }
+ }
+
+ return [$width, $height, strlen($data), $data];
+ }
+
+ /**
+ * Convert a 24 bit bitmap into the modified internal format used by Windows.
+ * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
+ * MSDN library.
+ *
+ * @param string $bitmap The bitmap to process
+ *
+ * @return array Array with data and properties of the bitmap
+ */
+ public function processBitmap($bitmap)
+ {
+ // Open file.
+ $bmp_fd = @fopen($bitmap, 'rb');
+ if (!$bmp_fd) {
+ throw new WriterException("Couldn't import $bitmap");
+ }
+
+ // Slurp the file into a string.
+ $data = fread($bmp_fd, filesize($bitmap));
+
+ // Check that the file is big enough to be a bitmap.
+ if (strlen($data) <= 0x36) {
+ throw new WriterException("$bitmap doesn't contain enough data.\n");
+ }
+
+ // The first 2 bytes are used to identify the bitmap.
+ $identity = unpack('A2ident', $data);
+ if ($identity['ident'] != 'BM') {
+ throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n");
+ }
+
+ // Remove bitmap data: ID.
+ $data = substr($data, 2);
+
+ // Read and remove the bitmap size. This is more reliable than reading
+ // the data size at offset 0x22.
+ //
+ $size_array = unpack('Vsa', substr($data, 0, 4));
+ $size = $size_array['sa'];
+ $data = substr($data, 4);
+ $size -= 0x36; // Subtract size of bitmap header.
+ $size += 0x0C; // Add size of BIFF header.
+
+ // Remove bitmap data: reserved, offset, header length.
+ $data = substr($data, 12);
+
+ // Read and remove the bitmap width and height. Verify the sizes.
+ $width_and_height = unpack('V2', substr($data, 0, 8));
+ $width = $width_and_height[1];
+ $height = $width_and_height[2];
+ $data = substr($data, 8);
+ if ($width > 0xFFFF) {
+ throw new WriterException("$bitmap: largest image width supported is 65k.\n");
+ }
+ if ($height > 0xFFFF) {
+ throw new WriterException("$bitmap: largest image height supported is 65k.\n");
+ }
+
+ // Read and remove the bitmap planes and bpp data. Verify them.
+ $planes_and_bitcount = unpack('v2', substr($data, 0, 4));
+ $data = substr($data, 4);
+ if ($planes_and_bitcount[2] != 24) { // Bitcount
+ throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n");
+ }
+ if ($planes_and_bitcount[1] != 1) {
+ throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n");
+ }
+
+ // Read and remove the bitmap compression. Verify compression.
+ $compression = unpack('Vcomp', substr($data, 0, 4));
+ $data = substr($data, 4);
+
+ if ($compression['comp'] != 0) {
+ throw new WriterException("$bitmap: compression not supported in bitmap image.\n");
+ }
+
+ // Remove bitmap data: data size, hres, vres, colours, imp. colours.
+ $data = substr($data, 20);
+
+ // Add the BITMAPCOREHEADER data
+ $header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18);
+ $data = $header . $data;
+
+ return [$width, $height, $size, $data];
+ }
+
+ /**
+ * Store the window zoom factor. This should be a reduced fraction but for
+ * simplicity we will store all fractions with a numerator of 100.
+ */
+ private function writeZoom(): void
+ {
+ // If scale is 100 we don't need to write a record
+ if ($this->phpSheet->getSheetView()->getZoomScale() == 100) {
+ return;
+ }
+
+ $record = 0x00A0; // Record identifier
+ $length = 0x0004; // Bytes to follow
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Get Escher object.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ public function getEscher()
+ {
+ return $this->escher;
+ }
+
+ /**
+ * Set Escher object.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
+ */
+ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null): void
+ {
+ $this->escher = $pValue;
+ }
+
+ /**
+ * Write MSODRAWING record.
+ */
+ private function writeMsoDrawing(): void
+ {
+ // write the Escher stream if necessary
+ if (isset($this->escher)) {
+ $writer = new Escher($this->escher);
+ $data = $writer->close();
+ $spOffsets = $writer->getSpOffsets();
+ $spTypes = $writer->getSpTypes();
+ // write the neccesary MSODRAWING, OBJ records
+
+ // split the Escher stream
+ $spOffsets[0] = 0;
+ $nm = count($spOffsets) - 1; // number of shapes excluding first shape
+ for ($i = 1; $i <= $nm; ++$i) {
+ // MSODRAWING record
+ $record = 0x00EC; // Record identifier
+
+ // chunk of Escher stream for one shape
+ $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]);
+
+ $length = strlen($dataChunk);
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $dataChunk);
+
+ // OBJ record
+ $record = 0x005D; // record identifier
+ $objData = '';
+
+ // ftCmo
+ if ($spTypes[$i] == 0x00C9) {
+ // Add ftCmo (common object data) subobject
+ $objData .=
+ pack(
+ 'vvvvvVVV',
+ 0x0015, // 0x0015 = ftCmo
+ 0x0012, // length of ftCmo data
+ 0x0014, // object type, 0x0014 = filter
+ $i, // object id number, Excel seems to use 1-based index, local for the sheet
+ 0x2101, // option flags, 0x2001 is what OpenOffice.org uses
+ 0, // reserved
+ 0, // reserved
+ 0 // reserved
+ );
+
+ // Add ftSbs Scroll bar subobject
+ $objData .= pack('vv', 0x00C, 0x0014);
+ $objData .= pack('H*', '0000000000000000640001000A00000010000100');
+ // Add ftLbsData (List box data) subobject
+ $objData .= pack('vv', 0x0013, 0x1FEE);
+ $objData .= pack('H*', '00000000010001030000020008005700');
+ } else {
+ // Add ftCmo (common object data) subobject
+ $objData .=
+ pack(
+ 'vvvvvVVV',
+ 0x0015, // 0x0015 = ftCmo
+ 0x0012, // length of ftCmo data
+ 0x0008, // object type, 0x0008 = picture
+ $i, // object id number, Excel seems to use 1-based index, local for the sheet
+ 0x6011, // option flags, 0x6011 is what OpenOffice.org uses
+ 0, // reserved
+ 0, // reserved
+ 0 // reserved
+ );
+ }
+
+ // ftEnd
+ $objData .=
+ pack(
+ 'vv',
+ 0x0000, // 0x0000 = ftEnd
+ 0x0000 // length of ftEnd data
+ );
+
+ $length = strlen($objData);
+ $header = pack('vv', $record, $length);
+ $this->append($header . $objData);
+ }
+ }
+ }
+
+ /**
+ * Store the DATAVALIDATIONS and DATAVALIDATION records.
+ */
+ private function writeDataValidity(): void
+ {
+ // Datavalidation collection
+ $dataValidationCollection = $this->phpSheet->getDataValidationCollection();
+
+ // Write data validations?
+ if (!empty($dataValidationCollection)) {
+ // DATAVALIDATIONS record
+ $record = 0x01B2; // Record identifier
+ $length = 0x0012; // Bytes to follow
+
+ $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records
+ $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position
+ $verPos = 0x00000000; // Vertical position of prompt box, if fixed position
+ $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection));
+ $this->append($header . $data);
+
+ // DATAVALIDATION records
+ $record = 0x01BE; // Record identifier
+
+ foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) {
+ // options
+ $options = 0x00000000;
+
+ // data type
+ $type = CellDataValidation::type($dataValidation);
+
+ $options |= $type << 0;
+
+ // error style
+ $errorStyle = CellDataValidation::errorStyle($dataValidation);
+
+ $options |= $errorStyle << 4;
+
+ // explicit formula?
+ if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) {
+ $options |= 0x01 << 7;
+ }
+
+ // empty cells allowed
+ $options |= $dataValidation->getAllowBlank() << 8;
+
+ // show drop down
+ $options |= (!$dataValidation->getShowDropDown()) << 9;
+
+ // show input message
+ $options |= $dataValidation->getShowInputMessage() << 18;
+
+ // show error message
+ $options |= $dataValidation->getShowErrorMessage() << 19;
+
+ // condition operator
+ $operator = CellDataValidation::operator($dataValidation);
+
+ $options |= $operator << 20;
+
+ $data = pack('V', $options);
+
+ // prompt title
+ $promptTitle = $dataValidation->getPromptTitle() !== '' ?
+ $dataValidation->getPromptTitle() : chr(0);
+ $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle);
+
+ // error title
+ $errorTitle = $dataValidation->getErrorTitle() !== '' ?
+ $dataValidation->getErrorTitle() : chr(0);
+ $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle);
+
+ // prompt text
+ $prompt = $dataValidation->getPrompt() !== '' ?
+ $dataValidation->getPrompt() : chr(0);
+ $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt);
+
+ // error text
+ $error = $dataValidation->getError() !== '' ?
+ $dataValidation->getError() : chr(0);
+ $data .= StringHelper::UTF8toBIFF8UnicodeLong($error);
+
+ // formula 1
+ try {
+ $formula1 = $dataValidation->getFormula1();
+ if ($type == 0x03) { // list type
+ $formula1 = str_replace(',', chr(0), $formula1);
+ }
+ $this->parser->parse($formula1);
+ $formula1 = $this->parser->toReversePolish();
+ $sz1 = strlen($formula1);
+ } catch (PhpSpreadsheetException $e) {
+ $sz1 = 0;
+ $formula1 = '';
+ }
+ $data .= pack('vv', $sz1, 0x0000);
+ $data .= $formula1;
+
+ // formula 2
+ try {
+ $formula2 = $dataValidation->getFormula2();
+ if ($formula2 === '') {
+ throw new WriterException('No formula2');
+ }
+ $this->parser->parse($formula2);
+ $formula2 = $this->parser->toReversePolish();
+ $sz2 = strlen($formula2);
+ } catch (PhpSpreadsheetException $e) {
+ $sz2 = 0;
+ $formula2 = '';
+ }
+ $data .= pack('vv', $sz2, 0x0000);
+ $data .= $formula2;
+
+ // cell range address list
+ $data .= pack('v', 0x0001);
+ $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate);
+
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+
+ $this->append($header . $data);
+ }
+ }
+ }
+
+ /**
+ * Write PLV Record.
+ */
+ private function writePageLayoutView(): void
+ {
+ $record = 0x088B; // Record identifier
+ $length = 0x0010; // Bytes to follow
+
+ $rt = 0x088B; // 2
+ $grbitFrt = 0x0000; // 2
+ $reserved = 0x0000000000000000; // 8
+ $wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2
+
+ // The options flags that comprise $grbit
+ if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) {
+ $fPageLayoutView = 1;
+ } else {
+ $fPageLayoutView = 0;
+ }
+ $fRulerVisible = 0;
+ $fWhitespaceHidden = 0;
+
+ $grbit = $fPageLayoutView; // 2
+ $grbit |= $fRulerVisible << 1;
+ $grbit |= $fWhitespaceHidden << 3;
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit);
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write CFRule Record.
+ */
+ private function writeCFRule(Conditional $conditional): void
+ {
+ $record = 0x01B1; // Record identifier
+ $type = null; // Type of the CF
+ $operatorType = null; // Comparison operator
+
+ if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) {
+ $type = 0x02;
+ $operatorType = 0x00;
+ } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) {
+ $type = 0x01;
+
+ switch ($conditional->getOperatorType()) {
+ case Conditional::OPERATOR_NONE:
+ $operatorType = 0x00;
+
+ break;
+ case Conditional::OPERATOR_EQUAL:
+ $operatorType = 0x03;
+
+ break;
+ case Conditional::OPERATOR_GREATERTHAN:
+ $operatorType = 0x05;
+
+ break;
+ case Conditional::OPERATOR_GREATERTHANOREQUAL:
+ $operatorType = 0x07;
+
+ break;
+ case Conditional::OPERATOR_LESSTHAN:
+ $operatorType = 0x06;
+
+ break;
+ case Conditional::OPERATOR_LESSTHANOREQUAL:
+ $operatorType = 0x08;
+
+ break;
+ case Conditional::OPERATOR_NOTEQUAL:
+ $operatorType = 0x04;
+
+ break;
+ case Conditional::OPERATOR_BETWEEN:
+ $operatorType = 0x01;
+
+ break;
+ // not OPERATOR_NOTBETWEEN 0x02
+ }
+ }
+
+ // $szValue1 : size of the formula data for first value or formula
+ // $szValue2 : size of the formula data for second value or formula
+ $arrConditions = $conditional->getConditions();
+ $numConditions = count($arrConditions);
+ if ($numConditions == 1) {
+ $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000);
+ $szValue2 = 0x0000;
+ $operand1 = pack('Cv', 0x1E, $arrConditions[0]);
+ $operand2 = null;
+ } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) {
+ $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000);
+ $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000);
+ $operand1 = pack('Cv', 0x1E, $arrConditions[0]);
+ $operand2 = pack('Cv', 0x1E, $arrConditions[1]);
+ } else {
+ $szValue1 = 0x0000;
+ $szValue2 = 0x0000;
+ $operand1 = null;
+ $operand2 = null;
+ }
+
+ // $flags : Option flags
+ // Alignment
+ $bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() === null ? 1 : 0);
+ $bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() === null ? 1 : 0);
+ $bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() === false ? 1 : 0);
+ $bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() === null ? 1 : 0);
+ $bIndent = ($conditional->getStyle()->getAlignment()->getIndent() === 0 ? 1 : 0);
+ $bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() === false ? 1 : 0);
+ if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) {
+ $bFormatAlign = 1;
+ } else {
+ $bFormatAlign = 0;
+ }
+ // Protection
+ $bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() == null ? 1 : 0);
+ $bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() == null ? 1 : 0);
+ if ($bProtLocked == 0 || $bProtHidden == 0) {
+ $bFormatProt = 1;
+ } else {
+ $bFormatProt = 0;
+ }
+ // Border
+ $bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getRight()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getTop()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ if ($bBorderLeft == 0 || $bBorderRight == 0 || $bBorderTop == 0 || $bBorderBottom == 0) {
+ $bFormatBorder = 1;
+ } else {
+ $bFormatBorder = 0;
+ }
+ // Pattern
+ $bFillStyle = ($conditional->getStyle()->getFill()->getFillType() === null ? 0 : 1);
+ $bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() == null ? 0 : 1);
+ $bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() == null ? 0 : 1);
+ if ($bFillStyle == 0 || $bFillColor == 0 || $bFillColorBg == 0) {
+ $bFormatFill = 1;
+ } else {
+ $bFormatFill = 0;
+ }
+ // Font
+ if (
+ $conditional->getStyle()->getFont()->getName() !== null
+ || $conditional->getStyle()->getFont()->getSize() !== null
+ || $conditional->getStyle()->getFont()->getBold() !== null
+ || $conditional->getStyle()->getFont()->getItalic() !== null
+ || $conditional->getStyle()->getFont()->getSuperscript() !== null
+ || $conditional->getStyle()->getFont()->getSubscript() !== null
+ || $conditional->getStyle()->getFont()->getUnderline() !== null
+ || $conditional->getStyle()->getFont()->getStrikethrough() !== null
+ || $conditional->getStyle()->getFont()->getColor()->getARGB() != null
+ ) {
+ $bFormatFont = 1;
+ } else {
+ $bFormatFont = 0;
+ }
+ // Alignment
+ $flags = 0;
+ $flags |= (1 == $bAlignHz ? 0x00000001 : 0);
+ $flags |= (1 == $bAlignVt ? 0x00000002 : 0);
+ $flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0);
+ $flags |= (1 == $bTxRotation ? 0x00000008 : 0);
+ // Justify last line flag
+ $flags |= (1 == 1 ? 0x00000010 : 0);
+ $flags |= (1 == $bIndent ? 0x00000020 : 0);
+ $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0);
+ // Default
+ $flags |= (1 == 1 ? 0x00000080 : 0);
+ // Protection
+ $flags |= (1 == $bProtLocked ? 0x00000100 : 0);
+ $flags |= (1 == $bProtHidden ? 0x00000200 : 0);
+ // Border
+ $flags |= (1 == $bBorderLeft ? 0x00000400 : 0);
+ $flags |= (1 == $bBorderRight ? 0x00000800 : 0);
+ $flags |= (1 == $bBorderTop ? 0x00001000 : 0);
+ $flags |= (1 == $bBorderBottom ? 0x00002000 : 0);
+ $flags |= (1 == 1 ? 0x00004000 : 0); // Top left to Bottom right border
+ $flags |= (1 == 1 ? 0x00008000 : 0); // Bottom left to Top right border
+ // Pattern
+ $flags |= (1 == $bFillStyle ? 0x00010000 : 0);
+ $flags |= (1 == $bFillColor ? 0x00020000 : 0);
+ $flags |= (1 == $bFillColorBg ? 0x00040000 : 0);
+ $flags |= (1 == 1 ? 0x00380000 : 0);
+ // Font
+ $flags |= (1 == $bFormatFont ? 0x04000000 : 0);
+ // Alignment:
+ $flags |= (1 == $bFormatAlign ? 0x08000000 : 0);
+ // Border
+ $flags |= (1 == $bFormatBorder ? 0x10000000 : 0);
+ // Pattern
+ $flags |= (1 == $bFormatFill ? 0x20000000 : 0);
+ // Protection
+ $flags |= (1 == $bFormatProt ? 0x40000000 : 0);
+ // Text direction
+ $flags |= (1 == 0 ? 0x80000000 : 0);
+
+ $dataBlockFont = null;
+ $dataBlockAlign = null;
+ $dataBlockBorder = null;
+ $dataBlockFill = null;
+
+ // Data Blocks
+ if ($bFormatFont == 1) {
+ // Font Name
+ if ($conditional->getStyle()->getFont()->getName() === null) {
+ $dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
+ $dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
+ } else {
+ $dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName());
+ }
+ // Font Size
+ if ($conditional->getStyle()->getFont()->getSize() === null) {
+ $dataBlockFont .= pack('V', 20 * 11);
+ } else {
+ $dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize());
+ }
+ // Font Options
+ $dataBlockFont .= pack('V', 0);
+ // Font weight
+ if ($conditional->getStyle()->getFont()->getBold() === true) {
+ $dataBlockFont .= pack('v', 0x02BC);
+ } else {
+ $dataBlockFont .= pack('v', 0x0190);
+ }
+ // Escapement type
+ if ($conditional->getStyle()->getFont()->getSubscript() === true) {
+ $dataBlockFont .= pack('v', 0x02);
+ $fontEscapement = 0;
+ } elseif ($conditional->getStyle()->getFont()->getSuperscript() === true) {
+ $dataBlockFont .= pack('v', 0x01);
+ $fontEscapement = 0;
+ } else {
+ $dataBlockFont .= pack('v', 0x00);
+ $fontEscapement = 1;
+ }
+ // Underline type
+ switch ($conditional->getStyle()->getFont()->getUnderline()) {
+ case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE:
+ $dataBlockFont .= pack('C', 0x00);
+ $fontUnderline = 0;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE:
+ $dataBlockFont .= pack('C', 0x02);
+ $fontUnderline = 0;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING:
+ $dataBlockFont .= pack('C', 0x22);
+ $fontUnderline = 0;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE:
+ $dataBlockFont .= pack('C', 0x01);
+ $fontUnderline = 0;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING:
+ $dataBlockFont .= pack('C', 0x21);
+ $fontUnderline = 0;
+
+ break;
+ default:
+ $dataBlockFont .= pack('C', 0x00);
+ $fontUnderline = 1;
+
+ break;
+ }
+ // Not used (3)
+ $dataBlockFont .= pack('vC', 0x0000, 0x00);
+ // Font color index
+ $colorIdx = Style\ColorMap::lookup($conditional->getStyle()->getFont()->getColor(), 0x00);
+
+ $dataBlockFont .= pack('V', $colorIdx);
+ // Not used (4)
+ $dataBlockFont .= pack('V', 0x00000000);
+ // Options flags for modified font attributes
+ $optionsFlags = 0;
+ $optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0);
+ $optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0);
+ $optionsFlags |= (1 == 1 ? 0x00000008 : 0);
+ $optionsFlags |= (1 == 1 ? 0x00000010 : 0);
+ $optionsFlags |= (1 == 0 ? 0x00000020 : 0);
+ $optionsFlags |= (1 == 1 ? 0x00000080 : 0);
+ $dataBlockFont .= pack('V', $optionsFlags);
+ // Escapement type
+ $dataBlockFont .= pack('V', $fontEscapement);
+ // Underline type
+ $dataBlockFont .= pack('V', $fontUnderline);
+ // Always
+ $dataBlockFont .= pack('V', 0x00000000);
+ // Always
+ $dataBlockFont .= pack('V', 0x00000000);
+ // Not used (8)
+ $dataBlockFont .= pack('VV', 0x00000000, 0x00000000);
+ // Always
+ $dataBlockFont .= pack('v', 0x0001);
+ }
+ if ($bFormatAlign === 1) {
+ // Alignment and text break
+ $blockAlign = Style\CellAlignment::horizontal($conditional->getStyle()->getAlignment());
+ $blockAlign |= Style\CellAlignment::wrap($conditional->getStyle()->getAlignment()) << 3;
+ $blockAlign |= Style\CellAlignment::vertical($conditional->getStyle()->getAlignment()) << 4;
+ $blockAlign |= 0 << 7;
+
+ // Text rotation angle
+ $blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation();
+
+ // Indentation
+ $blockIndent = $conditional->getStyle()->getAlignment()->getIndent();
+ if ($conditional->getStyle()->getAlignment()->getShrinkToFit() === true) {
+ $blockIndent |= 1 << 4;
+ } else {
+ $blockIndent |= 0 << 4;
+ }
+ $blockIndent |= 0 << 6;
+
+ // Relative indentation
+ $blockIndentRelative = 255;
+
+ $dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000);
+ }
+ if ($bFormatBorder === 1) {
+ $blockLineStyle = Style\CellBorder::style($conditional->getStyle()->getBorders()->getLeft());
+ $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getRight()) << 4;
+ $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8;
+ $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12;
+
+ // TODO writeCFRule() => $blockLineStyle => Index Color for left line
+ // TODO writeCFRule() => $blockLineStyle => Index Color for right line
+ // TODO writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off
+ // TODO writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off
+ $blockColor = 0;
+ // TODO writeCFRule() => $blockColor => Index Color for top line
+ // TODO writeCFRule() => $blockColor => Index Color for bottom line
+ // TODO writeCFRule() => $blockColor => Index Color for diagonal line
+ $blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21;
+ $dataBlockBorder = pack('vv', $blockLineStyle, $blockColor);
+ }
+ if ($bFormatFill === 1) {
+ // Fill Pattern Style
+ $blockFillPatternStyle = Style\CellFill::style($conditional->getStyle()->getFill());
+ // Background Color
+ $colorIdxBg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getStartColor(), 0x41);
+ // Foreground Color
+ $colorIdxFg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getEndColor(), 0x40);
+
+ $dataBlockFill = pack('v', $blockFillPatternStyle);
+ $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7));
+ }
+
+ $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000);
+ if ($bFormatFont === 1) { // Block Formatting : OK
+ $data .= $dataBlockFont;
+ }
+ if ($bFormatAlign === 1) {
+ $data .= $dataBlockAlign;
+ }
+ if ($bFormatBorder === 1) {
+ $data .= $dataBlockBorder;
+ }
+ if ($bFormatFill === 1) { // Block Formatting : OK
+ $data .= $dataBlockFill;
+ }
+ if ($bFormatProt == 1) {
+ $data .= $this->getDataBlockProtection($conditional);
+ }
+ if ($operand1 !== null) {
+ $data .= $operand1;
+ }
+ if ($operand2 !== null) {
+ $data .= $operand2;
+ }
+ $header = pack('vv', $record, strlen($data));
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write CFHeader record.
+ */
+ private function writeCFHeader(): void
+ {
+ $record = 0x01B0; // Record identifier
+ $length = 0x0016; // Bytes to follow
+
+ $numColumnMin = null;
+ $numColumnMax = null;
+ $numRowMin = null;
+ $numRowMax = null;
+ $arrConditional = [];
+ foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+ foreach ($conditionalStyles as $conditional) {
+ if (
+ $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION ||
+ $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+ ) {
+ if (!in_array($conditional->getHashCode(), $arrConditional)) {
+ $arrConditional[] = $conditional->getHashCode();
+ }
+ // Cells
+ $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate);
+ if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) {
+ $numColumnMin = $rangeCoordinates[0][0];
+ }
+ if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) {
+ $numColumnMax = $rangeCoordinates[1][0];
+ }
+ if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) {
+ $numRowMin = (int) $rangeCoordinates[0][1];
+ }
+ if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) {
+ $numRowMax = (int) $rangeCoordinates[1][1];
+ }
+ }
+ }
+ }
+ $needRedraw = 1;
+ $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1);
+
+ $header = pack('vv', $record, $length);
+ $data = pack('vv', count($arrConditional), $needRedraw);
+ $data .= $cellRange;
+ $data .= pack('v', 0x0001);
+ $data .= $cellRange;
+ $this->append($header . $data);
+ }
+
+ private function getDataBlockProtection(Conditional $conditional): int
+ {
+ $dataBlockProtection = 0;
+ if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) {
+ $dataBlockProtection = 1;
+ }
+ if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) {
+ $dataBlockProtection = 1 << 1;
+ }
+
+ return $dataBlockProtection;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php
new file mode 100644
index 0000000..b2dbc67
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php
@@ -0,0 +1,418 @@
+
+// *
+// * The majority of this is _NOT_ my code. I simply ported it from the
+// * PERL Spreadsheet::WriteExcel module.
+// *
+// * The author of the Spreadsheet::WriteExcel module is John McNamara
+// *
+// *
+// * I _DO_ maintain this code, and John McNamara has nothing to do with the
+// * porting of this code to PHP. Any questions directly related to this
+// * class library should be directed to me.
+// *
+// * License Information:
+// *
+// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
+// * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// * This library is free software; you can redistribute it and/or
+// * modify it under the terms of the GNU Lesser General Public
+// * License as published by the Free Software Foundation; either
+// * version 2.1 of the License, or (at your option) any later version.
+// *
+// * This library is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// * Lesser General Public License for more details.
+// *
+// * You should have received a copy of the GNU Lesser General Public
+// * License along with this library; if not, write to the Free Software
+// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+// */
+class Xf
+{
+ /**
+ * Style XF or a cell XF ?
+ *
+ * @var bool
+ */
+ private $isStyleXf;
+
+ /**
+ * Index to the FONT record. Index 4 does not exist.
+ *
+ * @var int
+ */
+ private $fontIndex;
+
+ /**
+ * An index (2 bytes) to a FORMAT record (number format).
+ *
+ * @var int
+ */
+ private $numberFormatIndex;
+
+ /**
+ * 1 bit, apparently not used.
+ *
+ * @var int
+ */
+ private $textJustLast;
+
+ /**
+ * The cell's foreground color.
+ *
+ * @var int
+ */
+ private $foregroundColor;
+
+ /**
+ * The cell's background color.
+ *
+ * @var int
+ */
+ private $backgroundColor;
+
+ /**
+ * Color of the bottom border of the cell.
+ *
+ * @var int
+ */
+ private $bottomBorderColor;
+
+ /**
+ * Color of the top border of the cell.
+ *
+ * @var int
+ */
+ private $topBorderColor;
+
+ /**
+ * Color of the left border of the cell.
+ *
+ * @var int
+ */
+ private $leftBorderColor;
+
+ /**
+ * Color of the right border of the cell.
+ *
+ * @var int
+ */
+ private $rightBorderColor;
+
+ /**
+ * @var int
+ */
+ private $diag;
+
+ /**
+ * @var int
+ */
+ private $diagColor;
+
+ /**
+ * @var Style
+ */
+ private $style;
+
+ /**
+ * Constructor.
+ *
+ * @param Style $style The XF format
+ */
+ public function __construct(Style $style)
+ {
+ $this->isStyleXf = false;
+ $this->fontIndex = 0;
+
+ $this->numberFormatIndex = 0;
+
+ $this->textJustLast = 0;
+
+ $this->foregroundColor = 0x40;
+ $this->backgroundColor = 0x41;
+
+ $this->diag = 0;
+
+ $this->bottomBorderColor = 0x40;
+ $this->topBorderColor = 0x40;
+ $this->leftBorderColor = 0x40;
+ $this->rightBorderColor = 0x40;
+ $this->diagColor = 0x40;
+ $this->style = $style;
+ }
+
+ /**
+ * Generate an Excel BIFF XF record (style or cell).
+ *
+ * @return string The XF record
+ */
+ public function writeXf()
+ {
+ // Set the type of the XF record and some of the attributes.
+ if ($this->isStyleXf) {
+ $style = 0xFFF5;
+ } else {
+ $style = self::mapLocked($this->style->getProtection()->getLocked());
+ $style |= self::mapHidden($this->style->getProtection()->getHidden()) << 1;
+ }
+
+ // Flags to indicate if attributes have been set.
+ $atr_num = ($this->numberFormatIndex != 0) ? 1 : 0;
+ $atr_fnt = ($this->fontIndex != 0) ? 1 : 0;
+ $atr_alc = ((int) $this->style->getAlignment()->getWrapText()) ? 1 : 0;
+ $atr_bdr = (CellBorder::style($this->style->getBorders()->getBottom()) ||
+ CellBorder::style($this->style->getBorders()->getTop()) ||
+ CellBorder::style($this->style->getBorders()->getLeft()) ||
+ CellBorder::style($this->style->getBorders()->getRight())) ? 1 : 0;
+ $atr_pat = ($this->foregroundColor != 0x40) ? 1 : 0;
+ $atr_pat = ($this->backgroundColor != 0x41) ? 1 : $atr_pat;
+ $atr_pat = CellFill::style($this->style->getFill()) ? 1 : $atr_pat;
+ $atr_prot = self::mapLocked($this->style->getProtection()->getLocked())
+ | self::mapHidden($this->style->getProtection()->getHidden());
+
+ // Zero the default border colour if the border has not been set.
+ if (CellBorder::style($this->style->getBorders()->getBottom()) == 0) {
+ $this->bottomBorderColor = 0;
+ }
+ if (CellBorder::style($this->style->getBorders()->getTop()) == 0) {
+ $this->topBorderColor = 0;
+ }
+ if (CellBorder::style($this->style->getBorders()->getRight()) == 0) {
+ $this->rightBorderColor = 0;
+ }
+ if (CellBorder::style($this->style->getBorders()->getLeft()) == 0) {
+ $this->leftBorderColor = 0;
+ }
+ if (CellBorder::style($this->style->getBorders()->getDiagonal()) == 0) {
+ $this->diagColor = 0;
+ }
+
+ $record = 0x00E0; // Record identifier
+ $length = 0x0014; // Number of bytes to follow
+
+ $ifnt = $this->fontIndex; // Index to FONT record
+ $ifmt = $this->numberFormatIndex; // Index to FORMAT record
+
+ // Alignment
+ $align = CellAlignment::horizontal($this->style->getAlignment());
+ $align |= CellAlignment::wrap($this->style->getAlignment()) << 3;
+ $align |= CellAlignment::vertical($this->style->getAlignment()) << 4;
+ $align |= $this->textJustLast << 7;
+
+ $used_attrib = $atr_num << 2;
+ $used_attrib |= $atr_fnt << 3;
+ $used_attrib |= $atr_alc << 4;
+ $used_attrib |= $atr_bdr << 5;
+ $used_attrib |= $atr_pat << 6;
+ $used_attrib |= $atr_prot << 7;
+
+ $icv = $this->foregroundColor; // fg and bg pattern colors
+ $icv |= $this->backgroundColor << 7;
+
+ $border1 = CellBorder::style($this->style->getBorders()->getLeft()); // Border line style and color
+ $border1 |= CellBorder::style($this->style->getBorders()->getRight()) << 4;
+ $border1 |= CellBorder::style($this->style->getBorders()->getTop()) << 8;
+ $border1 |= CellBorder::style($this->style->getBorders()->getBottom()) << 12;
+ $border1 |= $this->leftBorderColor << 16;
+ $border1 |= $this->rightBorderColor << 23;
+
+ $diagonalDirection = $this->style->getBorders()->getDiagonalDirection();
+ $diag_tl_to_rb = $diagonalDirection == Borders::DIAGONAL_BOTH
+ || $diagonalDirection == Borders::DIAGONAL_DOWN;
+ $diag_tr_to_lb = $diagonalDirection == Borders::DIAGONAL_BOTH
+ || $diagonalDirection == Borders::DIAGONAL_UP;
+ $border1 |= $diag_tl_to_rb << 30;
+ $border1 |= $diag_tr_to_lb << 31;
+
+ $border2 = $this->topBorderColor; // Border color
+ $border2 |= $this->bottomBorderColor << 7;
+ $border2 |= $this->diagColor << 14;
+ $border2 |= CellBorder::style($this->style->getBorders()->getDiagonal()) << 21;
+ $border2 |= CellFill::style($this->style->getFill()) << 26;
+
+ $header = pack('vv', $record, $length);
+
+ //BIFF8 options: identation, shrinkToFit and text direction
+ $biff8_options = $this->style->getAlignment()->getIndent();
+ $biff8_options |= (int) $this->style->getAlignment()->getShrinkToFit() << 4;
+
+ $data = pack('vvvC', $ifnt, $ifmt, $style, $align);
+ $data .= pack('CCC', self::mapTextRotation($this->style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib);
+ $data .= pack('VVv', $border1, $border2, $icv);
+
+ return $header . $data;
+ }
+
+ /**
+ * Is this a style XF ?
+ *
+ * @param bool $value
+ */
+ public function setIsStyleXf($value): void
+ {
+ $this->isStyleXf = $value;
+ }
+
+ /**
+ * Sets the cell's bottom border color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setBottomColor($colorIndex): void
+ {
+ $this->bottomBorderColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's top border color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setTopColor($colorIndex): void
+ {
+ $this->topBorderColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's left border color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setLeftColor($colorIndex): void
+ {
+ $this->leftBorderColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's right border color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setRightColor($colorIndex): void
+ {
+ $this->rightBorderColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's diagonal border color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setDiagColor($colorIndex): void
+ {
+ $this->diagColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's foreground color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setFgColor($colorIndex): void
+ {
+ $this->foregroundColor = $colorIndex;
+ }
+
+ /**
+ * Sets the cell's background color.
+ *
+ * @param int $colorIndex Color index
+ */
+ public function setBgColor($colorIndex): void
+ {
+ $this->backgroundColor = $colorIndex;
+ }
+
+ /**
+ * Sets the index to the number format record
+ * It can be date, time, currency, etc...
+ *
+ * @param int $numberFormatIndex Index to format record
+ */
+ public function setNumberFormatIndex($numberFormatIndex): void
+ {
+ $this->numberFormatIndex = $numberFormatIndex;
+ }
+
+ /**
+ * Set the font index.
+ *
+ * @param int $value Font index, note that value 4 does not exist
+ */
+ public function setFontIndex($value): void
+ {
+ $this->fontIndex = $value;
+ }
+
+ /**
+ * Map to BIFF8 codes for text rotation angle.
+ *
+ * @param int $textRotation
+ *
+ * @return int
+ */
+ private static function mapTextRotation($textRotation)
+ {
+ if ($textRotation >= 0) {
+ return $textRotation;
+ }
+ if ($textRotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
+ return Alignment::TEXTROTATION_STACK_EXCEL;
+ }
+
+ return 90 - $textRotation;
+ }
+
+ private const LOCK_ARRAY = [
+ Protection::PROTECTION_INHERIT => 1,
+ Protection::PROTECTION_PROTECTED => 1,
+ Protection::PROTECTION_UNPROTECTED => 0,
+ ];
+
+ /**
+ * Map locked values.
+ *
+ * @param string $locked
+ *
+ * @return int
+ */
+ private static function mapLocked($locked)
+ {
+ return array_key_exists($locked, self::LOCK_ARRAY) ? self::LOCK_ARRAY[$locked] : 1;
+ }
+
+ private const HIDDEN_ARRAY = [
+ Protection::PROTECTION_INHERIT => 0,
+ Protection::PROTECTION_PROTECTED => 1,
+ Protection::PROTECTION_UNPROTECTED => 0,
+ ];
+
+ /**
+ * Map hidden.
+ *
+ * @param string $hidden
+ *
+ * @return int
+ */
+ private static function mapHidden($hidden)
+ {
+ return array_key_exists($hidden, self::HIDDEN_ARRAY) ? self::HIDDEN_ARRAY[$hidden] : 0;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php
new file mode 100644
index 0000000..27ceb55
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php
@@ -0,0 +1,668 @@
+
+ */
+ private $stylesConditionalHashTable;
+
+ /**
+ * Private unique Style HashTable.
+ *
+ * @var HashTable<\PhpOffice\PhpSpreadsheet\Style\Style>
+ */
+ private $styleHashTable;
+
+ /**
+ * Private unique Fill HashTable.
+ *
+ * @var HashTable
+ */
+ private $fillHashTable;
+
+ /**
+ * Private unique \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
+ *
+ * @var HashTable
+ */
+ private $fontHashTable;
+
+ /**
+ * Private unique Borders HashTable.
+ *
+ * @var HashTable
+ */
+ private $bordersHashTable;
+
+ /**
+ * Private unique NumberFormat HashTable.
+ *
+ * @var HashTable
+ */
+ private $numFmtHashTable;
+
+ /**
+ * Private unique \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
+ *
+ * @var HashTable
+ */
+ private $drawingHashTable;
+
+ /**
+ * Private handle for zip stream.
+ *
+ * @var ZipStream
+ */
+ private $zip;
+
+ /**
+ * @var Chart
+ */
+ private $writerPartChart;
+
+ /**
+ * @var Comments
+ */
+ private $writerPartComments;
+
+ /**
+ * @var ContentTypes
+ */
+ private $writerPartContentTypes;
+
+ /**
+ * @var DocProps
+ */
+ private $writerPartDocProps;
+
+ /**
+ * @var Drawing
+ */
+ private $writerPartDrawing;
+
+ /**
+ * @var Rels
+ */
+ private $writerPartRels;
+
+ /**
+ * @var RelsRibbon
+ */
+ private $writerPartRelsRibbon;
+
+ /**
+ * @var RelsVBA
+ */
+ private $writerPartRelsVBA;
+
+ /**
+ * @var StringTable
+ */
+ private $writerPartStringTable;
+
+ /**
+ * @var Style
+ */
+ private $writerPartStyle;
+
+ /**
+ * @var Theme
+ */
+ private $writerPartTheme;
+
+ /**
+ * @var Workbook
+ */
+ private $writerPartWorkbook;
+
+ /**
+ * @var Worksheet
+ */
+ private $writerPartWorksheet;
+
+ /**
+ * Create a new Xlsx Writer.
+ */
+ public function __construct(Spreadsheet $spreadsheet)
+ {
+ // Assign PhpSpreadsheet
+ $this->setSpreadsheet($spreadsheet);
+
+ $this->writerPartChart = new Chart($this);
+ $this->writerPartComments = new Comments($this);
+ $this->writerPartContentTypes = new ContentTypes($this);
+ $this->writerPartDocProps = new DocProps($this);
+ $this->writerPartDrawing = new Drawing($this);
+ $this->writerPartRels = new Rels($this);
+ $this->writerPartRelsRibbon = new RelsRibbon($this);
+ $this->writerPartRelsVBA = new RelsVBA($this);
+ $this->writerPartStringTable = new StringTable($this);
+ $this->writerPartStyle = new Style($this);
+ $this->writerPartTheme = new Theme($this);
+ $this->writerPartWorkbook = new Workbook($this);
+ $this->writerPartWorksheet = new Worksheet($this);
+
+ // Set HashTable variables
+ // @phpstan-ignore-next-line
+ $this->bordersHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->drawingHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->fillHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->fontHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->numFmtHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->styleHashTable = new HashTable();
+ // @phpstan-ignore-next-line
+ $this->stylesConditionalHashTable = new HashTable();
+ }
+
+ public function getWriterPartChart(): Chart
+ {
+ return $this->writerPartChart;
+ }
+
+ public function getWriterPartComments(): Comments
+ {
+ return $this->writerPartComments;
+ }
+
+ public function getWriterPartContentTypes(): ContentTypes
+ {
+ return $this->writerPartContentTypes;
+ }
+
+ public function getWriterPartDocProps(): DocProps
+ {
+ return $this->writerPartDocProps;
+ }
+
+ public function getWriterPartDrawing(): Drawing
+ {
+ return $this->writerPartDrawing;
+ }
+
+ public function getWriterPartRels(): Rels
+ {
+ return $this->writerPartRels;
+ }
+
+ public function getWriterPartRelsRibbon(): RelsRibbon
+ {
+ return $this->writerPartRelsRibbon;
+ }
+
+ public function getWriterPartRelsVBA(): RelsVBA
+ {
+ return $this->writerPartRelsVBA;
+ }
+
+ public function getWriterPartStringTable(): StringTable
+ {
+ return $this->writerPartStringTable;
+ }
+
+ public function getWriterPartStyle(): Style
+ {
+ return $this->writerPartStyle;
+ }
+
+ public function getWriterPartTheme(): Theme
+ {
+ return $this->writerPartTheme;
+ }
+
+ public function getWriterPartWorkbook(): Workbook
+ {
+ return $this->writerPartWorkbook;
+ }
+
+ public function getWriterPartWorksheet(): Worksheet
+ {
+ return $this->writerPartWorksheet;
+ }
+
+ /**
+ * Save PhpSpreadsheet to file.
+ *
+ * @param resource|string $pFilename
+ */
+ public function save($pFilename): void
+ {
+ // garbage collect
+ $this->pathNames = [];
+ $this->spreadSheet->garbageCollect();
+
+ $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog();
+ Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false);
+ $saveDateReturnType = Functions::getReturnDateType();
+ Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
+
+ // Create string lookup table
+ $this->stringTable = [];
+ for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+ $this->stringTable = $this->getWriterPartStringTable()->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable);
+ }
+
+ // Create styles dictionaries
+ $this->styleHashTable->addFromSource($this->getWriterPartStyle()->allStyles($this->spreadSheet));
+ $this->stylesConditionalHashTable->addFromSource($this->getWriterPartStyle()->allConditionalStyles($this->spreadSheet));
+ $this->fillHashTable->addFromSource($this->getWriterPartStyle()->allFills($this->spreadSheet));
+ $this->fontHashTable->addFromSource($this->getWriterPartStyle()->allFonts($this->spreadSheet));
+ $this->bordersHashTable->addFromSource($this->getWriterPartStyle()->allBorders($this->spreadSheet));
+ $this->numFmtHashTable->addFromSource($this->getWriterPartStyle()->allNumberFormats($this->spreadSheet));
+
+ // Create drawing dictionary
+ $this->drawingHashTable->addFromSource($this->getWriterPartDrawing()->allDrawings($this->spreadSheet));
+
+ $zipContent = [];
+ // Add [Content_Types].xml to ZIP file
+ $zipContent['[Content_Types].xml'] = $this->getWriterPartContentTypes()->writeContentTypes($this->spreadSheet, $this->includeCharts);
+
+ //if hasMacros, add the vbaProject.bin file, Certificate file(if exists)
+ if ($this->spreadSheet->hasMacros()) {
+ $macrosCode = $this->spreadSheet->getMacrosCode();
+ if ($macrosCode !== null) {
+ // we have the code ?
+ $zipContent['xl/vbaProject.bin'] = $macrosCode; //allways in 'xl', allways named vbaProject.bin
+ if ($this->spreadSheet->hasMacrosCertificate()) {
+ //signed macros ?
+ // Yes : add the certificate file and the related rels file
+ $zipContent['xl/vbaProjectSignature.bin'] = $this->spreadSheet->getMacrosCertificate();
+ $zipContent['xl/_rels/vbaProject.bin.rels'] = $this->getWriterPartRelsVBA()->writeVBARelationships($this->spreadSheet);
+ }
+ }
+ }
+ //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels)
+ if ($this->spreadSheet->hasRibbon()) {
+ $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target');
+ $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data');
+ if ($this->spreadSheet->hasRibbonBinObjects()) {
+ $tmpRootPath = dirname($tmpRibbonTarget) . '/';
+ $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write
+ foreach ($ribbonBinObjects as $aPath => $aContent) {
+ $zipContent[$tmpRootPath . $aPath] = $aContent;
+ }
+ //the rels for files
+ $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet);
+ }
+ }
+
+ // Add relationships to ZIP file
+ $zipContent['_rels/.rels'] = $this->getWriterPartRels()->writeRelationships($this->spreadSheet);
+ $zipContent['xl/_rels/workbook.xml.rels'] = $this->getWriterPartRels()->writeWorkbookRelationships($this->spreadSheet);
+
+ // Add document properties to ZIP file
+ $zipContent['docProps/app.xml'] = $this->getWriterPartDocProps()->writeDocPropsApp($this->spreadSheet);
+ $zipContent['docProps/core.xml'] = $this->getWriterPartDocProps()->writeDocPropsCore($this->spreadSheet);
+ $customPropertiesPart = $this->getWriterPartDocProps()->writeDocPropsCustom($this->spreadSheet);
+ if ($customPropertiesPart !== null) {
+ $zipContent['docProps/custom.xml'] = $customPropertiesPart;
+ }
+
+ // Add theme to ZIP file
+ $zipContent['xl/theme/theme1.xml'] = $this->getWriterPartTheme()->writeTheme($this->spreadSheet);
+
+ // Add string table to ZIP file
+ $zipContent['xl/sharedStrings.xml'] = $this->getWriterPartStringTable()->writeStringTable($this->stringTable);
+
+ // Add styles to ZIP file
+ $zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);
+
+ // Add workbook to ZIP file
+ $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
+
+ $chartCount = 0;
+ // Add worksheets
+ for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+ $zipContent['xl/worksheets/sheet' . ($i + 1) . '.xml'] = $this->getWriterPartWorksheet()->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts);
+ if ($this->includeCharts) {
+ $charts = $this->spreadSheet->getSheet($i)->getChartCollection();
+ if (count($charts) > 0) {
+ foreach ($charts as $chart) {
+ $zipContent['xl/charts/chart' . ($chartCount + 1) . '.xml'] = $this->getWriterPartChart()->writeChart($chart, $this->preCalculateFormulas);
+ ++$chartCount;
+ }
+ }
+ }
+ }
+
+ $chartRef1 = 0;
+ // Add worksheet relationships (drawings, ...)
+ for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+ // Add relationships
+ $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts);
+
+ // Add unparsedLoadedData
+ $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
+ $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData();
+ if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) {
+ foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) {
+ $zipContent[$ctrlProp['filePath']] = $ctrlProp['content'];
+ }
+ }
+ if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) {
+ foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) {
+ $zipContent[$ctrlProp['filePath']] = $ctrlProp['content'];
+ }
+ }
+
+ $drawings = $this->spreadSheet->getSheet($i)->getDrawingCollection();
+ $drawingCount = count($drawings);
+ if ($this->includeCharts) {
+ $chartCount = $this->spreadSheet->getSheet($i)->getChartCount();
+ }
+
+ // Add drawing and image relationship parts
+ if (($drawingCount > 0) || ($chartCount > 0)) {
+ // Drawing relationships
+ $zipContent['xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts);
+
+ // Drawings
+ $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts);
+ } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) {
+ // Drawings
+ $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts);
+ }
+
+ // Add unparsed drawings
+ if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'])) {
+ foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'] as $relId => $drawingXml) {
+ $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']);
+ if ($drawingFile !== false) {
+ $drawingFile = ltrim($drawingFile, '.');
+ $zipContent['xl' . $drawingFile] = $drawingXml;
+ }
+ }
+ }
+
+ // Add comment relationship parts
+ if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
+ // VML Comments
+ $zipContent['xl/drawings/vmlDrawing' . ($i + 1) . '.vml'] = $this->getWriterPartComments()->writeVMLComments($this->spreadSheet->getSheet($i));
+
+ // Comments
+ $zipContent['xl/comments' . ($i + 1) . '.xml'] = $this->getWriterPartComments()->writeComments($this->spreadSheet->getSheet($i));
+ }
+
+ // Add unparsed relationship parts
+ if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) {
+ foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) {
+ $zipContent[$vmlDrawing['filePath']] = $vmlDrawing['content'];
+ }
+ }
+
+ // Add header/footer relationship parts
+ if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
+ // VML Drawings
+ $zipContent['xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml'] = $this->getWriterPartDrawing()->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i));
+
+ // VML Drawing relationships
+ $zipContent['xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels'] = $this->getWriterPartRels()->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i));
+
+ // Media
+ foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
+ $zipContent['xl/media/' . $image->getIndexedFilename()] = file_get_contents($image->getPath());
+ }
+ }
+ }
+
+ // Add media
+ for ($i = 0; $i < $this->getDrawingHashTable()->count(); ++$i) {
+ if ($this->getDrawingHashTable()->getByIndex($i) instanceof WorksheetDrawing) {
+ $imageContents = null;
+ $imagePath = $this->getDrawingHashTable()->getByIndex($i)->getPath();
+ if (strpos($imagePath, 'zip://') !== false) {
+ $imagePath = substr($imagePath, 6);
+ $imagePathSplitted = explode('#', $imagePath);
+
+ $imageZip = new ZipArchive();
+ $imageZip->open($imagePathSplitted[0]);
+ $imageContents = $imageZip->getFromName($imagePathSplitted[1]);
+ $imageZip->close();
+ unset($imageZip);
+ } else {
+ $imageContents = file_get_contents($imagePath);
+ }
+
+ $zipContent['xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename())] = $imageContents;
+ } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) {
+ ob_start();
+ call_user_func(
+ $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction(),
+ $this->getDrawingHashTable()->getByIndex($i)->getImageResource()
+ );
+ $imageContents = ob_get_contents();
+ ob_end_clean();
+
+ $zipContent['xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename())] = $imageContents;
+ }
+ }
+
+ Functions::setReturnDateType($saveDateReturnType);
+ Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+
+ $this->openFileHandle($pFilename);
+
+ $options = new Archive();
+ $options->setEnableZip64(false);
+ $options->setOutputStream($this->fileHandle);
+
+ $this->zip = new ZipStream(null, $options);
+
+ $this->addZipFiles($zipContent);
+
+ // Close file
+ try {
+ $this->zip->finish();
+ } catch (OverflowException $e) {
+ throw new WriterException('Could not close resource.');
+ }
+
+ $this->maybeCloseFileHandle();
+ }
+
+ /**
+ * Get Spreadsheet object.
+ *
+ * @return Spreadsheet
+ */
+ public function getSpreadsheet()
+ {
+ return $this->spreadSheet;
+ }
+
+ /**
+ * Set Spreadsheet object.
+ *
+ * @param Spreadsheet $spreadsheet PhpSpreadsheet object
+ *
+ * @return $this
+ */
+ public function setSpreadsheet(Spreadsheet $spreadsheet)
+ {
+ $this->spreadSheet = $spreadsheet;
+
+ return $this;
+ }
+
+ /**
+ * Get string table.
+ *
+ * @return string[]
+ */
+ public function getStringTable()
+ {
+ return $this->stringTable;
+ }
+
+ /**
+ * Get Style HashTable.
+ *
+ * @return HashTable<\PhpOffice\PhpSpreadsheet\Style\Style>
+ */
+ public function getStyleHashTable()
+ {
+ return $this->styleHashTable;
+ }
+
+ /**
+ * Get Conditional HashTable.
+ *
+ * @return HashTable
+ */
+ public function getStylesConditionalHashTable()
+ {
+ return $this->stylesConditionalHashTable;
+ }
+
+ /**
+ * Get Fill HashTable.
+ *
+ * @return HashTable
+ */
+ public function getFillHashTable()
+ {
+ return $this->fillHashTable;
+ }
+
+ /**
+ * Get \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
+ *
+ * @return HashTable
+ */
+ public function getFontHashTable()
+ {
+ return $this->fontHashTable;
+ }
+
+ /**
+ * Get Borders HashTable.
+ *
+ * @return HashTable
+ */
+ public function getBordersHashTable()
+ {
+ return $this->bordersHashTable;
+ }
+
+ /**
+ * Get NumberFormat HashTable.
+ *
+ * @return HashTable
+ */
+ public function getNumFmtHashTable()
+ {
+ return $this->numFmtHashTable;
+ }
+
+ /**
+ * Get \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
+ *
+ * @return HashTable
+ */
+ public function getDrawingHashTable()
+ {
+ return $this->drawingHashTable;
+ }
+
+ /**
+ * Get Office2003 compatibility.
+ *
+ * @return bool
+ */
+ public function getOffice2003Compatibility()
+ {
+ return $this->office2003compatibility;
+ }
+
+ /**
+ * Set Office2003 compatibility.
+ *
+ * @param bool $pValue Office2003 compatibility?
+ *
+ * @return $this
+ */
+ public function setOffice2003Compatibility($pValue)
+ {
+ $this->office2003compatibility = $pValue;
+
+ return $this;
+ }
+
+ private $pathNames = [];
+
+ private function addZipFile(string $path, string $content): void
+ {
+ if (!in_array($path, $this->pathNames)) {
+ $this->pathNames[] = $path;
+ $this->zip->addFile($path, $content);
+ }
+ }
+
+ private function addZipFiles(array $zipContent): void
+ {
+ foreach ($zipContent as $path => $content) {
+ $this->addZipFile($path, $content);
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
new file mode 100644
index 0000000..56b890b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
@@ -0,0 +1,1519 @@
+calculateCellValues = $calculateCellValues;
+
+ // 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);
+ }
+ // Ensure that data series values are up-to-date before we save
+ if ($this->calculateCellValues) {
+ $pChart->refresh();
+ }
+
+ // XML header
+ $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+ // c:chartSpace
+ $objWriter->startElement('c:chartSpace');
+ $objWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart');
+ $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
+ $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
+
+ $objWriter->startElement('c:date1904');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ $objWriter->startElement('c:lang');
+ $objWriter->writeAttribute('val', 'en-GB');
+ $objWriter->endElement();
+ $objWriter->startElement('c:roundedCorners');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $this->writeAlternateContent($objWriter);
+
+ $objWriter->startElement('c:chart');
+
+ $this->writeTitle($objWriter, $pChart->getTitle());
+
+ $objWriter->startElement('c:autoTitleDeleted');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $this->writePlotArea($objWriter, $pChart->getWorksheet(), $pChart->getPlotArea(), $pChart->getXAxisLabel(), $pChart->getYAxisLabel(), $pChart->getChartAxisX(), $pChart->getChartAxisY(), $pChart->getMajorGridlines(), $pChart->getMinorGridlines());
+
+ $this->writeLegend($objWriter, $pChart->getLegend());
+
+ $objWriter->startElement('c:plotVisOnly');
+ $objWriter->writeAttribute('val', (int) $pChart->getPlotVisibleOnly());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:dispBlanksAs');
+ $objWriter->writeAttribute('val', $pChart->getDisplayBlanksAs());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showDLblsOverMax');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $this->writePrintSettings($objWriter);
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write Chart Title.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Title $title
+ */
+ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void
+ {
+ if ($title === null) {
+ return;
+ }
+
+ $objWriter->startElement('c:title');
+ $objWriter->startElement('c:tx');
+ $objWriter->startElement('c:rich');
+
+ $objWriter->startElement('a:bodyPr');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:lstStyle');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:p');
+
+ $caption = $title->getCaption();
+ if ((is_array($caption)) && (count($caption) > 0)) {
+ $caption = $caption[0];
+ }
+ $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $this->writeLayout($objWriter, $title->getLayout());
+
+ $objWriter->startElement('c:overlay');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Chart Legend.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Legend $legend
+ */
+ private function writeLegend(XMLWriter $objWriter, ?Legend $legend = null): void
+ {
+ if ($legend === null) {
+ return;
+ }
+
+ $objWriter->startElement('c:legend');
+
+ $objWriter->startElement('c:legendPos');
+ $objWriter->writeAttribute('val', $legend->getPosition());
+ $objWriter->endElement();
+
+ $this->writeLayout($objWriter, $legend->getLayout());
+
+ $objWriter->startElement('c:overlay');
+ $objWriter->writeAttribute('val', ($legend->getOverlay()) ? '1' : '0');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:txPr');
+ $objWriter->startElement('a:bodyPr');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:lstStyle');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:p');
+ $objWriter->startElement('a:pPr');
+ $objWriter->writeAttribute('rtl', 0);
+
+ $objWriter->startElement('a:defRPr');
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:endParaRPr');
+ $objWriter->writeAttribute('lang', 'en-US');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Chart Plot Area.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Title $xAxisLabel
+ * @param Title $yAxisLabel
+ * @param Axis $xAxis
+ * @param Axis $yAxis
+ */
+ private function writePlotArea(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pSheet, PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void
+ {
+ if ($plotArea === null) {
+ return;
+ }
+
+ $id1 = $id2 = 0;
+ $this->seriesIndex = 0;
+ $objWriter->startElement('c:plotArea');
+
+ $layout = $plotArea->getLayout();
+
+ $this->writeLayout($objWriter, $layout);
+
+ $chartTypes = self::getChartType($plotArea);
+ $catIsMultiLevelSeries = $valIsMultiLevelSeries = false;
+ $plotGroupingType = '';
+ $chartType = null;
+ foreach ($chartTypes as $chartType) {
+ $objWriter->startElement('c:' . $chartType);
+
+ $groupCount = $plotArea->getPlotGroupCount();
+ $plotGroup = null;
+ for ($i = 0; $i < $groupCount; ++$i) {
+ $plotGroup = $plotArea->getPlotGroupByIndex($i);
+ $groupType = $plotGroup->getPlotType();
+ if ($groupType == $chartType) {
+ $plotStyle = $plotGroup->getPlotStyle();
+ if ($groupType === DataSeries::TYPE_RADARCHART) {
+ $objWriter->startElement('c:radarStyle');
+ $objWriter->writeAttribute('val', $plotStyle);
+ $objWriter->endElement();
+ } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) {
+ $objWriter->startElement('c:scatterStyle');
+ $objWriter->writeAttribute('val', $plotStyle);
+ $objWriter->endElement();
+ }
+
+ $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType);
+ }
+ }
+
+ $this->writeDataLabels($objWriter, $layout);
+
+ if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) {
+ // Line only, Line3D can't be smoothed
+ $objWriter->startElement('c:smooth');
+ $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine());
+ $objWriter->endElement();
+ } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) {
+ $objWriter->startElement('c:gapWidth');
+ $objWriter->writeAttribute('val', 150);
+ $objWriter->endElement();
+
+ if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') {
+ $objWriter->startElement('c:overlap');
+ $objWriter->writeAttribute('val', 100);
+ $objWriter->endElement();
+ }
+ } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) {
+ $objWriter->startElement('c:bubbleScale');
+ $objWriter->writeAttribute('val', 25);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showNegBubbles');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ } elseif ($chartType === DataSeries::TYPE_STOCKCHART) {
+ $objWriter->startElement('c:hiLowLines');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:upDownBars');
+
+ $objWriter->startElement('c:gapWidth');
+ $objWriter->writeAttribute('val', 300);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:upBars');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:downBars');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ // Generate 2 unique numbers to use for axId values
+ $id1 = '75091328';
+ $id2 = '75089408';
+
+ if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
+ $objWriter->startElement('c:axId');
+ $objWriter->writeAttribute('val', $id1);
+ $objWriter->endElement();
+ $objWriter->startElement('c:axId');
+ $objWriter->writeAttribute('val', $id2);
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('c:firstSliceAng');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ if ($chartType === DataSeries::TYPE_DONUTCHART) {
+ $objWriter->startElement('c:holeSize');
+ $objWriter->writeAttribute('val', 50);
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
+ if ($chartType === DataSeries::TYPE_BUBBLECHART) {
+ $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines);
+ } else {
+ $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis);
+ }
+
+ $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines);
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Data Labels.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param \PhpOffice\PhpSpreadsheet\Chart\Layout $chartLayout Chart layout
+ */
+ private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void
+ {
+ $objWriter->startElement('c:dLbls');
+
+ $objWriter->startElement('c:showLegendKey');
+ $showLegendKey = (empty($chartLayout)) ? 0 : $chartLayout->getShowLegendKey();
+ $objWriter->writeAttribute('val', ((empty($showLegendKey)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showVal');
+ $showVal = (empty($chartLayout)) ? 0 : $chartLayout->getShowVal();
+ $objWriter->writeAttribute('val', ((empty($showVal)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showCatName');
+ $showCatName = (empty($chartLayout)) ? 0 : $chartLayout->getShowCatName();
+ $objWriter->writeAttribute('val', ((empty($showCatName)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showSerName');
+ $showSerName = (empty($chartLayout)) ? 0 : $chartLayout->getShowSerName();
+ $objWriter->writeAttribute('val', ((empty($showSerName)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showPercent');
+ $showPercent = (empty($chartLayout)) ? 0 : $chartLayout->getShowPercent();
+ $objWriter->writeAttribute('val', ((empty($showPercent)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showBubbleSize');
+ $showBubbleSize = (empty($chartLayout)) ? 0 : $chartLayout->getShowBubbleSize();
+ $objWriter->writeAttribute('val', ((empty($showBubbleSize)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:showLeaderLines');
+ $showLeaderLines = (empty($chartLayout)) ? 1 : $chartLayout->getShowLeaderLines();
+ $objWriter->writeAttribute('val', ((empty($showLeaderLines)) ? 0 : 1));
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Category Axis.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Title $xAxisLabel
+ * @param string $id1
+ * @param string $id2
+ * @param bool $isMultiLevelSeries
+ */
+ private function writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void
+ {
+ $objWriter->startElement('c:catAx');
+
+ if ($id1 > 0) {
+ $objWriter->startElement('c:axId');
+ $objWriter->writeAttribute('val', $id1);
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:scaling');
+ $objWriter->startElement('c:orientation');
+ $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation'));
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:delete');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:axPos');
+ $objWriter->writeAttribute('val', 'b');
+ $objWriter->endElement();
+
+ if ($xAxisLabel !== null) {
+ $objWriter->startElement('c:title');
+ $objWriter->startElement('c:tx');
+ $objWriter->startElement('c:rich');
+
+ $objWriter->startElement('a:bodyPr');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:lstStyle');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:p');
+ $objWriter->startElement('a:r');
+
+ $caption = $xAxisLabel->getCaption();
+ if (is_array($caption)) {
+ $caption = $caption[0];
+ }
+ $objWriter->startElement('a:t');
+ $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption));
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $layout = $xAxisLabel->getLayout();
+ $this->writeLayout($objWriter, $layout);
+
+ $objWriter->startElement('c:overlay');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:numFmt');
+ $objWriter->writeAttribute('formatCode', $yAxis->getAxisNumberFormat());
+ $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:majorTickMark');
+ $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark'));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:minorTickMark');
+ $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark'));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:tickLblPos');
+ $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels'));
+ $objWriter->endElement();
+
+ if ($id2 > 0) {
+ $objWriter->startElement('c:crossAx');
+ $objWriter->writeAttribute('val', $id2);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:crosses');
+ $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses'));
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:auto');
+ $objWriter->writeAttribute('val', 1);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:lblAlgn');
+ $objWriter->writeAttribute('val', 'ctr');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:lblOffset');
+ $objWriter->writeAttribute('val', 100);
+ $objWriter->endElement();
+
+ if ($isMultiLevelSeries) {
+ $objWriter->startElement('c:noMultiLvlLbl');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ }
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Value Axis.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Title $yAxisLabel
+ * @param string $groupType Chart type
+ * @param string $id1
+ * @param string $id2
+ * @param bool $isMultiLevelSeries
+ */
+ private function writeValueAxis($objWriter, $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, GridLines $majorGridlines, GridLines $minorGridlines): void
+ {
+ $objWriter->startElement('c:valAx');
+
+ if ($id2 > 0) {
+ $objWriter->startElement('c:axId');
+ $objWriter->writeAttribute('val', $id2);
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:scaling');
+
+ if ($xAxis->getAxisOptionsProperty('maximum') !== null) {
+ $objWriter->startElement('c:max');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('maximum'));
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getAxisOptionsProperty('minimum') !== null) {
+ $objWriter->startElement('c:min');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minimum'));
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:orientation');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation'));
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:delete');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:axPos');
+ $objWriter->writeAttribute('val', 'l');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:majorGridlines');
+ $objWriter->startElement('c:spPr');
+
+ if ($majorGridlines->getLineColorProperty('value') !== null) {
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width'));
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}");
+ $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha'));
+ $objWriter->endElement(); //end alpha
+ $objWriter->endElement(); //end srgbClr
+ $objWriter->endElement(); //end solidFill
+
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash'));
+ $objWriter->endElement();
+
+ if ($majorGridlines->getLineStyleProperty('join') == 'miter') {
+ $objWriter->startElement('a:miter');
+ $objWriter->writeAttribute('lim', '800000');
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('a:bevel');
+ $objWriter->endElement();
+ }
+
+ if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) {
+ $objWriter->startElement('a:headEnd');
+ $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']));
+ $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w'));
+ $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len'));
+ $objWriter->endElement();
+ }
+
+ if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) {
+ $objWriter->startElement('a:tailEnd');
+ $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']));
+ $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w'));
+ $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len'));
+ $objWriter->endElement();
+ }
+ $objWriter->endElement(); //end ln
+ }
+ $objWriter->startElement('a:effectLst');
+
+ if ($majorGridlines->getGlowSize() !== null) {
+ $objWriter->startElement('a:glow');
+ $objWriter->writeAttribute('rad', $majorGridlines->getGlowSize());
+ $objWriter->startElement("a:{$majorGridlines->getGlowColor('type')}");
+ $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('alpha'));
+ $objWriter->endElement(); //end alpha
+ $objWriter->endElement(); //end schemeClr
+ $objWriter->endElement(); //end glow
+ }
+
+ if ($majorGridlines->getShadowProperty('presets') !== null) {
+ $objWriter->startElement("a:{$majorGridlines->getShadowProperty('effect')}");
+ if ($majorGridlines->getShadowProperty('blur') !== null) {
+ $objWriter->writeAttribute('blurRad', $majorGridlines->getShadowProperty('blur'));
+ }
+ if ($majorGridlines->getShadowProperty('distance') !== null) {
+ $objWriter->writeAttribute('dist', $majorGridlines->getShadowProperty('distance'));
+ }
+ if ($majorGridlines->getShadowProperty('direction') !== null) {
+ $objWriter->writeAttribute('dir', $majorGridlines->getShadowProperty('direction'));
+ }
+ if ($majorGridlines->getShadowProperty('algn') !== null) {
+ $objWriter->writeAttribute('algn', $majorGridlines->getShadowProperty('algn'));
+ }
+ if ($majorGridlines->getShadowProperty(['size', 'sx']) !== null) {
+ $objWriter->writeAttribute('sx', $majorGridlines->getShadowProperty(['size', 'sx']));
+ }
+ if ($majorGridlines->getShadowProperty(['size', 'sy']) !== null) {
+ $objWriter->writeAttribute('sy', $majorGridlines->getShadowProperty(['size', 'sy']));
+ }
+ if ($majorGridlines->getShadowProperty(['size', 'kx']) !== null) {
+ $objWriter->writeAttribute('kx', $majorGridlines->getShadowProperty(['size', 'kx']));
+ }
+ if ($majorGridlines->getShadowProperty('rotWithShape') !== null) {
+ $objWriter->writeAttribute('rotWithShape', $majorGridlines->getShadowProperty('rotWithShape'));
+ }
+ $objWriter->startElement("a:{$majorGridlines->getShadowProperty(['color', 'type'])}");
+ $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'value']));
+
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'alpha']));
+ $objWriter->endElement(); //end alpha
+
+ $objWriter->endElement(); //end color:type
+ $objWriter->endElement(); //end shadow
+ }
+
+ if ($majorGridlines->getSoftEdgesSize() !== null) {
+ $objWriter->startElement('a:softEdge');
+ $objWriter->writeAttribute('rad', $majorGridlines->getSoftEdgesSize());
+ $objWriter->endElement(); //end softEdge
+ }
+
+ $objWriter->endElement(); //end effectLst
+ $objWriter->endElement(); //end spPr
+ $objWriter->endElement(); //end majorGridLines
+
+ if ($minorGridlines->getObjectState()) {
+ $objWriter->startElement('c:minorGridlines');
+ $objWriter->startElement('c:spPr');
+
+ if ($minorGridlines->getLineColorProperty('value') !== null) {
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width'));
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}");
+ $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha'));
+ $objWriter->endElement(); //end alpha
+ $objWriter->endElement(); //end srgbClr
+ $objWriter->endElement(); //end solidFill
+
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash'));
+ $objWriter->endElement();
+
+ if ($minorGridlines->getLineStyleProperty('join') == 'miter') {
+ $objWriter->startElement('a:miter');
+ $objWriter->writeAttribute('lim', '800000');
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('a:bevel');
+ $objWriter->endElement();
+ }
+
+ if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) {
+ $objWriter->startElement('a:headEnd');
+ $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']));
+ $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w'));
+ $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len'));
+ $objWriter->endElement();
+ }
+
+ if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) {
+ $objWriter->startElement('a:tailEnd');
+ $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']));
+ $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w'));
+ $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len'));
+ $objWriter->endElement();
+ }
+ $objWriter->endElement(); //end ln
+ }
+
+ $objWriter->startElement('a:effectLst');
+
+ if ($minorGridlines->getGlowSize() !== null) {
+ $objWriter->startElement('a:glow');
+ $objWriter->writeAttribute('rad', $minorGridlines->getGlowSize());
+ $objWriter->startElement("a:{$minorGridlines->getGlowColor('type')}");
+ $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('alpha'));
+ $objWriter->endElement(); //end alpha
+ $objWriter->endElement(); //end schemeClr
+ $objWriter->endElement(); //end glow
+ }
+
+ if ($minorGridlines->getShadowProperty('presets') !== null) {
+ $objWriter->startElement("a:{$minorGridlines->getShadowProperty('effect')}");
+ if ($minorGridlines->getShadowProperty('blur') !== null) {
+ $objWriter->writeAttribute('blurRad', $minorGridlines->getShadowProperty('blur'));
+ }
+ if ($minorGridlines->getShadowProperty('distance') !== null) {
+ $objWriter->writeAttribute('dist', $minorGridlines->getShadowProperty('distance'));
+ }
+ if ($minorGridlines->getShadowProperty('direction') !== null) {
+ $objWriter->writeAttribute('dir', $minorGridlines->getShadowProperty('direction'));
+ }
+ if ($minorGridlines->getShadowProperty('algn') !== null) {
+ $objWriter->writeAttribute('algn', $minorGridlines->getShadowProperty('algn'));
+ }
+ if ($minorGridlines->getShadowProperty(['size', 'sx']) !== null) {
+ $objWriter->writeAttribute('sx', $minorGridlines->getShadowProperty(['size', 'sx']));
+ }
+ if ($minorGridlines->getShadowProperty(['size', 'sy']) !== null) {
+ $objWriter->writeAttribute('sy', $minorGridlines->getShadowProperty(['size', 'sy']));
+ }
+ if ($minorGridlines->getShadowProperty(['size', 'kx']) !== null) {
+ $objWriter->writeAttribute('kx', $minorGridlines->getShadowProperty(['size', 'kx']));
+ }
+ if ($minorGridlines->getShadowProperty('rotWithShape') !== null) {
+ $objWriter->writeAttribute('rotWithShape', $minorGridlines->getShadowProperty('rotWithShape'));
+ }
+ $objWriter->startElement("a:{$minorGridlines->getShadowProperty(['color', 'type'])}");
+ $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'value']));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'alpha']));
+ $objWriter->endElement(); //end alpha
+ $objWriter->endElement(); //end color:type
+ $objWriter->endElement(); //end shadow
+ }
+
+ if ($minorGridlines->getSoftEdgesSize() !== null) {
+ $objWriter->startElement('a:softEdge');
+ $objWriter->writeAttribute('rad', $minorGridlines->getSoftEdgesSize());
+ $objWriter->endElement(); //end softEdge
+ }
+
+ $objWriter->endElement(); //end effectLst
+ $objWriter->endElement(); //end spPr
+ $objWriter->endElement(); //end minorGridLines
+ }
+
+ if ($yAxisLabel !== null) {
+ $objWriter->startElement('c:title');
+ $objWriter->startElement('c:tx');
+ $objWriter->startElement('c:rich');
+
+ $objWriter->startElement('a:bodyPr');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:lstStyle');
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:p');
+ $objWriter->startElement('a:r');
+
+ $caption = $yAxisLabel->getCaption();
+ if (is_array($caption)) {
+ $caption = $caption[0];
+ }
+
+ $objWriter->startElement('a:t');
+ $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption));
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
+ $layout = $yAxisLabel->getLayout();
+ $this->writeLayout($objWriter, $layout);
+ }
+
+ $objWriter->startElement('c:overlay');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:numFmt');
+ $objWriter->writeAttribute('formatCode', $xAxis->getAxisNumberFormat());
+ $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:majorTickMark');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark'));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:minorTickMark');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark'));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:tickLblPos');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels'));
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:spPr');
+
+ if ($xAxis->getFillProperty('value') !== null) {
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement('a:' . $xAxis->getFillProperty('type'));
+ $objWriter->writeAttribute('val', $xAxis->getFillProperty('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha'));
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('a:ln');
+
+ $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width'));
+ $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap'));
+ $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound'));
+
+ if ($xAxis->getLineProperty('value') !== null) {
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement('a:' . $xAxis->getLineProperty('type'));
+ $objWriter->writeAttribute('val', $xAxis->getLineProperty('value'));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha'));
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash'));
+ $objWriter->endElement();
+
+ if ($xAxis->getLineStyleProperty('join') == 'miter') {
+ $objWriter->startElement('a:miter');
+ $objWriter->writeAttribute('lim', '800000');
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('a:bevel');
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) {
+ $objWriter->startElement('a:headEnd');
+ $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type']));
+ $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head'));
+ $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head'));
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) {
+ $objWriter->startElement('a:tailEnd');
+ $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type']));
+ $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end'));
+ $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end'));
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+
+ $objWriter->startElement('a:effectLst');
+
+ if ($xAxis->getGlowProperty('size') !== null) {
+ $objWriter->startElement('a:glow');
+ $objWriter->writeAttribute('rad', $xAxis->getGlowProperty('size'));
+ $objWriter->startElement("a:{$xAxis->getGlowProperty(['color', 'type'])}");
+ $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'value']));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'alpha']));
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getShadowProperty('presets') !== null) {
+ $objWriter->startElement("a:{$xAxis->getShadowProperty('effect')}");
+
+ if ($xAxis->getShadowProperty('blur') !== null) {
+ $objWriter->writeAttribute('blurRad', $xAxis->getShadowProperty('blur'));
+ }
+ if ($xAxis->getShadowProperty('distance') !== null) {
+ $objWriter->writeAttribute('dist', $xAxis->getShadowProperty('distance'));
+ }
+ if ($xAxis->getShadowProperty('direction') !== null) {
+ $objWriter->writeAttribute('dir', $xAxis->getShadowProperty('direction'));
+ }
+ if ($xAxis->getShadowProperty('algn') !== null) {
+ $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn'));
+ }
+ if ($xAxis->getShadowProperty(['size', 'sx']) !== null) {
+ $objWriter->writeAttribute('sx', $xAxis->getShadowProperty(['size', 'sx']));
+ }
+ if ($xAxis->getShadowProperty(['size', 'sy']) !== null) {
+ $objWriter->writeAttribute('sy', $xAxis->getShadowProperty(['size', 'sy']));
+ }
+ if ($xAxis->getShadowProperty(['size', 'kx']) !== null) {
+ $objWriter->writeAttribute('kx', $xAxis->getShadowProperty(['size', 'kx']));
+ }
+ if ($xAxis->getShadowProperty('rotWithShape') !== null) {
+ $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape'));
+ }
+
+ $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}");
+ $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value']));
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'alpha']));
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getSoftEdgesSize() !== null) {
+ $objWriter->startElement('a:softEdge');
+ $objWriter->writeAttribute('rad', $xAxis->getSoftEdgesSize());
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement(); //effectList
+ $objWriter->endElement(); //end spPr
+
+ if ($id1 > 0) {
+ $objWriter->startElement('c:crossAx');
+ $objWriter->writeAttribute('val', $id2);
+ $objWriter->endElement();
+
+ if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) {
+ $objWriter->startElement('c:crossesAt');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value'));
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('c:crosses');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses'));
+ $objWriter->endElement();
+ }
+
+ $objWriter->startElement('c:crossBetween');
+ $objWriter->writeAttribute('val', 'midCat');
+ $objWriter->endElement();
+
+ if ($xAxis->getAxisOptionsProperty('major_unit') !== null) {
+ $objWriter->startElement('c:majorUnit');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_unit'));
+ $objWriter->endElement();
+ }
+
+ if ($xAxis->getAxisOptionsProperty('minor_unit') !== null) {
+ $objWriter->startElement('c:minorUnit');
+ $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_unit'));
+ $objWriter->endElement();
+ }
+ }
+
+ if ($isMultiLevelSeries) {
+ if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
+ $objWriter->startElement('c:noMultiLvlLbl');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Get the data series type(s) for a chart plot series.
+ *
+ * @param PlotArea $plotArea
+ *
+ * @return array|string
+ */
+ private static function getChartType($plotArea)
+ {
+ $groupCount = $plotArea->getPlotGroupCount();
+
+ if ($groupCount == 1) {
+ $chartType = [$plotArea->getPlotGroupByIndex(0)->getPlotType()];
+ } else {
+ $chartTypes = [];
+ for ($i = 0; $i < $groupCount; ++$i) {
+ $chartTypes[] = $plotArea->getPlotGroupByIndex($i)->getPlotType();
+ }
+ $chartType = array_unique($chartTypes);
+ if (count($chartTypes) == 0) {
+ throw new WriterException('Chart is not yet implemented');
+ }
+ }
+
+ return $chartType;
+ }
+
+ /**
+ * Method writing plot series values.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param int $val value for idx (default: 3)
+ * @param string $fillColor hex color (default: FF9900)
+ *
+ * @return XMLWriter XML Writer
+ */
+ private function writePlotSeriesValuesElement($objWriter, $val = 3, $fillColor = 'FF9900')
+ {
+ $objWriter->startElement('c:dPt');
+ $objWriter->startElement('c:idx');
+ $objWriter->writeAttribute('val', $val);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:bubble3D');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:spPr');
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', $fillColor);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ return $objWriter;
+ }
+
+ /**
+ * Write Plot Group (series of related plots).
+ *
+ * @param DataSeries $plotGroup
+ * @param string $groupType Type of plot for dataseries
+ * @param XMLWriter $objWriter XML Writer
+ * @param bool $catIsMultiLevelSeries Is category a multi-series category
+ * @param bool $valIsMultiLevelSeries Is value set a multi-series set
+ * @param string $plotGroupingType Type of grouping for multi-series values
+ */
+ private function writePlotGroup($plotGroup, $groupType, $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void
+ {
+ if ($plotGroup === null) {
+ return;
+ }
+
+ if (($groupType == DataSeries::TYPE_BARCHART) || ($groupType == DataSeries::TYPE_BARCHART_3D)) {
+ $objWriter->startElement('c:barDir');
+ $objWriter->writeAttribute('val', $plotGroup->getPlotDirection());
+ $objWriter->endElement();
+ }
+
+ if ($plotGroup->getPlotGrouping() !== null) {
+ $plotGroupingType = $plotGroup->getPlotGrouping();
+ $objWriter->startElement('c:grouping');
+ $objWriter->writeAttribute('val', $plotGroupingType);
+ $objWriter->endElement();
+ }
+
+ // Get these details before the loop, because we can use the count to check for varyColors
+ $plotSeriesOrder = $plotGroup->getPlotOrder();
+ $plotSeriesCount = count($plotSeriesOrder);
+
+ if (($groupType !== DataSeries::TYPE_RADARCHART) && ($groupType !== DataSeries::TYPE_STOCKCHART)) {
+ if ($groupType !== DataSeries::TYPE_LINECHART) {
+ if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) {
+ $objWriter->startElement('c:varyColors');
+ $objWriter->writeAttribute('val', 1);
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('c:varyColors');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ }
+ }
+ }
+
+ $plotSeriesIdx = 0;
+ foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) {
+ $objWriter->startElement('c:ser');
+
+ $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx);
+ if ($plotLabel) {
+ $fillColor = $plotLabel->getFillColor();
+ if ($fillColor !== null && !is_array($fillColor)) {
+ $objWriter->startElement('c:spPr');
+ $objWriter->startElement('a:solidFill');
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', $fillColor);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->startElement('c:idx');
+ $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:order');
+ $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef);
+ $objWriter->endElement();
+
+ // Values
+ $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesRef);
+
+ if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) {
+ $fillColorValues = $plotSeriesValues->getFillColor();
+ if ($fillColorValues !== null && is_array($fillColorValues)) {
+ foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) {
+ $this->writePlotSeriesValuesElement($objWriter, $dataKey, ($fillColorValues[$dataKey] ?? 'FF9900'));
+ }
+ } else {
+ $this->writePlotSeriesValuesElement($objWriter);
+ }
+ }
+
+ // Labels
+ $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesRef);
+ if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) {
+ $objWriter->startElement('c:tx');
+ $objWriter->startElement('c:strRef');
+ $this->writePlotSeriesLabel($plotSeriesLabel, $objWriter);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+
+ // Formatting for the points
+ if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) {
+ $plotLineWidth = 12700;
+ if ($plotSeriesValues) {
+ $plotLineWidth = $plotSeriesValues->getLineWidth();
+ }
+
+ $objWriter->startElement('c:spPr');
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', $plotLineWidth);
+ if ($groupType == DataSeries::TYPE_STOCKCHART) {
+ $objWriter->startElement('a:noFill');
+ $objWriter->endElement();
+ }
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+
+ if ($plotSeriesValues) {
+ $plotSeriesMarker = $plotSeriesValues->getPointMarker();
+ if ($plotSeriesMarker) {
+ $objWriter->startElement('c:marker');
+ $objWriter->startElement('c:symbol');
+ $objWriter->writeAttribute('val', $plotSeriesMarker);
+ $objWriter->endElement();
+
+ if ($plotSeriesMarker !== 'none') {
+ $objWriter->startElement('c:size');
+ $objWriter->writeAttribute('val', 3);
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) {
+ $objWriter->startElement('c:invertIfNegative');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ }
+
+ // Category Labels
+ $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesRef);
+ if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) {
+ $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries();
+
+ if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) {
+ if ($plotGroup->getPlotStyle() !== null) {
+ $plotStyle = $plotGroup->getPlotStyle();
+ if ($plotStyle) {
+ $objWriter->startElement('c:explosion');
+ $objWriter->writeAttribute('val', 25);
+ $objWriter->endElement();
+ }
+ }
+ }
+
+ if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
+ $objWriter->startElement('c:xVal');
+ } else {
+ $objWriter->startElement('c:cat');
+ }
+
+ $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str');
+ $objWriter->endElement();
+ }
+
+ // Values
+ if ($plotSeriesValues) {
+ $valIsMultiLevelSeries = $valIsMultiLevelSeries || $plotSeriesValues->isMultiLevelSeries();
+
+ if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
+ $objWriter->startElement('c:yVal');
+ } else {
+ $objWriter->startElement('c:val');
+ }
+
+ $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num');
+ $objWriter->endElement();
+ }
+
+ if ($groupType === DataSeries::TYPE_BUBBLECHART) {
+ $this->writeBubbles($plotSeriesValues, $objWriter);
+ }
+
+ $objWriter->endElement();
+ }
+
+ $this->seriesIndex += $plotSeriesIdx + 1;
+ }
+
+ /**
+ * Write Plot Series Label.
+ *
+ * @param DataSeriesValues $plotSeriesLabel
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writePlotSeriesLabel($plotSeriesLabel, $objWriter): void
+ {
+ if ($plotSeriesLabel === null) {
+ return;
+ }
+
+ $objWriter->startElement('c:f');
+ $objWriter->writeRawData($plotSeriesLabel->getDataSource());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:strCache');
+ $objWriter->startElement('c:ptCount');
+ $objWriter->writeAttribute('val', $plotSeriesLabel->getPointCount());
+ $objWriter->endElement();
+
+ foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) {
+ $objWriter->startElement('c:pt');
+ $objWriter->writeAttribute('idx', $plotLabelKey);
+
+ $objWriter->startElement('c:v');
+ $objWriter->writeRawData($plotLabelValue);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Plot Series Values.
+ *
+ * @param DataSeriesValues $plotSeriesValues
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $groupType Type of plot for dataseries
+ * @param string $dataType Datatype of series values
+ */
+ private function writePlotSeriesValues($plotSeriesValues, XMLWriter $objWriter, $groupType, $dataType = 'str'): void
+ {
+ if ($plotSeriesValues === null) {
+ return;
+ }
+
+ if ($plotSeriesValues->isMultiLevelSeries()) {
+ $levelCount = $plotSeriesValues->multiLevelCount();
+
+ $objWriter->startElement('c:multiLvlStrRef');
+
+ $objWriter->startElement('c:f');
+ $objWriter->writeRawData($plotSeriesValues->getDataSource());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:multiLvlStrCache');
+
+ $objWriter->startElement('c:ptCount');
+ $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount());
+ $objWriter->endElement();
+
+ for ($level = 0; $level < $levelCount; ++$level) {
+ $objWriter->startElement('c:lvl');
+
+ foreach ($plotSeriesValues->getDataValues() as $plotSeriesKey => $plotSeriesValue) {
+ if (isset($plotSeriesValue[$level])) {
+ $objWriter->startElement('c:pt');
+ $objWriter->writeAttribute('idx', $plotSeriesKey);
+
+ $objWriter->startElement('c:v');
+ $objWriter->writeRawData($plotSeriesValue[$level]);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ } else {
+ $objWriter->startElement('c:' . $dataType . 'Ref');
+
+ $objWriter->startElement('c:f');
+ $objWriter->writeRawData($plotSeriesValues->getDataSource());
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:' . $dataType . 'Cache');
+
+ if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
+ if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) {
+ $objWriter->startElement('c:formatCode');
+ $objWriter->writeRawData($plotSeriesValues->getFormatCode());
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->startElement('c:ptCount');
+ $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount());
+ $objWriter->endElement();
+
+ $dataValues = $plotSeriesValues->getDataValues();
+ if (!empty($dataValues)) {
+ if (is_array($dataValues)) {
+ foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
+ $objWriter->startElement('c:pt');
+ $objWriter->writeAttribute('idx', $plotSeriesKey);
+
+ $objWriter->startElement('c:v');
+ $objWriter->writeRawData($plotSeriesValue);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+ }
+ }
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write Bubble Chart Details.
+ *
+ * @param DataSeriesValues $plotSeriesValues
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeBubbles($plotSeriesValues, $objWriter): void
+ {
+ if ($plotSeriesValues === null) {
+ return;
+ }
+
+ $objWriter->startElement('c:bubbleSize');
+ $objWriter->startElement('c:numLit');
+
+ $objWriter->startElement('c:formatCode');
+ $objWriter->writeRawData('General');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:ptCount');
+ $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount());
+ $objWriter->endElement();
+
+ $dataValues = $plotSeriesValues->getDataValues();
+ if (!empty($dataValues)) {
+ if (is_array($dataValues)) {
+ foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
+ $objWriter->startElement('c:pt');
+ $objWriter->writeAttribute('idx', $plotSeriesKey);
+ $objWriter->startElement('c:v');
+ $objWriter->writeRawData(1);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ }
+ }
+ }
+
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:bubble3D');
+ $objWriter->writeAttribute('val', 0);
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Layout.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Layout $layout
+ */
+ private function writeLayout(XMLWriter $objWriter, ?Layout $layout = null): void
+ {
+ $objWriter->startElement('c:layout');
+
+ if ($layout !== null) {
+ $objWriter->startElement('c:manualLayout');
+
+ $layoutTarget = $layout->getLayoutTarget();
+ if ($layoutTarget !== null) {
+ $objWriter->startElement('c:layoutTarget');
+ $objWriter->writeAttribute('val', $layoutTarget);
+ $objWriter->endElement();
+ }
+
+ $xMode = $layout->getXMode();
+ if ($xMode !== null) {
+ $objWriter->startElement('c:xMode');
+ $objWriter->writeAttribute('val', $xMode);
+ $objWriter->endElement();
+ }
+
+ $yMode = $layout->getYMode();
+ if ($yMode !== null) {
+ $objWriter->startElement('c:yMode');
+ $objWriter->writeAttribute('val', $yMode);
+ $objWriter->endElement();
+ }
+
+ $x = $layout->getXPosition();
+ if ($x !== null) {
+ $objWriter->startElement('c:x');
+ $objWriter->writeAttribute('val', $x);
+ $objWriter->endElement();
+ }
+
+ $y = $layout->getYPosition();
+ if ($y !== null) {
+ $objWriter->startElement('c:y');
+ $objWriter->writeAttribute('val', $y);
+ $objWriter->endElement();
+ }
+
+ $w = $layout->getWidth();
+ if ($w !== null) {
+ $objWriter->startElement('c:w');
+ $objWriter->writeAttribute('val', $w);
+ $objWriter->endElement();
+ }
+
+ $h = $layout->getHeight();
+ if ($h !== null) {
+ $objWriter->startElement('c:h');
+ $objWriter->writeAttribute('val', $h);
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Alternate Content block.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeAlternateContent($objWriter): void
+ {
+ $objWriter->startElement('mc:AlternateContent');
+ $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006');
+
+ $objWriter->startElement('mc:Choice');
+ $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart');
+ $objWriter->writeAttribute('Requires', 'c14');
+
+ $objWriter->startElement('c14:style');
+ $objWriter->writeAttribute('val', '102');
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('mc:Fallback');
+ $objWriter->startElement('c:style');
+ $objWriter->writeAttribute('val', '2');
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Printer Settings.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writePrintSettings($objWriter): void
+ {
+ $objWriter->startElement('c:printSettings');
+
+ $objWriter->startElement('c:headerFooter');
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:pageMargins');
+ $objWriter->writeAttribute('footer', 0.3);
+ $objWriter->writeAttribute('header', 0.3);
+ $objWriter->writeAttribute('r', 0.7);
+ $objWriter->writeAttribute('l', 0.7);
+ $objWriter->writeAttribute('t', 0.75);
+ $objWriter->writeAttribute('b', 0.75);
+ $objWriter->endElement();
+
+ $objWriter->startElement('c:pageSetup');
+ $objWriter->writeAttribute('orientation', 'portrait');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php
new file mode 100644
index 0000000..51f4248
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php
@@ -0,0 +1,231 @@
+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');
+
+ // Comments cache
+ $comments = $pWorksheet->getComments();
+
+ // Authors cache
+ $authors = [];
+ $authorId = 0;
+ foreach ($comments as $comment) {
+ if (!isset($authors[$comment->getAuthor()])) {
+ $authors[$comment->getAuthor()] = $authorId++;
+ }
+ }
+
+ // comments
+ $objWriter->startElement('comments');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
+
+ // Loop through authors
+ $objWriter->startElement('authors');
+ foreach ($authors as $author => $index) {
+ $objWriter->writeElement('author', $author);
+ }
+ $objWriter->endElement();
+
+ // Loop through comments
+ $objWriter->startElement('commentList');
+ foreach ($comments as $key => $value) {
+ $this->writeComment($objWriter, $key, $value, $authors);
+ }
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write comment to XML format.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pCellReference Cell reference
+ * @param Comment $pComment Comment
+ * @param array $pAuthors Array of authors
+ */
+ private function writeComment(XMLWriter $objWriter, $pCellReference, Comment $pComment, array $pAuthors): void
+ {
+ // comment
+ $objWriter->startElement('comment');
+ $objWriter->writeAttribute('ref', $pCellReference);
+ $objWriter->writeAttribute('authorId', $pAuthors[$pComment->getAuthor()]);
+
+ // text
+ $objWriter->startElement('text');
+ $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $pComment->getText());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write VML comments to XML format.
+ *
+ * @return string XML Output
+ */
+ public function writeVMLComments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet)
+ {
+ // 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');
+
+ // Comments cache
+ $comments = $pWorksheet->getComments();
+
+ // 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_t202');
+ $objWriter->writeAttribute('coordsize', '21600,21600');
+ $objWriter->writeAttribute('o:spt', '202');
+ $objWriter->writeAttribute('path', 'm,l,21600r21600,l21600,xe');
+
+ // v:stroke
+ $objWriter->startElement('v:stroke');
+ $objWriter->writeAttribute('joinstyle', 'miter');
+ $objWriter->endElement();
+
+ // v:path
+ $objWriter->startElement('v:path');
+ $objWriter->writeAttribute('gradientshapeok', 't');
+ $objWriter->writeAttribute('o:connecttype', 'rect');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // Loop through comments
+ foreach ($comments as $key => $value) {
+ $this->writeVMLComment($objWriter, $key, $value);
+ }
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write VML comment to XML format.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pCellReference Cell reference, eg: 'A1'
+ * @param Comment $pComment Comment
+ */
+ private function writeVMLComment(XMLWriter $objWriter, $pCellReference, Comment $pComment): void
+ {
+ // Metadata
+ [$column, $row] = Coordinate::indexesFromString($pCellReference);
+ $id = 1024 + $column + $row;
+ $id = substr($id, 0, 4);
+
+ // v:shape
+ $objWriter->startElement('v:shape');
+ $objWriter->writeAttribute('id', '_x0000_s' . $id);
+ $objWriter->writeAttribute('type', '#_x0000_t202');
+ $objWriter->writeAttribute('style', 'position:absolute;margin-left:' . $pComment->getMarginLeft() . ';margin-top:' . $pComment->getMarginTop() . ';width:' . $pComment->getWidth() . ';height:' . $pComment->getHeight() . ';z-index:1;visibility:' . ($pComment->getVisible() ? 'visible' : 'hidden'));
+ $objWriter->writeAttribute('fillcolor', '#' . $pComment->getFillColor()->getRGB());
+ $objWriter->writeAttribute('o:insetmode', 'auto');
+
+ // v:fill
+ $objWriter->startElement('v:fill');
+ $objWriter->writeAttribute('color2', '#' . $pComment->getFillColor()->getRGB());
+ $objWriter->endElement();
+
+ // v:shadow
+ $objWriter->startElement('v:shadow');
+ $objWriter->writeAttribute('on', 't');
+ $objWriter->writeAttribute('color', 'black');
+ $objWriter->writeAttribute('obscured', 't');
+ $objWriter->endElement();
+
+ // v:path
+ $objWriter->startElement('v:path');
+ $objWriter->writeAttribute('o:connecttype', 'none');
+ $objWriter->endElement();
+
+ // v:textbox
+ $objWriter->startElement('v:textbox');
+ $objWriter->writeAttribute('style', 'mso-direction-alt:auto');
+
+ // div
+ $objWriter->startElement('div');
+ $objWriter->writeAttribute('style', 'text-align:left');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // x:ClientData
+ $objWriter->startElement('x:ClientData');
+ $objWriter->writeAttribute('ObjectType', 'Note');
+
+ // x:MoveWithCells
+ $objWriter->writeElement('x:MoveWithCells', '');
+
+ // x:SizeWithCells
+ $objWriter->writeElement('x:SizeWithCells', '');
+
+ // x:AutoFill
+ $objWriter->writeElement('x:AutoFill', 'False');
+
+ // x:Row
+ $objWriter->writeElement('x:Row', ($row - 1));
+
+ // x:Column
+ $objWriter->writeElement('x:Column', ($column - 1));
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
new file mode 100644
index 0000000..2cff1a8
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
@@ -0,0 +1,240 @@
+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');
+
+ // Types
+ $objWriter->startElement('Types');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/content-types');
+
+ // Theme
+ $this->writeOverrideContentType($objWriter, '/xl/theme/theme1.xml', 'application/vnd.openxmlformats-officedocument.theme+xml');
+
+ // Styles
+ $this->writeOverrideContentType($objWriter, '/xl/styles.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml');
+
+ // Rels
+ $this->writeDefaultContentType($objWriter, 'rels', 'application/vnd.openxmlformats-package.relationships+xml');
+
+ // XML
+ $this->writeDefaultContentType($objWriter, 'xml', 'application/xml');
+
+ // VML
+ $this->writeDefaultContentType($objWriter, 'vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing');
+
+ // Workbook
+ if ($spreadsheet->hasMacros()) { //Macros in workbook ?
+ // Yes : not standard content but "macroEnabled"
+ $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.ms-excel.sheet.macroEnabled.main+xml');
+ //... and define a new type for the VBA project
+ // Better use Override, because we can use 'bin' also for xl\printerSettings\printerSettings1.bin
+ $this->writeOverrideContentType($objWriter, '/xl/vbaProject.bin', 'application/vnd.ms-office.vbaProject');
+ if ($spreadsheet->hasMacrosCertificate()) {
+ // signed macros ?
+ // Yes : add needed information
+ $this->writeOverrideContentType($objWriter, '/xl/vbaProjectSignature.bin', 'application/vnd.ms-office.vbaProjectSignature');
+ }
+ } else {
+ // no macros in workbook, so standard type
+ $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml');
+ }
+
+ // DocProps
+ $this->writeOverrideContentType($objWriter, '/docProps/app.xml', 'application/vnd.openxmlformats-officedocument.extended-properties+xml');
+
+ $this->writeOverrideContentType($objWriter, '/docProps/core.xml', 'application/vnd.openxmlformats-package.core-properties+xml');
+
+ $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+ if (!empty($customPropertyList)) {
+ $this->writeOverrideContentType($objWriter, '/docProps/custom.xml', 'application/vnd.openxmlformats-officedocument.custom-properties+xml');
+ }
+
+ // Worksheets
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ $this->writeOverrideContentType($objWriter, '/xl/worksheets/sheet' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml');
+ }
+
+ // Shared strings
+ $this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml');
+
+ // Add worksheet relationship content types
+ $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData();
+ $chart = 1;
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ $drawings = $spreadsheet->getSheet($i)->getDrawingCollection();
+ $drawingCount = count($drawings);
+ $chartCount = ($includeCharts) ? $spreadsheet->getSheet($i)->getChartCount() : 0;
+ $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$spreadsheet->getSheet($i)->getCodeName()]['drawingOriginalIds']);
+
+ // We need a drawing relationship for the worksheet if we have either drawings or charts
+ if (($drawingCount > 0) || ($chartCount > 0) || $hasUnparsedDrawing) {
+ $this->writeOverrideContentType($objWriter, '/xl/drawings/drawing' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.drawing+xml');
+ }
+
+ // If we have charts, then we need a chart relationship for every individual chart
+ if ($chartCount > 0) {
+ for ($c = 0; $c < $chartCount; ++$c) {
+ $this->writeOverrideContentType($objWriter, '/xl/charts/chart' . $chart++ . '.xml', 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml');
+ }
+ }
+ }
+
+ // Comments
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ if (count($spreadsheet->getSheet($i)->getComments()) > 0) {
+ $this->writeOverrideContentType($objWriter, '/xl/comments' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml');
+ }
+ }
+
+ // Add media content-types
+ $aMediaContentTypes = [];
+ $mediaCount = $this->getParentWriter()->getDrawingHashTable()->count();
+ for ($i = 0; $i < $mediaCount; ++$i) {
+ $extension = '';
+ $mimeType = '';
+
+ if ($this->getParentWriter()->getDrawingHashTable()->getByIndex($i) instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing) {
+ $extension = strtolower($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getExtension());
+ $mimeType = $this->getImageMimeType($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getPath());
+ } elseif ($this->getParentWriter()->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) {
+ $extension = strtolower($this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getMimeType());
+ $extension = explode('/', $extension);
+ $extension = $extension[1];
+
+ $mimeType = $this->getParentWriter()->getDrawingHashTable()->getByIndex($i)->getMimeType();
+ }
+
+ if (!isset($aMediaContentTypes[$extension])) {
+ $aMediaContentTypes[$extension] = $mimeType;
+
+ $this->writeDefaultContentType($objWriter, $extension, $mimeType);
+ }
+ }
+ if ($spreadsheet->hasRibbonBinObjects()) {
+ // Some additional objects in the ribbon ?
+ // we need to write "Extension" but not already write for media content
+ $tabRibbonTypes = array_diff($spreadsheet->getRibbonBinObjects('types'), array_keys($aMediaContentTypes));
+ foreach ($tabRibbonTypes as $aRibbonType) {
+ $mimeType = 'image/.' . $aRibbonType; //we wrote $mimeType like customUI Editor
+ $this->writeDefaultContentType($objWriter, $aRibbonType, $mimeType);
+ }
+ }
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ if (count($spreadsheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
+ foreach ($spreadsheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
+ if (!isset($aMediaContentTypes[strtolower($image->getExtension())])) {
+ $aMediaContentTypes[strtolower($image->getExtension())] = $this->getImageMimeType($image->getPath());
+
+ $this->writeDefaultContentType($objWriter, strtolower($image->getExtension()), $aMediaContentTypes[strtolower($image->getExtension())]);
+ }
+ }
+ }
+ }
+
+ // unparsed defaults
+ if (isset($unparsedLoadedData['default_content_types'])) {
+ foreach ($unparsedLoadedData['default_content_types'] as $extName => $contentType) {
+ $this->writeDefaultContentType($objWriter, $extName, $contentType);
+ }
+ }
+
+ // unparsed overrides
+ if (isset($unparsedLoadedData['override_content_types'])) {
+ foreach ($unparsedLoadedData['override_content_types'] as $partName => $overrideType) {
+ $this->writeOverrideContentType($objWriter, $partName, $overrideType);
+ }
+ }
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Get image mime type.
+ *
+ * @param string $pFile Filename
+ *
+ * @return string Mime Type
+ */
+ private function getImageMimeType($pFile)
+ {
+ if (File::fileExists($pFile)) {
+ $image = getimagesize($pFile);
+
+ return image_type_to_mime_type($image[2]);
+ }
+
+ throw new WriterException("File $pFile does not exist");
+ }
+
+ /**
+ * Write Default content type.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pPartname Part name
+ * @param string $pContentType Content type
+ */
+ private function writeDefaultContentType(XMLWriter $objWriter, $pPartname, $pContentType): void
+ {
+ if ($pPartname != '' && $pContentType != '') {
+ // Write content type
+ $objWriter->startElement('Default');
+ $objWriter->writeAttribute('Extension', $pPartname);
+ $objWriter->writeAttribute('ContentType', $pContentType);
+ $objWriter->endElement();
+ } else {
+ throw new WriterException('Invalid parameters passed.');
+ }
+ }
+
+ /**
+ * Write Override content type.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pPartname Part name
+ * @param string $pContentType Content type
+ */
+ private function writeOverrideContentType(XMLWriter $objWriter, $pPartname, $pContentType): void
+ {
+ if ($pPartname != '' && $pContentType != '') {
+ // Write content type
+ $objWriter->startElement('Override');
+ $objWriter->writeAttribute('PartName', $pPartname);
+ $objWriter->writeAttribute('ContentType', $pContentType);
+ $objWriter->endElement();
+ } else {
+ throw new WriterException('Invalid parameters passed.');
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php
new file mode 100644
index 0000000..4c0929d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php
@@ -0,0 +1,228 @@
+objWriter = $objWriter;
+ $this->spreadsheet = $spreadsheet;
+ }
+
+ public function write(): void
+ {
+ // Write defined names
+ $this->objWriter->startElement('definedNames');
+
+ // Named ranges
+ if (count($this->spreadsheet->getDefinedNames()) > 0) {
+ // Named ranges
+ $this->writeNamedRangesAndFormulae();
+ }
+
+ // Other defined names
+ $sheetCount = $this->spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ // NamedRange for autoFilter
+ $this->writeNamedRangeForAutofilter($this->spreadsheet->getSheet($i), $i);
+
+ // NamedRange for Print_Titles
+ $this->writeNamedRangeForPrintTitles($this->spreadsheet->getSheet($i), $i);
+
+ // NamedRange for Print_Area
+ $this->writeNamedRangeForPrintArea($this->spreadsheet->getSheet($i), $i);
+ }
+
+ $this->objWriter->endElement();
+ }
+
+ /**
+ * Write defined names.
+ */
+ private function writeNamedRangesAndFormulae(): void
+ {
+ // Loop named ranges
+ $definedNames = $this->spreadsheet->getDefinedNames();
+ foreach ($definedNames as $definedName) {
+ $this->writeDefinedName($definedName);
+ }
+ }
+
+ /**
+ * Write Defined Name for named range.
+ */
+ private function writeDefinedName(DefinedName $pDefinedName): void
+ {
+ // definedName for named range
+ $this->objWriter->startElement('definedName');
+ $this->objWriter->writeAttribute('name', $pDefinedName->getName());
+ if ($pDefinedName->getLocalOnly() && $pDefinedName->getScope() !== null) {
+ $this->objWriter->writeAttribute(
+ 'localSheetId',
+ $pDefinedName->getScope()->getParent()->getIndex($pDefinedName->getScope())
+ );
+ }
+
+ $definedRange = $this->getDefinedRange($pDefinedName);
+
+ $this->objWriter->writeRawData($definedRange);
+
+ $this->objWriter->endElement();
+ }
+
+ /**
+ * Write Defined Name for autoFilter.
+ */
+ private function writeNamedRangeForAutofilter(Worksheet $pSheet, int $pSheetId = 0): void
+ {
+ // NamedRange for autoFilter
+ $autoFilterRange = $pSheet->getAutoFilter()->getRange();
+ if (!empty($autoFilterRange)) {
+ $this->objWriter->startElement('definedName');
+ $this->objWriter->writeAttribute('name', '_xlnm._FilterDatabase');
+ $this->objWriter->writeAttribute('localSheetId', $pSheetId);
+ $this->objWriter->writeAttribute('hidden', '1');
+
+ // Create absolute coordinate and write as raw text
+ $range = Coordinate::splitRange($autoFilterRange);
+ $range = $range[0];
+ // Strip any worksheet ref so we can make the cell ref absolute
+ [, $range[0]] = Worksheet::extractSheetTitle($range[0], true);
+
+ $range[0] = Coordinate::absoluteCoordinate($range[0]);
+ $range[1] = Coordinate::absoluteCoordinate($range[1]);
+ $range = implode(':', $range);
+
+ $this->objWriter->writeRawData('\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!' . $range);
+
+ $this->objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write Defined Name for PrintTitles.
+ */
+ private function writeNamedRangeForPrintTitles(Worksheet $pSheet, int $pSheetId = 0): void
+ {
+ // NamedRange for PrintTitles
+ if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet() || $pSheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
+ $this->objWriter->startElement('definedName');
+ $this->objWriter->writeAttribute('name', '_xlnm.Print_Titles');
+ $this->objWriter->writeAttribute('localSheetId', $pSheetId);
+
+ // Setting string
+ $settingString = '';
+
+ // Columns to repeat
+ if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) {
+ $repeat = $pSheet->getPageSetup()->getColumnsToRepeatAtLeft();
+
+ $settingString .= '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1];
+ }
+
+ // Rows to repeat
+ if ($pSheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
+ if ($pSheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) {
+ $settingString .= ',';
+ }
+
+ $repeat = $pSheet->getPageSetup()->getRowsToRepeatAtTop();
+
+ $settingString .= '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1];
+ }
+
+ $this->objWriter->writeRawData($settingString);
+
+ $this->objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write Defined Name for PrintTitles.
+ */
+ private function writeNamedRangeForPrintArea(Worksheet $pSheet, int $pSheetId = 0): void
+ {
+ // NamedRange for PrintArea
+ if ($pSheet->getPageSetup()->isPrintAreaSet()) {
+ $this->objWriter->startElement('definedName');
+ $this->objWriter->writeAttribute('name', '_xlnm.Print_Area');
+ $this->objWriter->writeAttribute('localSheetId', $pSheetId);
+
+ // Print area
+ $printArea = Coordinate::splitRange($pSheet->getPageSetup()->getPrintArea());
+
+ $chunks = [];
+ foreach ($printArea as $printAreaRect) {
+ $printAreaRect[0] = Coordinate::absoluteReference($printAreaRect[0]);
+ $printAreaRect[1] = Coordinate::absoluteReference($printAreaRect[1]);
+ $chunks[] = '\'' . str_replace("'", "''", $pSheet->getTitle()) . '\'!' . implode(':', $printAreaRect);
+ }
+
+ $this->objWriter->writeRawData(implode(',', $chunks));
+
+ $this->objWriter->endElement();
+ }
+ }
+
+ private function getDefinedRange(DefinedName $pDefinedName): string
+ {
+ $definedRange = $pDefinedName->getValue();
+ $splitCount = preg_match_all(
+ '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
+ $definedRange,
+ $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) || ($definedRange[$offset - 1] !== ':')) {
+ // We should have a worksheet
+ $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null;
+ }
+ } else {
+ $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+ }
+
+ if (!empty($worksheet)) {
+ $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
+ }
+ $newRange = "{$newRange}{$column}{$row}";
+
+ $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
+ }
+
+ if (substr($definedRange, 0, 1) === '=') {
+ $definedRange = substr($definedRange, 1);
+ }
+
+ return $definedRange;
+ }
+}
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..a4b09d3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
@@ -0,0 +1,504 @@
+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 = $pWorksheet->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 = $pWorksheet->getChartCount();
+ // Loop through charts and write the chart position
+ if ($chartCount > 0) {
+ for ($c = 0; $c < $chartCount; ++$c) {
+ $this->writeChart($objWriter, $pWorksheet->getChartByIndex($c), $c + $i);
+ }
+ }
+ }
+
+ // unparsed AlternateContent
+ $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
+ if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'])) {
+ foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'] as $drawingAlternateContent) {
+ $objWriter->writeRaw($drawingAlternateContent);
+ }
+ }
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write drawings to XML format.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param int $pRelationId
+ */
+ public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart, $pRelationId = -1): void
+ {
+ $tl = $pChart->getTopLeftPosition();
+ $tlColRow = Coordinate::indexesFromString($tl['cell']);
+ $br = $pChart->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 ' . $pRelationId);
+ $objWriter->writeAttribute('id', 1025 * $pRelationId);
+ $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' . $pRelationId);
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ $objWriter->startElement('xdr:clientData');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write drawings to XML format.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param int $pRelationId
+ * @param null|int $hlinkClickId
+ */
+ public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRelationId = -1, $hlinkClickId = null): void
+ {
+ if ($pRelationId >= 0) {
+ // xdr:oneCellAnchor
+ $objWriter->startElement('xdr:oneCellAnchor');
+ // Image location
+ $aCoordinates = Coordinate::indexesFromString($pDrawing->getCoordinates());
+
+ // xdr:from
+ $objWriter->startElement('xdr:from');
+ $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1);
+ $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetX()));
+ $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1);
+ $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetY()));
+ $objWriter->endElement();
+
+ // xdr:ext
+ $objWriter->startElement('xdr:ext');
+ $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getWidth()));
+ $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getHeight()));
+ $objWriter->endElement();
+
+ // xdr:pic
+ $objWriter->startElement('xdr:pic');
+
+ // xdr:nvPicPr
+ $objWriter->startElement('xdr:nvPicPr');
+
+ // xdr:cNvPr
+ $objWriter->startElement('xdr:cNvPr');
+ $objWriter->writeAttribute('id', $pRelationId);
+ $objWriter->writeAttribute('name', $pDrawing->getName());
+ $objWriter->writeAttribute('descr', $pDrawing->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' . $pRelationId);
+ $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($pDrawing->getRotation()));
+ $objWriter->endElement();
+
+ // a:prstGeom
+ $objWriter->startElement('a:prstGeom');
+ $objWriter->writeAttribute('prst', 'rect');
+
+ // a:avLst
+ $objWriter->writeElement('a:avLst', null);
+
+ $objWriter->endElement();
+
+ if ($pDrawing->getShadow()->getVisible()) {
+ // a:effectLst
+ $objWriter->startElement('a:effectLst');
+
+ // a:outerShdw
+ $objWriter->startElement('a:outerShdw');
+ $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getBlurRadius()));
+ $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getDistance()));
+ $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($pDrawing->getShadow()->getDirection()));
+ $objWriter->writeAttribute('algn', $pDrawing->getShadow()->getAlignment());
+ $objWriter->writeAttribute('rotWithShape', '0');
+
+ // a:srgbClr
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', $pDrawing->getShadow()->getColor()->getRGB());
+
+ // a:alpha
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', $pDrawing->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 $pWorksheet)
+ {
+ // 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 = $pWorksheet->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 XMLWriter $objWriter XML Writer
+ * @param string $pReference Reference
+ * @param HeaderFooterDrawing $pImage Image
+ */
+ private function writeVMLHeaderFooterImage(XMLWriter $objWriter, $pReference, HeaderFooterDrawing $pImage): void
+ {
+ // Calculate object id
+ preg_match('{(\d+)}', md5($pReference), $m);
+ $id = 1500 + ((int) substr($m[1], 0, 2) * 1);
+
+ // Calculate offset
+ $width = $pImage->getWidth();
+ $height = $pImage->getHeight();
+ $marginLeft = $pImage->getOffsetX();
+ $marginTop = $pImage->getOffsetY();
+
+ // v:shape
+ $objWriter->startElement('v:shape');
+ $objWriter->writeAttribute('id', $pReference);
+ $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' . $pReference);
+ $objWriter->writeAttribute('o:title', $pImage->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 \PhpOffice\PhpSpreadsheet\Worksheet\Drawing[] 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/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php
new file mode 100644
index 0000000..a67d3ef
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php
@@ -0,0 +1,449 @@
+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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+
+ $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+ if (!empty($customPropertyList)) {
+ // Relationship docProps/app.xml
+ $this->writeRelationship(
+ $objWriter,
+ 4,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties',
+ 'docProps/custom.xml'
+ );
+ }
+
+ // Relationship docProps/app.xml
+ $this->writeRelationship(
+ $objWriter,
+ 3,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
+ 'docProps/app.xml'
+ );
+
+ // Relationship docProps/core.xml
+ $this->writeRelationship(
+ $objWriter,
+ 2,
+ 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
+ 'docProps/core.xml'
+ );
+
+ // Relationship xl/workbook.xml
+ $this->writeRelationship(
+ $objWriter,
+ 1,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
+ 'xl/workbook.xml'
+ );
+ // a custom UI in workbook ?
+ if ($spreadsheet->hasRibbon()) {
+ $this->writeRelationShip(
+ $objWriter,
+ 5,
+ 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility',
+ $spreadsheet->getRibbonXMLData('target')
+ );
+ }
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write workbook relationships to XML format.
+ *
+ * @return string XML Output
+ */
+ public function writeWorkbookRelationships(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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+
+ // Relationship styles.xml
+ $this->writeRelationship(
+ $objWriter,
+ 1,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
+ 'styles.xml'
+ );
+
+ // Relationship theme/theme1.xml
+ $this->writeRelationship(
+ $objWriter,
+ 2,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
+ 'theme/theme1.xml'
+ );
+
+ // Relationship sharedStrings.xml
+ $this->writeRelationship(
+ $objWriter,
+ 3,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
+ 'sharedStrings.xml'
+ );
+
+ // Relationships with sheets
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ $this->writeRelationship(
+ $objWriter,
+ ($i + 1 + 3),
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
+ 'worksheets/sheet' . ($i + 1) . '.xml'
+ );
+ }
+ // Relationships for vbaProject if needed
+ // id : just after the last sheet
+ if ($spreadsheet->hasMacros()) {
+ $this->writeRelationShip(
+ $objWriter,
+ ($i + 1 + 3),
+ 'http://schemas.microsoft.com/office/2006/relationships/vbaProject',
+ 'vbaProject.bin'
+ );
+ ++$i; //increment i if needed for an another relation
+ }
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write worksheet relationships to XML format.
+ *
+ * Numbering is as follows:
+ * rId1 - Drawings
+ * rId_hyperlink_x - Hyperlinks
+ *
+ * @param int $pWorksheetId
+ * @param bool $includeCharts Flag indicating if we should write charts
+ *
+ * @return string XML Output
+ */
+ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, $pWorksheetId = 1, $includeCharts = false)
+ {
+ // 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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+
+ // Write drawing relationships?
+ $drawingOriginalIds = [];
+ $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
+ if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds'])) {
+ $drawingOriginalIds = $unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingOriginalIds'];
+ }
+
+ if ($includeCharts) {
+ $charts = $pWorksheet->getChartCollection();
+ } else {
+ $charts = [];
+ }
+
+ if (($pWorksheet->getDrawingCollection()->count() > 0) || (count($charts) > 0) || $drawingOriginalIds) {
+ $rId = 1;
+
+ // Use original $relPath to get original $rId.
+ // Take first. In future can be overwritten.
+ // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet::writeDrawings)
+ reset($drawingOriginalIds);
+ $relPath = key($drawingOriginalIds);
+ if (isset($drawingOriginalIds[$relPath])) {
+ $rId = (int) (substr($drawingOriginalIds[$relPath], 3));
+ }
+
+ // Generate new $relPath to write drawing relationship
+ $relPath = '../drawings/drawing' . $pWorksheetId . '.xml';
+ $this->writeRelationship(
+ $objWriter,
+ $rId,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ $relPath
+ );
+ }
+
+ // Write hyperlink relationships?
+ $i = 1;
+ foreach ($pWorksheet->getHyperlinkCollection() as $hyperlink) {
+ if (!$hyperlink->isInternal()) {
+ $this->writeRelationship(
+ $objWriter,
+ '_hyperlink_' . $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
+ $hyperlink->getUrl(),
+ 'External'
+ );
+
+ ++$i;
+ }
+ }
+
+ // Write comments relationship?
+ $i = 1;
+ if (count($pWorksheet->getComments()) > 0) {
+ $this->writeRelationship(
+ $objWriter,
+ '_comments_vml' . $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
+ '../drawings/vmlDrawing' . $pWorksheetId . '.vml'
+ );
+
+ $this->writeRelationship(
+ $objWriter,
+ '_comments' . $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
+ '../comments' . $pWorksheetId . '.xml'
+ );
+ }
+
+ // Write header/footer relationship?
+ $i = 1;
+ if (count($pWorksheet->getHeaderFooter()->getImages()) > 0) {
+ $this->writeRelationship(
+ $objWriter,
+ '_headerfooter_vml' . $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
+ '../drawings/vmlDrawingHF' . $pWorksheetId . '.vml'
+ );
+ }
+
+ $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'ctrlProps', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp');
+ $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'vmlDrawings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing');
+ $this->writeUnparsedRelationship($pWorksheet, $objWriter, 'printerSettings', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings');
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, XMLWriter $objWriter, $relationship, $type): void
+ {
+ $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
+ if (!isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship])) {
+ return;
+ }
+
+ foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()][$relationship] as $rId => $value) {
+ $this->writeRelationship(
+ $objWriter,
+ $rId,
+ $type,
+ $value['relFilePath']
+ );
+ }
+ }
+
+ /**
+ * Write drawing relationships to XML format.
+ *
+ * @param int $chartRef Chart ID
+ * @param bool $includeCharts Flag indicating if we should write charts
+ *
+ * @return string XML Output
+ */
+ public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, &$chartRef, $includeCharts = false)
+ {
+ // 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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+
+ // Loop through images and write relationships
+ $i = 1;
+ $iterator = $pWorksheet->getDrawingCollection()->getIterator();
+ while ($iterator->valid()) {
+ if (
+ $iterator->current() instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing
+ || $iterator->current() instanceof MemoryDrawing
+ ) {
+ // Write relationship for image drawing
+ /** @var \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing */
+ $drawing = $iterator->current();
+ $this->writeRelationship(
+ $objWriter,
+ $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
+ '../media/' . str_replace(' ', '', $drawing->getIndexedFilename())
+ );
+
+ $i = $this->writeDrawingHyperLink($objWriter, $drawing, $i);
+ }
+
+ $iterator->next();
+ ++$i;
+ }
+
+ if ($includeCharts) {
+ // Loop through charts and write relationships
+ $chartCount = $pWorksheet->getChartCount();
+ if ($chartCount > 0) {
+ for ($c = 0; $c < $chartCount; ++$c) {
+ $this->writeRelationship(
+ $objWriter,
+ $i++,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart',
+ '../charts/chart' . ++$chartRef . '.xml'
+ );
+ }
+ }
+ }
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write header/footer drawing relationships to XML format.
+ *
+ * @return string XML Output
+ */
+ public function writeHeaderFooterDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet)
+ {
+ // 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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+
+ // Loop through images and write relationships
+ foreach ($pWorksheet->getHeaderFooter()->getImages() as $key => $value) {
+ // Write relationship for image drawing
+ $this->writeRelationship(
+ $objWriter,
+ $key,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
+ '../media/' . $value->getIndexedFilename()
+ );
+ }
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write Override content type.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param int $pId Relationship ID. rId will be prepended!
+ * @param string $pType Relationship type
+ * @param string $pTarget Relationship target
+ * @param string $pTargetMode Relationship target mode
+ */
+ private function writeRelationship(XMLWriter $objWriter, $pId, $pType, $pTarget, $pTargetMode = ''): void
+ {
+ if ($pType != '' && $pTarget != '') {
+ // Write relationship
+ $objWriter->startElement('Relationship');
+ $objWriter->writeAttribute('Id', 'rId' . $pId);
+ $objWriter->writeAttribute('Type', $pType);
+ $objWriter->writeAttribute('Target', $pTarget);
+
+ if ($pTargetMode != '') {
+ $objWriter->writeAttribute('TargetMode', $pTargetMode);
+ }
+
+ $objWriter->endElement();
+ } else {
+ throw new WriterException('Invalid parameters passed.');
+ }
+ }
+
+ /**
+ * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing
+ *
+ * @return int
+ */
+ private function writeDrawingHyperLink($objWriter, $drawing, $i)
+ {
+ if ($drawing->getHyperlink() === null) {
+ return $i;
+ }
+
+ ++$i;
+ $this->writeRelationship(
+ $objWriter,
+ $i,
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
+ $drawing->getHyperlink()->getUrl(),
+ $drawing->getHyperlink()->getTypeHyperlink()
+ );
+
+ return $i;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php
new file mode 100644
index 0000000..8005207
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php
@@ -0,0 +1,45 @@
+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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+ $localRels = $spreadsheet->getRibbonBinObjects('names');
+ if (is_array($localRels)) {
+ foreach ($localRels as $aId => $aTarget) {
+ $objWriter->startElement('Relationship');
+ $objWriter->writeAttribute('Id', $aId);
+ $objWriter->writeAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
+ $objWriter->writeAttribute('Target', $aTarget);
+ $objWriter->endElement();
+ }
+ }
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php
new file mode 100644
index 0000000..55bcd36
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php
@@ -0,0 +1,40 @@
+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');
+
+ // Relationships
+ $objWriter->startElement('Relationships');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/package/2006/relationships');
+ $objWriter->startElement('Relationship');
+ $objWriter->writeAttribute('Id', 'rId1');
+ $objWriter->writeAttribute('Type', 'http://schemas.microsoft.com/office/2006/relationships/vbaProjectSignature');
+ $objWriter->writeAttribute('Target', 'vbaProjectSignature.bin');
+ $objWriter->endElement();
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php
new file mode 100644
index 0000000..b0f7d6d
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php
@@ -0,0 +1,282 @@
+flipStringTable($aStringTable);
+
+ // Loop through cells
+ foreach ($pSheet->getCoordinates() as $coordinate) {
+ $cell = $pSheet->getCell($coordinate);
+ $cellValue = $cell->getValue();
+ if (
+ !is_object($cellValue) &&
+ ($cellValue !== null) &&
+ $cellValue !== '' &&
+ !isset($aFlippedStringTable[$cellValue]) &&
+ ($cell->getDataType() == DataType::TYPE_STRING || $cell->getDataType() == DataType::TYPE_STRING2 || $cell->getDataType() == DataType::TYPE_NULL)
+ ) {
+ $aStringTable[] = $cellValue;
+ $aFlippedStringTable[$cellValue] = true;
+ } elseif (
+ $cellValue instanceof RichText &&
+ ($cellValue !== null) &&
+ !isset($aFlippedStringTable[$cellValue->getHashCode()])
+ ) {
+ $aStringTable[] = $cellValue;
+ $aFlippedStringTable[$cellValue->getHashCode()] = true;
+ }
+ }
+
+ return $aStringTable;
+ }
+
+ /**
+ * Write string table to XML format.
+ *
+ * @param string[] $pStringTable
+ *
+ * @return string XML Output
+ */
+ public function writeStringTable(array $pStringTable)
+ {
+ // 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');
+
+ // String table
+ $objWriter->startElement('sst');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
+ $objWriter->writeAttribute('uniqueCount', count($pStringTable));
+
+ // Loop through string table
+ foreach ($pStringTable as $textElement) {
+ $objWriter->startElement('si');
+
+ if (!$textElement instanceof RichText) {
+ $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement);
+ $objWriter->startElement('t');
+ if ($textToWrite !== trim($textToWrite)) {
+ $objWriter->writeAttribute('xml:space', 'preserve');
+ }
+ $objWriter->writeRawData($textToWrite);
+ $objWriter->endElement();
+ } elseif ($textElement instanceof RichText) {
+ $this->writeRichText($objWriter, $textElement);
+ }
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write Rich Text.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param RichText $pRichText Rich text
+ * @param string $prefix Optional Namespace prefix
+ */
+ public function writeRichText(XMLWriter $objWriter, RichText $pRichText, $prefix = null): void
+ {
+ if ($prefix !== null) {
+ $prefix .= ':';
+ }
+
+ // Loop through rich text elements
+ $elements = $pRichText->getRichTextElements();
+ foreach ($elements as $element) {
+ // r
+ $objWriter->startElement($prefix . 'r');
+
+ // rPr
+ if ($element instanceof Run) {
+ // rPr
+ $objWriter->startElement($prefix . 'rPr');
+
+ // rFont
+ $objWriter->startElement($prefix . 'rFont');
+ $objWriter->writeAttribute('val', $element->getFont()->getName());
+ $objWriter->endElement();
+
+ // Bold
+ $objWriter->startElement($prefix . 'b');
+ $objWriter->writeAttribute('val', ($element->getFont()->getBold() ? 'true' : 'false'));
+ $objWriter->endElement();
+
+ // Italic
+ $objWriter->startElement($prefix . 'i');
+ $objWriter->writeAttribute('val', ($element->getFont()->getItalic() ? 'true' : 'false'));
+ $objWriter->endElement();
+
+ // Superscript / subscript
+ if ($element->getFont()->getSuperscript() || $element->getFont()->getSubscript()) {
+ $objWriter->startElement($prefix . 'vertAlign');
+ if ($element->getFont()->getSuperscript()) {
+ $objWriter->writeAttribute('val', 'superscript');
+ } elseif ($element->getFont()->getSubscript()) {
+ $objWriter->writeAttribute('val', 'subscript');
+ }
+ $objWriter->endElement();
+ }
+
+ // Strikethrough
+ $objWriter->startElement($prefix . 'strike');
+ $objWriter->writeAttribute('val', ($element->getFont()->getStrikethrough() ? 'true' : 'false'));
+ $objWriter->endElement();
+
+ // Color
+ $objWriter->startElement($prefix . 'color');
+ $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB());
+ $objWriter->endElement();
+
+ // Size
+ $objWriter->startElement($prefix . 'sz');
+ $objWriter->writeAttribute('val', $element->getFont()->getSize());
+ $objWriter->endElement();
+
+ // Underline
+ $objWriter->startElement($prefix . 'u');
+ $objWriter->writeAttribute('val', $element->getFont()->getUnderline());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ // t
+ $objWriter->startElement($prefix . 't');
+ $objWriter->writeAttribute('xml:space', 'preserve');
+ $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText()));
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write Rich Text.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param RichText|string $pRichText text string or Rich text
+ * @param string $prefix Optional Namespace prefix
+ */
+ public function writeRichTextForCharts(XMLWriter $objWriter, $pRichText = null, $prefix = null): void
+ {
+ if (!$pRichText instanceof RichText) {
+ $textRun = $pRichText;
+ $pRichText = new RichText();
+ $pRichText->createTextRun($textRun);
+ }
+
+ if ($prefix !== null) {
+ $prefix .= ':';
+ }
+
+ // Loop through rich text elements
+ $elements = $pRichText->getRichTextElements();
+ foreach ($elements as $element) {
+ // r
+ $objWriter->startElement($prefix . 'r');
+
+ // rPr
+ $objWriter->startElement($prefix . 'rPr');
+
+ // Bold
+ $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0));
+ // Italic
+ $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0));
+ // Underline
+ $underlineType = $element->getFont()->getUnderline();
+ switch ($underlineType) {
+ case 'single':
+ $underlineType = 'sng';
+
+ break;
+ case 'double':
+ $underlineType = 'dbl';
+
+ break;
+ }
+ $objWriter->writeAttribute('u', $underlineType);
+ // Strikethrough
+ $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike'));
+
+ // rFont
+ $objWriter->startElement($prefix . 'latin');
+ $objWriter->writeAttribute('typeface', $element->getFont()->getName());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // t
+ $objWriter->startElement($prefix . 't');
+ $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText()));
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Flip string table (for index searching).
+ *
+ * @param array $stringTable Stringtable
+ *
+ * @return array
+ */
+ public function flipStringTable(array $stringTable)
+ {
+ // Return value
+ $returnValue = [];
+
+ // Loop through stringtable and add flipped items to $returnValue
+ foreach ($stringTable as $key => $value) {
+ if (!$value instanceof RichText) {
+ $returnValue[$value] = $key;
+ } elseif ($value instanceof RichText) {
+ $returnValue[$value->getHashCode()] = $key;
+ }
+ }
+
+ return $returnValue;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php
new file mode 100644
index 0000000..0c43fbf
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php
@@ -0,0 +1,676 @@
+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');
+
+ // styleSheet
+ $objWriter->startElement('styleSheet');
+ $objWriter->writeAttribute('xml:space', 'preserve');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
+
+ // numFmts
+ $objWriter->startElement('numFmts');
+ $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count());
+
+ // numFmt
+ for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) {
+ $this->writeNumFmt($objWriter, $this->getParentWriter()->getNumFmtHashTable()->getByIndex($i), $i);
+ }
+
+ $objWriter->endElement();
+
+ // fonts
+ $objWriter->startElement('fonts');
+ $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count());
+
+ // font
+ for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) {
+ $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i));
+ }
+
+ $objWriter->endElement();
+
+ // fills
+ $objWriter->startElement('fills');
+ $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count());
+
+ // fill
+ for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) {
+ $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i));
+ }
+
+ $objWriter->endElement();
+
+ // borders
+ $objWriter->startElement('borders');
+ $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count());
+
+ // border
+ for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) {
+ $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i));
+ }
+
+ $objWriter->endElement();
+
+ // cellStyleXfs
+ $objWriter->startElement('cellStyleXfs');
+ $objWriter->writeAttribute('count', 1);
+
+ // xf
+ $objWriter->startElement('xf');
+ $objWriter->writeAttribute('numFmtId', 0);
+ $objWriter->writeAttribute('fontId', 0);
+ $objWriter->writeAttribute('fillId', 0);
+ $objWriter->writeAttribute('borderId', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // cellXfs
+ $objWriter->startElement('cellXfs');
+ $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection()));
+
+ // xf
+ foreach ($spreadsheet->getCellXfCollection() as $cellXf) {
+ $this->writeCellStyleXf($objWriter, $cellXf, $spreadsheet);
+ }
+
+ $objWriter->endElement();
+
+ // cellStyles
+ $objWriter->startElement('cellStyles');
+ $objWriter->writeAttribute('count', 1);
+
+ // cellStyle
+ $objWriter->startElement('cellStyle');
+ $objWriter->writeAttribute('name', 'Normal');
+ $objWriter->writeAttribute('xfId', 0);
+ $objWriter->writeAttribute('builtinId', 0);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // dxfs
+ $objWriter->startElement('dxfs');
+ $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count());
+
+ // dxf
+ for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) {
+ $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle());
+ }
+
+ $objWriter->endElement();
+
+ // tableStyles
+ $objWriter->startElement('tableStyles');
+ $objWriter->writeAttribute('defaultTableStyle', 'TableStyleMedium9');
+ $objWriter->writeAttribute('defaultPivotStyle', 'PivotTableStyle1');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write Fill.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Fill $pFill Fill style
+ */
+ private function writeFill(XMLWriter $objWriter, Fill $pFill): void
+ {
+ // Check if this is a pattern type or gradient type
+ if (
+ $pFill->getFillType() === Fill::FILL_GRADIENT_LINEAR ||
+ $pFill->getFillType() === Fill::FILL_GRADIENT_PATH
+ ) {
+ // Gradient fill
+ $this->writeGradientFill($objWriter, $pFill);
+ } elseif ($pFill->getFillType() !== null) {
+ // Pattern fill
+ $this->writePatternFill($objWriter, $pFill);
+ }
+ }
+
+ /**
+ * Write Gradient Fill.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Fill $pFill Fill style
+ */
+ private function writeGradientFill(XMLWriter $objWriter, Fill $pFill): void
+ {
+ // fill
+ $objWriter->startElement('fill');
+
+ // gradientFill
+ $objWriter->startElement('gradientFill');
+ $objWriter->writeAttribute('type', $pFill->getFillType());
+ $objWriter->writeAttribute('degree', $pFill->getRotation());
+
+ // stop
+ $objWriter->startElement('stop');
+ $objWriter->writeAttribute('position', '0');
+
+ // color
+ $objWriter->startElement('color');
+ $objWriter->writeAttribute('rgb', $pFill->getStartColor()->getARGB());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // stop
+ $objWriter->startElement('stop');
+ $objWriter->writeAttribute('position', '1');
+
+ // color
+ $objWriter->startElement('color');
+ $objWriter->writeAttribute('rgb', $pFill->getEndColor()->getARGB());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Pattern Fill.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Fill $pFill Fill style
+ */
+ private function writePatternFill(XMLWriter $objWriter, Fill $pFill): void
+ {
+ // fill
+ $objWriter->startElement('fill');
+
+ // patternFill
+ $objWriter->startElement('patternFill');
+ $objWriter->writeAttribute('patternType', $pFill->getFillType());
+
+ if ($pFill->getFillType() !== Fill::FILL_NONE) {
+ // fgColor
+ if ($pFill->getStartColor()->getARGB()) {
+ $objWriter->startElement('fgColor');
+ $objWriter->writeAttribute('rgb', $pFill->getStartColor()->getARGB());
+ $objWriter->endElement();
+ }
+ }
+ if ($pFill->getFillType() !== Fill::FILL_NONE) {
+ // bgColor
+ if ($pFill->getEndColor()->getARGB()) {
+ $objWriter->startElement('bgColor');
+ $objWriter->writeAttribute('rgb', $pFill->getEndColor()->getARGB());
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Font.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Font $pFont Font style
+ */
+ private function writeFont(XMLWriter $objWriter, Font $pFont): void
+ {
+ // font
+ $objWriter->startElement('font');
+ // Weird! The order of these elements actually makes a difference when opening Xlsx
+ // files in Excel2003 with the compatibility pack. It's not documented behaviour,
+ // and makes for a real WTF!
+
+ // Bold. We explicitly write this element also when false (like MS Office Excel 2007 does
+ // for conditional formatting). Otherwise it will apparently not be picked up in conditional
+ // formatting style dialog
+ if ($pFont->getBold() !== null) {
+ $objWriter->startElement('b');
+ $objWriter->writeAttribute('val', $pFont->getBold() ? '1' : '0');
+ $objWriter->endElement();
+ }
+
+ // Italic
+ if ($pFont->getItalic() !== null) {
+ $objWriter->startElement('i');
+ $objWriter->writeAttribute('val', $pFont->getItalic() ? '1' : '0');
+ $objWriter->endElement();
+ }
+
+ // Strikethrough
+ if ($pFont->getStrikethrough() !== null) {
+ $objWriter->startElement('strike');
+ $objWriter->writeAttribute('val', $pFont->getStrikethrough() ? '1' : '0');
+ $objWriter->endElement();
+ }
+
+ // Underline
+ if ($pFont->getUnderline() !== null) {
+ $objWriter->startElement('u');
+ $objWriter->writeAttribute('val', $pFont->getUnderline());
+ $objWriter->endElement();
+ }
+
+ // Superscript / subscript
+ if ($pFont->getSuperscript() === true || $pFont->getSubscript() === true) {
+ $objWriter->startElement('vertAlign');
+ if ($pFont->getSuperscript() === true) {
+ $objWriter->writeAttribute('val', 'superscript');
+ } elseif ($pFont->getSubscript() === true) {
+ $objWriter->writeAttribute('val', 'subscript');
+ }
+ $objWriter->endElement();
+ }
+
+ // Size
+ if ($pFont->getSize() !== null) {
+ $objWriter->startElement('sz');
+ $objWriter->writeAttribute('val', StringHelper::formatNumber($pFont->getSize()));
+ $objWriter->endElement();
+ }
+
+ // Foreground color
+ if ($pFont->getColor()->getARGB() !== null) {
+ $objWriter->startElement('color');
+ $objWriter->writeAttribute('rgb', $pFont->getColor()->getARGB());
+ $objWriter->endElement();
+ }
+
+ // Name
+ if ($pFont->getName() !== null) {
+ $objWriter->startElement('name');
+ $objWriter->writeAttribute('val', $pFont->getName());
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Border.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param Borders $pBorders Borders style
+ */
+ private function writeBorder(XMLWriter $objWriter, Borders $pBorders): void
+ {
+ // Write border
+ $objWriter->startElement('border');
+ // Diagonal?
+ switch ($pBorders->getDiagonalDirection()) {
+ case Borders::DIAGONAL_UP:
+ $objWriter->writeAttribute('diagonalUp', 'true');
+ $objWriter->writeAttribute('diagonalDown', 'false');
+
+ break;
+ case Borders::DIAGONAL_DOWN:
+ $objWriter->writeAttribute('diagonalUp', 'false');
+ $objWriter->writeAttribute('diagonalDown', 'true');
+
+ break;
+ case Borders::DIAGONAL_BOTH:
+ $objWriter->writeAttribute('diagonalUp', 'true');
+ $objWriter->writeAttribute('diagonalDown', 'true');
+
+ break;
+ }
+
+ // BorderPr
+ $this->writeBorderPr($objWriter, 'left', $pBorders->getLeft());
+ $this->writeBorderPr($objWriter, 'right', $pBorders->getRight());
+ $this->writeBorderPr($objWriter, 'top', $pBorders->getTop());
+ $this->writeBorderPr($objWriter, 'bottom', $pBorders->getBottom());
+ $this->writeBorderPr($objWriter, 'diagonal', $pBorders->getDiagonal());
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Cell Style Xf.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param \PhpOffice\PhpSpreadsheet\Style\Style $pStyle Style
+ * @param Spreadsheet $spreadsheet Workbook
+ */
+ private function writeCellStyleXf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $pStyle, Spreadsheet $spreadsheet): void
+ {
+ // xf
+ $objWriter->startElement('xf');
+ $objWriter->writeAttribute('xfId', 0);
+ $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($pStyle->getFont()->getHashCode()));
+ if ($pStyle->getQuotePrefix()) {
+ $objWriter->writeAttribute('quotePrefix', 1);
+ }
+
+ if ($pStyle->getNumberFormat()->getBuiltInFormatCode() === false) {
+ $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($pStyle->getNumberFormat()->getHashCode()) + 164));
+ } else {
+ $objWriter->writeAttribute('numFmtId', (int) $pStyle->getNumberFormat()->getBuiltInFormatCode());
+ }
+
+ $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($pStyle->getFill()->getHashCode()));
+ $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($pStyle->getBorders()->getHashCode()));
+
+ // Apply styles?
+ $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $pStyle->getFont()->getHashCode()) ? '1' : '0');
+ $objWriter->writeAttribute('applyNumberFormat', ($spreadsheet->getDefaultStyle()->getNumberFormat()->getHashCode() != $pStyle->getNumberFormat()->getHashCode()) ? '1' : '0');
+ $objWriter->writeAttribute('applyFill', ($spreadsheet->getDefaultStyle()->getFill()->getHashCode() != $pStyle->getFill()->getHashCode()) ? '1' : '0');
+ $objWriter->writeAttribute('applyBorder', ($spreadsheet->getDefaultStyle()->getBorders()->getHashCode() != $pStyle->getBorders()->getHashCode()) ? '1' : '0');
+ $objWriter->writeAttribute('applyAlignment', ($spreadsheet->getDefaultStyle()->getAlignment()->getHashCode() != $pStyle->getAlignment()->getHashCode()) ? '1' : '0');
+ if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+ $objWriter->writeAttribute('applyProtection', 'true');
+ }
+
+ // alignment
+ $objWriter->startElement('alignment');
+ $objWriter->writeAttribute('horizontal', $pStyle->getAlignment()->getHorizontal());
+ $objWriter->writeAttribute('vertical', $pStyle->getAlignment()->getVertical());
+
+ $textRotation = 0;
+ if ($pStyle->getAlignment()->getTextRotation() >= 0) {
+ $textRotation = $pStyle->getAlignment()->getTextRotation();
+ } elseif ($pStyle->getAlignment()->getTextRotation() < 0) {
+ $textRotation = 90 - $pStyle->getAlignment()->getTextRotation();
+ }
+ $objWriter->writeAttribute('textRotation', $textRotation);
+
+ $objWriter->writeAttribute('wrapText', ($pStyle->getAlignment()->getWrapText() ? 'true' : 'false'));
+ $objWriter->writeAttribute('shrinkToFit', ($pStyle->getAlignment()->getShrinkToFit() ? 'true' : 'false'));
+
+ if ($pStyle->getAlignment()->getIndent() > 0) {
+ $objWriter->writeAttribute('indent', $pStyle->getAlignment()->getIndent());
+ }
+ if ($pStyle->getAlignment()->getReadOrder() > 0) {
+ $objWriter->writeAttribute('readingOrder', $pStyle->getAlignment()->getReadOrder());
+ }
+ $objWriter->endElement();
+
+ // protection
+ if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+ $objWriter->startElement('protection');
+ if ($pStyle->getProtection()->getLocked() != Protection::PROTECTION_INHERIT) {
+ $objWriter->writeAttribute('locked', ($pStyle->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+ }
+ if ($pStyle->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+ $objWriter->writeAttribute('hidden', ($pStyle->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+ }
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Cell Style Dxf.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param \PhpOffice\PhpSpreadsheet\Style\Style $pStyle Style
+ */
+ private function writeCellStyleDxf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $pStyle): void
+ {
+ // dxf
+ $objWriter->startElement('dxf');
+
+ // font
+ $this->writeFont($objWriter, $pStyle->getFont());
+
+ // numFmt
+ $this->writeNumFmt($objWriter, $pStyle->getNumberFormat());
+
+ // fill
+ $this->writeFill($objWriter, $pStyle->getFill());
+
+ // alignment
+ $objWriter->startElement('alignment');
+ if ($pStyle->getAlignment()->getHorizontal() !== null) {
+ $objWriter->writeAttribute('horizontal', $pStyle->getAlignment()->getHorizontal());
+ }
+ if ($pStyle->getAlignment()->getVertical() !== null) {
+ $objWriter->writeAttribute('vertical', $pStyle->getAlignment()->getVertical());
+ }
+
+ if ($pStyle->getAlignment()->getTextRotation() !== null) {
+ $textRotation = 0;
+ if ($pStyle->getAlignment()->getTextRotation() >= 0) {
+ $textRotation = $pStyle->getAlignment()->getTextRotation();
+ } elseif ($pStyle->getAlignment()->getTextRotation() < 0) {
+ $textRotation = 90 - $pStyle->getAlignment()->getTextRotation();
+ }
+ $objWriter->writeAttribute('textRotation', $textRotation);
+ }
+ $objWriter->endElement();
+
+ // border
+ $this->writeBorder($objWriter, $pStyle->getBorders());
+
+ // protection
+ if (($pStyle->getProtection()->getLocked() !== null) || ($pStyle->getProtection()->getHidden() !== null)) {
+ if (
+ $pStyle->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT ||
+ $pStyle->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT
+ ) {
+ $objWriter->startElement('protection');
+ if (
+ ($pStyle->getProtection()->getLocked() !== null) &&
+ ($pStyle->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT)
+ ) {
+ $objWriter->writeAttribute('locked', ($pStyle->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+ }
+ if (
+ ($pStyle->getProtection()->getHidden() !== null) &&
+ ($pStyle->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT)
+ ) {
+ $objWriter->writeAttribute('hidden', ($pStyle->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+ }
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write BorderPr.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pName Element name
+ * @param Border $pBorder Border style
+ */
+ private function writeBorderPr(XMLWriter $objWriter, $pName, Border $pBorder): void
+ {
+ // Write BorderPr
+ if ($pBorder->getBorderStyle() != Border::BORDER_NONE) {
+ $objWriter->startElement($pName);
+ $objWriter->writeAttribute('style', $pBorder->getBorderStyle());
+
+ // color
+ $objWriter->startElement('color');
+ $objWriter->writeAttribute('rgb', $pBorder->getColor()->getARGB());
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write NumberFormat.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param NumberFormat $pNumberFormat Number Format
+ * @param int $pId Number Format identifier
+ */
+ private function writeNumFmt(XMLWriter $objWriter, NumberFormat $pNumberFormat, $pId = 0): void
+ {
+ // Translate formatcode
+ $formatCode = $pNumberFormat->getFormatCode();
+
+ // numFmt
+ if ($formatCode !== null) {
+ $objWriter->startElement('numFmt');
+ $objWriter->writeAttribute('numFmtId', ($pId + 164));
+ $objWriter->writeAttribute('formatCode', $formatCode);
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Get an array of all styles.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Style\Style[] All styles in PhpSpreadsheet
+ */
+ public function allStyles(Spreadsheet $spreadsheet)
+ {
+ return $spreadsheet->getCellXfCollection();
+ }
+
+ /**
+ * Get an array of all conditional styles.
+ *
+ * @return Conditional[] All conditional styles in PhpSpreadsheet
+ */
+ public function allConditionalStyles(Spreadsheet $spreadsheet)
+ {
+ // Get an array of all styles
+ $aStyles = [];
+
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ foreach ($spreadsheet->getSheet($i)->getConditionalStylesCollection() as $conditionalStyles) {
+ foreach ($conditionalStyles as $conditionalStyle) {
+ $aStyles[] = $conditionalStyle;
+ }
+ }
+ }
+
+ return $aStyles;
+ }
+
+ /**
+ * Get an array of all fills.
+ *
+ * @return Fill[] All fills in PhpSpreadsheet
+ */
+ public function allFills(Spreadsheet $spreadsheet)
+ {
+ // Get an array of unique fills
+ $aFills = [];
+
+ // Two first fills are predefined
+ $fill0 = new Fill();
+ $fill0->setFillType(Fill::FILL_NONE);
+ $aFills[] = $fill0;
+
+ $fill1 = new Fill();
+ $fill1->setFillType(Fill::FILL_PATTERN_GRAY125);
+ $aFills[] = $fill1;
+ // The remaining fills
+ $aStyles = $this->allStyles($spreadsheet);
+ /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+ foreach ($aStyles as $style) {
+ if (!isset($aFills[$style->getFill()->getHashCode()])) {
+ $aFills[$style->getFill()->getHashCode()] = $style->getFill();
+ }
+ }
+
+ return $aFills;
+ }
+
+ /**
+ * Get an array of all fonts.
+ *
+ * @return Font[] All fonts in PhpSpreadsheet
+ */
+ public function allFonts(Spreadsheet $spreadsheet)
+ {
+ // Get an array of unique fonts
+ $aFonts = [];
+ $aStyles = $this->allStyles($spreadsheet);
+
+ /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+ foreach ($aStyles as $style) {
+ if (!isset($aFonts[$style->getFont()->getHashCode()])) {
+ $aFonts[$style->getFont()->getHashCode()] = $style->getFont();
+ }
+ }
+
+ return $aFonts;
+ }
+
+ /**
+ * Get an array of all borders.
+ *
+ * @return Borders[] All borders in PhpSpreadsheet
+ */
+ public function allBorders(Spreadsheet $spreadsheet)
+ {
+ // Get an array of unique borders
+ $aBorders = [];
+ $aStyles = $this->allStyles($spreadsheet);
+
+ /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+ foreach ($aStyles as $style) {
+ if (!isset($aBorders[$style->getBorders()->getHashCode()])) {
+ $aBorders[$style->getBorders()->getHashCode()] = $style->getBorders();
+ }
+ }
+
+ return $aBorders;
+ }
+
+ /**
+ * Get an array of all number formats.
+ *
+ * @return NumberFormat[] All number formats in PhpSpreadsheet
+ */
+ public function allNumberFormats(Spreadsheet $spreadsheet)
+ {
+ // Get an array of unique number formats
+ $aNumFmts = [];
+ $aStyles = $this->allStyles($spreadsheet);
+
+ /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+ foreach ($aStyles as $style) {
+ if ($style->getNumberFormat()->getBuiltInFormatCode() === false && !isset($aNumFmts[$style->getNumberFormat()->getHashCode()])) {
+ $aNumFmts[$style->getNumberFormat()->getHashCode()] = $style->getNumberFormat();
+ }
+ }
+
+ return $aNumFmts;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Theme.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Theme.php
new file mode 100644
index 0000000..9917729
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Theme.php
@@ -0,0 +1,829 @@
+ 'MS Pゴシック',
+ 'Hang' => '맑은 고딕',
+ 'Hans' => '宋体',
+ 'Hant' => '新細明體',
+ 'Arab' => 'Times New Roman',
+ 'Hebr' => 'Times New Roman',
+ 'Thai' => 'Tahoma',
+ 'Ethi' => 'Nyala',
+ 'Beng' => 'Vrinda',
+ 'Gujr' => 'Shruti',
+ 'Khmr' => 'MoolBoran',
+ 'Knda' => 'Tunga',
+ 'Guru' => 'Raavi',
+ 'Cans' => 'Euphemia',
+ 'Cher' => 'Plantagenet Cherokee',
+ 'Yiii' => 'Microsoft Yi Baiti',
+ 'Tibt' => 'Microsoft Himalaya',
+ 'Thaa' => 'MV Boli',
+ 'Deva' => 'Mangal',
+ 'Telu' => 'Gautami',
+ 'Taml' => 'Latha',
+ 'Syrc' => 'Estrangelo Edessa',
+ 'Orya' => 'Kalinga',
+ 'Mlym' => 'Kartika',
+ 'Laoo' => 'DokChampa',
+ 'Sinh' => 'Iskoola Pota',
+ 'Mong' => 'Mongolian Baiti',
+ 'Viet' => 'Times New Roman',
+ 'Uigh' => 'Microsoft Uighur',
+ 'Geor' => 'Sylfaen',
+ ];
+
+ /**
+ * Map of Minor fonts to write.
+ *
+ * @var string[]
+ */
+ private static $minorFonts = [
+ 'Jpan' => 'MS Pゴシック',
+ 'Hang' => '맑은 고딕',
+ 'Hans' => '宋体',
+ 'Hant' => '新細明體',
+ 'Arab' => 'Arial',
+ 'Hebr' => 'Arial',
+ 'Thai' => 'Tahoma',
+ 'Ethi' => 'Nyala',
+ 'Beng' => 'Vrinda',
+ 'Gujr' => 'Shruti',
+ 'Khmr' => 'DaunPenh',
+ 'Knda' => 'Tunga',
+ 'Guru' => 'Raavi',
+ 'Cans' => 'Euphemia',
+ 'Cher' => 'Plantagenet Cherokee',
+ 'Yiii' => 'Microsoft Yi Baiti',
+ 'Tibt' => 'Microsoft Himalaya',
+ 'Thaa' => 'MV Boli',
+ 'Deva' => 'Mangal',
+ 'Telu' => 'Gautami',
+ 'Taml' => 'Latha',
+ 'Syrc' => 'Estrangelo Edessa',
+ 'Orya' => 'Kalinga',
+ 'Mlym' => 'Kartika',
+ 'Laoo' => 'DokChampa',
+ 'Sinh' => 'Iskoola Pota',
+ 'Mong' => 'Mongolian Baiti',
+ 'Viet' => 'Arial',
+ 'Uigh' => 'Microsoft Uighur',
+ 'Geor' => 'Sylfaen',
+ ];
+
+ /**
+ * Map of core colours.
+ *
+ * @var string[]
+ */
+ private static $colourScheme = [
+ 'dk2' => '1F497D',
+ 'lt2' => 'EEECE1',
+ 'accent1' => '4F81BD',
+ 'accent2' => 'C0504D',
+ 'accent3' => '9BBB59',
+ 'accent4' => '8064A2',
+ 'accent5' => '4BACC6',
+ 'accent6' => 'F79646',
+ 'hlink' => '0000FF',
+ 'folHlink' => '800080',
+ ];
+
+ /**
+ * Write theme to XML format.
+ *
+ * @return string XML Output
+ */
+ public function writeTheme(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');
+
+ // a:theme
+ $objWriter->startElement('a:theme');
+ $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
+ $objWriter->writeAttribute('name', 'Office Theme');
+
+ // a:themeElements
+ $objWriter->startElement('a:themeElements');
+
+ // a:clrScheme
+ $objWriter->startElement('a:clrScheme');
+ $objWriter->writeAttribute('name', 'Office');
+
+ // a:dk1
+ $objWriter->startElement('a:dk1');
+
+ // a:sysClr
+ $objWriter->startElement('a:sysClr');
+ $objWriter->writeAttribute('val', 'windowText');
+ $objWriter->writeAttribute('lastClr', '000000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:lt1
+ $objWriter->startElement('a:lt1');
+
+ // a:sysClr
+ $objWriter->startElement('a:sysClr');
+ $objWriter->writeAttribute('val', 'window');
+ $objWriter->writeAttribute('lastClr', 'FFFFFF');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:dk2
+ $this->writeColourScheme($objWriter);
+
+ $objWriter->endElement();
+
+ // a:fontScheme
+ $objWriter->startElement('a:fontScheme');
+ $objWriter->writeAttribute('name', 'Office');
+
+ // a:majorFont
+ $objWriter->startElement('a:majorFont');
+ $this->writeFonts($objWriter, 'Cambria', self::$majorFonts);
+ $objWriter->endElement();
+
+ // a:minorFont
+ $objWriter->startElement('a:minorFont');
+ $this->writeFonts($objWriter, 'Calibri', self::$minorFonts);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:fmtScheme
+ $objWriter->startElement('a:fmtScheme');
+ $objWriter->writeAttribute('name', 'Office');
+
+ // a:fillStyleLst
+ $objWriter->startElement('a:fillStyleLst');
+
+ // a:solidFill
+ $objWriter->startElement('a:solidFill');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gradFill
+ $objWriter->startElement('a:gradFill');
+ $objWriter->writeAttribute('rotWithShape', '1');
+
+ // a:gsLst
+ $objWriter->startElement('a:gsLst');
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '0');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '50000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '300000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '35000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '37000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '300000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '100000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '15000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '350000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:lin
+ $objWriter->startElement('a:lin');
+ $objWriter->writeAttribute('ang', '16200000');
+ $objWriter->writeAttribute('scaled', '1');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gradFill
+ $objWriter->startElement('a:gradFill');
+ $objWriter->writeAttribute('rotWithShape', '1');
+
+ // a:gsLst
+ $objWriter->startElement('a:gsLst');
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '0');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '51000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '130000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '80000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '93000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '130000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '100000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '94000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '135000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:lin
+ $objWriter->startElement('a:lin');
+ $objWriter->writeAttribute('ang', '16200000');
+ $objWriter->writeAttribute('scaled', '0');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:lnStyleLst
+ $objWriter->startElement('a:lnStyleLst');
+
+ // a:ln
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', '9525');
+ $objWriter->writeAttribute('cap', 'flat');
+ $objWriter->writeAttribute('cmpd', 'sng');
+ $objWriter->writeAttribute('algn', 'ctr');
+
+ // a:solidFill
+ $objWriter->startElement('a:solidFill');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '95000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '105000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:prstDash
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', 'solid');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:ln
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', '25400');
+ $objWriter->writeAttribute('cap', 'flat');
+ $objWriter->writeAttribute('cmpd', 'sng');
+ $objWriter->writeAttribute('algn', 'ctr');
+
+ // a:solidFill
+ $objWriter->startElement('a:solidFill');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:prstDash
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', 'solid');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:ln
+ $objWriter->startElement('a:ln');
+ $objWriter->writeAttribute('w', '38100');
+ $objWriter->writeAttribute('cap', 'flat');
+ $objWriter->writeAttribute('cmpd', 'sng');
+ $objWriter->writeAttribute('algn', 'ctr');
+
+ // a:solidFill
+ $objWriter->startElement('a:solidFill');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:prstDash
+ $objWriter->startElement('a:prstDash');
+ $objWriter->writeAttribute('val', 'solid');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:effectStyleLst
+ $objWriter->startElement('a:effectStyleLst');
+
+ // a:effectStyle
+ $objWriter->startElement('a:effectStyle');
+
+ // a:effectLst
+ $objWriter->startElement('a:effectLst');
+
+ // a:outerShdw
+ $objWriter->startElement('a:outerShdw');
+ $objWriter->writeAttribute('blurRad', '40000');
+ $objWriter->writeAttribute('dist', '20000');
+ $objWriter->writeAttribute('dir', '5400000');
+ $objWriter->writeAttribute('rotWithShape', '0');
+
+ // a:srgbClr
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', '000000');
+
+ // a:alpha
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', '38000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:effectStyle
+ $objWriter->startElement('a:effectStyle');
+
+ // a:effectLst
+ $objWriter->startElement('a:effectLst');
+
+ // a:outerShdw
+ $objWriter->startElement('a:outerShdw');
+ $objWriter->writeAttribute('blurRad', '40000');
+ $objWriter->writeAttribute('dist', '23000');
+ $objWriter->writeAttribute('dir', '5400000');
+ $objWriter->writeAttribute('rotWithShape', '0');
+
+ // a:srgbClr
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', '000000');
+
+ // a:alpha
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', '35000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:effectStyle
+ $objWriter->startElement('a:effectStyle');
+
+ // a:effectLst
+ $objWriter->startElement('a:effectLst');
+
+ // a:outerShdw
+ $objWriter->startElement('a:outerShdw');
+ $objWriter->writeAttribute('blurRad', '40000');
+ $objWriter->writeAttribute('dist', '23000');
+ $objWriter->writeAttribute('dir', '5400000');
+ $objWriter->writeAttribute('rotWithShape', '0');
+
+ // a:srgbClr
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', '000000');
+
+ // a:alpha
+ $objWriter->startElement('a:alpha');
+ $objWriter->writeAttribute('val', '35000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:scene3d
+ $objWriter->startElement('a:scene3d');
+
+ // a:camera
+ $objWriter->startElement('a:camera');
+ $objWriter->writeAttribute('prst', 'orthographicFront');
+
+ // a:rot
+ $objWriter->startElement('a:rot');
+ $objWriter->writeAttribute('lat', '0');
+ $objWriter->writeAttribute('lon', '0');
+ $objWriter->writeAttribute('rev', '0');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:lightRig
+ $objWriter->startElement('a:lightRig');
+ $objWriter->writeAttribute('rig', 'threePt');
+ $objWriter->writeAttribute('dir', 't');
+
+ // a:rot
+ $objWriter->startElement('a:rot');
+ $objWriter->writeAttribute('lat', '0');
+ $objWriter->writeAttribute('lon', '0');
+ $objWriter->writeAttribute('rev', '1200000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:sp3d
+ $objWriter->startElement('a:sp3d');
+
+ // a:bevelT
+ $objWriter->startElement('a:bevelT');
+ $objWriter->writeAttribute('w', '63500');
+ $objWriter->writeAttribute('h', '25400');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:bgFillStyleLst
+ $objWriter->startElement('a:bgFillStyleLst');
+
+ // a:solidFill
+ $objWriter->startElement('a:solidFill');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gradFill
+ $objWriter->startElement('a:gradFill');
+ $objWriter->writeAttribute('rotWithShape', '1');
+
+ // a:gsLst
+ $objWriter->startElement('a:gsLst');
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '0');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '40000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '350000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '40000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '45000');
+ $objWriter->endElement();
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '99000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '350000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '100000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '20000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '255000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:path
+ $objWriter->startElement('a:path');
+ $objWriter->writeAttribute('path', 'circle');
+
+ // a:fillToRect
+ $objWriter->startElement('a:fillToRect');
+ $objWriter->writeAttribute('l', '50000');
+ $objWriter->writeAttribute('t', '-80000');
+ $objWriter->writeAttribute('r', '50000');
+ $objWriter->writeAttribute('b', '180000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gradFill
+ $objWriter->startElement('a:gradFill');
+ $objWriter->writeAttribute('rotWithShape', '1');
+
+ // a:gsLst
+ $objWriter->startElement('a:gsLst');
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '0');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:tint
+ $objWriter->startElement('a:tint');
+ $objWriter->writeAttribute('val', '80000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '300000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:gs
+ $objWriter->startElement('a:gs');
+ $objWriter->writeAttribute('pos', '100000');
+
+ // a:schemeClr
+ $objWriter->startElement('a:schemeClr');
+ $objWriter->writeAttribute('val', 'phClr');
+
+ // a:shade
+ $objWriter->startElement('a:shade');
+ $objWriter->writeAttribute('val', '30000');
+ $objWriter->endElement();
+
+ // a:satMod
+ $objWriter->startElement('a:satMod');
+ $objWriter->writeAttribute('val', '200000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:path
+ $objWriter->startElement('a:path');
+ $objWriter->writeAttribute('path', 'circle');
+
+ // a:fillToRect
+ $objWriter->startElement('a:fillToRect');
+ $objWriter->writeAttribute('l', '50000');
+ $objWriter->writeAttribute('t', '50000');
+ $objWriter->writeAttribute('r', '50000');
+ $objWriter->writeAttribute('b', '50000');
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ // a:objectDefaults
+ $objWriter->writeElement('a:objectDefaults', null);
+
+ // a:extraClrSchemeLst
+ $objWriter->writeElement('a:extraClrSchemeLst', null);
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write fonts to XML format.
+ *
+ * @param string[] $fontSet
+ */
+ private function writeFonts(XMLWriter $objWriter, string $latinFont, array $fontSet): void
+ {
+ // a:latin
+ $objWriter->startElement('a:latin');
+ $objWriter->writeAttribute('typeface', $latinFont);
+ $objWriter->endElement();
+
+ // a:ea
+ $objWriter->startElement('a:ea');
+ $objWriter->writeAttribute('typeface', '');
+ $objWriter->endElement();
+
+ // a:cs
+ $objWriter->startElement('a:cs');
+ $objWriter->writeAttribute('typeface', '');
+ $objWriter->endElement();
+
+ foreach ($fontSet as $fontScript => $typeface) {
+ $objWriter->startElement('a:font');
+ $objWriter->writeAttribute('script', $fontScript);
+ $objWriter->writeAttribute('typeface', $typeface);
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write colour scheme to XML format.
+ */
+ private function writeColourScheme(XMLWriter $objWriter): void
+ {
+ foreach (self::$colourScheme as $colourName => $colourValue) {
+ $objWriter->startElement('a:' . $colourName);
+
+ $objWriter->startElement('a:srgbClr');
+ $objWriter->writeAttribute('val', $colourValue);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
new file mode 100644
index 0000000..0a20ea9
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
@@ -0,0 +1,225 @@
+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');
+
+ // workbook
+ $objWriter->startElement('workbook');
+ $objWriter->writeAttribute('xml:space', 'preserve');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
+ $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
+
+ // fileVersion
+ $this->writeFileVersion($objWriter);
+
+ // workbookPr
+ $this->writeWorkbookPr($objWriter);
+
+ // workbookProtection
+ $this->writeWorkbookProtection($objWriter, $spreadsheet);
+
+ // bookViews
+ if ($this->getParentWriter()->getOffice2003Compatibility() === false) {
+ $this->writeBookViews($objWriter, $spreadsheet);
+ }
+
+ // sheets
+ $this->writeSheets($objWriter, $spreadsheet);
+
+ // definedNames
+ (new DefinedNamesWriter($objWriter, $spreadsheet))->write();
+
+ // calcPr
+ $this->writeCalcPr($objWriter, $recalcRequired);
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write file version.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeFileVersion(XMLWriter $objWriter): void
+ {
+ $objWriter->startElement('fileVersion');
+ $objWriter->writeAttribute('appName', 'xl');
+ $objWriter->writeAttribute('lastEdited', '4');
+ $objWriter->writeAttribute('lowestEdited', '4');
+ $objWriter->writeAttribute('rupBuild', '4505');
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write WorkbookPr.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeWorkbookPr(XMLWriter $objWriter): void
+ {
+ $objWriter->startElement('workbookPr');
+
+ if (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) {
+ $objWriter->writeAttribute('date1904', '1');
+ }
+
+ $objWriter->writeAttribute('codeName', 'ThisWorkbook');
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write BookViews.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeBookViews(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+ {
+ // bookViews
+ $objWriter->startElement('bookViews');
+
+ // workbookView
+ $objWriter->startElement('workbookView');
+
+ $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex());
+ $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false'));
+ $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex());
+ $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false'));
+ $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false'));
+ $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false'));
+ $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false'));
+ $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio());
+ $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility());
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write WorkbookProtection.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+ {
+ if ($spreadsheet->getSecurity()->isSecurityEnabled()) {
+ $objWriter->startElement('workbookProtection');
+ $objWriter->writeAttribute('lockRevision', ($spreadsheet->getSecurity()->getLockRevision() ? 'true' : 'false'));
+ $objWriter->writeAttribute('lockStructure', ($spreadsheet->getSecurity()->getLockStructure() ? 'true' : 'false'));
+ $objWriter->writeAttribute('lockWindows', ($spreadsheet->getSecurity()->getLockWindows() ? 'true' : 'false'));
+
+ if ($spreadsheet->getSecurity()->getRevisionsPassword() != '') {
+ $objWriter->writeAttribute('revisionsPassword', $spreadsheet->getSecurity()->getRevisionsPassword());
+ }
+
+ if ($spreadsheet->getSecurity()->getWorkbookPassword() != '') {
+ $objWriter->writeAttribute('workbookPassword', $spreadsheet->getSecurity()->getWorkbookPassword());
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write calcPr.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param bool $recalcRequired Indicate whether formulas should be recalculated before writing
+ */
+ private function writeCalcPr(XMLWriter $objWriter, $recalcRequired = true): void
+ {
+ $objWriter->startElement('calcPr');
+
+ // Set the calcid to a higher value than Excel itself will use, otherwise Excel will always recalc
+ // If MS Excel does do a recalc, then users opening a file in MS Excel will be prompted to save on exit
+ // because the file has changed
+ $objWriter->writeAttribute('calcId', '999999');
+ $objWriter->writeAttribute('calcMode', 'auto');
+ // fullCalcOnLoad isn't needed if we've recalculating for the save
+ $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0);
+ $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1);
+ $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1);
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write sheets.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ */
+ private function writeSheets(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+ {
+ // Write sheets
+ $objWriter->startElement('sheets');
+ $sheetCount = $spreadsheet->getSheetCount();
+ for ($i = 0; $i < $sheetCount; ++$i) {
+ // sheet
+ $this->writeSheet(
+ $objWriter,
+ $spreadsheet->getSheet($i)->getTitle(),
+ ($i + 1),
+ ($i + 1 + 3),
+ $spreadsheet->getSheet($i)->getSheetState()
+ );
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write sheet.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param string $pSheetname Sheet name
+ * @param int $pSheetId Sheet id
+ * @param int $pRelId Relationship ID
+ * @param string $sheetState Sheet state (visible, hidden, veryHidden)
+ */
+ private function writeSheet(XMLWriter $objWriter, $pSheetname, $pSheetId = 1, $pRelId = 1, $sheetState = 'visible'): void
+ {
+ if ($pSheetname != '') {
+ // Write sheet
+ $objWriter->startElement('sheet');
+ $objWriter->writeAttribute('name', $pSheetname);
+ $objWriter->writeAttribute('sheetId', $pSheetId);
+ if ($sheetState !== 'visible' && $sheetState != '') {
+ $objWriter->writeAttribute('state', $sheetState);
+ }
+ $objWriter->writeAttribute('r:id', 'rId' . $pRelId);
+ $objWriter->endElement();
+ } else {
+ throw new WriterException('Invalid parameters passed.');
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
new file mode 100644
index 0000000..38dead4
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
@@ -0,0 +1,1426 @@
+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');
+
+ // Worksheet
+ $objWriter->startElement('worksheet');
+ $objWriter->writeAttribute('xml:space', 'preserve');
+ $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
+ $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
+
+ $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
+ $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main');
+ $objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main');
+ $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006');
+ $objWriter->writeAttribute('mc:Ignorable', 'x14ac');
+ $objWriter->writeAttribute('xmlns:x14ac', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac');
+
+ // sheetPr
+ $this->writeSheetPr($objWriter, $pSheet);
+
+ // Dimension
+ $this->writeDimension($objWriter, $pSheet);
+
+ // sheetViews
+ $this->writeSheetViews($objWriter, $pSheet);
+
+ // sheetFormatPr
+ $this->writeSheetFormatPr($objWriter, $pSheet);
+
+ // cols
+ $this->writeCols($objWriter, $pSheet);
+
+ // sheetData
+ $this->writeSheetData($objWriter, $pSheet, $pStringTable);
+
+ // sheetProtection
+ $this->writeSheetProtection($objWriter, $pSheet);
+
+ // protectedRanges
+ $this->writeProtectedRanges($objWriter, $pSheet);
+
+ // autoFilter
+ $this->writeAutoFilter($objWriter, $pSheet);
+
+ // mergeCells
+ $this->writeMergeCells($objWriter, $pSheet);
+
+ // conditionalFormatting
+ $this->writeConditionalFormatting($objWriter, $pSheet);
+
+ // dataValidations
+ $this->writeDataValidations($objWriter, $pSheet);
+
+ // hyperlinks
+ $this->writeHyperlinks($objWriter, $pSheet);
+
+ // Print options
+ $this->writePrintOptions($objWriter, $pSheet);
+
+ // Page margins
+ $this->writePageMargins($objWriter, $pSheet);
+
+ // Page setup
+ $this->writePageSetup($objWriter, $pSheet);
+
+ // Header / footer
+ $this->writeHeaderFooter($objWriter, $pSheet);
+
+ // Breaks
+ $this->writeBreaks($objWriter, $pSheet);
+
+ // Drawings and/or Charts
+ $this->writeDrawings($objWriter, $pSheet, $includeCharts);
+
+ // LegacyDrawing
+ $this->writeLegacyDrawing($objWriter, $pSheet);
+
+ // LegacyDrawingHF
+ $this->writeLegacyDrawingHF($objWriter, $pSheet);
+
+ // AlternateContent
+ $this->writeAlternateContent($objWriter, $pSheet);
+
+ // ConditionalFormattingRuleExtensionList
+ // (Must be inserted last. Not insert last, an Excel parse error will occur)
+ $this->writeExtLst($objWriter, $pSheet);
+
+ $objWriter->endElement();
+
+ // Return
+ return $objWriter->getData();
+ }
+
+ /**
+ * Write SheetPr.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // sheetPr
+ $objWriter->startElement('sheetPr');
+ if ($pSheet->getParent()->hasMacros()) {
+ //if the workbook have macros, we need to have codeName for the sheet
+ if (!$pSheet->hasCodeName()) {
+ $pSheet->setCodeName($pSheet->getTitle());
+ }
+ $objWriter->writeAttribute('codeName', $pSheet->getCodeName());
+ }
+ $autoFilterRange = $pSheet->getAutoFilter()->getRange();
+ if (!empty($autoFilterRange)) {
+ $objWriter->writeAttribute('filterMode', 1);
+ $pSheet->getAutoFilter()->showHideRows();
+ }
+
+ // tabColor
+ if ($pSheet->isTabColorSet()) {
+ $objWriter->startElement('tabColor');
+ $objWriter->writeAttribute('rgb', $pSheet->getTabColor()->getARGB());
+ $objWriter->endElement();
+ }
+
+ // outlinePr
+ $objWriter->startElement('outlinePr');
+ $objWriter->writeAttribute('summaryBelow', ($pSheet->getShowSummaryBelow() ? '1' : '0'));
+ $objWriter->writeAttribute('summaryRight', ($pSheet->getShowSummaryRight() ? '1' : '0'));
+ $objWriter->endElement();
+
+ // pageSetUpPr
+ if ($pSheet->getPageSetup()->getFitToPage()) {
+ $objWriter->startElement('pageSetUpPr');
+ $objWriter->writeAttribute('fitToPage', '1');
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Dimension.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeDimension(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // dimension
+ $objWriter->startElement('dimension');
+ $objWriter->writeAttribute('ref', $pSheet->calculateWorksheetDimension());
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write SheetViews.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // sheetViews
+ $objWriter->startElement('sheetViews');
+
+ // Sheet selected?
+ $sheetSelected = false;
+ if ($this->getParentWriter()->getSpreadsheet()->getIndex($pSheet) == $this->getParentWriter()->getSpreadsheet()->getActiveSheetIndex()) {
+ $sheetSelected = true;
+ }
+
+ // sheetView
+ $objWriter->startElement('sheetView');
+ $objWriter->writeAttribute('tabSelected', $sheetSelected ? '1' : '0');
+ $objWriter->writeAttribute('workbookViewId', '0');
+
+ // Zoom scales
+ if ($pSheet->getSheetView()->getZoomScale() != 100) {
+ $objWriter->writeAttribute('zoomScale', $pSheet->getSheetView()->getZoomScale());
+ }
+ if ($pSheet->getSheetView()->getZoomScaleNormal() != 100) {
+ $objWriter->writeAttribute('zoomScaleNormal', $pSheet->getSheetView()->getZoomScaleNormal());
+ }
+
+ // Show zeros (Excel also writes this attribute only if set to false)
+ if ($pSheet->getSheetView()->getShowZeros() === false) {
+ $objWriter->writeAttribute('showZeros', 0);
+ }
+
+ // View Layout Type
+ if ($pSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_NORMAL) {
+ $objWriter->writeAttribute('view', $pSheet->getSheetView()->getView());
+ }
+
+ // Gridlines
+ if ($pSheet->getShowGridlines()) {
+ $objWriter->writeAttribute('showGridLines', 'true');
+ } else {
+ $objWriter->writeAttribute('showGridLines', 'false');
+ }
+
+ // Row and column headers
+ if ($pSheet->getShowRowColHeaders()) {
+ $objWriter->writeAttribute('showRowColHeaders', '1');
+ } else {
+ $objWriter->writeAttribute('showRowColHeaders', '0');
+ }
+
+ // Right-to-left
+ if ($pSheet->getRightToLeft()) {
+ $objWriter->writeAttribute('rightToLeft', 'true');
+ }
+
+ $activeCell = $pSheet->getActiveCell();
+ $sqref = $pSheet->getSelectedCells();
+
+ // Pane
+ $pane = '';
+ if ($pSheet->getFreezePane()) {
+ [$xSplit, $ySplit] = Coordinate::coordinateFromString($pSheet->getFreezePane());
+ $xSplit = Coordinate::columnIndexFromString($xSplit);
+ --$xSplit;
+ --$ySplit;
+
+ $topLeftCell = $pSheet->getTopLeftCell();
+
+ // pane
+ $pane = 'topRight';
+ $objWriter->startElement('pane');
+ if ($xSplit > 0) {
+ $objWriter->writeAttribute('xSplit', $xSplit);
+ }
+ if ($ySplit > 0) {
+ $objWriter->writeAttribute('ySplit', $ySplit);
+ $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft';
+ }
+ $objWriter->writeAttribute('topLeftCell', $topLeftCell);
+ $objWriter->writeAttribute('activePane', $pane);
+ $objWriter->writeAttribute('state', 'frozen');
+ $objWriter->endElement();
+
+ if (($xSplit > 0) && ($ySplit > 0)) {
+ // Write additional selections if more than two panes (ie both an X and a Y split)
+ $objWriter->startElement('selection');
+ $objWriter->writeAttribute('pane', 'topRight');
+ $objWriter->endElement();
+ $objWriter->startElement('selection');
+ $objWriter->writeAttribute('pane', 'bottomLeft');
+ $objWriter->endElement();
+ }
+ }
+
+ // Selection
+ // Only need to write selection element if we have a split pane
+ // We cheat a little by over-riding the active cell selection, setting it to the split cell
+ $objWriter->startElement('selection');
+ if ($pane != '') {
+ $objWriter->writeAttribute('pane', $pane);
+ }
+ $objWriter->writeAttribute('activeCell', $activeCell);
+ $objWriter->writeAttribute('sqref', $sqref);
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write SheetFormatPr.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeSheetFormatPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // sheetFormatPr
+ $objWriter->startElement('sheetFormatPr');
+
+ // Default row height
+ if ($pSheet->getDefaultRowDimension()->getRowHeight() >= 0) {
+ $objWriter->writeAttribute('customHeight', 'true');
+ $objWriter->writeAttribute('defaultRowHeight', StringHelper::formatNumber($pSheet->getDefaultRowDimension()->getRowHeight()));
+ } else {
+ $objWriter->writeAttribute('defaultRowHeight', '14.4');
+ }
+
+ // Set Zero Height row
+ if (
+ (string) $pSheet->getDefaultRowDimension()->getZeroHeight() === '1' ||
+ strtolower((string) $pSheet->getDefaultRowDimension()->getZeroHeight()) == 'true'
+ ) {
+ $objWriter->writeAttribute('zeroHeight', '1');
+ }
+
+ // Default column width
+ if ($pSheet->getDefaultColumnDimension()->getWidth() >= 0) {
+ $objWriter->writeAttribute('defaultColWidth', StringHelper::formatNumber($pSheet->getDefaultColumnDimension()->getWidth()));
+ }
+
+ // Outline level - row
+ $outlineLevelRow = 0;
+ foreach ($pSheet->getRowDimensions() as $dimension) {
+ if ($dimension->getOutlineLevel() > $outlineLevelRow) {
+ $outlineLevelRow = $dimension->getOutlineLevel();
+ }
+ }
+ $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow);
+
+ // Outline level - column
+ $outlineLevelCol = 0;
+ foreach ($pSheet->getColumnDimensions() as $dimension) {
+ if ($dimension->getOutlineLevel() > $outlineLevelCol) {
+ $outlineLevelCol = $dimension->getOutlineLevel();
+ }
+ }
+ $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol);
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Cols.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeCols(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // cols
+ if (count($pSheet->getColumnDimensions()) > 0) {
+ $objWriter->startElement('cols');
+
+ $pSheet->calculateColumnWidths();
+
+ // Loop through column dimensions
+ foreach ($pSheet->getColumnDimensions() as $colDimension) {
+ // col
+ $objWriter->startElement('col');
+ $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
+ $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
+
+ if ($colDimension->getWidth() < 0) {
+ // No width set, apply default of 10
+ $objWriter->writeAttribute('width', '9.10');
+ } else {
+ // Width set
+ $objWriter->writeAttribute('width', StringHelper::formatNumber($colDimension->getWidth()));
+ }
+
+ // Column visibility
+ if ($colDimension->getVisible() === false) {
+ $objWriter->writeAttribute('hidden', 'true');
+ }
+
+ // Auto size?
+ if ($colDimension->getAutoSize()) {
+ $objWriter->writeAttribute('bestFit', 'true');
+ }
+
+ // Custom width?
+ if ($colDimension->getWidth() != $pSheet->getDefaultColumnDimension()->getWidth()) {
+ $objWriter->writeAttribute('customWidth', 'true');
+ }
+
+ // Collapsed
+ if ($colDimension->getCollapsed() === true) {
+ $objWriter->writeAttribute('collapsed', 'true');
+ }
+
+ // Outline level
+ if ($colDimension->getOutlineLevel() > 0) {
+ $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel());
+ }
+
+ // Style
+ $objWriter->writeAttribute('style', $colDimension->getXfIndex());
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write SheetProtection.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // sheetProtection
+ $objWriter->startElement('sheetProtection');
+
+ $protection = $pSheet->getProtection();
+
+ if ($protection->getAlgorithm()) {
+ $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm());
+ $objWriter->writeAttribute('hashValue', $protection->getPassword());
+ $objWriter->writeAttribute('saltValue', $protection->getSalt());
+ $objWriter->writeAttribute('spinCount', $protection->getSpinCount());
+ } elseif ($protection->getPassword() !== '') {
+ $objWriter->writeAttribute('password', $protection->getPassword());
+ }
+
+ $objWriter->writeAttribute('sheet', ($protection->getSheet() ? 'true' : 'false'));
+ $objWriter->writeAttribute('objects', ($protection->getObjects() ? 'true' : 'false'));
+ $objWriter->writeAttribute('scenarios', ($protection->getScenarios() ? 'true' : 'false'));
+ $objWriter->writeAttribute('formatCells', ($protection->getFormatCells() ? 'true' : 'false'));
+ $objWriter->writeAttribute('formatColumns', ($protection->getFormatColumns() ? 'true' : 'false'));
+ $objWriter->writeAttribute('formatRows', ($protection->getFormatRows() ? 'true' : 'false'));
+ $objWriter->writeAttribute('insertColumns', ($protection->getInsertColumns() ? 'true' : 'false'));
+ $objWriter->writeAttribute('insertRows', ($protection->getInsertRows() ? 'true' : 'false'));
+ $objWriter->writeAttribute('insertHyperlinks', ($protection->getInsertHyperlinks() ? 'true' : 'false'));
+ $objWriter->writeAttribute('deleteColumns', ($protection->getDeleteColumns() ? 'true' : 'false'));
+ $objWriter->writeAttribute('deleteRows', ($protection->getDeleteRows() ? 'true' : 'false'));
+ $objWriter->writeAttribute('selectLockedCells', ($protection->getSelectLockedCells() ? 'true' : 'false'));
+ $objWriter->writeAttribute('sort', ($protection->getSort() ? 'true' : 'false'));
+ $objWriter->writeAttribute('autoFilter', ($protection->getAutoFilter() ? 'true' : 'false'));
+ $objWriter->writeAttribute('pivotTables', ($protection->getPivotTables() ? 'true' : 'false'));
+ $objWriter->writeAttribute('selectUnlockedCells', ($protection->getSelectUnlockedCells() ? 'true' : 'false'));
+ $objWriter->endElement();
+ }
+
+ private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void
+ {
+ if ($condition) {
+ $objWriter->writeAttribute($attr, $val);
+ }
+ }
+
+ private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void
+ {
+ if ($condition) {
+ $objWriter->writeElement($attr, $val);
+ }
+ }
+
+ private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
+ {
+ if (
+ $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+ || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT
+ || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
+ ) {
+ foreach ($conditional->getConditions() as $formula) {
+ // Formula
+ $objWriter->writeElement('formula', Xlfn::addXlfn($formula));
+ }
+ } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
+ // formula copied from ms xlsx xml source file
+ $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0');
+ } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) {
+ // formula copied from ms xlsx xml source file
+ $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0');
+ }
+ }
+
+ private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
+ {
+ $txt = $conditional->getText();
+ if ($txt !== null) {
+ $objWriter->writeAttribute('text', $txt);
+ if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) {
+ $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))');
+ } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) {
+ $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"');
+ } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) {
+ $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"');
+ } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) {
+ $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))');
+ }
+ }
+ }
+
+ private static function writeExtConditionalFormattingElements(XMLWriter $objWriter, ConditionalFormattingRuleExtension $ruleExtension): void
+ {
+ $prefix = 'x14';
+ $objWriter->startElementNs($prefix, 'conditionalFormatting', null);
+
+ $objWriter->startElementNs($prefix, 'cfRule', null);
+ $objWriter->writeAttribute('type', $ruleExtension->getCfRule());
+ $objWriter->writeAttribute('id', $ruleExtension->getId());
+ $objWriter->startElementNs($prefix, 'dataBar', null);
+ $dataBar = $ruleExtension->getDataBarExt();
+ foreach ($dataBar->getXmlAttributes() as $attrKey => $val) {
+ $objWriter->writeAttribute($attrKey, $val);
+ }
+ $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
+ if ($minCfvo) {
+ $objWriter->startElementNs($prefix, 'cfvo', null);
+ $objWriter->writeAttribute('type', $minCfvo->getType());
+ if ($minCfvo->getCellFormula()) {
+ $objWriter->writeElement('xm:f', $minCfvo->getCellFormula());
+ }
+ $objWriter->endElement(); //end cfvo
+ }
+
+ $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
+ if ($maxCfvo) {
+ $objWriter->startElementNs($prefix, 'cfvo', null);
+ $objWriter->writeAttribute('type', $maxCfvo->getType());
+ if ($maxCfvo->getCellFormula()) {
+ $objWriter->writeElement('xm:f', $maxCfvo->getCellFormula());
+ }
+ $objWriter->endElement(); //end cfvo
+ }
+
+ foreach ($dataBar->getXmlElements() as $elmKey => $elmAttr) {
+ $objWriter->startElementNs($prefix, $elmKey, null);
+ foreach ($elmAttr as $attrKey => $attrVal) {
+ $objWriter->writeAttribute($attrKey, $attrVal);
+ }
+ $objWriter->endElement(); //end elmKey
+ }
+ $objWriter->endElement(); //end dataBar
+ $objWriter->endElement(); //end cfRule
+ $objWriter->writeElement('xm:sqref', $ruleExtension->getSqref());
+ $objWriter->endElement(); //end conditionalFormatting
+ }
+
+ private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void
+ {
+ /** @var ConditionalDataBar $dataBar */
+ if ($dataBar) {
+ $objWriter->startElement('dataBar');
+ self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0');
+
+ $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
+ if ($minCfvo) {
+ $objWriter->startElement('cfvo');
+ self::writeAttributeIf($objWriter, $minCfvo->getType(), 'type', (string) $minCfvo->getType());
+ self::writeAttributeIf($objWriter, $minCfvo->getValue(), 'val', (string) $minCfvo->getValue());
+ $objWriter->endElement();
+ }
+ $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
+ if ($maxCfvo) {
+ $objWriter->startElement('cfvo');
+ self::writeAttributeIf($objWriter, $maxCfvo->getType(), 'type', (string) $maxCfvo->getType());
+ self::writeAttributeIf($objWriter, $maxCfvo->getValue(), 'val', (string) $maxCfvo->getValue());
+ $objWriter->endElement();
+ }
+ if ($dataBar->getColor()) {
+ $objWriter->startElement('color');
+ $objWriter->writeAttribute('rgb', $dataBar->getColor());
+ $objWriter->endElement();
+ }
+ $objWriter->endElement(); // end dataBar
+
+ if ($dataBar->getConditionalFormattingRuleExt()) {
+ $objWriter->startElement('extLst');
+ $extension = $dataBar->getConditionalFormattingRuleExt();
+ $objWriter->startElement('ext');
+ $objWriter->writeAttribute('uri', '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}');
+ $objWriter->startElementNs('x14', 'id', null);
+ $objWriter->text($extension->getId());
+ $objWriter->endElement();
+ $objWriter->endElement();
+ $objWriter->endElement(); //end extLst
+ }
+ }
+ }
+
+ /**
+ * Write ConditionalFormatting.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // Conditional id
+ $id = 1;
+
+ // Loop through styles in the current worksheet
+ foreach ($pSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+ foreach ($conditionalStyles as $conditional) {
+ // WHY was this again?
+ // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') {
+ // continue;
+ // }
+ if ($conditional->getConditionType() != Conditional::CONDITION_NONE) {
+ // conditionalFormatting
+ $objWriter->startElement('conditionalFormatting');
+ $objWriter->writeAttribute('sqref', $cellCoordinate);
+
+ // cfRule
+ $objWriter->startElement('cfRule');
+ $objWriter->writeAttribute('type', $conditional->getConditionType());
+ self::writeAttributeIf(
+ $objWriter,
+ ($conditional->getConditionType() != Conditional::CONDITION_DATABAR),
+ 'dxfId',
+ $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
+ );
+ $objWriter->writeAttribute('priority', $id++);
+
+ self::writeAttributeif(
+ $objWriter,
+ (
+ $conditional->getConditionType() === Conditional::CONDITION_CELLIS
+ || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
+ || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
+ ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE,
+ 'operator',
+ $conditional->getOperatorType()
+ );
+
+ self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1');
+
+ if (
+ $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
+ || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
+ ) {
+ self::writeTextCondElements($objWriter, $conditional, $cellCoordinate);
+ } else {
+ self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate);
+ }
+
+ //
+ self::writeDataBarElements($objWriter, $conditional->getDataBar());
+
+ $objWriter->endElement(); //end cfRule
+
+ $objWriter->endElement();
+ }
+ }
+ }
+ }
+
+ /**
+ * Write DataValidations.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // Datavalidation collection
+ $dataValidationCollection = $pSheet->getDataValidationCollection();
+
+ // Write data validations?
+ if (!empty($dataValidationCollection)) {
+ $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection);
+ $objWriter->startElement('dataValidations');
+ $objWriter->writeAttribute('count', count($dataValidationCollection));
+
+ foreach ($dataValidationCollection as $coordinate => $dv) {
+ $objWriter->startElement('dataValidation');
+
+ if ($dv->getType() != '') {
+ $objWriter->writeAttribute('type', $dv->getType());
+ }
+
+ if ($dv->getErrorStyle() != '') {
+ $objWriter->writeAttribute('errorStyle', $dv->getErrorStyle());
+ }
+
+ if ($dv->getOperator() != '') {
+ $objWriter->writeAttribute('operator', $dv->getOperator());
+ }
+
+ $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0'));
+ $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0'));
+ $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0'));
+ $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0'));
+
+ if ($dv->getErrorTitle() !== '') {
+ $objWriter->writeAttribute('errorTitle', $dv->getErrorTitle());
+ }
+ if ($dv->getError() !== '') {
+ $objWriter->writeAttribute('error', $dv->getError());
+ }
+ if ($dv->getPromptTitle() !== '') {
+ $objWriter->writeAttribute('promptTitle', $dv->getPromptTitle());
+ }
+ if ($dv->getPrompt() !== '') {
+ $objWriter->writeAttribute('prompt', $dv->getPrompt());
+ }
+
+ $objWriter->writeAttribute('sqref', $coordinate);
+
+ if ($dv->getFormula1() !== '') {
+ $objWriter->writeElement('formula1', $dv->getFormula1());
+ }
+ if ($dv->getFormula2() !== '') {
+ $objWriter->writeElement('formula2', $dv->getFormula2());
+ }
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write Hyperlinks.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // Hyperlink collection
+ $hyperlinkCollection = $pSheet->getHyperlinkCollection();
+
+ // Relation ID
+ $relationId = 1;
+
+ // Write hyperlinks?
+ if (!empty($hyperlinkCollection)) {
+ $objWriter->startElement('hyperlinks');
+
+ foreach ($hyperlinkCollection as $coordinate => $hyperlink) {
+ $objWriter->startElement('hyperlink');
+
+ $objWriter->writeAttribute('ref', $coordinate);
+ if (!$hyperlink->isInternal()) {
+ $objWriter->writeAttribute('r:id', 'rId_hyperlink_' . $relationId);
+ ++$relationId;
+ } else {
+ $objWriter->writeAttribute('location', str_replace('sheet://', '', $hyperlink->getUrl()));
+ }
+
+ if ($hyperlink->getTooltip() !== '') {
+ $objWriter->writeAttribute('tooltip', $hyperlink->getTooltip());
+ $objWriter->writeAttribute('display', $hyperlink->getTooltip());
+ }
+
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write ProtectedRanges.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ if (count($pSheet->getProtectedCells()) > 0) {
+ // protectedRanges
+ $objWriter->startElement('protectedRanges');
+
+ // Loop protectedRanges
+ foreach ($pSheet->getProtectedCells() as $protectedCell => $passwordHash) {
+ // protectedRange
+ $objWriter->startElement('protectedRange');
+ $objWriter->writeAttribute('name', 'p' . md5($protectedCell));
+ $objWriter->writeAttribute('sqref', $protectedCell);
+ if (!empty($passwordHash)) {
+ $objWriter->writeAttribute('password', $passwordHash);
+ }
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write MergeCells.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeMergeCells(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ if (count($pSheet->getMergeCells()) > 0) {
+ // mergeCells
+ $objWriter->startElement('mergeCells');
+
+ // Loop mergeCells
+ foreach ($pSheet->getMergeCells() as $mergeCell) {
+ // mergeCell
+ $objWriter->startElement('mergeCell');
+ $objWriter->writeAttribute('ref', $mergeCell);
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write PrintOptions.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writePrintOptions(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // printOptions
+ $objWriter->startElement('printOptions');
+
+ $objWriter->writeAttribute('gridLines', ($pSheet->getPrintGridlines() ? 'true' : 'false'));
+ $objWriter->writeAttribute('gridLinesSet', 'true');
+
+ if ($pSheet->getPageSetup()->getHorizontalCentered()) {
+ $objWriter->writeAttribute('horizontalCentered', 'true');
+ }
+
+ if ($pSheet->getPageSetup()->getVerticalCentered()) {
+ $objWriter->writeAttribute('verticalCentered', 'true');
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write PageMargins.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writePageMargins(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // pageMargins
+ $objWriter->startElement('pageMargins');
+ $objWriter->writeAttribute('left', StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()));
+ $objWriter->writeAttribute('right', StringHelper::formatNumber($pSheet->getPageMargins()->getRight()));
+ $objWriter->writeAttribute('top', StringHelper::formatNumber($pSheet->getPageMargins()->getTop()));
+ $objWriter->writeAttribute('bottom', StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()));
+ $objWriter->writeAttribute('header', StringHelper::formatNumber($pSheet->getPageMargins()->getHeader()));
+ $objWriter->writeAttribute('footer', StringHelper::formatNumber($pSheet->getPageMargins()->getFooter()));
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write AutoFilter.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ $autoFilterRange = $pSheet->getAutoFilter()->getRange();
+ if (!empty($autoFilterRange)) {
+ // autoFilter
+ $objWriter->startElement('autoFilter');
+
+ // Strip any worksheet reference from the filter coordinates
+ $range = Coordinate::splitRange($autoFilterRange);
+ $range = $range[0];
+ // Strip any worksheet ref
+ [$ws, $range[0]] = PhpspreadsheetWorksheet::extractSheetTitle($range[0], true);
+ $range = implode(':', $range);
+
+ $objWriter->writeAttribute('ref', str_replace('$', '', $range));
+
+ $columns = $pSheet->getAutoFilter()->getColumns();
+ if (count($columns) > 0) {
+ foreach ($columns as $columnID => $column) {
+ $rules = $column->getRules();
+ if (count($rules) > 0) {
+ $objWriter->startElement('filterColumn');
+ $objWriter->writeAttribute('colId', $pSheet->getAutoFilter()->getColumnOffset($columnID));
+
+ $objWriter->startElement($column->getFilterType());
+ if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) {
+ $objWriter->writeAttribute('and', 1);
+ }
+
+ foreach ($rules as $rule) {
+ if (
+ ($column->getFilterType() === Column::AUTOFILTER_FILTERTYPE_FILTER) &&
+ ($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_EQUAL) &&
+ ($rule->getValue() === '')
+ ) {
+ // Filter rule for Blanks
+ $objWriter->writeAttribute('blank', 1);
+ } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) {
+ // Dynamic Filter Rule
+ $objWriter->writeAttribute('type', $rule->getGrouping());
+ $val = $column->getAttribute('val');
+ if ($val !== null) {
+ $objWriter->writeAttribute('val', $val);
+ }
+ $maxVal = $column->getAttribute('maxVal');
+ if ($maxVal !== null) {
+ $objWriter->writeAttribute('maxVal', $maxVal);
+ }
+ } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_TOPTENFILTER) {
+ // Top 10 Filter Rule
+ $objWriter->writeAttribute('val', $rule->getValue());
+ $objWriter->writeAttribute('percent', (($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) ? '1' : '0'));
+ $objWriter->writeAttribute('top', (($rule->getGrouping() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) ? '1' : '0'));
+ } else {
+ // Filter, DateGroupItem or CustomFilter
+ $objWriter->startElement($rule->getRuleType());
+
+ if ($rule->getOperator() !== Rule::AUTOFILTER_COLUMN_RULE_EQUAL) {
+ $objWriter->writeAttribute('operator', $rule->getOperator());
+ }
+ if ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DATEGROUP) {
+ // Date Group filters
+ foreach ($rule->getValue() as $key => $value) {
+ if ($value > '') {
+ $objWriter->writeAttribute($key, $value);
+ }
+ }
+ $objWriter->writeAttribute('dateTimeGrouping', $rule->getGrouping());
+ } else {
+ $objWriter->writeAttribute('val', $rule->getValue());
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+
+ $objWriter->endElement();
+ }
+ }
+ }
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write PageSetup.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // pageSetup
+ $objWriter->startElement('pageSetup');
+ $objWriter->writeAttribute('paperSize', $pSheet->getPageSetup()->getPaperSize());
+ $objWriter->writeAttribute('orientation', $pSheet->getPageSetup()->getOrientation());
+
+ if ($pSheet->getPageSetup()->getScale() !== null) {
+ $objWriter->writeAttribute('scale', $pSheet->getPageSetup()->getScale());
+ }
+ if ($pSheet->getPageSetup()->getFitToHeight() !== null) {
+ $objWriter->writeAttribute('fitToHeight', $pSheet->getPageSetup()->getFitToHeight());
+ } else {
+ $objWriter->writeAttribute('fitToHeight', '0');
+ }
+ if ($pSheet->getPageSetup()->getFitToWidth() !== null) {
+ $objWriter->writeAttribute('fitToWidth', $pSheet->getPageSetup()->getFitToWidth());
+ } else {
+ $objWriter->writeAttribute('fitToWidth', '0');
+ }
+ if ($pSheet->getPageSetup()->getFirstPageNumber() !== null) {
+ $objWriter->writeAttribute('firstPageNumber', $pSheet->getPageSetup()->getFirstPageNumber());
+ $objWriter->writeAttribute('useFirstPageNumber', '1');
+ }
+ $objWriter->writeAttribute('pageOrder', $pSheet->getPageSetup()->getPageOrder());
+
+ $getUnparsedLoadedData = $pSheet->getParent()->getUnparsedLoadedData();
+ if (isset($getUnparsedLoadedData['sheets'][$pSheet->getCodeName()]['pageSetupRelId'])) {
+ $objWriter->writeAttribute('r:id', $getUnparsedLoadedData['sheets'][$pSheet->getCodeName()]['pageSetupRelId']);
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Header / Footer.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // headerFooter
+ $objWriter->startElement('headerFooter');
+ $objWriter->writeAttribute('differentOddEven', ($pSheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false'));
+ $objWriter->writeAttribute('differentFirst', ($pSheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false'));
+ $objWriter->writeAttribute('scaleWithDoc', ($pSheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false'));
+ $objWriter->writeAttribute('alignWithMargins', ($pSheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false'));
+
+ $objWriter->writeElement('oddHeader', $pSheet->getHeaderFooter()->getOddHeader());
+ $objWriter->writeElement('oddFooter', $pSheet->getHeaderFooter()->getOddFooter());
+ $objWriter->writeElement('evenHeader', $pSheet->getHeaderFooter()->getEvenHeader());
+ $objWriter->writeElement('evenFooter', $pSheet->getHeaderFooter()->getEvenFooter());
+ $objWriter->writeElement('firstHeader', $pSheet->getHeaderFooter()->getFirstHeader());
+ $objWriter->writeElement('firstFooter', $pSheet->getHeaderFooter()->getFirstFooter());
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Breaks.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // Get row and column breaks
+ $aRowBreaks = [];
+ $aColumnBreaks = [];
+ foreach ($pSheet->getBreaks() as $cell => $breakType) {
+ if ($breakType == PhpspreadsheetWorksheet::BREAK_ROW) {
+ $aRowBreaks[] = $cell;
+ } elseif ($breakType == PhpspreadsheetWorksheet::BREAK_COLUMN) {
+ $aColumnBreaks[] = $cell;
+ }
+ }
+
+ // rowBreaks
+ if (!empty($aRowBreaks)) {
+ $objWriter->startElement('rowBreaks');
+ $objWriter->writeAttribute('count', count($aRowBreaks));
+ $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks));
+
+ foreach ($aRowBreaks as $cell) {
+ $coords = Coordinate::coordinateFromString($cell);
+
+ $objWriter->startElement('brk');
+ $objWriter->writeAttribute('id', $coords[1]);
+ $objWriter->writeAttribute('man', '1');
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+
+ // Second, write column breaks
+ if (!empty($aColumnBreaks)) {
+ $objWriter->startElement('colBreaks');
+ $objWriter->writeAttribute('count', count($aColumnBreaks));
+ $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks));
+
+ foreach ($aColumnBreaks as $cell) {
+ $coords = Coordinate::coordinateFromString($cell);
+
+ $objWriter->startElement('brk');
+ $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1);
+ $objWriter->writeAttribute('man', '1');
+ $objWriter->endElement();
+ }
+
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write SheetData.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ * @param string[] $pStringTable String table
+ */
+ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, array $pStringTable): void
+ {
+ // Flipped stringtable, for faster index searching
+ $aFlippedStringTable = $this->getParentWriter()->getWriterPartstringtable()->flipStringTable($pStringTable);
+
+ // sheetData
+ $objWriter->startElement('sheetData');
+
+ // Get column count
+ $colCount = Coordinate::columnIndexFromString($pSheet->getHighestColumn());
+
+ // Highest row number
+ $highestRow = $pSheet->getHighestRow();
+
+ // Loop through cells
+ $cellsByRow = [];
+ foreach ($pSheet->getCoordinates() as $coordinate) {
+ $cellAddress = Coordinate::coordinateFromString($coordinate);
+ $cellsByRow[$cellAddress[1]][] = $coordinate;
+ }
+
+ $currentRow = 0;
+ while ($currentRow++ < $highestRow) {
+ // Get row dimension
+ $rowDimension = $pSheet->getRowDimension($currentRow);
+
+ // Write current row?
+ $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() == false || $rowDimension->getCollapsed() == true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null;
+
+ if ($writeCurrentRow) {
+ // Start a new row
+ $objWriter->startElement('row');
+ $objWriter->writeAttribute('r', $currentRow);
+ $objWriter->writeAttribute('spans', '1:' . $colCount);
+
+ // Row dimensions
+ if ($rowDimension->getRowHeight() >= 0) {
+ $objWriter->writeAttribute('customHeight', '1');
+ $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
+ }
+
+ // Row visibility
+ if (!$rowDimension->getVisible() === true) {
+ $objWriter->writeAttribute('hidden', 'true');
+ }
+
+ // Collapsed
+ if ($rowDimension->getCollapsed() === true) {
+ $objWriter->writeAttribute('collapsed', 'true');
+ }
+
+ // Outline level
+ if ($rowDimension->getOutlineLevel() > 0) {
+ $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel());
+ }
+
+ // Style
+ if ($rowDimension->getXfIndex() !== null) {
+ $objWriter->writeAttribute('s', $rowDimension->getXfIndex());
+ $objWriter->writeAttribute('customFormat', '1');
+ }
+
+ // Write cells
+ if (isset($cellsByRow[$currentRow])) {
+ foreach ($cellsByRow[$currentRow] as $cellAddress) {
+ // Write cell
+ $this->writeCell($objWriter, $pSheet, $cellAddress, $aFlippedStringTable);
+ }
+ }
+
+ // End row
+ $objWriter->endElement();
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * @param RichText|string $cellValue
+ */
+ private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, $cellValue): void
+ {
+ $objWriter->writeAttribute('t', $mappedType);
+ if (!$cellValue instanceof RichText) {
+ $objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue)));
+ } elseif ($cellValue instanceof RichText) {
+ $objWriter->startElement('is');
+ $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue);
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * @param RichText|string $cellValue
+ * @param string[] $pFlippedStringTable
+ */
+ private function writeCellString(XMLWriter $objWriter, string $mappedType, $cellValue, array $pFlippedStringTable): void
+ {
+ $objWriter->writeAttribute('t', $mappedType);
+ if (!$cellValue instanceof RichText) {
+ self::writeElementIf($objWriter, isset($pFlippedStringTable[$cellValue]), 'v', $pFlippedStringTable[$cellValue] ?? '');
+ } else {
+ $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]);
+ }
+ }
+
+ /**
+ * @param float|int $cellValue
+ */
+ private function writeCellNumeric(XMLWriter $objWriter, $cellValue): void
+ {
+ //force a decimal to be written if the type is float
+ if (is_float($cellValue)) {
+ // force point as decimal separator in case current locale uses comma
+ $cellValue = str_replace(',', '.', (string) $cellValue);
+ if (strpos($cellValue, '.') === false) {
+ $cellValue = $cellValue . '.0';
+ }
+ }
+ $objWriter->writeElement('v', $cellValue);
+ }
+
+ private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void
+ {
+ $objWriter->writeAttribute('t', $mappedType);
+ $objWriter->writeElement('v', $cellValue ? '1' : '0');
+ }
+
+ private function writeCellError(XMLWriter $objWriter, string $mappedType, string $cellValue, string $formulaerr = '#NULL!'): void
+ {
+ $objWriter->writeAttribute('t', $mappedType);
+ $cellIsFormula = substr($cellValue, 0, 1) === '=';
+ self::writeElementIf($objWriter, $cellIsFormula, 'f', Xlfn::addXlfnStripEquals($cellValue));
+ $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
+ }
+
+ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $pCell): void
+ {
+ $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $pCell->getCalculatedValue() : $cellValue;
+ if (is_string($calculatedValue)) {
+ if (\PhpOffice\PhpSpreadsheet\Calculation\Functions::isError($calculatedValue)) {
+ $this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue);
+
+ return;
+ }
+ $objWriter->writeAttribute('t', 'str');
+ } elseif (is_bool($calculatedValue)) {
+ $objWriter->writeAttribute('t', 'b');
+ $calculatedValue = (int) $calculatedValue;
+ }
+ // array values are not yet supported
+ //$attributes = $pCell->getFormulaAttributes();
+ //if (($attributes['t'] ?? null) === 'array') {
+ // $objWriter->startElement('f');
+ // $objWriter->writeAttribute('t', 'array');
+ // $objWriter->writeAttribute('ref', $pCellAddress);
+ // $objWriter->writeAttribute('aca', '1');
+ // $objWriter->writeAttribute('ca', '1');
+ // $objWriter->text(substr($cellValue, 1));
+ // $objWriter->endElement();
+ //} else {
+ // $objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
+ //}
+ $objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
+ self::writeElementIf(
+ $objWriter,
+ $this->getParentWriter()->getOffice2003Compatibility() === false,
+ 'v',
+ ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#')
+ ? StringHelper::formatNumber($calculatedValue) : '0'
+ );
+ }
+
+ /**
+ * Write Cell.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ * @param string $pCellAddress Cell Address
+ * @param string[] $pFlippedStringTable String table (flipped), for faster index searching
+ */
+ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, string $pCellAddress, array $pFlippedStringTable): void
+ {
+ // Cell
+ $pCell = $pSheet->getCell($pCellAddress);
+ $objWriter->startElement('c');
+ $objWriter->writeAttribute('r', $pCellAddress);
+
+ // Sheet styles
+ $xfi = $pCell->getXfIndex();
+ self::writeAttributeIf($objWriter, $xfi, 's', $xfi);
+
+ // If cell value is supplied, write cell value
+ $cellValue = $pCell->getValue();
+ if (is_object($cellValue) || $cellValue !== '') {
+ // Map type
+ $mappedType = $pCell->getDataType();
+
+ // Write data depending on its type
+ switch (strtolower($mappedType)) {
+ case 'inlinestr': // Inline string
+ $this->writeCellInlineStr($objWriter, $mappedType, $cellValue);
+
+ break;
+ case 's': // String
+ $this->writeCellString($objWriter, $mappedType, $cellValue, $pFlippedStringTable);
+
+ break;
+ case 'f': // Formula
+ $this->writeCellFormula($objWriter, $cellValue, $pCell);
+
+ break;
+ case 'n': // Numeric
+ $this->writeCellNumeric($objWriter, $cellValue);
+
+ break;
+ case 'b': // Boolean
+ $this->writeCellBoolean($objWriter, $mappedType, $cellValue);
+
+ break;
+ case 'e': // Error
+ $this->writeCellError($objWriter, $mappedType, $cellValue);
+ }
+ }
+
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write Drawings.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ * @param bool $includeCharts Flag indicating if we should include drawing details for charts
+ */
+ private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, $includeCharts = false): void
+ {
+ $unparsedLoadedData = $pSheet->getParent()->getUnparsedLoadedData();
+ $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds']);
+ $chartCount = ($includeCharts) ? $pSheet->getChartCollection()->count() : 0;
+ if ($chartCount == 0 && $pSheet->getDrawingCollection()->count() == 0 && !$hasUnparsedDrawing) {
+ return;
+ }
+
+ // If sheet contains drawings, add the relationships
+ $objWriter->startElement('drawing');
+
+ $rId = 'rId1';
+ if (isset($unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds'])) {
+ $drawingOriginalIds = $unparsedLoadedData['sheets'][$pSheet->getCodeName()]['drawingOriginalIds'];
+ // take first. In future can be overriten
+ // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels::writeWorksheetRelationships)
+ $rId = reset($drawingOriginalIds);
+ }
+
+ $objWriter->writeAttribute('r:id', $rId);
+ $objWriter->endElement();
+ }
+
+ /**
+ * Write LegacyDrawing.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // If sheet contains comments, add the relationships
+ if (count($pSheet->getComments()) > 0) {
+ $objWriter->startElement('legacyDrawing');
+ $objWriter->writeAttribute('r:id', 'rId_comments_vml1');
+ $objWriter->endElement();
+ }
+ }
+
+ /**
+ * Write LegacyDrawingHF.
+ *
+ * @param XMLWriter $objWriter XML Writer
+ * @param PhpspreadsheetWorksheet $pSheet Worksheet
+ */
+ private function writeLegacyDrawingHF(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ // If sheet contains images, add the relationships
+ if (count($pSheet->getHeaderFooter()->getImages()) > 0) {
+ $objWriter->startElement('legacyDrawingHF');
+ $objWriter->writeAttribute('r:id', 'rId_headerfooter_vml1');
+ $objWriter->endElement();
+ }
+ }
+
+ private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ if (empty($pSheet->getParent()->getUnparsedLoadedData()['sheets'][$pSheet->getCodeName()]['AlternateContents'])) {
+ return;
+ }
+
+ foreach ($pSheet->getParent()->getUnparsedLoadedData()['sheets'][$pSheet->getCodeName()]['AlternateContents'] as $alternateContent) {
+ $objWriter->writeRaw($alternateContent);
+ }
+ }
+
+ /**
+ * write
+ * only implementation conditionalFormattings.
+ *
+ * @url https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
+ */
+ private function writeExtLst(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet): void
+ {
+ $conditionalFormattingRuleExtList = [];
+ foreach ($pSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+ /** @var Conditional $conditional */
+ foreach ($conditionalStyles as $conditional) {
+ $dataBar = $conditional->getDataBar();
+ if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) {
+ $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt();
+ }
+ }
+ }
+
+ if (count($conditionalFormattingRuleExtList) > 0) {
+ $conditionalFormattingRuleExtNsPrefix = 'x14';
+ $objWriter->startElement('extLst');
+ $objWriter->startElement('ext');
+ $objWriter->writeAttribute('uri', '{78C0D931-6437-407d-A8EE-F0AAD7539E65}');
+ $objWriter->startElementNs($conditionalFormattingRuleExtNsPrefix, 'conditionalFormattings', null);
+ foreach ($conditionalFormattingRuleExtList as $extension) {
+ self::writeExtConditionalFormattingElements($objWriter, $extension);
+ }
+ $objWriter->endElement(); //end conditionalFormattings
+ $objWriter->endElement(); //end ext
+ $objWriter->endElement(); //end extLst
+ }
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/WriterPart.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/WriterPart.php
new file mode 100644
index 0000000..a9137df
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/WriterPart.php
@@ -0,0 +1,33 @@
+parentWriter;
+ }
+
+ /**
+ * Set parent Xlsx object.
+ */
+ public function __construct(Xlsx $pWriter)
+ {
+ $this->parentWriter = $pWriter;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php
new file mode 100644
index 0000000..c88ef24
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php
@@ -0,0 +1,166 @@
+=5.3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/cache/src/CacheException.php b/vendor/psr/cache/src/CacheException.php
new file mode 100644
index 0000000..25288e6
--- /dev/null
+++ b/vendor/psr/cache/src/CacheException.php
@@ -0,0 +1,10 @@
+=5.3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/container/src/ContainerExceptionInterface.php b/vendor/psr/container/src/ContainerExceptionInterface.php
new file mode 100644
index 0000000..30d09d9
--- /dev/null
+++ b/vendor/psr/container/src/ContainerExceptionInterface.php
@@ -0,0 +1,13 @@
+=7.0.0",
+ "psr/http-message": "^1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/http-factory/src/RequestFactoryInterface.php b/vendor/psr/http-factory/src/RequestFactoryInterface.php
new file mode 100644
index 0000000..cb39a08
--- /dev/null
+++ b/vendor/psr/http-factory/src/RequestFactoryInterface.php
@@ -0,0 +1,18 @@
+=5.3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php
new file mode 100644
index 0000000..dd46e5e
--- /dev/null
+++ b/vendor/psr/http-message/src/MessageInterface.php
@@ -0,0 +1,187 @@
+getHeaders() as $name => $values) {
+ * echo $name . ": " . implode(", ", $values);
+ * }
+ *
+ * // Emit headers iteratively:
+ * foreach ($message->getHeaders() as $name => $values) {
+ * foreach ($values as $value) {
+ * header(sprintf('%s: %s', $name, $value), false);
+ * }
+ * }
+ *
+ * While header names are not case-sensitive, getHeaders() will preserve the
+ * exact case in which headers were originally specified.
+ *
+ * @return string[][] Returns an associative array of the message's headers. Each
+ * key MUST be a header name, and each value MUST be an array of strings
+ * for that header.
+ */
+ public function getHeaders();
+
+ /**
+ * Checks if a header exists by the given case-insensitive name.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return bool Returns true if any header names match the given header
+ * name using a case-insensitive string comparison. Returns false if
+ * no matching header name is found in the message.
+ */
+ public function hasHeader($name);
+
+ /**
+ * Retrieves a message header value by the given case-insensitive name.
+ *
+ * This method returns an array of all the header values of the given
+ * case-insensitive header name.
+ *
+ * If the header does not appear in the message, this method MUST return an
+ * empty array.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string[] An array of string values as provided for the given
+ * header. If the header does not appear in the message, this method MUST
+ * return an empty array.
+ */
+ public function getHeader($name);
+
+ /**
+ * Retrieves a comma-separated string of the values for a single header.
+ *
+ * This method returns all of the header values of the given
+ * case-insensitive header name as a string concatenated together using
+ * a comma.
+ *
+ * NOTE: Not all header values may be appropriately represented using
+ * comma concatenation. For such headers, use getHeader() instead
+ * and supply your own delimiter when concatenating.
+ *
+ * If the header does not appear in the message, this method MUST return
+ * an empty string.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string A string of values as provided for the given header
+ * concatenated together using a comma. If the header does not appear in
+ * the message, this method MUST return an empty string.
+ */
+ public function getHeaderLine($name);
+
+ /**
+ * Return an instance with the provided value replacing the specified header.
+ *
+ * While header names are case-insensitive, the casing of the header will
+ * be preserved by this function, and returned from getHeaders().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new and/or updated header and value.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withHeader($name, $value);
+
+ /**
+ * Return an instance with the specified header appended with the given value.
+ *
+ * Existing values for the specified header will be maintained. The new
+ * value(s) will be appended to the existing list. If the header did not
+ * exist previously, it will be added.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new header and/or value.
+ *
+ * @param string $name Case-insensitive header field name to add.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withAddedHeader($name, $value);
+
+ /**
+ * Return an instance without the specified header.
+ *
+ * Header resolution MUST be done without case-sensitivity.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the named header.
+ *
+ * @param string $name Case-insensitive header field name to remove.
+ * @return static
+ */
+ public function withoutHeader($name);
+
+ /**
+ * Gets the body of the message.
+ *
+ * @return StreamInterface Returns the body as a stream.
+ */
+ public function getBody();
+
+ /**
+ * Return an instance with the specified message body.
+ *
+ * The body MUST be a StreamInterface object.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return a new instance that has the
+ * new body stream.
+ *
+ * @param StreamInterface $body Body.
+ * @return static
+ * @throws \InvalidArgumentException When the body is not valid.
+ */
+ public function withBody(StreamInterface $body);
+}
diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php
new file mode 100644
index 0000000..a96d4fd
--- /dev/null
+++ b/vendor/psr/http-message/src/RequestInterface.php
@@ -0,0 +1,129 @@
+getQuery()`
+ * or from the `QUERY_STRING` server param.
+ *
+ * @return array
+ */
+ public function getQueryParams();
+
+ /**
+ * Return an instance with the specified query string arguments.
+ *
+ * These values SHOULD remain immutable over the course of the incoming
+ * request. They MAY be injected during instantiation, such as from PHP's
+ * $_GET superglobal, or MAY be derived from some other value such as the
+ * URI. In cases where the arguments are parsed from the URI, the data
+ * MUST be compatible with what PHP's parse_str() would return for
+ * purposes of how duplicate query parameters are handled, and how nested
+ * sets are handled.
+ *
+ * Setting query string arguments MUST NOT change the URI stored by the
+ * request, nor the values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated query string arguments.
+ *
+ * @param array $query Array of query string arguments, typically from
+ * $_GET.
+ * @return static
+ */
+ public function withQueryParams(array $query);
+
+ /**
+ * Retrieve normalized file upload data.
+ *
+ * This method returns upload metadata in a normalized tree, with each leaf
+ * an instance of Psr\Http\Message\UploadedFileInterface.
+ *
+ * These values MAY be prepared from $_FILES or the message body during
+ * instantiation, or MAY be injected via withUploadedFiles().
+ *
+ * @return array An array tree of UploadedFileInterface instances; an empty
+ * array MUST be returned if no data is present.
+ */
+ public function getUploadedFiles();
+
+ /**
+ * Create a new instance with the specified uploaded files.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+ * @return static
+ * @throws \InvalidArgumentException if an invalid structure is provided.
+ */
+ public function withUploadedFiles(array $uploadedFiles);
+
+ /**
+ * Retrieve any parameters provided in the request body.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this method MUST
+ * return the contents of $_POST.
+ *
+ * Otherwise, this method may return any results of deserializing
+ * the request body content; as parsing returns structured content, the
+ * potential types MUST be arrays or objects only. A null value indicates
+ * the absence of body content.
+ *
+ * @return null|array|object The deserialized body parameters, if any.
+ * These will typically be an array or object.
+ */
+ public function getParsedBody();
+
+ /**
+ * Return an instance with the specified body parameters.
+ *
+ * These MAY be injected during instantiation.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, use this method
+ * ONLY to inject the contents of $_POST.
+ *
+ * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+ * deserializing the request body content. Deserialization/parsing returns
+ * structured data, and, as such, this method ONLY accepts arrays or objects,
+ * or a null value if nothing was available to parse.
+ *
+ * As an example, if content negotiation determines that the request data
+ * is a JSON payload, this method could be used to create a request
+ * instance with the deserialized parameters.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param null|array|object $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return static
+ * @throws \InvalidArgumentException if an unsupported argument type is
+ * provided.
+ */
+ public function withParsedBody($data);
+
+ /**
+ * Retrieve attributes derived from the request.
+ *
+ * The request "attributes" may be used to allow injection of any
+ * parameters derived from the request: e.g., the results of path
+ * match operations; the results of decrypting cookies; the results of
+ * deserializing non-form-encoded message bodies; etc. Attributes
+ * will be application and request specific, and CAN be mutable.
+ *
+ * @return array Attributes derived from the request.
+ */
+ public function getAttributes();
+
+ /**
+ * Retrieve a single derived request attribute.
+ *
+ * Retrieves a single derived request attribute as described in
+ * getAttributes(). If the attribute has not been previously set, returns
+ * the default value as provided.
+ *
+ * This method obviates the need for a hasAttribute() method, as it allows
+ * specifying a default value to return if the attribute is not found.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $default Default value to return if the attribute does not exist.
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null);
+
+ /**
+ * Return an instance with the specified derived request attribute.
+ *
+ * This method allows setting a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return static
+ */
+ public function withAttribute($name, $value);
+
+ /**
+ * Return an instance that removes the specified derived request attribute.
+ *
+ * This method allows removing a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @return static
+ */
+ public function withoutAttribute($name);
+}
diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php
new file mode 100644
index 0000000..f68f391
--- /dev/null
+++ b/vendor/psr/http-message/src/StreamInterface.php
@@ -0,0 +1,158 @@
+
+ * [user-info@]host[:port]
+ *
+ *
+ * If the port component is not set or is the standard port for the current
+ * scheme, it SHOULD NOT be included.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.2
+ * @return string The URI authority, in "[user-info@]host[:port]" format.
+ */
+ public function getAuthority();
+
+ /**
+ * Retrieve the user information component of the URI.
+ *
+ * If no user information is present, this method MUST return an empty
+ * string.
+ *
+ * If a user is present in the URI, this will return that value;
+ * additionally, if the password is also present, it will be appended to the
+ * user value, with a colon (":") separating the values.
+ *
+ * The trailing "@" character is not part of the user information and MUST
+ * NOT be added.
+ *
+ * @return string The URI user information, in "username[:password]" format.
+ */
+ public function getUserInfo();
+
+ /**
+ * Retrieve the host component of the URI.
+ *
+ * If no host is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.2.2.
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+ * @return string The URI host.
+ */
+ public function getHost();
+
+ /**
+ * Retrieve the port component of the URI.
+ *
+ * If a port is present, and it is non-standard for the current scheme,
+ * this method MUST return it as an integer. If the port is the standard port
+ * used with the current scheme, this method SHOULD return null.
+ *
+ * If no port is present, and no scheme is present, this method MUST return
+ * a null value.
+ *
+ * If no port is present, but a scheme is present, this method MAY return
+ * the standard port for that scheme, but SHOULD return null.
+ *
+ * @return null|int The URI port.
+ */
+ public function getPort();
+
+ /**
+ * Retrieve the path component of the URI.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * Normally, the empty path "" and absolute path "/" are considered equal as
+ * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+ * do this normalization because in contexts with a trimmed base path, e.g.
+ * the front controller, this difference becomes significant. It's the task
+ * of the user to handle both "" and "/".
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.3.
+ *
+ * As an example, if the value should include a slash ("/") not intended as
+ * delimiter between path segments, that value MUST be passed in encoded
+ * form (e.g., "%2F") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.3
+ * @return string The URI path.
+ */
+ public function getPath();
+
+ /**
+ * Retrieve the query string of the URI.
+ *
+ * If no query string is present, this method MUST return an empty string.
+ *
+ * The leading "?" character is not part of the query and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.4.
+ *
+ * As an example, if a value in a key/value pair of the query string should
+ * include an ampersand ("&") not intended as a delimiter between values,
+ * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.4
+ * @return string The URI query string.
+ */
+ public function getQuery();
+
+ /**
+ * Retrieve the fragment component of the URI.
+ *
+ * If no fragment is present, this method MUST return an empty string.
+ *
+ * The leading "#" character is not part of the fragment and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.5.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.5
+ * @return string The URI fragment.
+ */
+ public function getFragment();
+
+ /**
+ * Return an instance with the specified scheme.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified scheme.
+ *
+ * Implementations MUST support the schemes "http" and "https" case
+ * insensitively, and MAY accommodate other schemes if required.
+ *
+ * An empty scheme is equivalent to removing the scheme.
+ *
+ * @param string $scheme The scheme to use with the new instance.
+ * @return static A new instance with the specified scheme.
+ * @throws \InvalidArgumentException for invalid or unsupported schemes.
+ */
+ public function withScheme($scheme);
+
+ /**
+ * Return an instance with the specified user information.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified user information.
+ *
+ * Password is optional, but the user information MUST include the
+ * user; an empty string for the user is equivalent to removing user
+ * information.
+ *
+ * @param string $user The user name to use for authority.
+ * @param null|string $password The password associated with $user.
+ * @return static A new instance with the specified user information.
+ */
+ public function withUserInfo($user, $password = null);
+
+ /**
+ * Return an instance with the specified host.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified host.
+ *
+ * An empty host value is equivalent to removing the host.
+ *
+ * @param string $host The hostname to use with the new instance.
+ * @return static A new instance with the specified host.
+ * @throws \InvalidArgumentException for invalid hostnames.
+ */
+ public function withHost($host);
+
+ /**
+ * Return an instance with the specified port.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified port.
+ *
+ * Implementations MUST raise an exception for ports outside the
+ * established TCP and UDP port ranges.
+ *
+ * A null value provided for the port is equivalent to removing the port
+ * information.
+ *
+ * @param null|int $port The port to use with the new instance; a null value
+ * removes the port information.
+ * @return static A new instance with the specified port.
+ * @throws \InvalidArgumentException for invalid ports.
+ */
+ public function withPort($port);
+
+ /**
+ * Return an instance with the specified path.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified path.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * If the path is intended to be domain-relative rather than path relative then
+ * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+ * are assumed to be relative to some base path known to the application or
+ * consumer.
+ *
+ * Users can provide both encoded and decoded path characters.
+ * Implementations ensure the correct encoding as outlined in getPath().
+ *
+ * @param string $path The path to use with the new instance.
+ * @return static A new instance with the specified path.
+ * @throws \InvalidArgumentException for invalid paths.
+ */
+ public function withPath($path);
+
+ /**
+ * Return an instance with the specified query string.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified query string.
+ *
+ * Users can provide both encoded and decoded query characters.
+ * Implementations ensure the correct encoding as outlined in getQuery().
+ *
+ * An empty query string value is equivalent to removing the query string.
+ *
+ * @param string $query The query string to use with the new instance.
+ * @return static A new instance with the specified query string.
+ * @throws \InvalidArgumentException for invalid query strings.
+ */
+ public function withQuery($query);
+
+ /**
+ * Return an instance with the specified URI fragment.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified URI fragment.
+ *
+ * Users can provide both encoded and decoded fragment characters.
+ * Implementations ensure the correct encoding as outlined in getFragment().
+ *
+ * An empty fragment value is equivalent to removing the fragment.
+ *
+ * @param string $fragment The fragment to use with the new instance.
+ * @return static A new instance with the specified fragment.
+ */
+ public function withFragment($fragment);
+
+ /**
+ * Return the string representation as a URI reference.
+ *
+ * Depending on which components of the URI are present, the resulting
+ * string is either a full URI or relative reference according to RFC 3986,
+ * Section 4.1. The method concatenates the various components of the URI,
+ * using the appropriate delimiters:
+ *
+ * - If a scheme is present, it MUST be suffixed by ":".
+ * - If an authority is present, it MUST be prefixed by "//".
+ * - The path can be concatenated without delimiters. But there are two
+ * cases where the path has to be adjusted to make the URI reference
+ * valid as PHP does not allow to throw an exception in __toString():
+ * - If the path is rootless and an authority is present, the path MUST
+ * be prefixed by "/".
+ * - If the path is starting with more than one "/" and no authority is
+ * present, the starting slashes MUST be reduced to one.
+ * - If a query is present, it MUST be prefixed by "?".
+ * - If a fragment is present, it MUST be prefixed by "#".
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-4.1
+ * @return string
+ */
+ public function __toString();
+}
diff --git a/vendor/psr/log/LICENSE b/vendor/psr/log/LICENSE
new file mode 100644
index 0000000..8a71cf7
--- /dev/null
+++ b/vendor/psr/log/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 PHP Framework Interoperability Group
+
+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/psr/log/Psr/Log/AbstractLogger.php b/vendor/psr/log/Psr/Log/AbstractLogger.php
new file mode 100644
index 0000000..e33e75a
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/AbstractLogger.php
@@ -0,0 +1,128 @@
+log(LogLevel::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function alert($message, array $context = array())
+ {
+ $this->log(LogLevel::ALERT, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function critical($message, array $context = array())
+ {
+ $this->log(LogLevel::CRITICAL, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function error($message, array $context = array())
+ {
+ $this->log(LogLevel::ERROR, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function warning($message, array $context = array())
+ {
+ $this->log(LogLevel::WARNING, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function notice($message, array $context = array())
+ {
+ $this->log(LogLevel::NOTICE, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function info($message, array $context = array())
+ {
+ $this->log(LogLevel::INFO, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function debug($message, array $context = array())
+ {
+ $this->log(LogLevel::DEBUG, $message, $context);
+ }
+}
diff --git a/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/vendor/psr/log/Psr/Log/InvalidArgumentException.php
new file mode 100644
index 0000000..8e76d23
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/InvalidArgumentException.php
@@ -0,0 +1,7 @@
+logger = $logger;
+ }
+}
diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/Psr/Log/LoggerInterface.php
new file mode 100644
index 0000000..c020c90
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/LoggerInterface.php
@@ -0,0 +1,125 @@
+log(LogLevel::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function alert($message, array $context = array())
+ {
+ $this->log(LogLevel::ALERT, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function critical($message, array $context = array())
+ {
+ $this->log(LogLevel::CRITICAL, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function error($message, array $context = array())
+ {
+ $this->log(LogLevel::ERROR, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function warning($message, array $context = array())
+ {
+ $this->log(LogLevel::WARNING, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function notice($message, array $context = array())
+ {
+ $this->log(LogLevel::NOTICE, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function info($message, array $context = array())
+ {
+ $this->log(LogLevel::INFO, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function debug($message, array $context = array())
+ {
+ $this->log(LogLevel::DEBUG, $message, $context);
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ *
+ * @throws \Psr\Log\InvalidArgumentException
+ */
+ abstract public function log($level, $message, array $context = array());
+}
diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/Psr/Log/NullLogger.php
new file mode 100644
index 0000000..34347cd
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/NullLogger.php
@@ -0,0 +1,30 @@
+logger) { }`
+ * blocks.
+ */
+class NullLogger extends AbstractLogger
+{
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ *
+ * @throws \Psr\Log\InvalidArgumentException
+ */
+ public function log($level, $message, array $context = array())
+ {
+ // noop
+ }
+}
diff --git a/vendor/psr/log/Psr/Log/Test/DummyTest.php b/vendor/psr/log/Psr/Log/Test/DummyTest.php
new file mode 100644
index 0000000..37ac246
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/Test/DummyTest.php
@@ -0,0 +1,18 @@
+ ".
+ *
+ * Example ->error('Foo') would yield "error Foo".
+ *
+ * @return string[]
+ */
+ abstract public function getLogs();
+
+ public function testImplements()
+ {
+ $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
+ }
+
+ /**
+ * @dataProvider provideLevelsAndMessages
+ */
+ public function testLogsAtAllLevels($level, $message)
+ {
+ $logger = $this->getLogger();
+ $logger->{$level}($message, array('user' => 'Bob'));
+ $logger->log($level, $message, array('user' => 'Bob'));
+
+ $expected = array(
+ $level.' message of level '.$level.' with context: Bob',
+ $level.' message of level '.$level.' with context: Bob',
+ );
+ $this->assertEquals($expected, $this->getLogs());
+ }
+
+ public function provideLevelsAndMessages()
+ {
+ return array(
+ LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
+ LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
+ LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
+ LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
+ LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
+ LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
+ LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
+ LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
+ );
+ }
+
+ /**
+ * @expectedException \Psr\Log\InvalidArgumentException
+ */
+ public function testThrowsOnInvalidLevel()
+ {
+ $logger = $this->getLogger();
+ $logger->log('invalid level', 'Foo');
+ }
+
+ public function testContextReplacement()
+ {
+ $logger = $this->getLogger();
+ $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
+
+ $expected = array('info {Message {nothing} Bob Bar a}');
+ $this->assertEquals($expected, $this->getLogs());
+ }
+
+ public function testObjectCastToString()
+ {
+ if (method_exists($this, 'createPartialMock')) {
+ $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
+ } else {
+ $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
+ }
+ $dummy->expects($this->once())
+ ->method('__toString')
+ ->will($this->returnValue('DUMMY'));
+
+ $this->getLogger()->warning($dummy);
+
+ $expected = array('warning DUMMY');
+ $this->assertEquals($expected, $this->getLogs());
+ }
+
+ public function testContextCanContainAnything()
+ {
+ $closed = fopen('php://memory', 'r');
+ fclose($closed);
+
+ $context = array(
+ 'bool' => true,
+ 'null' => null,
+ 'string' => 'Foo',
+ 'int' => 0,
+ 'float' => 0.5,
+ 'nested' => array('with object' => new DummyTest),
+ 'object' => new \DateTime,
+ 'resource' => fopen('php://memory', 'r'),
+ 'closed' => $closed,
+ );
+
+ $this->getLogger()->warning('Crazy context data', $context);
+
+ $expected = array('warning Crazy context data');
+ $this->assertEquals($expected, $this->getLogs());
+ }
+
+ public function testContextExceptionKeyCanBeExceptionOrOtherValues()
+ {
+ $logger = $this->getLogger();
+ $logger->warning('Random message', array('exception' => 'oops'));
+ $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
+
+ $expected = array(
+ 'warning Random message',
+ 'critical Uncaught Exception!'
+ );
+ $this->assertEquals($expected, $this->getLogs());
+ }
+}
diff --git a/vendor/psr/log/Psr/Log/Test/TestLogger.php b/vendor/psr/log/Psr/Log/Test/TestLogger.php
new file mode 100644
index 0000000..0a108f4
--- /dev/null
+++ b/vendor/psr/log/Psr/Log/Test/TestLogger.php
@@ -0,0 +1,147 @@
+ $level,
+ 'message' => $message,
+ 'context' => $context,
+ ];
+
+ $this->recordsByLevel[$record['level']][] = $record;
+ $this->records[] = $record;
+ }
+
+ public function hasRecords($level)
+ {
+ return isset($this->recordsByLevel[$level]);
+ }
+
+ public function hasRecord($record, $level)
+ {
+ if (is_string($record)) {
+ $record = ['message' => $record];
+ }
+ return $this->hasRecordThatPasses(function ($rec) use ($record) {
+ if ($rec['message'] !== $record['message']) {
+ return false;
+ }
+ if (isset($record['context']) && $rec['context'] !== $record['context']) {
+ return false;
+ }
+ return true;
+ }, $level);
+ }
+
+ public function hasRecordThatContains($message, $level)
+ {
+ return $this->hasRecordThatPasses(function ($rec) use ($message) {
+ return strpos($rec['message'], $message) !== false;
+ }, $level);
+ }
+
+ public function hasRecordThatMatches($regex, $level)
+ {
+ return $this->hasRecordThatPasses(function ($rec) use ($regex) {
+ return preg_match($regex, $rec['message']) > 0;
+ }, $level);
+ }
+
+ public function hasRecordThatPasses(callable $predicate, $level)
+ {
+ if (!isset($this->recordsByLevel[$level])) {
+ return false;
+ }
+ foreach ($this->recordsByLevel[$level] as $i => $rec) {
+ if (call_user_func($predicate, $rec, $i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function __call($method, $args)
+ {
+ if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
+ $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
+ $level = strtolower($matches[2]);
+ if (method_exists($this, $genericMethod)) {
+ $args[] = $level;
+ return call_user_func_array([$this, $genericMethod], $args);
+ }
+ }
+ throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
+ }
+
+ public function reset()
+ {
+ $this->records = [];
+ $this->recordsByLevel = [];
+ }
+}
diff --git a/vendor/psr/log/README.md b/vendor/psr/log/README.md
new file mode 100644
index 0000000..edbc917
--- /dev/null
+++ b/vendor/psr/log/README.md
@@ -0,0 +1,58 @@
+PSR Log
+=======
+
+This repository holds all interfaces/classes/traits related to
+[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
+
+Note that this is not a logger of its own. It is merely an interface that
+describes a logger. See the specification for more details.
+
+Installation
+------------
+
+```bash
+composer require psr/log
+```
+
+Usage
+-----
+
+If you need a logger, you can use the interface like this:
+
+```php
+logger = $logger;
+ }
+
+ public function doSomething()
+ {
+ if ($this->logger) {
+ $this->logger->info('Doing work');
+ }
+
+ try {
+ $this->doSomethingElse();
+ } catch (Exception $exception) {
+ $this->logger->error('Oh no!', array('exception' => $exception));
+ }
+
+ // do something useful
+ }
+}
+```
+
+You can then pick one of the implementations of the interface to get a logger.
+
+If you want to implement the interface, you can require this package and
+implement `Psr\Log\LoggerInterface` in your code. Please read the
+[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+for details.
diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json
new file mode 100644
index 0000000..f1f0850
--- /dev/null
+++ b/vendor/psr/log/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "psr/log",
+ "description": "Common interface for logging libraries",
+ "keywords": ["psr", "psr-3", "log"],
+ "homepage": "https://github.com/php-fig/log",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/simple-cache/.editorconfig b/vendor/psr/simple-cache/.editorconfig
new file mode 100644
index 0000000..308d493
--- /dev/null
+++ b/vendor/psr/simple-cache/.editorconfig
@@ -0,0 +1,12 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/vendor/psr/simple-cache/LICENSE.md b/vendor/psr/simple-cache/LICENSE.md
new file mode 100644
index 0000000..6d9da8c
--- /dev/null
+++ b/vendor/psr/simple-cache/LICENSE.md
@@ -0,0 +1,21 @@
+# The MIT License (MIT)
+
+Copyright (c) 2016 PHP Framework Interoperability Group
+
+> 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/psr/simple-cache/README.md b/vendor/psr/simple-cache/README.md
new file mode 100644
index 0000000..1c9f71f
--- /dev/null
+++ b/vendor/psr/simple-cache/README.md
@@ -0,0 +1,8 @@
+PHP FIG Simple Cache PSR
+========================
+
+This repository holds all interfaces related to PSR-16.
+
+Note that this is not a cache implementation of its own. It is merely an interface that describes a cache implementation. See [the specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) for more details.
+
+You can find implementations of the specification by looking for packages providing the [psr/simple-cache-implementation](https://packagist.org/providers/psr/simple-cache-implementation) virtual package.
diff --git a/vendor/psr/simple-cache/composer.json b/vendor/psr/simple-cache/composer.json
new file mode 100644
index 0000000..ca4a31a
--- /dev/null
+++ b/vendor/psr/simple-cache/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "psr/simple-cache",
+ "description": "Common interfaces for simple caching",
+ "keywords": ["psr", "psr-16", "cache", "simple-cache", "caching"],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
diff --git a/vendor/psr/simple-cache/src/CacheException.php b/vendor/psr/simple-cache/src/CacheException.php
new file mode 100644
index 0000000..4e5a06d
--- /dev/null
+++ b/vendor/psr/simple-cache/src/CacheException.php
@@ -0,0 +1,10 @@
+ value pairs. Cache keys that do not exist or are stale will have $default as value.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function getMultiple($keys, $default = null);
+
+ /**
+ * Persists a set of key => value pairs in the cache, with an optional TTL.
+ *
+ * @param iterable $values A list of key => value pairs for a multiple-set operation.
+ * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
+ * the driver supports TTL then the library may set a default value
+ * for it or let the driver take care of that.
+ *
+ * @return bool True on success and false on failure.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $values is neither an array nor a Traversable,
+ * or if any of the $values are not a legal value.
+ */
+ public function setMultiple($values, $ttl = null);
+
+ /**
+ * Deletes multiple cache items in a single operation.
+ *
+ * @param iterable $keys A list of string-based keys to be deleted.
+ *
+ * @return bool True if the items were successfully removed. False if there was an error.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function deleteMultiple($keys);
+
+ /**
+ * Determines whether an item is present in the cache.
+ *
+ * NOTE: It is recommended that has() is only to be used for cache warming type purposes
+ * and not to be used within your live applications operations for get/set, as this method
+ * is subject to a race condition where your has() will return true and immediately after,
+ * another script can remove it making the state of your app out of date.
+ *
+ * @param string $key The cache item key.
+ *
+ * @return bool
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if the $key string is not a legal value.
+ */
+ public function has($key);
+}
diff --git a/vendor/psr/simple-cache/src/InvalidArgumentException.php b/vendor/psr/simple-cache/src/InvalidArgumentException.php
new file mode 100644
index 0000000..f97bca0
--- /dev/null
+++ b/vendor/psr/simple-cache/src/InvalidArgumentException.php
@@ -0,0 +1,13 @@
+= 5.3.
+
+[![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders)
+[![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master)
+[![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders)
+[![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders)
+[![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders)
+
+
+This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php).
+
+## Install
+
+For PHP version **`>= 5.6`**:
+
+```
+composer require ralouphie/getallheaders
+```
+
+For PHP version **`< 5.6`**:
+
+```
+composer require ralouphie/getallheaders "^2"
+```
diff --git a/vendor/ralouphie/getallheaders/composer.json b/vendor/ralouphie/getallheaders/composer.json
new file mode 100644
index 0000000..de8ce62
--- /dev/null
+++ b/vendor/ralouphie/getallheaders/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "ralouphie/getallheaders",
+ "description": "A polyfill for getallheaders.",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5 || ^6.5",
+ "php-coveralls/php-coveralls": "^2.1"
+ },
+ "autoload": {
+ "files": ["src/getallheaders.php"]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "getallheaders\\Tests\\": "tests/"
+ }
+ }
+}
diff --git a/vendor/ralouphie/getallheaders/src/getallheaders.php b/vendor/ralouphie/getallheaders/src/getallheaders.php
new file mode 100644
index 0000000..c7285a5
--- /dev/null
+++ b/vendor/ralouphie/getallheaders/src/getallheaders.php
@@ -0,0 +1,46 @@
+ 'Content-Type',
+ 'CONTENT_LENGTH' => 'Content-Length',
+ 'CONTENT_MD5' => 'Content-Md5',
+ );
+
+ foreach ($_SERVER as $key => $value) {
+ if (substr($key, 0, 5) === 'HTTP_') {
+ $key = substr($key, 5);
+ if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) {
+ $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
+ $headers[$key] = $value;
+ }
+ } elseif (isset($copy_server[$key])) {
+ $headers[$copy_server[$key]] = $value;
+ }
+ }
+
+ if (!isset($headers['Authorization'])) {
+ if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
+ $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+ } elseif (isset($_SERVER['PHP_AUTH_USER'])) {
+ $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
+ $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass);
+ } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
+ $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
+ }
+ }
+
+ return $headers;
+ }
+
+}
diff --git a/vendor/services.php b/vendor/services.php
new file mode 100644
index 0000000..446dedc
--- /dev/null
+++ b/vendor/services.php
@@ -0,0 +1,5 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Ctype;
+
+/**
+ * Ctype implementation through regex.
+ *
+ * @internal
+ *
+ * @author Gert de Pagter
+ */
+final class Ctype
+{
+ /**
+ * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise.
+ *
+ * @see https://php.net/ctype-alnum
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_alnum($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is a letter, FALSE otherwise.
+ *
+ * @see https://php.net/ctype-alpha
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_alpha($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise.
+ *
+ * @see https://php.net/ctype-cntrl
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_cntrl($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
+ *
+ * @see https://php.net/ctype-digit
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_digit($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise.
+ *
+ * @see https://php.net/ctype-graph
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_graph($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is a lowercase letter.
+ *
+ * @see https://php.net/ctype-lower
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_lower($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all.
+ *
+ * @see https://php.net/ctype-print
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_print($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise.
+ *
+ * @see https://php.net/ctype-punct
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_punct($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters.
+ *
+ * @see https://php.net/ctype-space
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_space($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is an uppercase letter.
+ *
+ * @see https://php.net/ctype-upper
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_upper($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text);
+ }
+
+ /**
+ * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise.
+ *
+ * @see https://php.net/ctype-xdigit
+ *
+ * @param string|int $text
+ *
+ * @return bool
+ */
+ public static function ctype_xdigit($text)
+ {
+ $text = self::convert_int_to_char_for_ctype($text);
+
+ return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text);
+ }
+
+ /**
+ * Converts integers to their char versions according to normal ctype behaviour, if needed.
+ *
+ * If an integer between -128 and 255 inclusive is provided,
+ * it is interpreted as the ASCII value of a single character
+ * (negative values have 256 added in order to allow characters in the Extended ASCII range).
+ * Any other integer is interpreted as a string containing the decimal digits of the integer.
+ *
+ * @param string|int $int
+ *
+ * @return mixed
+ */
+ private static function convert_int_to_char_for_ctype($int)
+ {
+ if (!\is_int($int)) {
+ return $int;
+ }
+
+ if ($int < -128 || $int > 255) {
+ return (string) $int;
+ }
+
+ if ($int < 0) {
+ $int += 256;
+ }
+
+ return \chr($int);
+ }
+}
diff --git a/vendor/symfony/polyfill-ctype/LICENSE b/vendor/symfony/polyfill-ctype/LICENSE
new file mode 100644
index 0000000..3f853aa
--- /dev/null
+++ b/vendor/symfony/polyfill-ctype/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2018-2019 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.
diff --git a/vendor/symfony/polyfill-ctype/README.md b/vendor/symfony/polyfill-ctype/README.md
new file mode 100644
index 0000000..8add1ab
--- /dev/null
+++ b/vendor/symfony/polyfill-ctype/README.md
@@ -0,0 +1,12 @@
+Symfony Polyfill / Ctype
+========================
+
+This component provides `ctype_*` functions to users who run php versions without the ctype extension.
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/vendor/symfony/polyfill-ctype/bootstrap.php b/vendor/symfony/polyfill-ctype/bootstrap.php
new file mode 100644
index 0000000..d54524b
--- /dev/null
+++ b/vendor/symfony/polyfill-ctype/bootstrap.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Ctype as p;
+
+if (\PHP_VERSION_ID >= 80000) {
+ return require __DIR__.'/bootstrap80.php';
+}
+
+if (!function_exists('ctype_alnum')) {
+ function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
+}
+if (!function_exists('ctype_alpha')) {
+ function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
+}
+if (!function_exists('ctype_cntrl')) {
+ function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
+}
+if (!function_exists('ctype_digit')) {
+ function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
+}
+if (!function_exists('ctype_graph')) {
+ function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
+}
+if (!function_exists('ctype_lower')) {
+ function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
+}
+if (!function_exists('ctype_print')) {
+ function ctype_print($text) { return p\Ctype::ctype_print($text); }
+}
+if (!function_exists('ctype_punct')) {
+ function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
+}
+if (!function_exists('ctype_space')) {
+ function ctype_space($text) { return p\Ctype::ctype_space($text); }
+}
+if (!function_exists('ctype_upper')) {
+ function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
+}
+if (!function_exists('ctype_xdigit')) {
+ function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
+}
diff --git a/vendor/symfony/polyfill-ctype/bootstrap80.php b/vendor/symfony/polyfill-ctype/bootstrap80.php
new file mode 100644
index 0000000..ab2f861
--- /dev/null
+++ b/vendor/symfony/polyfill-ctype/bootstrap80.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Ctype as p;
+
+if (!function_exists('ctype_alnum')) {
+ function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); }
+}
+if (!function_exists('ctype_alpha')) {
+ function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); }
+}
+if (!function_exists('ctype_cntrl')) {
+ function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); }
+}
+if (!function_exists('ctype_digit')) {
+ function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); }
+}
+if (!function_exists('ctype_graph')) {
+ function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); }
+}
+if (!function_exists('ctype_lower')) {
+ function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); }
+}
+if (!function_exists('ctype_print')) {
+ function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); }
+}
+if (!function_exists('ctype_punct')) {
+ function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); }
+}
+if (!function_exists('ctype_space')) {
+ function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); }
+}
+if (!function_exists('ctype_upper')) {
+ function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); }
+}
+if (!function_exists('ctype_xdigit')) {
+ function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); }
+}
diff --git a/vendor/symfony/polyfill-ctype/composer.json b/vendor/symfony/polyfill-ctype/composer.json
new file mode 100644
index 0000000..f0621a3
--- /dev/null
+++ b/vendor/symfony/polyfill-ctype/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "symfony/polyfill-ctype",
+ "type": "library",
+ "description": "Symfony polyfill for ctype functions",
+ "keywords": ["polyfill", "compatibility", "portable", "ctype"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.1"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
+ "files": [ "bootstrap.php" ]
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE
new file mode 100644
index 0000000..adb8d6f
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2015-2019 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.
diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php
new file mode 100644
index 0000000..40a4456
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Mbstring.php
@@ -0,0 +1,847 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Mbstring;
+
+/**
+ * Partial mbstring implementation in PHP, iconv based, UTF-8 centric.
+ *
+ * Implemented:
+ * - mb_chr - Returns a specific character from its Unicode code point
+ * - mb_convert_encoding - Convert character encoding
+ * - mb_convert_variables - Convert character code in variable(s)
+ * - mb_decode_mimeheader - Decode string in MIME header field
+ * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
+ * - mb_decode_numericentity - Decode HTML numeric string reference to character
+ * - mb_encode_numericentity - Encode character to HTML numeric string reference
+ * - mb_convert_case - Perform case folding on a string
+ * - mb_detect_encoding - Detect character encoding
+ * - mb_get_info - Get internal settings of mbstring
+ * - mb_http_input - Detect HTTP input character encoding
+ * - mb_http_output - Set/Get HTTP output character encoding
+ * - mb_internal_encoding - Set/Get internal character encoding
+ * - mb_list_encodings - Returns an array of all supported encodings
+ * - mb_ord - Returns the Unicode code point of a character
+ * - mb_output_handler - Callback function converts character encoding in output buffer
+ * - mb_scrub - Replaces ill-formed byte sequences with substitute characters
+ * - mb_strlen - Get string length
+ * - mb_strpos - Find position of first occurrence of string in a string
+ * - mb_strrpos - Find position of last occurrence of a string in a string
+ * - mb_str_split - Convert a string to an array
+ * - mb_strtolower - Make a string lowercase
+ * - mb_strtoupper - Make a string uppercase
+ * - mb_substitute_character - Set/Get substitution character
+ * - mb_substr - Get part of string
+ * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive
+ * - mb_stristr - Finds first occurrence of a string within another, case insensitive
+ * - mb_strrchr - Finds the last occurrence of a character in a string within another
+ * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive
+ * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive
+ * - mb_strstr - Finds first occurrence of a string within another
+ * - mb_strwidth - Return width of string
+ * - mb_substr_count - Count the number of substring occurrences
+ *
+ * Not implemented:
+ * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
+ * - mb_ereg_* - Regular expression with multibyte support
+ * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable
+ * - mb_preferred_mime_name - Get MIME charset string
+ * - mb_regex_encoding - Returns current encoding for multibyte regex as string
+ * - mb_regex_set_options - Set/Get the default options for mbregex functions
+ * - mb_send_mail - Send encoded mail
+ * - mb_split - Split multibyte string using regular expression
+ * - mb_strcut - Get part of string
+ * - mb_strimwidth - Get truncated string with specified width
+ *
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class Mbstring
+{
+ const MB_CASE_FOLD = PHP_INT_MAX;
+
+ private static $encodingList = array('ASCII', 'UTF-8');
+ private static $language = 'neutral';
+ private static $internalEncoding = 'UTF-8';
+ private static $caseFold = array(
+ array('µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"),
+ array('μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'),
+ );
+
+ public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
+ {
+ if (\is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) {
+ $fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
+ } else {
+ $fromEncoding = self::getEncoding($fromEncoding);
+ }
+
+ $toEncoding = self::getEncoding($toEncoding);
+
+ if ('BASE64' === $fromEncoding) {
+ $s = base64_decode($s);
+ $fromEncoding = $toEncoding;
+ }
+
+ if ('BASE64' === $toEncoding) {
+ return base64_encode($s);
+ }
+
+ if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
+ if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
+ $fromEncoding = 'Windows-1252';
+ }
+ if ('UTF-8' !== $fromEncoding) {
+ $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s);
+ }
+
+ return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s);
+ }
+
+ if ('HTML-ENTITIES' === $fromEncoding) {
+ $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8');
+ $fromEncoding = 'UTF-8';
+ }
+
+ return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
+ }
+
+ public static function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null)
+ {
+ $vars = array(&$a, &$b, &$c, &$d, &$e, &$f);
+
+ $ok = true;
+ array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
+ if (false === $v = Mbstring::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
+ $ok = false;
+ }
+ });
+
+ return $ok ? $fromEncoding : false;
+ }
+
+ public static function mb_decode_mimeheader($s)
+ {
+ return iconv_mime_decode($s, 2, self::$internalEncoding);
+ }
+
+ public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
+ {
+ trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', E_USER_WARNING);
+ }
+
+ public static function mb_decode_numericentity($s, $convmap, $encoding = null)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) {
+ trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || !$convmap) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING);
+
+ return ''; // Instead of null (cf. mb_encode_numericentity).
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+
+ for ($i = 0; $i < $cnt; $i += 4) {
+ // collector_decode_htmlnumericentity ignores $convmap[$i + 3]
+ $convmap[$i] += $convmap[$i + 2];
+ $convmap[$i + 1] += $convmap[$i + 2];
+ }
+
+ $s = preg_replace_callback('/(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) {
+ $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
+ for ($i = 0; $i < $cnt; $i += 4) {
+ if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
+ return Mbstring::mb_chr($c - $convmap[$i + 2]);
+ }
+ }
+
+ return $m[0];
+ }, $s);
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) {
+ trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || !$convmap) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING);
+
+ return null; // Instead of '' (cf. mb_decode_numericentity).
+ }
+
+ if (null !== $is_hex && !\is_scalar($is_hex)) {
+ trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', E_USER_WARNING);
+
+ return null;
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4);
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+ $i = 0;
+ $len = \strlen($s);
+ $result = '';
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+ $c = self::mb_ord($uchr);
+
+ for ($j = 0; $j < $cnt; $j += 4) {
+ if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
+ $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
+ $result .= $is_hex ? sprintf('%X;', $cOffset) : ''.$cOffset.';';
+ continue 2;
+ }
+ }
+ $result .= $uchr;
+ }
+
+ if (null === $encoding) {
+ return $result;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $result);
+ }
+
+ public static function mb_convert_case($s, $mode, $encoding = null)
+ {
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ if (MB_CASE_TITLE == $mode) {
+ static $titleRegexp = null;
+ if (null === $titleRegexp) {
+ $titleRegexp = self::getData('titleCaseRegexp');
+ }
+ $s = preg_replace_callback($titleRegexp, array(__CLASS__, 'title_case'), $s);
+ } else {
+ if (MB_CASE_UPPER == $mode) {
+ static $upper = null;
+ if (null === $upper) {
+ $upper = self::getData('upperCase');
+ }
+ $map = $upper;
+ } else {
+ if (self::MB_CASE_FOLD === $mode) {
+ $s = str_replace(self::$caseFold[0], self::$caseFold[1], $s);
+ }
+
+ static $lower = null;
+ if (null === $lower) {
+ $lower = self::getData('lowerCase');
+ }
+ $map = $lower;
+ }
+
+ static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4);
+
+ $i = 0;
+ $len = \strlen($s);
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+
+ if (isset($map[$uchr])) {
+ $uchr = $map[$uchr];
+ $nlen = \strlen($uchr);
+
+ if ($nlen == $ulen) {
+ $nlen = $i;
+ do {
+ $s[--$nlen] = $uchr[--$ulen];
+ } while ($ulen);
+ } else {
+ $s = substr_replace($s, $uchr, $i - $ulen, $ulen);
+ $len += $nlen - $ulen;
+ $i += $nlen - $ulen;
+ }
+ }
+ }
+ }
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_internal_encoding($encoding = null)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding || false !== @iconv($encoding, $encoding, ' ')) {
+ self::$internalEncoding = $encoding;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function mb_language($lang = null)
+ {
+ if (null === $lang) {
+ return self::$language;
+ }
+
+ switch ($lang = strtolower($lang)) {
+ case 'uni':
+ case 'neutral':
+ self::$language = $lang;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function mb_list_encodings()
+ {
+ return array('UTF-8');
+ }
+
+ public static function mb_encoding_aliases($encoding)
+ {
+ switch (strtoupper($encoding)) {
+ case 'UTF8':
+ case 'UTF-8':
+ return array('utf8');
+ }
+
+ return false;
+ }
+
+ public static function mb_check_encoding($var = null, $encoding = null)
+ {
+ if (null === $encoding) {
+ if (null === $var) {
+ return false;
+ }
+ $encoding = self::$internalEncoding;
+ }
+
+ return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var);
+ }
+
+ public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
+ {
+ if (null === $encodingList) {
+ $encodingList = self::$encodingList;
+ } else {
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+ }
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ case 'ASCII':
+ if (!preg_match('/[\x80-\xFF]/', $str)) {
+ return $enc;
+ }
+ break;
+
+ case 'UTF8':
+ case 'UTF-8':
+ if (preg_match('//u', $str)) {
+ return 'UTF-8';
+ }
+ break;
+
+ default:
+ if (0 === strncmp($enc, 'ISO-8859-', 9)) {
+ return $enc;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static function mb_detect_order($encodingList = null)
+ {
+ if (null === $encodingList) {
+ return self::$encodingList;
+ }
+
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ default:
+ if (strncmp($enc, 'ISO-8859-', 9)) {
+ return false;
+ }
+ // no break
+ case 'ASCII':
+ case 'UTF8':
+ case 'UTF-8':
+ }
+ }
+
+ self::$encodingList = $encodingList;
+
+ return true;
+ }
+
+ public static function mb_strlen($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return \strlen($s);
+ }
+
+ return @iconv_strlen($s, $encoding);
+ }
+
+ public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strpos($haystack, $needle, $offset);
+ }
+
+ $needle = (string) $needle;
+ if ('' === $needle) {
+ trigger_error(__METHOD__.': Empty delimiter', E_USER_WARNING);
+
+ return false;
+ }
+
+ return iconv_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strrpos($haystack, $needle, $offset);
+ }
+
+ if ($offset != (int) $offset) {
+ $offset = 0;
+ } elseif ($offset = (int) $offset) {
+ if ($offset < 0) {
+ if (0 > $offset += self::mb_strlen($needle)) {
+ $haystack = self::mb_substr($haystack, 0, $offset, $encoding);
+ }
+ $offset = 0;
+ } else {
+ $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
+ }
+ }
+
+ $pos = iconv_strrpos($haystack, $needle, $encoding);
+
+ return false !== $pos ? $offset + $pos : false;
+ }
+
+ public static function mb_str_split($string, $split_length = 1, $encoding = null)
+ {
+ if (null !== $string && !\is_scalar($string) && !(\is_object($string) && \method_exists($string, '__toString'))) {
+ trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', E_USER_WARNING);
+
+ return null;
+ }
+
+ if (1 > $split_length = (int) $split_length) {
+ trigger_error('The length of each segment must be greater than zero', E_USER_WARNING);
+
+ return false;
+ }
+
+ if (null === $encoding) {
+ $encoding = mb_internal_encoding();
+ }
+
+ if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
+ $rx = '/(';
+ while (65535 < $split_length) {
+ $rx .= '.{65535}';
+ $split_length -= 65535;
+ }
+ $rx .= '.{'.$split_length.'})/us';
+
+ return preg_split($rx, $string, null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ }
+
+ $result = array();
+ $length = mb_strlen($string, $encoding);
+
+ for ($i = 0; $i < $length; $i += $split_length) {
+ $result[] = mb_substr($string, $i, $split_length, $encoding);
+ }
+
+ return $result;
+ }
+
+ public static function mb_strtolower($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, MB_CASE_LOWER, $encoding);
+ }
+
+ public static function mb_strtoupper($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, MB_CASE_UPPER, $encoding);
+ }
+
+ public static function mb_substitute_character($c = null)
+ {
+ if (0 === strcasecmp($c, 'none')) {
+ return true;
+ }
+
+ return null !== $c ? false : 'none';
+ }
+
+ public static function mb_substr($s, $start, $length = null, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return (string) substr($s, $start, null === $length ? 2147483647 : $length);
+ }
+
+ if ($start < 0) {
+ $start = iconv_strlen($s, $encoding) + $start;
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+
+ if (null === $length) {
+ $length = 2147483647;
+ } elseif ($length < 0) {
+ $length = iconv_strlen($s, $encoding) + $length - $start;
+ if ($length < 0) {
+ return '';
+ }
+ }
+
+ return (string) iconv_substr($s, $start, $length, $encoding);
+ }
+
+ public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding);
+ $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding);
+
+ return self::mb_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = self::mb_stripos($haystack, $needle, 0, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strrchr($haystack, $needle, $part);
+ }
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = iconv_strrpos($haystack, $needle, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = self::mb_strripos($haystack, $needle, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding);
+ $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding);
+
+ return self::mb_strrpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = strpos($haystack, $needle);
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return substr($haystack, 0, $pos);
+ }
+
+ return substr($haystack, $pos);
+ }
+
+ public static function mb_get_info($type = 'all')
+ {
+ $info = array(
+ 'internal_encoding' => self::$internalEncoding,
+ 'http_output' => 'pass',
+ 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
+ 'func_overload' => 0,
+ 'func_overload_list' => 'no overload',
+ 'mail_charset' => 'UTF-8',
+ 'mail_header_encoding' => 'BASE64',
+ 'mail_body_encoding' => 'BASE64',
+ 'illegal_chars' => 0,
+ 'encoding_translation' => 'Off',
+ 'language' => self::$language,
+ 'detect_order' => self::$encodingList,
+ 'substitute_character' => 'none',
+ 'strict_detection' => 'Off',
+ );
+
+ if ('all' === $type) {
+ return $info;
+ }
+ if (isset($info[$type])) {
+ return $info[$type];
+ }
+
+ return false;
+ }
+
+ public static function mb_http_input($type = '')
+ {
+ return false;
+ }
+
+ public static function mb_http_output($encoding = null)
+ {
+ return null !== $encoding ? 'pass' === $encoding : 'pass';
+ }
+
+ public static function mb_strwidth($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' !== $encoding) {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide);
+
+ return ($wide << 1) + iconv_strlen($s, 'UTF-8');
+ }
+
+ public static function mb_substr_count($haystack, $needle, $encoding = null)
+ {
+ return substr_count($haystack, $needle);
+ }
+
+ public static function mb_output_handler($contents, $status)
+ {
+ return $contents;
+ }
+
+ public static function mb_chr($code, $encoding = null)
+ {
+ if (0x80 > $code %= 0x200000) {
+ $s = \chr($code);
+ } elseif (0x800 > $code) {
+ $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
+ } elseif (0x10000 > $code) {
+ $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ } else {
+ $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ }
+
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, $encoding, 'UTF-8');
+ }
+
+ return $s;
+ }
+
+ public static function mb_ord($s, $encoding = null)
+ {
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, 'UTF-8', $encoding);
+ }
+
+ if (1 === \strlen($s)) {
+ return \ord($s);
+ }
+
+ $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
+ if (0xF0 <= $code) {
+ return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
+ }
+ if (0xE0 <= $code) {
+ return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
+ }
+ if (0xC0 <= $code) {
+ return (($code - 0xC0) << 6) + $s[2] - 0x80;
+ }
+
+ return $code;
+ }
+
+ private static function getSubpart($pos, $part, $haystack, $encoding)
+ {
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return self::mb_substr($haystack, 0, $pos, $encoding);
+ }
+
+ return self::mb_substr($haystack, $pos, null, $encoding);
+ }
+
+ private static function html_encoding_callback(array $m)
+ {
+ $i = 1;
+ $entities = '';
+ $m = unpack('C*', htmlentities($m[0], ENT_COMPAT, 'UTF-8'));
+
+ while (isset($m[$i])) {
+ if (0x80 > $m[$i]) {
+ $entities .= \chr($m[$i++]);
+ continue;
+ }
+ if (0xF0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } elseif (0xE0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } else {
+ $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
+ }
+
+ $entities .= ''.$c.';';
+ }
+
+ return $entities;
+ }
+
+ private static function title_case(array $s)
+ {
+ return self::mb_convert_case($s[1], MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], MB_CASE_LOWER, 'UTF-8');
+ }
+
+ private static function getData($file)
+ {
+ if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
+ return require $file;
+ }
+
+ return false;
+ }
+
+ private static function getEncoding($encoding)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ if ('UTF-8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ $encoding = strtoupper($encoding);
+
+ if ('8BIT' === $encoding || 'BINARY' === $encoding) {
+ return 'CP850';
+ }
+
+ if ('UTF8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ return $encoding;
+ }
+}
diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md
new file mode 100644
index 0000000..6dc3d12
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/README.md
@@ -0,0 +1,13 @@
+Symfony Polyfill / Mbstring
+===========================
+
+This component provides a partial, native PHP implementation for the
+[Mbstring](https://php.net/mbstring) extension.
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
new file mode 100644
index 0000000..00b7cc4
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
@@ -0,0 +1,1397 @@
+ 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ 'À' => 'à',
+ 'Á' => 'á',
+ 'Â' => 'â',
+ 'Ã' => 'ã',
+ 'Ä' => 'ä',
+ 'Å' => 'å',
+ 'Æ' => 'æ',
+ 'Ç' => 'ç',
+ 'È' => 'è',
+ 'É' => 'é',
+ 'Ê' => 'ê',
+ 'Ë' => 'ë',
+ 'Ì' => 'ì',
+ 'Í' => 'í',
+ 'Î' => 'î',
+ 'Ï' => 'ï',
+ 'Ð' => 'ð',
+ 'Ñ' => 'ñ',
+ 'Ò' => 'ò',
+ 'Ó' => 'ó',
+ 'Ô' => 'ô',
+ 'Õ' => 'õ',
+ 'Ö' => 'ö',
+ 'Ø' => 'ø',
+ 'Ù' => 'ù',
+ 'Ú' => 'ú',
+ 'Û' => 'û',
+ 'Ü' => 'ü',
+ 'Ý' => 'ý',
+ 'Þ' => 'þ',
+ 'Ā' => 'ā',
+ 'Ă' => 'ă',
+ 'Ą' => 'ą',
+ 'Ć' => 'ć',
+ 'Ĉ' => 'ĉ',
+ 'Ċ' => 'ċ',
+ 'Č' => 'č',
+ 'Ď' => 'ď',
+ 'Đ' => 'đ',
+ 'Ē' => 'ē',
+ 'Ĕ' => 'ĕ',
+ 'Ė' => 'ė',
+ 'Ę' => 'ę',
+ 'Ě' => 'ě',
+ 'Ĝ' => 'ĝ',
+ 'Ğ' => 'ğ',
+ 'Ġ' => 'ġ',
+ 'Ģ' => 'ģ',
+ 'Ĥ' => 'ĥ',
+ 'Ħ' => 'ħ',
+ 'Ĩ' => 'ĩ',
+ 'Ī' => 'ī',
+ 'Ĭ' => 'ĭ',
+ 'Į' => 'į',
+ 'İ' => 'i',
+ 'IJ' => 'ij',
+ 'Ĵ' => 'ĵ',
+ 'Ķ' => 'ķ',
+ 'Ĺ' => 'ĺ',
+ 'Ļ' => 'ļ',
+ 'Ľ' => 'ľ',
+ 'Ŀ' => 'ŀ',
+ 'Ł' => 'ł',
+ 'Ń' => 'ń',
+ 'Ņ' => 'ņ',
+ 'Ň' => 'ň',
+ 'Ŋ' => 'ŋ',
+ 'Ō' => 'ō',
+ 'Ŏ' => 'ŏ',
+ 'Ő' => 'ő',
+ 'Œ' => 'œ',
+ 'Ŕ' => 'ŕ',
+ 'Ŗ' => 'ŗ',
+ 'Ř' => 'ř',
+ 'Ś' => 'ś',
+ 'Ŝ' => 'ŝ',
+ 'Ş' => 'ş',
+ 'Š' => 'š',
+ 'Ţ' => 'ţ',
+ 'Ť' => 'ť',
+ 'Ŧ' => 'ŧ',
+ 'Ũ' => 'ũ',
+ 'Ū' => 'ū',
+ 'Ŭ' => 'ŭ',
+ 'Ů' => 'ů',
+ 'Ű' => 'ű',
+ 'Ų' => 'ų',
+ 'Ŵ' => 'ŵ',
+ 'Ŷ' => 'ŷ',
+ 'Ÿ' => 'ÿ',
+ 'Ź' => 'ź',
+ 'Ż' => 'ż',
+ 'Ž' => 'ž',
+ 'Ɓ' => 'ɓ',
+ 'Ƃ' => 'ƃ',
+ 'Ƅ' => 'ƅ',
+ 'Ɔ' => 'ɔ',
+ 'Ƈ' => 'ƈ',
+ 'Ɖ' => 'ɖ',
+ 'Ɗ' => 'ɗ',
+ 'Ƌ' => 'ƌ',
+ 'Ǝ' => 'ǝ',
+ 'Ə' => 'ə',
+ 'Ɛ' => 'ɛ',
+ 'Ƒ' => 'ƒ',
+ 'Ɠ' => 'ɠ',
+ 'Ɣ' => 'ɣ',
+ 'Ɩ' => 'ɩ',
+ 'Ɨ' => 'ɨ',
+ 'Ƙ' => 'ƙ',
+ 'Ɯ' => 'ɯ',
+ 'Ɲ' => 'ɲ',
+ 'Ɵ' => 'ɵ',
+ 'Ơ' => 'ơ',
+ 'Ƣ' => 'ƣ',
+ 'Ƥ' => 'ƥ',
+ 'Ʀ' => 'ʀ',
+ 'Ƨ' => 'ƨ',
+ 'Ʃ' => 'ʃ',
+ 'Ƭ' => 'ƭ',
+ 'Ʈ' => 'ʈ',
+ 'Ư' => 'ư',
+ 'Ʊ' => 'ʊ',
+ 'Ʋ' => 'ʋ',
+ 'Ƴ' => 'ƴ',
+ 'Ƶ' => 'ƶ',
+ 'Ʒ' => 'ʒ',
+ 'Ƹ' => 'ƹ',
+ 'Ƽ' => 'ƽ',
+ 'DŽ' => 'dž',
+ 'Dž' => 'dž',
+ 'LJ' => 'lj',
+ 'Lj' => 'lj',
+ 'NJ' => 'nj',
+ 'Nj' => 'nj',
+ 'Ǎ' => 'ǎ',
+ 'Ǐ' => 'ǐ',
+ 'Ǒ' => 'ǒ',
+ 'Ǔ' => 'ǔ',
+ 'Ǖ' => 'ǖ',
+ 'Ǘ' => 'ǘ',
+ 'Ǚ' => 'ǚ',
+ 'Ǜ' => 'ǜ',
+ 'Ǟ' => 'ǟ',
+ 'Ǡ' => 'ǡ',
+ 'Ǣ' => 'ǣ',
+ 'Ǥ' => 'ǥ',
+ 'Ǧ' => 'ǧ',
+ 'Ǩ' => 'ǩ',
+ 'Ǫ' => 'ǫ',
+ 'Ǭ' => 'ǭ',
+ 'Ǯ' => 'ǯ',
+ 'DZ' => 'dz',
+ 'Dz' => 'dz',
+ 'Ǵ' => 'ǵ',
+ 'Ƕ' => 'ƕ',
+ 'Ƿ' => 'ƿ',
+ 'Ǹ' => 'ǹ',
+ 'Ǻ' => 'ǻ',
+ 'Ǽ' => 'ǽ',
+ 'Ǿ' => 'ǿ',
+ 'Ȁ' => 'ȁ',
+ 'Ȃ' => 'ȃ',
+ 'Ȅ' => 'ȅ',
+ 'Ȇ' => 'ȇ',
+ 'Ȉ' => 'ȉ',
+ 'Ȋ' => 'ȋ',
+ 'Ȍ' => 'ȍ',
+ 'Ȏ' => 'ȏ',
+ 'Ȑ' => 'ȑ',
+ 'Ȓ' => 'ȓ',
+ 'Ȕ' => 'ȕ',
+ 'Ȗ' => 'ȗ',
+ 'Ș' => 'ș',
+ 'Ț' => 'ț',
+ 'Ȝ' => 'ȝ',
+ 'Ȟ' => 'ȟ',
+ 'Ƞ' => 'ƞ',
+ 'Ȣ' => 'ȣ',
+ 'Ȥ' => 'ȥ',
+ 'Ȧ' => 'ȧ',
+ 'Ȩ' => 'ȩ',
+ 'Ȫ' => 'ȫ',
+ 'Ȭ' => 'ȭ',
+ 'Ȯ' => 'ȯ',
+ 'Ȱ' => 'ȱ',
+ 'Ȳ' => 'ȳ',
+ 'Ⱥ' => 'ⱥ',
+ 'Ȼ' => 'ȼ',
+ 'Ƚ' => 'ƚ',
+ 'Ⱦ' => 'ⱦ',
+ 'Ɂ' => 'ɂ',
+ 'Ƀ' => 'ƀ',
+ 'Ʉ' => 'ʉ',
+ 'Ʌ' => 'ʌ',
+ 'Ɇ' => 'ɇ',
+ 'Ɉ' => 'ɉ',
+ 'Ɋ' => 'ɋ',
+ 'Ɍ' => 'ɍ',
+ 'Ɏ' => 'ɏ',
+ 'Ͱ' => 'ͱ',
+ 'Ͳ' => 'ͳ',
+ 'Ͷ' => 'ͷ',
+ 'Ϳ' => 'ϳ',
+ 'Ά' => 'ά',
+ 'Έ' => 'έ',
+ 'Ή' => 'ή',
+ 'Ί' => 'ί',
+ 'Ό' => 'ό',
+ 'Ύ' => 'ύ',
+ 'Ώ' => 'ώ',
+ 'Α' => 'α',
+ 'Β' => 'β',
+ 'Γ' => 'γ',
+ 'Δ' => 'δ',
+ 'Ε' => 'ε',
+ 'Ζ' => 'ζ',
+ 'Η' => 'η',
+ 'Θ' => 'θ',
+ 'Ι' => 'ι',
+ 'Κ' => 'κ',
+ 'Λ' => 'λ',
+ 'Μ' => 'μ',
+ 'Ν' => 'ν',
+ 'Ξ' => 'ξ',
+ 'Ο' => 'ο',
+ 'Π' => 'π',
+ 'Ρ' => 'ρ',
+ 'Σ' => 'σ',
+ 'Τ' => 'τ',
+ 'Υ' => 'υ',
+ 'Φ' => 'φ',
+ 'Χ' => 'χ',
+ 'Ψ' => 'ψ',
+ 'Ω' => 'ω',
+ 'Ϊ' => 'ϊ',
+ 'Ϋ' => 'ϋ',
+ 'Ϗ' => 'ϗ',
+ 'Ϙ' => 'ϙ',
+ 'Ϛ' => 'ϛ',
+ 'Ϝ' => 'ϝ',
+ 'Ϟ' => 'ϟ',
+ 'Ϡ' => 'ϡ',
+ 'Ϣ' => 'ϣ',
+ 'Ϥ' => 'ϥ',
+ 'Ϧ' => 'ϧ',
+ 'Ϩ' => 'ϩ',
+ 'Ϫ' => 'ϫ',
+ 'Ϭ' => 'ϭ',
+ 'Ϯ' => 'ϯ',
+ 'ϴ' => 'θ',
+ 'Ϸ' => 'ϸ',
+ 'Ϲ' => 'ϲ',
+ 'Ϻ' => 'ϻ',
+ 'Ͻ' => 'ͻ',
+ 'Ͼ' => 'ͼ',
+ 'Ͽ' => 'ͽ',
+ 'Ѐ' => 'ѐ',
+ 'Ё' => 'ё',
+ 'Ђ' => 'ђ',
+ 'Ѓ' => 'ѓ',
+ 'Є' => 'є',
+ 'Ѕ' => 'ѕ',
+ 'І' => 'і',
+ 'Ї' => 'ї',
+ 'Ј' => 'ј',
+ 'Љ' => 'љ',
+ 'Њ' => 'њ',
+ 'Ћ' => 'ћ',
+ 'Ќ' => 'ќ',
+ 'Ѝ' => 'ѝ',
+ 'Ў' => 'ў',
+ 'Џ' => 'џ',
+ 'А' => 'а',
+ 'Б' => 'б',
+ 'В' => 'в',
+ 'Г' => 'г',
+ 'Д' => 'д',
+ 'Е' => 'е',
+ 'Ж' => 'ж',
+ 'З' => 'з',
+ 'И' => 'и',
+ 'Й' => 'й',
+ 'К' => 'к',
+ 'Л' => 'л',
+ 'М' => 'м',
+ 'Н' => 'н',
+ 'О' => 'о',
+ 'П' => 'п',
+ 'Р' => 'р',
+ 'С' => 'с',
+ 'Т' => 'т',
+ 'У' => 'у',
+ 'Ф' => 'ф',
+ 'Х' => 'х',
+ 'Ц' => 'ц',
+ 'Ч' => 'ч',
+ 'Ш' => 'ш',
+ 'Щ' => 'щ',
+ 'Ъ' => 'ъ',
+ 'Ы' => 'ы',
+ 'Ь' => 'ь',
+ 'Э' => 'э',
+ 'Ю' => 'ю',
+ 'Я' => 'я',
+ 'Ѡ' => 'ѡ',
+ 'Ѣ' => 'ѣ',
+ 'Ѥ' => 'ѥ',
+ 'Ѧ' => 'ѧ',
+ 'Ѩ' => 'ѩ',
+ 'Ѫ' => 'ѫ',
+ 'Ѭ' => 'ѭ',
+ 'Ѯ' => 'ѯ',
+ 'Ѱ' => 'ѱ',
+ 'Ѳ' => 'ѳ',
+ 'Ѵ' => 'ѵ',
+ 'Ѷ' => 'ѷ',
+ 'Ѹ' => 'ѹ',
+ 'Ѻ' => 'ѻ',
+ 'Ѽ' => 'ѽ',
+ 'Ѿ' => 'ѿ',
+ 'Ҁ' => 'ҁ',
+ 'Ҋ' => 'ҋ',
+ 'Ҍ' => 'ҍ',
+ 'Ҏ' => 'ҏ',
+ 'Ґ' => 'ґ',
+ 'Ғ' => 'ғ',
+ 'Ҕ' => 'ҕ',
+ 'Җ' => 'җ',
+ 'Ҙ' => 'ҙ',
+ 'Қ' => 'қ',
+ 'Ҝ' => 'ҝ',
+ 'Ҟ' => 'ҟ',
+ 'Ҡ' => 'ҡ',
+ 'Ң' => 'ң',
+ 'Ҥ' => 'ҥ',
+ 'Ҧ' => 'ҧ',
+ 'Ҩ' => 'ҩ',
+ 'Ҫ' => 'ҫ',
+ 'Ҭ' => 'ҭ',
+ 'Ү' => 'ү',
+ 'Ұ' => 'ұ',
+ 'Ҳ' => 'ҳ',
+ 'Ҵ' => 'ҵ',
+ 'Ҷ' => 'ҷ',
+ 'Ҹ' => 'ҹ',
+ 'Һ' => 'һ',
+ 'Ҽ' => 'ҽ',
+ 'Ҿ' => 'ҿ',
+ 'Ӏ' => 'ӏ',
+ 'Ӂ' => 'ӂ',
+ 'Ӄ' => 'ӄ',
+ 'Ӆ' => 'ӆ',
+ 'Ӈ' => 'ӈ',
+ 'Ӊ' => 'ӊ',
+ 'Ӌ' => 'ӌ',
+ 'Ӎ' => 'ӎ',
+ 'Ӑ' => 'ӑ',
+ 'Ӓ' => 'ӓ',
+ 'Ӕ' => 'ӕ',
+ 'Ӗ' => 'ӗ',
+ 'Ә' => 'ә',
+ 'Ӛ' => 'ӛ',
+ 'Ӝ' => 'ӝ',
+ 'Ӟ' => 'ӟ',
+ 'Ӡ' => 'ӡ',
+ 'Ӣ' => 'ӣ',
+ 'Ӥ' => 'ӥ',
+ 'Ӧ' => 'ӧ',
+ 'Ө' => 'ө',
+ 'Ӫ' => 'ӫ',
+ 'Ӭ' => 'ӭ',
+ 'Ӯ' => 'ӯ',
+ 'Ӱ' => 'ӱ',
+ 'Ӳ' => 'ӳ',
+ 'Ӵ' => 'ӵ',
+ 'Ӷ' => 'ӷ',
+ 'Ӹ' => 'ӹ',
+ 'Ӻ' => 'ӻ',
+ 'Ӽ' => 'ӽ',
+ 'Ӿ' => 'ӿ',
+ 'Ԁ' => 'ԁ',
+ 'Ԃ' => 'ԃ',
+ 'Ԅ' => 'ԅ',
+ 'Ԇ' => 'ԇ',
+ 'Ԉ' => 'ԉ',
+ 'Ԋ' => 'ԋ',
+ 'Ԍ' => 'ԍ',
+ 'Ԏ' => 'ԏ',
+ 'Ԑ' => 'ԑ',
+ 'Ԓ' => 'ԓ',
+ 'Ԕ' => 'ԕ',
+ 'Ԗ' => 'ԗ',
+ 'Ԙ' => 'ԙ',
+ 'Ԛ' => 'ԛ',
+ 'Ԝ' => 'ԝ',
+ 'Ԟ' => 'ԟ',
+ 'Ԡ' => 'ԡ',
+ 'Ԣ' => 'ԣ',
+ 'Ԥ' => 'ԥ',
+ 'Ԧ' => 'ԧ',
+ 'Ԩ' => 'ԩ',
+ 'Ԫ' => 'ԫ',
+ 'Ԭ' => 'ԭ',
+ 'Ԯ' => 'ԯ',
+ 'Ա' => 'ա',
+ 'Բ' => 'բ',
+ 'Գ' => 'գ',
+ 'Դ' => 'դ',
+ 'Ե' => 'ե',
+ 'Զ' => 'զ',
+ 'Է' => 'է',
+ 'Ը' => 'ը',
+ 'Թ' => 'թ',
+ 'Ժ' => 'ժ',
+ 'Ի' => 'ի',
+ 'Լ' => 'լ',
+ 'Խ' => 'խ',
+ 'Ծ' => 'ծ',
+ 'Կ' => 'կ',
+ 'Հ' => 'հ',
+ 'Ձ' => 'ձ',
+ 'Ղ' => 'ղ',
+ 'Ճ' => 'ճ',
+ 'Մ' => 'մ',
+ 'Յ' => 'յ',
+ 'Ն' => 'ն',
+ 'Շ' => 'շ',
+ 'Ո' => 'ո',
+ 'Չ' => 'չ',
+ 'Պ' => 'պ',
+ 'Ջ' => 'ջ',
+ 'Ռ' => 'ռ',
+ 'Ս' => 'ս',
+ 'Վ' => 'վ',
+ 'Տ' => 'տ',
+ 'Ր' => 'ր',
+ 'Ց' => 'ց',
+ 'Ւ' => 'ւ',
+ 'Փ' => 'փ',
+ 'Ք' => 'ք',
+ 'Օ' => 'օ',
+ 'Ֆ' => 'ֆ',
+ 'Ⴀ' => 'ⴀ',
+ 'Ⴁ' => 'ⴁ',
+ 'Ⴂ' => 'ⴂ',
+ 'Ⴃ' => 'ⴃ',
+ 'Ⴄ' => 'ⴄ',
+ 'Ⴅ' => 'ⴅ',
+ 'Ⴆ' => 'ⴆ',
+ 'Ⴇ' => 'ⴇ',
+ 'Ⴈ' => 'ⴈ',
+ 'Ⴉ' => 'ⴉ',
+ 'Ⴊ' => 'ⴊ',
+ 'Ⴋ' => 'ⴋ',
+ 'Ⴌ' => 'ⴌ',
+ 'Ⴍ' => 'ⴍ',
+ 'Ⴎ' => 'ⴎ',
+ 'Ⴏ' => 'ⴏ',
+ 'Ⴐ' => 'ⴐ',
+ 'Ⴑ' => 'ⴑ',
+ 'Ⴒ' => 'ⴒ',
+ 'Ⴓ' => 'ⴓ',
+ 'Ⴔ' => 'ⴔ',
+ 'Ⴕ' => 'ⴕ',
+ 'Ⴖ' => 'ⴖ',
+ 'Ⴗ' => 'ⴗ',
+ 'Ⴘ' => 'ⴘ',
+ 'Ⴙ' => 'ⴙ',
+ 'Ⴚ' => 'ⴚ',
+ 'Ⴛ' => 'ⴛ',
+ 'Ⴜ' => 'ⴜ',
+ 'Ⴝ' => 'ⴝ',
+ 'Ⴞ' => 'ⴞ',
+ 'Ⴟ' => 'ⴟ',
+ 'Ⴠ' => 'ⴠ',
+ 'Ⴡ' => 'ⴡ',
+ 'Ⴢ' => 'ⴢ',
+ 'Ⴣ' => 'ⴣ',
+ 'Ⴤ' => 'ⴤ',
+ 'Ⴥ' => 'ⴥ',
+ 'Ⴧ' => 'ⴧ',
+ 'Ⴭ' => 'ⴭ',
+ 'Ꭰ' => 'ꭰ',
+ 'Ꭱ' => 'ꭱ',
+ 'Ꭲ' => 'ꭲ',
+ 'Ꭳ' => 'ꭳ',
+ 'Ꭴ' => 'ꭴ',
+ 'Ꭵ' => 'ꭵ',
+ 'Ꭶ' => 'ꭶ',
+ 'Ꭷ' => 'ꭷ',
+ 'Ꭸ' => 'ꭸ',
+ 'Ꭹ' => 'ꭹ',
+ 'Ꭺ' => 'ꭺ',
+ 'Ꭻ' => 'ꭻ',
+ 'Ꭼ' => 'ꭼ',
+ 'Ꭽ' => 'ꭽ',
+ 'Ꭾ' => 'ꭾ',
+ 'Ꭿ' => 'ꭿ',
+ 'Ꮀ' => 'ꮀ',
+ 'Ꮁ' => 'ꮁ',
+ 'Ꮂ' => 'ꮂ',
+ 'Ꮃ' => 'ꮃ',
+ 'Ꮄ' => 'ꮄ',
+ 'Ꮅ' => 'ꮅ',
+ 'Ꮆ' => 'ꮆ',
+ 'Ꮇ' => 'ꮇ',
+ 'Ꮈ' => 'ꮈ',
+ 'Ꮉ' => 'ꮉ',
+ 'Ꮊ' => 'ꮊ',
+ 'Ꮋ' => 'ꮋ',
+ 'Ꮌ' => 'ꮌ',
+ 'Ꮍ' => 'ꮍ',
+ 'Ꮎ' => 'ꮎ',
+ 'Ꮏ' => 'ꮏ',
+ 'Ꮐ' => 'ꮐ',
+ 'Ꮑ' => 'ꮑ',
+ 'Ꮒ' => 'ꮒ',
+ 'Ꮓ' => 'ꮓ',
+ 'Ꮔ' => 'ꮔ',
+ 'Ꮕ' => 'ꮕ',
+ 'Ꮖ' => 'ꮖ',
+ 'Ꮗ' => 'ꮗ',
+ 'Ꮘ' => 'ꮘ',
+ 'Ꮙ' => 'ꮙ',
+ 'Ꮚ' => 'ꮚ',
+ 'Ꮛ' => 'ꮛ',
+ 'Ꮜ' => 'ꮜ',
+ 'Ꮝ' => 'ꮝ',
+ 'Ꮞ' => 'ꮞ',
+ 'Ꮟ' => 'ꮟ',
+ 'Ꮠ' => 'ꮠ',
+ 'Ꮡ' => 'ꮡ',
+ 'Ꮢ' => 'ꮢ',
+ 'Ꮣ' => 'ꮣ',
+ 'Ꮤ' => 'ꮤ',
+ 'Ꮥ' => 'ꮥ',
+ 'Ꮦ' => 'ꮦ',
+ 'Ꮧ' => 'ꮧ',
+ 'Ꮨ' => 'ꮨ',
+ 'Ꮩ' => 'ꮩ',
+ 'Ꮪ' => 'ꮪ',
+ 'Ꮫ' => 'ꮫ',
+ 'Ꮬ' => 'ꮬ',
+ 'Ꮭ' => 'ꮭ',
+ 'Ꮮ' => 'ꮮ',
+ 'Ꮯ' => 'ꮯ',
+ 'Ꮰ' => 'ꮰ',
+ 'Ꮱ' => 'ꮱ',
+ 'Ꮲ' => 'ꮲ',
+ 'Ꮳ' => 'ꮳ',
+ 'Ꮴ' => 'ꮴ',
+ 'Ꮵ' => 'ꮵ',
+ 'Ꮶ' => 'ꮶ',
+ 'Ꮷ' => 'ꮷ',
+ 'Ꮸ' => 'ꮸ',
+ 'Ꮹ' => 'ꮹ',
+ 'Ꮺ' => 'ꮺ',
+ 'Ꮻ' => 'ꮻ',
+ 'Ꮼ' => 'ꮼ',
+ 'Ꮽ' => 'ꮽ',
+ 'Ꮾ' => 'ꮾ',
+ 'Ꮿ' => 'ꮿ',
+ 'Ᏸ' => 'ᏸ',
+ 'Ᏹ' => 'ᏹ',
+ 'Ᏺ' => 'ᏺ',
+ 'Ᏻ' => 'ᏻ',
+ 'Ᏼ' => 'ᏼ',
+ 'Ᏽ' => 'ᏽ',
+ 'Ა' => 'ა',
+ 'Ბ' => 'ბ',
+ 'Გ' => 'გ',
+ 'Დ' => 'დ',
+ 'Ე' => 'ე',
+ 'Ვ' => 'ვ',
+ 'Ზ' => 'ზ',
+ 'Თ' => 'თ',
+ 'Ი' => 'ი',
+ 'Კ' => 'კ',
+ 'Ლ' => 'ლ',
+ 'Მ' => 'მ',
+ 'Ნ' => 'ნ',
+ 'Ო' => 'ო',
+ 'Პ' => 'პ',
+ 'Ჟ' => 'ჟ',
+ 'Რ' => 'რ',
+ 'Ს' => 'ს',
+ 'Ტ' => 'ტ',
+ 'Უ' => 'უ',
+ 'Ფ' => 'ფ',
+ 'Ქ' => 'ქ',
+ 'Ღ' => 'ღ',
+ 'Ყ' => 'ყ',
+ 'Შ' => 'შ',
+ 'Ჩ' => 'ჩ',
+ 'Ც' => 'ც',
+ 'Ძ' => 'ძ',
+ 'Წ' => 'წ',
+ 'Ჭ' => 'ჭ',
+ 'Ხ' => 'ხ',
+ 'Ჯ' => 'ჯ',
+ 'Ჰ' => 'ჰ',
+ 'Ჱ' => 'ჱ',
+ 'Ჲ' => 'ჲ',
+ 'Ჳ' => 'ჳ',
+ 'Ჴ' => 'ჴ',
+ 'Ჵ' => 'ჵ',
+ 'Ჶ' => 'ჶ',
+ 'Ჷ' => 'ჷ',
+ 'Ჸ' => 'ჸ',
+ 'Ჹ' => 'ჹ',
+ 'Ჺ' => 'ჺ',
+ 'Ჽ' => 'ჽ',
+ 'Ჾ' => 'ჾ',
+ 'Ჿ' => 'ჿ',
+ 'Ḁ' => 'ḁ',
+ 'Ḃ' => 'ḃ',
+ 'Ḅ' => 'ḅ',
+ 'Ḇ' => 'ḇ',
+ 'Ḉ' => 'ḉ',
+ 'Ḋ' => 'ḋ',
+ 'Ḍ' => 'ḍ',
+ 'Ḏ' => 'ḏ',
+ 'Ḑ' => 'ḑ',
+ 'Ḓ' => 'ḓ',
+ 'Ḕ' => 'ḕ',
+ 'Ḗ' => 'ḗ',
+ 'Ḙ' => 'ḙ',
+ 'Ḛ' => 'ḛ',
+ 'Ḝ' => 'ḝ',
+ 'Ḟ' => 'ḟ',
+ 'Ḡ' => 'ḡ',
+ 'Ḣ' => 'ḣ',
+ 'Ḥ' => 'ḥ',
+ 'Ḧ' => 'ḧ',
+ 'Ḩ' => 'ḩ',
+ 'Ḫ' => 'ḫ',
+ 'Ḭ' => 'ḭ',
+ 'Ḯ' => 'ḯ',
+ 'Ḱ' => 'ḱ',
+ 'Ḳ' => 'ḳ',
+ 'Ḵ' => 'ḵ',
+ 'Ḷ' => 'ḷ',
+ 'Ḹ' => 'ḹ',
+ 'Ḻ' => 'ḻ',
+ 'Ḽ' => 'ḽ',
+ 'Ḿ' => 'ḿ',
+ 'Ṁ' => 'ṁ',
+ 'Ṃ' => 'ṃ',
+ 'Ṅ' => 'ṅ',
+ 'Ṇ' => 'ṇ',
+ 'Ṉ' => 'ṉ',
+ 'Ṋ' => 'ṋ',
+ 'Ṍ' => 'ṍ',
+ 'Ṏ' => 'ṏ',
+ 'Ṑ' => 'ṑ',
+ 'Ṓ' => 'ṓ',
+ 'Ṕ' => 'ṕ',
+ 'Ṗ' => 'ṗ',
+ 'Ṙ' => 'ṙ',
+ 'Ṛ' => 'ṛ',
+ 'Ṝ' => 'ṝ',
+ 'Ṟ' => 'ṟ',
+ 'Ṡ' => 'ṡ',
+ 'Ṣ' => 'ṣ',
+ 'Ṥ' => 'ṥ',
+ 'Ṧ' => 'ṧ',
+ 'Ṩ' => 'ṩ',
+ 'Ṫ' => 'ṫ',
+ 'Ṭ' => 'ṭ',
+ 'Ṯ' => 'ṯ',
+ 'Ṱ' => 'ṱ',
+ 'Ṳ' => 'ṳ',
+ 'Ṵ' => 'ṵ',
+ 'Ṷ' => 'ṷ',
+ 'Ṹ' => 'ṹ',
+ 'Ṻ' => 'ṻ',
+ 'Ṽ' => 'ṽ',
+ 'Ṿ' => 'ṿ',
+ 'Ẁ' => 'ẁ',
+ 'Ẃ' => 'ẃ',
+ 'Ẅ' => 'ẅ',
+ 'Ẇ' => 'ẇ',
+ 'Ẉ' => 'ẉ',
+ 'Ẋ' => 'ẋ',
+ 'Ẍ' => 'ẍ',
+ 'Ẏ' => 'ẏ',
+ 'Ẑ' => 'ẑ',
+ 'Ẓ' => 'ẓ',
+ 'Ẕ' => 'ẕ',
+ 'ẞ' => 'ß',
+ 'Ạ' => 'ạ',
+ 'Ả' => 'ả',
+ 'Ấ' => 'ấ',
+ 'Ầ' => 'ầ',
+ 'Ẩ' => 'ẩ',
+ 'Ẫ' => 'ẫ',
+ 'Ậ' => 'ậ',
+ 'Ắ' => 'ắ',
+ 'Ằ' => 'ằ',
+ 'Ẳ' => 'ẳ',
+ 'Ẵ' => 'ẵ',
+ 'Ặ' => 'ặ',
+ 'Ẹ' => 'ẹ',
+ 'Ẻ' => 'ẻ',
+ 'Ẽ' => 'ẽ',
+ 'Ế' => 'ế',
+ 'Ề' => 'ề',
+ 'Ể' => 'ể',
+ 'Ễ' => 'ễ',
+ 'Ệ' => 'ệ',
+ 'Ỉ' => 'ỉ',
+ 'Ị' => 'ị',
+ 'Ọ' => 'ọ',
+ 'Ỏ' => 'ỏ',
+ 'Ố' => 'ố',
+ 'Ồ' => 'ồ',
+ 'Ổ' => 'ổ',
+ 'Ỗ' => 'ỗ',
+ 'Ộ' => 'ộ',
+ 'Ớ' => 'ớ',
+ 'Ờ' => 'ờ',
+ 'Ở' => 'ở',
+ 'Ỡ' => 'ỡ',
+ 'Ợ' => 'ợ',
+ 'Ụ' => 'ụ',
+ 'Ủ' => 'ủ',
+ 'Ứ' => 'ứ',
+ 'Ừ' => 'ừ',
+ 'Ử' => 'ử',
+ 'Ữ' => 'ữ',
+ 'Ự' => 'ự',
+ 'Ỳ' => 'ỳ',
+ 'Ỵ' => 'ỵ',
+ 'Ỷ' => 'ỷ',
+ 'Ỹ' => 'ỹ',
+ 'Ỻ' => 'ỻ',
+ 'Ỽ' => 'ỽ',
+ 'Ỿ' => 'ỿ',
+ 'Ἀ' => 'ἀ',
+ 'Ἁ' => 'ἁ',
+ 'Ἂ' => 'ἂ',
+ 'Ἃ' => 'ἃ',
+ 'Ἄ' => 'ἄ',
+ 'Ἅ' => 'ἅ',
+ 'Ἆ' => 'ἆ',
+ 'Ἇ' => 'ἇ',
+ 'Ἐ' => 'ἐ',
+ 'Ἑ' => 'ἑ',
+ 'Ἒ' => 'ἒ',
+ 'Ἓ' => 'ἓ',
+ 'Ἔ' => 'ἔ',
+ 'Ἕ' => 'ἕ',
+ 'Ἠ' => 'ἠ',
+ 'Ἡ' => 'ἡ',
+ 'Ἢ' => 'ἢ',
+ 'Ἣ' => 'ἣ',
+ 'Ἤ' => 'ἤ',
+ 'Ἥ' => 'ἥ',
+ 'Ἦ' => 'ἦ',
+ 'Ἧ' => 'ἧ',
+ 'Ἰ' => 'ἰ',
+ 'Ἱ' => 'ἱ',
+ 'Ἲ' => 'ἲ',
+ 'Ἳ' => 'ἳ',
+ 'Ἴ' => 'ἴ',
+ 'Ἵ' => 'ἵ',
+ 'Ἶ' => 'ἶ',
+ 'Ἷ' => 'ἷ',
+ 'Ὀ' => 'ὀ',
+ 'Ὁ' => 'ὁ',
+ 'Ὂ' => 'ὂ',
+ 'Ὃ' => 'ὃ',
+ 'Ὄ' => 'ὄ',
+ 'Ὅ' => 'ὅ',
+ 'Ὑ' => 'ὑ',
+ 'Ὓ' => 'ὓ',
+ 'Ὕ' => 'ὕ',
+ 'Ὗ' => 'ὗ',
+ 'Ὠ' => 'ὠ',
+ 'Ὡ' => 'ὡ',
+ 'Ὢ' => 'ὢ',
+ 'Ὣ' => 'ὣ',
+ 'Ὤ' => 'ὤ',
+ 'Ὥ' => 'ὥ',
+ 'Ὦ' => 'ὦ',
+ 'Ὧ' => 'ὧ',
+ 'ᾈ' => 'ᾀ',
+ 'ᾉ' => 'ᾁ',
+ 'ᾊ' => 'ᾂ',
+ 'ᾋ' => 'ᾃ',
+ 'ᾌ' => 'ᾄ',
+ 'ᾍ' => 'ᾅ',
+ 'ᾎ' => 'ᾆ',
+ 'ᾏ' => 'ᾇ',
+ 'ᾘ' => 'ᾐ',
+ 'ᾙ' => 'ᾑ',
+ 'ᾚ' => 'ᾒ',
+ 'ᾛ' => 'ᾓ',
+ 'ᾜ' => 'ᾔ',
+ 'ᾝ' => 'ᾕ',
+ 'ᾞ' => 'ᾖ',
+ 'ᾟ' => 'ᾗ',
+ 'ᾨ' => 'ᾠ',
+ 'ᾩ' => 'ᾡ',
+ 'ᾪ' => 'ᾢ',
+ 'ᾫ' => 'ᾣ',
+ 'ᾬ' => 'ᾤ',
+ 'ᾭ' => 'ᾥ',
+ 'ᾮ' => 'ᾦ',
+ 'ᾯ' => 'ᾧ',
+ 'Ᾰ' => 'ᾰ',
+ 'Ᾱ' => 'ᾱ',
+ 'Ὰ' => 'ὰ',
+ 'Ά' => 'ά',
+ 'ᾼ' => 'ᾳ',
+ 'Ὲ' => 'ὲ',
+ 'Έ' => 'έ',
+ 'Ὴ' => 'ὴ',
+ 'Ή' => 'ή',
+ 'ῌ' => 'ῃ',
+ 'Ῐ' => 'ῐ',
+ 'Ῑ' => 'ῑ',
+ 'Ὶ' => 'ὶ',
+ 'Ί' => 'ί',
+ 'Ῠ' => 'ῠ',
+ 'Ῡ' => 'ῡ',
+ 'Ὺ' => 'ὺ',
+ 'Ύ' => 'ύ',
+ 'Ῥ' => 'ῥ',
+ 'Ὸ' => 'ὸ',
+ 'Ό' => 'ό',
+ 'Ὼ' => 'ὼ',
+ 'Ώ' => 'ώ',
+ 'ῼ' => 'ῳ',
+ 'Ω' => 'ω',
+ 'K' => 'k',
+ 'Å' => 'å',
+ 'Ⅎ' => 'ⅎ',
+ 'Ⅰ' => 'ⅰ',
+ 'Ⅱ' => 'ⅱ',
+ 'Ⅲ' => 'ⅲ',
+ 'Ⅳ' => 'ⅳ',
+ 'Ⅴ' => 'ⅴ',
+ 'Ⅵ' => 'ⅵ',
+ 'Ⅶ' => 'ⅶ',
+ 'Ⅷ' => 'ⅷ',
+ 'Ⅸ' => 'ⅸ',
+ 'Ⅹ' => 'ⅹ',
+ 'Ⅺ' => 'ⅺ',
+ 'Ⅻ' => 'ⅻ',
+ 'Ⅼ' => 'ⅼ',
+ 'Ⅽ' => 'ⅽ',
+ 'Ⅾ' => 'ⅾ',
+ 'Ⅿ' => 'ⅿ',
+ 'Ↄ' => 'ↄ',
+ 'Ⓐ' => 'ⓐ',
+ 'Ⓑ' => 'ⓑ',
+ 'Ⓒ' => 'ⓒ',
+ 'Ⓓ' => 'ⓓ',
+ 'Ⓔ' => 'ⓔ',
+ 'Ⓕ' => 'ⓕ',
+ 'Ⓖ' => 'ⓖ',
+ 'Ⓗ' => 'ⓗ',
+ 'Ⓘ' => 'ⓘ',
+ 'Ⓙ' => 'ⓙ',
+ 'Ⓚ' => 'ⓚ',
+ 'Ⓛ' => 'ⓛ',
+ 'Ⓜ' => 'ⓜ',
+ 'Ⓝ' => 'ⓝ',
+ 'Ⓞ' => 'ⓞ',
+ 'Ⓟ' => 'ⓟ',
+ 'Ⓠ' => 'ⓠ',
+ 'Ⓡ' => 'ⓡ',
+ 'Ⓢ' => 'ⓢ',
+ 'Ⓣ' => 'ⓣ',
+ 'Ⓤ' => 'ⓤ',
+ 'Ⓥ' => 'ⓥ',
+ 'Ⓦ' => 'ⓦ',
+ 'Ⓧ' => 'ⓧ',
+ 'Ⓨ' => 'ⓨ',
+ 'Ⓩ' => 'ⓩ',
+ 'Ⰰ' => 'ⰰ',
+ 'Ⰱ' => 'ⰱ',
+ 'Ⰲ' => 'ⰲ',
+ 'Ⰳ' => 'ⰳ',
+ 'Ⰴ' => 'ⰴ',
+ 'Ⰵ' => 'ⰵ',
+ 'Ⰶ' => 'ⰶ',
+ 'Ⰷ' => 'ⰷ',
+ 'Ⰸ' => 'ⰸ',
+ 'Ⰹ' => 'ⰹ',
+ 'Ⰺ' => 'ⰺ',
+ 'Ⰻ' => 'ⰻ',
+ 'Ⰼ' => 'ⰼ',
+ 'Ⰽ' => 'ⰽ',
+ 'Ⰾ' => 'ⰾ',
+ 'Ⰿ' => 'ⰿ',
+ 'Ⱀ' => 'ⱀ',
+ 'Ⱁ' => 'ⱁ',
+ 'Ⱂ' => 'ⱂ',
+ 'Ⱃ' => 'ⱃ',
+ 'Ⱄ' => 'ⱄ',
+ 'Ⱅ' => 'ⱅ',
+ 'Ⱆ' => 'ⱆ',
+ 'Ⱇ' => 'ⱇ',
+ 'Ⱈ' => 'ⱈ',
+ 'Ⱉ' => 'ⱉ',
+ 'Ⱊ' => 'ⱊ',
+ 'Ⱋ' => 'ⱋ',
+ 'Ⱌ' => 'ⱌ',
+ 'Ⱍ' => 'ⱍ',
+ 'Ⱎ' => 'ⱎ',
+ 'Ⱏ' => 'ⱏ',
+ 'Ⱐ' => 'ⱐ',
+ 'Ⱑ' => 'ⱑ',
+ 'Ⱒ' => 'ⱒ',
+ 'Ⱓ' => 'ⱓ',
+ 'Ⱔ' => 'ⱔ',
+ 'Ⱕ' => 'ⱕ',
+ 'Ⱖ' => 'ⱖ',
+ 'Ⱗ' => 'ⱗ',
+ 'Ⱘ' => 'ⱘ',
+ 'Ⱙ' => 'ⱙ',
+ 'Ⱚ' => 'ⱚ',
+ 'Ⱛ' => 'ⱛ',
+ 'Ⱜ' => 'ⱜ',
+ 'Ⱝ' => 'ⱝ',
+ 'Ⱞ' => 'ⱞ',
+ 'Ⱡ' => 'ⱡ',
+ 'Ɫ' => 'ɫ',
+ 'Ᵽ' => 'ᵽ',
+ 'Ɽ' => 'ɽ',
+ 'Ⱨ' => 'ⱨ',
+ 'Ⱪ' => 'ⱪ',
+ 'Ⱬ' => 'ⱬ',
+ 'Ɑ' => 'ɑ',
+ 'Ɱ' => 'ɱ',
+ 'Ɐ' => 'ɐ',
+ 'Ɒ' => 'ɒ',
+ 'Ⱳ' => 'ⱳ',
+ 'Ⱶ' => 'ⱶ',
+ 'Ȿ' => 'ȿ',
+ 'Ɀ' => 'ɀ',
+ 'Ⲁ' => 'ⲁ',
+ 'Ⲃ' => 'ⲃ',
+ 'Ⲅ' => 'ⲅ',
+ 'Ⲇ' => 'ⲇ',
+ 'Ⲉ' => 'ⲉ',
+ 'Ⲋ' => 'ⲋ',
+ 'Ⲍ' => 'ⲍ',
+ 'Ⲏ' => 'ⲏ',
+ 'Ⲑ' => 'ⲑ',
+ 'Ⲓ' => 'ⲓ',
+ 'Ⲕ' => 'ⲕ',
+ 'Ⲗ' => 'ⲗ',
+ 'Ⲙ' => 'ⲙ',
+ 'Ⲛ' => 'ⲛ',
+ 'Ⲝ' => 'ⲝ',
+ 'Ⲟ' => 'ⲟ',
+ 'Ⲡ' => 'ⲡ',
+ 'Ⲣ' => 'ⲣ',
+ 'Ⲥ' => 'ⲥ',
+ 'Ⲧ' => 'ⲧ',
+ 'Ⲩ' => 'ⲩ',
+ 'Ⲫ' => 'ⲫ',
+ 'Ⲭ' => 'ⲭ',
+ 'Ⲯ' => 'ⲯ',
+ 'Ⲱ' => 'ⲱ',
+ 'Ⲳ' => 'ⲳ',
+ 'Ⲵ' => 'ⲵ',
+ 'Ⲷ' => 'ⲷ',
+ 'Ⲹ' => 'ⲹ',
+ 'Ⲻ' => 'ⲻ',
+ 'Ⲽ' => 'ⲽ',
+ 'Ⲿ' => 'ⲿ',
+ 'Ⳁ' => 'ⳁ',
+ 'Ⳃ' => 'ⳃ',
+ 'Ⳅ' => 'ⳅ',
+ 'Ⳇ' => 'ⳇ',
+ 'Ⳉ' => 'ⳉ',
+ 'Ⳋ' => 'ⳋ',
+ 'Ⳍ' => 'ⳍ',
+ 'Ⳏ' => 'ⳏ',
+ 'Ⳑ' => 'ⳑ',
+ 'Ⳓ' => 'ⳓ',
+ 'Ⳕ' => 'ⳕ',
+ 'Ⳗ' => 'ⳗ',
+ 'Ⳙ' => 'ⳙ',
+ 'Ⳛ' => 'ⳛ',
+ 'Ⳝ' => 'ⳝ',
+ 'Ⳟ' => 'ⳟ',
+ 'Ⳡ' => 'ⳡ',
+ 'Ⳣ' => 'ⳣ',
+ 'Ⳬ' => 'ⳬ',
+ 'Ⳮ' => 'ⳮ',
+ 'Ⳳ' => 'ⳳ',
+ 'Ꙁ' => 'ꙁ',
+ 'Ꙃ' => 'ꙃ',
+ 'Ꙅ' => 'ꙅ',
+ 'Ꙇ' => 'ꙇ',
+ 'Ꙉ' => 'ꙉ',
+ 'Ꙋ' => 'ꙋ',
+ 'Ꙍ' => 'ꙍ',
+ 'Ꙏ' => 'ꙏ',
+ 'Ꙑ' => 'ꙑ',
+ 'Ꙓ' => 'ꙓ',
+ 'Ꙕ' => 'ꙕ',
+ 'Ꙗ' => 'ꙗ',
+ 'Ꙙ' => 'ꙙ',
+ 'Ꙛ' => 'ꙛ',
+ 'Ꙝ' => 'ꙝ',
+ 'Ꙟ' => 'ꙟ',
+ 'Ꙡ' => 'ꙡ',
+ 'Ꙣ' => 'ꙣ',
+ 'Ꙥ' => 'ꙥ',
+ 'Ꙧ' => 'ꙧ',
+ 'Ꙩ' => 'ꙩ',
+ 'Ꙫ' => 'ꙫ',
+ 'Ꙭ' => 'ꙭ',
+ 'Ꚁ' => 'ꚁ',
+ 'Ꚃ' => 'ꚃ',
+ 'Ꚅ' => 'ꚅ',
+ 'Ꚇ' => 'ꚇ',
+ 'Ꚉ' => 'ꚉ',
+ 'Ꚋ' => 'ꚋ',
+ 'Ꚍ' => 'ꚍ',
+ 'Ꚏ' => 'ꚏ',
+ 'Ꚑ' => 'ꚑ',
+ 'Ꚓ' => 'ꚓ',
+ 'Ꚕ' => 'ꚕ',
+ 'Ꚗ' => 'ꚗ',
+ 'Ꚙ' => 'ꚙ',
+ 'Ꚛ' => 'ꚛ',
+ 'Ꜣ' => 'ꜣ',
+ 'Ꜥ' => 'ꜥ',
+ 'Ꜧ' => 'ꜧ',
+ 'Ꜩ' => 'ꜩ',
+ 'Ꜫ' => 'ꜫ',
+ 'Ꜭ' => 'ꜭ',
+ 'Ꜯ' => 'ꜯ',
+ 'Ꜳ' => 'ꜳ',
+ 'Ꜵ' => 'ꜵ',
+ 'Ꜷ' => 'ꜷ',
+ 'Ꜹ' => 'ꜹ',
+ 'Ꜻ' => 'ꜻ',
+ 'Ꜽ' => 'ꜽ',
+ 'Ꜿ' => 'ꜿ',
+ 'Ꝁ' => 'ꝁ',
+ 'Ꝃ' => 'ꝃ',
+ 'Ꝅ' => 'ꝅ',
+ 'Ꝇ' => 'ꝇ',
+ 'Ꝉ' => 'ꝉ',
+ 'Ꝋ' => 'ꝋ',
+ 'Ꝍ' => 'ꝍ',
+ 'Ꝏ' => 'ꝏ',
+ 'Ꝑ' => 'ꝑ',
+ 'Ꝓ' => 'ꝓ',
+ 'Ꝕ' => 'ꝕ',
+ 'Ꝗ' => 'ꝗ',
+ 'Ꝙ' => 'ꝙ',
+ 'Ꝛ' => 'ꝛ',
+ 'Ꝝ' => 'ꝝ',
+ 'Ꝟ' => 'ꝟ',
+ 'Ꝡ' => 'ꝡ',
+ 'Ꝣ' => 'ꝣ',
+ 'Ꝥ' => 'ꝥ',
+ 'Ꝧ' => 'ꝧ',
+ 'Ꝩ' => 'ꝩ',
+ 'Ꝫ' => 'ꝫ',
+ 'Ꝭ' => 'ꝭ',
+ 'Ꝯ' => 'ꝯ',
+ 'Ꝺ' => 'ꝺ',
+ 'Ꝼ' => 'ꝼ',
+ 'Ᵹ' => 'ᵹ',
+ 'Ꝿ' => 'ꝿ',
+ 'Ꞁ' => 'ꞁ',
+ 'Ꞃ' => 'ꞃ',
+ 'Ꞅ' => 'ꞅ',
+ 'Ꞇ' => 'ꞇ',
+ 'Ꞌ' => 'ꞌ',
+ 'Ɥ' => 'ɥ',
+ 'Ꞑ' => 'ꞑ',
+ 'Ꞓ' => 'ꞓ',
+ 'Ꞗ' => 'ꞗ',
+ 'Ꞙ' => 'ꞙ',
+ 'Ꞛ' => 'ꞛ',
+ 'Ꞝ' => 'ꞝ',
+ 'Ꞟ' => 'ꞟ',
+ 'Ꞡ' => 'ꞡ',
+ 'Ꞣ' => 'ꞣ',
+ 'Ꞥ' => 'ꞥ',
+ 'Ꞧ' => 'ꞧ',
+ 'Ꞩ' => 'ꞩ',
+ 'Ɦ' => 'ɦ',
+ 'Ɜ' => 'ɜ',
+ 'Ɡ' => 'ɡ',
+ 'Ɬ' => 'ɬ',
+ 'Ɪ' => 'ɪ',
+ 'Ʞ' => 'ʞ',
+ 'Ʇ' => 'ʇ',
+ 'Ʝ' => 'ʝ',
+ 'Ꭓ' => 'ꭓ',
+ 'Ꞵ' => 'ꞵ',
+ 'Ꞷ' => 'ꞷ',
+ 'Ꞹ' => 'ꞹ',
+ 'Ꞻ' => 'ꞻ',
+ 'Ꞽ' => 'ꞽ',
+ 'Ꞿ' => 'ꞿ',
+ 'Ꟃ' => 'ꟃ',
+ 'Ꞔ' => 'ꞔ',
+ 'Ʂ' => 'ʂ',
+ 'Ᶎ' => 'ᶎ',
+ 'Ꟈ' => 'ꟈ',
+ 'Ꟊ' => 'ꟊ',
+ 'Ꟶ' => 'ꟶ',
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ '𐐀' => '𐐨',
+ '𐐁' => '𐐩',
+ '𐐂' => '𐐪',
+ '𐐃' => '𐐫',
+ '𐐄' => '𐐬',
+ '𐐅' => '𐐭',
+ '𐐆' => '𐐮',
+ '𐐇' => '𐐯',
+ '𐐈' => '𐐰',
+ '𐐉' => '𐐱',
+ '𐐊' => '𐐲',
+ '𐐋' => '𐐳',
+ '𐐌' => '𐐴',
+ '𐐍' => '𐐵',
+ '𐐎' => '𐐶',
+ '𐐏' => '𐐷',
+ '𐐐' => '𐐸',
+ '𐐑' => '𐐹',
+ '𐐒' => '𐐺',
+ '𐐓' => '𐐻',
+ '𐐔' => '𐐼',
+ '𐐕' => '𐐽',
+ '𐐖' => '𐐾',
+ '𐐗' => '𐐿',
+ '𐐘' => '𐑀',
+ '𐐙' => '𐑁',
+ '𐐚' => '𐑂',
+ '𐐛' => '𐑃',
+ '𐐜' => '𐑄',
+ '𐐝' => '𐑅',
+ '𐐞' => '𐑆',
+ '𐐟' => '𐑇',
+ '𐐠' => '𐑈',
+ '𐐡' => '𐑉',
+ '𐐢' => '𐑊',
+ '𐐣' => '𐑋',
+ '𐐤' => '𐑌',
+ '𐐥' => '𐑍',
+ '𐐦' => '𐑎',
+ '𐐧' => '𐑏',
+ '𐒰' => '𐓘',
+ '𐒱' => '𐓙',
+ '𐒲' => '𐓚',
+ '𐒳' => '𐓛',
+ '𐒴' => '𐓜',
+ '𐒵' => '𐓝',
+ '𐒶' => '𐓞',
+ '𐒷' => '𐓟',
+ '𐒸' => '𐓠',
+ '𐒹' => '𐓡',
+ '𐒺' => '𐓢',
+ '𐒻' => '𐓣',
+ '𐒼' => '𐓤',
+ '𐒽' => '𐓥',
+ '𐒾' => '𐓦',
+ '𐒿' => '𐓧',
+ '𐓀' => '𐓨',
+ '𐓁' => '𐓩',
+ '𐓂' => '𐓪',
+ '𐓃' => '𐓫',
+ '𐓄' => '𐓬',
+ '𐓅' => '𐓭',
+ '𐓆' => '𐓮',
+ '𐓇' => '𐓯',
+ '𐓈' => '𐓰',
+ '𐓉' => '𐓱',
+ '𐓊' => '𐓲',
+ '𐓋' => '𐓳',
+ '𐓌' => '𐓴',
+ '𐓍' => '𐓵',
+ '𐓎' => '𐓶',
+ '𐓏' => '𐓷',
+ '𐓐' => '𐓸',
+ '𐓑' => '𐓹',
+ '𐓒' => '𐓺',
+ '𐓓' => '𐓻',
+ '𐲀' => '𐳀',
+ '𐲁' => '𐳁',
+ '𐲂' => '𐳂',
+ '𐲃' => '𐳃',
+ '𐲄' => '𐳄',
+ '𐲅' => '𐳅',
+ '𐲆' => '𐳆',
+ '𐲇' => '𐳇',
+ '𐲈' => '𐳈',
+ '𐲉' => '𐳉',
+ '𐲊' => '𐳊',
+ '𐲋' => '𐳋',
+ '𐲌' => '𐳌',
+ '𐲍' => '𐳍',
+ '𐲎' => '𐳎',
+ '𐲏' => '𐳏',
+ '𐲐' => '𐳐',
+ '𐲑' => '𐳑',
+ '𐲒' => '𐳒',
+ '𐲓' => '𐳓',
+ '𐲔' => '𐳔',
+ '𐲕' => '𐳕',
+ '𐲖' => '𐳖',
+ '𐲗' => '𐳗',
+ '𐲘' => '𐳘',
+ '𐲙' => '𐳙',
+ '𐲚' => '𐳚',
+ '𐲛' => '𐳛',
+ '𐲜' => '𐳜',
+ '𐲝' => '𐳝',
+ '𐲞' => '𐳞',
+ '𐲟' => '𐳟',
+ '𐲠' => '𐳠',
+ '𐲡' => '𐳡',
+ '𐲢' => '𐳢',
+ '𐲣' => '𐳣',
+ '𐲤' => '𐳤',
+ '𐲥' => '𐳥',
+ '𐲦' => '𐳦',
+ '𐲧' => '𐳧',
+ '𐲨' => '𐳨',
+ '𐲩' => '𐳩',
+ '𐲪' => '𐳪',
+ '𐲫' => '𐳫',
+ '𐲬' => '𐳬',
+ '𐲭' => '𐳭',
+ '𐲮' => '𐳮',
+ '𐲯' => '𐳯',
+ '𐲰' => '𐳰',
+ '𐲱' => '𐳱',
+ '𐲲' => '𐳲',
+ '𑢠' => '𑣀',
+ '𑢡' => '𑣁',
+ '𑢢' => '𑣂',
+ '𑢣' => '𑣃',
+ '𑢤' => '𑣄',
+ '𑢥' => '𑣅',
+ '𑢦' => '𑣆',
+ '𑢧' => '𑣇',
+ '𑢨' => '𑣈',
+ '𑢩' => '𑣉',
+ '𑢪' => '𑣊',
+ '𑢫' => '𑣋',
+ '𑢬' => '𑣌',
+ '𑢭' => '𑣍',
+ '𑢮' => '𑣎',
+ '𑢯' => '𑣏',
+ '𑢰' => '𑣐',
+ '𑢱' => '𑣑',
+ '𑢲' => '𑣒',
+ '𑢳' => '𑣓',
+ '𑢴' => '𑣔',
+ '𑢵' => '𑣕',
+ '𑢶' => '𑣖',
+ '𑢷' => '𑣗',
+ '𑢸' => '𑣘',
+ '𑢹' => '𑣙',
+ '𑢺' => '𑣚',
+ '𑢻' => '𑣛',
+ '𑢼' => '𑣜',
+ '𑢽' => '𑣝',
+ '𑢾' => '𑣞',
+ '𑢿' => '𑣟',
+ '𖹀' => '𖹠',
+ '𖹁' => '𖹡',
+ '𖹂' => '𖹢',
+ '𖹃' => '𖹣',
+ '𖹄' => '𖹤',
+ '𖹅' => '𖹥',
+ '𖹆' => '𖹦',
+ '𖹇' => '𖹧',
+ '𖹈' => '𖹨',
+ '𖹉' => '𖹩',
+ '𖹊' => '𖹪',
+ '𖹋' => '𖹫',
+ '𖹌' => '𖹬',
+ '𖹍' => '𖹭',
+ '𖹎' => '𖹮',
+ '𖹏' => '𖹯',
+ '𖹐' => '𖹰',
+ '𖹑' => '𖹱',
+ '𖹒' => '𖹲',
+ '𖹓' => '𖹳',
+ '𖹔' => '𖹴',
+ '𖹕' => '𖹵',
+ '𖹖' => '𖹶',
+ '𖹗' => '𖹷',
+ '𖹘' => '𖹸',
+ '𖹙' => '𖹹',
+ '𖹚' => '𖹺',
+ '𖹛' => '𖹻',
+ '𖹜' => '𖹼',
+ '𖹝' => '𖹽',
+ '𖹞' => '𖹾',
+ '𖹟' => '𖹿',
+ '𞤀' => '𞤢',
+ '𞤁' => '𞤣',
+ '𞤂' => '𞤤',
+ '𞤃' => '𞤥',
+ '𞤄' => '𞤦',
+ '𞤅' => '𞤧',
+ '𞤆' => '𞤨',
+ '𞤇' => '𞤩',
+ '𞤈' => '𞤪',
+ '𞤉' => '𞤫',
+ '𞤊' => '𞤬',
+ '𞤋' => '𞤭',
+ '𞤌' => '𞤮',
+ '𞤍' => '𞤯',
+ '𞤎' => '𞤰',
+ '𞤏' => '𞤱',
+ '𞤐' => '𞤲',
+ '𞤑' => '𞤳',
+ '𞤒' => '𞤴',
+ '𞤓' => '𞤵',
+ '𞤔' => '𞤶',
+ '𞤕' => '𞤷',
+ '𞤖' => '𞤸',
+ '𞤗' => '𞤹',
+ '𞤘' => '𞤺',
+ '𞤙' => '𞤻',
+ '𞤚' => '𞤼',
+ '𞤛' => '𞤽',
+ '𞤜' => '𞤾',
+ '𞤝' => '𞤿',
+ '𞤞' => '𞥀',
+ '𞤟' => '𞥁',
+ '𞤠' => '𞥂',
+ '𞤡' => '𞥃',
+);
diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
new file mode 100644
index 0000000..d2f10e0
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
@@ -0,0 +1,5 @@
+ 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ 'µ' => 'Μ',
+ 'à' => 'À',
+ 'á' => 'Á',
+ 'â' => 'Â',
+ 'ã' => 'Ã',
+ 'ä' => 'Ä',
+ 'å' => 'Å',
+ 'æ' => 'Æ',
+ 'ç' => 'Ç',
+ 'è' => 'È',
+ 'é' => 'É',
+ 'ê' => 'Ê',
+ 'ë' => 'Ë',
+ 'ì' => 'Ì',
+ 'í' => 'Í',
+ 'î' => 'Î',
+ 'ï' => 'Ï',
+ 'ð' => 'Ð',
+ 'ñ' => 'Ñ',
+ 'ò' => 'Ò',
+ 'ó' => 'Ó',
+ 'ô' => 'Ô',
+ 'õ' => 'Õ',
+ 'ö' => 'Ö',
+ 'ø' => 'Ø',
+ 'ù' => 'Ù',
+ 'ú' => 'Ú',
+ 'û' => 'Û',
+ 'ü' => 'Ü',
+ 'ý' => 'Ý',
+ 'þ' => 'Þ',
+ 'ÿ' => 'Ÿ',
+ 'ā' => 'Ā',
+ 'ă' => 'Ă',
+ 'ą' => 'Ą',
+ 'ć' => 'Ć',
+ 'ĉ' => 'Ĉ',
+ 'ċ' => 'Ċ',
+ 'č' => 'Č',
+ 'ď' => 'Ď',
+ 'đ' => 'Đ',
+ 'ē' => 'Ē',
+ 'ĕ' => 'Ĕ',
+ 'ė' => 'Ė',
+ 'ę' => 'Ę',
+ 'ě' => 'Ě',
+ 'ĝ' => 'Ĝ',
+ 'ğ' => 'Ğ',
+ 'ġ' => 'Ġ',
+ 'ģ' => 'Ģ',
+ 'ĥ' => 'Ĥ',
+ 'ħ' => 'Ħ',
+ 'ĩ' => 'Ĩ',
+ 'ī' => 'Ī',
+ 'ĭ' => 'Ĭ',
+ 'į' => 'Į',
+ 'ı' => 'I',
+ 'ij' => 'IJ',
+ 'ĵ' => 'Ĵ',
+ 'ķ' => 'Ķ',
+ 'ĺ' => 'Ĺ',
+ 'ļ' => 'Ļ',
+ 'ľ' => 'Ľ',
+ 'ŀ' => 'Ŀ',
+ 'ł' => 'Ł',
+ 'ń' => 'Ń',
+ 'ņ' => 'Ņ',
+ 'ň' => 'Ň',
+ 'ŋ' => 'Ŋ',
+ 'ō' => 'Ō',
+ 'ŏ' => 'Ŏ',
+ 'ő' => 'Ő',
+ 'œ' => 'Œ',
+ 'ŕ' => 'Ŕ',
+ 'ŗ' => 'Ŗ',
+ 'ř' => 'Ř',
+ 'ś' => 'Ś',
+ 'ŝ' => 'Ŝ',
+ 'ş' => 'Ş',
+ 'š' => 'Š',
+ 'ţ' => 'Ţ',
+ 'ť' => 'Ť',
+ 'ŧ' => 'Ŧ',
+ 'ũ' => 'Ũ',
+ 'ū' => 'Ū',
+ 'ŭ' => 'Ŭ',
+ 'ů' => 'Ů',
+ 'ű' => 'Ű',
+ 'ų' => 'Ų',
+ 'ŵ' => 'Ŵ',
+ 'ŷ' => 'Ŷ',
+ 'ź' => 'Ź',
+ 'ż' => 'Ż',
+ 'ž' => 'Ž',
+ 'ſ' => 'S',
+ 'ƀ' => 'Ƀ',
+ 'ƃ' => 'Ƃ',
+ 'ƅ' => 'Ƅ',
+ 'ƈ' => 'Ƈ',
+ 'ƌ' => 'Ƌ',
+ 'ƒ' => 'Ƒ',
+ 'ƕ' => 'Ƕ',
+ 'ƙ' => 'Ƙ',
+ 'ƚ' => 'Ƚ',
+ 'ƞ' => 'Ƞ',
+ 'ơ' => 'Ơ',
+ 'ƣ' => 'Ƣ',
+ 'ƥ' => 'Ƥ',
+ 'ƨ' => 'Ƨ',
+ 'ƭ' => 'Ƭ',
+ 'ư' => 'Ư',
+ 'ƴ' => 'Ƴ',
+ 'ƶ' => 'Ƶ',
+ 'ƹ' => 'Ƹ',
+ 'ƽ' => 'Ƽ',
+ 'ƿ' => 'Ƿ',
+ 'Dž' => 'DŽ',
+ 'dž' => 'DŽ',
+ 'Lj' => 'LJ',
+ 'lj' => 'LJ',
+ 'Nj' => 'NJ',
+ 'nj' => 'NJ',
+ 'ǎ' => 'Ǎ',
+ 'ǐ' => 'Ǐ',
+ 'ǒ' => 'Ǒ',
+ 'ǔ' => 'Ǔ',
+ 'ǖ' => 'Ǖ',
+ 'ǘ' => 'Ǘ',
+ 'ǚ' => 'Ǚ',
+ 'ǜ' => 'Ǜ',
+ 'ǝ' => 'Ǝ',
+ 'ǟ' => 'Ǟ',
+ 'ǡ' => 'Ǡ',
+ 'ǣ' => 'Ǣ',
+ 'ǥ' => 'Ǥ',
+ 'ǧ' => 'Ǧ',
+ 'ǩ' => 'Ǩ',
+ 'ǫ' => 'Ǫ',
+ 'ǭ' => 'Ǭ',
+ 'ǯ' => 'Ǯ',
+ 'Dz' => 'DZ',
+ 'dz' => 'DZ',
+ 'ǵ' => 'Ǵ',
+ 'ǹ' => 'Ǹ',
+ 'ǻ' => 'Ǻ',
+ 'ǽ' => 'Ǽ',
+ 'ǿ' => 'Ǿ',
+ 'ȁ' => 'Ȁ',
+ 'ȃ' => 'Ȃ',
+ 'ȅ' => 'Ȅ',
+ 'ȇ' => 'Ȇ',
+ 'ȉ' => 'Ȉ',
+ 'ȋ' => 'Ȋ',
+ 'ȍ' => 'Ȍ',
+ 'ȏ' => 'Ȏ',
+ 'ȑ' => 'Ȑ',
+ 'ȓ' => 'Ȓ',
+ 'ȕ' => 'Ȕ',
+ 'ȗ' => 'Ȗ',
+ 'ș' => 'Ș',
+ 'ț' => 'Ț',
+ 'ȝ' => 'Ȝ',
+ 'ȟ' => 'Ȟ',
+ 'ȣ' => 'Ȣ',
+ 'ȥ' => 'Ȥ',
+ 'ȧ' => 'Ȧ',
+ 'ȩ' => 'Ȩ',
+ 'ȫ' => 'Ȫ',
+ 'ȭ' => 'Ȭ',
+ 'ȯ' => 'Ȯ',
+ 'ȱ' => 'Ȱ',
+ 'ȳ' => 'Ȳ',
+ 'ȼ' => 'Ȼ',
+ 'ȿ' => 'Ȿ',
+ 'ɀ' => 'Ɀ',
+ 'ɂ' => 'Ɂ',
+ 'ɇ' => 'Ɇ',
+ 'ɉ' => 'Ɉ',
+ 'ɋ' => 'Ɋ',
+ 'ɍ' => 'Ɍ',
+ 'ɏ' => 'Ɏ',
+ 'ɐ' => 'Ɐ',
+ 'ɑ' => 'Ɑ',
+ 'ɒ' => 'Ɒ',
+ 'ɓ' => 'Ɓ',
+ 'ɔ' => 'Ɔ',
+ 'ɖ' => 'Ɖ',
+ 'ɗ' => 'Ɗ',
+ 'ə' => 'Ə',
+ 'ɛ' => 'Ɛ',
+ 'ɜ' => 'Ɜ',
+ 'ɠ' => 'Ɠ',
+ 'ɡ' => 'Ɡ',
+ 'ɣ' => 'Ɣ',
+ 'ɥ' => 'Ɥ',
+ 'ɦ' => 'Ɦ',
+ 'ɨ' => 'Ɨ',
+ 'ɩ' => 'Ɩ',
+ 'ɪ' => 'Ɪ',
+ 'ɫ' => 'Ɫ',
+ 'ɬ' => 'Ɬ',
+ 'ɯ' => 'Ɯ',
+ 'ɱ' => 'Ɱ',
+ 'ɲ' => 'Ɲ',
+ 'ɵ' => 'Ɵ',
+ 'ɽ' => 'Ɽ',
+ 'ʀ' => 'Ʀ',
+ 'ʂ' => 'Ʂ',
+ 'ʃ' => 'Ʃ',
+ 'ʇ' => 'Ʇ',
+ 'ʈ' => 'Ʈ',
+ 'ʉ' => 'Ʉ',
+ 'ʊ' => 'Ʊ',
+ 'ʋ' => 'Ʋ',
+ 'ʌ' => 'Ʌ',
+ 'ʒ' => 'Ʒ',
+ 'ʝ' => 'Ʝ',
+ 'ʞ' => 'Ʞ',
+ 'ͅ' => 'Ι',
+ 'ͱ' => 'Ͱ',
+ 'ͳ' => 'Ͳ',
+ 'ͷ' => 'Ͷ',
+ 'ͻ' => 'Ͻ',
+ 'ͼ' => 'Ͼ',
+ 'ͽ' => 'Ͽ',
+ 'ά' => 'Ά',
+ 'έ' => 'Έ',
+ 'ή' => 'Ή',
+ 'ί' => 'Ί',
+ 'α' => 'Α',
+ 'β' => 'Β',
+ 'γ' => 'Γ',
+ 'δ' => 'Δ',
+ 'ε' => 'Ε',
+ 'ζ' => 'Ζ',
+ 'η' => 'Η',
+ 'θ' => 'Θ',
+ 'ι' => 'Ι',
+ 'κ' => 'Κ',
+ 'λ' => 'Λ',
+ 'μ' => 'Μ',
+ 'ν' => 'Ν',
+ 'ξ' => 'Ξ',
+ 'ο' => 'Ο',
+ 'π' => 'Π',
+ 'ρ' => 'Ρ',
+ 'ς' => 'Σ',
+ 'σ' => 'Σ',
+ 'τ' => 'Τ',
+ 'υ' => 'Υ',
+ 'φ' => 'Φ',
+ 'χ' => 'Χ',
+ 'ψ' => 'Ψ',
+ 'ω' => 'Ω',
+ 'ϊ' => 'Ϊ',
+ 'ϋ' => 'Ϋ',
+ 'ό' => 'Ό',
+ 'ύ' => 'Ύ',
+ 'ώ' => 'Ώ',
+ 'ϐ' => 'Β',
+ 'ϑ' => 'Θ',
+ 'ϕ' => 'Φ',
+ 'ϖ' => 'Π',
+ 'ϗ' => 'Ϗ',
+ 'ϙ' => 'Ϙ',
+ 'ϛ' => 'Ϛ',
+ 'ϝ' => 'Ϝ',
+ 'ϟ' => 'Ϟ',
+ 'ϡ' => 'Ϡ',
+ 'ϣ' => 'Ϣ',
+ 'ϥ' => 'Ϥ',
+ 'ϧ' => 'Ϧ',
+ 'ϩ' => 'Ϩ',
+ 'ϫ' => 'Ϫ',
+ 'ϭ' => 'Ϭ',
+ 'ϯ' => 'Ϯ',
+ 'ϰ' => 'Κ',
+ 'ϱ' => 'Ρ',
+ 'ϲ' => 'Ϲ',
+ 'ϳ' => 'Ϳ',
+ 'ϵ' => 'Ε',
+ 'ϸ' => 'Ϸ',
+ 'ϻ' => 'Ϻ',
+ 'а' => 'А',
+ 'б' => 'Б',
+ 'в' => 'В',
+ 'г' => 'Г',
+ 'д' => 'Д',
+ 'е' => 'Е',
+ 'ж' => 'Ж',
+ 'з' => 'З',
+ 'и' => 'И',
+ 'й' => 'Й',
+ 'к' => 'К',
+ 'л' => 'Л',
+ 'м' => 'М',
+ 'н' => 'Н',
+ 'о' => 'О',
+ 'п' => 'П',
+ 'р' => 'Р',
+ 'с' => 'С',
+ 'т' => 'Т',
+ 'у' => 'У',
+ 'ф' => 'Ф',
+ 'х' => 'Х',
+ 'ц' => 'Ц',
+ 'ч' => 'Ч',
+ 'ш' => 'Ш',
+ 'щ' => 'Щ',
+ 'ъ' => 'Ъ',
+ 'ы' => 'Ы',
+ 'ь' => 'Ь',
+ 'э' => 'Э',
+ 'ю' => 'Ю',
+ 'я' => 'Я',
+ 'ѐ' => 'Ѐ',
+ 'ё' => 'Ё',
+ 'ђ' => 'Ђ',
+ 'ѓ' => 'Ѓ',
+ 'є' => 'Є',
+ 'ѕ' => 'Ѕ',
+ 'і' => 'І',
+ 'ї' => 'Ї',
+ 'ј' => 'Ј',
+ 'љ' => 'Љ',
+ 'њ' => 'Њ',
+ 'ћ' => 'Ћ',
+ 'ќ' => 'Ќ',
+ 'ѝ' => 'Ѝ',
+ 'ў' => 'Ў',
+ 'џ' => 'Џ',
+ 'ѡ' => 'Ѡ',
+ 'ѣ' => 'Ѣ',
+ 'ѥ' => 'Ѥ',
+ 'ѧ' => 'Ѧ',
+ 'ѩ' => 'Ѩ',
+ 'ѫ' => 'Ѫ',
+ 'ѭ' => 'Ѭ',
+ 'ѯ' => 'Ѯ',
+ 'ѱ' => 'Ѱ',
+ 'ѳ' => 'Ѳ',
+ 'ѵ' => 'Ѵ',
+ 'ѷ' => 'Ѷ',
+ 'ѹ' => 'Ѹ',
+ 'ѻ' => 'Ѻ',
+ 'ѽ' => 'Ѽ',
+ 'ѿ' => 'Ѿ',
+ 'ҁ' => 'Ҁ',
+ 'ҋ' => 'Ҋ',
+ 'ҍ' => 'Ҍ',
+ 'ҏ' => 'Ҏ',
+ 'ґ' => 'Ґ',
+ 'ғ' => 'Ғ',
+ 'ҕ' => 'Ҕ',
+ 'җ' => 'Җ',
+ 'ҙ' => 'Ҙ',
+ 'қ' => 'Қ',
+ 'ҝ' => 'Ҝ',
+ 'ҟ' => 'Ҟ',
+ 'ҡ' => 'Ҡ',
+ 'ң' => 'Ң',
+ 'ҥ' => 'Ҥ',
+ 'ҧ' => 'Ҧ',
+ 'ҩ' => 'Ҩ',
+ 'ҫ' => 'Ҫ',
+ 'ҭ' => 'Ҭ',
+ 'ү' => 'Ү',
+ 'ұ' => 'Ұ',
+ 'ҳ' => 'Ҳ',
+ 'ҵ' => 'Ҵ',
+ 'ҷ' => 'Ҷ',
+ 'ҹ' => 'Ҹ',
+ 'һ' => 'Һ',
+ 'ҽ' => 'Ҽ',
+ 'ҿ' => 'Ҿ',
+ 'ӂ' => 'Ӂ',
+ 'ӄ' => 'Ӄ',
+ 'ӆ' => 'Ӆ',
+ 'ӈ' => 'Ӈ',
+ 'ӊ' => 'Ӊ',
+ 'ӌ' => 'Ӌ',
+ 'ӎ' => 'Ӎ',
+ 'ӏ' => 'Ӏ',
+ 'ӑ' => 'Ӑ',
+ 'ӓ' => 'Ӓ',
+ 'ӕ' => 'Ӕ',
+ 'ӗ' => 'Ӗ',
+ 'ә' => 'Ә',
+ 'ӛ' => 'Ӛ',
+ 'ӝ' => 'Ӝ',
+ 'ӟ' => 'Ӟ',
+ 'ӡ' => 'Ӡ',
+ 'ӣ' => 'Ӣ',
+ 'ӥ' => 'Ӥ',
+ 'ӧ' => 'Ӧ',
+ 'ө' => 'Ө',
+ 'ӫ' => 'Ӫ',
+ 'ӭ' => 'Ӭ',
+ 'ӯ' => 'Ӯ',
+ 'ӱ' => 'Ӱ',
+ 'ӳ' => 'Ӳ',
+ 'ӵ' => 'Ӵ',
+ 'ӷ' => 'Ӷ',
+ 'ӹ' => 'Ӹ',
+ 'ӻ' => 'Ӻ',
+ 'ӽ' => 'Ӽ',
+ 'ӿ' => 'Ӿ',
+ 'ԁ' => 'Ԁ',
+ 'ԃ' => 'Ԃ',
+ 'ԅ' => 'Ԅ',
+ 'ԇ' => 'Ԇ',
+ 'ԉ' => 'Ԉ',
+ 'ԋ' => 'Ԋ',
+ 'ԍ' => 'Ԍ',
+ 'ԏ' => 'Ԏ',
+ 'ԑ' => 'Ԑ',
+ 'ԓ' => 'Ԓ',
+ 'ԕ' => 'Ԕ',
+ 'ԗ' => 'Ԗ',
+ 'ԙ' => 'Ԙ',
+ 'ԛ' => 'Ԛ',
+ 'ԝ' => 'Ԝ',
+ 'ԟ' => 'Ԟ',
+ 'ԡ' => 'Ԡ',
+ 'ԣ' => 'Ԣ',
+ 'ԥ' => 'Ԥ',
+ 'ԧ' => 'Ԧ',
+ 'ԩ' => 'Ԩ',
+ 'ԫ' => 'Ԫ',
+ 'ԭ' => 'Ԭ',
+ 'ԯ' => 'Ԯ',
+ 'ա' => 'Ա',
+ 'բ' => 'Բ',
+ 'գ' => 'Գ',
+ 'դ' => 'Դ',
+ 'ե' => 'Ե',
+ 'զ' => 'Զ',
+ 'է' => 'Է',
+ 'ը' => 'Ը',
+ 'թ' => 'Թ',
+ 'ժ' => 'Ժ',
+ 'ի' => 'Ի',
+ 'լ' => 'Լ',
+ 'խ' => 'Խ',
+ 'ծ' => 'Ծ',
+ 'կ' => 'Կ',
+ 'հ' => 'Հ',
+ 'ձ' => 'Ձ',
+ 'ղ' => 'Ղ',
+ 'ճ' => 'Ճ',
+ 'մ' => 'Մ',
+ 'յ' => 'Յ',
+ 'ն' => 'Ն',
+ 'շ' => 'Շ',
+ 'ո' => 'Ո',
+ 'չ' => 'Չ',
+ 'պ' => 'Պ',
+ 'ջ' => 'Ջ',
+ 'ռ' => 'Ռ',
+ 'ս' => 'Ս',
+ 'վ' => 'Վ',
+ 'տ' => 'Տ',
+ 'ր' => 'Ր',
+ 'ց' => 'Ց',
+ 'ւ' => 'Ւ',
+ 'փ' => 'Փ',
+ 'ք' => 'Ք',
+ 'օ' => 'Օ',
+ 'ֆ' => 'Ֆ',
+ 'ა' => 'Ა',
+ 'ბ' => 'Ბ',
+ 'გ' => 'Გ',
+ 'დ' => 'Დ',
+ 'ე' => 'Ე',
+ 'ვ' => 'Ვ',
+ 'ზ' => 'Ზ',
+ 'თ' => 'Თ',
+ 'ი' => 'Ი',
+ 'კ' => 'Კ',
+ 'ლ' => 'Ლ',
+ 'მ' => 'Მ',
+ 'ნ' => 'Ნ',
+ 'ო' => 'Ო',
+ 'პ' => 'Პ',
+ 'ჟ' => 'Ჟ',
+ 'რ' => 'Რ',
+ 'ს' => 'Ს',
+ 'ტ' => 'Ტ',
+ 'უ' => 'Უ',
+ 'ფ' => 'Ფ',
+ 'ქ' => 'Ქ',
+ 'ღ' => 'Ღ',
+ 'ყ' => 'Ყ',
+ 'შ' => 'Შ',
+ 'ჩ' => 'Ჩ',
+ 'ც' => 'Ც',
+ 'ძ' => 'Ძ',
+ 'წ' => 'Წ',
+ 'ჭ' => 'Ჭ',
+ 'ხ' => 'Ხ',
+ 'ჯ' => 'Ჯ',
+ 'ჰ' => 'Ჰ',
+ 'ჱ' => 'Ჱ',
+ 'ჲ' => 'Ჲ',
+ 'ჳ' => 'Ჳ',
+ 'ჴ' => 'Ჴ',
+ 'ჵ' => 'Ჵ',
+ 'ჶ' => 'Ჶ',
+ 'ჷ' => 'Ჷ',
+ 'ჸ' => 'Ჸ',
+ 'ჹ' => 'Ჹ',
+ 'ჺ' => 'Ჺ',
+ 'ჽ' => 'Ჽ',
+ 'ჾ' => 'Ჾ',
+ 'ჿ' => 'Ჿ',
+ 'ᏸ' => 'Ᏸ',
+ 'ᏹ' => 'Ᏹ',
+ 'ᏺ' => 'Ᏺ',
+ 'ᏻ' => 'Ᏻ',
+ 'ᏼ' => 'Ᏼ',
+ 'ᏽ' => 'Ᏽ',
+ 'ᲀ' => 'В',
+ 'ᲁ' => 'Д',
+ 'ᲂ' => 'О',
+ 'ᲃ' => 'С',
+ 'ᲄ' => 'Т',
+ 'ᲅ' => 'Т',
+ 'ᲆ' => 'Ъ',
+ 'ᲇ' => 'Ѣ',
+ 'ᲈ' => 'Ꙋ',
+ 'ᵹ' => 'Ᵹ',
+ 'ᵽ' => 'Ᵽ',
+ 'ᶎ' => 'Ᶎ',
+ 'ḁ' => 'Ḁ',
+ 'ḃ' => 'Ḃ',
+ 'ḅ' => 'Ḅ',
+ 'ḇ' => 'Ḇ',
+ 'ḉ' => 'Ḉ',
+ 'ḋ' => 'Ḋ',
+ 'ḍ' => 'Ḍ',
+ 'ḏ' => 'Ḏ',
+ 'ḑ' => 'Ḑ',
+ 'ḓ' => 'Ḓ',
+ 'ḕ' => 'Ḕ',
+ 'ḗ' => 'Ḗ',
+ 'ḙ' => 'Ḙ',
+ 'ḛ' => 'Ḛ',
+ 'ḝ' => 'Ḝ',
+ 'ḟ' => 'Ḟ',
+ 'ḡ' => 'Ḡ',
+ 'ḣ' => 'Ḣ',
+ 'ḥ' => 'Ḥ',
+ 'ḧ' => 'Ḧ',
+ 'ḩ' => 'Ḩ',
+ 'ḫ' => 'Ḫ',
+ 'ḭ' => 'Ḭ',
+ 'ḯ' => 'Ḯ',
+ 'ḱ' => 'Ḱ',
+ 'ḳ' => 'Ḳ',
+ 'ḵ' => 'Ḵ',
+ 'ḷ' => 'Ḷ',
+ 'ḹ' => 'Ḹ',
+ 'ḻ' => 'Ḻ',
+ 'ḽ' => 'Ḽ',
+ 'ḿ' => 'Ḿ',
+ 'ṁ' => 'Ṁ',
+ 'ṃ' => 'Ṃ',
+ 'ṅ' => 'Ṅ',
+ 'ṇ' => 'Ṇ',
+ 'ṉ' => 'Ṉ',
+ 'ṋ' => 'Ṋ',
+ 'ṍ' => 'Ṍ',
+ 'ṏ' => 'Ṏ',
+ 'ṑ' => 'Ṑ',
+ 'ṓ' => 'Ṓ',
+ 'ṕ' => 'Ṕ',
+ 'ṗ' => 'Ṗ',
+ 'ṙ' => 'Ṙ',
+ 'ṛ' => 'Ṛ',
+ 'ṝ' => 'Ṝ',
+ 'ṟ' => 'Ṟ',
+ 'ṡ' => 'Ṡ',
+ 'ṣ' => 'Ṣ',
+ 'ṥ' => 'Ṥ',
+ 'ṧ' => 'Ṧ',
+ 'ṩ' => 'Ṩ',
+ 'ṫ' => 'Ṫ',
+ 'ṭ' => 'Ṭ',
+ 'ṯ' => 'Ṯ',
+ 'ṱ' => 'Ṱ',
+ 'ṳ' => 'Ṳ',
+ 'ṵ' => 'Ṵ',
+ 'ṷ' => 'Ṷ',
+ 'ṹ' => 'Ṹ',
+ 'ṻ' => 'Ṻ',
+ 'ṽ' => 'Ṽ',
+ 'ṿ' => 'Ṿ',
+ 'ẁ' => 'Ẁ',
+ 'ẃ' => 'Ẃ',
+ 'ẅ' => 'Ẅ',
+ 'ẇ' => 'Ẇ',
+ 'ẉ' => 'Ẉ',
+ 'ẋ' => 'Ẋ',
+ 'ẍ' => 'Ẍ',
+ 'ẏ' => 'Ẏ',
+ 'ẑ' => 'Ẑ',
+ 'ẓ' => 'Ẓ',
+ 'ẕ' => 'Ẕ',
+ 'ẛ' => 'Ṡ',
+ 'ạ' => 'Ạ',
+ 'ả' => 'Ả',
+ 'ấ' => 'Ấ',
+ 'ầ' => 'Ầ',
+ 'ẩ' => 'Ẩ',
+ 'ẫ' => 'Ẫ',
+ 'ậ' => 'Ậ',
+ 'ắ' => 'Ắ',
+ 'ằ' => 'Ằ',
+ 'ẳ' => 'Ẳ',
+ 'ẵ' => 'Ẵ',
+ 'ặ' => 'Ặ',
+ 'ẹ' => 'Ẹ',
+ 'ẻ' => 'Ẻ',
+ 'ẽ' => 'Ẽ',
+ 'ế' => 'Ế',
+ 'ề' => 'Ề',
+ 'ể' => 'Ể',
+ 'ễ' => 'Ễ',
+ 'ệ' => 'Ệ',
+ 'ỉ' => 'Ỉ',
+ 'ị' => 'Ị',
+ 'ọ' => 'Ọ',
+ 'ỏ' => 'Ỏ',
+ 'ố' => 'Ố',
+ 'ồ' => 'Ồ',
+ 'ổ' => 'Ổ',
+ 'ỗ' => 'Ỗ',
+ 'ộ' => 'Ộ',
+ 'ớ' => 'Ớ',
+ 'ờ' => 'Ờ',
+ 'ở' => 'Ở',
+ 'ỡ' => 'Ỡ',
+ 'ợ' => 'Ợ',
+ 'ụ' => 'Ụ',
+ 'ủ' => 'Ủ',
+ 'ứ' => 'Ứ',
+ 'ừ' => 'Ừ',
+ 'ử' => 'Ử',
+ 'ữ' => 'Ữ',
+ 'ự' => 'Ự',
+ 'ỳ' => 'Ỳ',
+ 'ỵ' => 'Ỵ',
+ 'ỷ' => 'Ỷ',
+ 'ỹ' => 'Ỹ',
+ 'ỻ' => 'Ỻ',
+ 'ỽ' => 'Ỽ',
+ 'ỿ' => 'Ỿ',
+ 'ἀ' => 'Ἀ',
+ 'ἁ' => 'Ἁ',
+ 'ἂ' => 'Ἂ',
+ 'ἃ' => 'Ἃ',
+ 'ἄ' => 'Ἄ',
+ 'ἅ' => 'Ἅ',
+ 'ἆ' => 'Ἆ',
+ 'ἇ' => 'Ἇ',
+ 'ἐ' => 'Ἐ',
+ 'ἑ' => 'Ἑ',
+ 'ἒ' => 'Ἒ',
+ 'ἓ' => 'Ἓ',
+ 'ἔ' => 'Ἔ',
+ 'ἕ' => 'Ἕ',
+ 'ἠ' => 'Ἠ',
+ 'ἡ' => 'Ἡ',
+ 'ἢ' => 'Ἢ',
+ 'ἣ' => 'Ἣ',
+ 'ἤ' => 'Ἤ',
+ 'ἥ' => 'Ἥ',
+ 'ἦ' => 'Ἦ',
+ 'ἧ' => 'Ἧ',
+ 'ἰ' => 'Ἰ',
+ 'ἱ' => 'Ἱ',
+ 'ἲ' => 'Ἲ',
+ 'ἳ' => 'Ἳ',
+ 'ἴ' => 'Ἴ',
+ 'ἵ' => 'Ἵ',
+ 'ἶ' => 'Ἶ',
+ 'ἷ' => 'Ἷ',
+ 'ὀ' => 'Ὀ',
+ 'ὁ' => 'Ὁ',
+ 'ὂ' => 'Ὂ',
+ 'ὃ' => 'Ὃ',
+ 'ὄ' => 'Ὄ',
+ 'ὅ' => 'Ὅ',
+ 'ὑ' => 'Ὑ',
+ 'ὓ' => 'Ὓ',
+ 'ὕ' => 'Ὕ',
+ 'ὗ' => 'Ὗ',
+ 'ὠ' => 'Ὠ',
+ 'ὡ' => 'Ὡ',
+ 'ὢ' => 'Ὢ',
+ 'ὣ' => 'Ὣ',
+ 'ὤ' => 'Ὤ',
+ 'ὥ' => 'Ὥ',
+ 'ὦ' => 'Ὦ',
+ 'ὧ' => 'Ὧ',
+ 'ὰ' => 'Ὰ',
+ 'ά' => 'Ά',
+ 'ὲ' => 'Ὲ',
+ 'έ' => 'Έ',
+ 'ὴ' => 'Ὴ',
+ 'ή' => 'Ή',
+ 'ὶ' => 'Ὶ',
+ 'ί' => 'Ί',
+ 'ὸ' => 'Ὸ',
+ 'ό' => 'Ό',
+ 'ὺ' => 'Ὺ',
+ 'ύ' => 'Ύ',
+ 'ὼ' => 'Ὼ',
+ 'ώ' => 'Ώ',
+ 'ᾀ' => 'ᾈ',
+ 'ᾁ' => 'ᾉ',
+ 'ᾂ' => 'ᾊ',
+ 'ᾃ' => 'ᾋ',
+ 'ᾄ' => 'ᾌ',
+ 'ᾅ' => 'ᾍ',
+ 'ᾆ' => 'ᾎ',
+ 'ᾇ' => 'ᾏ',
+ 'ᾐ' => 'ᾘ',
+ 'ᾑ' => 'ᾙ',
+ 'ᾒ' => 'ᾚ',
+ 'ᾓ' => 'ᾛ',
+ 'ᾔ' => 'ᾜ',
+ 'ᾕ' => 'ᾝ',
+ 'ᾖ' => 'ᾞ',
+ 'ᾗ' => 'ᾟ',
+ 'ᾠ' => 'ᾨ',
+ 'ᾡ' => 'ᾩ',
+ 'ᾢ' => 'ᾪ',
+ 'ᾣ' => 'ᾫ',
+ 'ᾤ' => 'ᾬ',
+ 'ᾥ' => 'ᾭ',
+ 'ᾦ' => 'ᾮ',
+ 'ᾧ' => 'ᾯ',
+ 'ᾰ' => 'Ᾰ',
+ 'ᾱ' => 'Ᾱ',
+ 'ᾳ' => 'ᾼ',
+ 'ι' => 'Ι',
+ 'ῃ' => 'ῌ',
+ 'ῐ' => 'Ῐ',
+ 'ῑ' => 'Ῑ',
+ 'ῠ' => 'Ῠ',
+ 'ῡ' => 'Ῡ',
+ 'ῥ' => 'Ῥ',
+ 'ῳ' => 'ῼ',
+ 'ⅎ' => 'Ⅎ',
+ 'ⅰ' => 'Ⅰ',
+ 'ⅱ' => 'Ⅱ',
+ 'ⅲ' => 'Ⅲ',
+ 'ⅳ' => 'Ⅳ',
+ 'ⅴ' => 'Ⅴ',
+ 'ⅵ' => 'Ⅵ',
+ 'ⅶ' => 'Ⅶ',
+ 'ⅷ' => 'Ⅷ',
+ 'ⅸ' => 'Ⅸ',
+ 'ⅹ' => 'Ⅹ',
+ 'ⅺ' => 'Ⅺ',
+ 'ⅻ' => 'Ⅻ',
+ 'ⅼ' => 'Ⅼ',
+ 'ⅽ' => 'Ⅽ',
+ 'ⅾ' => 'Ⅾ',
+ 'ⅿ' => 'Ⅿ',
+ 'ↄ' => 'Ↄ',
+ 'ⓐ' => 'Ⓐ',
+ 'ⓑ' => 'Ⓑ',
+ 'ⓒ' => 'Ⓒ',
+ 'ⓓ' => 'Ⓓ',
+ 'ⓔ' => 'Ⓔ',
+ 'ⓕ' => 'Ⓕ',
+ 'ⓖ' => 'Ⓖ',
+ 'ⓗ' => 'Ⓗ',
+ 'ⓘ' => 'Ⓘ',
+ 'ⓙ' => 'Ⓙ',
+ 'ⓚ' => 'Ⓚ',
+ 'ⓛ' => 'Ⓛ',
+ 'ⓜ' => 'Ⓜ',
+ 'ⓝ' => 'Ⓝ',
+ 'ⓞ' => 'Ⓞ',
+ 'ⓟ' => 'Ⓟ',
+ 'ⓠ' => 'Ⓠ',
+ 'ⓡ' => 'Ⓡ',
+ 'ⓢ' => 'Ⓢ',
+ 'ⓣ' => 'Ⓣ',
+ 'ⓤ' => 'Ⓤ',
+ 'ⓥ' => 'Ⓥ',
+ 'ⓦ' => 'Ⓦ',
+ 'ⓧ' => 'Ⓧ',
+ 'ⓨ' => 'Ⓨ',
+ 'ⓩ' => 'Ⓩ',
+ 'ⰰ' => 'Ⰰ',
+ 'ⰱ' => 'Ⰱ',
+ 'ⰲ' => 'Ⰲ',
+ 'ⰳ' => 'Ⰳ',
+ 'ⰴ' => 'Ⰴ',
+ 'ⰵ' => 'Ⰵ',
+ 'ⰶ' => 'Ⰶ',
+ 'ⰷ' => 'Ⰷ',
+ 'ⰸ' => 'Ⰸ',
+ 'ⰹ' => 'Ⰹ',
+ 'ⰺ' => 'Ⰺ',
+ 'ⰻ' => 'Ⰻ',
+ 'ⰼ' => 'Ⰼ',
+ 'ⰽ' => 'Ⰽ',
+ 'ⰾ' => 'Ⰾ',
+ 'ⰿ' => 'Ⰿ',
+ 'ⱀ' => 'Ⱀ',
+ 'ⱁ' => 'Ⱁ',
+ 'ⱂ' => 'Ⱂ',
+ 'ⱃ' => 'Ⱃ',
+ 'ⱄ' => 'Ⱄ',
+ 'ⱅ' => 'Ⱅ',
+ 'ⱆ' => 'Ⱆ',
+ 'ⱇ' => 'Ⱇ',
+ 'ⱈ' => 'Ⱈ',
+ 'ⱉ' => 'Ⱉ',
+ 'ⱊ' => 'Ⱊ',
+ 'ⱋ' => 'Ⱋ',
+ 'ⱌ' => 'Ⱌ',
+ 'ⱍ' => 'Ⱍ',
+ 'ⱎ' => 'Ⱎ',
+ 'ⱏ' => 'Ⱏ',
+ 'ⱐ' => 'Ⱐ',
+ 'ⱑ' => 'Ⱑ',
+ 'ⱒ' => 'Ⱒ',
+ 'ⱓ' => 'Ⱓ',
+ 'ⱔ' => 'Ⱔ',
+ 'ⱕ' => 'Ⱕ',
+ 'ⱖ' => 'Ⱖ',
+ 'ⱗ' => 'Ⱗ',
+ 'ⱘ' => 'Ⱘ',
+ 'ⱙ' => 'Ⱙ',
+ 'ⱚ' => 'Ⱚ',
+ 'ⱛ' => 'Ⱛ',
+ 'ⱜ' => 'Ⱜ',
+ 'ⱝ' => 'Ⱝ',
+ 'ⱞ' => 'Ⱞ',
+ 'ⱡ' => 'Ⱡ',
+ 'ⱥ' => 'Ⱥ',
+ 'ⱦ' => 'Ⱦ',
+ 'ⱨ' => 'Ⱨ',
+ 'ⱪ' => 'Ⱪ',
+ 'ⱬ' => 'Ⱬ',
+ 'ⱳ' => 'Ⱳ',
+ 'ⱶ' => 'Ⱶ',
+ 'ⲁ' => 'Ⲁ',
+ 'ⲃ' => 'Ⲃ',
+ 'ⲅ' => 'Ⲅ',
+ 'ⲇ' => 'Ⲇ',
+ 'ⲉ' => 'Ⲉ',
+ 'ⲋ' => 'Ⲋ',
+ 'ⲍ' => 'Ⲍ',
+ 'ⲏ' => 'Ⲏ',
+ 'ⲑ' => 'Ⲑ',
+ 'ⲓ' => 'Ⲓ',
+ 'ⲕ' => 'Ⲕ',
+ 'ⲗ' => 'Ⲗ',
+ 'ⲙ' => 'Ⲙ',
+ 'ⲛ' => 'Ⲛ',
+ 'ⲝ' => 'Ⲝ',
+ 'ⲟ' => 'Ⲟ',
+ 'ⲡ' => 'Ⲡ',
+ 'ⲣ' => 'Ⲣ',
+ 'ⲥ' => 'Ⲥ',
+ 'ⲧ' => 'Ⲧ',
+ 'ⲩ' => 'Ⲩ',
+ 'ⲫ' => 'Ⲫ',
+ 'ⲭ' => 'Ⲭ',
+ 'ⲯ' => 'Ⲯ',
+ 'ⲱ' => 'Ⲱ',
+ 'ⲳ' => 'Ⲳ',
+ 'ⲵ' => 'Ⲵ',
+ 'ⲷ' => 'Ⲷ',
+ 'ⲹ' => 'Ⲹ',
+ 'ⲻ' => 'Ⲻ',
+ 'ⲽ' => 'Ⲽ',
+ 'ⲿ' => 'Ⲿ',
+ 'ⳁ' => 'Ⳁ',
+ 'ⳃ' => 'Ⳃ',
+ 'ⳅ' => 'Ⳅ',
+ 'ⳇ' => 'Ⳇ',
+ 'ⳉ' => 'Ⳉ',
+ 'ⳋ' => 'Ⳋ',
+ 'ⳍ' => 'Ⳍ',
+ 'ⳏ' => 'Ⳏ',
+ 'ⳑ' => 'Ⳑ',
+ 'ⳓ' => 'Ⳓ',
+ 'ⳕ' => 'Ⳕ',
+ 'ⳗ' => 'Ⳗ',
+ 'ⳙ' => 'Ⳙ',
+ 'ⳛ' => 'Ⳛ',
+ 'ⳝ' => 'Ⳝ',
+ 'ⳟ' => 'Ⳟ',
+ 'ⳡ' => 'Ⳡ',
+ 'ⳣ' => 'Ⳣ',
+ 'ⳬ' => 'Ⳬ',
+ 'ⳮ' => 'Ⳮ',
+ 'ⳳ' => 'Ⳳ',
+ 'ⴀ' => 'Ⴀ',
+ 'ⴁ' => 'Ⴁ',
+ 'ⴂ' => 'Ⴂ',
+ 'ⴃ' => 'Ⴃ',
+ 'ⴄ' => 'Ⴄ',
+ 'ⴅ' => 'Ⴅ',
+ 'ⴆ' => 'Ⴆ',
+ 'ⴇ' => 'Ⴇ',
+ 'ⴈ' => 'Ⴈ',
+ 'ⴉ' => 'Ⴉ',
+ 'ⴊ' => 'Ⴊ',
+ 'ⴋ' => 'Ⴋ',
+ 'ⴌ' => 'Ⴌ',
+ 'ⴍ' => 'Ⴍ',
+ 'ⴎ' => 'Ⴎ',
+ 'ⴏ' => 'Ⴏ',
+ 'ⴐ' => 'Ⴐ',
+ 'ⴑ' => 'Ⴑ',
+ 'ⴒ' => 'Ⴒ',
+ 'ⴓ' => 'Ⴓ',
+ 'ⴔ' => 'Ⴔ',
+ 'ⴕ' => 'Ⴕ',
+ 'ⴖ' => 'Ⴖ',
+ 'ⴗ' => 'Ⴗ',
+ 'ⴘ' => 'Ⴘ',
+ 'ⴙ' => 'Ⴙ',
+ 'ⴚ' => 'Ⴚ',
+ 'ⴛ' => 'Ⴛ',
+ 'ⴜ' => 'Ⴜ',
+ 'ⴝ' => 'Ⴝ',
+ 'ⴞ' => 'Ⴞ',
+ 'ⴟ' => 'Ⴟ',
+ 'ⴠ' => 'Ⴠ',
+ 'ⴡ' => 'Ⴡ',
+ 'ⴢ' => 'Ⴢ',
+ 'ⴣ' => 'Ⴣ',
+ 'ⴤ' => 'Ⴤ',
+ 'ⴥ' => 'Ⴥ',
+ 'ⴧ' => 'Ⴧ',
+ 'ⴭ' => 'Ⴭ',
+ 'ꙁ' => 'Ꙁ',
+ 'ꙃ' => 'Ꙃ',
+ 'ꙅ' => 'Ꙅ',
+ 'ꙇ' => 'Ꙇ',
+ 'ꙉ' => 'Ꙉ',
+ 'ꙋ' => 'Ꙋ',
+ 'ꙍ' => 'Ꙍ',
+ 'ꙏ' => 'Ꙏ',
+ 'ꙑ' => 'Ꙑ',
+ 'ꙓ' => 'Ꙓ',
+ 'ꙕ' => 'Ꙕ',
+ 'ꙗ' => 'Ꙗ',
+ 'ꙙ' => 'Ꙙ',
+ 'ꙛ' => 'Ꙛ',
+ 'ꙝ' => 'Ꙝ',
+ 'ꙟ' => 'Ꙟ',
+ 'ꙡ' => 'Ꙡ',
+ 'ꙣ' => 'Ꙣ',
+ 'ꙥ' => 'Ꙥ',
+ 'ꙧ' => 'Ꙧ',
+ 'ꙩ' => 'Ꙩ',
+ 'ꙫ' => 'Ꙫ',
+ 'ꙭ' => 'Ꙭ',
+ 'ꚁ' => 'Ꚁ',
+ 'ꚃ' => 'Ꚃ',
+ 'ꚅ' => 'Ꚅ',
+ 'ꚇ' => 'Ꚇ',
+ 'ꚉ' => 'Ꚉ',
+ 'ꚋ' => 'Ꚋ',
+ 'ꚍ' => 'Ꚍ',
+ 'ꚏ' => 'Ꚏ',
+ 'ꚑ' => 'Ꚑ',
+ 'ꚓ' => 'Ꚓ',
+ 'ꚕ' => 'Ꚕ',
+ 'ꚗ' => 'Ꚗ',
+ 'ꚙ' => 'Ꚙ',
+ 'ꚛ' => 'Ꚛ',
+ 'ꜣ' => 'Ꜣ',
+ 'ꜥ' => 'Ꜥ',
+ 'ꜧ' => 'Ꜧ',
+ 'ꜩ' => 'Ꜩ',
+ 'ꜫ' => 'Ꜫ',
+ 'ꜭ' => 'Ꜭ',
+ 'ꜯ' => 'Ꜯ',
+ 'ꜳ' => 'Ꜳ',
+ 'ꜵ' => 'Ꜵ',
+ 'ꜷ' => 'Ꜷ',
+ 'ꜹ' => 'Ꜹ',
+ 'ꜻ' => 'Ꜻ',
+ 'ꜽ' => 'Ꜽ',
+ 'ꜿ' => 'Ꜿ',
+ 'ꝁ' => 'Ꝁ',
+ 'ꝃ' => 'Ꝃ',
+ 'ꝅ' => 'Ꝅ',
+ 'ꝇ' => 'Ꝇ',
+ 'ꝉ' => 'Ꝉ',
+ 'ꝋ' => 'Ꝋ',
+ 'ꝍ' => 'Ꝍ',
+ 'ꝏ' => 'Ꝏ',
+ 'ꝑ' => 'Ꝑ',
+ 'ꝓ' => 'Ꝓ',
+ 'ꝕ' => 'Ꝕ',
+ 'ꝗ' => 'Ꝗ',
+ 'ꝙ' => 'Ꝙ',
+ 'ꝛ' => 'Ꝛ',
+ 'ꝝ' => 'Ꝝ',
+ 'ꝟ' => 'Ꝟ',
+ 'ꝡ' => 'Ꝡ',
+ 'ꝣ' => 'Ꝣ',
+ 'ꝥ' => 'Ꝥ',
+ 'ꝧ' => 'Ꝧ',
+ 'ꝩ' => 'Ꝩ',
+ 'ꝫ' => 'Ꝫ',
+ 'ꝭ' => 'Ꝭ',
+ 'ꝯ' => 'Ꝯ',
+ 'ꝺ' => 'Ꝺ',
+ 'ꝼ' => 'Ꝼ',
+ 'ꝿ' => 'Ꝿ',
+ 'ꞁ' => 'Ꞁ',
+ 'ꞃ' => 'Ꞃ',
+ 'ꞅ' => 'Ꞅ',
+ 'ꞇ' => 'Ꞇ',
+ 'ꞌ' => 'Ꞌ',
+ 'ꞑ' => 'Ꞑ',
+ 'ꞓ' => 'Ꞓ',
+ 'ꞔ' => 'Ꞔ',
+ 'ꞗ' => 'Ꞗ',
+ 'ꞙ' => 'Ꞙ',
+ 'ꞛ' => 'Ꞛ',
+ 'ꞝ' => 'Ꞝ',
+ 'ꞟ' => 'Ꞟ',
+ 'ꞡ' => 'Ꞡ',
+ 'ꞣ' => 'Ꞣ',
+ 'ꞥ' => 'Ꞥ',
+ 'ꞧ' => 'Ꞧ',
+ 'ꞩ' => 'Ꞩ',
+ 'ꞵ' => 'Ꞵ',
+ 'ꞷ' => 'Ꞷ',
+ 'ꞹ' => 'Ꞹ',
+ 'ꞻ' => 'Ꞻ',
+ 'ꞽ' => 'Ꞽ',
+ 'ꞿ' => 'Ꞿ',
+ 'ꟃ' => 'Ꟃ',
+ 'ꟈ' => 'Ꟈ',
+ 'ꟊ' => 'Ꟊ',
+ 'ꟶ' => 'Ꟶ',
+ 'ꭓ' => 'Ꭓ',
+ 'ꭰ' => 'Ꭰ',
+ 'ꭱ' => 'Ꭱ',
+ 'ꭲ' => 'Ꭲ',
+ 'ꭳ' => 'Ꭳ',
+ 'ꭴ' => 'Ꭴ',
+ 'ꭵ' => 'Ꭵ',
+ 'ꭶ' => 'Ꭶ',
+ 'ꭷ' => 'Ꭷ',
+ 'ꭸ' => 'Ꭸ',
+ 'ꭹ' => 'Ꭹ',
+ 'ꭺ' => 'Ꭺ',
+ 'ꭻ' => 'Ꭻ',
+ 'ꭼ' => 'Ꭼ',
+ 'ꭽ' => 'Ꭽ',
+ 'ꭾ' => 'Ꭾ',
+ 'ꭿ' => 'Ꭿ',
+ 'ꮀ' => 'Ꮀ',
+ 'ꮁ' => 'Ꮁ',
+ 'ꮂ' => 'Ꮂ',
+ 'ꮃ' => 'Ꮃ',
+ 'ꮄ' => 'Ꮄ',
+ 'ꮅ' => 'Ꮅ',
+ 'ꮆ' => 'Ꮆ',
+ 'ꮇ' => 'Ꮇ',
+ 'ꮈ' => 'Ꮈ',
+ 'ꮉ' => 'Ꮉ',
+ 'ꮊ' => 'Ꮊ',
+ 'ꮋ' => 'Ꮋ',
+ 'ꮌ' => 'Ꮌ',
+ 'ꮍ' => 'Ꮍ',
+ 'ꮎ' => 'Ꮎ',
+ 'ꮏ' => 'Ꮏ',
+ 'ꮐ' => 'Ꮐ',
+ 'ꮑ' => 'Ꮑ',
+ 'ꮒ' => 'Ꮒ',
+ 'ꮓ' => 'Ꮓ',
+ 'ꮔ' => 'Ꮔ',
+ 'ꮕ' => 'Ꮕ',
+ 'ꮖ' => 'Ꮖ',
+ 'ꮗ' => 'Ꮗ',
+ 'ꮘ' => 'Ꮘ',
+ 'ꮙ' => 'Ꮙ',
+ 'ꮚ' => 'Ꮚ',
+ 'ꮛ' => 'Ꮛ',
+ 'ꮜ' => 'Ꮜ',
+ 'ꮝ' => 'Ꮝ',
+ 'ꮞ' => 'Ꮞ',
+ 'ꮟ' => 'Ꮟ',
+ 'ꮠ' => 'Ꮠ',
+ 'ꮡ' => 'Ꮡ',
+ 'ꮢ' => 'Ꮢ',
+ 'ꮣ' => 'Ꮣ',
+ 'ꮤ' => 'Ꮤ',
+ 'ꮥ' => 'Ꮥ',
+ 'ꮦ' => 'Ꮦ',
+ 'ꮧ' => 'Ꮧ',
+ 'ꮨ' => 'Ꮨ',
+ 'ꮩ' => 'Ꮩ',
+ 'ꮪ' => 'Ꮪ',
+ 'ꮫ' => 'Ꮫ',
+ 'ꮬ' => 'Ꮬ',
+ 'ꮭ' => 'Ꮭ',
+ 'ꮮ' => 'Ꮮ',
+ 'ꮯ' => 'Ꮯ',
+ 'ꮰ' => 'Ꮰ',
+ 'ꮱ' => 'Ꮱ',
+ 'ꮲ' => 'Ꮲ',
+ 'ꮳ' => 'Ꮳ',
+ 'ꮴ' => 'Ꮴ',
+ 'ꮵ' => 'Ꮵ',
+ 'ꮶ' => 'Ꮶ',
+ 'ꮷ' => 'Ꮷ',
+ 'ꮸ' => 'Ꮸ',
+ 'ꮹ' => 'Ꮹ',
+ 'ꮺ' => 'Ꮺ',
+ 'ꮻ' => 'Ꮻ',
+ 'ꮼ' => 'Ꮼ',
+ 'ꮽ' => 'Ꮽ',
+ 'ꮾ' => 'Ꮾ',
+ 'ꮿ' => 'Ꮿ',
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ '𐐨' => '𐐀',
+ '𐐩' => '𐐁',
+ '𐐪' => '𐐂',
+ '𐐫' => '𐐃',
+ '𐐬' => '𐐄',
+ '𐐭' => '𐐅',
+ '𐐮' => '𐐆',
+ '𐐯' => '𐐇',
+ '𐐰' => '𐐈',
+ '𐐱' => '𐐉',
+ '𐐲' => '𐐊',
+ '𐐳' => '𐐋',
+ '𐐴' => '𐐌',
+ '𐐵' => '𐐍',
+ '𐐶' => '𐐎',
+ '𐐷' => '𐐏',
+ '𐐸' => '𐐐',
+ '𐐹' => '𐐑',
+ '𐐺' => '𐐒',
+ '𐐻' => '𐐓',
+ '𐐼' => '𐐔',
+ '𐐽' => '𐐕',
+ '𐐾' => '𐐖',
+ '𐐿' => '𐐗',
+ '𐑀' => '𐐘',
+ '𐑁' => '𐐙',
+ '𐑂' => '𐐚',
+ '𐑃' => '𐐛',
+ '𐑄' => '𐐜',
+ '𐑅' => '𐐝',
+ '𐑆' => '𐐞',
+ '𐑇' => '𐐟',
+ '𐑈' => '𐐠',
+ '𐑉' => '𐐡',
+ '𐑊' => '𐐢',
+ '𐑋' => '𐐣',
+ '𐑌' => '𐐤',
+ '𐑍' => '𐐥',
+ '𐑎' => '𐐦',
+ '𐑏' => '𐐧',
+ '𐓘' => '𐒰',
+ '𐓙' => '𐒱',
+ '𐓚' => '𐒲',
+ '𐓛' => '𐒳',
+ '𐓜' => '𐒴',
+ '𐓝' => '𐒵',
+ '𐓞' => '𐒶',
+ '𐓟' => '𐒷',
+ '𐓠' => '𐒸',
+ '𐓡' => '𐒹',
+ '𐓢' => '𐒺',
+ '𐓣' => '𐒻',
+ '𐓤' => '𐒼',
+ '𐓥' => '𐒽',
+ '𐓦' => '𐒾',
+ '𐓧' => '𐒿',
+ '𐓨' => '𐓀',
+ '𐓩' => '𐓁',
+ '𐓪' => '𐓂',
+ '𐓫' => '𐓃',
+ '𐓬' => '𐓄',
+ '𐓭' => '𐓅',
+ '𐓮' => '𐓆',
+ '𐓯' => '𐓇',
+ '𐓰' => '𐓈',
+ '𐓱' => '𐓉',
+ '𐓲' => '𐓊',
+ '𐓳' => '𐓋',
+ '𐓴' => '𐓌',
+ '𐓵' => '𐓍',
+ '𐓶' => '𐓎',
+ '𐓷' => '𐓏',
+ '𐓸' => '𐓐',
+ '𐓹' => '𐓑',
+ '𐓺' => '𐓒',
+ '𐓻' => '𐓓',
+ '𐳀' => '𐲀',
+ '𐳁' => '𐲁',
+ '𐳂' => '𐲂',
+ '𐳃' => '𐲃',
+ '𐳄' => '𐲄',
+ '𐳅' => '𐲅',
+ '𐳆' => '𐲆',
+ '𐳇' => '𐲇',
+ '𐳈' => '𐲈',
+ '𐳉' => '𐲉',
+ '𐳊' => '𐲊',
+ '𐳋' => '𐲋',
+ '𐳌' => '𐲌',
+ '𐳍' => '𐲍',
+ '𐳎' => '𐲎',
+ '𐳏' => '𐲏',
+ '𐳐' => '𐲐',
+ '𐳑' => '𐲑',
+ '𐳒' => '𐲒',
+ '𐳓' => '𐲓',
+ '𐳔' => '𐲔',
+ '𐳕' => '𐲕',
+ '𐳖' => '𐲖',
+ '𐳗' => '𐲗',
+ '𐳘' => '𐲘',
+ '𐳙' => '𐲙',
+ '𐳚' => '𐲚',
+ '𐳛' => '𐲛',
+ '𐳜' => '𐲜',
+ '𐳝' => '𐲝',
+ '𐳞' => '𐲞',
+ '𐳟' => '𐲟',
+ '𐳠' => '𐲠',
+ '𐳡' => '𐲡',
+ '𐳢' => '𐲢',
+ '𐳣' => '𐲣',
+ '𐳤' => '𐲤',
+ '𐳥' => '𐲥',
+ '𐳦' => '𐲦',
+ '𐳧' => '𐲧',
+ '𐳨' => '𐲨',
+ '𐳩' => '𐲩',
+ '𐳪' => '𐲪',
+ '𐳫' => '𐲫',
+ '𐳬' => '𐲬',
+ '𐳭' => '𐲭',
+ '𐳮' => '𐲮',
+ '𐳯' => '𐲯',
+ '𐳰' => '𐲰',
+ '𐳱' => '𐲱',
+ '𐳲' => '𐲲',
+ '𑣀' => '𑢠',
+ '𑣁' => '𑢡',
+ '𑣂' => '𑢢',
+ '𑣃' => '𑢣',
+ '𑣄' => '𑢤',
+ '𑣅' => '𑢥',
+ '𑣆' => '𑢦',
+ '𑣇' => '𑢧',
+ '𑣈' => '𑢨',
+ '𑣉' => '𑢩',
+ '𑣊' => '𑢪',
+ '𑣋' => '𑢫',
+ '𑣌' => '𑢬',
+ '𑣍' => '𑢭',
+ '𑣎' => '𑢮',
+ '𑣏' => '𑢯',
+ '𑣐' => '𑢰',
+ '𑣑' => '𑢱',
+ '𑣒' => '𑢲',
+ '𑣓' => '𑢳',
+ '𑣔' => '𑢴',
+ '𑣕' => '𑢵',
+ '𑣖' => '𑢶',
+ '𑣗' => '𑢷',
+ '𑣘' => '𑢸',
+ '𑣙' => '𑢹',
+ '𑣚' => '𑢺',
+ '𑣛' => '𑢻',
+ '𑣜' => '𑢼',
+ '𑣝' => '𑢽',
+ '𑣞' => '𑢾',
+ '𑣟' => '𑢿',
+ '𖹠' => '𖹀',
+ '𖹡' => '𖹁',
+ '𖹢' => '𖹂',
+ '𖹣' => '𖹃',
+ '𖹤' => '𖹄',
+ '𖹥' => '𖹅',
+ '𖹦' => '𖹆',
+ '𖹧' => '𖹇',
+ '𖹨' => '𖹈',
+ '𖹩' => '𖹉',
+ '𖹪' => '𖹊',
+ '𖹫' => '𖹋',
+ '𖹬' => '𖹌',
+ '𖹭' => '𖹍',
+ '𖹮' => '𖹎',
+ '𖹯' => '𖹏',
+ '𖹰' => '𖹐',
+ '𖹱' => '𖹑',
+ '𖹲' => '𖹒',
+ '𖹳' => '𖹓',
+ '𖹴' => '𖹔',
+ '𖹵' => '𖹕',
+ '𖹶' => '𖹖',
+ '𖹷' => '𖹗',
+ '𖹸' => '𖹘',
+ '𖹹' => '𖹙',
+ '𖹺' => '𖹚',
+ '𖹻' => '𖹛',
+ '𖹼' => '𖹜',
+ '𖹽' => '𖹝',
+ '𖹾' => '𖹞',
+ '𖹿' => '𖹟',
+ '𞤢' => '𞤀',
+ '𞤣' => '𞤁',
+ '𞤤' => '𞤂',
+ '𞤥' => '𞤃',
+ '𞤦' => '𞤄',
+ '𞤧' => '𞤅',
+ '𞤨' => '𞤆',
+ '𞤩' => '𞤇',
+ '𞤪' => '𞤈',
+ '𞤫' => '𞤉',
+ '𞤬' => '𞤊',
+ '𞤭' => '𞤋',
+ '𞤮' => '𞤌',
+ '𞤯' => '𞤍',
+ '𞤰' => '𞤎',
+ '𞤱' => '𞤏',
+ '𞤲' => '𞤐',
+ '𞤳' => '𞤑',
+ '𞤴' => '𞤒',
+ '𞤵' => '𞤓',
+ '𞤶' => '𞤔',
+ '𞤷' => '𞤕',
+ '𞤸' => '𞤖',
+ '𞤹' => '𞤗',
+ '𞤺' => '𞤘',
+ '𞤻' => '𞤙',
+ '𞤼' => '𞤚',
+ '𞤽' => '𞤛',
+ '𞤾' => '𞤜',
+ '𞤿' => '𞤝',
+ '𞥀' => '𞤞',
+ '𞥁' => '𞤟',
+ '𞥂' => '𞤠',
+ '𞥃' => '𞤡',
+);
diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php
new file mode 100644
index 0000000..eaa992f
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/bootstrap.php
@@ -0,0 +1,141 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Mbstring as p;
+
+if (!function_exists('mb_convert_encoding')) {
+ function mb_convert_encoding($s, $to, $from = null) { return p\Mbstring::mb_convert_encoding($s, $to, $from); }
+}
+if (!function_exists('mb_decode_mimeheader')) {
+ function mb_decode_mimeheader($s) { return p\Mbstring::mb_decode_mimeheader($s); }
+}
+if (!function_exists('mb_encode_mimeheader')) {
+ function mb_encode_mimeheader($s, $charset = null, $transferEnc = null, $lf = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($s, $charset, $transferEnc, $lf, $indent); }
+}
+if (!function_exists('mb_decode_numericentity')) {
+ function mb_decode_numericentity($s, $convmap, $enc = null) { return p\Mbstring::mb_decode_numericentity($s, $convmap, $enc); }
+}
+if (!function_exists('mb_encode_numericentity')) {
+ function mb_encode_numericentity($s, $convmap, $enc = null, $is_hex = false) { return p\Mbstring::mb_encode_numericentity($s, $convmap, $enc, $is_hex); }
+}
+if (!function_exists('mb_convert_case')) {
+ function mb_convert_case($s, $mode, $enc = null) { return p\Mbstring::mb_convert_case($s, $mode, $enc); }
+}
+if (!function_exists('mb_internal_encoding')) {
+ function mb_internal_encoding($enc = null) { return p\Mbstring::mb_internal_encoding($enc); }
+}
+if (!function_exists('mb_language')) {
+ function mb_language($lang = null) { return p\Mbstring::mb_language($lang); }
+}
+if (!function_exists('mb_list_encodings')) {
+ function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
+}
+if (!function_exists('mb_encoding_aliases')) {
+ function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
+}
+if (!function_exists('mb_check_encoding')) {
+ function mb_check_encoding($var = null, $encoding = null) { return p\Mbstring::mb_check_encoding($var, $encoding); }
+}
+if (!function_exists('mb_detect_encoding')) {
+ function mb_detect_encoding($str, $encodingList = null, $strict = false) { return p\Mbstring::mb_detect_encoding($str, $encodingList, $strict); }
+}
+if (!function_exists('mb_detect_order')) {
+ function mb_detect_order($encodingList = null) { return p\Mbstring::mb_detect_order($encodingList); }
+}
+if (!function_exists('mb_parse_str')) {
+ function mb_parse_str($s, &$result = array()) { parse_str($s, $result); }
+}
+if (!function_exists('mb_strlen')) {
+ function mb_strlen($s, $enc = null) { return p\Mbstring::mb_strlen($s, $enc); }
+}
+if (!function_exists('mb_strpos')) {
+ function mb_strpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strpos($s, $needle, $offset, $enc); }
+}
+if (!function_exists('mb_strtolower')) {
+ function mb_strtolower($s, $enc = null) { return p\Mbstring::mb_strtolower($s, $enc); }
+}
+if (!function_exists('mb_strtoupper')) {
+ function mb_strtoupper($s, $enc = null) { return p\Mbstring::mb_strtoupper($s, $enc); }
+}
+if (!function_exists('mb_substitute_character')) {
+ function mb_substitute_character($char = null) { return p\Mbstring::mb_substitute_character($char); }
+}
+if (!function_exists('mb_substr')) {
+ function mb_substr($s, $start, $length = 2147483647, $enc = null) { return p\Mbstring::mb_substr($s, $start, $length, $enc); }
+}
+if (!function_exists('mb_stripos')) {
+ function mb_stripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_stripos($s, $needle, $offset, $enc); }
+}
+if (!function_exists('mb_stristr')) {
+ function mb_stristr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_stristr($s, $needle, $part, $enc); }
+}
+if (!function_exists('mb_strrchr')) {
+ function mb_strrchr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrchr($s, $needle, $part, $enc); }
+}
+if (!function_exists('mb_strrichr')) {
+ function mb_strrichr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrichr($s, $needle, $part, $enc); }
+}
+if (!function_exists('mb_strripos')) {
+ function mb_strripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strripos($s, $needle, $offset, $enc); }
+}
+if (!function_exists('mb_strrpos')) {
+ function mb_strrpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strrpos($s, $needle, $offset, $enc); }
+}
+if (!function_exists('mb_strstr')) {
+ function mb_strstr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strstr($s, $needle, $part, $enc); }
+}
+if (!function_exists('mb_get_info')) {
+ function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
+}
+if (!function_exists('mb_http_output')) {
+ function mb_http_output($enc = null) { return p\Mbstring::mb_http_output($enc); }
+}
+if (!function_exists('mb_strwidth')) {
+ function mb_strwidth($s, $enc = null) { return p\Mbstring::mb_strwidth($s, $enc); }
+}
+if (!function_exists('mb_substr_count')) {
+ function mb_substr_count($haystack, $needle, $enc = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $enc); }
+}
+if (!function_exists('mb_output_handler')) {
+ function mb_output_handler($contents, $status) { return p\Mbstring::mb_output_handler($contents, $status); }
+}
+if (!function_exists('mb_http_input')) {
+ function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); }
+}
+if (!function_exists('mb_convert_variables')) {
+ function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { return p\Mbstring::mb_convert_variables($toEncoding, $fromEncoding, $a, $b, $c, $d, $e, $f); }
+}
+if (!function_exists('mb_ord')) {
+ function mb_ord($s, $enc = null) { return p\Mbstring::mb_ord($s, $enc); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr($code, $enc = null) { return p\Mbstring::mb_chr($code, $enc); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); }
+}
+if (!function_exists('mb_str_split')) {
+ function mb_str_split($string, $split_length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $split_length, $encoding); }
+}
+
+if (extension_loaded('mbstring')) {
+ return;
+}
+
+if (!defined('MB_CASE_UPPER')) {
+ define('MB_CASE_UPPER', 0);
+}
+if (!defined('MB_CASE_LOWER')) {
+ define('MB_CASE_LOWER', 1);
+}
+if (!defined('MB_CASE_TITLE')) {
+ define('MB_CASE_TITLE', 2);
+}
diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json
new file mode 100644
index 0000000..1cced4a
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "symfony/polyfill-mbstring",
+ "type": "library",
+ "description": "Symfony polyfill for the Mbstring extension",
+ "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
+ "files": [ "bootstrap.php" ]
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/vendor/symfony/polyfill-php72/LICENSE b/vendor/symfony/polyfill-php72/LICENSE
new file mode 100644
index 0000000..adb8d6f
--- /dev/null
+++ b/vendor/symfony/polyfill-php72/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2015-2019 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.
diff --git a/vendor/symfony/polyfill-php72/Php72.php b/vendor/symfony/polyfill-php72/Php72.php
new file mode 100644
index 0000000..b6fe3f2
--- /dev/null
+++ b/vendor/symfony/polyfill-php72/Php72.php
@@ -0,0 +1,217 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php72;
+
+/**
+ * @author Nicolas Grekas
+ * @author Dariusz Rumiński
+ *
+ * @internal
+ */
+final class Php72
+{
+ private static $hashMask;
+
+ public static function utf8_encode($s)
+ {
+ $s .= $s;
+ $len = \strlen($s);
+
+ for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
+ switch (true) {
+ case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
+ case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
+ default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break;
+ }
+ }
+
+ return substr($s, 0, $j);
+ }
+
+ public static function utf8_decode($s)
+ {
+ $s = (string) $s;
+ $len = \strlen($s);
+
+ for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
+ switch ($s[$i] & "\xF0") {
+ case "\xC0":
+ case "\xD0":
+ $c = (\ord($s[$i] & "\x1F") << 6) | \ord($s[++$i] & "\x3F");
+ $s[$j] = $c < 256 ? \chr($c) : '?';
+ break;
+
+ case "\xF0":
+ ++$i;
+ // no break
+
+ case "\xE0":
+ $s[$j] = '?';
+ $i += 2;
+ break;
+
+ default:
+ $s[$j] = $s[$i];
+ }
+ }
+
+ return substr($s, 0, $j);
+ }
+
+ public static function php_os_family()
+ {
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ return 'Windows';
+ }
+
+ $map = array(
+ 'Darwin' => 'Darwin',
+ 'DragonFly' => 'BSD',
+ 'FreeBSD' => 'BSD',
+ 'NetBSD' => 'BSD',
+ 'OpenBSD' => 'BSD',
+ 'Linux' => 'Linux',
+ 'SunOS' => 'Solaris',
+ );
+
+ return isset($map[PHP_OS]) ? $map[PHP_OS] : 'Unknown';
+ }
+
+ public static function spl_object_id($object)
+ {
+ if (null === self::$hashMask) {
+ self::initHashMask();
+ }
+ if (null === $hash = spl_object_hash($object)) {
+ return;
+ }
+
+ // On 32-bit systems, PHP_INT_SIZE is 4,
+ return self::$hashMask ^ hexdec(substr($hash, 16 - (\PHP_INT_SIZE * 2 - 1), (\PHP_INT_SIZE * 2 - 1)));
+ }
+
+ public static function sapi_windows_vt100_support($stream, $enable = null)
+ {
+ if (!\is_resource($stream)) {
+ trigger_error('sapi_windows_vt100_support() expects parameter 1 to be resource, '.\gettype($stream).' given', E_USER_WARNING);
+
+ return false;
+ }
+
+ $meta = stream_get_meta_data($stream);
+
+ if ('STDIO' !== $meta['stream_type']) {
+ trigger_error('sapi_windows_vt100_support() was not able to analyze the specified stream', E_USER_WARNING);
+
+ return false;
+ }
+
+ // We cannot actually disable vt100 support if it is set
+ if (false === $enable || !self::stream_isatty($stream)) {
+ return false;
+ }
+
+ // The native function does not apply to stdin
+ $meta = array_map('strtolower', $meta);
+ $stdin = 'php://stdin' === $meta['uri'] || 'php://fd/0' === $meta['uri'];
+
+ return !$stdin
+ && (false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ || 'xterm' === getenv('TERM')
+ || 'Hyper' === getenv('TERM_PROGRAM'));
+ }
+
+ public static function stream_isatty($stream)
+ {
+ if (!\is_resource($stream)) {
+ trigger_error('stream_isatty() expects parameter 1 to be resource, '.\gettype($stream).' given', E_USER_WARNING);
+
+ return false;
+ }
+
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $stat = @fstat($stream);
+ // Check if formatted mode is S_IFCHR
+ return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
+ }
+
+ return \function_exists('posix_isatty') && @posix_isatty($stream);
+ }
+
+ private static function initHashMask()
+ {
+ $obj = (object) array();
+ self::$hashMask = -1;
+
+ // check if we are nested in an output buffering handler to prevent a fatal error with ob_start() below
+ $obFuncs = array('ob_clean', 'ob_end_clean', 'ob_flush', 'ob_end_flush', 'ob_get_contents', 'ob_get_flush');
+ foreach (debug_backtrace(\PHP_VERSION_ID >= 50400 ? DEBUG_BACKTRACE_IGNORE_ARGS : false) as $frame) {
+ if (isset($frame['function'][0]) && !isset($frame['class']) && 'o' === $frame['function'][0] && \in_array($frame['function'], $obFuncs)) {
+ $frame['line'] = 0;
+ break;
+ }
+ }
+ if (!empty($frame['line'])) {
+ ob_start();
+ debug_zval_dump($obj);
+ self::$hashMask = (int) substr(ob_get_clean(), 17);
+ }
+
+ self::$hashMask ^= hexdec(substr(spl_object_hash($obj), 16 - (\PHP_INT_SIZE * 2 - 1), (\PHP_INT_SIZE * 2 - 1)));
+ }
+
+ public static function mb_chr($code, $encoding = null)
+ {
+ if (0x80 > $code %= 0x200000) {
+ $s = \chr($code);
+ } elseif (0x800 > $code) {
+ $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
+ } elseif (0x10000 > $code) {
+ $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ } else {
+ $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ }
+
+ if ('UTF-8' !== $encoding) {
+ $s = mb_convert_encoding($s, $encoding, 'UTF-8');
+ }
+
+ return $s;
+ }
+
+ public static function mb_ord($s, $encoding = null)
+ {
+ if (null == $encoding) {
+ $s = mb_convert_encoding($s, 'UTF-8');
+ } elseif ('UTF-8' !== $encoding) {
+ $s = mb_convert_encoding($s, 'UTF-8', $encoding);
+ }
+
+ if (1 === \strlen($s)) {
+ return \ord($s);
+ }
+
+ $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
+ if (0xF0 <= $code) {
+ return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
+ }
+ if (0xE0 <= $code) {
+ return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
+ }
+ if (0xC0 <= $code) {
+ return (($code - 0xC0) << 6) + $s[2] - 0x80;
+ }
+
+ return $code;
+ }
+}
diff --git a/vendor/symfony/polyfill-php72/README.md b/vendor/symfony/polyfill-php72/README.md
new file mode 100644
index 0000000..8396701
--- /dev/null
+++ b/vendor/symfony/polyfill-php72/README.md
@@ -0,0 +1,28 @@
+Symfony Polyfill / Php72
+========================
+
+This component provides functions added to PHP 7.2 core:
+
+- [`spl_object_id`](https://php.net/spl_object_id)
+- [`stream_isatty`](https://php.net/stream_isatty)
+
+On Windows only:
+
+- [`sapi_windows_vt100_support`](https://php.net/sapi_windows_vt100_support)
+
+Moved to core since 7.2 (was in the optional XML extension earlier):
+
+- [`utf8_encode`](https://php.net/utf8_encode)
+- [`utf8_decode`](https://php.net/utf8_decode)
+
+Also, it provides constants added to PHP 7.2:
+- [`PHP_FLOAT_*`](https://php.net/reserved.constants#constant.php-float-dig)
+- [`PHP_OS_FAMILY`](https://php.net/reserved.constants#constant.php-os-family)
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/vendor/symfony/polyfill-php72/bootstrap.php b/vendor/symfony/polyfill-php72/bootstrap.php
new file mode 100644
index 0000000..1295483
--- /dev/null
+++ b/vendor/symfony/polyfill-php72/bootstrap.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Php72 as p;
+
+if (PHP_VERSION_ID >= 70200) {
+ return;
+}
+
+if (!defined('PHP_FLOAT_DIG')) {
+ define('PHP_FLOAT_DIG', 15);
+}
+if (!defined('PHP_FLOAT_EPSILON')) {
+ define('PHP_FLOAT_EPSILON', 2.2204460492503E-16);
+}
+if (!defined('PHP_FLOAT_MIN')) {
+ define('PHP_FLOAT_MIN', 2.2250738585072E-308);
+}
+if (!defined('PHP_FLOAT_MAX')) {
+ define('PHP_FLOAT_MAX', 1.7976931348623157E+308);
+}
+if (!defined('PHP_OS_FAMILY')) {
+ define('PHP_OS_FAMILY', p\Php72::php_os_family());
+}
+
+if ('\\' === DIRECTORY_SEPARATOR && !function_exists('sapi_windows_vt100_support')) {
+ function sapi_windows_vt100_support($stream, $enable = null) { return p\Php72::sapi_windows_vt100_support($stream, $enable); }
+}
+if (!function_exists('stream_isatty')) {
+ function stream_isatty($stream) { return p\Php72::stream_isatty($stream); }
+}
+if (!function_exists('utf8_encode')) {
+ function utf8_encode($s) { return p\Php72::utf8_encode($s); }
+}
+if (!function_exists('utf8_decode')) {
+ function utf8_decode($s) { return p\Php72::utf8_decode($s); }
+}
+if (!function_exists('spl_object_id')) {
+ function spl_object_id($s) { return p\Php72::spl_object_id($s); }
+}
+if (!function_exists('mb_ord')) {
+ function mb_ord($s, $enc = null) { return p\Php72::mb_ord($s, $enc); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr($code, $enc = null) { return p\Php72::mb_chr($code, $enc); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); }
+}
diff --git a/vendor/symfony/polyfill-php72/composer.json b/vendor/symfony/polyfill-php72/composer.json
new file mode 100644
index 0000000..cf08a43
--- /dev/null
+++ b/vendor/symfony/polyfill-php72/composer.json
@@ -0,0 +1,35 @@
+{
+ "name": "symfony/polyfill-php72",
+ "type": "library",
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+ "keywords": ["polyfill", "shim", "compatibility", "portable"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Php72\\": "" },
+ "files": [ "bootstrap.php" ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/vendor/symfony/polyfill-php80/LICENSE b/vendor/symfony/polyfill-php80/LICENSE
new file mode 100644
index 0000000..9bf2ee0
--- /dev/null
+++ b/vendor/symfony/polyfill-php80/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020 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.
diff --git a/vendor/symfony/polyfill-php80/Php80.php b/vendor/symfony/polyfill-php80/Php80.php
new file mode 100644
index 0000000..716522a
--- /dev/null
+++ b/vendor/symfony/polyfill-php80/Php80.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php80;
+
+/**
+ * @author Ion Bazan
+ * @author Nico Oelgart
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class Php80
+{
+ public static function fdiv(float $dividend, float $divisor): float
+ {
+ return @($dividend / $divisor);
+ }
+
+ public static function get_debug_type($value): string
+ {
+ switch (true) {
+ case null === $value: return 'null';
+ case \is_bool($value): return 'bool';
+ case \is_string($value): return 'string';
+ case \is_array($value): return 'array';
+ case \is_int($value): return 'int';
+ case \is_float($value): return 'float';
+ case \is_object($value): break;
+ case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
+ default:
+ if (null === $type = @get_resource_type($value)) {
+ return 'unknown';
+ }
+
+ if ('Unknown' === $type) {
+ $type = 'closed';
+ }
+
+ return "resource ($type)";
+ }
+
+ $class = \get_class($value);
+
+ if (false === strpos($class, '@')) {
+ return $class;
+ }
+
+ return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
+ }
+
+ public static function get_resource_id($res): int
+ {
+ if (!\is_resource($res) && null === @get_resource_type($res)) {
+ throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
+ }
+
+ return (int) $res;
+ }
+
+ public static function preg_last_error_msg(): string
+ {
+ switch (preg_last_error()) {
+ case PREG_INTERNAL_ERROR:
+ return 'Internal error';
+ case PREG_BAD_UTF8_ERROR:
+ return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ case PREG_BAD_UTF8_OFFSET_ERROR:
+ return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
+ case PREG_BACKTRACK_LIMIT_ERROR:
+ return 'Backtrack limit exhausted';
+ case PREG_RECURSION_LIMIT_ERROR:
+ return 'Recursion limit exhausted';
+ case PREG_JIT_STACKLIMIT_ERROR:
+ return 'JIT stack limit exhausted';
+ case PREG_NO_ERROR:
+ return 'No error';
+ default:
+ return 'Unknown error';
+ }
+ }
+
+ public static function str_contains(string $haystack, string $needle): bool
+ {
+ return '' === $needle || false !== strpos($haystack, $needle);
+ }
+
+ public static function str_starts_with(string $haystack, string $needle): bool
+ {
+ return 0 === \strncmp($haystack, $needle, \strlen($needle));
+ }
+
+ public static function str_ends_with(string $haystack, string $needle): bool
+ {
+ return '' === $needle || ('' !== $haystack && 0 === \substr_compare($haystack, $needle, -\strlen($needle)));
+ }
+}
diff --git a/vendor/symfony/polyfill-php80/README.md b/vendor/symfony/polyfill-php80/README.md
new file mode 100644
index 0000000..6a1c0f9
--- /dev/null
+++ b/vendor/symfony/polyfill-php80/README.md
@@ -0,0 +1,24 @@
+Symfony Polyfill / Php80
+========================
+
+This component provides features added to PHP 8.0 core:
+
+- `Stringable` interface
+- [`fdiv`](https://php.net/fdiv)
+- `ValueError` class
+- `UnhandledMatchError` class
+- `FILTER_VALIDATE_BOOL` constant
+- [`get_debug_type`](https://php.net/get_debug_type)
+- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
+- [`str_contains`](https://php.net/str_contains)
+- [`str_starts_with`](https://php.net/str_starts_with)
+- [`str_ends_with`](https://php.net/str_ends_with)
+- [`get_resource_id`](https://php.net/get_resource_id)
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php
new file mode 100644
index 0000000..10d22ef
--- /dev/null
+++ b/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php
@@ -0,0 +1,9 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Php80 as p;
+
+if (PHP_VERSION_ID >= 80000) {
+ return;
+}
+
+if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
+ define('FILTER_VALIDATE_BOOL', FILTER_VALIDATE_BOOLEAN);
+}
+
+if (!function_exists('fdiv')) {
+ function fdiv(float $dividend, float $divisor): float { return p\Php80::fdiv($dividend, $divisor); }
+}
+if (!function_exists('preg_last_error_msg')) {
+ function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
+}
+if (!function_exists('str_contains')) {
+ function str_contains(string $haystack, string $needle): bool { return p\Php80::str_contains($haystack, $needle); }
+}
+if (!function_exists('str_starts_with')) {
+ function str_starts_with(string $haystack, string $needle): bool { return p\Php80::str_starts_with($haystack, $needle); }
+}
+if (!function_exists('str_ends_with')) {
+ function str_ends_with(string $haystack, string $needle): bool { return p\Php80::str_ends_with($haystack, $needle); }
+}
+if (!function_exists('get_debug_type')) {
+ function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
+}
+if (!function_exists('get_resource_id')) {
+ function get_resource_id($res): int { return p\Php80::get_resource_id($res); }
+}
diff --git a/vendor/symfony/polyfill-php80/composer.json b/vendor/symfony/polyfill-php80/composer.json
new file mode 100644
index 0000000..eba876a
--- /dev/null
+++ b/vendor/symfony/polyfill-php80/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "symfony/polyfill-php80",
+ "type": "library",
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "keywords": ["polyfill", "shim", "compatibility", "portable"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.0.8"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Php80\\": "" },
+ "files": [ "bootstrap.php" ],
+ "classmap": [ "Resources/stubs" ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/CHANGELOG.md b/vendor/symfony/var-dumper/CHANGELOG.md
new file mode 100644
index 0000000..3c76329
--- /dev/null
+++ b/vendor/symfony/var-dumper/CHANGELOG.md
@@ -0,0 +1,53 @@
+CHANGELOG
+=========
+
+4.4.0
+-----
+
+ * added `VarDumperTestTrait::setUpVarDumper()` and `VarDumperTestTrait::tearDownVarDumper()`
+ to configure casters & flags to use in tests
+ * added `ImagineCaster` and infrastructure to dump images
+ * added the stamps of a message after it is dispatched in `TraceableMessageBus` and `MessengerDataCollector` collected data
+ * added `UuidCaster`
+ * made all casters final
+ * added support for the `NO_COLOR` env var (https://no-color.org/)
+
+4.3.0
+-----
+
+ * added `DsCaster` to support dumping the contents of data structures from the Ds extension
+
+4.2.0
+-----
+
+ * support selecting the format to use by setting the environment variable `VAR_DUMPER_FORMAT` to `html` or `cli`
+
+4.1.0
+-----
+
+ * added a `ServerDumper` to send serialized Data clones to a server
+ * added a `ServerDumpCommand` and `DumpServer` to run a server collecting
+ and displaying dumps on a single place with multiple formats support
+ * added `CliDescriptor` and `HtmlDescriptor` descriptors for `server:dump` CLI and HTML formats support
+
+4.0.0
+-----
+
+ * support for passing `\ReflectionClass` instances to the `Caster::castObject()`
+ method has been dropped, pass class names as strings instead
+ * the `Data::getRawData()` method has been removed
+ * the `VarDumperTestTrait::assertDumpEquals()` method expects a 3rd `$filter = 0`
+ argument and moves `$message = ''` argument at 4th position.
+ * the `VarDumperTestTrait::assertDumpMatchesFormat()` method expects a 3rd `$filter = 0`
+ argument and moves `$message = ''` argument at 4th position.
+
+3.4.0
+-----
+
+ * added `AbstractCloner::setMinDepth()` function to ensure minimum tree depth
+ * deprecated `MongoCaster`
+
+2.7.0
+-----
+
+ * deprecated `Cloner\Data::getLimitedClone()`. Use `withMaxDepth`, `withMaxItemsPerDepth` or `withRefHandles` instead.
diff --git a/vendor/symfony/var-dumper/Caster/AmqpCaster.php b/vendor/symfony/var-dumper/Caster/AmqpCaster.php
new file mode 100644
index 0000000..a292251
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/AmqpCaster.php
@@ -0,0 +1,212 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Amqp related classes to array representation.
+ *
+ * @author Grégoire Pineau
+ *
+ * @final since Symfony 4.4
+ */
+class AmqpCaster
+{
+ private static $flags = [
+ AMQP_DURABLE => 'AMQP_DURABLE',
+ AMQP_PASSIVE => 'AMQP_PASSIVE',
+ AMQP_EXCLUSIVE => 'AMQP_EXCLUSIVE',
+ AMQP_AUTODELETE => 'AMQP_AUTODELETE',
+ AMQP_INTERNAL => 'AMQP_INTERNAL',
+ AMQP_NOLOCAL => 'AMQP_NOLOCAL',
+ AMQP_AUTOACK => 'AMQP_AUTOACK',
+ AMQP_IFEMPTY => 'AMQP_IFEMPTY',
+ AMQP_IFUNUSED => 'AMQP_IFUNUSED',
+ AMQP_MANDATORY => 'AMQP_MANDATORY',
+ AMQP_IMMEDIATE => 'AMQP_IMMEDIATE',
+ AMQP_MULTIPLE => 'AMQP_MULTIPLE',
+ AMQP_NOWAIT => 'AMQP_NOWAIT',
+ AMQP_REQUEUE => 'AMQP_REQUEUE',
+ ];
+
+ private static $exchangeTypes = [
+ AMQP_EX_TYPE_DIRECT => 'AMQP_EX_TYPE_DIRECT',
+ AMQP_EX_TYPE_FANOUT => 'AMQP_EX_TYPE_FANOUT',
+ AMQP_EX_TYPE_TOPIC => 'AMQP_EX_TYPE_TOPIC',
+ AMQP_EX_TYPE_HEADERS => 'AMQP_EX_TYPE_HEADERS',
+ ];
+
+ public static function castConnection(\AMQPConnection $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a += [
+ $prefix.'is_connected' => $c->isConnected(),
+ ];
+
+ // Recent version of the extension already expose private properties
+ if (isset($a["\x00AMQPConnection\x00login"])) {
+ return $a;
+ }
+
+ // BC layer in the amqp lib
+ if (method_exists($c, 'getReadTimeout')) {
+ $timeout = $c->getReadTimeout();
+ } else {
+ $timeout = $c->getTimeout();
+ }
+
+ $a += [
+ $prefix.'is_connected' => $c->isConnected(),
+ $prefix.'login' => $c->getLogin(),
+ $prefix.'password' => $c->getPassword(),
+ $prefix.'host' => $c->getHost(),
+ $prefix.'vhost' => $c->getVhost(),
+ $prefix.'port' => $c->getPort(),
+ $prefix.'read_timeout' => $timeout,
+ ];
+
+ return $a;
+ }
+
+ public static function castChannel(\AMQPChannel $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a += [
+ $prefix.'is_connected' => $c->isConnected(),
+ $prefix.'channel_id' => $c->getChannelId(),
+ ];
+
+ // Recent version of the extension already expose private properties
+ if (isset($a["\x00AMQPChannel\x00connection"])) {
+ return $a;
+ }
+
+ $a += [
+ $prefix.'connection' => $c->getConnection(),
+ $prefix.'prefetch_size' => $c->getPrefetchSize(),
+ $prefix.'prefetch_count' => $c->getPrefetchCount(),
+ ];
+
+ return $a;
+ }
+
+ public static function castQueue(\AMQPQueue $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a += [
+ $prefix.'flags' => self::extractFlags($c->getFlags()),
+ ];
+
+ // Recent version of the extension already expose private properties
+ if (isset($a["\x00AMQPQueue\x00name"])) {
+ return $a;
+ }
+
+ $a += [
+ $prefix.'connection' => $c->getConnection(),
+ $prefix.'channel' => $c->getChannel(),
+ $prefix.'name' => $c->getName(),
+ $prefix.'arguments' => $c->getArguments(),
+ ];
+
+ return $a;
+ }
+
+ public static function castExchange(\AMQPExchange $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a += [
+ $prefix.'flags' => self::extractFlags($c->getFlags()),
+ ];
+
+ $type = isset(self::$exchangeTypes[$c->getType()]) ? new ConstStub(self::$exchangeTypes[$c->getType()], $c->getType()) : $c->getType();
+
+ // Recent version of the extension already expose private properties
+ if (isset($a["\x00AMQPExchange\x00name"])) {
+ $a["\x00AMQPExchange\x00type"] = $type;
+
+ return $a;
+ }
+
+ $a += [
+ $prefix.'connection' => $c->getConnection(),
+ $prefix.'channel' => $c->getChannel(),
+ $prefix.'name' => $c->getName(),
+ $prefix.'type' => $type,
+ $prefix.'arguments' => $c->getArguments(),
+ ];
+
+ return $a;
+ }
+
+ public static function castEnvelope(\AMQPEnvelope $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $deliveryMode = new ConstStub($c->getDeliveryMode().(2 === $c->getDeliveryMode() ? ' (persistent)' : ' (non-persistent)'), $c->getDeliveryMode());
+
+ // Recent version of the extension already expose private properties
+ if (isset($a["\x00AMQPEnvelope\x00body"])) {
+ $a["\0AMQPEnvelope\0delivery_mode"] = $deliveryMode;
+
+ return $a;
+ }
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE)) {
+ $a += [$prefix.'body' => $c->getBody()];
+ }
+
+ $a += [
+ $prefix.'delivery_tag' => $c->getDeliveryTag(),
+ $prefix.'is_redelivery' => $c->isRedelivery(),
+ $prefix.'exchange_name' => $c->getExchangeName(),
+ $prefix.'routing_key' => $c->getRoutingKey(),
+ $prefix.'content_type' => $c->getContentType(),
+ $prefix.'content_encoding' => $c->getContentEncoding(),
+ $prefix.'headers' => $c->getHeaders(),
+ $prefix.'delivery_mode' => $deliveryMode,
+ $prefix.'priority' => $c->getPriority(),
+ $prefix.'correlation_id' => $c->getCorrelationId(),
+ $prefix.'reply_to' => $c->getReplyTo(),
+ $prefix.'expiration' => $c->getExpiration(),
+ $prefix.'message_id' => $c->getMessageId(),
+ $prefix.'timestamp' => $c->getTimeStamp(),
+ $prefix.'type' => $c->getType(),
+ $prefix.'user_id' => $c->getUserId(),
+ $prefix.'app_id' => $c->getAppId(),
+ ];
+
+ return $a;
+ }
+
+ private static function extractFlags(int $flags): ConstStub
+ {
+ $flagsArray = [];
+
+ foreach (self::$flags as $value => $name) {
+ if ($flags & $value) {
+ $flagsArray[] = $name;
+ }
+ }
+
+ if (!$flagsArray) {
+ $flagsArray = ['AMQP_NOPARAM'];
+ }
+
+ return new ConstStub(implode('|', $flagsArray), $flags);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ArgsStub.php b/vendor/symfony/var-dumper/Caster/ArgsStub.php
new file mode 100644
index 0000000..70f346c
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ArgsStub.php
@@ -0,0 +1,80 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents a list of function arguments.
+ *
+ * @author Nicolas Grekas
+ */
+class ArgsStub extends EnumStub
+{
+ private static $parameters = [];
+
+ public function __construct(array $args, string $function, ?string $class)
+ {
+ list($variadic, $params) = self::getParameters($function, $class);
+
+ $values = [];
+ foreach ($args as $k => $v) {
+ $values[$k] = !is_scalar($v) && !$v instanceof Stub ? new CutStub($v) : $v;
+ }
+ if (null === $params) {
+ parent::__construct($values, false);
+
+ return;
+ }
+ if (\count($values) < \count($params)) {
+ $params = \array_slice($params, 0, \count($values));
+ } elseif (\count($values) > \count($params)) {
+ $values[] = new EnumStub(array_splice($values, \count($params)), false);
+ $params[] = $variadic;
+ }
+ if (['...'] === $params) {
+ $this->dumpKeys = false;
+ $this->value = $values[0]->value;
+ } else {
+ $this->value = array_combine($params, $values);
+ }
+ }
+
+ private static function getParameters(string $function, ?string $class): array
+ {
+ if (isset(self::$parameters[$k = $class.'::'.$function])) {
+ return self::$parameters[$k];
+ }
+
+ try {
+ $r = null !== $class ? new \ReflectionMethod($class, $function) : new \ReflectionFunction($function);
+ } catch (\ReflectionException $e) {
+ return [null, null];
+ }
+
+ $variadic = '...';
+ $params = [];
+ foreach ($r->getParameters() as $v) {
+ $k = '$'.$v->name;
+ if ($v->isPassedByReference()) {
+ $k = '&'.$k;
+ }
+ if ($v->isVariadic()) {
+ $variadic .= $k;
+ } else {
+ $params[] = $k;
+ }
+ }
+
+ return self::$parameters[$k] = [$variadic, $params];
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/Caster.php b/vendor/symfony/var-dumper/Caster/Caster.php
new file mode 100644
index 0000000..07ac251
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/Caster.php
@@ -0,0 +1,175 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Helper for filtering out properties in casters.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final
+ */
+class Caster
+{
+ const EXCLUDE_VERBOSE = 1;
+ const EXCLUDE_VIRTUAL = 2;
+ const EXCLUDE_DYNAMIC = 4;
+ const EXCLUDE_PUBLIC = 8;
+ const EXCLUDE_PROTECTED = 16;
+ const EXCLUDE_PRIVATE = 32;
+ const EXCLUDE_NULL = 64;
+ const EXCLUDE_EMPTY = 128;
+ const EXCLUDE_NOT_IMPORTANT = 256;
+ const EXCLUDE_STRICT = 512;
+
+ const PREFIX_VIRTUAL = "\0~\0";
+ const PREFIX_DYNAMIC = "\0+\0";
+ const PREFIX_PROTECTED = "\0*\0";
+
+ /**
+ * Casts objects to arrays and adds the dynamic property prefix.
+ *
+ * @param object $obj The object to cast
+ * @param bool $hasDebugInfo Whether the __debugInfo method exists on $obj or not
+ *
+ * @return array The array-cast of the object, with prefixed dynamic properties
+ */
+ public static function castObject($obj, string $class, bool $hasDebugInfo = false, string $debugClass = null): array
+ {
+ if ($hasDebugInfo) {
+ try {
+ $debugInfo = $obj->__debugInfo();
+ } catch (\Exception $e) {
+ // ignore failing __debugInfo()
+ $hasDebugInfo = false;
+ }
+ }
+
+ $a = $obj instanceof \Closure ? [] : (array) $obj;
+
+ if ($obj instanceof \__PHP_Incomplete_Class) {
+ return $a;
+ }
+
+ if ($a) {
+ static $publicProperties = [];
+ $debugClass = $debugClass ?? get_debug_type($obj);
+
+ $i = 0;
+ $prefixedKeys = [];
+ foreach ($a as $k => $v) {
+ if (isset($k[0]) ? "\0" !== $k[0] : \PHP_VERSION_ID >= 70200) {
+ if (!isset($publicProperties[$class])) {
+ foreach ((new \ReflectionClass($class))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
+ $publicProperties[$class][$prop->name] = true;
+ }
+ }
+ if (!isset($publicProperties[$class][$k])) {
+ $prefixedKeys[$i] = self::PREFIX_DYNAMIC.$k;
+ }
+ } elseif ($debugClass !== $class && 1 === strpos($k, $class)) {
+ $prefixedKeys[$i] = "\0".$debugClass.strrchr($k, "\0");
+ }
+ ++$i;
+ }
+ if ($prefixedKeys) {
+ $keys = array_keys($a);
+ foreach ($prefixedKeys as $i => $k) {
+ $keys[$i] = $k;
+ }
+ $a = array_combine($keys, $a);
+ }
+ }
+
+ if ($hasDebugInfo && \is_array($debugInfo)) {
+ foreach ($debugInfo as $k => $v) {
+ if (!isset($k[0]) || "\0" !== $k[0]) {
+ if (\array_key_exists(self::PREFIX_DYNAMIC.$k, $a)) {
+ continue;
+ }
+ $k = self::PREFIX_VIRTUAL.$k;
+ }
+
+ unset($a[$k]);
+ $a[$k] = $v;
+ }
+ }
+
+ return $a;
+ }
+
+ /**
+ * Filters out the specified properties.
+ *
+ * By default, a single match in the $filter bit field filters properties out, following an "or" logic.
+ * When EXCLUDE_STRICT is set, an "and" logic is applied: all bits must match for a property to be removed.
+ *
+ * @param array $a The array containing the properties to filter
+ * @param int $filter A bit field of Caster::EXCLUDE_* constants specifying which properties to filter out
+ * @param string[] $listedProperties List of properties to exclude when Caster::EXCLUDE_VERBOSE is set, and to preserve when Caster::EXCLUDE_NOT_IMPORTANT is set
+ * @param int &$count Set to the number of removed properties
+ *
+ * @return array The filtered array
+ */
+ public static function filter(array $a, int $filter, array $listedProperties = [], ?int &$count = 0): array
+ {
+ $count = 0;
+
+ foreach ($a as $k => $v) {
+ $type = self::EXCLUDE_STRICT & $filter;
+
+ if (null === $v) {
+ $type |= self::EXCLUDE_NULL & $filter;
+ $type |= self::EXCLUDE_EMPTY & $filter;
+ } elseif (false === $v || '' === $v || '0' === $v || 0 === $v || 0.0 === $v || [] === $v) {
+ $type |= self::EXCLUDE_EMPTY & $filter;
+ }
+ if ((self::EXCLUDE_NOT_IMPORTANT & $filter) && !\in_array($k, $listedProperties, true)) {
+ $type |= self::EXCLUDE_NOT_IMPORTANT;
+ }
+ if ((self::EXCLUDE_VERBOSE & $filter) && \in_array($k, $listedProperties, true)) {
+ $type |= self::EXCLUDE_VERBOSE;
+ }
+
+ if (!isset($k[1]) || "\0" !== $k[0]) {
+ $type |= self::EXCLUDE_PUBLIC & $filter;
+ } elseif ('~' === $k[1]) {
+ $type |= self::EXCLUDE_VIRTUAL & $filter;
+ } elseif ('+' === $k[1]) {
+ $type |= self::EXCLUDE_DYNAMIC & $filter;
+ } elseif ('*' === $k[1]) {
+ $type |= self::EXCLUDE_PROTECTED & $filter;
+ } else {
+ $type |= self::EXCLUDE_PRIVATE & $filter;
+ }
+
+ if ((self::EXCLUDE_STRICT & $filter) ? $type === $filter : $type) {
+ unset($a[$k]);
+ ++$count;
+ }
+ }
+
+ return $a;
+ }
+
+ public static function castPhpIncompleteClass(\__PHP_Incomplete_Class $c, array $a, Stub $stub, bool $isNested): array
+ {
+ if (isset($a['__PHP_Incomplete_Class_Name'])) {
+ $stub->class .= '('.$a['__PHP_Incomplete_Class_Name'].')';
+ unset($a['__PHP_Incomplete_Class_Name']);
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ClassStub.php b/vendor/symfony/var-dumper/Caster/ClassStub.php
new file mode 100644
index 0000000..9a6c7f7
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ClassStub.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents a PHP class identifier.
+ *
+ * @author Nicolas Grekas
+ */
+class ClassStub extends ConstStub
+{
+ /**
+ * @param string $identifier A PHP identifier, e.g. a class, method, interface, etc. name
+ * @param callable $callable The callable targeted by the identifier when it is ambiguous or not a real PHP identifier
+ */
+ public function __construct(string $identifier, $callable = null)
+ {
+ $this->value = $identifier;
+
+ try {
+ if (null !== $callable) {
+ if ($callable instanceof \Closure) {
+ $r = new \ReflectionFunction($callable);
+ } elseif (\is_object($callable)) {
+ $r = [$callable, '__invoke'];
+ } elseif (\is_array($callable)) {
+ $r = $callable;
+ } elseif (false !== $i = strpos($callable, '::')) {
+ $r = [substr($callable, 0, $i), substr($callable, 2 + $i)];
+ } else {
+ $r = new \ReflectionFunction($callable);
+ }
+ } elseif (0 < $i = strpos($identifier, '::') ?: strpos($identifier, '->')) {
+ $r = [substr($identifier, 0, $i), substr($identifier, 2 + $i)];
+ } else {
+ $r = new \ReflectionClass($identifier);
+ }
+
+ if (\is_array($r)) {
+ try {
+ $r = new \ReflectionMethod($r[0], $r[1]);
+ } catch (\ReflectionException $e) {
+ $r = new \ReflectionClass($r[0]);
+ }
+ }
+
+ if (false !== strpos($identifier, "@anonymous\0")) {
+ $this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
+ return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
+ }, $identifier);
+ }
+
+ if (null !== $callable && $r instanceof \ReflectionFunctionAbstract) {
+ $s = ReflectionCaster::castFunctionAbstract($r, [], new Stub(), true, Caster::EXCLUDE_VERBOSE);
+ $s = ReflectionCaster::getSignature($s);
+
+ if ('()' === substr($identifier, -2)) {
+ $this->value = substr_replace($identifier, $s, -2);
+ } else {
+ $this->value .= $s;
+ }
+ }
+ } catch (\ReflectionException $e) {
+ return;
+ } finally {
+ if (0 < $i = strrpos($this->value, '\\')) {
+ $this->attr['ellipsis'] = \strlen($this->value) - $i;
+ $this->attr['ellipsis-type'] = 'class';
+ $this->attr['ellipsis-tail'] = 1;
+ }
+ }
+
+ if ($f = $r->getFileName()) {
+ $this->attr['file'] = $f;
+ $this->attr['line'] = $r->getStartLine();
+ }
+ }
+
+ public static function wrapCallable($callable)
+ {
+ if (\is_object($callable) || !\is_callable($callable)) {
+ return $callable;
+ }
+
+ if (!\is_array($callable)) {
+ $callable = new static($callable, $callable);
+ } elseif (\is_string($callable[0])) {
+ $callable[0] = new static($callable[0], $callable);
+ } else {
+ $callable[1] = new static($callable[1], $callable);
+ }
+
+ return $callable;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ConstStub.php b/vendor/symfony/var-dumper/Caster/ConstStub.php
new file mode 100644
index 0000000..34faefd
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ConstStub.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents a PHP constant and its value.
+ *
+ * @author Nicolas Grekas
+ */
+class ConstStub extends Stub
+{
+ public function __construct(string $name, $value = null)
+ {
+ $this->class = $name;
+ $this->value = 1 < \func_num_args() ? $value : $name;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->value;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/CutArrayStub.php b/vendor/symfony/var-dumper/Caster/CutArrayStub.php
new file mode 100644
index 0000000..140d17e
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/CutArrayStub.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+/**
+ * Represents a cut array.
+ *
+ * @author Nicolas Grekas
+ */
+class CutArrayStub extends CutStub
+{
+ public $preservedSubset;
+
+ public function __construct(array $value, array $preservedKeys)
+ {
+ parent::__construct($value);
+
+ $this->preservedSubset = array_intersect_key($value, array_flip($preservedKeys));
+ $this->cut -= \count($this->preservedSubset);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/CutStub.php b/vendor/symfony/var-dumper/Caster/CutStub.php
new file mode 100644
index 0000000..aa7508c
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/CutStub.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents the main properties of a PHP variable, pre-casted by a caster.
+ *
+ * @author Nicolas Grekas
+ */
+class CutStub extends Stub
+{
+ public function __construct($value)
+ {
+ $this->value = $value;
+
+ switch (\gettype($value)) {
+ case 'object':
+ $this->type = self::TYPE_OBJECT;
+ $this->class = \get_class($value);
+
+ if ($value instanceof \Closure) {
+ ReflectionCaster::castClosure($value, [], $this, true, Caster::EXCLUDE_VERBOSE);
+ }
+
+ $this->cut = -1;
+ break;
+
+ case 'array':
+ $this->type = self::TYPE_ARRAY;
+ $this->class = self::ARRAY_ASSOC;
+ $this->cut = $this->value = \count($value);
+ break;
+
+ case 'resource':
+ case 'unknown type':
+ case 'resource (closed)':
+ $this->type = self::TYPE_RESOURCE;
+ $this->handle = (int) $value;
+ if ('Unknown' === $this->class = @get_resource_type($value)) {
+ $this->class = 'Closed';
+ }
+ $this->cut = -1;
+ break;
+
+ case 'string':
+ $this->type = self::TYPE_STRING;
+ $this->class = preg_match('//u', $value) ? self::STRING_UTF8 : self::STRING_BINARY;
+ $this->cut = self::STRING_BINARY === $this->class ? \strlen($value) : mb_strlen($value, 'UTF-8');
+ $this->value = '';
+ break;
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/DOMCaster.php b/vendor/symfony/var-dumper/Caster/DOMCaster.php
new file mode 100644
index 0000000..7abc3dc
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/DOMCaster.php
@@ -0,0 +1,304 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts DOM related classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class DOMCaster
+{
+ private static $errorCodes = [
+ DOM_PHP_ERR => 'DOM_PHP_ERR',
+ DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR',
+ DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR',
+ DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR',
+ DOM_WRONG_DOCUMENT_ERR => 'DOM_WRONG_DOCUMENT_ERR',
+ DOM_INVALID_CHARACTER_ERR => 'DOM_INVALID_CHARACTER_ERR',
+ DOM_NO_DATA_ALLOWED_ERR => 'DOM_NO_DATA_ALLOWED_ERR',
+ DOM_NO_MODIFICATION_ALLOWED_ERR => 'DOM_NO_MODIFICATION_ALLOWED_ERR',
+ DOM_NOT_FOUND_ERR => 'DOM_NOT_FOUND_ERR',
+ DOM_NOT_SUPPORTED_ERR => 'DOM_NOT_SUPPORTED_ERR',
+ DOM_INUSE_ATTRIBUTE_ERR => 'DOM_INUSE_ATTRIBUTE_ERR',
+ DOM_INVALID_STATE_ERR => 'DOM_INVALID_STATE_ERR',
+ DOM_SYNTAX_ERR => 'DOM_SYNTAX_ERR',
+ DOM_INVALID_MODIFICATION_ERR => 'DOM_INVALID_MODIFICATION_ERR',
+ DOM_NAMESPACE_ERR => 'DOM_NAMESPACE_ERR',
+ DOM_INVALID_ACCESS_ERR => 'DOM_INVALID_ACCESS_ERR',
+ DOM_VALIDATION_ERR => 'DOM_VALIDATION_ERR',
+ ];
+
+ private static $nodeTypes = [
+ XML_ELEMENT_NODE => 'XML_ELEMENT_NODE',
+ XML_ATTRIBUTE_NODE => 'XML_ATTRIBUTE_NODE',
+ XML_TEXT_NODE => 'XML_TEXT_NODE',
+ XML_CDATA_SECTION_NODE => 'XML_CDATA_SECTION_NODE',
+ XML_ENTITY_REF_NODE => 'XML_ENTITY_REF_NODE',
+ XML_ENTITY_NODE => 'XML_ENTITY_NODE',
+ XML_PI_NODE => 'XML_PI_NODE',
+ XML_COMMENT_NODE => 'XML_COMMENT_NODE',
+ XML_DOCUMENT_NODE => 'XML_DOCUMENT_NODE',
+ XML_DOCUMENT_TYPE_NODE => 'XML_DOCUMENT_TYPE_NODE',
+ XML_DOCUMENT_FRAG_NODE => 'XML_DOCUMENT_FRAG_NODE',
+ XML_NOTATION_NODE => 'XML_NOTATION_NODE',
+ XML_HTML_DOCUMENT_NODE => 'XML_HTML_DOCUMENT_NODE',
+ XML_DTD_NODE => 'XML_DTD_NODE',
+ XML_ELEMENT_DECL_NODE => 'XML_ELEMENT_DECL_NODE',
+ XML_ATTRIBUTE_DECL_NODE => 'XML_ATTRIBUTE_DECL_NODE',
+ XML_ENTITY_DECL_NODE => 'XML_ENTITY_DECL_NODE',
+ XML_NAMESPACE_DECL_NODE => 'XML_NAMESPACE_DECL_NODE',
+ ];
+
+ public static function castException(\DOMException $e, array $a, Stub $stub, $isNested)
+ {
+ $k = Caster::PREFIX_PROTECTED.'code';
+ if (isset($a[$k], self::$errorCodes[$a[$k]])) {
+ $a[$k] = new ConstStub(self::$errorCodes[$a[$k]], $a[$k]);
+ }
+
+ return $a;
+ }
+
+ public static function castLength($dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'length' => $dom->length,
+ ];
+
+ return $a;
+ }
+
+ public static function castImplementation($dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'Core' => '1.0',
+ Caster::PREFIX_VIRTUAL.'XML' => '2.0',
+ ];
+
+ return $a;
+ }
+
+ public static function castNode(\DOMNode $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'nodeName' => $dom->nodeName,
+ 'nodeValue' => new CutStub($dom->nodeValue),
+ 'nodeType' => new ConstStub(self::$nodeTypes[$dom->nodeType], $dom->nodeType),
+ 'parentNode' => new CutStub($dom->parentNode),
+ 'childNodes' => $dom->childNodes,
+ 'firstChild' => new CutStub($dom->firstChild),
+ 'lastChild' => new CutStub($dom->lastChild),
+ 'previousSibling' => new CutStub($dom->previousSibling),
+ 'nextSibling' => new CutStub($dom->nextSibling),
+ 'attributes' => $dom->attributes,
+ 'ownerDocument' => new CutStub($dom->ownerDocument),
+ 'namespaceURI' => $dom->namespaceURI,
+ 'prefix' => $dom->prefix,
+ 'localName' => $dom->localName,
+ 'baseURI' => $dom->baseURI ? new LinkStub($dom->baseURI) : $dom->baseURI,
+ 'textContent' => new CutStub($dom->textContent),
+ ];
+
+ return $a;
+ }
+
+ public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'nodeName' => $dom->nodeName,
+ 'nodeValue' => new CutStub($dom->nodeValue),
+ 'nodeType' => new ConstStub(self::$nodeTypes[$dom->nodeType], $dom->nodeType),
+ 'prefix' => $dom->prefix,
+ 'localName' => $dom->localName,
+ 'namespaceURI' => $dom->namespaceURI,
+ 'ownerDocument' => new CutStub($dom->ownerDocument),
+ 'parentNode' => new CutStub($dom->parentNode),
+ ];
+
+ return $a;
+ }
+
+ public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $a += [
+ 'doctype' => $dom->doctype,
+ 'implementation' => $dom->implementation,
+ 'documentElement' => new CutStub($dom->documentElement),
+ 'actualEncoding' => $dom->actualEncoding,
+ 'encoding' => $dom->encoding,
+ 'xmlEncoding' => $dom->xmlEncoding,
+ 'standalone' => $dom->standalone,
+ 'xmlStandalone' => $dom->xmlStandalone,
+ 'version' => $dom->version,
+ 'xmlVersion' => $dom->xmlVersion,
+ 'strictErrorChecking' => $dom->strictErrorChecking,
+ 'documentURI' => $dom->documentURI ? new LinkStub($dom->documentURI) : $dom->documentURI,
+ 'config' => $dom->config,
+ 'formatOutput' => $dom->formatOutput,
+ 'validateOnParse' => $dom->validateOnParse,
+ 'resolveExternals' => $dom->resolveExternals,
+ 'preserveWhiteSpace' => $dom->preserveWhiteSpace,
+ 'recover' => $dom->recover,
+ 'substituteEntities' => $dom->substituteEntities,
+ ];
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE)) {
+ $formatOutput = $dom->formatOutput;
+ $dom->formatOutput = true;
+ $a += [Caster::PREFIX_VIRTUAL.'xml' => $dom->saveXML()];
+ $dom->formatOutput = $formatOutput;
+ }
+
+ return $a;
+ }
+
+ public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'data' => $dom->data,
+ 'length' => $dom->length,
+ ];
+
+ return $a;
+ }
+
+ public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'name' => $dom->name,
+ 'specified' => $dom->specified,
+ 'value' => $dom->value,
+ 'ownerElement' => $dom->ownerElement,
+ 'schemaTypeInfo' => $dom->schemaTypeInfo,
+ ];
+
+ return $a;
+ }
+
+ public static function castElement(\DOMElement $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'tagName' => $dom->tagName,
+ 'schemaTypeInfo' => $dom->schemaTypeInfo,
+ ];
+
+ return $a;
+ }
+
+ public static function castText(\DOMText $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'wholeText' => $dom->wholeText,
+ ];
+
+ return $a;
+ }
+
+ public static function castTypeinfo(\DOMTypeinfo $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'typeName' => $dom->typeName,
+ 'typeNamespace' => $dom->typeNamespace,
+ ];
+
+ return $a;
+ }
+
+ public static function castDomError(\DOMDomError $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'severity' => $dom->severity,
+ 'message' => $dom->message,
+ 'type' => $dom->type,
+ 'relatedException' => $dom->relatedException,
+ 'related_data' => $dom->related_data,
+ 'location' => $dom->location,
+ ];
+
+ return $a;
+ }
+
+ public static function castLocator(\DOMLocator $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'lineNumber' => $dom->lineNumber,
+ 'columnNumber' => $dom->columnNumber,
+ 'offset' => $dom->offset,
+ 'relatedNode' => $dom->relatedNode,
+ 'uri' => $dom->uri ? new LinkStub($dom->uri, $dom->lineNumber) : $dom->uri,
+ ];
+
+ return $a;
+ }
+
+ public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'name' => $dom->name,
+ 'entities' => $dom->entities,
+ 'notations' => $dom->notations,
+ 'publicId' => $dom->publicId,
+ 'systemId' => $dom->systemId,
+ 'internalSubset' => $dom->internalSubset,
+ ];
+
+ return $a;
+ }
+
+ public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'publicId' => $dom->publicId,
+ 'systemId' => $dom->systemId,
+ ];
+
+ return $a;
+ }
+
+ public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'publicId' => $dom->publicId,
+ 'systemId' => $dom->systemId,
+ 'notationName' => $dom->notationName,
+ 'actualEncoding' => $dom->actualEncoding,
+ 'encoding' => $dom->encoding,
+ 'version' => $dom->version,
+ ];
+
+ return $a;
+ }
+
+ public static function castProcessingInstruction(\DOMProcessingInstruction $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'target' => $dom->target,
+ 'data' => $dom->data,
+ ];
+
+ return $a;
+ }
+
+ public static function castXPath(\DOMXPath $dom, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ 'document' => $dom->document,
+ ];
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/DateCaster.php b/vendor/symfony/var-dumper/Caster/DateCaster.php
new file mode 100644
index 0000000..1403c54
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/DateCaster.php
@@ -0,0 +1,128 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts DateTimeInterface related classes to array representation.
+ *
+ * @author Dany Maillard
+ *
+ * @final since Symfony 4.4
+ */
+class DateCaster
+{
+ private const PERIOD_LIMIT = 3;
+
+ public static function castDateTime(\DateTimeInterface $d, array $a, Stub $stub, $isNested, $filter)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $location = $d->getTimezone()->getLocation();
+ $fromNow = (new \DateTime())->diff($d);
+
+ $title = $d->format('l, F j, Y')
+ ."\n".self::formatInterval($fromNow).' from now'
+ .($location ? ($d->format('I') ? "\nDST On" : "\nDST Off") : '')
+ ;
+
+ unset(
+ $a[Caster::PREFIX_DYNAMIC.'date'],
+ $a[Caster::PREFIX_DYNAMIC.'timezone'],
+ $a[Caster::PREFIX_DYNAMIC.'timezone_type']
+ );
+ $a[$prefix.'date'] = new ConstStub(self::formatDateTime($d, $location ? ' e (P)' : ' P'), $title);
+
+ $stub->class .= $d->format(' @U');
+
+ return $a;
+ }
+
+ public static function castInterval(\DateInterval $interval, array $a, Stub $stub, $isNested, $filter)
+ {
+ $now = new \DateTimeImmutable();
+ $numberOfSeconds = $now->add($interval)->getTimestamp() - $now->getTimestamp();
+ $title = number_format($numberOfSeconds, 0, '.', ' ').'s';
+
+ $i = [Caster::PREFIX_VIRTUAL.'interval' => new ConstStub(self::formatInterval($interval), $title)];
+
+ return $filter & Caster::EXCLUDE_VERBOSE ? $i : $i + $a;
+ }
+
+ private static function formatInterval(\DateInterval $i): string
+ {
+ $format = '%R ';
+
+ if (0 === $i->y && 0 === $i->m && ($i->h >= 24 || $i->i >= 60 || $i->s >= 60)) {
+ $i = date_diff($d = new \DateTime(), date_add(clone $d, $i)); // recalculate carry over points
+ $format .= 0 < $i->days ? '%ad ' : '';
+ } else {
+ $format .= ($i->y ? '%yy ' : '').($i->m ? '%mm ' : '').($i->d ? '%dd ' : '');
+ }
+
+ $format .= $i->h || $i->i || $i->s || $i->f ? '%H:%I:'.self::formatSeconds($i->s, substr($i->f, 2)) : '';
+ $format = '%R ' === $format ? '0s' : $format;
+
+ return $i->format(rtrim($format));
+ }
+
+ public static function castTimeZone(\DateTimeZone $timeZone, array $a, Stub $stub, $isNested, $filter)
+ {
+ $location = $timeZone->getLocation();
+ $formatted = (new \DateTime('now', $timeZone))->format($location ? 'e (P)' : 'P');
+ $title = $location && \extension_loaded('intl') ? \Locale::getDisplayRegion('-'.$location['country_code']) : '';
+
+ $z = [Caster::PREFIX_VIRTUAL.'timezone' => new ConstStub($formatted, $title)];
+
+ return $filter & Caster::EXCLUDE_VERBOSE ? $z : $z + $a;
+ }
+
+ public static function castPeriod(\DatePeriod $p, array $a, Stub $stub, $isNested, $filter)
+ {
+ $dates = [];
+ if (\PHP_VERSION_ID >= 70107) { // see https://bugs.php.net/74639
+ foreach (clone $p as $i => $d) {
+ if (self::PERIOD_LIMIT === $i) {
+ $now = new \DateTimeImmutable();
+ $dates[] = sprintf('%s more', ($end = $p->getEndDate())
+ ? ceil(($end->format('U.u') - $d->format('U.u')) / ((int) $now->add($p->getDateInterval())->format('U.u') - (int) $now->format('U.u')))
+ : $p->recurrences - $i
+ );
+ break;
+ }
+ $dates[] = sprintf('%s) %s', $i + 1, self::formatDateTime($d));
+ }
+ }
+
+ $period = sprintf(
+ 'every %s, from %s (%s) %s',
+ self::formatInterval($p->getDateInterval()),
+ self::formatDateTime($p->getStartDate()),
+ $p->include_start_date ? 'included' : 'excluded',
+ ($end = $p->getEndDate()) ? 'to '.self::formatDateTime($end) : 'recurring '.$p->recurrences.' time/s'
+ );
+
+ $p = [Caster::PREFIX_VIRTUAL.'period' => new ConstStub($period, implode("\n", $dates))];
+
+ return $filter & Caster::EXCLUDE_VERBOSE ? $p : $p + $a;
+ }
+
+ private static function formatDateTime(\DateTimeInterface $d, string $extra = ''): string
+ {
+ return $d->format('Y-m-d H:i:'.self::formatSeconds($d->format('s'), $d->format('u')).$extra);
+ }
+
+ private static function formatSeconds(string $s, string $us): string
+ {
+ return sprintf('%02d.%s', $s, 0 === ($len = \strlen($t = rtrim($us, '0'))) ? '0' : ($len <= 3 ? str_pad($t, 3, '0') : $us));
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/DoctrineCaster.php b/vendor/symfony/var-dumper/Caster/DoctrineCaster.php
new file mode 100644
index 0000000..12790e4
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/DoctrineCaster.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Doctrine\Common\Proxy\Proxy as CommonProxy;
+use Doctrine\ORM\PersistentCollection;
+use Doctrine\ORM\Proxy\Proxy as OrmProxy;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Doctrine related classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class DoctrineCaster
+{
+ public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, $isNested)
+ {
+ foreach (['__cloner__', '__initializer__'] as $k) {
+ if (\array_key_exists($k, $a)) {
+ unset($a[$k]);
+ ++$stub->cut;
+ }
+ }
+
+ return $a;
+ }
+
+ public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, $isNested)
+ {
+ foreach (['_entityPersister', '_identifier'] as $k) {
+ if (\array_key_exists($k = "\0Doctrine\\ORM\\Proxy\\Proxy\0".$k, $a)) {
+ unset($a[$k]);
+ ++$stub->cut;
+ }
+ }
+
+ return $a;
+ }
+
+ public static function castPersistentCollection(PersistentCollection $coll, array $a, Stub $stub, $isNested)
+ {
+ foreach (['snapshot', 'association', 'typeClass'] as $k) {
+ if (\array_key_exists($k = "\0Doctrine\\ORM\\PersistentCollection\0".$k, $a)) {
+ $a[$k] = new CutStub($a[$k]);
+ }
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/DsCaster.php b/vendor/symfony/var-dumper/Caster/DsCaster.php
new file mode 100644
index 0000000..49e3263
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/DsCaster.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Ds\Collection;
+use Ds\Map;
+use Ds\Pair;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Ds extension classes to array representation.
+ *
+ * @author Jáchym Toušek
+ *
+ * @final since Symfony 4.4
+ */
+class DsCaster
+{
+ public static function castCollection(Collection $c, array $a, Stub $stub, bool $isNested): array
+ {
+ $a[Caster::PREFIX_VIRTUAL.'count'] = $c->count();
+ $a[Caster::PREFIX_VIRTUAL.'capacity'] = $c->capacity();
+
+ if (!$c instanceof Map) {
+ $a += $c->toArray();
+ }
+
+ return $a;
+ }
+
+ public static function castMap(Map $c, array $a, Stub $stub, bool $isNested): array
+ {
+ foreach ($c as $k => $v) {
+ $a[] = new DsPairStub($k, $v);
+ }
+
+ return $a;
+ }
+
+ public static function castPair(Pair $c, array $a, Stub $stub, bool $isNested): array
+ {
+ foreach ($c->toArray() as $k => $v) {
+ $a[Caster::PREFIX_VIRTUAL.$k] = $v;
+ }
+
+ return $a;
+ }
+
+ public static function castPairStub(DsPairStub $c, array $a, Stub $stub, bool $isNested): array
+ {
+ if ($isNested) {
+ $stub->class = Pair::class;
+ $stub->value = null;
+ $stub->handle = 0;
+
+ $a = $c->value;
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/DsPairStub.php b/vendor/symfony/var-dumper/Caster/DsPairStub.php
new file mode 100644
index 0000000..c6c4a0d
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/DsPairStub.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Nicolas Grekas
+ */
+class DsPairStub extends Stub
+{
+ public function __construct($key, $value)
+ {
+ $this->value = [
+ Caster::PREFIX_VIRTUAL.'key' => $key,
+ Caster::PREFIX_VIRTUAL.'value' => $value,
+ ];
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/EnumStub.php b/vendor/symfony/var-dumper/Caster/EnumStub.php
new file mode 100644
index 0000000..cb77482
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/EnumStub.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents an enumeration of values.
+ *
+ * @author Nicolas Grekas
+ */
+class EnumStub extends Stub
+{
+ public $dumpKeys = true;
+
+ public function __construct(array $values, bool $dumpKeys = true)
+ {
+ $this->value = $values;
+ $this->dumpKeys = $dumpKeys;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ExceptionCaster.php b/vendor/symfony/var-dumper/Caster/ExceptionCaster.php
new file mode 100644
index 0000000..7d683fe
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ExceptionCaster.php
@@ -0,0 +1,382 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
+use Symfony\Component\VarDumper\Cloner\Stub;
+use Symfony\Component\VarDumper\Exception\ThrowingCasterException;
+
+/**
+ * Casts common Exception classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class ExceptionCaster
+{
+ public static $srcContext = 1;
+ public static $traceArgs = true;
+ public static $errorTypes = [
+ E_DEPRECATED => 'E_DEPRECATED',
+ E_USER_DEPRECATED => 'E_USER_DEPRECATED',
+ E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
+ E_ERROR => 'E_ERROR',
+ E_WARNING => 'E_WARNING',
+ E_PARSE => 'E_PARSE',
+ E_NOTICE => 'E_NOTICE',
+ E_CORE_ERROR => 'E_CORE_ERROR',
+ E_CORE_WARNING => 'E_CORE_WARNING',
+ E_COMPILE_ERROR => 'E_COMPILE_ERROR',
+ E_COMPILE_WARNING => 'E_COMPILE_WARNING',
+ E_USER_ERROR => 'E_USER_ERROR',
+ E_USER_WARNING => 'E_USER_WARNING',
+ E_USER_NOTICE => 'E_USER_NOTICE',
+ E_STRICT => 'E_STRICT',
+ ];
+
+ private static $framesCache = [];
+
+ public static function castError(\Error $e, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ return self::filterExceptionArray($stub->class, $a, "\0Error\0", $filter);
+ }
+
+ public static function castException(\Exception $e, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ return self::filterExceptionArray($stub->class, $a, "\0Exception\0", $filter);
+ }
+
+ public static function castErrorException(\ErrorException $e, array $a, Stub $stub, $isNested)
+ {
+ if (isset($a[$s = Caster::PREFIX_PROTECTED.'severity'], self::$errorTypes[$a[$s]])) {
+ $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]);
+ }
+
+ return $a;
+ }
+
+ public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, $isNested)
+ {
+ $trace = Caster::PREFIX_VIRTUAL.'trace';
+ $prefix = Caster::PREFIX_PROTECTED;
+ $xPrefix = "\0Exception\0";
+
+ if (isset($a[$xPrefix.'previous'], $a[$trace]) && $a[$xPrefix.'previous'] instanceof \Exception) {
+ $b = (array) $a[$xPrefix.'previous'];
+ $class = get_debug_type($a[$xPrefix.'previous']);
+ self::traceUnshift($b[$xPrefix.'trace'], $class, $b[$prefix.'file'], $b[$prefix.'line']);
+ $a[$trace] = new TraceStub($b[$xPrefix.'trace'], false, 0, -\count($a[$trace]->value));
+ }
+
+ unset($a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']);
+
+ return $a;
+ }
+
+ public static function castSilencedErrorContext(SilencedErrorContext $e, array $a, Stub $stub, $isNested)
+ {
+ $sPrefix = "\0".SilencedErrorContext::class."\0";
+
+ if (!isset($a[$s = $sPrefix.'severity'])) {
+ return $a;
+ }
+
+ if (isset(self::$errorTypes[$a[$s]])) {
+ $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]);
+ }
+
+ $trace = [[
+ 'file' => $a[$sPrefix.'file'],
+ 'line' => $a[$sPrefix.'line'],
+ ]];
+
+ if (isset($a[$sPrefix.'trace'])) {
+ $trace = array_merge($trace, $a[$sPrefix.'trace']);
+ }
+
+ unset($a[$sPrefix.'file'], $a[$sPrefix.'line'], $a[$sPrefix.'trace']);
+ $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs);
+
+ return $a;
+ }
+
+ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $isNested)
+ {
+ if (!$isNested) {
+ return $a;
+ }
+ $stub->class = '';
+ $stub->handle = 0;
+ $frames = $trace->value;
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a = [];
+ $j = \count($frames);
+ if (0 > $i = $trace->sliceOffset) {
+ $i = max(0, $j + $i);
+ }
+ if (!isset($trace->value[$i])) {
+ return [];
+ }
+ $lastCall = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '';
+ $frames[] = ['function' => ''];
+ $collapse = false;
+
+ for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) {
+ $f = $frames[$i];
+ $call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'] : '???';
+
+ $frame = new FrameStub(
+ [
+ 'object' => isset($f['object']) ? $f['object'] : null,
+ 'class' => isset($f['class']) ? $f['class'] : null,
+ 'type' => isset($f['type']) ? $f['type'] : null,
+ 'function' => isset($f['function']) ? $f['function'] : null,
+ ] + $frames[$i - 1],
+ false,
+ true
+ );
+ $f = self::castFrameStub($frame, [], $frame, true);
+ if (isset($f[$prefix.'src'])) {
+ foreach ($f[$prefix.'src']->value as $label => $frame) {
+ if (0 === strpos($label, "\0~collapse=0")) {
+ if ($collapse) {
+ $label = substr_replace($label, '1', 11, 1);
+ } else {
+ $collapse = true;
+ }
+ }
+ $label = substr_replace($label, "title=Stack level $j.&", 2, 0);
+ }
+ $f = $frames[$i - 1];
+ if ($trace->keepArgs && !empty($f['args']) && $frame instanceof EnumStub) {
+ $frame->value['arguments'] = new ArgsStub($f['args'], isset($f['function']) ? $f['function'] : null, isset($f['class']) ? $f['class'] : null);
+ }
+ } elseif ('???' !== $lastCall) {
+ $label = new ClassStub($lastCall);
+ if (isset($label->attr['ellipsis'])) {
+ $label->attr['ellipsis'] += 2;
+ $label = substr_replace($prefix, "ellipsis-type=class&ellipsis={$label->attr['ellipsis']}&ellipsis-tail=1&title=Stack level $j.", 2, 0).$label->value.'()';
+ } else {
+ $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$label->value.'()';
+ }
+ } else {
+ $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall;
+ }
+ $a[substr_replace($label, sprintf('separator=%s&', $frame instanceof EnumStub ? ' ' : ':'), 2, 0)] = $frame;
+
+ $lastCall = $call;
+ }
+ if (null !== $trace->sliceLength) {
+ $a = \array_slice($a, 0, $trace->sliceLength, true);
+ }
+
+ return $a;
+ }
+
+ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $isNested)
+ {
+ if (!$isNested) {
+ return $a;
+ }
+ $f = $frame->value;
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ if (isset($f['file'], $f['line'])) {
+ $cacheKey = $f;
+ unset($cacheKey['object'], $cacheKey['args']);
+ $cacheKey[] = self::$srcContext;
+ $cacheKey = implode('-', $cacheKey);
+
+ if (isset(self::$framesCache[$cacheKey])) {
+ $a[$prefix.'src'] = self::$framesCache[$cacheKey];
+ } else {
+ if (preg_match('/\((\d+)\)(?:\([\da-f]{32}\))? : (?:eval\(\)\'d code|runtime-created function)$/', $f['file'], $match)) {
+ $f['file'] = substr($f['file'], 0, -\strlen($match[0]));
+ $f['line'] = (int) $match[1];
+ }
+ $src = $f['line'];
+ $srcKey = $f['file'];
+ $ellipsis = new LinkStub($srcKey, 0);
+ $srcAttr = 'collapse='.(int) $ellipsis->inVendor;
+ $ellipsisTail = isset($ellipsis->attr['ellipsis-tail']) ? $ellipsis->attr['ellipsis-tail'] : 0;
+ $ellipsis = isset($ellipsis->attr['ellipsis']) ? $ellipsis->attr['ellipsis'] : 0;
+
+ if (file_exists($f['file']) && 0 <= self::$srcContext) {
+ if (!empty($f['class']) && (is_subclass_of($f['class'], 'Twig\Template') || is_subclass_of($f['class'], 'Twig_Template')) && method_exists($f['class'], 'getDebugInfo')) {
+ $template = isset($f['object']) ? $f['object'] : unserialize(sprintf('O:%d:"%s":0:{}', \strlen($f['class']), $f['class']));
+
+ $ellipsis = 0;
+ $templateSrc = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : '');
+ $templateInfo = $template->getDebugInfo();
+ if (isset($templateInfo[$f['line']])) {
+ if (!method_exists($template, 'getSourceContext') || !file_exists($templatePath = $template->getSourceContext()->getPath())) {
+ $templatePath = null;
+ }
+ if ($templateSrc) {
+ $src = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext, 'twig', $templatePath, $f);
+ $srcKey = ($templatePath ?: $template->getTemplateName()).':'.$templateInfo[$f['line']];
+ }
+ }
+ }
+ if ($srcKey == $f['file']) {
+ $src = self::extractSource(file_get_contents($f['file']), $f['line'], self::$srcContext, 'php', $f['file'], $f);
+ $srcKey .= ':'.$f['line'];
+ if ($ellipsis) {
+ $ellipsis += 1 + \strlen($f['line']);
+ }
+ }
+ $srcAttr .= sprintf('&separator= &file=%s&line=%d', rawurlencode($f['file']), $f['line']);
+ } else {
+ $srcAttr .= '&separator=:';
+ }
+ $srcAttr .= $ellipsis ? '&ellipsis-type=path&ellipsis='.$ellipsis.'&ellipsis-tail='.$ellipsisTail : '';
+ self::$framesCache[$cacheKey] = $a[$prefix.'src'] = new EnumStub(["\0~$srcAttr\0$srcKey" => $src]);
+ }
+ }
+
+ unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']);
+ if ($frame->inTraceStub) {
+ unset($a[$prefix.'class'], $a[$prefix.'type'], $a[$prefix.'function']);
+ }
+ foreach ($a as $k => $v) {
+ if (!$v) {
+ unset($a[$k]);
+ }
+ }
+ if ($frame->keepArgs && !empty($f['args'])) {
+ $a[$prefix.'arguments'] = new ArgsStub($f['args'], $f['function'], $f['class']);
+ }
+
+ return $a;
+ }
+
+ private static function filterExceptionArray(string $xClass, array $a, string $xPrefix, int $filter): array
+ {
+ if (isset($a[$xPrefix.'trace'])) {
+ $trace = $a[$xPrefix.'trace'];
+ unset($a[$xPrefix.'trace']); // Ensures the trace is always last
+ } else {
+ $trace = [];
+ }
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE) && $trace) {
+ if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) {
+ self::traceUnshift($trace, $xClass, $a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']);
+ }
+ $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs);
+ }
+ if (empty($a[$xPrefix.'previous'])) {
+ unset($a[$xPrefix.'previous']);
+ }
+ unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message'], $a[Caster::PREFIX_DYNAMIC.'__destructorException']);
+
+ if (isset($a[Caster::PREFIX_PROTECTED.'message']) && false !== strpos($a[Caster::PREFIX_PROTECTED.'message'], "@anonymous\0")) {
+ $a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
+ return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
+ }, $a[Caster::PREFIX_PROTECTED.'message']);
+ }
+
+ if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) {
+ $a[Caster::PREFIX_PROTECTED.'file'] = new LinkStub($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']);
+ }
+
+ return $a;
+ }
+
+ private static function traceUnshift(array &$trace, ?string $class, string $file, int $line): void
+ {
+ if (isset($trace[0]['file'], $trace[0]['line']) && $trace[0]['file'] === $file && $trace[0]['line'] === $line) {
+ return;
+ }
+ array_unshift($trace, [
+ 'function' => $class ? 'new '.$class : null,
+ 'file' => $file,
+ 'line' => $line,
+ ]);
+ }
+
+ private static function extractSource(string $srcLines, int $line, int $srcContext, string $lang, ?string $file, array $frame): EnumStub
+ {
+ $srcLines = explode("\n", $srcLines);
+ $src = [];
+
+ for ($i = $line - 1 - $srcContext; $i <= $line - 1 + $srcContext; ++$i) {
+ $src[] = (isset($srcLines[$i]) ? $srcLines[$i] : '')."\n";
+ }
+
+ if ($frame['function'] ?? false) {
+ $stub = new CutStub(new \stdClass());
+ $stub->class = (isset($frame['class']) ? $frame['class'].$frame['type'] : '').$frame['function'];
+ $stub->type = Stub::TYPE_OBJECT;
+ $stub->attr['cut_hash'] = true;
+ $stub->attr['file'] = $frame['file'];
+ $stub->attr['line'] = $frame['line'];
+
+ try {
+ $caller = isset($frame['class']) ? new \ReflectionMethod($frame['class'], $frame['function']) : new \ReflectionFunction($frame['function']);
+ $stub->class .= ReflectionCaster::getSignature(ReflectionCaster::castFunctionAbstract($caller, [], $stub, true, Caster::EXCLUDE_VERBOSE));
+
+ if ($f = $caller->getFileName()) {
+ $stub->attr['file'] = $f;
+ $stub->attr['line'] = $caller->getStartLine();
+ }
+ } catch (\ReflectionException $e) {
+ // ignore fake class/function
+ }
+
+ $srcLines = ["\0~separator=\0" => $stub];
+ } else {
+ $stub = null;
+ $srcLines = [];
+ }
+
+ $ltrim = 0;
+ do {
+ $pad = null;
+ for ($i = $srcContext << 1; $i >= 0; --$i) {
+ if (isset($src[$i][$ltrim]) && "\r" !== ($c = $src[$i][$ltrim]) && "\n" !== $c) {
+ if (null === $pad) {
+ $pad = $c;
+ }
+ if ((' ' !== $c && "\t" !== $c) || $pad !== $c) {
+ break;
+ }
+ }
+ }
+ ++$ltrim;
+ } while (0 > $i && null !== $pad);
+
+ --$ltrim;
+
+ foreach ($src as $i => $c) {
+ if ($ltrim) {
+ $c = isset($c[$ltrim]) && "\r" !== $c[$ltrim] ? substr($c, $ltrim) : ltrim($c, " \t");
+ }
+ $c = substr($c, 0, -1);
+ if ($i !== $srcContext) {
+ $c = new ConstStub('default', $c);
+ } else {
+ $c = new ConstStub($c, $stub ? 'in '.$stub->class : '');
+ if (null !== $file) {
+ $c->attr['file'] = $file;
+ $c->attr['line'] = $line;
+ }
+ }
+ $c->attr['lang'] = $lang;
+ $srcLines[sprintf("\0~separator=› &%d\0", $i + $line - $srcContext)] = $c;
+ }
+
+ return new EnumStub($srcLines);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/FrameStub.php b/vendor/symfony/var-dumper/Caster/FrameStub.php
new file mode 100644
index 0000000..a3a3fda
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/FrameStub.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+/**
+ * Represents a single backtrace frame as returned by debug_backtrace() or Exception->getTrace().
+ *
+ * @author Nicolas Grekas
+ */
+class FrameStub extends EnumStub
+{
+ public $keepArgs;
+ public $inTraceStub;
+
+ public function __construct(array $frame, bool $keepArgs = true, bool $inTraceStub = false)
+ {
+ $this->value = $frame;
+ $this->keepArgs = $keepArgs;
+ $this->inTraceStub = $inTraceStub;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/GmpCaster.php b/vendor/symfony/var-dumper/Caster/GmpCaster.php
new file mode 100644
index 0000000..572ec43
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/GmpCaster.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts GMP objects to array representation.
+ *
+ * @author Hamza Amrouche
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class GmpCaster
+{
+ public static function castGmp(\GMP $gmp, array $a, Stub $stub, $isNested, $filter): array
+ {
+ $a[Caster::PREFIX_VIRTUAL.'value'] = new ConstStub(gmp_strval($gmp), gmp_strval($gmp));
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ImagineCaster.php b/vendor/symfony/var-dumper/Caster/ImagineCaster.php
new file mode 100644
index 0000000..204689b
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ImagineCaster.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Imagine\Image\ImageInterface;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Grégoire Pineau
+ */
+final class ImagineCaster
+{
+ public static function castImage(ImageInterface $c, array $a, Stub $stub, bool $isNested): array
+ {
+ $imgData = $c->get('png');
+ if (\strlen($imgData) > 1 * 1000 * 1000) {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'image' => new ConstStub($c->getSize()),
+ ];
+ } else {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'image' => new ImgStub($imgData, 'image/png', $c->getSize()),
+ ];
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ImgStub.php b/vendor/symfony/var-dumper/Caster/ImgStub.php
new file mode 100644
index 0000000..bdec88e
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ImgStub.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+/**
+ * @author Grégoire Pineau
+ */
+class ImgStub extends ConstStub
+{
+ public function __construct(string $data, string $contentType, string $size)
+ {
+ $this->value = '';
+ $this->attr['img-data'] = $data;
+ $this->attr['img-size'] = $size;
+ $this->attr['content-type'] = $contentType;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/IntlCaster.php b/vendor/symfony/var-dumper/Caster/IntlCaster.php
new file mode 100644
index 0000000..7bff2eb
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/IntlCaster.php
@@ -0,0 +1,172 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Nicolas Grekas
+ * @author Jan Schädlich
+ *
+ * @final since Symfony 4.4
+ */
+class IntlCaster
+{
+ public static function castMessageFormatter(\MessageFormatter $c, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(),
+ Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(),
+ ];
+
+ return self::castError($c, $a);
+ }
+
+ public static function castNumberFormatter(\NumberFormatter $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(),
+ Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(),
+ ];
+
+ if ($filter & Caster::EXCLUDE_VERBOSE) {
+ $stub->cut += 3;
+
+ return self::castError($c, $a);
+ }
+
+ $a += [
+ Caster::PREFIX_VIRTUAL.'attributes' => new EnumStub(
+ [
+ 'PARSE_INT_ONLY' => $c->getAttribute(\NumberFormatter::PARSE_INT_ONLY),
+ 'GROUPING_USED' => $c->getAttribute(\NumberFormatter::GROUPING_USED),
+ 'DECIMAL_ALWAYS_SHOWN' => $c->getAttribute(\NumberFormatter::DECIMAL_ALWAYS_SHOWN),
+ 'MAX_INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_INTEGER_DIGITS),
+ 'MIN_INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_INTEGER_DIGITS),
+ 'INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::INTEGER_DIGITS),
+ 'MAX_FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_FRACTION_DIGITS),
+ 'MIN_FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_FRACTION_DIGITS),
+ 'FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::FRACTION_DIGITS),
+ 'MULTIPLIER' => $c->getAttribute(\NumberFormatter::MULTIPLIER),
+ 'GROUPING_SIZE' => $c->getAttribute(\NumberFormatter::GROUPING_SIZE),
+ 'ROUNDING_MODE' => $c->getAttribute(\NumberFormatter::ROUNDING_MODE),
+ 'ROUNDING_INCREMENT' => $c->getAttribute(\NumberFormatter::ROUNDING_INCREMENT),
+ 'FORMAT_WIDTH' => $c->getAttribute(\NumberFormatter::FORMAT_WIDTH),
+ 'PADDING_POSITION' => $c->getAttribute(\NumberFormatter::PADDING_POSITION),
+ 'SECONDARY_GROUPING_SIZE' => $c->getAttribute(\NumberFormatter::SECONDARY_GROUPING_SIZE),
+ 'SIGNIFICANT_DIGITS_USED' => $c->getAttribute(\NumberFormatter::SIGNIFICANT_DIGITS_USED),
+ 'MIN_SIGNIFICANT_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_SIGNIFICANT_DIGITS),
+ 'MAX_SIGNIFICANT_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_SIGNIFICANT_DIGITS),
+ 'LENIENT_PARSE' => $c->getAttribute(\NumberFormatter::LENIENT_PARSE),
+ ]
+ ),
+ Caster::PREFIX_VIRTUAL.'text_attributes' => new EnumStub(
+ [
+ 'POSITIVE_PREFIX' => $c->getTextAttribute(\NumberFormatter::POSITIVE_PREFIX),
+ 'POSITIVE_SUFFIX' => $c->getTextAttribute(\NumberFormatter::POSITIVE_SUFFIX),
+ 'NEGATIVE_PREFIX' => $c->getTextAttribute(\NumberFormatter::NEGATIVE_PREFIX),
+ 'NEGATIVE_SUFFIX' => $c->getTextAttribute(\NumberFormatter::NEGATIVE_SUFFIX),
+ 'PADDING_CHARACTER' => $c->getTextAttribute(\NumberFormatter::PADDING_CHARACTER),
+ 'CURRENCY_CODE' => $c->getTextAttribute(\NumberFormatter::CURRENCY_CODE),
+ 'DEFAULT_RULESET' => $c->getTextAttribute(\NumberFormatter::DEFAULT_RULESET),
+ 'PUBLIC_RULESETS' => $c->getTextAttribute(\NumberFormatter::PUBLIC_RULESETS),
+ ]
+ ),
+ Caster::PREFIX_VIRTUAL.'symbols' => new EnumStub(
+ [
+ 'DECIMAL_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL),
+ 'GROUPING_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL),
+ 'PATTERN_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::PATTERN_SEPARATOR_SYMBOL),
+ 'PERCENT_SYMBOL' => $c->getSymbol(\NumberFormatter::PERCENT_SYMBOL),
+ 'ZERO_DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::ZERO_DIGIT_SYMBOL),
+ 'DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::DIGIT_SYMBOL),
+ 'MINUS_SIGN_SYMBOL' => $c->getSymbol(\NumberFormatter::MINUS_SIGN_SYMBOL),
+ 'PLUS_SIGN_SYMBOL' => $c->getSymbol(\NumberFormatter::PLUS_SIGN_SYMBOL),
+ 'CURRENCY_SYMBOL' => $c->getSymbol(\NumberFormatter::CURRENCY_SYMBOL),
+ 'INTL_CURRENCY_SYMBOL' => $c->getSymbol(\NumberFormatter::INTL_CURRENCY_SYMBOL),
+ 'MONETARY_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::MONETARY_SEPARATOR_SYMBOL),
+ 'EXPONENTIAL_SYMBOL' => $c->getSymbol(\NumberFormatter::EXPONENTIAL_SYMBOL),
+ 'PERMILL_SYMBOL' => $c->getSymbol(\NumberFormatter::PERMILL_SYMBOL),
+ 'PAD_ESCAPE_SYMBOL' => $c->getSymbol(\NumberFormatter::PAD_ESCAPE_SYMBOL),
+ 'INFINITY_SYMBOL' => $c->getSymbol(\NumberFormatter::INFINITY_SYMBOL),
+ 'NAN_SYMBOL' => $c->getSymbol(\NumberFormatter::NAN_SYMBOL),
+ 'SIGNIFICANT_DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL),
+ 'MONETARY_GROUPING_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL),
+ ]
+ ),
+ ];
+
+ return self::castError($c, $a);
+ }
+
+ public static function castIntlTimeZone(\IntlTimeZone $c, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'display_name' => $c->getDisplayName(),
+ Caster::PREFIX_VIRTUAL.'id' => $c->getID(),
+ Caster::PREFIX_VIRTUAL.'raw_offset' => $c->getRawOffset(),
+ ];
+
+ if ($c->useDaylightTime()) {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'dst_savings' => $c->getDSTSavings(),
+ ];
+ }
+
+ return self::castError($c, $a);
+ }
+
+ public static function castIntlCalendar(\IntlCalendar $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'type' => $c->getType(),
+ Caster::PREFIX_VIRTUAL.'first_day_of_week' => $c->getFirstDayOfWeek(),
+ Caster::PREFIX_VIRTUAL.'minimal_days_in_first_week' => $c->getMinimalDaysInFirstWeek(),
+ Caster::PREFIX_VIRTUAL.'repeated_wall_time_option' => $c->getRepeatedWallTimeOption(),
+ Caster::PREFIX_VIRTUAL.'skipped_wall_time_option' => $c->getSkippedWallTimeOption(),
+ Caster::PREFIX_VIRTUAL.'time' => $c->getTime(),
+ Caster::PREFIX_VIRTUAL.'in_daylight_time' => $c->inDaylightTime(),
+ Caster::PREFIX_VIRTUAL.'is_lenient' => $c->isLenient(),
+ Caster::PREFIX_VIRTUAL.'time_zone' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getTimeZone()) : $c->getTimeZone(),
+ ];
+
+ return self::castError($c, $a);
+ }
+
+ public static function castIntlDateFormatter(\IntlDateFormatter $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(),
+ Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(),
+ Caster::PREFIX_VIRTUAL.'calendar' => $c->getCalendar(),
+ Caster::PREFIX_VIRTUAL.'time_zone_id' => $c->getTimeZoneId(),
+ Caster::PREFIX_VIRTUAL.'time_type' => $c->getTimeType(),
+ Caster::PREFIX_VIRTUAL.'date_type' => $c->getDateType(),
+ Caster::PREFIX_VIRTUAL.'calendar_object' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getCalendarObject()) : $c->getCalendarObject(),
+ Caster::PREFIX_VIRTUAL.'time_zone' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getTimeZone()) : $c->getTimeZone(),
+ ];
+
+ return self::castError($c, $a);
+ }
+
+ private static function castError($c, array $a): array
+ {
+ if ($errorCode = $c->getErrorCode()) {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'error_code' => $errorCode,
+ Caster::PREFIX_VIRTUAL.'error_message' => $c->getErrorMessage(),
+ ];
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/LinkStub.php b/vendor/symfony/var-dumper/Caster/LinkStub.php
new file mode 100644
index 0000000..6e6147c
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/LinkStub.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+/**
+ * Represents a file or a URL.
+ *
+ * @author Nicolas Grekas
+ */
+class LinkStub extends ConstStub
+{
+ public $inVendor = false;
+
+ private static $vendorRoots;
+ private static $composerRoots;
+
+ public function __construct($label, int $line = 0, $href = null)
+ {
+ $this->value = $label;
+
+ if (null === $href) {
+ $href = $label;
+ }
+ if (!\is_string($href)) {
+ return;
+ }
+ if (0 === strpos($href, 'file://')) {
+ if ($href === $label) {
+ $label = substr($label, 7);
+ }
+ $href = substr($href, 7);
+ } elseif (false !== strpos($href, '://')) {
+ $this->attr['href'] = $href;
+
+ return;
+ }
+ if (!file_exists($href)) {
+ return;
+ }
+ if ($line) {
+ $this->attr['line'] = $line;
+ }
+ if ($label !== $this->attr['file'] = realpath($href) ?: $href) {
+ return;
+ }
+ if ($composerRoot = $this->getComposerRoot($href, $this->inVendor)) {
+ $this->attr['ellipsis'] = \strlen($href) - \strlen($composerRoot) + 1;
+ $this->attr['ellipsis-type'] = 'path';
+ $this->attr['ellipsis-tail'] = 1 + ($this->inVendor ? 2 + \strlen(implode('', \array_slice(explode(\DIRECTORY_SEPARATOR, substr($href, 1 - $this->attr['ellipsis'])), 0, 2))) : 0);
+ } elseif (3 < \count($ellipsis = explode(\DIRECTORY_SEPARATOR, $href))) {
+ $this->attr['ellipsis'] = 2 + \strlen(implode('', \array_slice($ellipsis, -2)));
+ $this->attr['ellipsis-type'] = 'path';
+ $this->attr['ellipsis-tail'] = 1;
+ }
+ }
+
+ private function getComposerRoot(string $file, bool &$inVendor)
+ {
+ if (null === self::$vendorRoots) {
+ self::$vendorRoots = [];
+
+ foreach (get_declared_classes() as $class) {
+ if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) {
+ $r = new \ReflectionClass($class);
+ $v = \dirname($r->getFileName(), 2);
+ if (file_exists($v.'/composer/installed.json')) {
+ self::$vendorRoots[] = $v.\DIRECTORY_SEPARATOR;
+ }
+ }
+ }
+ }
+ $inVendor = false;
+
+ if (isset(self::$composerRoots[$dir = \dirname($file)])) {
+ return self::$composerRoots[$dir];
+ }
+
+ foreach (self::$vendorRoots as $root) {
+ if ($inVendor = 0 === strpos($file, $root)) {
+ return $root;
+ }
+ }
+
+ $parent = $dir;
+ while (!@file_exists($parent.'/composer.json')) {
+ if (!@file_exists($parent)) {
+ // open_basedir restriction in effect
+ break;
+ }
+ if ($parent === \dirname($parent)) {
+ return self::$composerRoots[$dir] = false;
+ }
+
+ $parent = \dirname($parent);
+ }
+
+ return self::$composerRoots[$dir] = $parent.\DIRECTORY_SEPARATOR;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/MemcachedCaster.php b/vendor/symfony/var-dumper/Caster/MemcachedCaster.php
new file mode 100644
index 0000000..81b9996
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/MemcachedCaster.php
@@ -0,0 +1,81 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Jan Schädlich
+ *
+ * @final since Symfony 4.4
+ */
+class MemcachedCaster
+{
+ private static $optionConstants;
+ private static $defaultOptions;
+
+ public static function castMemcached(\Memcached $c, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'servers' => $c->getServerList(),
+ Caster::PREFIX_VIRTUAL.'options' => new EnumStub(
+ self::getNonDefaultOptions($c)
+ ),
+ ];
+
+ return $a;
+ }
+
+ private static function getNonDefaultOptions(\Memcached $c): array
+ {
+ self::$defaultOptions = self::$defaultOptions ?? self::discoverDefaultOptions();
+ self::$optionConstants = self::$optionConstants ?? self::getOptionConstants();
+
+ $nonDefaultOptions = [];
+ foreach (self::$optionConstants as $constantKey => $value) {
+ if (self::$defaultOptions[$constantKey] !== $option = $c->getOption($value)) {
+ $nonDefaultOptions[$constantKey] = $option;
+ }
+ }
+
+ return $nonDefaultOptions;
+ }
+
+ private static function discoverDefaultOptions(): array
+ {
+ $defaultMemcached = new \Memcached();
+ $defaultMemcached->addServer('127.0.0.1', 11211);
+
+ $defaultOptions = [];
+ self::$optionConstants = self::$optionConstants ?? self::getOptionConstants();
+
+ foreach (self::$optionConstants as $constantKey => $value) {
+ $defaultOptions[$constantKey] = $defaultMemcached->getOption($value);
+ }
+
+ return $defaultOptions;
+ }
+
+ private static function getOptionConstants(): array
+ {
+ $reflectedMemcached = new \ReflectionClass(\Memcached::class);
+
+ $optionConstants = [];
+ foreach ($reflectedMemcached->getConstants() as $constantKey => $value) {
+ if (0 === strpos($constantKey, 'OPT_')) {
+ $optionConstants[$constantKey] = $value;
+ }
+ }
+
+ return $optionConstants;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/PdoCaster.php b/vendor/symfony/var-dumper/Caster/PdoCaster.php
new file mode 100644
index 0000000..c315c28
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/PdoCaster.php
@@ -0,0 +1,122 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts PDO related classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class PdoCaster
+{
+ private static $pdoAttributes = [
+ 'CASE' => [
+ \PDO::CASE_LOWER => 'LOWER',
+ \PDO::CASE_NATURAL => 'NATURAL',
+ \PDO::CASE_UPPER => 'UPPER',
+ ],
+ 'ERRMODE' => [
+ \PDO::ERRMODE_SILENT => 'SILENT',
+ \PDO::ERRMODE_WARNING => 'WARNING',
+ \PDO::ERRMODE_EXCEPTION => 'EXCEPTION',
+ ],
+ 'TIMEOUT',
+ 'PREFETCH',
+ 'AUTOCOMMIT',
+ 'PERSISTENT',
+ 'DRIVER_NAME',
+ 'SERVER_INFO',
+ 'ORACLE_NULLS' => [
+ \PDO::NULL_NATURAL => 'NATURAL',
+ \PDO::NULL_EMPTY_STRING => 'EMPTY_STRING',
+ \PDO::NULL_TO_STRING => 'TO_STRING',
+ ],
+ 'CLIENT_VERSION',
+ 'SERVER_VERSION',
+ 'STATEMENT_CLASS',
+ 'EMULATE_PREPARES',
+ 'CONNECTION_STATUS',
+ 'STRINGIFY_FETCHES',
+ 'DEFAULT_FETCH_MODE' => [
+ \PDO::FETCH_ASSOC => 'ASSOC',
+ \PDO::FETCH_BOTH => 'BOTH',
+ \PDO::FETCH_LAZY => 'LAZY',
+ \PDO::FETCH_NUM => 'NUM',
+ \PDO::FETCH_OBJ => 'OBJ',
+ ],
+ ];
+
+ public static function castPdo(\PDO $c, array $a, Stub $stub, $isNested)
+ {
+ $attr = [];
+ $errmode = $c->getAttribute(\PDO::ATTR_ERRMODE);
+ $c->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+
+ foreach (self::$pdoAttributes as $k => $v) {
+ if (!isset($k[0])) {
+ $k = $v;
+ $v = [];
+ }
+
+ try {
+ $attr[$k] = 'ERRMODE' === $k ? $errmode : $c->getAttribute(\constant('PDO::ATTR_'.$k));
+ if ($v && isset($v[$attr[$k]])) {
+ $attr[$k] = new ConstStub($v[$attr[$k]], $attr[$k]);
+ }
+ } catch (\Exception $e) {
+ }
+ }
+ if (isset($attr[$k = 'STATEMENT_CLASS'][1])) {
+ if ($attr[$k][1]) {
+ $attr[$k][1] = new ArgsStub($attr[$k][1], '__construct', $attr[$k][0]);
+ }
+ $attr[$k][0] = new ClassStub($attr[$k][0]);
+ }
+
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $a += [
+ $prefix.'inTransaction' => method_exists($c, 'inTransaction'),
+ $prefix.'errorInfo' => $c->errorInfo(),
+ $prefix.'attributes' => new EnumStub($attr),
+ ];
+
+ if ($a[$prefix.'inTransaction']) {
+ $a[$prefix.'inTransaction'] = $c->inTransaction();
+ } else {
+ unset($a[$prefix.'inTransaction']);
+ }
+
+ if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) {
+ unset($a[$prefix.'errorInfo']);
+ }
+
+ $c->setAttribute(\PDO::ATTR_ERRMODE, $errmode);
+
+ return $a;
+ }
+
+ public static function castPdoStatement(\PDOStatement $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $a[$prefix.'errorInfo'] = $c->errorInfo();
+
+ if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) {
+ unset($a[$prefix.'errorInfo']);
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/PgSqlCaster.php b/vendor/symfony/var-dumper/Caster/PgSqlCaster.php
new file mode 100644
index 0000000..bd698a1
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/PgSqlCaster.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts pqsql resources to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class PgSqlCaster
+{
+ private static $paramCodes = [
+ 'server_encoding',
+ 'client_encoding',
+ 'is_superuser',
+ 'session_authorization',
+ 'DateStyle',
+ 'TimeZone',
+ 'IntervalStyle',
+ 'integer_datetimes',
+ 'application_name',
+ 'standard_conforming_strings',
+ ];
+
+ private static $transactionStatus = [
+ PGSQL_TRANSACTION_IDLE => 'PGSQL_TRANSACTION_IDLE',
+ PGSQL_TRANSACTION_ACTIVE => 'PGSQL_TRANSACTION_ACTIVE',
+ PGSQL_TRANSACTION_INTRANS => 'PGSQL_TRANSACTION_INTRANS',
+ PGSQL_TRANSACTION_INERROR => 'PGSQL_TRANSACTION_INERROR',
+ PGSQL_TRANSACTION_UNKNOWN => 'PGSQL_TRANSACTION_UNKNOWN',
+ ];
+
+ private static $resultStatus = [
+ PGSQL_EMPTY_QUERY => 'PGSQL_EMPTY_QUERY',
+ PGSQL_COMMAND_OK => 'PGSQL_COMMAND_OK',
+ PGSQL_TUPLES_OK => 'PGSQL_TUPLES_OK',
+ PGSQL_COPY_OUT => 'PGSQL_COPY_OUT',
+ PGSQL_COPY_IN => 'PGSQL_COPY_IN',
+ PGSQL_BAD_RESPONSE => 'PGSQL_BAD_RESPONSE',
+ PGSQL_NONFATAL_ERROR => 'PGSQL_NONFATAL_ERROR',
+ PGSQL_FATAL_ERROR => 'PGSQL_FATAL_ERROR',
+ ];
+
+ private static $diagCodes = [
+ 'severity' => PGSQL_DIAG_SEVERITY,
+ 'sqlstate' => PGSQL_DIAG_SQLSTATE,
+ 'message' => PGSQL_DIAG_MESSAGE_PRIMARY,
+ 'detail' => PGSQL_DIAG_MESSAGE_DETAIL,
+ 'hint' => PGSQL_DIAG_MESSAGE_HINT,
+ 'statement position' => PGSQL_DIAG_STATEMENT_POSITION,
+ 'internal position' => PGSQL_DIAG_INTERNAL_POSITION,
+ 'internal query' => PGSQL_DIAG_INTERNAL_QUERY,
+ 'context' => PGSQL_DIAG_CONTEXT,
+ 'file' => PGSQL_DIAG_SOURCE_FILE,
+ 'line' => PGSQL_DIAG_SOURCE_LINE,
+ 'function' => PGSQL_DIAG_SOURCE_FUNCTION,
+ ];
+
+ public static function castLargeObject($lo, array $a, Stub $stub, $isNested)
+ {
+ $a['seek position'] = pg_lo_tell($lo);
+
+ return $a;
+ }
+
+ public static function castLink($link, array $a, Stub $stub, $isNested)
+ {
+ $a['status'] = pg_connection_status($link);
+ $a['status'] = new ConstStub(PGSQL_CONNECTION_OK === $a['status'] ? 'PGSQL_CONNECTION_OK' : 'PGSQL_CONNECTION_BAD', $a['status']);
+ $a['busy'] = pg_connection_busy($link);
+
+ $a['transaction'] = pg_transaction_status($link);
+ if (isset(self::$transactionStatus[$a['transaction']])) {
+ $a['transaction'] = new ConstStub(self::$transactionStatus[$a['transaction']], $a['transaction']);
+ }
+
+ $a['pid'] = pg_get_pid($link);
+ $a['last error'] = pg_last_error($link);
+ $a['last notice'] = pg_last_notice($link);
+ $a['host'] = pg_host($link);
+ $a['port'] = pg_port($link);
+ $a['dbname'] = pg_dbname($link);
+ $a['options'] = pg_options($link);
+ $a['version'] = pg_version($link);
+
+ foreach (self::$paramCodes as $v) {
+ if (false !== $s = pg_parameter_status($link, $v)) {
+ $a['param'][$v] = $s;
+ }
+ }
+
+ $a['param']['client_encoding'] = pg_client_encoding($link);
+ $a['param'] = new EnumStub($a['param']);
+
+ return $a;
+ }
+
+ public static function castResult($result, array $a, Stub $stub, $isNested)
+ {
+ $a['num rows'] = pg_num_rows($result);
+ $a['status'] = pg_result_status($result);
+ if (isset(self::$resultStatus[$a['status']])) {
+ $a['status'] = new ConstStub(self::$resultStatus[$a['status']], $a['status']);
+ }
+ $a['command-completion tag'] = pg_result_status($result, PGSQL_STATUS_STRING);
+
+ if (-1 === $a['num rows']) {
+ foreach (self::$diagCodes as $k => $v) {
+ $a['error'][$k] = pg_result_error_field($result, $v);
+ }
+ }
+
+ $a['affected rows'] = pg_affected_rows($result);
+ $a['last OID'] = pg_last_oid($result);
+
+ $fields = pg_num_fields($result);
+
+ for ($i = 0; $i < $fields; ++$i) {
+ $field = [
+ 'name' => pg_field_name($result, $i),
+ 'table' => sprintf('%s (OID: %s)', pg_field_table($result, $i), pg_field_table($result, $i, true)),
+ 'type' => sprintf('%s (OID: %s)', pg_field_type($result, $i), pg_field_type_oid($result, $i)),
+ 'nullable' => (bool) pg_field_is_null($result, $i),
+ 'storage' => pg_field_size($result, $i).' bytes',
+ 'display' => pg_field_prtlen($result, $i).' chars',
+ ];
+ if (' (OID: )' === $field['table']) {
+ $field['table'] = null;
+ }
+ if ('-1 bytes' === $field['storage']) {
+ $field['storage'] = 'variable size';
+ } elseif ('1 bytes' === $field['storage']) {
+ $field['storage'] = '1 byte';
+ }
+ if ('1 chars' === $field['display']) {
+ $field['display'] = '1 char';
+ }
+ $a['fields'][] = new EnumStub($field);
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ProxyManagerCaster.php b/vendor/symfony/var-dumper/Caster/ProxyManagerCaster.php
new file mode 100644
index 0000000..927b2c9
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ProxyManagerCaster.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use ProxyManager\Proxy\ProxyInterface;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class ProxyManagerCaster
+{
+ public static function castProxy(ProxyInterface $c, array $a, Stub $stub, $isNested)
+ {
+ if ($parent = get_parent_class($c)) {
+ $stub->class .= ' - '.$parent;
+ }
+ $stub->class .= '@proxy';
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/RedisCaster.php b/vendor/symfony/var-dumper/Caster/RedisCaster.php
new file mode 100644
index 0000000..ebd73df
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/RedisCaster.php
@@ -0,0 +1,152 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Redis class from ext-redis to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class RedisCaster
+{
+ private static $serializer = [
+ \Redis::SERIALIZER_NONE => 'NONE',
+ \Redis::SERIALIZER_PHP => 'PHP',
+ 2 => 'IGBINARY', // Optional Redis::SERIALIZER_IGBINARY
+ ];
+
+ private static $mode = [
+ \Redis::ATOMIC => 'ATOMIC',
+ \Redis::MULTI => 'MULTI',
+ \Redis::PIPELINE => 'PIPELINE',
+ ];
+
+ private static $compression = [
+ 0 => 'NONE', // Redis::COMPRESSION_NONE
+ 1 => 'LZF', // Redis::COMPRESSION_LZF
+ ];
+
+ private static $failover = [
+ \RedisCluster::FAILOVER_NONE => 'NONE',
+ \RedisCluster::FAILOVER_ERROR => 'ERROR',
+ \RedisCluster::FAILOVER_DISTRIBUTE => 'DISTRIBUTE',
+ \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES => 'DISTRIBUTE_SLAVES',
+ ];
+
+ public static function castRedis(\Redis $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ if (!$connected = $c->isConnected()) {
+ return $a + [
+ $prefix.'isConnected' => $connected,
+ ];
+ }
+
+ $mode = $c->getMode();
+
+ return $a + [
+ $prefix.'isConnected' => $connected,
+ $prefix.'host' => $c->getHost(),
+ $prefix.'port' => $c->getPort(),
+ $prefix.'auth' => $c->getAuth(),
+ $prefix.'mode' => isset(self::$mode[$mode]) ? new ConstStub(self::$mode[$mode], $mode) : $mode,
+ $prefix.'dbNum' => $c->getDbNum(),
+ $prefix.'timeout' => $c->getTimeout(),
+ $prefix.'lastError' => $c->getLastError(),
+ $prefix.'persistentId' => $c->getPersistentID(),
+ $prefix.'options' => self::getRedisOptions($c),
+ ];
+ }
+
+ public static function castRedisArray(\RedisArray $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ return $a + [
+ $prefix.'hosts' => $c->_hosts(),
+ $prefix.'function' => ClassStub::wrapCallable($c->_function()),
+ $prefix.'lastError' => $c->getLastError(),
+ $prefix.'options' => self::getRedisOptions($c),
+ ];
+ }
+
+ public static function castRedisCluster(\RedisCluster $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $failover = $c->getOption(\RedisCluster::OPT_SLAVE_FAILOVER);
+
+ $a += [
+ $prefix.'_masters' => $c->_masters(),
+ $prefix.'_redir' => $c->_redir(),
+ $prefix.'mode' => new ConstStub($c->getMode() ? 'MULTI' : 'ATOMIC', $c->getMode()),
+ $prefix.'lastError' => $c->getLastError(),
+ $prefix.'options' => self::getRedisOptions($c, [
+ 'SLAVE_FAILOVER' => isset(self::$failover[$failover]) ? new ConstStub(self::$failover[$failover], $failover) : $failover,
+ ]),
+ ];
+
+ return $a;
+ }
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster $redis
+ */
+ private static function getRedisOptions($redis, array $options = []): EnumStub
+ {
+ $serializer = $redis->getOption(\Redis::OPT_SERIALIZER);
+ if (\is_array($serializer)) {
+ foreach ($serializer as &$v) {
+ if (isset(self::$serializer[$v])) {
+ $v = new ConstStub(self::$serializer[$v], $v);
+ }
+ }
+ } elseif (isset(self::$serializer[$serializer])) {
+ $serializer = new ConstStub(self::$serializer[$serializer], $serializer);
+ }
+
+ $compression = \defined('Redis::OPT_COMPRESSION') ? $redis->getOption(\Redis::OPT_COMPRESSION) : 0;
+ if (\is_array($compression)) {
+ foreach ($compression as &$v) {
+ if (isset(self::$compression[$v])) {
+ $v = new ConstStub(self::$compression[$v], $v);
+ }
+ }
+ } elseif (isset(self::$compression[$compression])) {
+ $compression = new ConstStub(self::$compression[$compression], $compression);
+ }
+
+ $retry = \defined('Redis::OPT_SCAN') ? $redis->getOption(\Redis::OPT_SCAN) : 0;
+ if (\is_array($retry)) {
+ foreach ($retry as &$v) {
+ $v = new ConstStub($v ? 'RETRY' : 'NORETRY', $v);
+ }
+ } else {
+ $retry = new ConstStub($retry ? 'RETRY' : 'NORETRY', $retry);
+ }
+
+ $options += [
+ 'TCP_KEEPALIVE' => \defined('Redis::OPT_TCP_KEEPALIVE') ? $redis->getOption(\Redis::OPT_TCP_KEEPALIVE) : 0,
+ 'READ_TIMEOUT' => $redis->getOption(\Redis::OPT_READ_TIMEOUT),
+ 'COMPRESSION' => $compression,
+ 'SERIALIZER' => $serializer,
+ 'PREFIX' => $redis->getOption(\Redis::OPT_PREFIX),
+ 'SCAN' => $retry,
+ ];
+
+ return new EnumStub($options);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ReflectionCaster.php b/vendor/symfony/var-dumper/Caster/ReflectionCaster.php
new file mode 100644
index 0000000..0fab38c
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ReflectionCaster.php
@@ -0,0 +1,392 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts Reflector related classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class ReflectionCaster
+{
+ const UNSET_CLOSURE_FILE_INFO = ['Closure' => __CLASS__.'::unsetClosureFileInfo'];
+
+ private static $extraMap = [
+ 'docComment' => 'getDocComment',
+ 'extension' => 'getExtensionName',
+ 'isDisabled' => 'isDisabled',
+ 'isDeprecated' => 'isDeprecated',
+ 'isInternal' => 'isInternal',
+ 'isUserDefined' => 'isUserDefined',
+ 'isGenerator' => 'isGenerator',
+ 'isVariadic' => 'isVariadic',
+ ];
+
+ public static function castClosure(\Closure $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $c = new \ReflectionFunction($c);
+
+ $a = static::castFunctionAbstract($c, $a, $stub, $isNested, $filter);
+
+ if (false === strpos($c->name, '{closure}')) {
+ $stub->class = isset($a[$prefix.'class']) ? $a[$prefix.'class']->value.'::'.$c->name : $c->name;
+ unset($a[$prefix.'class']);
+ }
+ unset($a[$prefix.'extra']);
+
+ $stub->class .= self::getSignature($a);
+
+ if ($f = $c->getFileName()) {
+ $stub->attr['file'] = $f;
+ $stub->attr['line'] = $c->getStartLine();
+ }
+
+ unset($a[$prefix.'parameters']);
+
+ if ($filter & Caster::EXCLUDE_VERBOSE) {
+ $stub->cut += ($c->getFileName() ? 2 : 0) + \count($a);
+
+ return [];
+ }
+
+ if ($f) {
+ $a[$prefix.'file'] = new LinkStub($f, $c->getStartLine());
+ $a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine();
+ }
+
+ return $a;
+ }
+
+ public static function unsetClosureFileInfo(\Closure $c, array $a)
+ {
+ unset($a[Caster::PREFIX_VIRTUAL.'file'], $a[Caster::PREFIX_VIRTUAL.'line']);
+
+ return $a;
+ }
+
+ public static function castGenerator(\Generator $c, array $a, Stub $stub, $isNested)
+ {
+ // Cannot create ReflectionGenerator based on a terminated Generator
+ try {
+ $reflectionGenerator = new \ReflectionGenerator($c);
+ } catch (\Exception $e) {
+ $a[Caster::PREFIX_VIRTUAL.'closed'] = true;
+
+ return $a;
+ }
+
+ return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested);
+ }
+
+ public static function castType(\ReflectionType $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ $a += [
+ $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c,
+ $prefix.'allowsNull' => $c->allowsNull(),
+ $prefix.'isBuiltin' => $c->isBuiltin(),
+ ];
+
+ return $a;
+ }
+
+ public static function castReflectionGenerator(\ReflectionGenerator $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ if ($c->getThis()) {
+ $a[$prefix.'this'] = new CutStub($c->getThis());
+ }
+ $function = $c->getFunction();
+ $frame = [
+ 'class' => isset($function->class) ? $function->class : null,
+ 'type' => isset($function->class) ? ($function->isStatic() ? '::' : '->') : null,
+ 'function' => $function->name,
+ 'file' => $c->getExecutingFile(),
+ 'line' => $c->getExecutingLine(),
+ ];
+ if ($trace = $c->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS)) {
+ $function = new \ReflectionGenerator($c->getExecutingGenerator());
+ array_unshift($trace, [
+ 'function' => 'yield',
+ 'file' => $function->getExecutingFile(),
+ 'line' => $function->getExecutingLine() - 1,
+ ]);
+ $trace[] = $frame;
+ $a[$prefix.'trace'] = new TraceStub($trace, false, 0, -1, -1);
+ } else {
+ $function = new FrameStub($frame, false, true);
+ $function = ExceptionCaster::castFrameStub($function, [], $function, true);
+ $a[$prefix.'executing'] = $function[$prefix.'src'];
+ }
+
+ $a[Caster::PREFIX_VIRTUAL.'closed'] = false;
+
+ return $a;
+ }
+
+ public static function castClass(\ReflectionClass $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ if ($n = \Reflection::getModifierNames($c->getModifiers())) {
+ $a[$prefix.'modifiers'] = implode(' ', $n);
+ }
+
+ self::addMap($a, $c, [
+ 'extends' => 'getParentClass',
+ 'implements' => 'getInterfaceNames',
+ 'constants' => 'getConstants',
+ ]);
+
+ foreach ($c->getProperties() as $n) {
+ $a[$prefix.'properties'][$n->name] = $n;
+ }
+
+ foreach ($c->getMethods() as $n) {
+ $a[$prefix.'methods'][$n->name] = $n;
+ }
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) {
+ self::addExtra($a, $c);
+ }
+
+ return $a;
+ }
+
+ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, array $a, Stub $stub, $isNested, $filter = 0)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ self::addMap($a, $c, [
+ 'returnsReference' => 'returnsReference',
+ 'returnType' => 'getReturnType',
+ 'class' => 'getClosureScopeClass',
+ 'this' => 'getClosureThis',
+ ]);
+
+ if (isset($a[$prefix.'returnType'])) {
+ $v = $a[$prefix.'returnType'];
+ $v = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v;
+ $a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType']->allowsNull() ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']);
+ }
+ if (isset($a[$prefix.'class'])) {
+ $a[$prefix.'class'] = new ClassStub($a[$prefix.'class']);
+ }
+ if (isset($a[$prefix.'this'])) {
+ $a[$prefix.'this'] = new CutStub($a[$prefix.'this']);
+ }
+
+ foreach ($c->getParameters() as $v) {
+ $k = '$'.$v->name;
+ if ($v->isVariadic()) {
+ $k = '...'.$k;
+ }
+ if ($v->isPassedByReference()) {
+ $k = '&'.$k;
+ }
+ $a[$prefix.'parameters'][$k] = $v;
+ }
+ if (isset($a[$prefix.'parameters'])) {
+ $a[$prefix.'parameters'] = new EnumStub($a[$prefix.'parameters']);
+ }
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE) && $v = $c->getStaticVariables()) {
+ foreach ($v as $k => &$v) {
+ if (\is_object($v)) {
+ $a[$prefix.'use']['$'.$k] = new CutStub($v);
+ } else {
+ $a[$prefix.'use']['$'.$k] = &$v;
+ }
+ }
+ unset($v);
+ $a[$prefix.'use'] = new EnumStub($a[$prefix.'use']);
+ }
+
+ if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) {
+ self::addExtra($a, $c);
+ }
+
+ return $a;
+ }
+
+ public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, $isNested)
+ {
+ $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers()));
+
+ return $a;
+ }
+
+ public static function castParameter(\ReflectionParameter $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ self::addMap($a, $c, [
+ 'position' => 'getPosition',
+ 'isVariadic' => 'isVariadic',
+ 'byReference' => 'isPassedByReference',
+ 'allowsNull' => 'allowsNull',
+ ]);
+
+ if ($v = $c->getType()) {
+ $a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v;
+ }
+
+ if (isset($a[$prefix.'typeHint'])) {
+ $v = $a[$prefix.'typeHint'];
+ $a[$prefix.'typeHint'] = new ClassStub($v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']);
+ } else {
+ unset($a[$prefix.'allowsNull']);
+ }
+
+ try {
+ $a[$prefix.'default'] = $v = $c->getDefaultValue();
+ if ($c->isDefaultValueConstant()) {
+ $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v);
+ }
+ if (null === $v) {
+ unset($a[$prefix.'allowsNull']);
+ }
+ } catch (\ReflectionException $e) {
+ }
+
+ return $a;
+ }
+
+ public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, $isNested)
+ {
+ $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers()));
+ self::addExtra($a, $c);
+
+ return $a;
+ }
+
+ public static function castReference(\ReflectionReference $c, array $a, Stub $stub, $isNested)
+ {
+ $a[Caster::PREFIX_VIRTUAL.'id'] = $c->getId();
+
+ return $a;
+ }
+
+ public static function castExtension(\ReflectionExtension $c, array $a, Stub $stub, $isNested)
+ {
+ self::addMap($a, $c, [
+ 'version' => 'getVersion',
+ 'dependencies' => 'getDependencies',
+ 'iniEntries' => 'getIniEntries',
+ 'isPersistent' => 'isPersistent',
+ 'isTemporary' => 'isTemporary',
+ 'constants' => 'getConstants',
+ 'functions' => 'getFunctions',
+ 'classes' => 'getClasses',
+ ]);
+
+ return $a;
+ }
+
+ public static function castZendExtension(\ReflectionZendExtension $c, array $a, Stub $stub, $isNested)
+ {
+ self::addMap($a, $c, [
+ 'version' => 'getVersion',
+ 'author' => 'getAuthor',
+ 'copyright' => 'getCopyright',
+ 'url' => 'getURL',
+ ]);
+
+ return $a;
+ }
+
+ public static function getSignature(array $a)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $signature = '';
+
+ if (isset($a[$prefix.'parameters'])) {
+ foreach ($a[$prefix.'parameters']->value as $k => $param) {
+ $signature .= ', ';
+ if ($type = $param->getType()) {
+ if (!$type instanceof \ReflectionNamedType) {
+ $signature .= $type.' ';
+ } else {
+ if (!$param->isOptional() && $param->allowsNull()) {
+ $signature .= '?';
+ }
+ $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' ';
+ }
+ }
+ $signature .= $k;
+
+ if (!$param->isDefaultValueAvailable()) {
+ continue;
+ }
+ $v = $param->getDefaultValue();
+ $signature .= ' = ';
+
+ if ($param->isDefaultValueConstant()) {
+ $signature .= substr(strrchr('\\'.$param->getDefaultValueConstantName(), '\\'), 1);
+ } elseif (null === $v) {
+ $signature .= 'null';
+ } elseif (\is_array($v)) {
+ $signature .= $v ? '[…'.\count($v).']' : '[]';
+ } elseif (\is_string($v)) {
+ $signature .= 10 > \strlen($v) && false === strpos($v, '\\') ? "'{$v}'" : "'…".\strlen($v)."'";
+ } elseif (\is_bool($v)) {
+ $signature .= $v ? 'true' : 'false';
+ } else {
+ $signature .= $v;
+ }
+ }
+ }
+ $signature = (empty($a[$prefix.'returnsReference']) ? '' : '&').'('.substr($signature, 2).')';
+
+ if (isset($a[$prefix.'returnType'])) {
+ $signature .= ': '.substr(strrchr('\\'.$a[$prefix.'returnType'], '\\'), 1);
+ }
+
+ return $signature;
+ }
+
+ private static function addExtra(array &$a, \Reflector $c)
+ {
+ $x = isset($a[Caster::PREFIX_VIRTUAL.'extra']) ? $a[Caster::PREFIX_VIRTUAL.'extra']->value : [];
+
+ if (method_exists($c, 'getFileName') && $m = $c->getFileName()) {
+ $x['file'] = new LinkStub($m, $c->getStartLine());
+ $x['line'] = $c->getStartLine().' to '.$c->getEndLine();
+ }
+
+ self::addMap($x, $c, self::$extraMap, '');
+
+ if ($x) {
+ $a[Caster::PREFIX_VIRTUAL.'extra'] = new EnumStub($x);
+ }
+ }
+
+ private static function addMap(array &$a, \Reflector $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL)
+ {
+ foreach ($map as $k => $m) {
+ if (\PHP_VERSION_ID >= 80000 && 'isDisabled' === $k) {
+ continue;
+ }
+
+ if (method_exists($c, $m) && false !== ($m = $c->$m()) && null !== $m) {
+ $a[$prefix.$k] = $m instanceof \Reflector ? $m->name : $m;
+ }
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/ResourceCaster.php b/vendor/symfony/var-dumper/Caster/ResourceCaster.php
new file mode 100644
index 0000000..aca8adf
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/ResourceCaster.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts common resource types to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class ResourceCaster
+{
+ /**
+ * @param \CurlHandle|resource $h
+ *
+ * @return array
+ */
+ public static function castCurl($h, array $a, Stub $stub, $isNested)
+ {
+ return curl_getinfo($h);
+ }
+
+ public static function castDba($dba, array $a, Stub $stub, $isNested)
+ {
+ $list = dba_list();
+ $a['file'] = $list[(int) $dba];
+
+ return $a;
+ }
+
+ public static function castProcess($process, array $a, Stub $stub, $isNested)
+ {
+ return proc_get_status($process);
+ }
+
+ public static function castStream($stream, array $a, Stub $stub, $isNested)
+ {
+ $a = stream_get_meta_data($stream) + static::castStreamContext($stream, $a, $stub, $isNested);
+ if (isset($a['uri'])) {
+ $a['uri'] = new LinkStub($a['uri']);
+ }
+
+ return $a;
+ }
+
+ public static function castStreamContext($stream, array $a, Stub $stub, $isNested)
+ {
+ return @stream_context_get_params($stream) ?: $a;
+ }
+
+ public static function castGd($gd, array $a, Stub $stub, $isNested)
+ {
+ $a['size'] = imagesx($gd).'x'.imagesy($gd);
+ $a['trueColor'] = imageistruecolor($gd);
+
+ return $a;
+ }
+
+ public static function castMysqlLink($h, array $a, Stub $stub, $isNested)
+ {
+ $a['host'] = mysql_get_host_info($h);
+ $a['protocol'] = mysql_get_proto_info($h);
+ $a['server'] = mysql_get_server_info($h);
+
+ return $a;
+ }
+
+ public static function castOpensslX509($h, array $a, Stub $stub, $isNested)
+ {
+ $stub->cut = -1;
+ $info = openssl_x509_parse($h, false);
+
+ $pin = openssl_pkey_get_public($h);
+ $pin = openssl_pkey_get_details($pin)['key'];
+ $pin = \array_slice(explode("\n", $pin), 1, -2);
+ $pin = base64_decode(implode('', $pin));
+ $pin = base64_encode(hash('sha256', $pin, true));
+
+ $a += [
+ 'subject' => new EnumStub(array_intersect_key($info['subject'], ['organizationName' => true, 'commonName' => true])),
+ 'issuer' => new EnumStub(array_intersect_key($info['issuer'], ['organizationName' => true, 'commonName' => true])),
+ 'expiry' => new ConstStub(date(\DateTime::ISO8601, $info['validTo_time_t']), $info['validTo_time_t']),
+ 'fingerprint' => new EnumStub([
+ 'md5' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'md5')), 2, ':', true)),
+ 'sha1' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'sha1')), 2, ':', true)),
+ 'sha256' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'sha256')), 2, ':', true)),
+ 'pin-sha256' => new ConstStub($pin),
+ ]),
+ ];
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/SplCaster.php b/vendor/symfony/var-dumper/Caster/SplCaster.php
new file mode 100644
index 0000000..e23292d
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/SplCaster.php
@@ -0,0 +1,223 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts SPL related classes to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class SplCaster
+{
+ private static $splFileObjectFlags = [
+ \SplFileObject::DROP_NEW_LINE => 'DROP_NEW_LINE',
+ \SplFileObject::READ_AHEAD => 'READ_AHEAD',
+ \SplFileObject::SKIP_EMPTY => 'SKIP_EMPTY',
+ \SplFileObject::READ_CSV => 'READ_CSV',
+ ];
+
+ public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, $isNested)
+ {
+ return self::castSplArray($c, $a, $stub, $isNested);
+ }
+
+ public static function castArrayIterator(\ArrayIterator $c, array $a, Stub $stub, $isNested)
+ {
+ return self::castSplArray($c, $a, $stub, $isNested);
+ }
+
+ public static function castHeap(\Iterator $c, array $a, Stub $stub, $isNested)
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'heap' => iterator_to_array(clone $c),
+ ];
+
+ return $a;
+ }
+
+ public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, Stub $stub, $isNested)
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $mode = $c->getIteratorMode();
+ $c->setIteratorMode(\SplDoublyLinkedList::IT_MODE_KEEP | $mode & ~\SplDoublyLinkedList::IT_MODE_DELETE);
+
+ $a += [
+ $prefix.'mode' => new ConstStub((($mode & \SplDoublyLinkedList::IT_MODE_LIFO) ? 'IT_MODE_LIFO' : 'IT_MODE_FIFO').' | '.(($mode & \SplDoublyLinkedList::IT_MODE_DELETE) ? 'IT_MODE_DELETE' : 'IT_MODE_KEEP'), $mode),
+ $prefix.'dllist' => iterator_to_array($c),
+ ];
+ $c->setIteratorMode($mode);
+
+ return $a;
+ }
+
+ public static function castFileInfo(\SplFileInfo $c, array $a, Stub $stub, $isNested)
+ {
+ static $map = [
+ 'path' => 'getPath',
+ 'filename' => 'getFilename',
+ 'basename' => 'getBasename',
+ 'pathname' => 'getPathname',
+ 'extension' => 'getExtension',
+ 'realPath' => 'getRealPath',
+ 'aTime' => 'getATime',
+ 'mTime' => 'getMTime',
+ 'cTime' => 'getCTime',
+ 'inode' => 'getInode',
+ 'size' => 'getSize',
+ 'perms' => 'getPerms',
+ 'owner' => 'getOwner',
+ 'group' => 'getGroup',
+ 'type' => 'getType',
+ 'writable' => 'isWritable',
+ 'readable' => 'isReadable',
+ 'executable' => 'isExecutable',
+ 'file' => 'isFile',
+ 'dir' => 'isDir',
+ 'link' => 'isLink',
+ 'linkTarget' => 'getLinkTarget',
+ ];
+
+ $prefix = Caster::PREFIX_VIRTUAL;
+ unset($a["\0SplFileInfo\0fileName"]);
+ unset($a["\0SplFileInfo\0pathName"]);
+
+ if (false === $c->getPathname()) {
+ $a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state';
+
+ return $a;
+ }
+
+ foreach ($map as $key => $accessor) {
+ try {
+ $a[$prefix.$key] = $c->$accessor();
+ } catch (\Exception $e) {
+ }
+ }
+
+ if (isset($a[$prefix.'realPath'])) {
+ $a[$prefix.'realPath'] = new LinkStub($a[$prefix.'realPath']);
+ }
+
+ if (isset($a[$prefix.'perms'])) {
+ $a[$prefix.'perms'] = new ConstStub(sprintf('0%o', $a[$prefix.'perms']), $a[$prefix.'perms']);
+ }
+
+ static $mapDate = ['aTime', 'mTime', 'cTime'];
+ foreach ($mapDate as $key) {
+ if (isset($a[$prefix.$key])) {
+ $a[$prefix.$key] = new ConstStub(date('Y-m-d H:i:s', $a[$prefix.$key]), $a[$prefix.$key]);
+ }
+ }
+
+ return $a;
+ }
+
+ public static function castFileObject(\SplFileObject $c, array $a, Stub $stub, $isNested)
+ {
+ static $map = [
+ 'csvControl' => 'getCsvControl',
+ 'flags' => 'getFlags',
+ 'maxLineLen' => 'getMaxLineLen',
+ 'fstat' => 'fstat',
+ 'eof' => 'eof',
+ 'key' => 'key',
+ ];
+
+ $prefix = Caster::PREFIX_VIRTUAL;
+
+ foreach ($map as $key => $accessor) {
+ try {
+ $a[$prefix.$key] = $c->$accessor();
+ } catch (\Exception $e) {
+ }
+ }
+
+ if (isset($a[$prefix.'flags'])) {
+ $flagsArray = [];
+ foreach (self::$splFileObjectFlags as $value => $name) {
+ if ($a[$prefix.'flags'] & $value) {
+ $flagsArray[] = $name;
+ }
+ }
+ $a[$prefix.'flags'] = new ConstStub(implode('|', $flagsArray), $a[$prefix.'flags']);
+ }
+
+ if (isset($a[$prefix.'fstat'])) {
+ $a[$prefix.'fstat'] = new CutArrayStub($a[$prefix.'fstat'], ['dev', 'ino', 'nlink', 'rdev', 'blksize', 'blocks']);
+ }
+
+ return $a;
+ }
+
+ public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $stub, $isNested)
+ {
+ $storage = [];
+ unset($a[Caster::PREFIX_DYNAMIC."\0gcdata"]); // Don't hit https://bugs.php.net/65967
+ unset($a["\0SplObjectStorage\0storage"]);
+
+ $clone = clone $c;
+ foreach ($clone as $obj) {
+ $storage[] = [
+ 'object' => $obj,
+ 'info' => $clone->getInfo(),
+ ];
+ }
+
+ $a += [
+ Caster::PREFIX_VIRTUAL.'storage' => $storage,
+ ];
+
+ return $a;
+ }
+
+ public static function castOuterIterator(\OuterIterator $c, array $a, Stub $stub, $isNested)
+ {
+ $a[Caster::PREFIX_VIRTUAL.'innerIterator'] = $c->getInnerIterator();
+
+ return $a;
+ }
+
+ public static function castWeakReference(\WeakReference $c, array $a, Stub $stub, $isNested)
+ {
+ $a[Caster::PREFIX_VIRTUAL.'object'] = $c->get();
+
+ return $a;
+ }
+
+ private static function castSplArray($c, array $a, Stub $stub, bool $isNested): array
+ {
+ $prefix = Caster::PREFIX_VIRTUAL;
+ $flags = $c->getFlags();
+
+ if (!($flags & \ArrayObject::STD_PROP_LIST)) {
+ $c->setFlags(\ArrayObject::STD_PROP_LIST);
+ $a = Caster::castObject($c, \get_class($c), method_exists($c, '__debugInfo'), $stub->class);
+ $c->setFlags($flags);
+ }
+ if (\PHP_VERSION_ID < 70400) {
+ $a[$prefix.'storage'] = $c->getArrayCopy();
+ }
+ $a += [
+ $prefix.'flag::STD_PROP_LIST' => (bool) ($flags & \ArrayObject::STD_PROP_LIST),
+ $prefix.'flag::ARRAY_AS_PROPS' => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS),
+ ];
+ if ($c instanceof \ArrayObject) {
+ $a[$prefix.'iteratorClass'] = new ClassStub($c->getIteratorClass());
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/StubCaster.php b/vendor/symfony/var-dumper/Caster/StubCaster.php
new file mode 100644
index 0000000..3c54afc
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/StubCaster.php
@@ -0,0 +1,84 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts a caster's Stub.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class StubCaster
+{
+ public static function castStub(Stub $c, array $a, Stub $stub, $isNested)
+ {
+ if ($isNested) {
+ $stub->type = $c->type;
+ $stub->class = $c->class;
+ $stub->value = $c->value;
+ $stub->handle = $c->handle;
+ $stub->cut = $c->cut;
+ $stub->attr = $c->attr;
+
+ if (Stub::TYPE_REF === $c->type && !$c->class && \is_string($c->value) && !preg_match('//u', $c->value)) {
+ $stub->type = Stub::TYPE_STRING;
+ $stub->class = Stub::STRING_BINARY;
+ }
+
+ $a = [];
+ }
+
+ return $a;
+ }
+
+ public static function castCutArray(CutArrayStub $c, array $a, Stub $stub, $isNested)
+ {
+ return $isNested ? $c->preservedSubset : $a;
+ }
+
+ public static function cutInternals($obj, array $a, Stub $stub, $isNested)
+ {
+ if ($isNested) {
+ $stub->cut += \count($a);
+
+ return [];
+ }
+
+ return $a;
+ }
+
+ public static function castEnum(EnumStub $c, array $a, Stub $stub, $isNested)
+ {
+ if ($isNested) {
+ $stub->class = $c->dumpKeys ? '' : null;
+ $stub->handle = 0;
+ $stub->value = null;
+ $stub->cut = $c->cut;
+ $stub->attr = $c->attr;
+
+ $a = [];
+
+ if ($c->value) {
+ foreach (array_keys($c->value) as $k) {
+ $keys[] = !isset($k[0]) || "\0" !== $k[0] ? Caster::PREFIX_VIRTUAL.$k : $k;
+ }
+ // Preserve references with array_combine()
+ $a = array_combine($keys, $c->value);
+ }
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/SymfonyCaster.php b/vendor/symfony/var-dumper/Caster/SymfonyCaster.php
new file mode 100644
index 0000000..78f26f1
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/SymfonyCaster.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @final since Symfony 4.4
+ */
+class SymfonyCaster
+{
+ private static $requestGetters = [
+ 'pathInfo' => 'getPathInfo',
+ 'requestUri' => 'getRequestUri',
+ 'baseUrl' => 'getBaseUrl',
+ 'basePath' => 'getBasePath',
+ 'method' => 'getMethod',
+ 'format' => 'getRequestFormat',
+ ];
+
+ public static function castRequest(Request $request, array $a, Stub $stub, $isNested)
+ {
+ $clone = null;
+
+ foreach (self::$requestGetters as $prop => $getter) {
+ $key = Caster::PREFIX_PROTECTED.$prop;
+ if (\array_key_exists($key, $a) && null === $a[$key]) {
+ if (null === $clone) {
+ $clone = clone $request;
+ }
+ $a[Caster::PREFIX_VIRTUAL.$prop] = $clone->{$getter}();
+ }
+ }
+
+ return $a;
+ }
+
+ public static function castHttpClient($client, array $a, Stub $stub, $isNested)
+ {
+ $multiKey = sprintf("\0%s\0multi", \get_class($client));
+ if (isset($a[$multiKey])) {
+ $a[$multiKey] = new CutStub($a[$multiKey]);
+ }
+
+ return $a;
+ }
+
+ public static function castHttpClientResponse($response, array $a, Stub $stub, $isNested)
+ {
+ $stub->cut += \count($a);
+ $a = [];
+
+ foreach ($response->getInfo() as $k => $v) {
+ $a[Caster::PREFIX_VIRTUAL.$k] = $v;
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/TraceStub.php b/vendor/symfony/var-dumper/Caster/TraceStub.php
new file mode 100644
index 0000000..e8d136e
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/TraceStub.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Represents a backtrace as returned by debug_backtrace() or Exception->getTrace().
+ *
+ * @author Nicolas Grekas
+ */
+class TraceStub extends Stub
+{
+ public $keepArgs;
+ public $sliceOffset;
+ public $sliceLength;
+ public $numberingOffset;
+
+ public function __construct(array $trace, bool $keepArgs = true, int $sliceOffset = 0, int $sliceLength = null, int $numberingOffset = 0)
+ {
+ $this->value = $trace;
+ $this->keepArgs = $keepArgs;
+ $this->sliceOffset = $sliceOffset;
+ $this->sliceLength = $sliceLength;
+ $this->numberingOffset = $numberingOffset;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/UuidCaster.php b/vendor/symfony/var-dumper/Caster/UuidCaster.php
new file mode 100644
index 0000000..46aff0f
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/UuidCaster.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Ramsey\Uuid\UuidInterface;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * @author Grégoire Pineau
+ */
+final class UuidCaster
+{
+ public static function castRamseyUuid(UuidInterface $c, array $a, Stub $stub, bool $isNested): array
+ {
+ $a += [
+ Caster::PREFIX_VIRTUAL.'uuid' => (string) $c,
+ ];
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/XmlReaderCaster.php b/vendor/symfony/var-dumper/Caster/XmlReaderCaster.php
new file mode 100644
index 0000000..5169034
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/XmlReaderCaster.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts XmlReader class to array representation.
+ *
+ * @author Baptiste Clavié
+ *
+ * @final since Symfony 4.4
+ */
+class XmlReaderCaster
+{
+ private static $nodeTypes = [
+ \XMLReader::NONE => 'NONE',
+ \XMLReader::ELEMENT => 'ELEMENT',
+ \XMLReader::ATTRIBUTE => 'ATTRIBUTE',
+ \XMLReader::TEXT => 'TEXT',
+ \XMLReader::CDATA => 'CDATA',
+ \XMLReader::ENTITY_REF => 'ENTITY_REF',
+ \XMLReader::ENTITY => 'ENTITY',
+ \XMLReader::PI => 'PI (Processing Instruction)',
+ \XMLReader::COMMENT => 'COMMENT',
+ \XMLReader::DOC => 'DOC',
+ \XMLReader::DOC_TYPE => 'DOC_TYPE',
+ \XMLReader::DOC_FRAGMENT => 'DOC_FRAGMENT',
+ \XMLReader::NOTATION => 'NOTATION',
+ \XMLReader::WHITESPACE => 'WHITESPACE',
+ \XMLReader::SIGNIFICANT_WHITESPACE => 'SIGNIFICANT_WHITESPACE',
+ \XMLReader::END_ELEMENT => 'END_ELEMENT',
+ \XMLReader::END_ENTITY => 'END_ENTITY',
+ \XMLReader::XML_DECLARATION => 'XML_DECLARATION',
+ ];
+
+ public static function castXmlReader(\XMLReader $reader, array $a, Stub $stub, $isNested)
+ {
+ $props = Caster::PREFIX_VIRTUAL.'parserProperties';
+ $info = [
+ 'localName' => $reader->localName,
+ 'prefix' => $reader->prefix,
+ 'nodeType' => new ConstStub(self::$nodeTypes[$reader->nodeType], $reader->nodeType),
+ 'depth' => $reader->depth,
+ 'isDefault' => $reader->isDefault,
+ 'isEmptyElement' => \XMLReader::NONE === $reader->nodeType ? null : $reader->isEmptyElement,
+ 'xmlLang' => $reader->xmlLang,
+ 'attributeCount' => $reader->attributeCount,
+ 'value' => $reader->value,
+ 'namespaceURI' => $reader->namespaceURI,
+ 'baseURI' => $reader->baseURI ? new LinkStub($reader->baseURI) : $reader->baseURI,
+ $props => [
+ 'LOADDTD' => $reader->getParserProperty(\XMLReader::LOADDTD),
+ 'DEFAULTATTRS' => $reader->getParserProperty(\XMLReader::DEFAULTATTRS),
+ 'VALIDATE' => $reader->getParserProperty(\XMLReader::VALIDATE),
+ 'SUBST_ENTITIES' => $reader->getParserProperty(\XMLReader::SUBST_ENTITIES),
+ ],
+ ];
+
+ if ($info[$props] = Caster::filter($info[$props], Caster::EXCLUDE_EMPTY, [], $count)) {
+ $info[$props] = new EnumStub($info[$props]);
+ $info[$props]->cut = $count;
+ }
+
+ $info = Caster::filter($info, Caster::EXCLUDE_EMPTY, [], $count);
+ // +2 because hasValue and hasAttributes are always filtered
+ $stub->cut += $count + 2;
+
+ return $a + $info;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Caster/XmlResourceCaster.php b/vendor/symfony/var-dumper/Caster/XmlResourceCaster.php
new file mode 100644
index 0000000..a04f986
--- /dev/null
+++ b/vendor/symfony/var-dumper/Caster/XmlResourceCaster.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Caster;
+
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * Casts XML resources to array representation.
+ *
+ * @author Nicolas Grekas
+ *
+ * @final since Symfony 4.4
+ */
+class XmlResourceCaster
+{
+ private static $xmlErrors = [
+ XML_ERROR_NONE => 'XML_ERROR_NONE',
+ XML_ERROR_NO_MEMORY => 'XML_ERROR_NO_MEMORY',
+ XML_ERROR_SYNTAX => 'XML_ERROR_SYNTAX',
+ XML_ERROR_NO_ELEMENTS => 'XML_ERROR_NO_ELEMENTS',
+ XML_ERROR_INVALID_TOKEN => 'XML_ERROR_INVALID_TOKEN',
+ XML_ERROR_UNCLOSED_TOKEN => 'XML_ERROR_UNCLOSED_TOKEN',
+ XML_ERROR_PARTIAL_CHAR => 'XML_ERROR_PARTIAL_CHAR',
+ XML_ERROR_TAG_MISMATCH => 'XML_ERROR_TAG_MISMATCH',
+ XML_ERROR_DUPLICATE_ATTRIBUTE => 'XML_ERROR_DUPLICATE_ATTRIBUTE',
+ XML_ERROR_JUNK_AFTER_DOC_ELEMENT => 'XML_ERROR_JUNK_AFTER_DOC_ELEMENT',
+ XML_ERROR_PARAM_ENTITY_REF => 'XML_ERROR_PARAM_ENTITY_REF',
+ XML_ERROR_UNDEFINED_ENTITY => 'XML_ERROR_UNDEFINED_ENTITY',
+ XML_ERROR_RECURSIVE_ENTITY_REF => 'XML_ERROR_RECURSIVE_ENTITY_REF',
+ XML_ERROR_ASYNC_ENTITY => 'XML_ERROR_ASYNC_ENTITY',
+ XML_ERROR_BAD_CHAR_REF => 'XML_ERROR_BAD_CHAR_REF',
+ XML_ERROR_BINARY_ENTITY_REF => 'XML_ERROR_BINARY_ENTITY_REF',
+ XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF => 'XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF',
+ XML_ERROR_MISPLACED_XML_PI => 'XML_ERROR_MISPLACED_XML_PI',
+ XML_ERROR_UNKNOWN_ENCODING => 'XML_ERROR_UNKNOWN_ENCODING',
+ XML_ERROR_INCORRECT_ENCODING => 'XML_ERROR_INCORRECT_ENCODING',
+ XML_ERROR_UNCLOSED_CDATA_SECTION => 'XML_ERROR_UNCLOSED_CDATA_SECTION',
+ XML_ERROR_EXTERNAL_ENTITY_HANDLING => 'XML_ERROR_EXTERNAL_ENTITY_HANDLING',
+ ];
+
+ public static function castXml($h, array $a, Stub $stub, $isNested)
+ {
+ $a['current_byte_index'] = xml_get_current_byte_index($h);
+ $a['current_column_number'] = xml_get_current_column_number($h);
+ $a['current_line_number'] = xml_get_current_line_number($h);
+ $a['error_code'] = xml_get_error_code($h);
+
+ if (isset(self::$xmlErrors[$a['error_code']])) {
+ $a['error_code'] = new ConstStub(self::$xmlErrors[$a['error_code']], $a['error_code']);
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Cloner/AbstractCloner.php b/vendor/symfony/var-dumper/Cloner/AbstractCloner.php
new file mode 100644
index 0000000..936296d
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/AbstractCloner.php
@@ -0,0 +1,367 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+use Symfony\Component\VarDumper\Caster\Caster;
+use Symfony\Component\VarDumper\Exception\ThrowingCasterException;
+
+/**
+ * AbstractCloner implements a generic caster mechanism for objects and resources.
+ *
+ * @author Nicolas Grekas
+ */
+abstract class AbstractCloner implements ClonerInterface
+{
+ public static $defaultCasters = [
+ '__PHP_Incomplete_Class' => ['Symfony\Component\VarDumper\Caster\Caster', 'castPhpIncompleteClass'],
+
+ 'Symfony\Component\VarDumper\Caster\CutStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castStub'],
+ 'Symfony\Component\VarDumper\Caster\CutArrayStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castCutArray'],
+ 'Symfony\Component\VarDumper\Caster\ConstStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castStub'],
+ 'Symfony\Component\VarDumper\Caster\EnumStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castEnum'],
+
+ 'Closure' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClosure'],
+ 'Generator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castGenerator'],
+ 'ReflectionType' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castType'],
+ 'ReflectionGenerator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castReflectionGenerator'],
+ 'ReflectionClass' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClass'],
+ 'ReflectionFunctionAbstract' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castFunctionAbstract'],
+ 'ReflectionMethod' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castMethod'],
+ 'ReflectionParameter' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castParameter'],
+ 'ReflectionProperty' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castProperty'],
+ 'ReflectionReference' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castReference'],
+ 'ReflectionExtension' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castExtension'],
+ 'ReflectionZendExtension' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castZendExtension'],
+
+ 'Doctrine\Common\Persistence\ObjectManager' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Doctrine\Common\Proxy\Proxy' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castCommonProxy'],
+ 'Doctrine\ORM\Proxy\Proxy' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castOrmProxy'],
+ 'Doctrine\ORM\PersistentCollection' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castPersistentCollection'],
+ 'Doctrine\Persistence\ObjectManager' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+
+ 'DOMException' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castException'],
+ 'DOMStringList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'],
+ 'DOMNameList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'],
+ 'DOMImplementation' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castImplementation'],
+ 'DOMImplementationList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'],
+ 'DOMNode' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNode'],
+ 'DOMNameSpaceNode' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNameSpaceNode'],
+ 'DOMDocument' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDocument'],
+ 'DOMNodeList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'],
+ 'DOMNamedNodeMap' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'],
+ 'DOMCharacterData' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castCharacterData'],
+ 'DOMAttr' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castAttr'],
+ 'DOMElement' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castElement'],
+ 'DOMText' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castText'],
+ 'DOMTypeinfo' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castTypeinfo'],
+ 'DOMDomError' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDomError'],
+ 'DOMLocator' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLocator'],
+ 'DOMDocumentType' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDocumentType'],
+ 'DOMNotation' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNotation'],
+ 'DOMEntity' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castEntity'],
+ 'DOMProcessingInstruction' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castProcessingInstruction'],
+ 'DOMXPath' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castXPath'],
+
+ 'XMLReader' => ['Symfony\Component\VarDumper\Caster\XmlReaderCaster', 'castXmlReader'],
+
+ 'ErrorException' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castErrorException'],
+ 'Exception' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castException'],
+ 'Error' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castError'],
+ 'Symfony\Component\DependencyInjection\ContainerInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Symfony\Component\EventDispatcher\EventDispatcherInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Symfony\Component\HttpClient\CurlHttpClient' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClient'],
+ 'Symfony\Component\HttpClient\NativeHttpClient' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClient'],
+ 'Symfony\Component\HttpClient\Response\CurlResponse' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClientResponse'],
+ 'Symfony\Component\HttpClient\Response\NativeResponse' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClientResponse'],
+ 'Symfony\Component\HttpFoundation\Request' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castRequest'],
+ 'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castThrowingCasterException'],
+ 'Symfony\Component\VarDumper\Caster\TraceStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'],
+ 'Symfony\Component\VarDumper\Caster\FrameStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'],
+ 'Symfony\Component\VarDumper\Cloner\AbstractCloner' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Symfony\Component\ErrorHandler\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'],
+
+ 'Imagine\Image\ImageInterface' => ['Symfony\Component\VarDumper\Caster\ImagineCaster', 'castImage'],
+
+ 'Ramsey\Uuid\UuidInterface' => ['Symfony\Component\VarDumper\Caster\UuidCaster', 'castRamseyUuid'],
+
+ 'ProxyManager\Proxy\ProxyInterface' => ['Symfony\Component\VarDumper\Caster\ProxyManagerCaster', 'castProxy'],
+ 'PHPUnit_Framework_MockObject_MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'PHPUnit\Framework\MockObject\MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'PHPUnit\Framework\MockObject\Stub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Prophecy\Prophecy\ProphecySubjectInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+ 'Mockery\MockInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],
+
+ 'PDO' => ['Symfony\Component\VarDumper\Caster\PdoCaster', 'castPdo'],
+ 'PDOStatement' => ['Symfony\Component\VarDumper\Caster\PdoCaster', 'castPdoStatement'],
+
+ 'AMQPConnection' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castConnection'],
+ 'AMQPChannel' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castChannel'],
+ 'AMQPQueue' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castQueue'],
+ 'AMQPExchange' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castExchange'],
+ 'AMQPEnvelope' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castEnvelope'],
+
+ 'ArrayObject' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castArrayObject'],
+ 'ArrayIterator' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castArrayIterator'],
+ 'SplDoublyLinkedList' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castDoublyLinkedList'],
+ 'SplFileInfo' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castFileInfo'],
+ 'SplFileObject' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castFileObject'],
+ 'SplHeap' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castHeap'],
+ 'SplObjectStorage' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castObjectStorage'],
+ 'SplPriorityQueue' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castHeap'],
+ 'OuterIterator' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castOuterIterator'],
+ 'WeakReference' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castWeakReference'],
+
+ 'Redis' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedis'],
+ 'RedisArray' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'],
+ 'RedisCluster' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisCluster'],
+
+ 'DateTimeInterface' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castDateTime'],
+ 'DateInterval' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castInterval'],
+ 'DateTimeZone' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castTimeZone'],
+ 'DatePeriod' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castPeriod'],
+
+ 'GMP' => ['Symfony\Component\VarDumper\Caster\GmpCaster', 'castGmp'],
+
+ 'MessageFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castMessageFormatter'],
+ 'NumberFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castNumberFormatter'],
+ 'IntlTimeZone' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlTimeZone'],
+ 'IntlCalendar' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlCalendar'],
+ 'IntlDateFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlDateFormatter'],
+
+ 'Memcached' => ['Symfony\Component\VarDumper\Caster\MemcachedCaster', 'castMemcached'],
+
+ 'Ds\Collection' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castCollection'],
+ 'Ds\Map' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castMap'],
+ 'Ds\Pair' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPair'],
+ 'Symfony\Component\VarDumper\Caster\DsPairStub' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPairStub'],
+
+ 'CurlHandle' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'],
+ ':curl' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'],
+
+ ':dba' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'],
+ ':dba persistent' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'],
+ ':gd' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'],
+ ':mysql link' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castMysqlLink'],
+ ':pgsql large object' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLargeObject'],
+ ':pgsql link' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLink'],
+ ':pgsql link persistent' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLink'],
+ ':pgsql result' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castResult'],
+ ':process' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castProcess'],
+ ':stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'],
+ ':OpenSSL X.509' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castOpensslX509'],
+ ':persistent stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'],
+ ':stream-context' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStreamContext'],
+ ':xml' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'],
+ ];
+
+ protected $maxItems = 2500;
+ protected $maxString = -1;
+ protected $minDepth = 1;
+
+ private $casters = [];
+ private $prevErrorHandler;
+ private $classInfo = [];
+ private $filter = 0;
+
+ /**
+ * @param callable[]|null $casters A map of casters
+ *
+ * @see addCasters
+ */
+ public function __construct(array $casters = null)
+ {
+ if (null === $casters) {
+ $casters = static::$defaultCasters;
+ }
+ $this->addCasters($casters);
+ }
+
+ /**
+ * Adds casters for resources and objects.
+ *
+ * Maps resources or objects types to a callback.
+ * Types are in the key, with a callable caster for value.
+ * Resource types are to be prefixed with a `:`,
+ * see e.g. static::$defaultCasters.
+ *
+ * @param callable[] $casters A map of casters
+ */
+ public function addCasters(array $casters)
+ {
+ foreach ($casters as $type => $callback) {
+ $this->casters[$type][] = $callback;
+ }
+ }
+
+ /**
+ * Sets the maximum number of items to clone past the minimum depth in nested structures.
+ *
+ * @param int $maxItems
+ */
+ public function setMaxItems($maxItems)
+ {
+ $this->maxItems = (int) $maxItems;
+ }
+
+ /**
+ * Sets the maximum cloned length for strings.
+ *
+ * @param int $maxString
+ */
+ public function setMaxString($maxString)
+ {
+ $this->maxString = (int) $maxString;
+ }
+
+ /**
+ * Sets the minimum tree depth where we are guaranteed to clone all the items. After this
+ * depth is reached, only setMaxItems items will be cloned.
+ *
+ * @param int $minDepth
+ */
+ public function setMinDepth($minDepth)
+ {
+ $this->minDepth = (int) $minDepth;
+ }
+
+ /**
+ * Clones a PHP variable.
+ *
+ * @param mixed $var Any PHP variable
+ * @param int $filter A bit field of Caster::EXCLUDE_* constants
+ *
+ * @return Data The cloned variable represented by a Data object
+ */
+ public function cloneVar($var, $filter = 0)
+ {
+ $this->prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) {
+ if (E_RECOVERABLE_ERROR === $type || E_USER_ERROR === $type) {
+ // Cloner never dies
+ throw new \ErrorException($msg, 0, $type, $file, $line);
+ }
+
+ if ($this->prevErrorHandler) {
+ return ($this->prevErrorHandler)($type, $msg, $file, $line, $context);
+ }
+
+ return false;
+ });
+ $this->filter = $filter;
+
+ if ($gc = gc_enabled()) {
+ gc_disable();
+ }
+ try {
+ return new Data($this->doClone($var));
+ } finally {
+ if ($gc) {
+ gc_enable();
+ }
+ restore_error_handler();
+ $this->prevErrorHandler = null;
+ }
+ }
+
+ /**
+ * Effectively clones the PHP variable.
+ *
+ * @param mixed $var Any PHP variable
+ *
+ * @return array The cloned variable represented in an array
+ */
+ abstract protected function doClone($var);
+
+ /**
+ * Casts an object to an array representation.
+ *
+ * @param bool $isNested True if the object is nested in the dumped structure
+ *
+ * @return array The object casted as array
+ */
+ protected function castObject(Stub $stub, $isNested)
+ {
+ $obj = $stub->value;
+ $class = $stub->class;
+
+ if (\PHP_VERSION_ID < 80000 ? "\0" === ($class[15] ?? null) : false !== strpos($class, "@anonymous\0")) {
+ $stub->class = get_debug_type($obj);
+ }
+ if (isset($this->classInfo[$class])) {
+ list($i, $parents, $hasDebugInfo, $fileInfo) = $this->classInfo[$class];
+ } else {
+ $i = 2;
+ $parents = [$class];
+ $hasDebugInfo = method_exists($class, '__debugInfo');
+
+ foreach (class_parents($class) as $p) {
+ $parents[] = $p;
+ ++$i;
+ }
+ foreach (class_implements($class) as $p) {
+ $parents[] = $p;
+ ++$i;
+ }
+ $parents[] = '*';
+
+ $r = new \ReflectionClass($class);
+ $fileInfo = $r->isInternal() || $r->isSubclassOf(Stub::class) ? [] : [
+ 'file' => $r->getFileName(),
+ 'line' => $r->getStartLine(),
+ ];
+
+ $this->classInfo[$class] = [$i, $parents, $hasDebugInfo, $fileInfo];
+ }
+
+ $stub->attr += $fileInfo;
+ $a = Caster::castObject($obj, $class, $hasDebugInfo, $stub->class);
+
+ try {
+ while ($i--) {
+ if (!empty($this->casters[$p = $parents[$i]])) {
+ foreach ($this->casters[$p] as $callback) {
+ $a = $callback($obj, $a, $stub, $isNested, $this->filter);
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ $a = [(Stub::TYPE_OBJECT === $stub->type ? Caster::PREFIX_VIRTUAL : '').'⚠' => new ThrowingCasterException($e)] + $a;
+ }
+
+ return $a;
+ }
+
+ /**
+ * Casts a resource to an array representation.
+ *
+ * @param bool $isNested True if the object is nested in the dumped structure
+ *
+ * @return array The resource casted as array
+ */
+ protected function castResource(Stub $stub, $isNested)
+ {
+ $a = [];
+ $res = $stub->value;
+ $type = $stub->class;
+
+ try {
+ if (!empty($this->casters[':'.$type])) {
+ foreach ($this->casters[':'.$type] as $callback) {
+ $a = $callback($res, $a, $stub, $isNested, $this->filter);
+ }
+ }
+ } catch (\Exception $e) {
+ $a = [(Stub::TYPE_OBJECT === $stub->type ? Caster::PREFIX_VIRTUAL : '').'⚠' => new ThrowingCasterException($e)] + $a;
+ }
+
+ return $a;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Cloner/ClonerInterface.php b/vendor/symfony/var-dumper/Cloner/ClonerInterface.php
new file mode 100644
index 0000000..1ddd2f7
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/ClonerInterface.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas
+ */
+interface ClonerInterface
+{
+ /**
+ * Clones a PHP variable.
+ *
+ * @param mixed $var Any PHP variable
+ *
+ * @return Data The cloned variable represented by a Data object
+ */
+ public function cloneVar($var);
+}
diff --git a/vendor/symfony/var-dumper/Cloner/Cursor.php b/vendor/symfony/var-dumper/Cloner/Cursor.php
new file mode 100644
index 0000000..0337f3c
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/Cursor.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * Represents the current state of a dumper while dumping.
+ *
+ * @author Nicolas Grekas
+ */
+class Cursor
+{
+ const HASH_INDEXED = Stub::ARRAY_INDEXED;
+ const HASH_ASSOC = Stub::ARRAY_ASSOC;
+ const HASH_OBJECT = Stub::TYPE_OBJECT;
+ const HASH_RESOURCE = Stub::TYPE_RESOURCE;
+
+ public $depth = 0;
+ public $refIndex = 0;
+ public $softRefTo = 0;
+ public $softRefCount = 0;
+ public $softRefHandle = 0;
+ public $hardRefTo = 0;
+ public $hardRefCount = 0;
+ public $hardRefHandle = 0;
+ public $hashType;
+ public $hashKey;
+ public $hashKeyIsBinary;
+ public $hashIndex = 0;
+ public $hashLength = 0;
+ public $hashCut = 0;
+ public $stop = false;
+ public $attr = [];
+ public $skipChildren = false;
+}
diff --git a/vendor/symfony/var-dumper/Cloner/Data.php b/vendor/symfony/var-dumper/Cloner/Data.php
new file mode 100644
index 0000000..cecca5a
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/Data.php
@@ -0,0 +1,455 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+use Symfony\Component\VarDumper\Caster\Caster;
+use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
+
+/**
+ * @author Nicolas Grekas
+ */
+class Data implements \ArrayAccess, \Countable, \IteratorAggregate
+{
+ private $data;
+ private $position = 0;
+ private $key = 0;
+ private $maxDepth = 20;
+ private $maxItemsPerDepth = -1;
+ private $useRefHandles = -1;
+ private $context = [];
+
+ /**
+ * @param array $data An array as returned by ClonerInterface::cloneVar()
+ */
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * @return string|null The type of the value
+ */
+ public function getType()
+ {
+ $item = $this->data[$this->position][$this->key];
+
+ if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
+ $item = $item->value;
+ }
+ if (!$item instanceof Stub) {
+ return \gettype($item);
+ }
+ if (Stub::TYPE_STRING === $item->type) {
+ return 'string';
+ }
+ if (Stub::TYPE_ARRAY === $item->type) {
+ return 'array';
+ }
+ if (Stub::TYPE_OBJECT === $item->type) {
+ return $item->class;
+ }
+ if (Stub::TYPE_RESOURCE === $item->type) {
+ return $item->class.' resource';
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array|bool $recursive Whether values should be resolved recursively or not
+ *
+ * @return string|int|float|bool|array|Data[]|null A native representation of the original value
+ */
+ public function getValue($recursive = false)
+ {
+ $item = $this->data[$this->position][$this->key];
+
+ if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
+ $item = $item->value;
+ }
+ if (!($item = $this->getStub($item)) instanceof Stub) {
+ return $item;
+ }
+ if (Stub::TYPE_STRING === $item->type) {
+ return $item->value;
+ }
+
+ $children = $item->position ? $this->data[$item->position] : [];
+
+ foreach ($children as $k => $v) {
+ if ($recursive && !($v = $this->getStub($v)) instanceof Stub) {
+ continue;
+ }
+ $children[$k] = clone $this;
+ $children[$k]->key = $k;
+ $children[$k]->position = $item->position;
+
+ if ($recursive) {
+ if (Stub::TYPE_REF === $v->type && ($v = $this->getStub($v->value)) instanceof Stub) {
+ $recursive = (array) $recursive;
+ if (isset($recursive[$v->position])) {
+ continue;
+ }
+ $recursive[$v->position] = true;
+ }
+ $children[$k] = $children[$k]->getValue($recursive);
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * @return int
+ */
+ public function count()
+ {
+ return \count($this->getValue());
+ }
+
+ /**
+ * @return \Traversable
+ */
+ public function getIterator()
+ {
+ if (!\is_array($value = $this->getValue())) {
+ throw new \LogicException(sprintf('"%s" object holds non-iterable type "%s".', self::class, \gettype($value)));
+ }
+
+ yield from $value;
+ }
+
+ public function __get($key)
+ {
+ if (null !== $data = $this->seek($key)) {
+ $item = $this->getStub($data->data[$data->position][$data->key]);
+
+ return $item instanceof Stub || [] === $item ? $data : $item;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function __isset($key)
+ {
+ return null !== $this->seek($key);
+ }
+
+ /**
+ * @return bool
+ */
+ public function offsetExists($key)
+ {
+ return $this->__isset($key);
+ }
+
+ public function offsetGet($key)
+ {
+ return $this->__get($key);
+ }
+
+ public function offsetSet($key, $value)
+ {
+ throw new \BadMethodCallException(self::class.' objects are immutable.');
+ }
+
+ public function offsetUnset($key)
+ {
+ throw new \BadMethodCallException(self::class.' objects are immutable.');
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ $value = $this->getValue();
+
+ if (!\is_array($value)) {
+ return (string) $value;
+ }
+
+ return sprintf('%s (count=%d)', $this->getType(), \count($value));
+ }
+
+ /**
+ * Returns a depth limited clone of $this.
+ *
+ * @param int $maxDepth The max dumped depth level
+ *
+ * @return static
+ */
+ public function withMaxDepth($maxDepth)
+ {
+ $data = clone $this;
+ $data->maxDepth = (int) $maxDepth;
+
+ return $data;
+ }
+
+ /**
+ * Limits the number of elements per depth level.
+ *
+ * @param int $maxItemsPerDepth The max number of items dumped per depth level
+ *
+ * @return static
+ */
+ public function withMaxItemsPerDepth($maxItemsPerDepth)
+ {
+ $data = clone $this;
+ $data->maxItemsPerDepth = (int) $maxItemsPerDepth;
+
+ return $data;
+ }
+
+ /**
+ * Enables/disables objects' identifiers tracking.
+ *
+ * @param bool $useRefHandles False to hide global ref. handles
+ *
+ * @return static
+ */
+ public function withRefHandles($useRefHandles)
+ {
+ $data = clone $this;
+ $data->useRefHandles = $useRefHandles ? -1 : 0;
+
+ return $data;
+ }
+
+ /**
+ * @return static
+ */
+ public function withContext(array $context)
+ {
+ $data = clone $this;
+ $data->context = $context;
+
+ return $data;
+ }
+
+ /**
+ * Seeks to a specific key in nested data structures.
+ *
+ * @param string|int $key The key to seek to
+ *
+ * @return static|null Null if the key is not set
+ */
+ public function seek($key)
+ {
+ $item = $this->data[$this->position][$this->key];
+
+ if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
+ $item = $item->value;
+ }
+ if (!($item = $this->getStub($item)) instanceof Stub || !$item->position) {
+ return null;
+ }
+ $keys = [$key];
+
+ switch ($item->type) {
+ case Stub::TYPE_OBJECT:
+ $keys[] = Caster::PREFIX_DYNAMIC.$key;
+ $keys[] = Caster::PREFIX_PROTECTED.$key;
+ $keys[] = Caster::PREFIX_VIRTUAL.$key;
+ $keys[] = "\0$item->class\0$key";
+ // no break
+ case Stub::TYPE_ARRAY:
+ case Stub::TYPE_RESOURCE:
+ break;
+ default:
+ return null;
+ }
+
+ $data = null;
+ $children = $this->data[$item->position];
+
+ foreach ($keys as $key) {
+ if (isset($children[$key]) || \array_key_exists($key, $children)) {
+ $data = clone $this;
+ $data->key = $key;
+ $data->position = $item->position;
+ break;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Dumps data with a DumperInterface dumper.
+ */
+ public function dump(DumperInterface $dumper)
+ {
+ $refs = [0];
+ $cursor = new Cursor();
+
+ if ($cursor->attr = $this->context[SourceContextProvider::class] ?? []) {
+ $cursor->attr['if_links'] = true;
+ $cursor->hashType = -1;
+ $dumper->dumpScalar($cursor, 'default', '^');
+ $cursor->attr = ['if_links' => true];
+ $dumper->dumpScalar($cursor, 'default', ' ');
+ $cursor->hashType = 0;
+ }
+
+ $this->dumpItem($dumper, $cursor, $refs, $this->data[$this->position][$this->key]);
+ }
+
+ /**
+ * Depth-first dumping of items.
+ *
+ * @param mixed $item A Stub object or the original value being dumped
+ */
+ private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, $item)
+ {
+ $cursor->refIndex = 0;
+ $cursor->softRefTo = $cursor->softRefHandle = $cursor->softRefCount = 0;
+ $cursor->hardRefTo = $cursor->hardRefHandle = $cursor->hardRefCount = 0;
+ $firstSeen = true;
+
+ if (!$item instanceof Stub) {
+ $cursor->attr = [];
+ $type = \gettype($item);
+ if ($item && 'array' === $type) {
+ $item = $this->getStub($item);
+ }
+ } elseif (Stub::TYPE_REF === $item->type) {
+ if ($item->handle) {
+ if (!isset($refs[$r = $item->handle - (PHP_INT_MAX >> 1)])) {
+ $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0];
+ } else {
+ $firstSeen = false;
+ }
+ $cursor->hardRefTo = $refs[$r];
+ $cursor->hardRefHandle = $this->useRefHandles & $item->handle;
+ $cursor->hardRefCount = $item->refCount;
+ }
+ $cursor->attr = $item->attr;
+ $type = $item->class ?: \gettype($item->value);
+ $item = $this->getStub($item->value);
+ }
+ if ($item instanceof Stub) {
+ if ($item->refCount) {
+ if (!isset($refs[$r = $item->handle])) {
+ $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0];
+ } else {
+ $firstSeen = false;
+ }
+ $cursor->softRefTo = $refs[$r];
+ }
+ $cursor->softRefHandle = $this->useRefHandles & $item->handle;
+ $cursor->softRefCount = $item->refCount;
+ $cursor->attr = $item->attr;
+ $cut = $item->cut;
+
+ if ($item->position && $firstSeen) {
+ $children = $this->data[$item->position];
+
+ if ($cursor->stop) {
+ if ($cut >= 0) {
+ $cut += \count($children);
+ }
+ $children = [];
+ }
+ } else {
+ $children = [];
+ }
+ switch ($item->type) {
+ case Stub::TYPE_STRING:
+ $dumper->dumpString($cursor, $item->value, Stub::STRING_BINARY === $item->class, $cut);
+ break;
+
+ case Stub::TYPE_ARRAY:
+ $item = clone $item;
+ $item->type = $item->class;
+ $item->class = $item->value;
+ // no break
+ case Stub::TYPE_OBJECT:
+ case Stub::TYPE_RESOURCE:
+ $withChildren = $children && $cursor->depth !== $this->maxDepth && $this->maxItemsPerDepth;
+ $dumper->enterHash($cursor, $item->type, $item->class, $withChildren);
+ if ($withChildren) {
+ if ($cursor->skipChildren) {
+ $withChildren = false;
+ $cut = -1;
+ } else {
+ $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, $item->type, null !== $item->class);
+ }
+ } elseif ($children && 0 <= $cut) {
+ $cut += \count($children);
+ }
+ $cursor->skipChildren = false;
+ $dumper->leaveHash($cursor, $item->type, $item->class, $withChildren, $cut);
+ break;
+
+ default:
+ throw new \RuntimeException(sprintf('Unexpected Stub type: "%s".', $item->type));
+ }
+ } elseif ('array' === $type) {
+ $dumper->enterHash($cursor, Cursor::HASH_INDEXED, 0, false);
+ $dumper->leaveHash($cursor, Cursor::HASH_INDEXED, 0, false, 0);
+ } elseif ('string' === $type) {
+ $dumper->dumpString($cursor, $item, false, 0);
+ } else {
+ $dumper->dumpScalar($cursor, $type, $item);
+ }
+ }
+
+ /**
+ * Dumps children of hash structures.
+ *
+ * @return int The final number of removed items
+ */
+ private function dumpChildren(DumperInterface $dumper, Cursor $parentCursor, array &$refs, array $children, int $hashCut, int $hashType, bool $dumpKeys): int
+ {
+ $cursor = clone $parentCursor;
+ ++$cursor->depth;
+ $cursor->hashType = $hashType;
+ $cursor->hashIndex = 0;
+ $cursor->hashLength = \count($children);
+ $cursor->hashCut = $hashCut;
+ foreach ($children as $key => $child) {
+ $cursor->hashKeyIsBinary = isset($key[0]) && !preg_match('//u', $key);
+ $cursor->hashKey = $dumpKeys ? $key : null;
+ $this->dumpItem($dumper, $cursor, $refs, $child);
+ if (++$cursor->hashIndex === $this->maxItemsPerDepth || $cursor->stop) {
+ $parentCursor->stop = true;
+
+ return $hashCut >= 0 ? $hashCut + $cursor->hashLength - $cursor->hashIndex : $hashCut;
+ }
+ }
+
+ return $hashCut;
+ }
+
+ private function getStub($item)
+ {
+ if (!$item || !\is_array($item)) {
+ return $item;
+ }
+
+ $stub = new Stub();
+ $stub->type = Stub::TYPE_ARRAY;
+ foreach ($item as $stub->class => $stub->position) {
+ }
+ if (isset($item[0])) {
+ $stub->cut = $item[0];
+ }
+ $stub->value = $stub->cut + ($stub->position ? \count($this->data[$stub->position]) : 0);
+
+ return $stub;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Cloner/DumperInterface.php b/vendor/symfony/var-dumper/Cloner/DumperInterface.php
new file mode 100644
index 0000000..b61d749
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/DumperInterface.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * DumperInterface used by Data objects.
+ *
+ * @author Nicolas Grekas
+ */
+interface DumperInterface
+{
+ /**
+ * Dumps a scalar value.
+ *
+ * @param string $type The PHP type of the value being dumped
+ * @param string|int|float|bool $value The scalar value being dumped
+ */
+ public function dumpScalar(Cursor $cursor, $type, $value);
+
+ /**
+ * Dumps a string.
+ *
+ * @param string $str The string being dumped
+ * @param bool $bin Whether $str is UTF-8 or binary encoded
+ * @param int $cut The number of characters $str has been cut by
+ */
+ public function dumpString(Cursor $cursor, $str, $bin, $cut);
+
+ /**
+ * Dumps while entering an hash.
+ *
+ * @param int $type A Cursor::HASH_* const for the type of hash
+ * @param string|int $class The object class, resource type or array count
+ * @param bool $hasChild When the dump of the hash has child item
+ */
+ public function enterHash(Cursor $cursor, $type, $class, $hasChild);
+
+ /**
+ * Dumps while leaving an hash.
+ *
+ * @param int $type A Cursor::HASH_* const for the type of hash
+ * @param string|int $class The object class, resource type or array count
+ * @param bool $hasChild When the dump of the hash has child item
+ * @param int $cut The number of items the hash has been cut by
+ */
+ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut);
+}
diff --git a/vendor/symfony/var-dumper/Cloner/Stub.php b/vendor/symfony/var-dumper/Cloner/Stub.php
new file mode 100644
index 0000000..b3b1217
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/Stub.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * Represents the main properties of a PHP variable.
+ *
+ * @author Nicolas Grekas
+ */
+class Stub
+{
+ const TYPE_REF = 1;
+ const TYPE_STRING = 2;
+ const TYPE_ARRAY = 3;
+ const TYPE_OBJECT = 4;
+ const TYPE_RESOURCE = 5;
+
+ const STRING_BINARY = 1;
+ const STRING_UTF8 = 2;
+
+ const ARRAY_ASSOC = 1;
+ const ARRAY_INDEXED = 2;
+
+ public $type = self::TYPE_REF;
+ public $class = '';
+ public $value;
+ public $cut = 0;
+ public $handle = 0;
+ public $refCount = 0;
+ public $position = 0;
+ public $attr = [];
+
+ private static $defaultProperties = [];
+
+ /**
+ * @internal
+ */
+ public function __sleep(): array
+ {
+ $properties = [];
+
+ if (!isset(self::$defaultProperties[$c = static::class])) {
+ self::$defaultProperties[$c] = get_class_vars($c);
+
+ foreach ((new \ReflectionClass($c))->getStaticProperties() as $k => $v) {
+ unset(self::$defaultProperties[$c][$k]);
+ }
+ }
+
+ foreach (self::$defaultProperties[$c] as $k => $v) {
+ if ($this->$k !== $v) {
+ $properties[] = $k;
+ }
+ }
+
+ return $properties;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Cloner/VarCloner.php b/vendor/symfony/var-dumper/Cloner/VarCloner.php
new file mode 100644
index 0000000..193d2f2
--- /dev/null
+++ b/vendor/symfony/var-dumper/Cloner/VarCloner.php
@@ -0,0 +1,302 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Cloner;
+
+/**
+ * @author Nicolas Grekas
+ */
+class VarCloner extends AbstractCloner
+{
+ private static $gid;
+ private static $arrayCache = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doClone($var)
+ {
+ $len = 1; // Length of $queue
+ $pos = 0; // Number of cloned items past the minimum depth
+ $refsCounter = 0; // Hard references counter
+ $queue = [[$var]]; // This breadth-first queue is the return value
+ $indexedArrays = []; // Map of queue indexes that hold numerically indexed arrays
+ $hardRefs = []; // Map of original zval ids to stub objects
+ $objRefs = []; // Map of original object handles to their stub object counterpart
+ $objects = []; // Keep a ref to objects to ensure their handle cannot be reused while cloning
+ $resRefs = []; // Map of original resource handles to their stub object counterpart
+ $values = []; // Map of stub objects' ids to original values
+ $maxItems = $this->maxItems;
+ $maxString = $this->maxString;
+ $minDepth = $this->minDepth;
+ $currentDepth = 0; // Current tree depth
+ $currentDepthFinalIndex = 0; // Final $queue index for current tree depth
+ $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached
+ $cookie = (object) []; // Unique object used to detect hard references
+ $a = null; // Array cast for nested structures
+ $stub = null; // Stub capturing the main properties of an original item value
+ // or null if the original value is used directly
+
+ if (!$gid = self::$gid) {
+ $gid = self::$gid = md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable
+ }
+ $arrayStub = new Stub();
+ $arrayStub->type = Stub::TYPE_ARRAY;
+ $fromObjCast = false;
+
+ for ($i = 0; $i < $len; ++$i) {
+ // Detect when we move on to the next tree depth
+ if ($i > $currentDepthFinalIndex) {
+ ++$currentDepth;
+ $currentDepthFinalIndex = $len - 1;
+ if ($currentDepth >= $minDepth) {
+ $minimumDepthReached = true;
+ }
+ }
+
+ $refs = $vals = $queue[$i];
+ if (\PHP_VERSION_ID < 70200 && empty($indexedArrays[$i])) {
+ // see https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts
+ foreach ($vals as $k => $v) {
+ if (\is_int($k)) {
+ continue;
+ }
+ foreach ([$k => true] as $gk => $gv) {
+ }
+ if ($gk !== $k) {
+ $fromObjCast = true;
+ $refs = $vals = array_values($queue[$i]);
+ break;
+ }
+ }
+ }
+ foreach ($vals as $k => $v) {
+ // $v is the original value or a stub object in case of hard references
+
+ if (\PHP_VERSION_ID >= 70400) {
+ $zvalIsRef = null !== \ReflectionReference::fromArrayElement($vals, $k);
+ } else {
+ $refs[$k] = $cookie;
+ $zvalIsRef = $vals[$k] === $cookie;
+ }
+
+ if ($zvalIsRef) {
+ $vals[$k] = &$stub; // Break hard references to make $queue completely
+ unset($stub); // independent from the original structure
+ if ($v instanceof Stub && isset($hardRefs[spl_object_id($v)])) {
+ $vals[$k] = $refs[$k] = $v;
+ if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) {
+ ++$v->value->refCount;
+ }
+ ++$v->refCount;
+ continue;
+ }
+ $refs[$k] = $vals[$k] = new Stub();
+ $refs[$k]->value = $v;
+ $h = spl_object_id($refs[$k]);
+ $hardRefs[$h] = &$refs[$k];
+ $values[$h] = $v;
+ $vals[$k]->handle = ++$refsCounter;
+ }
+ // Create $stub when the original value $v can not be used directly
+ // If $v is a nested structure, put that structure in array $a
+ switch (true) {
+ case null === $v:
+ case \is_bool($v):
+ case \is_int($v):
+ case \is_float($v):
+ continue 2;
+
+ case \is_string($v):
+ if ('' === $v) {
+ continue 2;
+ }
+ if (!preg_match('//u', $v)) {
+ $stub = new Stub();
+ $stub->type = Stub::TYPE_STRING;
+ $stub->class = Stub::STRING_BINARY;
+ if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) {
+ $stub->cut = $cut;
+ $stub->value = substr($v, 0, -$cut);
+ } else {
+ $stub->value = $v;
+ }
+ } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) {
+ $stub = new Stub();
+ $stub->type = Stub::TYPE_STRING;
+ $stub->class = Stub::STRING_UTF8;
+ $stub->cut = $cut;
+ $stub->value = mb_substr($v, 0, $maxString, 'UTF-8');
+ } else {
+ continue 2;
+ }
+ $a = null;
+ break;
+
+ case \is_array($v):
+ if (!$v) {
+ continue 2;
+ }
+ $stub = $arrayStub;
+ $stub->class = Stub::ARRAY_INDEXED;
+
+ $j = -1;
+ foreach ($v as $gk => $gv) {
+ if ($gk !== ++$j) {
+ $stub->class = Stub::ARRAY_ASSOC;
+ break;
+ }
+ }
+ $a = $v;
+
+ if (Stub::ARRAY_ASSOC === $stub->class) {
+ // Copies of $GLOBALS have very strange behavior,
+ // let's detect them with some black magic
+ $a[$gid] = true;
+
+ // Happens with copies of $GLOBALS
+ if (isset($v[$gid])) {
+ unset($v[$gid]);
+ $a = [];
+ foreach ($v as $gk => &$gv) {
+ $a[$gk] = &$gv;
+ }
+ unset($gv);
+ } else {
+ $a = $v;
+ }
+ } elseif (\PHP_VERSION_ID < 70200) {
+ $indexedArrays[$len] = true;
+ }
+ break;
+
+ case \is_object($v):
+ case $v instanceof \__PHP_Incomplete_Class:
+ if (empty($objRefs[$h = spl_object_id($v)])) {
+ $stub = new Stub();
+ $stub->type = Stub::TYPE_OBJECT;
+ $stub->class = \get_class($v);
+ $stub->value = $v;
+ $stub->handle = $h;
+ $a = $this->castObject($stub, 0 < $i);
+ if ($v !== $stub->value) {
+ if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) {
+ break;
+ }
+ $stub->handle = $h = spl_object_id($stub->value);
+ }
+ $stub->value = null;
+ if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
+ $stub->cut = \count($a);
+ $a = null;
+ }
+ }
+ if (empty($objRefs[$h])) {
+ $objRefs[$h] = $stub;
+ $objects[] = $v;
+ } else {
+ $stub = $objRefs[$h];
+ ++$stub->refCount;
+ $a = null;
+ }
+ break;
+
+ default: // resource
+ if (empty($resRefs[$h = (int) $v])) {
+ $stub = new Stub();
+ $stub->type = Stub::TYPE_RESOURCE;
+ if ('Unknown' === $stub->class = @get_resource_type($v)) {
+ $stub->class = 'Closed';
+ }
+ $stub->value = $v;
+ $stub->handle = $h;
+ $a = $this->castResource($stub, 0 < $i);
+ $stub->value = null;
+ if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
+ $stub->cut = \count($a);
+ $a = null;
+ }
+ }
+ if (empty($resRefs[$h])) {
+ $resRefs[$h] = $stub;
+ } else {
+ $stub = $resRefs[$h];
+ ++$stub->refCount;
+ $a = null;
+ }
+ break;
+ }
+
+ if ($a) {
+ if (!$minimumDepthReached || 0 > $maxItems) {
+ $queue[$len] = $a;
+ $stub->position = $len++;
+ } elseif ($pos < $maxItems) {
+ if ($maxItems < $pos += \count($a)) {
+ $a = \array_slice($a, 0, $maxItems - $pos);
+ if ($stub->cut >= 0) {
+ $stub->cut += $pos - $maxItems;
+ }
+ }
+ $queue[$len] = $a;
+ $stub->position = $len++;
+ } elseif ($stub->cut >= 0) {
+ $stub->cut += \count($a);
+ $stub->position = 0;
+ }
+ }
+
+ if ($arrayStub === $stub) {
+ if ($arrayStub->cut) {
+ $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position];
+ $arrayStub->cut = 0;
+ } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) {
+ $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position];
+ } else {
+ self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position];
+ }
+ }
+
+ if ($zvalIsRef) {
+ $refs[$k]->value = $stub;
+ } else {
+ $vals[$k] = $stub;
+ }
+ }
+
+ if ($fromObjCast) {
+ $fromObjCast = false;
+ $refs = $vals;
+ $vals = [];
+ $j = -1;
+ foreach ($queue[$i] as $k => $v) {
+ foreach ([$k => true] as $gk => $gv) {
+ }
+ if ($gk !== $k) {
+ $vals = (object) $vals;
+ $vals->{$k} = $refs[++$j];
+ $vals = (array) $vals;
+ } else {
+ $vals[$k] = $refs[++$j];
+ }
+ }
+ }
+
+ $queue[$i] = $vals;
+ }
+
+ foreach ($values as $h => $v) {
+ $hardRefs[$h] = $v;
+ }
+
+ return $queue;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php b/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php
new file mode 100644
index 0000000..2303765
--- /dev/null
+++ b/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Command\Descriptor;
+
+use Symfony\Component\Console\Formatter\OutputFormatterStyle;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Dumper\CliDumper;
+
+/**
+ * Describe collected data clones for cli output.
+ *
+ * @author Maxime Steinhausser
+ *
+ * @final
+ */
+class CliDescriptor implements DumpDescriptorInterface
+{
+ private $dumper;
+ private $lastIdentifier;
+ private $supportsHref;
+
+ public function __construct(CliDumper $dumper)
+ {
+ $this->dumper = $dumper;
+ $this->supportsHref = method_exists(OutputFormatterStyle::class, 'setHref');
+ }
+
+ public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void
+ {
+ $io = $output instanceof SymfonyStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output);
+ $this->dumper->setColors($output->isDecorated());
+
+ $rows = [['date', date('r', $context['timestamp'])]];
+ $lastIdentifier = $this->lastIdentifier;
+ $this->lastIdentifier = $clientId;
+
+ $section = "Received from client #$clientId";
+ if (isset($context['request'])) {
+ $request = $context['request'];
+ $this->lastIdentifier = $request['identifier'];
+ $section = sprintf('%s %s', $request['method'], $request['uri']);
+ if ($controller = $request['controller']) {
+ $rows[] = ['controller', rtrim($this->dumper->dump($controller, true), "\n")];
+ }
+ } elseif (isset($context['cli'])) {
+ $this->lastIdentifier = $context['cli']['identifier'];
+ $section = '$ '.$context['cli']['command_line'];
+ }
+
+ if ($this->lastIdentifier !== $lastIdentifier) {
+ $io->section($section);
+ }
+
+ if (isset($context['source'])) {
+ $source = $context['source'];
+ $sourceInfo = sprintf('%s on line %d', $source['name'], $source['line']);
+ $fileLink = $source['file_link'] ?? null;
+ if ($this->supportsHref && $fileLink) {
+ $sourceInfo = sprintf('%s>', $fileLink, $sourceInfo);
+ }
+ $rows[] = ['source', $sourceInfo];
+ $file = $source['file_relative'] ?? $source['file'];
+ $rows[] = ['file', $file];
+ }
+
+ $io->table([], $rows);
+
+ if (!$this->supportsHref && isset($fileLink)) {
+ $io->writeln(['Open source in your IDE/browser: ', $fileLink]);
+ $io->newLine();
+ }
+
+ $this->dumper->dump($data);
+ $io->newLine();
+ }
+}
diff --git a/vendor/symfony/var-dumper/Command/Descriptor/DumpDescriptorInterface.php b/vendor/symfony/var-dumper/Command/Descriptor/DumpDescriptorInterface.php
new file mode 100644
index 0000000..bf4d89a
--- /dev/null
+++ b/vendor/symfony/var-dumper/Command/Descriptor/DumpDescriptorInterface.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Command\Descriptor;
+
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * @author Maxime Steinhausser
+ */
+interface DumpDescriptorInterface
+{
+ public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void;
+}
diff --git a/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php b/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php
new file mode 100644
index 0000000..90fba38
--- /dev/null
+++ b/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php
@@ -0,0 +1,119 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Command\Descriptor;
+
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Dumper\HtmlDumper;
+
+/**
+ * Describe collected data clones for html output.
+ *
+ * @author Maxime Steinhausser
+ *
+ * @final
+ */
+class HtmlDescriptor implements DumpDescriptorInterface
+{
+ private $dumper;
+ private $initialized = false;
+
+ public function __construct(HtmlDumper $dumper)
+ {
+ $this->dumper = $dumper;
+ }
+
+ public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void
+ {
+ if (!$this->initialized) {
+ $styles = file_get_contents(__DIR__.'/../../Resources/css/htmlDescriptor.css');
+ $scripts = file_get_contents(__DIR__.'/../../Resources/js/htmlDescriptor.js');
+ $output->writeln("");
+ $this->initialized = true;
+ }
+
+ $title = '-';
+ if (isset($context['request'])) {
+ $request = $context['request'];
+ $controller = "{$this->dumper->dump($request['controller'], true, ['maxDepth' => 0])} ";
+ $title = sprintf('%s
%s ', $request['method'], $uri = $request['uri'], $uri);
+ $dedupIdentifier = $request['identifier'];
+ } elseif (isset($context['cli'])) {
+ $title = '$
'.$context['cli']['command_line'];
+ $dedupIdentifier = $context['cli']['identifier'];
+ } else {
+ $dedupIdentifier = uniqid('', true);
+ }
+
+ $sourceDescription = '';
+ if (isset($context['source'])) {
+ $source = $context['source'];
+ $projectDir = $source['project_dir'] ?? null;
+ $sourceDescription = sprintf('%s on line %d', $source['name'], $source['line']);
+ if (isset($source['file_link'])) {
+ $sourceDescription = sprintf('%s ', $source['file_link'], $sourceDescription);
+ }
+ }
+
+ $isoDate = $this->extractDate($context, 'c');
+ $tags = array_filter([
+ 'controller' => $controller ?? null,
+ 'project dir' => $projectDir ?? null,
+ ]);
+
+ $output->writeln(<<
+
+
+
+ $sourceDescription
+
+ {$this->dumper->dump($data, true)}
+
+
+HTML
+ );
+ }
+
+ private function extractDate(array $context, string $format = 'r'): string
+ {
+ return date($format, $context['timestamp']);
+ }
+
+ private function renderTags(array $tags): string
+ {
+ if (!$tags) {
+ return '';
+ }
+
+ $renderedTags = '';
+ foreach ($tags as $key => $value) {
+ $renderedTags .= sprintf('%s %s ', $key, $value);
+ }
+
+ return <<
+
+
+HTML;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Command/ServerDumpCommand.php b/vendor/symfony/var-dumper/Command/ServerDumpCommand.php
new file mode 100644
index 0000000..a0c1a03
--- /dev/null
+++ b/vendor/symfony/var-dumper/Command/ServerDumpCommand.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Command;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor;
+use Symfony\Component\VarDumper\Command\Descriptor\DumpDescriptorInterface;
+use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor;
+use Symfony\Component\VarDumper\Dumper\CliDumper;
+use Symfony\Component\VarDumper\Dumper\HtmlDumper;
+use Symfony\Component\VarDumper\Server\DumpServer;
+
+/**
+ * Starts a dump server to collect and output dumps on a single place with multiple formats support.
+ *
+ * @author Maxime Steinhausser
+ *
+ * @final
+ */
+class ServerDumpCommand extends Command
+{
+ protected static $defaultName = 'server:dump';
+
+ private $server;
+
+ /** @var DumpDescriptorInterface[] */
+ private $descriptors;
+
+ public function __construct(DumpServer $server, array $descriptors = [])
+ {
+ $this->server = $server;
+ $this->descriptors = $descriptors + [
+ 'cli' => new CliDescriptor(new CliDumper()),
+ 'html' => new HtmlDescriptor(new HtmlDumper()),
+ ];
+
+ parent::__construct();
+ }
+
+ protected function configure()
+ {
+ $availableFormats = implode(', ', array_keys($this->descriptors));
+
+ $this
+ ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format (%s)', $availableFormats), 'cli')
+ ->setDescription('Starts a dump server that collects and displays dumps in a single place')
+ ->setHelp(<<<'EOF'
+%command.name% starts a dump server that collects and displays
+dumps in a single place for debugging you application:
+
+ php %command.full_name%
+
+You can consult dumped data in HTML format in your browser by providing the --format=html option
+and redirecting the output to a file:
+
+ php %command.full_name% --format="html" > dump.html
+
+EOF
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $format = $input->getOption('format');
+
+ if (!$descriptor = $this->descriptors[$format] ?? null) {
+ throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $format));
+ }
+
+ $errorIo = $io->getErrorStyle();
+ $errorIo->title('Symfony Var Dumper Server');
+
+ $this->server->start();
+
+ $errorIo->success(sprintf('Server listening on %s', $this->server->getHost()));
+ $errorIo->comment('Quit the server with CONTROL-C.');
+
+ $this->server->listen(function (Data $data, array $context, int $clientId) use ($descriptor, $io) {
+ $descriptor->describe($io, $data, $context, $clientId);
+ });
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/AbstractDumper.php b/vendor/symfony/var-dumper/Dumper/AbstractDumper.php
new file mode 100644
index 0000000..08baa82
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/AbstractDumper.php
@@ -0,0 +1,212 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Cloner\DumperInterface;
+
+/**
+ * Abstract mechanism for dumping a Data object.
+ *
+ * @author Nicolas Grekas
+ */
+abstract class AbstractDumper implements DataDumperInterface, DumperInterface
+{
+ const DUMP_LIGHT_ARRAY = 1;
+ const DUMP_STRING_LENGTH = 2;
+ const DUMP_COMMA_SEPARATOR = 4;
+ const DUMP_TRAILING_COMMA = 8;
+
+ public static $defaultOutput = 'php://output';
+
+ protected $line = '';
+ protected $lineDumper;
+ protected $outputStream;
+ protected $decimalPoint; // This is locale dependent
+ protected $indentPad = ' ';
+ protected $flags;
+
+ private $charset = '';
+
+ /**
+ * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path, defaults to static::$defaultOutput
+ * @param string|null $charset The default character encoding to use for non-UTF8 strings
+ * @param int $flags A bit field of static::DUMP_* constants to fine tune dumps representation
+ */
+ public function __construct($output = null, string $charset = null, int $flags = 0)
+ {
+ $this->flags = $flags;
+ $this->setCharset($charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8');
+ $this->decimalPoint = localeconv();
+ $this->decimalPoint = $this->decimalPoint['decimal_point'];
+ $this->setOutput($output ?: static::$defaultOutput);
+ if (!$output && \is_string(static::$defaultOutput)) {
+ static::$defaultOutput = $this->outputStream;
+ }
+ }
+
+ /**
+ * Sets the output destination of the dumps.
+ *
+ * @param callable|resource|string $output A line dumper callable, an opened stream or an output path
+ *
+ * @return callable|resource|string The previous output destination
+ */
+ public function setOutput($output)
+ {
+ $prev = null !== $this->outputStream ? $this->outputStream : $this->lineDumper;
+
+ if (\is_callable($output)) {
+ $this->outputStream = null;
+ $this->lineDumper = $output;
+ } else {
+ if (\is_string($output)) {
+ $output = fopen($output, 'wb');
+ }
+ $this->outputStream = $output;
+ $this->lineDumper = [$this, 'echoLine'];
+ }
+
+ return $prev;
+ }
+
+ /**
+ * Sets the default character encoding to use for non-UTF8 strings.
+ *
+ * @param string $charset The default character encoding to use for non-UTF8 strings
+ *
+ * @return string The previous charset
+ */
+ public function setCharset($charset)
+ {
+ $prev = $this->charset;
+
+ $charset = strtoupper($charset);
+ $charset = null === $charset || 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset;
+
+ $this->charset = $charset;
+
+ return $prev;
+ }
+
+ /**
+ * Sets the indentation pad string.
+ *
+ * @param string $pad A string that will be prepended to dumped lines, repeated by nesting level
+ *
+ * @return string The previous indent pad
+ */
+ public function setIndentPad($pad)
+ {
+ $prev = $this->indentPad;
+ $this->indentPad = $pad;
+
+ return $prev;
+ }
+
+ /**
+ * Dumps a Data object.
+ *
+ * @param callable|resource|string|true|null $output A line dumper callable, an opened stream, an output path or true to return the dump
+ *
+ * @return string|null The dump as string when $output is true
+ */
+ public function dump(Data $data, $output = null)
+ {
+ $this->decimalPoint = localeconv();
+ $this->decimalPoint = $this->decimalPoint['decimal_point'];
+
+ if ($locale = $this->flags & (self::DUMP_COMMA_SEPARATOR | self::DUMP_TRAILING_COMMA) ? setlocale(LC_NUMERIC, 0) : null) {
+ setlocale(LC_NUMERIC, 'C');
+ }
+
+ if ($returnDump = true === $output) {
+ $output = fopen('php://memory', 'r+b');
+ }
+ if ($output) {
+ $prevOutput = $this->setOutput($output);
+ }
+ try {
+ $data->dump($this);
+ $this->dumpLine(-1);
+
+ if ($returnDump) {
+ $result = stream_get_contents($output, -1, 0);
+ fclose($output);
+
+ return $result;
+ }
+ } finally {
+ if ($output) {
+ $this->setOutput($prevOutput);
+ }
+ if ($locale) {
+ setlocale(LC_NUMERIC, $locale);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Dumps the current line.
+ *
+ * @param int $depth The recursive depth in the dumped structure for the line being dumped,
+ * or -1 to signal the end-of-dump to the line dumper callable
+ */
+ protected function dumpLine($depth)
+ {
+ ($this->lineDumper)($this->line, $depth, $this->indentPad);
+ $this->line = '';
+ }
+
+ /**
+ * Generic line dumper callback.
+ *
+ * @param string $line The line to write
+ * @param int $depth The recursive depth in the dumped structure
+ * @param string $indentPad The line indent pad
+ */
+ protected function echoLine($line, $depth, $indentPad)
+ {
+ if (-1 !== $depth) {
+ fwrite($this->outputStream, str_repeat($indentPad, $depth).$line."\n");
+ }
+ }
+
+ /**
+ * Converts a non-UTF-8 string to UTF-8.
+ *
+ * @param string|null $s The non-UTF-8 string to convert
+ *
+ * @return string|null The string converted to UTF-8
+ */
+ protected function utf8Encode($s)
+ {
+ if (null === $s || preg_match('//u', $s)) {
+ return $s;
+ }
+
+ if (!\function_exists('iconv')) {
+ throw new \RuntimeException('Unable to convert a non-UTF-8 string to UTF-8: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
+ }
+
+ if (false !== $c = @iconv($this->charset, 'UTF-8', $s)) {
+ return $c;
+ }
+ if ('CP1252' !== $this->charset && false !== $c = @iconv('CP1252', 'UTF-8', $s)) {
+ return $c;
+ }
+
+ return iconv('CP850', 'UTF-8', $s);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/CliDumper.php b/vendor/symfony/var-dumper/Dumper/CliDumper.php
new file mode 100644
index 0000000..d475ce3
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/CliDumper.php
@@ -0,0 +1,651 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Cursor;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * CliDumper dumps variables for command line output.
+ *
+ * @author Nicolas Grekas
+ */
+class CliDumper extends AbstractDumper
+{
+ public static $defaultColors;
+ public static $defaultOutput = 'php://stdout';
+
+ protected $colors;
+ protected $maxStringWidth = 0;
+ protected $styles = [
+ // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
+ 'default' => '0;38;5;208',
+ 'num' => '1;38;5;38',
+ 'const' => '1;38;5;208',
+ 'str' => '1;38;5;113',
+ 'note' => '38;5;38',
+ 'ref' => '38;5;247',
+ 'public' => '',
+ 'protected' => '',
+ 'private' => '',
+ 'meta' => '38;5;170',
+ 'key' => '38;5;113',
+ 'index' => '38;5;38',
+ ];
+
+ protected static $controlCharsRx = '/[\x00-\x1F\x7F]+/';
+ protected static $controlCharsMap = [
+ "\t" => '\t',
+ "\n" => '\n',
+ "\v" => '\v',
+ "\f" => '\f',
+ "\r" => '\r',
+ "\033" => '\e',
+ ];
+
+ protected $collapseNextHash = false;
+ protected $expandNextHash = false;
+
+ private $displayOptions = [
+ 'fileLinkFormat' => null,
+ ];
+
+ private $handlesHrefGracefully;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($output = null, string $charset = null, int $flags = 0)
+ {
+ parent::__construct($output, $charset, $flags);
+
+ if ('\\' === \DIRECTORY_SEPARATOR && !$this->isWindowsTrueColor()) {
+ // Use only the base 16 xterm colors when using ANSICON or standard Windows 10 CLI
+ $this->setStyles([
+ 'default' => '31',
+ 'num' => '1;34',
+ 'const' => '1;31',
+ 'str' => '1;32',
+ 'note' => '34',
+ 'ref' => '1;30',
+ 'meta' => '35',
+ 'key' => '32',
+ 'index' => '34',
+ ]);
+ }
+
+ $this->displayOptions['fileLinkFormat'] = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l';
+ }
+
+ /**
+ * Enables/disables colored output.
+ *
+ * @param bool $colors
+ */
+ public function setColors($colors)
+ {
+ $this->colors = (bool) $colors;
+ }
+
+ /**
+ * Sets the maximum number of characters per line for dumped strings.
+ *
+ * @param int $maxStringWidth
+ */
+ public function setMaxStringWidth($maxStringWidth)
+ {
+ $this->maxStringWidth = (int) $maxStringWidth;
+ }
+
+ /**
+ * Configures styles.
+ *
+ * @param array $styles A map of style names to style definitions
+ */
+ public function setStyles(array $styles)
+ {
+ $this->styles = $styles + $this->styles;
+ }
+
+ /**
+ * Configures display options.
+ *
+ * @param array $displayOptions A map of display options to customize the behavior
+ */
+ public function setDisplayOptions(array $displayOptions)
+ {
+ $this->displayOptions = $displayOptions + $this->displayOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dumpScalar(Cursor $cursor, $type, $value)
+ {
+ $this->dumpKey($cursor);
+
+ $style = 'const';
+ $attr = $cursor->attr;
+
+ switch ($type) {
+ case 'default':
+ $style = 'default';
+ break;
+
+ case 'integer':
+ $style = 'num';
+ break;
+
+ case 'double':
+ $style = 'num';
+
+ switch (true) {
+ case INF === $value: $value = 'INF'; break;
+ case -INF === $value: $value = '-INF'; break;
+ case is_nan($value): $value = 'NAN'; break;
+ default:
+ $value = (string) $value;
+ if (false === strpos($value, $this->decimalPoint)) {
+ $value .= $this->decimalPoint.'0';
+ }
+ break;
+ }
+ break;
+
+ case 'NULL':
+ $value = 'null';
+ break;
+
+ case 'boolean':
+ $value = $value ? 'true' : 'false';
+ break;
+
+ default:
+ $attr += ['value' => $this->utf8Encode($value)];
+ $value = $this->utf8Encode($type);
+ break;
+ }
+
+ $this->line .= $this->style($style, $value, $attr);
+
+ $this->endValue($cursor);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dumpString(Cursor $cursor, $str, $bin, $cut)
+ {
+ $this->dumpKey($cursor);
+ $attr = $cursor->attr;
+
+ if ($bin) {
+ $str = $this->utf8Encode($str);
+ }
+ if ('' === $str) {
+ $this->line .= '""';
+ $this->endValue($cursor);
+ } else {
+ $attr += [
+ 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
+ 'binary' => $bin,
+ ];
+ $str = explode("\n", $str);
+ if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) {
+ unset($str[1]);
+ $str[0] .= "\n";
+ }
+ $m = \count($str) - 1;
+ $i = $lineCut = 0;
+
+ if (self::DUMP_STRING_LENGTH & $this->flags) {
+ $this->line .= '('.$attr['length'].') ';
+ }
+ if ($bin) {
+ $this->line .= 'b';
+ }
+
+ if ($m) {
+ $this->line .= '"""';
+ $this->dumpLine($cursor->depth);
+ } else {
+ $this->line .= '"';
+ }
+
+ foreach ($str as $str) {
+ if ($i < $m) {
+ $str .= "\n";
+ }
+ if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = mb_strlen($str, 'UTF-8')) {
+ $str = mb_substr($str, 0, $this->maxStringWidth, 'UTF-8');
+ $lineCut = $len - $this->maxStringWidth;
+ }
+ if ($m && 0 < $cursor->depth) {
+ $this->line .= $this->indentPad;
+ }
+ if ('' !== $str) {
+ $this->line .= $this->style('str', $str, $attr);
+ }
+ if ($i++ == $m) {
+ if ($m) {
+ if ('' !== $str) {
+ $this->dumpLine($cursor->depth);
+ if (0 < $cursor->depth) {
+ $this->line .= $this->indentPad;
+ }
+ }
+ $this->line .= '"""';
+ } else {
+ $this->line .= '"';
+ }
+ if ($cut < 0) {
+ $this->line .= '…';
+ $lineCut = 0;
+ } elseif ($cut) {
+ $lineCut += $cut;
+ }
+ }
+ if ($lineCut) {
+ $this->line .= '…'.$lineCut;
+ $lineCut = 0;
+ }
+
+ if ($i > $m) {
+ $this->endValue($cursor);
+ } else {
+ $this->dumpLine($cursor->depth);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enterHash(Cursor $cursor, $type, $class, $hasChild)
+ {
+ $this->dumpKey($cursor);
+ $attr = $cursor->attr;
+
+ if ($this->collapseNextHash) {
+ $cursor->skipChildren = true;
+ $this->collapseNextHash = $hasChild = false;
+ }
+
+ $class = $this->utf8Encode($class);
+ if (Cursor::HASH_OBJECT === $type) {
+ $prefix = $class && 'stdClass' !== $class ? $this->style('note', $class, $attr).(empty($attr['cut_hash']) ? ' {' : '') : '{';
+ } elseif (Cursor::HASH_RESOURCE === $type) {
+ $prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' ');
+ } else {
+ $unstyledPrefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? 'array:'.$class : '';
+ $prefix = $this->style('note', $unstyledPrefix, $attr).($unstyledPrefix ? ' [' : '[');
+ }
+
+ if (($cursor->softRefCount || 0 < $cursor->softRefHandle) && empty($attr['cut_hash'])) {
+ $prefix .= $this->style('ref', (Cursor::HASH_RESOURCE === $type ? '@' : '#').(0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->softRefTo), ['count' => $cursor->softRefCount]);
+ } elseif ($cursor->hardRefTo && !$cursor->refIndex && $class) {
+ $prefix .= $this->style('ref', '&'.$cursor->hardRefTo, ['count' => $cursor->hardRefCount]);
+ } elseif (!$hasChild && Cursor::HASH_RESOURCE === $type) {
+ $prefix = substr($prefix, 0, -1);
+ }
+
+ $this->line .= $prefix;
+
+ if ($hasChild) {
+ $this->dumpLine($cursor->depth);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut)
+ {
+ if (empty($cursor->attr['cut_hash'])) {
+ $this->dumpEllipsis($cursor, $hasChild, $cut);
+ $this->line .= Cursor::HASH_OBJECT === $type ? '}' : (Cursor::HASH_RESOURCE !== $type ? ']' : ($hasChild ? '}' : ''));
+ }
+
+ $this->endValue($cursor);
+ }
+
+ /**
+ * Dumps an ellipsis for cut children.
+ *
+ * @param bool $hasChild When the dump of the hash has child item
+ * @param int $cut The number of items the hash has been cut by
+ */
+ protected function dumpEllipsis(Cursor $cursor, $hasChild, $cut)
+ {
+ if ($cut) {
+ $this->line .= ' …';
+ if (0 < $cut) {
+ $this->line .= $cut;
+ }
+ if ($hasChild) {
+ $this->dumpLine($cursor->depth + 1);
+ }
+ }
+ }
+
+ /**
+ * Dumps a key in a hash structure.
+ */
+ protected function dumpKey(Cursor $cursor)
+ {
+ if (null !== $key = $cursor->hashKey) {
+ if ($cursor->hashKeyIsBinary) {
+ $key = $this->utf8Encode($key);
+ }
+ $attr = ['binary' => $cursor->hashKeyIsBinary];
+ $bin = $cursor->hashKeyIsBinary ? 'b' : '';
+ $style = 'key';
+ switch ($cursor->hashType) {
+ default:
+ case Cursor::HASH_INDEXED:
+ if (self::DUMP_LIGHT_ARRAY & $this->flags) {
+ break;
+ }
+ $style = 'index';
+ // no break
+ case Cursor::HASH_ASSOC:
+ if (\is_int($key)) {
+ $this->line .= $this->style($style, $key).' => ';
+ } else {
+ $this->line .= $bin.'"'.$this->style($style, $key).'" => ';
+ }
+ break;
+
+ case Cursor::HASH_RESOURCE:
+ $key = "\0~\0".$key;
+ // no break
+ case Cursor::HASH_OBJECT:
+ if (!isset($key[0]) || "\0" !== $key[0]) {
+ $this->line .= '+'.$bin.$this->style('public', $key).': ';
+ } elseif (0 < strpos($key, "\0", 1)) {
+ $key = explode("\0", substr($key, 1), 2);
+
+ switch ($key[0][0]) {
+ case '+': // User inserted keys
+ $attr['dynamic'] = true;
+ $this->line .= '+'.$bin.'"'.$this->style('public', $key[1], $attr).'": ';
+ break 2;
+ case '~':
+ $style = 'meta';
+ if (isset($key[0][1])) {
+ parse_str(substr($key[0], 1), $attr);
+ $attr += ['binary' => $cursor->hashKeyIsBinary];
+ }
+ break;
+ case '*':
+ $style = 'protected';
+ $bin = '#'.$bin;
+ break;
+ default:
+ $attr['class'] = $key[0];
+ $style = 'private';
+ $bin = '-'.$bin;
+ break;
+ }
+
+ if (isset($attr['collapse'])) {
+ if ($attr['collapse']) {
+ $this->collapseNextHash = true;
+ } else {
+ $this->expandNextHash = true;
+ }
+ }
+
+ $this->line .= $bin.$this->style($style, $key[1], $attr).(isset($attr['separator']) ? $attr['separator'] : ': ');
+ } else {
+ // This case should not happen
+ $this->line .= '-'.$bin.'"'.$this->style('private', $key, ['class' => '']).'": ';
+ }
+ break;
+ }
+
+ if ($cursor->hardRefTo) {
+ $this->line .= $this->style('ref', '&'.($cursor->hardRefCount ? $cursor->hardRefTo : ''), ['count' => $cursor->hardRefCount]).' ';
+ }
+ }
+ }
+
+ /**
+ * Decorates a value with some style.
+ *
+ * @param string $style The type of style being applied
+ * @param string $value The value being styled
+ * @param array $attr Optional context information
+ *
+ * @return string The value with style decoration
+ */
+ protected function style($style, $value, $attr = [])
+ {
+ if (null === $this->colors) {
+ $this->colors = $this->supportsColors();
+ }
+
+ if (null === $this->handlesHrefGracefully) {
+ $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && !getenv('KONSOLE_VERSION');
+ }
+
+ if (isset($attr['ellipsis'], $attr['ellipsis-type'])) {
+ $prefix = substr($value, 0, -$attr['ellipsis']);
+ if ('cli' === \PHP_SAPI && 'path' === $attr['ellipsis-type'] && isset($_SERVER[$pwd = '\\' === \DIRECTORY_SEPARATOR ? 'CD' : 'PWD']) && 0 === strpos($prefix, $_SERVER[$pwd])) {
+ $prefix = '.'.substr($prefix, \strlen($_SERVER[$pwd]));
+ }
+ if (!empty($attr['ellipsis-tail'])) {
+ $prefix .= substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']);
+ $value = substr($value, -$attr['ellipsis'] + $attr['ellipsis-tail']);
+ } else {
+ $value = substr($value, -$attr['ellipsis']);
+ }
+
+ $value = $this->style('default', $prefix).$this->style($style, $value);
+
+ goto href;
+ }
+
+ $map = static::$controlCharsMap;
+ $startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : '';
+ $endCchr = $this->colors ? "\033[m\033[{$this->styles[$style]}m" : '';
+ $value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) {
+ $s = $startCchr;
+ $c = $c[$i = 0];
+ do {
+ $s .= isset($map[$c[$i]]) ? $map[$c[$i]] : sprintf('\x%02X', \ord($c[$i]));
+ } while (isset($c[++$i]));
+
+ return $s.$endCchr;
+ }, $value, -1, $cchrCount);
+
+ if ($this->colors) {
+ if ($cchrCount && "\033" === $value[0]) {
+ $value = substr($value, \strlen($startCchr));
+ } else {
+ $value = "\033[{$this->styles[$style]}m".$value;
+ }
+ if ($cchrCount && $endCchr === substr($value, -\strlen($endCchr))) {
+ $value = substr($value, 0, -\strlen($endCchr));
+ } else {
+ $value .= "\033[{$this->styles['default']}m";
+ }
+ }
+
+ href:
+ if ($this->colors && $this->handlesHrefGracefully) {
+ if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
+ if ('note' === $style) {
+ $value .= "\033]8;;{$href}\033\\^\033]8;;\033\\";
+ } else {
+ $attr['href'] = $href;
+ }
+ }
+ if (isset($attr['href'])) {
+ $value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\";
+ }
+ } elseif ($attr['if_links'] ?? false) {
+ return '';
+ }
+
+ return $value;
+ }
+
+ /**
+ * @return bool Tells if the current output stream supports ANSI colors or not
+ */
+ protected function supportsColors()
+ {
+ if ($this->outputStream !== static::$defaultOutput) {
+ return $this->hasColorSupport($this->outputStream);
+ }
+ if (null !== static::$defaultColors) {
+ return static::$defaultColors;
+ }
+ if (isset($_SERVER['argv'][1])) {
+ $colors = $_SERVER['argv'];
+ $i = \count($colors);
+ while (--$i > 0) {
+ if (isset($colors[$i][5])) {
+ switch ($colors[$i]) {
+ case '--ansi':
+ case '--color':
+ case '--color=yes':
+ case '--color=force':
+ case '--color=always':
+ return static::$defaultColors = true;
+
+ case '--no-ansi':
+ case '--color=no':
+ case '--color=none':
+ case '--color=never':
+ return static::$defaultColors = false;
+ }
+ }
+ }
+ }
+
+ $h = stream_get_meta_data($this->outputStream) + ['wrapper_type' => null];
+ $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'wb') : $this->outputStream;
+
+ return static::$defaultColors = $this->hasColorSupport($h);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function dumpLine($depth, $endOfValue = false)
+ {
+ if ($this->colors) {
+ $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line);
+ }
+ parent::dumpLine($depth);
+ }
+
+ protected function endValue(Cursor $cursor)
+ {
+ if (-1 === $cursor->hashType) {
+ return;
+ }
+
+ if (Stub::ARRAY_INDEXED === $cursor->hashType || Stub::ARRAY_ASSOC === $cursor->hashType) {
+ if (self::DUMP_TRAILING_COMMA & $this->flags && 0 < $cursor->depth) {
+ $this->line .= ',';
+ } elseif (self::DUMP_COMMA_SEPARATOR & $this->flags && 1 < $cursor->hashLength - $cursor->hashIndex) {
+ $this->line .= ',';
+ }
+ }
+
+ $this->dumpLine($cursor->depth, true);
+ }
+
+ /**
+ * Returns true if the stream supports colorization.
+ *
+ * Reference: Composer\XdebugHandler\Process::supportsColor
+ * https://github.com/composer/xdebug-handler
+ *
+ * @param mixed $stream A CLI output stream
+ */
+ private function hasColorSupport($stream): bool
+ {
+ if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
+ return false;
+ }
+
+ // Follow https://no-color.org/
+ if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
+ return false;
+ }
+
+ if ('Hyper' === getenv('TERM_PROGRAM')) {
+ return true;
+ }
+
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return (\function_exists('sapi_windows_vt100_support')
+ && @sapi_windows_vt100_support($stream))
+ || false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ || 'xterm' === getenv('TERM');
+ }
+
+ if (\function_exists('stream_isatty')) {
+ return @stream_isatty($stream);
+ }
+
+ if (\function_exists('posix_isatty')) {
+ return @posix_isatty($stream);
+ }
+
+ $stat = @fstat($stream);
+ // Check if formatted mode is S_IFCHR
+ return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
+ }
+
+ /**
+ * Returns true if the Windows terminal supports true color.
+ *
+ * Note that this does not check an output stream, but relies on environment
+ * variables from known implementations, or a PHP and Windows version that
+ * supports true color.
+ */
+ private function isWindowsTrueColor(): bool
+ {
+ $result = 183 <= getenv('ANSICON_VER')
+ || 'ON' === getenv('ConEmuANSI')
+ || 'xterm' === getenv('TERM')
+ || 'Hyper' === getenv('TERM_PROGRAM');
+
+ if (!$result && \PHP_VERSION_ID >= 70200) {
+ $version = sprintf(
+ '%s.%s.%s',
+ PHP_WINDOWS_VERSION_MAJOR,
+ PHP_WINDOWS_VERSION_MINOR,
+ PHP_WINDOWS_VERSION_BUILD
+ );
+ $result = $version >= '10.0.15063';
+ }
+
+ return $result;
+ }
+
+ private function getSourceLink(string $file, int $line)
+ {
+ if ($fmt = $this->displayOptions['fileLinkFormat']) {
+ return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file.'#L'.$line);
+ }
+
+ return false;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php b/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php
new file mode 100644
index 0000000..ef2ae3b
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
+
+/**
+ * Tries to provide context on CLI.
+ *
+ * @author Maxime Steinhausser
+ */
+final class CliContextProvider implements ContextProviderInterface
+{
+ public function getContext(): ?array
+ {
+ if ('cli' !== \PHP_SAPI) {
+ return null;
+ }
+
+ return [
+ 'command_line' => $commandLine = implode(' ', $_SERVER['argv'] ?? []),
+ 'identifier' => hash('crc32b', $commandLine.$_SERVER['REQUEST_TIME_FLOAT']),
+ ];
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php b/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php
new file mode 100644
index 0000000..136a5a8
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
+
+/**
+ * Interface to provide contextual data about dump data clones sent to a server.
+ *
+ * @author Maxime Steinhausser
+ */
+interface ContextProviderInterface
+{
+ /**
+ * @return array|null Context data or null if unable to provide any context
+ */
+ public function getContext(): ?array;
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php b/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php
new file mode 100644
index 0000000..6d813a3
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
+
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\VarDumper\Caster\ReflectionCaster;
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+
+/**
+ * Tries to provide context from a request.
+ *
+ * @author Maxime Steinhausser
+ */
+final class RequestContextProvider implements ContextProviderInterface
+{
+ private $requestStack;
+ private $cloner;
+
+ public function __construct(RequestStack $requestStack)
+ {
+ $this->requestStack = $requestStack;
+ $this->cloner = new VarCloner();
+ $this->cloner->setMaxItems(0);
+ $this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
+ }
+
+ public function getContext(): ?array
+ {
+ if (null === $request = $this->requestStack->getCurrentRequest()) {
+ return null;
+ }
+
+ $controller = $request->attributes->get('_controller');
+
+ return [
+ 'uri' => $request->getUri(),
+ 'method' => $request->getMethod(),
+ 'controller' => $controller ? $this->cloner->cloneVar($controller) : $controller,
+ 'identifier' => spl_object_hash($request),
+ ];
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php b/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php
new file mode 100644
index 0000000..0249583
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php
@@ -0,0 +1,126 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
+
+use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+use Symfony\Component\VarDumper\Dumper\HtmlDumper;
+use Symfony\Component\VarDumper\VarDumper;
+use Twig\Template;
+
+/**
+ * Tries to provide context from sources (class name, file, line, code excerpt, ...).
+ *
+ * @author Nicolas Grekas
+ * @author Maxime Steinhausser
+ */
+final class SourceContextProvider implements ContextProviderInterface
+{
+ private $limit;
+ private $charset;
+ private $projectDir;
+ private $fileLinkFormatter;
+
+ public function __construct(string $charset = null, string $projectDir = null, FileLinkFormatter $fileLinkFormatter = null, int $limit = 9)
+ {
+ $this->charset = $charset;
+ $this->projectDir = $projectDir;
+ $this->fileLinkFormatter = $fileLinkFormatter;
+ $this->limit = $limit;
+ }
+
+ public function getContext(): ?array
+ {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit);
+
+ $file = $trace[1]['file'];
+ $line = $trace[1]['line'];
+ $name = false;
+ $fileExcerpt = false;
+
+ for ($i = 2; $i < $this->limit; ++$i) {
+ if (isset($trace[$i]['class'], $trace[$i]['function'])
+ && 'dump' === $trace[$i]['function']
+ && VarDumper::class === $trace[$i]['class']
+ ) {
+ $file = $trace[$i]['file'] ?? $file;
+ $line = $trace[$i]['line'] ?? $line;
+
+ while (++$i < $this->limit) {
+ if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) {
+ $file = $trace[$i]['file'];
+ $line = $trace[$i]['line'];
+
+ break;
+ } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof Template) {
+ $template = $trace[$i]['object'];
+ $name = $template->getTemplateName();
+ $src = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : false);
+ $info = $template->getDebugInfo();
+ if (isset($info[$trace[$i - 1]['line']])) {
+ $line = $info[$trace[$i - 1]['line']];
+ $file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null;
+
+ if ($src) {
+ $src = explode("\n", $src);
+ $fileExcerpt = [];
+
+ for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) {
+ $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).'
';
+ }
+
+ $fileExcerpt = ''.implode("\n", $fileExcerpt).' ';
+ }
+ }
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (false === $name) {
+ $name = str_replace('\\', '/', $file);
+ $name = substr($name, strrpos($name, '/') + 1);
+ }
+
+ $context = ['name' => $name, 'file' => $file, 'line' => $line];
+ $context['file_excerpt'] = $fileExcerpt;
+
+ if (null !== $this->projectDir) {
+ $context['project_dir'] = $this->projectDir;
+ if (0 === strpos($file, $this->projectDir)) {
+ $context['file_relative'] = ltrim(substr($file, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
+ }
+ }
+
+ if ($this->fileLinkFormatter && $fileLink = $this->fileLinkFormatter->format($context['file'], $context['line'])) {
+ $context['file_link'] = $fileLink;
+ }
+
+ return $context;
+ }
+
+ private function htmlEncode(string $s): string
+ {
+ $html = '';
+
+ $dumper = new HtmlDumper(function ($line) use (&$html) { $html .= $line; }, $this->charset);
+ $dumper->setDumpHeader('');
+ $dumper->setDumpBoundaries('', '');
+
+ $cloner = new VarCloner();
+ $dumper->dump($cloner->cloneVar($s));
+
+ return substr(strip_tags($html), 1, -1);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php b/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php
new file mode 100644
index 0000000..3234ab8
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
+
+/**
+ * @author Kévin Thérage
+ */
+class ContextualizedDumper implements DataDumperInterface
+{
+ private $wrappedDumper;
+ private $contextProviders;
+
+ /**
+ * @param ContextProviderInterface[] $contextProviders
+ */
+ public function __construct(DataDumperInterface $wrappedDumper, array $contextProviders)
+ {
+ $this->wrappedDumper = $wrappedDumper;
+ $this->contextProviders = $contextProviders;
+ }
+
+ public function dump(Data $data)
+ {
+ $context = [];
+ foreach ($this->contextProviders as $contextProvider) {
+ $context[\get_class($contextProvider)] = $contextProvider->getContext();
+ }
+
+ $this->wrappedDumper->dump($data->withContext($context));
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php b/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php
new file mode 100644
index 0000000..cfd81e0
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * DataDumperInterface for dumping Data objects.
+ *
+ * @author Nicolas Grekas
+ */
+interface DataDumperInterface
+{
+ public function dump(Data $data);
+}
diff --git a/vendor/symfony/var-dumper/Dumper/HtmlDumper.php b/vendor/symfony/var-dumper/Dumper/HtmlDumper.php
new file mode 100644
index 0000000..16224c9
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/HtmlDumper.php
@@ -0,0 +1,1004 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Cursor;
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * HtmlDumper dumps variables as HTML.
+ *
+ * @author Nicolas Grekas
+ */
+class HtmlDumper extends CliDumper
+{
+ public static $defaultOutput = 'php://output';
+
+ protected static $themes = [
+ 'dark' => [
+ 'default' => 'background-color:#18171B; color:#FF8400; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all',
+ 'num' => 'font-weight:bold; color:#1299DA',
+ 'const' => 'font-weight:bold',
+ 'str' => 'font-weight:bold; color:#56DB3A',
+ 'note' => 'color:#1299DA',
+ 'ref' => 'color:#A0A0A0',
+ 'public' => 'color:#FFFFFF',
+ 'protected' => 'color:#FFFFFF',
+ 'private' => 'color:#FFFFFF',
+ 'meta' => 'color:#B729D9',
+ 'key' => 'color:#56DB3A',
+ 'index' => 'color:#1299DA',
+ 'ellipsis' => 'color:#FF8400',
+ 'ns' => 'user-select:none;',
+ ],
+ 'light' => [
+ 'default' => 'background:none; color:#CC7832; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all',
+ 'num' => 'font-weight:bold; color:#1299DA',
+ 'const' => 'font-weight:bold',
+ 'str' => 'font-weight:bold; color:#629755;',
+ 'note' => 'color:#6897BB',
+ 'ref' => 'color:#6E6E6E',
+ 'public' => 'color:#262626',
+ 'protected' => 'color:#262626',
+ 'private' => 'color:#262626',
+ 'meta' => 'color:#B729D9',
+ 'key' => 'color:#789339',
+ 'index' => 'color:#1299DA',
+ 'ellipsis' => 'color:#CC7832',
+ 'ns' => 'user-select:none;',
+ ],
+ ];
+
+ protected $dumpHeader;
+ protected $dumpPrefix = '
';
+ protected $dumpSuffix = ' ';
+ protected $dumpId = 'sf-dump';
+ protected $colors = true;
+ protected $headerIsDumped = false;
+ protected $lastDepth = -1;
+ protected $styles;
+
+ private $displayOptions = [
+ 'maxDepth' => 1,
+ 'maxStringLength' => 160,
+ 'fileLinkFormat' => null,
+ ];
+ private $extraDisplayOptions = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($output = null, string $charset = null, int $flags = 0)
+ {
+ AbstractDumper::__construct($output, $charset, $flags);
+ $this->dumpId = 'sf-dump-'.mt_rand();
+ $this->displayOptions['fileLinkFormat'] = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
+ $this->styles = static::$themes['dark'] ?? self::$themes['dark'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStyles(array $styles)
+ {
+ $this->headerIsDumped = false;
+ $this->styles = $styles + $this->styles;
+ }
+
+ public function setTheme(string $themeName)
+ {
+ if (!isset(static::$themes[$themeName])) {
+ throw new \InvalidArgumentException(sprintf('Theme "%s" does not exist in class "%s".', $themeName, static::class));
+ }
+
+ $this->setStyles(static::$themes[$themeName]);
+ }
+
+ /**
+ * Configures display options.
+ *
+ * @param array $displayOptions A map of display options to customize the behavior
+ */
+ public function setDisplayOptions(array $displayOptions)
+ {
+ $this->headerIsDumped = false;
+ $this->displayOptions = $displayOptions + $this->displayOptions;
+ }
+
+ /**
+ * Sets an HTML header that will be dumped once in the output stream.
+ *
+ * @param string $header An HTML string
+ */
+ public function setDumpHeader($header)
+ {
+ $this->dumpHeader = $header;
+ }
+
+ /**
+ * Sets an HTML prefix and suffix that will encapse every single dump.
+ *
+ * @param string $prefix The prepended HTML string
+ * @param string $suffix The appended HTML string
+ */
+ public function setDumpBoundaries($prefix, $suffix)
+ {
+ $this->dumpPrefix = $prefix;
+ $this->dumpSuffix = $suffix;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dump(Data $data, $output = null, array $extraDisplayOptions = [])
+ {
+ $this->extraDisplayOptions = $extraDisplayOptions;
+ $result = parent::dump($data, $output);
+ $this->dumpId = 'sf-dump-'.mt_rand();
+
+ return $result;
+ }
+
+ /**
+ * Dumps the HTML header.
+ */
+ protected function getDumpHeader()
+ {
+ $this->headerIsDumped = null !== $this->outputStream ? $this->outputStream : $this->lineDumper;
+
+ if (null !== $this->dumpHeader) {
+ return $this->dumpHeader;
+ }
+
+ $line = str_replace('{$options}', json_encode($this->displayOptions, JSON_FORCE_OBJECT), <<<'EOHTML'
+'.$this->dumpHeader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dumpString(Cursor $cursor, $str, $bin, $cut)
+ {
+ if ('' === $str && isset($cursor->attr['img-data'], $cursor->attr['content-type'])) {
+ $this->dumpKey($cursor);
+ $this->line .= $this->style('default', $cursor->attr['img-size'] ?? '', []).' ';
+ $this->endValue($cursor);
+ $this->line .= $this->indentPad;
+ $this->line .= sprintf(' ', $cursor->attr['content-type'], base64_encode($cursor->attr['img-data']));
+ $this->endValue($cursor);
+ } else {
+ parent::dumpString($cursor, $str, $bin, $cut);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enterHash(Cursor $cursor, $type, $class, $hasChild)
+ {
+ if (Cursor::HASH_OBJECT === $type) {
+ $cursor->attr['depth'] = $cursor->depth;
+ }
+ parent::enterHash($cursor, $type, $class, false);
+
+ if ($cursor->skipChildren) {
+ $cursor->skipChildren = false;
+ $eol = ' class=sf-dump-compact>';
+ } elseif ($this->expandNextHash) {
+ $this->expandNextHash = false;
+ $eol = ' class=sf-dump-expanded>';
+ } else {
+ $eol = '>';
+ }
+
+ if ($hasChild) {
+ $this->line .= 'refIndex) {
+ $r = Cursor::HASH_OBJECT !== $type ? 1 - (Cursor::HASH_RESOURCE !== $type) : 2;
+ $r .= $r && 0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->refIndex;
+
+ $this->line .= sprintf(' id=%s-ref%s', $this->dumpId, $r);
+ }
+ $this->line .= $eol;
+ $this->dumpLine($cursor->depth);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut)
+ {
+ $this->dumpEllipsis($cursor, $hasChild, $cut);
+ if ($hasChild) {
+ $this->line .= ' ';
+ }
+ parent::leaveHash($cursor, $type, $class, $hasChild, 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function style($style, $value, $attr = [])
+ {
+ if ('' === $value) {
+ return '';
+ }
+
+ $v = esc($value);
+
+ if ('ref' === $style) {
+ if (empty($attr['count'])) {
+ return sprintf('%s ', $v);
+ }
+ $r = ('#' !== $v[0] ? 1 - ('@' !== $v[0]) : 2).substr($value, 1);
+
+ return sprintf('%s ', $this->dumpId, $r, 1 + $attr['count'], $v);
+ }
+
+ if ('const' === $style && isset($attr['value'])) {
+ $style .= sprintf(' title="%s"', esc(is_scalar($attr['value']) ? $attr['value'] : json_encode($attr['value'])));
+ } elseif ('public' === $style) {
+ $style .= sprintf(' title="%s"', empty($attr['dynamic']) ? 'Public property' : 'Runtime added dynamic property');
+ } elseif ('str' === $style && 1 < $attr['length']) {
+ $style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : '');
+ } elseif ('note' === $style && 0 < ($attr['depth'] ?? 0) && false !== $c = strrpos($value, '\\')) {
+ $style .= ' title=""';
+ $attr += [
+ 'ellipsis' => \strlen($value) - $c,
+ 'ellipsis-type' => 'note',
+ 'ellipsis-tail' => 1,
+ ];
+ } elseif ('protected' === $style) {
+ $style .= ' title="Protected property"';
+ } elseif ('meta' === $style && isset($attr['title'])) {
+ $style .= sprintf(' title="%s"', esc($this->utf8Encode($attr['title'])));
+ } elseif ('private' === $style) {
+ $style .= sprintf(' title="Private property defined in class:
`%s`"', esc($this->utf8Encode($attr['class'])));
+ }
+ $map = static::$controlCharsMap;
+
+ if (isset($attr['ellipsis'])) {
+ $class = 'sf-dump-ellipsis';
+ if (isset($attr['ellipsis-type'])) {
+ $class = sprintf('"%s sf-dump-ellipsis-%s"', $class, $attr['ellipsis-type']);
+ }
+ $label = esc(substr($value, -$attr['ellipsis']));
+ $style = str_replace(' title="', " title=\"$v\n", $style);
+ $v = sprintf('%s ', $class, substr($v, 0, -\strlen($label)));
+
+ if (!empty($attr['ellipsis-tail'])) {
+ $tail = \strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail'])));
+ $v .= sprintf('%s %s', $class, substr($label, 0, $tail), substr($label, $tail));
+ } else {
+ $v .= $label;
+ }
+ }
+
+ $v = "".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) {
+ $s = $b = '';
+ }, $v).' ';
+
+ if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
+ $attr['href'] = $href;
+ }
+ if (isset($attr['href'])) {
+ $target = isset($attr['file']) ? '' : ' target="_blank"';
+ $v = sprintf('%s ', esc($this->utf8Encode($attr['href'])), $target, $v);
+ }
+ if (isset($attr['lang'])) {
+ $v = sprintf('%s
', esc($attr['lang']), $v);
+ }
+
+ return $v;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function dumpLine($depth, $endOfValue = false)
+ {
+ if (-1 === $this->lastDepth) {
+ $this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line;
+ }
+ if ($this->headerIsDumped !== (null !== $this->outputStream ? $this->outputStream : $this->lineDumper)) {
+ $this->line = $this->getDumpHeader().$this->line;
+ }
+
+ if (-1 === $depth) {
+ $args = ['"'.$this->dumpId.'"'];
+ if ($this->extraDisplayOptions) {
+ $args[] = json_encode($this->extraDisplayOptions, JSON_FORCE_OBJECT);
+ }
+ // Replace is for BC
+ $this->line .= sprintf(str_replace('"%s"', '%s', $this->dumpSuffix), implode(', ', $args));
+ }
+ $this->lastDepth = $depth;
+
+ $this->line = mb_convert_encoding($this->line, 'HTML-ENTITIES', 'UTF-8');
+
+ if (-1 === $depth) {
+ AbstractDumper::dumpLine(0);
+ }
+ AbstractDumper::dumpLine($depth);
+ }
+
+ private function getSourceLink(string $file, int $line)
+ {
+ $options = $this->extraDisplayOptions + $this->displayOptions;
+
+ if ($fmt = $options['fileLinkFormat']) {
+ return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
+ }
+
+ return false;
+ }
+}
+
+function esc($str)
+{
+ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
+}
diff --git a/vendor/symfony/var-dumper/Dumper/ServerDumper.php b/vendor/symfony/var-dumper/Dumper/ServerDumper.php
new file mode 100644
index 0000000..9b452f4
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/ServerDumper.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Dumper;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
+use Symfony\Component\VarDumper\Server\Connection;
+
+/**
+ * ServerDumper forwards serialized Data clones to a server.
+ *
+ * @author Maxime Steinhausser
+ */
+class ServerDumper implements DataDumperInterface
+{
+ private $connection;
+ private $wrappedDumper;
+
+ /**
+ * @param string $host The server host
+ * @param DataDumperInterface|null $wrappedDumper A wrapped instance used whenever we failed contacting the server
+ * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name
+ */
+ public function __construct(string $host, DataDumperInterface $wrappedDumper = null, array $contextProviders = [])
+ {
+ $this->connection = new Connection($host, $contextProviders);
+ $this->wrappedDumper = $wrappedDumper;
+ }
+
+ public function getContextProviders(): array
+ {
+ return $this->connection->getContextProviders();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dump(Data $data)
+ {
+ if (!$this->connection->write($data) && $this->wrappedDumper) {
+ $this->wrappedDumper->dump($data);
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php b/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php
new file mode 100644
index 0000000..1adfa7e
--- /dev/null
+++ b/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Exception;
+
+/**
+ * @author Nicolas Grekas
+ */
+class ThrowingCasterException extends \Exception
+{
+ /**
+ * @param \Throwable $prev The exception thrown from the caster
+ */
+ public function __construct(\Throwable $prev)
+ {
+ parent::__construct('Unexpected '.\get_class($prev).' thrown from a caster: '.$prev->getMessage(), 0, $prev);
+ }
+}
diff --git a/vendor/symfony/var-dumper/LICENSE b/vendor/symfony/var-dumper/LICENSE
new file mode 100644
index 0000000..0efd493
--- /dev/null
+++ b/vendor/symfony/var-dumper/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014-2020 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.
diff --git a/vendor/symfony/var-dumper/README.md b/vendor/symfony/var-dumper/README.md
new file mode 100644
index 0000000..4334698
--- /dev/null
+++ b/vendor/symfony/var-dumper/README.md
@@ -0,0 +1,15 @@
+VarDumper Component
+===================
+
+The VarDumper component provides mechanisms for walking through any arbitrary
+PHP variable. It provides a better `dump()` function that you can use instead
+of `var_dump`.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/var_dumper/introduction.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
diff --git a/vendor/symfony/var-dumper/Resources/bin/var-dump-server b/vendor/symfony/var-dumper/Resources/bin/var-dump-server
new file mode 100644
index 0000000..dbc705b
--- /dev/null
+++ b/vendor/symfony/var-dumper/Resources/bin/var-dump-server
@@ -0,0 +1,63 @@
+#!/usr/bin/env php
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Starts a dump server to collect and output dumps on a single place with multiple formats support.
+ *
+ * @author Maxime Steinhausser
+ */
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Input\ArgvInput;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Logger\ConsoleLogger;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\VarDumper\Command\ServerDumpCommand;
+use Symfony\Component\VarDumper\Server\DumpServer;
+
+function includeIfExists(string $file): bool
+{
+ return file_exists($file) && include $file;
+}
+
+if (
+ !includeIfExists(__DIR__ . '/../../../../autoload.php') &&
+ !includeIfExists(__DIR__ . '/../../vendor/autoload.php') &&
+ !includeIfExists(__DIR__ . '/../../../../../../vendor/autoload.php')
+) {
+ fwrite(STDERR, 'Install dependencies using Composer.'.PHP_EOL);
+ exit(1);
+}
+
+if (!class_exists(Application::class)) {
+ fwrite(STDERR, 'You need the "symfony/console" component in order to run the VarDumper server.'.PHP_EOL);
+ exit(1);
+}
+
+$input = new ArgvInput();
+$output = new ConsoleOutput();
+$defaultHost = '127.0.0.1:9912';
+$host = $input->getParameterOption(['--host'], $_SERVER['VAR_DUMPER_SERVER'] ?? $defaultHost, true);
+$logger = interface_exists(LoggerInterface::class) ? new ConsoleLogger($output->getErrorOutput()) : null;
+
+$app = new Application();
+
+$app->getDefinition()->addOption(
+ new InputOption('--host', null, InputOption::VALUE_REQUIRED, 'The address the server should listen to', $defaultHost)
+);
+
+$app->add($command = new ServerDumpCommand(new DumpServer($host, $logger)))
+ ->getApplication()
+ ->setDefaultCommand($command->getName(), true)
+ ->run($input, $output)
+;
diff --git a/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css b/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css
new file mode 100644
index 0000000..e271c27
--- /dev/null
+++ b/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css
@@ -0,0 +1,130 @@
+body {
+ display: flex;
+ flex-direction: column-reverse;
+ justify-content: flex-end;
+ max-width: 1140px;
+ margin: auto;
+ padding: 15px;
+ word-wrap: break-word;
+ background-color: #F9F9F9;
+ color: #222;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.4;
+}
+p {
+ margin: 0;
+}
+a {
+ color: #218BC3;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+.text-small {
+ font-size: 12px !important;
+}
+article {
+ margin: 5px;
+ margin-bottom: 10px;
+}
+article > header > .row {
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+ margin-bottom: 10px;
+}
+article > header > .row > .col {
+ flex: 1;
+ display: flex;
+ align-items: baseline;
+}
+article > header > .row > h2 {
+ font-size: 14px;
+ color: #222;
+ font-weight: normal;
+ font-family: "Lucida Console", monospace, sans-serif;
+ word-break: break-all;
+ margin: 20px 5px 0 0;
+ user-select: all;
+}
+article > header > .row > h2 > code {
+ white-space: nowrap;
+ user-select: none;
+ color: #cc2255;
+ background-color: #f7f7f9;
+ border: 1px solid #e1e1e8;
+ border-radius: 3px;
+ margin-right: 5px;
+ padding: 0 3px;
+}
+article > header > .row > time.col {
+ flex: 0;
+ text-align: right;
+ white-space: nowrap;
+ color: #999;
+ font-style: italic;
+}
+article > header ul.tags {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ font-size: 12px;
+}
+article > header ul.tags > li {
+ user-select: all;
+ margin-bottom: 2px;
+}
+article > header ul.tags > li > span.badge {
+ display: inline-block;
+ padding: .25em .4em;
+ margin-right: 5px;
+ border-radius: 4px;
+ background-color: #6c757d3b;
+ color: #524d4d;
+ font-size: 12px;
+ text-align: center;
+ font-weight: 700;
+ line-height: 1;
+ white-space: nowrap;
+ vertical-align: baseline;
+ user-select: none;
+}
+article > section.body {
+ border: 1px solid #d8d8d8;
+ background: #FFF;
+ padding: 10px;
+ border-radius: 3px;
+}
+pre.sf-dump {
+ border-radius: 3px;
+ margin-bottom: 0;
+}
+.hidden {
+ display: none !important;
+}
+.dumped-tag > .sf-dump {
+ display: inline-block;
+ margin: 0;
+ padding: 1px 5px;
+ line-height: 1.4;
+ vertical-align: top;
+ background-color: transparent;
+ user-select: auto;
+}
+.dumped-tag > pre.sf-dump,
+.dumped-tag > .sf-dump-default {
+ color: #CC7832;
+ background: none;
+}
+.dumped-tag > .sf-dump .sf-dump-str { color: #629755; }
+.dumped-tag > .sf-dump .sf-dump-private,
+.dumped-tag > .sf-dump .sf-dump-protected,
+.dumped-tag > .sf-dump .sf-dump-public { color: #262626; }
+.dumped-tag > .sf-dump .sf-dump-note { color: #6897BB; }
+.dumped-tag > .sf-dump .sf-dump-key { color: #789339; }
+.dumped-tag > .sf-dump .sf-dump-ref { color: #6E6E6E; }
+.dumped-tag > .sf-dump .sf-dump-ellipsis { color: #CC7832; max-width: 100em; }
+.dumped-tag > .sf-dump .sf-dump-ellipsis-path { max-width: 5em; }
+.dumped-tag > .sf-dump .sf-dump-ns { user-select: none; }
diff --git a/vendor/symfony/var-dumper/Resources/functions/dump.php b/vendor/symfony/var-dumper/Resources/functions/dump.php
new file mode 100644
index 0000000..eb5dbbc
--- /dev/null
+++ b/vendor/symfony/var-dumper/Resources/functions/dump.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Component\VarDumper\VarDumper;
+
+if (!function_exists('dump')) {
+ /**
+ * @author Nicolas Grekas
+ */
+ function dump($var, ...$moreVars)
+ {
+ VarDumper::dump($var);
+
+ foreach ($moreVars as $v) {
+ VarDumper::dump($v);
+ }
+
+ if (1 < func_num_args()) {
+ return func_get_args();
+ }
+
+ return $var;
+ }
+}
+
+if (!function_exists('dd')) {
+ function dd(...$vars)
+ {
+ foreach ($vars as $v) {
+ VarDumper::dump($v);
+ }
+
+ exit(1);
+ }
+}
diff --git a/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js b/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js
new file mode 100644
index 0000000..39bdbc7
--- /dev/null
+++ b/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js
@@ -0,0 +1,10 @@
+document.addEventListener('DOMContentLoaded', function() {
+ let prev = null;
+ Array.from(document.getElementsByTagName('article')).reverse().forEach(function (article) {
+ const dedupId = article.dataset.dedupId;
+ if (dedupId === prev) {
+ article.getElementsByTagName('header')[0].classList.add('hidden');
+ }
+ prev = dedupId;
+ });
+});
diff --git a/vendor/symfony/var-dumper/Server/Connection.php b/vendor/symfony/var-dumper/Server/Connection.php
new file mode 100644
index 0000000..f7ac2cc
--- /dev/null
+++ b/vendor/symfony/var-dumper/Server/Connection.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Server;
+
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
+
+/**
+ * Forwards serialized Data clones to a server.
+ *
+ * @author Maxime Steinhausser
+ */
+class Connection
+{
+ private $host;
+ private $contextProviders;
+ private $socket;
+
+ /**
+ * @param string $host The server host
+ * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name
+ */
+ public function __construct(string $host, array $contextProviders = [])
+ {
+ if (false === strpos($host, '://')) {
+ $host = 'tcp://'.$host;
+ }
+
+ $this->host = $host;
+ $this->contextProviders = $contextProviders;
+ }
+
+ public function getContextProviders(): array
+ {
+ return $this->contextProviders;
+ }
+
+ public function write(Data $data): bool
+ {
+ $socketIsFresh = !$this->socket;
+ if (!$this->socket = $this->socket ?: $this->createSocket()) {
+ return false;
+ }
+
+ $context = ['timestamp' => microtime(true)];
+ foreach ($this->contextProviders as $name => $provider) {
+ $context[$name] = $provider->getContext();
+ }
+ $context = array_filter($context);
+ $encodedPayload = base64_encode(serialize([$data, $context]))."\n";
+
+ set_error_handler([self::class, 'nullErrorHandler']);
+ try {
+ if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) {
+ return true;
+ }
+ if (!$socketIsFresh) {
+ stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
+ fclose($this->socket);
+ $this->socket = $this->createSocket();
+ }
+ if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) {
+ return true;
+ }
+ } finally {
+ restore_error_handler();
+ }
+
+ return false;
+ }
+
+ private static function nullErrorHandler($t, $m)
+ {
+ // no-op
+ }
+
+ private function createSocket()
+ {
+ set_error_handler([self::class, 'nullErrorHandler']);
+ try {
+ return stream_socket_client($this->host, $errno, $errstr, 3, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
+ } finally {
+ restore_error_handler();
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/Server/DumpServer.php b/vendor/symfony/var-dumper/Server/DumpServer.php
new file mode 100644
index 0000000..d6aa313
--- /dev/null
+++ b/vendor/symfony/var-dumper/Server/DumpServer.php
@@ -0,0 +1,107 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Server;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\VarDumper\Cloner\Data;
+use Symfony\Component\VarDumper\Cloner\Stub;
+
+/**
+ * A server collecting Data clones sent by a ServerDumper.
+ *
+ * @author Maxime Steinhausser
+ *
+ * @final
+ */
+class DumpServer
+{
+ private $host;
+ private $socket;
+ private $logger;
+
+ public function __construct(string $host, LoggerInterface $logger = null)
+ {
+ if (false === strpos($host, '://')) {
+ $host = 'tcp://'.$host;
+ }
+
+ $this->host = $host;
+ $this->logger = $logger;
+ }
+
+ public function start(): void
+ {
+ if (!$this->socket = stream_socket_server($this->host, $errno, $errstr)) {
+ throw new \RuntimeException(sprintf('Server start failed on "%s": ', $this->host).$errstr.' '.$errno);
+ }
+ }
+
+ public function listen(callable $callback): void
+ {
+ if (null === $this->socket) {
+ $this->start();
+ }
+
+ foreach ($this->getMessages() as $clientId => $message) {
+ $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]);
+
+ // Impossible to decode the message, give up.
+ if (false === $payload) {
+ if ($this->logger) {
+ $this->logger->warning('Unable to decode a message from {clientId} client.', ['clientId' => $clientId]);
+ }
+
+ continue;
+ }
+
+ if (!\is_array($payload) || \count($payload) < 2 || !$payload[0] instanceof Data || !\is_array($payload[1])) {
+ if ($this->logger) {
+ $this->logger->warning('Invalid payload from {clientId} client. Expected an array of two elements (Data $data, array $context)', ['clientId' => $clientId]);
+ }
+
+ continue;
+ }
+
+ list($data, $context) = $payload;
+
+ $callback($data, $context, $clientId);
+ }
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ private function getMessages(): iterable
+ {
+ $sockets = [(int) $this->socket => $this->socket];
+ $write = [];
+
+ while (true) {
+ $read = $sockets;
+ stream_select($read, $write, $write, null);
+
+ foreach ($read as $stream) {
+ if ($this->socket === $stream) {
+ $stream = stream_socket_accept($this->socket);
+ $sockets[(int) $stream] = $stream;
+ } elseif (feof($stream)) {
+ unset($sockets[(int) $stream]);
+ fclose($stream);
+ } else {
+ yield (int) $stream => fgets($stream);
+ }
+ }
+ }
+ }
+}
diff --git a/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php b/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php
new file mode 100644
index 0000000..247192b
--- /dev/null
+++ b/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper\Test;
+
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+use Symfony\Component\VarDumper\Dumper\CliDumper;
+
+/**
+ * @author Nicolas Grekas
+ */
+trait VarDumperTestTrait
+{
+ /**
+ * @internal
+ */
+ private $varDumperConfig = [
+ 'casters' => [],
+ 'flags' => null,
+ ];
+
+ protected function setUpVarDumper(array $casters, int $flags = null): void
+ {
+ $this->varDumperConfig['casters'] = $casters;
+ $this->varDumperConfig['flags'] = $flags;
+ }
+
+ /**
+ * @after
+ */
+ protected function tearDownVarDumper(): void
+ {
+ $this->varDumperConfig['casters'] = [];
+ $this->varDumperConfig['flags'] = null;
+ }
+
+ public function assertDumpEquals($expected, $data, $filter = 0, $message = '')
+ {
+ $this->assertSame($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message);
+ }
+
+ public function assertDumpMatchesFormat($expected, $data, $filter = 0, $message = '')
+ {
+ $this->assertStringMatchesFormat($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message);
+ }
+
+ /**
+ * @return string|null
+ */
+ protected function getDump($data, $key = null, $filter = 0)
+ {
+ if (null === $flags = $this->varDumperConfig['flags']) {
+ $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0;
+ $flags |= getenv('DUMP_STRING_LENGTH') ? CliDumper::DUMP_STRING_LENGTH : 0;
+ $flags |= getenv('DUMP_COMMA_SEPARATOR') ? CliDumper::DUMP_COMMA_SEPARATOR : 0;
+ }
+
+ $cloner = new VarCloner();
+ $cloner->addCasters($this->varDumperConfig['casters']);
+ $cloner->setMaxItems(-1);
+ $dumper = new CliDumper(null, null, $flags);
+ $dumper->setColors(false);
+ $data = $cloner->cloneVar($data, $filter)->withRefHandles(false);
+ if (null !== $key && null === $data = $data->seek($key)) {
+ return null;
+ }
+
+ return rtrim($dumper->dump($data, true));
+ }
+
+ private function prepareExpectation($expected, int $filter): string
+ {
+ if (!\is_string($expected)) {
+ $expected = $this->getDump($expected, null, $filter);
+ }
+
+ return rtrim($expected);
+ }
+}
diff --git a/vendor/symfony/var-dumper/VarDumper.php b/vendor/symfony/var-dumper/VarDumper.php
new file mode 100644
index 0000000..9497e97
--- /dev/null
+++ b/vendor/symfony/var-dumper/VarDumper.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\VarDumper;
+
+use Symfony\Component\VarDumper\Caster\ReflectionCaster;
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+use Symfony\Component\VarDumper\Dumper\CliDumper;
+use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
+use Symfony\Component\VarDumper\Dumper\ContextualizedDumper;
+use Symfony\Component\VarDumper\Dumper\HtmlDumper;
+
+// Load the global dump() function
+require_once __DIR__.'/Resources/functions/dump.php';
+
+/**
+ * @author Nicolas Grekas
+ */
+class VarDumper
+{
+ private static $handler;
+
+ public static function dump($var)
+ {
+ if (null === self::$handler) {
+ $cloner = new VarCloner();
+ $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
+
+ if (isset($_SERVER['VAR_DUMPER_FORMAT'])) {
+ $dumper = 'html' === $_SERVER['VAR_DUMPER_FORMAT'] ? new HtmlDumper() : new CliDumper();
+ } else {
+ $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg']) ? new CliDumper() : new HtmlDumper();
+ }
+
+ $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]);
+
+ self::$handler = function ($var) use ($cloner, $dumper) {
+ $dumper->dump($cloner->cloneVar($var));
+ };
+ }
+
+ return (self::$handler)($var);
+ }
+
+ public static function setHandler(callable $callable = null)
+ {
+ $prevHandler = self::$handler;
+ self::$handler = $callable;
+
+ return $prevHandler;
+ }
+}
diff --git a/vendor/symfony/var-dumper/composer.json b/vendor/symfony/var-dumper/composer.json
new file mode 100644
index 0000000..0fc710d
--- /dev/null
+++ b/vendor/symfony/var-dumper/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "symfony/var-dumper",
+ "type": "library",
+ "description": "Symfony mechanism for exploring and dumping PHP variables",
+ "keywords": ["dump", "debug"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php72": "~1.5",
+ "symfony/polyfill-php80": "^1.15"
+ },
+ "require-dev": {
+ "ext-iconv": "*",
+ "symfony/console": "^3.4|^4.0|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "twig/twig": "^1.34|^2.4|^3.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+ "symfony/console": "<3.4"
+ },
+ "suggest": {
+ "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
+ "ext-intl": "To show region name in time zone dump",
+ "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
+ },
+ "autoload": {
+ "files": [ "Resources/functions/dump.php" ],
+ "psr-4": { "Symfony\\Component\\VarDumper\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.4-dev"
+ }
+ }
+}
diff --git a/vendor/symfony/yaml/.gitignore b/vendor/symfony/yaml/.gitignore
new file mode 100644
index 0000000..c49a5d8
--- /dev/null
+++ b/vendor/symfony/yaml/.gitignore
@@ -0,0 +1,3 @@
+vendor/
+composer.lock
+phpunit.xml
diff --git a/vendor/symfony/yaml/CHANGELOG.md b/vendor/symfony/yaml/CHANGELOG.md
new file mode 100644
index 0000000..f55b570
--- /dev/null
+++ b/vendor/symfony/yaml/CHANGELOG.md
@@ -0,0 +1,28 @@
+CHANGELOG
+=========
+
+2.8.0
+-----
+
+ * Deprecated usage of a colon in an unquoted mapping value
+ * Deprecated usage of @, \`, | and > at the beginning of an unquoted string
+ * When surrounding strings with double-quotes, you must now escape `\` characters. Not
+ escaping those characters (when surrounded by double-quotes) is deprecated.
+
+ Before:
+
+ ```yml
+ class: "Foo\Var"
+ ```
+
+ After:
+
+ ```yml
+ class: "Foo\\Var"
+ ```
+
+2.1.0
+-----
+
+ * Yaml::parse() does not evaluate loaded files as PHP files by default
+ anymore (call Yaml::enablePhpParsing() to get back the old behavior)
diff --git a/vendor/symfony/yaml/Dumper.php b/vendor/symfony/yaml/Dumper.php
new file mode 100644
index 0000000..8b523d1
--- /dev/null
+++ b/vendor/symfony/yaml/Dumper.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+/**
+ * Dumper dumps PHP variables to YAML strings.
+ *
+ * @author Fabien Potencier
+ */
+class Dumper
+{
+ /**
+ * The amount of spaces to use for indentation of nested nodes.
+ *
+ * @var int
+ */
+ protected $indentation = 4;
+
+ /**
+ * Sets the indentation.
+ *
+ * @param int $num The amount of spaces to use for indentation of nested nodes
+ */
+ public function setIndentation($num)
+ {
+ if ($num < 1) {
+ throw new \InvalidArgumentException('The indentation must be greater than zero.');
+ }
+
+ $this->indentation = (int) $num;
+ }
+
+ /**
+ * Dumps a PHP value to YAML.
+ *
+ * @param mixed $input The PHP value
+ * @param int $inline The level where you switch to inline YAML
+ * @param int $indent The level of indentation (used internally)
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ *
+ * @return string The YAML representation of the PHP value
+ */
+ public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false)
+ {
+ $output = '';
+ $prefix = $indent ? str_repeat(' ', $indent) : '';
+
+ if ($inline <= 0 || !\is_array($input) || empty($input)) {
+ $output .= $prefix.Inline::dump($input, $exceptionOnInvalidType, $objectSupport);
+ } else {
+ $isAHash = Inline::isHash($input);
+
+ foreach ($input as $key => $value) {
+ $willBeInlined = $inline - 1 <= 0 || !\is_array($value) || empty($value);
+
+ $output .= sprintf('%s%s%s%s',
+ $prefix,
+ $isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport).':' : '-',
+ $willBeInlined ? ' ' : "\n",
+ $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport)
+ ).($willBeInlined ? "\n" : '');
+ }
+ }
+
+ return $output;
+ }
+}
diff --git a/vendor/symfony/yaml/Escaper.php b/vendor/symfony/yaml/Escaper.php
new file mode 100644
index 0000000..2b1321f
--- /dev/null
+++ b/vendor/symfony/yaml/Escaper.php
@@ -0,0 +1,101 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+/**
+ * Escaper encapsulates escaping rules for single and double-quoted
+ * YAML strings.
+ *
+ * @author Matthew Lewinski
+ *
+ * @internal
+ */
+class Escaper
+{
+ // Characters that would cause a dumped string to require double quoting.
+ const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9";
+
+ // Mapping arrays for escaping a double quoted string. The backslash is
+ // first to ensure proper escaping because str_replace operates iteratively
+ // on the input arrays. This ordering of the characters avoids the use of strtr,
+ // which performs more slowly.
+ private static $escapees = array('\\', '\\\\', '\\"', '"',
+ "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
+ "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f",
+ "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
+ "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f",
+ "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9",
+ );
+ private static $escaped = array('\\\\', '\\"', '\\\\', '\\"',
+ '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a',
+ '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f',
+ '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17',
+ '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f',
+ '\\N', '\\_', '\\L', '\\P',
+ );
+
+ /**
+ * Determines if a PHP value would require double quoting in YAML.
+ *
+ * @param string $value A PHP value
+ *
+ * @return bool True if the value would require double quotes
+ */
+ public static function requiresDoubleQuoting($value)
+ {
+ return 0 < preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value);
+ }
+
+ /**
+ * Escapes and surrounds a PHP value with double quotes.
+ *
+ * @param string $value A PHP value
+ *
+ * @return string The quoted, escaped string
+ */
+ public static function escapeWithDoubleQuotes($value)
+ {
+ return sprintf('"%s"', str_replace(self::$escapees, self::$escaped, $value));
+ }
+
+ /**
+ * Determines if a PHP value would require single quoting in YAML.
+ *
+ * @param string $value A PHP value
+ *
+ * @return bool True if the value would require single quotes
+ */
+ public static function requiresSingleQuoting($value)
+ {
+ // Determines if a PHP value is entirely composed of a value that would
+ // require single quoting in YAML.
+ if (\in_array(strtolower($value), array('null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'))) {
+ return true;
+ }
+
+ // Determines if the PHP value contains any single characters that would
+ // cause it to require single quoting in YAML.
+ return 0 < preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` ]/x', $value);
+ }
+
+ /**
+ * Escapes and surrounds a PHP value with single quotes.
+ *
+ * @param string $value A PHP value
+ *
+ * @return string The quoted, escaped string
+ */
+ public static function escapeWithSingleQuotes($value)
+ {
+ return sprintf("'%s'", str_replace('\'', '\'\'', $value));
+ }
+}
diff --git a/vendor/symfony/yaml/Exception/DumpException.php b/vendor/symfony/yaml/Exception/DumpException.php
new file mode 100644
index 0000000..cce972f
--- /dev/null
+++ b/vendor/symfony/yaml/Exception/DumpException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Exception;
+
+/**
+ * Exception class thrown when an error occurs during dumping.
+ *
+ * @author Fabien Potencier
+ */
+class DumpException extends RuntimeException
+{
+}
diff --git a/vendor/symfony/yaml/Exception/ExceptionInterface.php b/vendor/symfony/yaml/Exception/ExceptionInterface.php
new file mode 100644
index 0000000..ad850ee
--- /dev/null
+++ b/vendor/symfony/yaml/Exception/ExceptionInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Exception;
+
+/**
+ * Exception interface for all exceptions thrown by the component.
+ *
+ * @author Fabien Potencier
+ */
+interface ExceptionInterface
+{
+}
diff --git a/vendor/symfony/yaml/Exception/ParseException.php b/vendor/symfony/yaml/Exception/ParseException.php
new file mode 100644
index 0000000..60802b6
--- /dev/null
+++ b/vendor/symfony/yaml/Exception/ParseException.php
@@ -0,0 +1,144 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Exception;
+
+/**
+ * Exception class thrown when an error occurs during parsing.
+ *
+ * @author Fabien Potencier
+ */
+class ParseException extends RuntimeException
+{
+ private $parsedFile;
+ private $parsedLine;
+ private $snippet;
+ private $rawMessage;
+
+ /**
+ * @param string $message The error message
+ * @param int $parsedLine The line where the error occurred
+ * @param string|null $snippet The snippet of code near the problem
+ * @param string|null $parsedFile The file name where the error occurred
+ * @param \Exception|null $previous The previous exception
+ */
+ public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null)
+ {
+ $this->parsedFile = $parsedFile;
+ $this->parsedLine = $parsedLine;
+ $this->snippet = $snippet;
+ $this->rawMessage = $message;
+
+ $this->updateRepr();
+
+ parent::__construct($this->message, 0, $previous);
+ }
+
+ /**
+ * Gets the snippet of code near the error.
+ *
+ * @return string The snippet of code
+ */
+ public function getSnippet()
+ {
+ return $this->snippet;
+ }
+
+ /**
+ * Sets the snippet of code near the error.
+ *
+ * @param string $snippet The code snippet
+ */
+ public function setSnippet($snippet)
+ {
+ $this->snippet = $snippet;
+
+ $this->updateRepr();
+ }
+
+ /**
+ * Gets the filename where the error occurred.
+ *
+ * This method returns null if a string is parsed.
+ *
+ * @return string The filename
+ */
+ public function getParsedFile()
+ {
+ return $this->parsedFile;
+ }
+
+ /**
+ * Sets the filename where the error occurred.
+ *
+ * @param string $parsedFile The filename
+ */
+ public function setParsedFile($parsedFile)
+ {
+ $this->parsedFile = $parsedFile;
+
+ $this->updateRepr();
+ }
+
+ /**
+ * Gets the line where the error occurred.
+ *
+ * @return int The file line
+ */
+ public function getParsedLine()
+ {
+ return $this->parsedLine;
+ }
+
+ /**
+ * Sets the line where the error occurred.
+ *
+ * @param int $parsedLine The file line
+ */
+ public function setParsedLine($parsedLine)
+ {
+ $this->parsedLine = $parsedLine;
+
+ $this->updateRepr();
+ }
+
+ private function updateRepr()
+ {
+ $this->message = $this->rawMessage;
+
+ $dot = false;
+ if ('.' === substr($this->message, -1)) {
+ $this->message = substr($this->message, 0, -1);
+ $dot = true;
+ }
+
+ if (null !== $this->parsedFile) {
+ if (\PHP_VERSION_ID >= 50400) {
+ $jsonOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+ } else {
+ $jsonOptions = 0;
+ }
+ $this->message .= sprintf(' in %s', json_encode($this->parsedFile, $jsonOptions));
+ }
+
+ if ($this->parsedLine >= 0) {
+ $this->message .= sprintf(' at line %d', $this->parsedLine);
+ }
+
+ if ($this->snippet) {
+ $this->message .= sprintf(' (near "%s")', $this->snippet);
+ }
+
+ if ($dot) {
+ $this->message .= '.';
+ }
+ }
+}
diff --git a/vendor/symfony/yaml/Exception/RuntimeException.php b/vendor/symfony/yaml/Exception/RuntimeException.php
new file mode 100644
index 0000000..3f36b73
--- /dev/null
+++ b/vendor/symfony/yaml/Exception/RuntimeException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Exception;
+
+/**
+ * Exception class thrown when an error occurs during parsing.
+ *
+ * @author Romain Neutron
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/vendor/symfony/yaml/Inline.php b/vendor/symfony/yaml/Inline.php
new file mode 100644
index 0000000..639ff4a
--- /dev/null
+++ b/vendor/symfony/yaml/Inline.php
@@ -0,0 +1,609 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+use Symfony\Component\Yaml\Exception\DumpException;
+use Symfony\Component\Yaml\Exception\ParseException;
+
+/**
+ * Inline implements a YAML parser/dumper for the YAML inline syntax.
+ *
+ * @author Fabien Potencier
+ */
+class Inline
+{
+ const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')';
+
+ private static $exceptionOnInvalidType = false;
+ private static $objectSupport = false;
+ private static $objectForMap = false;
+
+ /**
+ * Converts a YAML string to a PHP value.
+ *
+ * @param string $value A YAML string
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ * @param bool $objectForMap True if maps should return a stdClass instead of array()
+ * @param array $references Mapping of variable names to values
+ *
+ * @return mixed A PHP value
+ *
+ * @throws ParseException
+ */
+ public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array())
+ {
+ self::$exceptionOnInvalidType = $exceptionOnInvalidType;
+ self::$objectSupport = $objectSupport;
+ self::$objectForMap = $objectForMap;
+
+ $value = trim($value);
+
+ if ('' === $value) {
+ return '';
+ }
+
+ if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
+ $mbEncoding = mb_internal_encoding();
+ mb_internal_encoding('ASCII');
+ }
+
+ $i = 0;
+ switch ($value[0]) {
+ case '[':
+ $result = self::parseSequence($value, $i, $references);
+ ++$i;
+ break;
+ case '{':
+ $result = self::parseMapping($value, $i, $references);
+ ++$i;
+ break;
+ default:
+ $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references);
+ }
+
+ // some comments are allowed at the end
+ if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
+ throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)));
+ }
+
+ if (isset($mbEncoding)) {
+ mb_internal_encoding($mbEncoding);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Dumps a given PHP variable to a YAML string.
+ *
+ * @param mixed $value The PHP variable to convert
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ *
+ * @return string The YAML string representing the PHP value
+ *
+ * @throws DumpException When trying to dump PHP resource
+ */
+ public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false)
+ {
+ switch (true) {
+ case \is_resource($value):
+ if ($exceptionOnInvalidType) {
+ throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
+ }
+
+ return 'null';
+ case \is_object($value):
+ if ($objectSupport) {
+ return '!php/object:'.serialize($value);
+ }
+
+ if ($exceptionOnInvalidType) {
+ throw new DumpException('Object support when dumping a YAML file has been disabled.');
+ }
+
+ return 'null';
+ case \is_array($value):
+ return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport);
+ case null === $value:
+ return 'null';
+ case true === $value:
+ return 'true';
+ case false === $value:
+ return 'false';
+ case ctype_digit($value):
+ return \is_string($value) ? "'$value'" : (int) $value;
+ case is_numeric($value):
+ $locale = setlocale(LC_NUMERIC, 0);
+ if (false !== $locale) {
+ setlocale(LC_NUMERIC, 'C');
+ }
+ if (\is_float($value)) {
+ $repr = (string) $value;
+ if (is_infinite($value)) {
+ $repr = str_ireplace('INF', '.Inf', $repr);
+ } elseif (floor($value) == $value && $repr == $value) {
+ // Preserve float data type since storing a whole number will result in integer value.
+ $repr = '!!float '.$repr;
+ }
+ } else {
+ $repr = \is_string($value) ? "'$value'" : (string) $value;
+ }
+ if (false !== $locale) {
+ setlocale(LC_NUMERIC, $locale);
+ }
+
+ return $repr;
+ case '' == $value:
+ return "''";
+ case Escaper::requiresDoubleQuoting($value):
+ return Escaper::escapeWithDoubleQuotes($value);
+ case Escaper::requiresSingleQuoting($value):
+ case Parser::preg_match(self::getHexRegex(), $value):
+ case Parser::preg_match(self::getTimestampRegex(), $value):
+ return Escaper::escapeWithSingleQuotes($value);
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Check if given array is hash or just normal indexed array.
+ *
+ * @internal
+ *
+ * @param array $value The PHP array to check
+ *
+ * @return bool true if value is hash array, false otherwise
+ */
+ public static function isHash(array $value)
+ {
+ $expectedKey = 0;
+
+ foreach ($value as $key => $val) {
+ if ($key !== $expectedKey++) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Dumps a PHP array to a YAML string.
+ *
+ * @param array $value The PHP array to dump
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ *
+ * @return string The YAML string representing the PHP array
+ */
+ private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport)
+ {
+ // array
+ if ($value && !self::isHash($value)) {
+ $output = array();
+ foreach ($value as $val) {
+ $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport);
+ }
+
+ return sprintf('[%s]', implode(', ', $output));
+ }
+
+ // hash
+ $output = array();
+ foreach ($value as $key => $val) {
+ $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport));
+ }
+
+ return sprintf('{ %s }', implode(', ', $output));
+ }
+
+ /**
+ * Parses a YAML scalar.
+ *
+ * @param string $scalar
+ * @param string[] $delimiters
+ * @param string[] $stringDelimiters
+ * @param int &$i
+ * @param bool $evaluate
+ * @param array $references
+ *
+ * @return string
+ *
+ * @throws ParseException When malformed inline YAML string is parsed
+ *
+ * @internal
+ */
+ public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array())
+ {
+ if (\in_array($scalar[$i], $stringDelimiters)) {
+ // quoted scalar
+ $output = self::parseQuotedScalar($scalar, $i);
+
+ if (null !== $delimiters) {
+ $tmp = ltrim(substr($scalar, $i), ' ');
+ if ('' === $tmp) {
+ throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)));
+ }
+ if (!\in_array($tmp[0], $delimiters)) {
+ throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
+ }
+ }
+ } else {
+ // "normal" string
+ if (!$delimiters) {
+ $output = substr($scalar, $i);
+ $i += \strlen($output);
+
+ // remove comments
+ if (Parser::preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) {
+ $output = substr($output, 0, $match[0][1]);
+ }
+ } elseif (Parser::preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
+ $output = $match[1];
+ $i += \strlen($output);
+ } else {
+ throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar));
+ }
+
+ // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
+ if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
+ @trigger_error(sprintf('Not quoting the scalar "%s" starting with "%s" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $output, $output[0]), E_USER_DEPRECATED);
+
+ // to be thrown in 3.0
+ // throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]));
+ }
+
+ if ($evaluate) {
+ $output = self::evaluateScalar($output, $references);
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Parses a YAML quoted scalar.
+ *
+ * @param string $scalar
+ * @param int &$i
+ *
+ * @return string
+ *
+ * @throws ParseException When malformed inline YAML string is parsed
+ */
+ private static function parseQuotedScalar($scalar, &$i)
+ {
+ if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
+ throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)));
+ }
+
+ $output = substr($match[0], 1, \strlen($match[0]) - 2);
+
+ $unescaper = new Unescaper();
+ if ('"' == $scalar[$i]) {
+ $output = $unescaper->unescapeDoubleQuotedString($output);
+ } else {
+ $output = $unescaper->unescapeSingleQuotedString($output);
+ }
+
+ $i += \strlen($match[0]);
+
+ return $output;
+ }
+
+ /**
+ * Parses a YAML sequence.
+ *
+ * @param string $sequence
+ * @param int &$i
+ * @param array $references
+ *
+ * @return array
+ *
+ * @throws ParseException When malformed inline YAML string is parsed
+ */
+ private static function parseSequence($sequence, &$i = 0, $references = array())
+ {
+ $output = array();
+ $len = \strlen($sequence);
+ ++$i;
+
+ // [foo, bar, ...]
+ while ($i < $len) {
+ switch ($sequence[$i]) {
+ case '[':
+ // nested sequence
+ $output[] = self::parseSequence($sequence, $i, $references);
+ break;
+ case '{':
+ // nested mapping
+ $output[] = self::parseMapping($sequence, $i, $references);
+ break;
+ case ']':
+ return $output;
+ case ',':
+ case ' ':
+ break;
+ default:
+ $isQuoted = \in_array($sequence[$i], array('"', "'"));
+ $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references);
+
+ // the value can be an array if a reference has been resolved to an array var
+ if (!\is_array($value) && !$isQuoted && false !== strpos($value, ': ')) {
+ // embedded mapping?
+ try {
+ $pos = 0;
+ $value = self::parseMapping('{'.$value.'}', $pos, $references);
+ } catch (\InvalidArgumentException $e) {
+ // no, it's not
+ }
+ }
+
+ $output[] = $value;
+
+ --$i;
+ }
+
+ ++$i;
+ }
+
+ throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence));
+ }
+
+ /**
+ * Parses a YAML mapping.
+ *
+ * @param string $mapping
+ * @param int &$i
+ * @param array $references
+ *
+ * @return array|\stdClass
+ *
+ * @throws ParseException When malformed inline YAML string is parsed
+ */
+ private static function parseMapping($mapping, &$i = 0, $references = array())
+ {
+ $output = array();
+ $len = \strlen($mapping);
+ ++$i;
+ $allowOverwrite = false;
+
+ // {foo: bar, bar:foo, ...}
+ while ($i < $len) {
+ switch ($mapping[$i]) {
+ case ' ':
+ case ',':
+ ++$i;
+ continue 2;
+ case '}':
+ if (self::$objectForMap) {
+ return (object) $output;
+ }
+
+ return $output;
+ }
+
+ // key
+ $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
+
+ if ('<<' === $key) {
+ $allowOverwrite = true;
+ }
+
+ // value
+ $done = false;
+
+ while ($i < $len) {
+ switch ($mapping[$i]) {
+ case '[':
+ // nested sequence
+ $value = self::parseSequence($mapping, $i, $references);
+ // Spec: Keys MUST be unique; first one wins.
+ // Parser cannot abort this mapping earlier, since lines
+ // are processed sequentially.
+ // But overwriting is allowed when a merge node is used in current block.
+ if ('<<' === $key) {
+ foreach ($value as $parsedValue) {
+ $output += $parsedValue;
+ }
+ } elseif ($allowOverwrite || !isset($output[$key])) {
+ $output[$key] = $value;
+ }
+ $done = true;
+ break;
+ case '{':
+ // nested mapping
+ $value = self::parseMapping($mapping, $i, $references);
+ // Spec: Keys MUST be unique; first one wins.
+ // Parser cannot abort this mapping earlier, since lines
+ // are processed sequentially.
+ // But overwriting is allowed when a merge node is used in current block.
+ if ('<<' === $key) {
+ $output += $value;
+ } elseif ($allowOverwrite || !isset($output[$key])) {
+ $output[$key] = $value;
+ }
+ $done = true;
+ break;
+ case ':':
+ case ' ':
+ break;
+ default:
+ $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references);
+ // Spec: Keys MUST be unique; first one wins.
+ // Parser cannot abort this mapping earlier, since lines
+ // are processed sequentially.
+ // But overwriting is allowed when a merge node is used in current block.
+ if ('<<' === $key) {
+ $output += $value;
+ } elseif ($allowOverwrite || !isset($output[$key])) {
+ $output[$key] = $value;
+ }
+ $done = true;
+ --$i;
+ }
+
+ ++$i;
+
+ if ($done) {
+ continue 2;
+ }
+ }
+ }
+
+ throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping));
+ }
+
+ /**
+ * Evaluates scalars and replaces magic values.
+ *
+ * @param string $scalar
+ * @param array $references
+ *
+ * @return mixed The evaluated YAML string
+ *
+ * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved
+ */
+ private static function evaluateScalar($scalar, $references = array())
+ {
+ $scalar = trim($scalar);
+ $scalarLower = strtolower($scalar);
+
+ if (0 === strpos($scalar, '*')) {
+ if (false !== $pos = strpos($scalar, '#')) {
+ $value = substr($scalar, 1, $pos - 2);
+ } else {
+ $value = substr($scalar, 1);
+ }
+
+ // an unquoted *
+ if (false === $value || '' === $value) {
+ throw new ParseException('A reference must contain at least one character.');
+ }
+
+ if (!array_key_exists($value, $references)) {
+ throw new ParseException(sprintf('Reference "%s" does not exist.', $value));
+ }
+
+ return $references[$value];
+ }
+
+ switch (true) {
+ case 'null' === $scalarLower:
+ case '' === $scalar:
+ case '~' === $scalar:
+ return;
+ case 'true' === $scalarLower:
+ return true;
+ case 'false' === $scalarLower:
+ return false;
+ // Optimise for returning strings.
+ case '+' === $scalar[0] || '-' === $scalar[0] || '.' === $scalar[0] || '!' === $scalar[0] || is_numeric($scalar[0]):
+ switch (true) {
+ case 0 === strpos($scalar, '!str'):
+ return (string) substr($scalar, 5);
+ case 0 === strpos($scalar, '! '):
+ return (int) self::parseScalar(substr($scalar, 2));
+ case 0 === strpos($scalar, '!php/object:'):
+ if (self::$objectSupport) {
+ return unserialize(substr($scalar, 12));
+ }
+
+ if (self::$exceptionOnInvalidType) {
+ throw new ParseException('Object support when parsing a YAML file has been disabled.');
+ }
+
+ return;
+ case 0 === strpos($scalar, '!!php/object:'):
+ if (self::$objectSupport) {
+ return unserialize(substr($scalar, 13));
+ }
+
+ if (self::$exceptionOnInvalidType) {
+ throw new ParseException('Object support when parsing a YAML file has been disabled.');
+ }
+
+ return;
+ case 0 === strpos($scalar, '!!float '):
+ return (float) substr($scalar, 8);
+ case ctype_digit($scalar):
+ $raw = $scalar;
+ $cast = (int) $scalar;
+
+ return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
+ case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
+ $raw = $scalar;
+ $cast = (int) $scalar;
+
+ return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw);
+ case is_numeric($scalar):
+ case Parser::preg_match(self::getHexRegex(), $scalar):
+ return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
+ case '.inf' === $scalarLower:
+ case '.nan' === $scalarLower:
+ return -log(0);
+ case '-.inf' === $scalarLower:
+ return log(0);
+ case Parser::preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
+ return (float) str_replace(',', '', $scalar);
+ case Parser::preg_match(self::getTimestampRegex(), $scalar):
+ $timeZone = date_default_timezone_get();
+ date_default_timezone_set('UTC');
+ $time = strtotime($scalar);
+ date_default_timezone_set($timeZone);
+
+ return $time;
+ }
+ // no break
+ default:
+ return (string) $scalar;
+ }
+ }
+
+ /**
+ * Gets a regex that matches a YAML date.
+ *
+ * @return string The regular expression
+ *
+ * @see http://www.yaml.org/spec/1.2/spec.html#id2761573
+ */
+ private static function getTimestampRegex()
+ {
+ return <<[0-9][0-9][0-9][0-9])
+ -(?P[0-9][0-9]?)
+ -(?P[0-9][0-9]?)
+ (?:(?:[Tt]|[ \t]+)
+ (?P[0-9][0-9]?)
+ :(?P[0-9][0-9])
+ :(?P[0-9][0-9])
+ (?:\.(?P[0-9]*))?
+ (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?)
+ (?::(?P[0-9][0-9]))?))?)?
+ $~x
+EOF;
+ }
+
+ /**
+ * Gets a regex that matches a YAML number in hexadecimal notation.
+ *
+ * @return string
+ */
+ private static function getHexRegex()
+ {
+ return '~^0x[0-9a-f]++$~i';
+ }
+}
diff --git a/vendor/symfony/yaml/LICENSE b/vendor/symfony/yaml/LICENSE
new file mode 100644
index 0000000..21d7fb9
--- /dev/null
+++ b/vendor/symfony/yaml/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-2018 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.
diff --git a/vendor/symfony/yaml/Parser.php b/vendor/symfony/yaml/Parser.php
new file mode 100644
index 0000000..cb0d8f1
--- /dev/null
+++ b/vendor/symfony/yaml/Parser.php
@@ -0,0 +1,852 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+use Symfony\Component\Yaml\Exception\ParseException;
+
+/**
+ * Parser parses YAML strings to convert them to PHP arrays.
+ *
+ * @author Fabien Potencier
+ */
+class Parser
+{
+ const BLOCK_SCALAR_HEADER_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?';
+ // BC - wrongly named
+ const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
+
+ private $offset = 0;
+ private $totalNumberOfLines;
+ private $lines = array();
+ private $currentLineNb = -1;
+ private $currentLine = '';
+ private $refs = array();
+ private $skippedLineNumbers = array();
+ private $locallySkippedLineNumbers = array();
+
+ /**
+ * @param int $offset The offset of YAML document (used for line numbers in error messages)
+ * @param int|null $totalNumberOfLines The overall number of lines being parsed
+ * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser
+ */
+ public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
+ {
+ $this->offset = $offset;
+ $this->totalNumberOfLines = $totalNumberOfLines;
+ $this->skippedLineNumbers = $skippedLineNumbers;
+ }
+
+ /**
+ * Parses a YAML string to a PHP value.
+ *
+ * @param string $value A YAML string
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ * @param bool $objectForMap True if maps should return a stdClass instead of array()
+ *
+ * @return mixed A PHP value
+ *
+ * @throws ParseException If the YAML is not valid
+ */
+ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
+ {
+ if (false === preg_match('//u', $value)) {
+ throw new ParseException('The YAML value does not appear to be valid UTF-8.');
+ }
+
+ $this->refs = array();
+
+ $mbEncoding = null;
+ $e = null;
+ $data = null;
+
+ if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
+ $mbEncoding = mb_internal_encoding();
+ mb_internal_encoding('UTF-8');
+ }
+
+ try {
+ $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
+ } catch (\Exception $e) {
+ } catch (\Throwable $e) {
+ }
+
+ if (null !== $mbEncoding) {
+ mb_internal_encoding($mbEncoding);
+ }
+
+ $this->lines = array();
+ $this->currentLine = '';
+ $this->refs = array();
+ $this->skippedLineNumbers = array();
+ $this->locallySkippedLineNumbers = array();
+
+ if (null !== $e) {
+ throw $e;
+ }
+
+ return $data;
+ }
+
+ private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
+ {
+ $this->currentLineNb = -1;
+ $this->currentLine = '';
+ $value = $this->cleanup($value);
+ $this->lines = explode("\n", $value);
+ $this->locallySkippedLineNumbers = array();
+
+ if (null === $this->totalNumberOfLines) {
+ $this->totalNumberOfLines = \count($this->lines);
+ }
+
+ $data = array();
+ $context = null;
+ $allowOverwrite = false;
+
+ while ($this->moveToNextLine()) {
+ if ($this->isCurrentLineEmpty()) {
+ continue;
+ }
+
+ // tab?
+ if ("\t" === $this->currentLine[0]) {
+ throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+
+ $isRef = $mergeNode = false;
+ if (self::preg_match('#^\-((?P\s+)(?P.+))?$#u', rtrim($this->currentLine), $values)) {
+ if ($context && 'mapping' == $context) {
+ throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+ $context = 'sequence';
+
+ if (isset($values['value']) && self::preg_match('#^&(?P[[^ ]+) *(?P].*)#u', $values['value'], $matches)) {
+ $isRef = $matches['ref'];
+ $values['value'] = $matches['value'];
+ }
+
+ // array
+ if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
+ $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
+ } else {
+ if (isset($values['leadspaces'])
+ && self::preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+))?$#u', rtrim($values['value']), $matches)
+ ) {
+ // this is a compact notation element, add to next block and parse
+ $block = $values['value'];
+ if ($this->isNextLineIndented()) {
+ $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
+ }
+
+ $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
+ } else {
+ $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
+ }
+ }
+ if ($isRef) {
+ $this->refs[$isRef] = end($data);
+ }
+ } elseif (
+ self::preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+))?$#u', rtrim($this->currentLine), $values)
+ && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], array('"', "'")))
+ ) {
+ if ($context && 'sequence' == $context) {
+ throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
+ }
+ $context = 'mapping';
+
+ // force correct settings
+ Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
+ try {
+ $key = Inline::parseScalar($values['key']);
+ } catch (ParseException $e) {
+ $e->setParsedLine($this->getRealCurrentLineNb() + 1);
+ $e->setSnippet($this->currentLine);
+
+ throw $e;
+ }
+
+ // Convert float keys to strings, to avoid being converted to integers by PHP
+ if (\is_float($key)) {
+ $key = (string) $key;
+ }
+
+ if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P[[^ ]+)#u', $values['value'], $refMatches))) {
+ $mergeNode = true;
+ $allowOverwrite = true;
+ if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
+ $refName = substr($values['value'], 1);
+ if (!array_key_exists($refName, $this->refs)) {
+ throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+
+ $refValue = $this->refs[$refName];
+
+ if (!\is_array($refValue)) {
+ throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+
+ $data += $refValue; // array union
+ } else {
+ if (isset($values['value']) && '' !== $values['value']) {
+ $value = $values['value'];
+ } else {
+ $value = $this->getNextEmbedBlock();
+ }
+ $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
+
+ if (!\is_array($parsed)) {
+ throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+
+ if (isset($parsed[0])) {
+ // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
+ // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
+ // in the sequence override keys specified in later mapping nodes.
+ foreach ($parsed as $parsedItem) {
+ if (!\is_array($parsedItem)) {
+ throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
+ }
+
+ $data += $parsedItem; // array union
+ }
+ } else {
+ // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
+ // current mapping, unless the key already exists in it.
+ $data += $parsed; // array union
+ }
+ }
+ } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P][[^ ]+) *(?P].*)#u', $values['value'], $matches)) {
+ $isRef = $matches['ref'];
+ $values['value'] = $matches['value'];
+ }
+
+ if ($mergeNode) {
+ // Merge keys
+ } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#') || '<<' === $key) {
+ // hash
+ // if next line is less indented or equal, then it means that the current value is null
+ if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
+ // Spec: Keys MUST be unique; first one wins.
+ // But overwriting is allowed when a merge node is used in current block.
+ if ($allowOverwrite || !isset($data[$key])) {
+ $data[$key] = null;
+ }
+ } else {
+ $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
+
+ if ('<<' === $key) {
+ $this->refs[$refMatches['ref']] = $value;
+ $data += $value;
+ } elseif ($allowOverwrite || !isset($data[$key])) {
+ // Spec: Keys MUST be unique; first one wins.
+ // But overwriting is allowed when a merge node is used in current block.
+ $data[$key] = $value;
+ }
+ }
+ } else {
+ $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
+ // Spec: Keys MUST be unique; first one wins.
+ // But overwriting is allowed when a merge node is used in current block.
+ if ($allowOverwrite || !isset($data[$key])) {
+ $data[$key] = $value;
+ }
+ }
+ if ($isRef) {
+ $this->refs[$isRef] = $data[$key];
+ }
+ } else {
+ // multiple documents are not supported
+ if ('---' === $this->currentLine) {
+ throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
+ }
+
+ // 1-liner optionally followed by newline(s)
+ if (\is_string($value) && $this->lines[0] === trim($value)) {
+ try {
+ $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
+ } catch (ParseException $e) {
+ $e->setParsedLine($this->getRealCurrentLineNb() + 1);
+ $e->setSnippet($this->currentLine);
+
+ throw $e;
+ }
+
+ return $value;
+ }
+
+ throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+ }
+
+ if ($objectForMap && !\is_object($data) && 'mapping' === $context) {
+ $object = new \stdClass();
+
+ foreach ($data as $key => $value) {
+ $object->$key = $value;
+ }
+
+ $data = $object;
+ }
+
+ return empty($data) ? null : $data;
+ }
+
+ private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
+ {
+ $skippedLineNumbers = $this->skippedLineNumbers;
+
+ foreach ($this->locallySkippedLineNumbers as $lineNumber) {
+ if ($lineNumber < $offset) {
+ continue;
+ }
+
+ $skippedLineNumbers[] = $lineNumber;
+ }
+
+ $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
+ $parser->refs = &$this->refs;
+
+ return $parser->doParse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
+ }
+
+ /**
+ * Returns the current line number (takes the offset into account).
+ *
+ * @return int The current line number
+ */
+ private function getRealCurrentLineNb()
+ {
+ $realCurrentLineNumber = $this->currentLineNb + $this->offset;
+
+ foreach ($this->skippedLineNumbers as $skippedLineNumber) {
+ if ($skippedLineNumber > $realCurrentLineNumber) {
+ break;
+ }
+
+ ++$realCurrentLineNumber;
+ }
+
+ return $realCurrentLineNumber;
+ }
+
+ /**
+ * Returns the current line indentation.
+ *
+ * @return int The current line indentation
+ */
+ private function getCurrentLineIndentation()
+ {
+ return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
+ }
+
+ /**
+ * Returns the next embed block of YAML.
+ *
+ * @param int $indentation The indent level at which the block is to be read, or null for default
+ * @param bool $inSequence True if the enclosing data structure is a sequence
+ *
+ * @return string A YAML string
+ *
+ * @throws ParseException When indentation problem are detected
+ */
+ private function getNextEmbedBlock($indentation = null, $inSequence = false)
+ {
+ $oldLineIndentation = $this->getCurrentLineIndentation();
+ $blockScalarIndentations = array();
+
+ if ($this->isBlockScalarHeader()) {
+ $blockScalarIndentations[] = $this->getCurrentLineIndentation();
+ }
+
+ if (!$this->moveToNextLine()) {
+ return;
+ }
+
+ if (null === $indentation) {
+ $newIndent = $this->getCurrentLineIndentation();
+
+ $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
+
+ if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
+ throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+ } else {
+ $newIndent = $indentation;
+ }
+
+ $data = array();
+ if ($this->getCurrentLineIndentation() >= $newIndent) {
+ $data[] = substr($this->currentLine, $newIndent);
+ } else {
+ $this->moveToPreviousLine();
+
+ return;
+ }
+
+ if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
+ // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
+ // and therefore no nested list or mapping
+ $this->moveToPreviousLine();
+
+ return;
+ }
+
+ $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
+
+ if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
+ $blockScalarIndentations[] = $this->getCurrentLineIndentation();
+ }
+
+ $previousLineIndentation = $this->getCurrentLineIndentation();
+
+ while ($this->moveToNextLine()) {
+ $indent = $this->getCurrentLineIndentation();
+
+ // terminate all block scalars that are more indented than the current line
+ if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) {
+ foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
+ if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
+ unset($blockScalarIndentations[$key]);
+ }
+ }
+ }
+
+ if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
+ $blockScalarIndentations[] = $this->getCurrentLineIndentation();
+ }
+
+ $previousLineIndentation = $indent;
+
+ if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
+ $this->moveToPreviousLine();
+ break;
+ }
+
+ if ($this->isCurrentLineBlank()) {
+ $data[] = substr($this->currentLine, $newIndent);
+ continue;
+ }
+
+ // we ignore "comment" lines only when we are not inside a scalar block
+ if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
+ // remember ignored comment lines (they are used later in nested
+ // parser calls to determine real line numbers)
+ //
+ // CAUTION: beware to not populate the global property here as it
+ // will otherwise influence the getRealCurrentLineNb() call here
+ // for consecutive comment lines and subsequent embedded blocks
+ $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
+
+ continue;
+ }
+
+ if ($indent >= $newIndent) {
+ $data[] = substr($this->currentLine, $newIndent);
+ } elseif (0 == $indent) {
+ $this->moveToPreviousLine();
+
+ break;
+ } else {
+ throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
+ }
+ }
+
+ return implode("\n", $data);
+ }
+
+ /**
+ * Moves the parser to the next line.
+ *
+ * @return bool
+ */
+ private function moveToNextLine()
+ {
+ if ($this->currentLineNb >= \count($this->lines) - 1) {
+ return false;
+ }
+
+ $this->currentLine = $this->lines[++$this->currentLineNb];
+
+ return true;
+ }
+
+ /**
+ * Moves the parser to the previous line.
+ *
+ * @return bool
+ */
+ private function moveToPreviousLine()
+ {
+ if ($this->currentLineNb < 1) {
+ return false;
+ }
+
+ $this->currentLine = $this->lines[--$this->currentLineNb];
+
+ return true;
+ }
+
+ /**
+ * Parses a YAML value.
+ *
+ * @param string $value A YAML value
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ * @param bool $objectForMap True if maps should return a stdClass instead of array()
+ * @param string $context The parser context (either sequence or mapping)
+ *
+ * @return mixed A PHP value
+ *
+ * @throws ParseException When reference does not exist
+ */
+ private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
+ {
+ if (0 === strpos($value, '*')) {
+ if (false !== $pos = strpos($value, '#')) {
+ $value = substr($value, 1, $pos - 2);
+ } else {
+ $value = substr($value, 1);
+ }
+
+ if (!array_key_exists($value, $this->refs)) {
+ throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
+ }
+
+ return $this->refs[$value];
+ }
+
+ if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
+ $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
+
+ return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
+ }
+
+ try {
+ $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
+
+ if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
+ @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
+
+ // to be thrown in 3.0
+ // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
+ }
+
+ return $parsedValue;
+ } catch (ParseException $e) {
+ $e->setParsedLine($this->getRealCurrentLineNb() + 1);
+ $e->setSnippet($this->currentLine);
+
+ throw $e;
+ }
+ }
+
+ /**
+ * Parses a block scalar.
+ *
+ * @param string $style The style indicator that was used to begin this block scalar (| or >)
+ * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
+ * @param int $indentation The indentation indicator that was used to begin this block scalar
+ *
+ * @return string The text value
+ */
+ private function parseBlockScalar($style, $chomping = '', $indentation = 0)
+ {
+ $notEOF = $this->moveToNextLine();
+ if (!$notEOF) {
+ return '';
+ }
+
+ $isCurrentLineBlank = $this->isCurrentLineBlank();
+ $blockLines = array();
+
+ // leading blank lines are consumed before determining indentation
+ while ($notEOF && $isCurrentLineBlank) {
+ // newline only if not EOF
+ if ($notEOF = $this->moveToNextLine()) {
+ $blockLines[] = '';
+ $isCurrentLineBlank = $this->isCurrentLineBlank();
+ }
+ }
+
+ // determine indentation if not specified
+ if (0 === $indentation) {
+ if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
+ $indentation = \strlen($matches[0]);
+ }
+ }
+
+ if ($indentation > 0) {
+ $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
+
+ while (
+ $notEOF && (
+ $isCurrentLineBlank ||
+ self::preg_match($pattern, $this->currentLine, $matches)
+ )
+ ) {
+ if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
+ $blockLines[] = substr($this->currentLine, $indentation);
+ } elseif ($isCurrentLineBlank) {
+ $blockLines[] = '';
+ } else {
+ $blockLines[] = $matches[1];
+ }
+
+ // newline only if not EOF
+ if ($notEOF = $this->moveToNextLine()) {
+ $isCurrentLineBlank = $this->isCurrentLineBlank();
+ }
+ }
+ } elseif ($notEOF) {
+ $blockLines[] = '';
+ }
+
+ if ($notEOF) {
+ $blockLines[] = '';
+ $this->moveToPreviousLine();
+ } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
+ $blockLines[] = '';
+ }
+
+ // folded style
+ if ('>' === $style) {
+ $text = '';
+ $previousLineIndented = false;
+ $previousLineBlank = false;
+
+ for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
+ if ('' === $blockLines[$i]) {
+ $text .= "\n";
+ $previousLineIndented = false;
+ $previousLineBlank = true;
+ } elseif (' ' === $blockLines[$i][0]) {
+ $text .= "\n".$blockLines[$i];
+ $previousLineIndented = true;
+ $previousLineBlank = false;
+ } elseif ($previousLineIndented) {
+ $text .= "\n".$blockLines[$i];
+ $previousLineIndented = false;
+ $previousLineBlank = false;
+ } elseif ($previousLineBlank || 0 === $i) {
+ $text .= $blockLines[$i];
+ $previousLineIndented = false;
+ $previousLineBlank = false;
+ } else {
+ $text .= ' '.$blockLines[$i];
+ $previousLineIndented = false;
+ $previousLineBlank = false;
+ }
+ }
+ } else {
+ $text = implode("\n", $blockLines);
+ }
+
+ // deal with trailing newlines
+ if ('' === $chomping) {
+ $text = preg_replace('/\n+$/', "\n", $text);
+ } elseif ('-' === $chomping) {
+ $text = preg_replace('/\n+$/', '', $text);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Returns true if the next line is indented.
+ *
+ * @return bool Returns true if the next line is indented, false otherwise
+ */
+ private function isNextLineIndented()
+ {
+ $currentIndentation = $this->getCurrentLineIndentation();
+ $EOF = !$this->moveToNextLine();
+
+ while (!$EOF && $this->isCurrentLineEmpty()) {
+ $EOF = !$this->moveToNextLine();
+ }
+
+ if ($EOF) {
+ return false;
+ }
+
+ $ret = $this->getCurrentLineIndentation() > $currentIndentation;
+
+ $this->moveToPreviousLine();
+
+ return $ret;
+ }
+
+ /**
+ * Returns true if the current line is blank or if it is a comment line.
+ *
+ * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
+ */
+ private function isCurrentLineEmpty()
+ {
+ return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
+ }
+
+ /**
+ * Returns true if the current line is blank.
+ *
+ * @return bool Returns true if the current line is blank, false otherwise
+ */
+ private function isCurrentLineBlank()
+ {
+ return '' == trim($this->currentLine, ' ');
+ }
+
+ /**
+ * Returns true if the current line is a comment line.
+ *
+ * @return bool Returns true if the current line is a comment line, false otherwise
+ */
+ private function isCurrentLineComment()
+ {
+ //checking explicitly the first char of the trim is faster than loops or strpos
+ $ltrimmedLine = ltrim($this->currentLine, ' ');
+
+ return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
+ }
+
+ private function isCurrentLineLastLineInDocument()
+ {
+ return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
+ }
+
+ /**
+ * Cleanups a YAML string to be parsed.
+ *
+ * @param string $value The input YAML string
+ *
+ * @return string A cleaned up YAML string
+ */
+ private function cleanup($value)
+ {
+ $value = str_replace(array("\r\n", "\r"), "\n", $value);
+
+ // strip YAML header
+ $count = 0;
+ $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
+ $this->offset += $count;
+
+ // remove leading comments
+ $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
+ if (1 == $count) {
+ // items have been removed, update the offset
+ $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
+ $value = $trimmedValue;
+ }
+
+ // remove start of the document marker (---)
+ $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
+ if (1 == $count) {
+ // items have been removed, update the offset
+ $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
+ $value = $trimmedValue;
+
+ // remove end of the document marker (...)
+ $value = preg_replace('#\.\.\.\s*$#', '', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns true if the next line starts unindented collection.
+ *
+ * @return bool Returns true if the next line starts unindented collection, false otherwise
+ */
+ private function isNextLineUnIndentedCollection()
+ {
+ $currentIndentation = $this->getCurrentLineIndentation();
+ $notEOF = $this->moveToNextLine();
+
+ while ($notEOF && $this->isCurrentLineEmpty()) {
+ $notEOF = $this->moveToNextLine();
+ }
+
+ if (false === $notEOF) {
+ return false;
+ }
+
+ $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
+
+ $this->moveToPreviousLine();
+
+ return $ret;
+ }
+
+ /**
+ * Returns true if the string is un-indented collection item.
+ *
+ * @return bool Returns true if the string is un-indented collection item, false otherwise
+ */
+ private function isStringUnIndentedCollectionItem()
+ {
+ return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
+ }
+
+ /**
+ * Tests whether or not the current line is the header of a block scalar.
+ *
+ * @return bool
+ */
+ private function isBlockScalarHeader()
+ {
+ return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
+ }
+
+ /**
+ * A local wrapper for `preg_match` which will throw a ParseException if there
+ * is an internal error in the PCRE engine.
+ *
+ * This avoids us needing to check for "false" every time PCRE is used
+ * in the YAML engine
+ *
+ * @throws ParseException on a PCRE internal error
+ *
+ * @see preg_last_error()
+ *
+ * @internal
+ */
+ public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
+ {
+ if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
+ switch (preg_last_error()) {
+ case PREG_INTERNAL_ERROR:
+ $error = 'Internal PCRE error.';
+ break;
+ case PREG_BACKTRACK_LIMIT_ERROR:
+ $error = 'pcre.backtrack_limit reached.';
+ break;
+ case PREG_RECURSION_LIMIT_ERROR:
+ $error = 'pcre.recursion_limit reached.';
+ break;
+ case PREG_BAD_UTF8_ERROR:
+ $error = 'Malformed UTF-8 data.';
+ break;
+ case PREG_BAD_UTF8_OFFSET_ERROR:
+ $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
+ break;
+ default:
+ $error = 'Error.';
+ }
+
+ throw new ParseException($error);
+ }
+
+ return $ret;
+ }
+}
diff --git a/vendor/symfony/yaml/README.md b/vendor/symfony/yaml/README.md
new file mode 100644
index 0000000..0d32488
--- /dev/null
+++ b/vendor/symfony/yaml/README.md
@@ -0,0 +1,13 @@
+Yaml Component
+==============
+
+The Yaml component loads and dumps YAML files.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/yaml/index.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
diff --git a/vendor/symfony/yaml/Tests/DumperTest.php b/vendor/symfony/yaml/Tests/DumperTest.php
new file mode 100644
index 0000000..6bac6da
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/DumperTest.php
@@ -0,0 +1,257 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Dumper;
+use Symfony\Component\Yaml\Parser;
+
+class DumperTest extends TestCase
+{
+ protected $parser;
+ protected $dumper;
+ protected $path;
+
+ protected $array = array(
+ '' => 'bar',
+ 'foo' => '#bar',
+ 'foo\'bar' => array(),
+ 'bar' => array(1, 'foo'),
+ 'foobar' => array(
+ 'foo' => 'bar',
+ 'bar' => array(1, 'foo'),
+ 'foobar' => array(
+ 'foo' => 'bar',
+ 'bar' => array(1, 'foo'),
+ ),
+ ),
+ );
+
+ protected function setUp()
+ {
+ $this->parser = new Parser();
+ $this->dumper = new Dumper();
+ $this->path = __DIR__.'/Fixtures';
+ }
+
+ protected function tearDown()
+ {
+ $this->parser = null;
+ $this->dumper = null;
+ $this->path = null;
+ $this->array = null;
+ }
+
+ public function testSetIndentation()
+ {
+ $this->dumper->setIndentation(7);
+
+ $expected = <<<'EOF'
+'': bar
+foo: '#bar'
+'foo''bar': { }
+bar:
+ - 1
+ - foo
+foobar:
+ foo: bar
+ bar:
+ - 1
+ - foo
+ foobar:
+ foo: bar
+ bar:
+ - 1
+ - foo
+
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 4, 0));
+ }
+
+ public function testSpecifications()
+ {
+ $files = $this->parser->parse(file_get_contents($this->path.'/index.yml'));
+ foreach ($files as $file) {
+ $yamls = file_get_contents($this->path.'/'.$file.'.yml');
+
+ // split YAMLs documents
+ foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) {
+ if (!$yaml) {
+ continue;
+ }
+
+ $test = $this->parser->parse($yaml);
+ if (isset($test['dump_skip']) && $test['dump_skip']) {
+ continue;
+ } elseif (isset($test['todo']) && $test['todo']) {
+ // TODO
+ } else {
+ eval('$expected = '.trim($test['php']).';');
+ $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']);
+ }
+ }
+ }
+ }
+
+ public function testInlineLevel()
+ {
+ $expected = <<<'EOF'
+{ '': bar, foo: '#bar', 'foo''bar': { }, bar: [1, foo], foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } }
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument');
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument');
+
+ $expected = <<<'EOF'
+'': bar
+foo: '#bar'
+'foo''bar': { }
+bar: [1, foo]
+foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } }
+
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument');
+
+ $expected = <<<'EOF'
+'': bar
+foo: '#bar'
+'foo''bar': { }
+bar:
+ - 1
+ - foo
+foobar:
+ foo: bar
+ bar: [1, foo]
+ foobar: { foo: bar, bar: [1, foo] }
+
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument');
+
+ $expected = <<<'EOF'
+'': bar
+foo: '#bar'
+'foo''bar': { }
+bar:
+ - 1
+ - foo
+foobar:
+ foo: bar
+ bar:
+ - 1
+ - foo
+ foobar:
+ foo: bar
+ bar: [1, foo]
+
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument');
+
+ $expected = <<<'EOF'
+'': bar
+foo: '#bar'
+'foo''bar': { }
+bar:
+ - 1
+ - foo
+foobar:
+ foo: bar
+ bar:
+ - 1
+ - foo
+ foobar:
+ foo: bar
+ bar:
+ - 1
+ - foo
+
+EOF;
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument');
+ $this->assertEquals($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument');
+ }
+
+ public function testObjectSupportEnabled()
+ {
+ $dump = $this->dumper->dump(array('foo' => new A(), 'bar' => 1), 0, 0, false, true);
+
+ $this->assertEquals('{ foo: !php/object:O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}, bar: 1 }', $dump, '->dump() is able to dump objects');
+ }
+
+ public function testObjectSupportDisabledButNoExceptions()
+ {
+ $dump = $this->dumper->dump(array('foo' => new A(), 'bar' => 1));
+
+ $this->assertEquals('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\DumpException
+ */
+ public function testObjectSupportDisabledWithExceptions()
+ {
+ $this->dumper->dump(array('foo' => new A(), 'bar' => 1), 0, 0, true, false);
+ }
+
+ /**
+ * @dataProvider getEscapeSequences
+ */
+ public function testEscapedEscapeSequencesInQuotedScalar($input, $expected)
+ {
+ $this->assertEquals($expected, $this->dumper->dump($input));
+ }
+
+ public function getEscapeSequences()
+ {
+ return array(
+ 'empty string' => array('', "''"),
+ 'null' => array("\x0", '"\\0"'),
+ 'bell' => array("\x7", '"\\a"'),
+ 'backspace' => array("\x8", '"\\b"'),
+ 'horizontal-tab' => array("\t", '"\\t"'),
+ 'line-feed' => array("\n", '"\\n"'),
+ 'vertical-tab' => array("\v", '"\\v"'),
+ 'form-feed' => array("\xC", '"\\f"'),
+ 'carriage-return' => array("\r", '"\\r"'),
+ 'escape' => array("\x1B", '"\\e"'),
+ 'space' => array(' ', "' '"),
+ 'double-quote' => array('"', "'\"'"),
+ 'slash' => array('/', '/'),
+ 'backslash' => array('\\', '\\'),
+ 'next-line' => array("\xC2\x85", '"\\N"'),
+ 'non-breaking-space' => array("\xc2\xa0", '"\\_"'),
+ 'line-separator' => array("\xE2\x80\xA8", '"\\L"'),
+ 'paragraph-separator' => array("\xE2\x80\xA9", '"\\P"'),
+ 'colon' => array(':', "':'"),
+ );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The indentation must be greater than zero
+ */
+ public function testZeroIndentationThrowsException()
+ {
+ $this->dumper->setIndentation(0);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The indentation must be greater than zero
+ */
+ public function testNegativeIndentationThrowsException()
+ {
+ $this->dumper->setIndentation(-4);
+ }
+}
+
+class A
+{
+ public $a = 'foo';
+}
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml
new file mode 100644
index 0000000..5f9c942
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsAnchorAlias.yml
@@ -0,0 +1,31 @@
+--- %YAML:1.0
+test: Simple Alias Example
+brief: >
+ If you need to refer to the same item of data twice,
+ you can give that item an alias. The alias is a plain
+ string, starting with an ampersand. The item may then
+ be referred to by the alias throughout your document
+ by using an asterisk before the name of the alias.
+ This is called an anchor.
+yaml: |
+ - &showell Steve
+ - Clark
+ - Brian
+ - Oren
+ - *showell
+php: |
+ array('Steve', 'Clark', 'Brian', 'Oren', 'Steve')
+
+---
+test: Alias of a Mapping
+brief: >
+ An alias can be used on any item of data, including
+ sequences, mappings, and other complex data types.
+yaml: |
+ - &hello
+ Meat: pork
+ Starch: potato
+ - banana
+ - *hello
+php: |
+ array(array('Meat'=>'pork', 'Starch'=>'potato'), 'banana', array('Meat'=>'pork', 'Starch'=>'potato'))
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml
new file mode 100644
index 0000000..dfd9302
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsBasicTests.yml
@@ -0,0 +1,202 @@
+--- %YAML:1.0
+test: Simple Sequence
+brief: |
+ You can specify a list in YAML by placing each
+ member of the list on a new line with an opening
+ dash. These lists are called sequences.
+yaml: |
+ - apple
+ - banana
+ - carrot
+php: |
+ array('apple', 'banana', 'carrot')
+---
+test: Sequence With Item Being Null In The Middle
+brief: |
+ You can specify a list in YAML by placing each
+ member of the list on a new line with an opening
+ dash. These lists are called sequences.
+yaml: |
+ - apple
+ -
+ - carrot
+php: |
+ array('apple', null, 'carrot')
+---
+test: Sequence With Last Item Being Null
+brief: |
+ You can specify a list in YAML by placing each
+ member of the list on a new line with an opening
+ dash. These lists are called sequences.
+yaml: |
+ - apple
+ - banana
+ -
+php: |
+ array('apple', 'banana', null)
+---
+test: Nested Sequences
+brief: |
+ You can include a sequence within another
+ sequence by giving the sequence an empty
+ dash, followed by an indented list.
+yaml: |
+ -
+ - foo
+ - bar
+ - baz
+php: |
+ array(array('foo', 'bar', 'baz'))
+---
+test: Mixed Sequences
+brief: |
+ Sequences can contain any YAML data,
+ including strings and other sequences.
+yaml: |
+ - apple
+ -
+ - foo
+ - bar
+ - x123
+ - banana
+ - carrot
+php: |
+ array('apple', array('foo', 'bar', 'x123'), 'banana', 'carrot')
+---
+test: Deeply Nested Sequences
+brief: |
+ Sequences can be nested even deeper, with each
+ level of indentation representing a level of
+ depth.
+yaml: |
+ -
+ -
+ - uno
+ - dos
+php: |
+ array(array(array('uno', 'dos')))
+---
+test: Simple Mapping
+brief: |
+ You can add a keyed list (also known as a dictionary or
+ hash) to your document by placing each member of the
+ list on a new line, with a colon separating the key
+ from its value. In YAML, this type of list is called
+ a mapping.
+yaml: |
+ foo: whatever
+ bar: stuff
+php: |
+ array('foo' => 'whatever', 'bar' => 'stuff')
+---
+test: Sequence in a Mapping
+brief: |
+ A value in a mapping can be a sequence.
+yaml: |
+ foo: whatever
+ bar:
+ - uno
+ - dos
+php: |
+ array('foo' => 'whatever', 'bar' => array('uno', 'dos'))
+---
+test: Nested Mappings
+brief: |
+ A value in a mapping can be another mapping.
+yaml: |
+ foo: whatever
+ bar:
+ fruit: apple
+ name: steve
+ sport: baseball
+php: |
+ array(
+ 'foo' => 'whatever',
+ 'bar' => array(
+ 'fruit' => 'apple',
+ 'name' => 'steve',
+ 'sport' => 'baseball'
+ )
+ )
+---
+test: Mixed Mapping
+brief: |
+ A mapping can contain any assortment
+ of mappings and sequences as values.
+yaml: |
+ foo: whatever
+ bar:
+ -
+ fruit: apple
+ name: steve
+ sport: baseball
+ - more
+ -
+ python: rocks
+ perl: papers
+ ruby: scissorses
+php: |
+ array(
+ 'foo' => 'whatever',
+ 'bar' => array(
+ array(
+ 'fruit' => 'apple',
+ 'name' => 'steve',
+ 'sport' => 'baseball'
+ ),
+ 'more',
+ array(
+ 'python' => 'rocks',
+ 'perl' => 'papers',
+ 'ruby' => 'scissorses'
+ )
+ )
+ )
+---
+test: Mapping-in-Sequence Shortcut
+todo: true
+brief: |
+ If you are adding a mapping to a sequence, you
+ can place the mapping on the same line as the
+ dash as a shortcut.
+yaml: |
+ - work on YAML.py:
+ - work on Store
+php: |
+ array(array('work on YAML.py' => array('work on Store')))
+---
+test: Sequence-in-Mapping Shortcut
+todo: true
+brief: |
+ The dash in a sequence counts as indentation, so
+ you can add a sequence inside of a mapping without
+ needing spaces as indentation.
+yaml: |
+ allow:
+ - 'localhost'
+ - '%.sourceforge.net'
+ - '%.freepan.org'
+php: |
+ array('allow' => array('localhost', '%.sourceforge.net', '%.freepan.org'))
+---
+todo: true
+test: Merge key
+brief: |
+ A merge key ('<<') can be used in a mapping to insert other mappings. If
+ the value associated with the merge key is a mapping, each of its key/value
+ pairs is inserted into the current mapping.
+yaml: |
+ mapping:
+ name: Joe
+ job: Accountant
+ <<:
+ age: 38
+php: |
+ array(
+ 'mapping' =>
+ array(
+ 'name' => 'Joe',
+ 'job' => 'Accountant',
+ 'age' => 38
+ )
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml
new file mode 100644
index 0000000..f7ca469
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsBlockMapping.yml
@@ -0,0 +1,51 @@
+---
+test: One Element Mapping
+brief: |
+ A mapping with one key/value pair
+yaml: |
+ foo: bar
+php: |
+ array('foo' => 'bar')
+---
+test: Multi Element Mapping
+brief: |
+ More than one key/value pair
+yaml: |
+ red: baron
+ white: walls
+ blue: berries
+php: |
+ array(
+ 'red' => 'baron',
+ 'white' => 'walls',
+ 'blue' => 'berries',
+ )
+---
+test: Values aligned
+brief: |
+ Often times human editors of documents will align the values even
+ though YAML emitters generally don't.
+yaml: |
+ red: baron
+ white: walls
+ blue: berries
+php: |
+ array(
+ 'red' => 'baron',
+ 'white' => 'walls',
+ 'blue' => 'berries',
+ )
+---
+test: Colons aligned
+brief: |
+ Spaces can come before the ': ' key/value separator.
+yaml: |
+ red : baron
+ white : walls
+ blue : berries
+php: |
+ array(
+ 'red' => 'baron',
+ 'white' => 'walls',
+ 'blue' => 'berries',
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml
new file mode 100644
index 0000000..d988102
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsDocumentSeparator.yml
@@ -0,0 +1,85 @@
+--- %YAML:1.0
+test: Trailing Document Separator
+todo: true
+brief: >
+ You can separate YAML documents
+ with a string of three dashes.
+yaml: |
+ - foo: 1
+ bar: 2
+ ---
+ more: stuff
+python: |
+ [
+ [ { 'foo': 1, 'bar': 2 } ],
+ { 'more': 'stuff' }
+ ]
+ruby: |
+ [ { 'foo' => 1, 'bar' => 2 } ]
+
+---
+test: Leading Document Separator
+todo: true
+brief: >
+ You can explicitly give an opening
+ document separator to your YAML stream.
+yaml: |
+ ---
+ - foo: 1
+ bar: 2
+ ---
+ more: stuff
+python: |
+ [
+ [ {'foo': 1, 'bar': 2}],
+ {'more': 'stuff'}
+ ]
+ruby: |
+ [ { 'foo' => 1, 'bar' => 2 } ]
+
+---
+test: YAML Header
+todo: true
+brief: >
+ The opening separator can contain directives
+ to the YAML parser, such as the version
+ number.
+yaml: |
+ --- %YAML:1.0
+ foo: 1
+ bar: 2
+php: |
+ array('foo' => 1, 'bar' => 2)
+documents: 1
+
+---
+test: Red Herring Document Separator
+brief: >
+ Separators included in blocks or strings
+ are treated as blocks or strings, as the
+ document separator should have no indentation
+ preceding it.
+yaml: |
+ foo: |
+ ---
+php: |
+ array('foo' => "---\n")
+
+---
+test: Multiple Document Separators in Block
+brief: >
+ This technique allows you to embed other YAML
+ documents within literal blocks.
+yaml: |
+ foo: |
+ ---
+ foo: bar
+ ---
+ yo: baz
+ bar: |
+ fooness
+php: |
+ array(
+ 'foo' => "---\nfoo: bar\n---\nyo: baz\n",
+ 'bar' => "fooness\n"
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml
new file mode 100644
index 0000000..e8506fc
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsErrorTests.yml
@@ -0,0 +1,25 @@
+---
+test: Missing value for hash item
+todo: true
+brief: |
+ Third item in this hash doesn't have a value
+yaml: |
+ okay: value
+ also okay: ~
+ causes error because no value specified
+ last key: value okay here too
+python-error: causes error because no value specified
+
+---
+test: Not indenting enough
+brief: |
+ There was a bug in PyYaml where it was off by one
+ in the indentation check. It was allowing the YAML
+ below.
+# This is actually valid YAML now. Someone should tell showell.
+yaml: |
+ foo:
+ firstline: 1
+ secondline: 2
+php: |
+ array('foo' => null, 'firstline' => 1, 'secondline' => 2)
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml
new file mode 100644
index 0000000..03090e4
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsFlowCollections.yml
@@ -0,0 +1,60 @@
+---
+test: Simple Inline Array
+brief: >
+ Sequences can be contained on a
+ single line, using the inline syntax.
+ Separate each entry with commas and
+ enclose in square brackets.
+yaml: |
+ seq: [ a, b, c ]
+php: |
+ array('seq' => array('a', 'b', 'c'))
+---
+test: Simple Inline Hash
+brief: >
+ Mapping can also be contained on
+ a single line, using the inline
+ syntax. Each key-value pair is
+ separated by a colon, with a comma
+ between each entry in the mapping.
+ Enclose with curly braces.
+yaml: |
+ hash: { name: Steve, foo: bar }
+php: |
+ array('hash' => array('name' => 'Steve', 'foo' => 'bar'))
+---
+test: Multi-line Inline Collections
+todo: true
+brief: >
+ Both inline sequences and inline mappings
+ can span multiple lines, provided that you
+ indent the additional lines.
+yaml: |
+ languages: [ Ruby,
+ Perl,
+ Python ]
+ websites: { YAML: yaml.org,
+ Ruby: ruby-lang.org,
+ Python: python.org,
+ Perl: use.perl.org }
+php: |
+ array(
+ 'languages' => array('Ruby', 'Perl', 'Python'),
+ 'websites' => array(
+ 'YAML' => 'yaml.org',
+ 'Ruby' => 'ruby-lang.org',
+ 'Python' => 'python.org',
+ 'Perl' => 'use.perl.org'
+ )
+ )
+---
+test: Commas in Values (not in the spec!)
+todo: true
+brief: >
+ List items in collections are delimited by commas, but
+ there must be a space after each comma. This allows you
+ to add numbers without quoting.
+yaml: |
+ attendances: [ 45,123, 70,000, 17,222 ]
+php: |
+ array('attendances' => array(45123, 70000, 17222))
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml
new file mode 100644
index 0000000..a14735a
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsFoldedScalars.yml
@@ -0,0 +1,176 @@
+--- %YAML:1.0
+test: Single ending newline
+brief: >
+ A pipe character, followed by an indented
+ block of text is treated as a literal
+ block, in which newlines are preserved
+ throughout the block, including the final
+ newline.
+yaml: |
+ ---
+ this: |
+ Foo
+ Bar
+php: |
+ array('this' => "Foo\nBar\n")
+---
+test: The '+' indicator
+brief: >
+ The '+' indicator says to keep newlines at the end of text
+ blocks.
+yaml: |
+ normal: |
+ extra new lines not kept
+
+ preserving: |+
+ extra new lines are kept
+
+
+ dummy: value
+php: |
+ array(
+ 'normal' => "extra new lines not kept\n",
+ 'preserving' => "extra new lines are kept\n\n\n",
+ 'dummy' => 'value'
+ )
+---
+test: Three trailing newlines in literals
+brief: >
+ To give you more control over how space
+ is preserved in text blocks, YAML has
+ the keep '+' and chomp '-' indicators.
+ The keep indicator will preserve all
+ ending newlines, while the chomp indicator
+ will strip all ending newlines.
+yaml: |
+ clipped: |
+ This has one newline.
+
+
+
+ same as "clipped" above: "This has one newline.\n"
+
+ stripped: |-
+ This has no newline.
+
+
+
+ same as "stripped" above: "This has no newline."
+
+ kept: |+
+ This has four newlines.
+
+
+
+ same as "kept" above: "This has four newlines.\n\n\n\n"
+php: |
+ array(
+ 'clipped' => "This has one newline.\n",
+ 'same as "clipped" above' => "This has one newline.\n",
+ 'stripped' => 'This has no newline.',
+ 'same as "stripped" above' => 'This has no newline.',
+ 'kept' => "This has four newlines.\n\n\n\n",
+ 'same as "kept" above' => "This has four newlines.\n\n\n\n"
+ )
+---
+test: Extra trailing newlines with spaces
+todo: true
+brief: >
+ Normally, only a single newline is kept
+ from the end of a literal block, unless the
+ keep '+' character is used in combination
+ with the pipe. The following example
+ will preserve all ending whitespace
+ since the last line of both literal blocks
+ contains spaces which extend past the indentation
+ level.
+yaml: |
+ ---
+ this: |
+ Foo
+
+
+ kept: |+
+ Foo
+
+
+php: |
+ array('this' => "Foo\n\n \n",
+ 'kept' => "Foo\n\n \n" )
+
+---
+test: Folded Block in a Sequence
+brief: >
+ A greater-then character, followed by an indented
+ block of text is treated as a folded block, in
+ which lines of text separated by a single newline
+ are concatenated as a single line.
+yaml: |
+ ---
+ - apple
+ - banana
+ - >
+ can't you see
+ the beauty of yaml?
+ hmm
+ - dog
+php: |
+ array(
+ 'apple',
+ 'banana',
+ "can't you see the beauty of yaml? hmm\n",
+ 'dog'
+ )
+---
+test: Folded Block as a Mapping Value
+brief: >
+ Both literal and folded blocks can be
+ used in collections, as values in a
+ sequence or a mapping.
+yaml: |
+ ---
+ quote: >
+ Mark McGwire's
+ year was crippled
+ by a knee injury.
+ source: espn
+php: |
+ array(
+ 'quote' => "Mark McGwire's year was crippled by a knee injury.\n",
+ 'source' => 'espn'
+ )
+---
+test: Three trailing newlines in folded blocks
+brief: >
+ The keep and chomp indicators can also
+ be applied to folded blocks.
+yaml: |
+ clipped: >
+ This has one newline.
+
+
+
+ same as "clipped" above: "This has one newline.\n"
+
+ stripped: >-
+ This has no newline.
+
+
+
+ same as "stripped" above: "This has no newline."
+
+ kept: >+
+ This has four newlines.
+
+
+
+ same as "kept" above: "This has four newlines.\n\n\n\n"
+php: |
+ array(
+ 'clipped' => "This has one newline.\n",
+ 'same as "clipped" above' => "This has one newline.\n",
+ 'stripped' => 'This has no newline.',
+ 'same as "stripped" above' => 'This has no newline.',
+ 'kept' => "This has four newlines.\n\n\n\n",
+ 'same as "kept" above' => "This has four newlines.\n\n\n\n"
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml
new file mode 100644
index 0000000..9a5300f
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsNullsAndEmpties.yml
@@ -0,0 +1,45 @@
+--- %YAML:1.0
+test: Empty Sequence
+brief: >
+ You can represent the empty sequence
+ with an empty inline sequence.
+yaml: |
+ empty: []
+php: |
+ array('empty' => array())
+---
+test: Empty Mapping
+brief: >
+ You can represent the empty mapping
+ with an empty inline mapping.
+yaml: |
+ empty: {}
+php: |
+ array('empty' => array())
+---
+test: Empty Sequence as Entire Document
+yaml: |
+ []
+php: |
+ array()
+---
+test: Empty Mapping as Entire Document
+yaml: |
+ {}
+php: |
+ array()
+---
+test: Null as Document
+yaml: |
+ ~
+php: |
+ null
+---
+test: Empty String
+brief: >
+ You can represent an empty string
+ with a pair of quotes.
+yaml: |
+ ''
+php: |
+ ''
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml
new file mode 100644
index 0000000..ec1c4c3
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsSpecificationExamples.yml
@@ -0,0 +1,1697 @@
+--- %YAML:1.0
+test: Sequence of scalars
+spec: 2.1
+yaml: |
+ - Mark McGwire
+ - Sammy Sosa
+ - Ken Griffey
+php: |
+ array('Mark McGwire', 'Sammy Sosa', 'Ken Griffey')
+---
+test: Mapping of scalars to scalars
+spec: 2.2
+yaml: |
+ hr: 65
+ avg: 0.278
+ rbi: 147
+php: |
+ array('hr' => 65, 'avg' => 0.278, 'rbi' => 147)
+---
+test: Mapping of scalars to sequences
+spec: 2.3
+yaml: |
+ american:
+ - Boston Red Sox
+ - Detroit Tigers
+ - New York Yankees
+ national:
+ - New York Mets
+ - Chicago Cubs
+ - Atlanta Braves
+php: |
+ array('american' =>
+ array( 'Boston Red Sox', 'Detroit Tigers',
+ 'New York Yankees' ),
+ 'national' =>
+ array( 'New York Mets', 'Chicago Cubs',
+ 'Atlanta Braves' )
+ )
+---
+test: Sequence of mappings
+spec: 2.4
+yaml: |
+ -
+ name: Mark McGwire
+ hr: 65
+ avg: 0.278
+ -
+ name: Sammy Sosa
+ hr: 63
+ avg: 0.288
+php: |
+ array(
+ array('name' => 'Mark McGwire', 'hr' => 65, 'avg' => 0.278),
+ array('name' => 'Sammy Sosa', 'hr' => 63, 'avg' => 0.288)
+ )
+---
+test: Legacy A5
+todo: true
+spec: legacy_A5
+yaml: |
+ ?
+ - New York Yankees
+ - Atlanta Braves
+ :
+ - 2001-07-02
+ - 2001-08-12
+ - 2001-08-14
+ ?
+ - Detroit Tigers
+ - Chicago Cubs
+ :
+ - 2001-07-23
+perl-busted: >
+ YAML.pm will be able to emulate this behavior soon. In this regard
+ it may be somewhat more correct than Python's native behaviour which
+ can only use tuples as mapping keys. PyYAML will also need to figure
+ out some clever way to roundtrip structured keys.
+python: |
+ [
+ {
+ ('New York Yankees', 'Atlanta Braves'):
+ [yaml.timestamp('2001-07-02'),
+ yaml.timestamp('2001-08-12'),
+ yaml.timestamp('2001-08-14')],
+ ('Detroit Tigers', 'Chicago Cubs'):
+ [yaml.timestamp('2001-07-23')]
+ }
+ ]
+ruby: |
+ {
+ [ 'New York Yankees', 'Atlanta Braves' ] =>
+ [ Date.new( 2001, 7, 2 ), Date.new( 2001, 8, 12 ), Date.new( 2001, 8, 14 ) ],
+ [ 'Detroit Tigers', 'Chicago Cubs' ] =>
+ [ Date.new( 2001, 7, 23 ) ]
+ }
+syck: |
+ struct test_node seq1[] = {
+ { T_STR, 0, "New York Yankees" },
+ { T_STR, 0, "Atlanta Braves" },
+ end_node
+ };
+ struct test_node seq2[] = {
+ { T_STR, 0, "2001-07-02" },
+ { T_STR, 0, "2001-08-12" },
+ { T_STR, 0, "2001-08-14" },
+ end_node
+ };
+ struct test_node seq3[] = {
+ { T_STR, 0, "Detroit Tigers" },
+ { T_STR, 0, "Chicago Cubs" },
+ end_node
+ };
+ struct test_node seq4[] = {
+ { T_STR, 0, "2001-07-23" },
+ end_node
+ };
+ struct test_node map[] = {
+ { T_SEQ, 0, 0, seq1 },
+ { T_SEQ, 0, 0, seq2 },
+ { T_SEQ, 0, 0, seq3 },
+ { T_SEQ, 0, 0, seq4 },
+ end_node
+ };
+ struct test_node stream[] = {
+ { T_MAP, 0, 0, map },
+ end_node
+ };
+
+---
+test: Sequence of sequences
+spec: 2.5
+yaml: |
+ - [ name , hr , avg ]
+ - [ Mark McGwire , 65 , 0.278 ]
+ - [ Sammy Sosa , 63 , 0.288 ]
+php: |
+ array(
+ array( 'name', 'hr', 'avg' ),
+ array( 'Mark McGwire', 65, 0.278 ),
+ array( 'Sammy Sosa', 63, 0.288 )
+ )
+---
+test: Mapping of mappings
+todo: true
+spec: 2.6
+yaml: |
+ Mark McGwire: {hr: 65, avg: 0.278}
+ Sammy Sosa: {
+ hr: 63,
+ avg: 0.288
+ }
+php: |
+ array(
+ 'Mark McGwire' =>
+ array( 'hr' => 65, 'avg' => 0.278 ),
+ 'Sammy Sosa' =>
+ array( 'hr' => 63, 'avg' => 0.288 )
+ )
+---
+test: Two documents in a stream each with a leading comment
+todo: true
+spec: 2.7
+yaml: |
+ # Ranking of 1998 home runs
+ ---
+ - Mark McGwire
+ - Sammy Sosa
+ - Ken Griffey
+
+ # Team ranking
+ ---
+ - Chicago Cubs
+ - St Louis Cardinals
+ruby: |
+ y = YAML::Stream.new
+ y.add( [ 'Mark McGwire', 'Sammy Sosa', 'Ken Griffey' ] )
+ y.add( [ 'Chicago Cubs', 'St Louis Cardinals' ] )
+documents: 2
+
+---
+test: Play by play feed from a game
+todo: true
+spec: 2.8
+yaml: |
+ ---
+ time: 20:03:20
+ player: Sammy Sosa
+ action: strike (miss)
+ ...
+ ---
+ time: 20:03:47
+ player: Sammy Sosa
+ action: grand slam
+ ...
+perl: |
+ [ 'Mark McGwire', 'Sammy Sosa', 'Ken Griffey' ]
+documents: 2
+
+---
+test: Single document with two comments
+spec: 2.9
+yaml: |
+ hr: # 1998 hr ranking
+ - Mark McGwire
+ - Sammy Sosa
+ rbi:
+ # 1998 rbi ranking
+ - Sammy Sosa
+ - Ken Griffey
+php: |
+ array(
+ 'hr' => array( 'Mark McGwire', 'Sammy Sosa' ),
+ 'rbi' => array( 'Sammy Sosa', 'Ken Griffey' )
+ )
+---
+test: Node for Sammy Sosa appears twice in this document
+spec: 2.10
+yaml: |
+ ---
+ hr:
+ - Mark McGwire
+ # Following node labeled SS
+ - &SS Sammy Sosa
+ rbi:
+ - *SS # Subsequent occurrence
+ - Ken Griffey
+php: |
+ array(
+ 'hr' =>
+ array('Mark McGwire', 'Sammy Sosa'),
+ 'rbi' =>
+ array('Sammy Sosa', 'Ken Griffey')
+ )
+---
+test: Mapping between sequences
+todo: true
+spec: 2.11
+yaml: |
+ ? # PLAY SCHEDULE
+ - Detroit Tigers
+ - Chicago Cubs
+ :
+ - 2001-07-23
+
+ ? [ New York Yankees,
+ Atlanta Braves ]
+ : [ 2001-07-02, 2001-08-12,
+ 2001-08-14 ]
+ruby: |
+ {
+ [ 'Detroit Tigers', 'Chicago Cubs' ] => [ Date.new( 2001, 7, 23 ) ],
+ [ 'New York Yankees', 'Atlanta Braves' ] => [ Date.new( 2001, 7, 2 ), Date.new( 2001, 8, 12 ), Date.new( 2001, 8, 14 ) ]
+ }
+syck: |
+ struct test_node seq1[] = {
+ { T_STR, 0, "New York Yankees" },
+ { T_STR, 0, "Atlanta Braves" },
+ end_node
+ };
+ struct test_node seq2[] = {
+ { T_STR, 0, "2001-07-02" },
+ { T_STR, 0, "2001-08-12" },
+ { T_STR, 0, "2001-08-14" },
+ end_node
+ };
+ struct test_node seq3[] = {
+ { T_STR, 0, "Detroit Tigers" },
+ { T_STR, 0, "Chicago Cubs" },
+ end_node
+ };
+ struct test_node seq4[] = {
+ { T_STR, 0, "2001-07-23" },
+ end_node
+ };
+ struct test_node map[] = {
+ { T_SEQ, 0, 0, seq3 },
+ { T_SEQ, 0, 0, seq4 },
+ { T_SEQ, 0, 0, seq1 },
+ { T_SEQ, 0, 0, seq2 },
+ end_node
+ };
+ struct test_node stream[] = {
+ { T_MAP, 0, 0, map },
+ end_node
+ };
+
+---
+test: Sequence key shortcut
+spec: 2.12
+yaml: |
+ ---
+ # products purchased
+ - item : Super Hoop
+ quantity: 1
+ - item : Basketball
+ quantity: 4
+ - item : Big Shoes
+ quantity: 1
+php: |
+ array (
+ array (
+ 'item' => 'Super Hoop',
+ 'quantity' => 1,
+ ),
+ array (
+ 'item' => 'Basketball',
+ 'quantity' => 4,
+ ),
+ array (
+ 'item' => 'Big Shoes',
+ 'quantity' => 1,
+ )
+ )
+perl: |
+ [
+ { item => 'Super Hoop', quantity => 1 },
+ { item => 'Basketball', quantity => 4 },
+ { item => 'Big Shoes', quantity => 1 }
+ ]
+
+ruby: |
+ [
+ { 'item' => 'Super Hoop', 'quantity' => 1 },
+ { 'item' => 'Basketball', 'quantity' => 4 },
+ { 'item' => 'Big Shoes', 'quantity' => 1 }
+ ]
+python: |
+ [
+ { 'item': 'Super Hoop', 'quantity': 1 },
+ { 'item': 'Basketball', 'quantity': 4 },
+ { 'item': 'Big Shoes', 'quantity': 1 }
+ ]
+syck: |
+ struct test_node map1[] = {
+ { T_STR, 0, "item" },
+ { T_STR, 0, "Super Hoop" },
+ { T_STR, 0, "quantity" },
+ { T_STR, 0, "1" },
+ end_node
+ };
+ struct test_node map2[] = {
+ { T_STR, 0, "item" },
+ { T_STR, 0, "Basketball" },
+ { T_STR, 0, "quantity" },
+ { T_STR, 0, "4" },
+ end_node
+ };
+ struct test_node map3[] = {
+ { T_STR, 0, "item" },
+ { T_STR, 0, "Big Shoes" },
+ { T_STR, 0, "quantity" },
+ { T_STR, 0, "1" },
+ end_node
+ };
+ struct test_node seq[] = {
+ { T_MAP, 0, 0, map1 },
+ { T_MAP, 0, 0, map2 },
+ { T_MAP, 0, 0, map3 },
+ end_node
+ };
+ struct test_node stream[] = {
+ { T_SEQ, 0, 0, seq },
+ end_node
+ };
+
+
+---
+test: Literal perserves newlines
+todo: true
+spec: 2.13
+yaml: |
+ # ASCII Art
+ --- |
+ \//||\/||
+ // || ||_
+perl: |
+ "\\//||\\/||\n// || ||_\n"
+ruby: |
+ "\\//||\\/||\n// || ||_\n"
+python: |
+ [
+ flushLeft(
+ """
+ \//||\/||
+ // || ||_
+ """
+ )
+ ]
+syck: |
+ struct test_node stream[] = {
+ { T_STR, 0, "\\//||\\/||\n// || ||_\n" },
+ end_node
+ };
+
+---
+test: Folded treats newlines as a space
+todo: true
+spec: 2.14
+yaml: |
+ ---
+ Mark McGwire's
+ year was crippled
+ by a knee injury.
+perl: |
+ "Mark McGwire's year was crippled by a knee injury."
+ruby: |
+ "Mark McGwire's year was crippled by a knee injury."
+python: |
+ [ "Mark McGwire's year was crippled by a knee injury." ]
+syck: |
+ struct test_node stream[] = {
+ { T_STR, 0, "Mark McGwire's year was crippled by a knee injury." },
+ end_node
+ };
+
+---
+test: Newlines preserved for indented and blank lines
+todo: true
+spec: 2.15
+yaml: |
+ --- >
+ Sammy Sosa completed another
+ fine season with great stats.
+
+ 63 Home Runs
+ 0.288 Batting Average
+
+ What a year!
+perl: |
+ "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n"
+ruby: |
+ "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n"
+python: |
+ [
+ flushLeft(
+ """
+ Sammy Sosa completed another fine season with great stats.
+
+ 63 Home Runs
+ 0.288 Batting Average
+
+ What a year!
+ """
+ )
+ ]
+syck: |
+ struct test_node stream[] = {
+ { T_STR, 0, "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n" },
+ end_node
+ };
+
+
+---
+test: Indentation determines scope
+spec: 2.16
+yaml: |
+ name: Mark McGwire
+ accomplishment: >
+ Mark set a major league
+ home run record in 1998.
+ stats: |
+ 65 Home Runs
+ 0.278 Batting Average
+php: |
+ array(
+ 'name' => 'Mark McGwire',
+ 'accomplishment' => "Mark set a major league home run record in 1998.\n",
+ 'stats' => "65 Home Runs\n0.278 Batting Average\n"
+ )
+---
+test: Quoted scalars
+todo: true
+spec: 2.17
+yaml: |
+ unicode: "Sosa did fine.\u263A"
+ control: "\b1998\t1999\t2000\n"
+ hexesc: "\x0D\x0A is \r\n"
+
+ single: '"Howdy!" he cried.'
+ quoted: ' # not a ''comment''.'
+ tie-fighter: '|\-*-/|'
+ruby: |
+ {
+ "tie-fighter" => "|\\-*-/|",
+ "control"=>"\0101998\t1999\t2000\n",
+ "unicode"=>"Sosa did fine." + ["263A".hex ].pack('U*'),
+ "quoted"=>" # not a 'comment'.",
+ "single"=>"\"Howdy!\" he cried.",
+ "hexesc"=>"\r\n is \r\n"
+ }
+---
+test: Multiline flow scalars
+todo: true
+spec: 2.18
+yaml: |
+ plain:
+ This unquoted scalar
+ spans many lines.
+
+ quoted: "So does this
+ quoted scalar.\n"
+ruby: |
+ {
+ 'plain' => 'This unquoted scalar spans many lines.',
+ 'quoted' => "So does this quoted scalar.\n"
+ }
+---
+test: Integers
+spec: 2.19
+yaml: |
+ canonical: 12345
+ decimal: +12,345
+ octal: 014
+ hexadecimal: 0xC
+php: |
+ array(
+ 'canonical' => 12345,
+ 'decimal' => 12345.0,
+ 'octal' => 014,
+ 'hexadecimal' => 0xC
+ )
+---
+# FIX: spec shows parens around -inf and NaN
+test: Floating point
+spec: 2.20
+yaml: |
+ canonical: 1.23015e+3
+ exponential: 12.3015e+02
+ fixed: 1,230.15
+ negative infinity: -.inf
+ not a number: .NaN
+ float as whole number: !!float 1
+php: |
+ array(
+ 'canonical' => 1230.15,
+ 'exponential' => 1230.15,
+ 'fixed' => 1230.15,
+ 'negative infinity' => log(0),
+ 'not a number' => -log(0),
+ 'float as whole number' => (float) 1
+ )
+---
+test: Miscellaneous
+spec: 2.21
+yaml: |
+ null: ~
+ true: true
+ false: false
+ string: '12345'
+php: |
+ array(
+ '' => null,
+ 1 => true,
+ 0 => false,
+ 'string' => '12345'
+ )
+---
+test: Timestamps
+todo: true
+spec: 2.22
+yaml: |
+ canonical: 2001-12-15T02:59:43.1Z
+ iso8601: 2001-12-14t21:59:43.10-05:00
+ spaced: 2001-12-14 21:59:43.10 -05:00
+ date: 2002-12-14 # Time is noon UTC
+php: |
+ array(
+ 'canonical' => YAML::mktime( 2001, 12, 15, 2, 59, 43, 0.10 ),
+ 'iso8601' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
+ 'spaced' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
+ 'date' => Date.new( 2002, 12, 14 )
+ )
+---
+test: legacy Timestamps test
+todo: true
+spec: legacy D4
+yaml: |
+ canonical: 2001-12-15T02:59:43.00Z
+ iso8601: 2001-02-28t21:59:43.00-05:00
+ spaced: 2001-12-14 21:59:43.00 -05:00
+ date: 2002-12-14
+php: |
+ array(
+ 'canonical' => Time::utc( 2001, 12, 15, 2, 59, 43, 0 ),
+ 'iso8601' => YAML::mktime( 2001, 2, 28, 21, 59, 43, 0, "-05:00" ),
+ 'spaced' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0, "-05:00" ),
+ 'date' => Date.new( 2002, 12, 14 )
+ )
+---
+test: Various explicit families
+todo: true
+spec: 2.23
+yaml: |
+ not-date: !str 2002-04-28
+ picture: !binary |
+ R0lGODlhDAAMAIQAAP//9/X
+ 17unp5WZmZgAAAOfn515eXv
+ Pz7Y6OjuDg4J+fn5OTk6enp
+ 56enmleECcgggoBADs=
+
+ application specific tag: !!something |
+ The semantics of the tag
+ above may be different for
+ different documents.
+
+ruby-setup: |
+ YAML.add_private_type( "something" ) do |type, val|
+ "SOMETHING: #{val}"
+ end
+ruby: |
+ {
+ 'not-date' => '2002-04-28',
+ 'picture' => "GIF89a\f\000\f\000\204\000\000\377\377\367\365\365\356\351\351\345fff\000\000\000\347\347\347^^^\363\363\355\216\216\216\340\340\340\237\237\237\223\223\223\247\247\247\236\236\236i^\020' \202\n\001\000;",
+ 'application specific tag' => "SOMETHING: The semantics of the tag\nabove may be different for\ndifferent documents.\n"
+ }
+---
+test: Application specific family
+todo: true
+spec: 2.24
+yaml: |
+ # Establish a tag prefix
+ --- !clarkevans.com,2002/graph/^shape
+ # Use the prefix: shorthand for
+ # !clarkevans.com,2002/graph/circle
+ - !^circle
+ center: &ORIGIN {x: 73, 'y': 129}
+ radius: 7
+ - !^line # !clarkevans.com,2002/graph/line
+ start: *ORIGIN
+ finish: { x: 89, 'y': 102 }
+ - !^label
+ start: *ORIGIN
+ color: 0xFFEEBB
+ value: Pretty vector drawing.
+ruby-setup: |
+ YAML.add_domain_type( "clarkevans.com,2002", 'graph/shape' ) { |type, val|
+ if Array === val
+ val << "Shape Container"
+ val
+ else
+ raise YAML::Error, "Invalid graph of class #{ val.class }: " + val.inspect
+ end
+ }
+ one_shape_proc = Proc.new { |type, val|
+ scheme, domain, type = type.split( /:/, 3 )
+ if val.is_a? ::Hash
+ val['TYPE'] = "Shape: #{type}"
+ val
+ else
+ raise YAML::Error, "Invalid graph of class #{ val.class }: " + val.inspect
+ end
+ }
+ YAML.add_domain_type( "clarkevans.com,2002", 'graph/circle', &one_shape_proc )
+ YAML.add_domain_type( "clarkevans.com,2002", 'graph/line', &one_shape_proc )
+ YAML.add_domain_type( "clarkevans.com,2002", 'graph/label', &one_shape_proc )
+ruby: |
+ [
+ {
+ "radius" => 7,
+ "center"=>
+ {
+ "x" => 73,
+ "y" => 129
+ },
+ "TYPE" => "Shape: graph/circle"
+ }, {
+ "finish" =>
+ {
+ "x" => 89,
+ "y" => 102
+ },
+ "TYPE" => "Shape: graph/line",
+ "start" =>
+ {
+ "x" => 73,
+ "y" => 129
+ }
+ }, {
+ "TYPE" => "Shape: graph/label",
+ "value" => "Pretty vector drawing.",
+ "start" =>
+ {
+ "x" => 73,
+ "y" => 129
+ },
+ "color" => 16772795
+ },
+ "Shape Container"
+ ]
+# ---
+# test: Unordered set
+# spec: 2.25
+# yaml: |
+# # sets are represented as a
+# # mapping where each key is
+# # associated with the empty string
+# --- !set
+# ? Mark McGwire
+# ? Sammy Sosa
+# ? Ken Griff
+---
+test: Ordered mappings
+todo: true
+spec: 2.26
+yaml: |
+ # ordered maps are represented as
+ # a sequence of mappings, with
+ # each mapping having one key
+ --- !omap
+ - Mark McGwire: 65
+ - Sammy Sosa: 63
+ - Ken Griffy: 58
+ruby: |
+ YAML::Omap[
+ 'Mark McGwire', 65,
+ 'Sammy Sosa', 63,
+ 'Ken Griffy', 58
+ ]
+---
+test: Invoice
+dump_skip: true
+spec: 2.27
+yaml: |
+ --- !clarkevans.com,2002/^invoice
+ invoice: 34843
+ date : 2001-01-23
+ bill-to: &id001
+ given : Chris
+ family : Dumars
+ address:
+ lines: |
+ 458 Walkman Dr.
+ Suite #292
+ city : Royal Oak
+ state : MI
+ postal : 48046
+ ship-to: *id001
+ product:
+ -
+ sku : BL394D
+ quantity : 4
+ description : Basketball
+ price : 450.00
+ -
+ sku : BL4438H
+ quantity : 1
+ description : Super Hoop
+ price : 2392.00
+ tax : 251.42
+ total: 4443.52
+ comments: >
+ Late afternoon is best.
+ Backup contact is Nancy
+ Billsmer @ 338-4338.
+php: |
+ array(
+ 'invoice' => 34843, 'date' => gmmktime(0, 0, 0, 1, 23, 2001),
+ 'bill-to' =>
+ array( 'given' => 'Chris', 'family' => 'Dumars', 'address' => array( 'lines' => "458 Walkman Dr.\nSuite #292\n", 'city' => 'Royal Oak', 'state' => 'MI', 'postal' => 48046 ) )
+ , 'ship-to' =>
+ array( 'given' => 'Chris', 'family' => 'Dumars', 'address' => array( 'lines' => "458 Walkman Dr.\nSuite #292\n", 'city' => 'Royal Oak', 'state' => 'MI', 'postal' => 48046 ) )
+ , 'product' =>
+ array(
+ array( 'sku' => 'BL394D', 'quantity' => 4, 'description' => 'Basketball', 'price' => 450.00 ),
+ array( 'sku' => 'BL4438H', 'quantity' => 1, 'description' => 'Super Hoop', 'price' => 2392.00 )
+ ),
+ 'tax' => 251.42, 'total' => 4443.52,
+ 'comments' => "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.\n"
+ )
+---
+test: Log file
+todo: true
+spec: 2.28
+yaml: |
+ ---
+ Time: 2001-11-23 15:01:42 -05:00
+ User: ed
+ Warning: >
+ This is an error message
+ for the log file
+ ---
+ Time: 2001-11-23 15:02:31 -05:00
+ User: ed
+ Warning: >
+ A slightly different error
+ message.
+ ---
+ Date: 2001-11-23 15:03:17 -05:00
+ User: ed
+ Fatal: >
+ Unknown variable "bar"
+ Stack:
+ - file: TopClass.py
+ line: 23
+ code: |
+ x = MoreObject("345\n")
+ - file: MoreClass.py
+ line: 58
+ code: |-
+ foo = bar
+ruby: |
+ y = YAML::Stream.new
+ y.add( { 'Time' => YAML::mktime( 2001, 11, 23, 15, 01, 42, 00, "-05:00" ),
+ 'User' => 'ed', 'Warning' => "This is an error message for the log file\n" } )
+ y.add( { 'Time' => YAML::mktime( 2001, 11, 23, 15, 02, 31, 00, "-05:00" ),
+ 'User' => 'ed', 'Warning' => "A slightly different error message.\n" } )
+ y.add( { 'Date' => YAML::mktime( 2001, 11, 23, 15, 03, 17, 00, "-05:00" ),
+ 'User' => 'ed', 'Fatal' => "Unknown variable \"bar\"\n",
+ 'Stack' => [
+ { 'file' => 'TopClass.py', 'line' => 23, 'code' => "x = MoreObject(\"345\\n\")\n" },
+ { 'file' => 'MoreClass.py', 'line' => 58, 'code' => "foo = bar" } ] } )
+documents: 3
+
+---
+test: Throwaway comments
+yaml: |
+ ### These are four throwaway comment ###
+
+ ### lines (the second line is empty). ###
+ this: | # Comments may trail lines.
+ contains three lines of text.
+ The third one starts with a
+ # character. This isn't a comment.
+
+ # These are three throwaway comment
+ # lines (the first line is empty).
+php: |
+ array(
+ 'this' => "contains three lines of text.\nThe third one starts with a\n# character. This isn't a comment.\n"
+ )
+---
+test: Document with a single value
+todo: true
+yaml: |
+ --- >
+ This YAML stream contains a single text value.
+ The next stream is a log file - a sequence of
+ log entries. Adding an entry to the log is a
+ simple matter of appending it at the end.
+ruby: |
+ "This YAML stream contains a single text value. The next stream is a log file - a sequence of log entries. Adding an entry to the log is a simple matter of appending it at the end.\n"
+---
+test: Document stream
+todo: true
+yaml: |
+ ---
+ at: 2001-08-12 09:25:00.00 Z
+ type: GET
+ HTTP: '1.0'
+ url: '/index.html'
+ ---
+ at: 2001-08-12 09:25:10.00 Z
+ type: GET
+ HTTP: '1.0'
+ url: '/toc.html'
+ruby: |
+ y = YAML::Stream.new
+ y.add( {
+ 'at' => Time::utc( 2001, 8, 12, 9, 25, 00 ),
+ 'type' => 'GET',
+ 'HTTP' => '1.0',
+ 'url' => '/index.html'
+ } )
+ y.add( {
+ 'at' => Time::utc( 2001, 8, 12, 9, 25, 10 ),
+ 'type' => 'GET',
+ 'HTTP' => '1.0',
+ 'url' => '/toc.html'
+ } )
+documents: 2
+
+---
+test: Top level mapping
+yaml: |
+ # This stream is an example of a top-level mapping.
+ invoice : 34843
+ date : 2001-01-23
+ total : 4443.52
+php: |
+ array(
+ 'invoice' => 34843,
+ 'date' => gmmktime(0, 0, 0, 1, 23, 2001),
+ 'total' => 4443.52
+ )
+---
+test: Single-line documents
+todo: true
+yaml: |
+ # The following is a sequence of three documents.
+ # The first contains an empty mapping, the second
+ # an empty sequence, and the last an empty string.
+ --- {}
+ --- [ ]
+ --- ''
+ruby: |
+ y = YAML::Stream.new
+ y.add( {} )
+ y.add( [] )
+ y.add( '' )
+documents: 3
+
+---
+test: Document with pause
+todo: true
+yaml: |
+ # A communication channel based on a YAML stream.
+ ---
+ sent at: 2002-06-06 11:46:25.10 Z
+ payload: Whatever
+ # Receiver can process this as soon as the following is sent:
+ ...
+ # Even if the next message is sent long after:
+ ---
+ sent at: 2002-06-06 12:05:53.47 Z
+ payload: Whatever
+ ...
+ruby: |
+ y = YAML::Stream.new
+ y.add(
+ { 'sent at' => YAML::mktime( 2002, 6, 6, 11, 46, 25, 0.10 ),
+ 'payload' => 'Whatever' }
+ )
+ y.add(
+ { "payload" => "Whatever", "sent at" => YAML::mktime( 2002, 6, 6, 12, 5, 53, 0.47 ) }
+ )
+documents: 2
+
+---
+test: Explicit typing
+yaml: |
+ integer: 12
+ also int: ! "12"
+ string: !str 12
+php: |
+ array( 'integer' => 12, 'also int' => 12, 'string' => '12' )
+---
+test: Private types
+todo: true
+yaml: |
+ # Both examples below make use of the 'x-private:ball'
+ # type family URI, but with different semantics.
+ ---
+ pool: !!ball
+ number: 8
+ color: black
+ ---
+ bearing: !!ball
+ material: steel
+ruby: |
+ y = YAML::Stream.new
+ y.add( { 'pool' =>
+ YAML::PrivateType.new( 'ball',
+ { 'number' => 8, 'color' => 'black' } ) }
+ )
+ y.add( { 'bearing' =>
+ YAML::PrivateType.new( 'ball',
+ { 'material' => 'steel' } ) }
+ )
+documents: 2
+
+---
+test: Type family under yaml.org
+yaml: |
+ # The URI is 'tag:yaml.org,2002:str'
+ - !str a Unicode string
+php: |
+ array( 'a Unicode string' )
+---
+test: Type family under perl.yaml.org
+todo: true
+yaml: |
+ # The URI is 'tag:perl.yaml.org,2002:Text::Tabs'
+ - !perl/Text::Tabs {}
+ruby: |
+ [ YAML::DomainType.new( 'perl.yaml.org,2002', 'Text::Tabs', {} ) ]
+---
+test: Type family under clarkevans.com
+todo: true
+yaml: |
+ # The URI is 'tag:clarkevans.com,2003-02:timesheet'
+ - !clarkevans.com,2003-02/timesheet {}
+ruby: |
+ [ YAML::DomainType.new( 'clarkevans.com,2003-02', 'timesheet', {} ) ]
+---
+test: URI Escaping
+todo: true
+yaml: |
+ same:
+ - !domain.tld,2002/type\x30 value
+ - !domain.tld,2002/type0 value
+ different: # As far as the YAML parser is concerned
+ - !domain.tld,2002/type%30 value
+ - !domain.tld,2002/type0 value
+ruby-setup: |
+ YAML.add_domain_type( "domain.tld,2002", "type0" ) { |type, val|
+ "ONE: #{val}"
+ }
+ YAML.add_domain_type( "domain.tld,2002", "type%30" ) { |type, val|
+ "TWO: #{val}"
+ }
+ruby: |
+ { 'same' => [ 'ONE: value', 'ONE: value' ], 'different' => [ 'TWO: value', 'ONE: value' ] }
+---
+test: URI Prefixing
+todo: true
+yaml: |
+ # 'tag:domain.tld,2002:invoice' is some type family.
+ invoice: !domain.tld,2002/^invoice
+ # 'seq' is shorthand for 'tag:yaml.org,2002:seq'.
+ # This does not effect '^customer' below
+ # because it is does not specify a prefix.
+ customers: !seq
+ # '^customer' is shorthand for the full
+ # notation 'tag:domain.tld,2002:customer'.
+ - !^customer
+ given : Chris
+ family : Dumars
+ruby-setup: |
+ YAML.add_domain_type( "domain.tld,2002", /(invoice|customer)/ ) { |type, val|
+ if val.is_a? ::Hash
+ scheme, domain, type = type.split( /:/, 3 )
+ val['type'] = "domain #{type}"
+ val
+ else
+ raise YAML::Error, "Not a Hash in domain.tld/invoice: " + val.inspect
+ end
+ }
+ruby: |
+ { "invoice"=> { "customers"=> [ { "given"=>"Chris", "type"=>"domain customer", "family"=>"Dumars" } ], "type"=>"domain invoice" } }
+
+---
+test: Overriding anchors
+yaml: |
+ anchor : &A001 This scalar has an anchor.
+ override : &A001 >
+ The alias node below is a
+ repeated use of this value.
+ alias : *A001
+php: |
+ array( 'anchor' => 'This scalar has an anchor.',
+ 'override' => "The alias node below is a repeated use of this value.\n",
+ 'alias' => "The alias node below is a repeated use of this value.\n" )
+---
+test: Flow and block formatting
+todo: true
+yaml: |
+ empty: []
+ flow: [ one, two, three # May span lines,
+ , four, # indentation is
+ five ] # mostly ignored.
+ block:
+ - First item in top sequence
+ -
+ - Subordinate sequence entry
+ - >
+ A folded sequence entry
+ - Sixth item in top sequence
+ruby: |
+ { 'empty' => [], 'flow' => [ 'one', 'two', 'three', 'four', 'five' ],
+ 'block' => [ 'First item in top sequence', [ 'Subordinate sequence entry' ],
+ "A folded sequence entry\n", 'Sixth item in top sequence' ] }
+---
+test: Complete mapping test
+todo: true
+yaml: |
+ empty: {}
+ flow: { one: 1, two: 2 }
+ spanning: { one: 1,
+ two: 2 }
+ block:
+ first : First entry
+ second:
+ key: Subordinate mapping
+ third:
+ - Subordinate sequence
+ - { }
+ - Previous mapping is empty.
+ - A key: value pair in a sequence.
+ A second: key:value pair.
+ - The previous entry is equal to the following one.
+ -
+ A key: value pair in a sequence.
+ A second: key:value pair.
+ !float 12 : This key is a float.
+ ? >
+ ?
+ : This key had to be protected.
+ "\a" : This key had to be escaped.
+ ? >
+ This is a
+ multi-line
+ folded key
+ : Whose value is
+ also multi-line.
+ ? this also works as a key
+ : with a value at the next line.
+ ?
+ - This key
+ - is a sequence
+ :
+ - With a sequence value.
+ ?
+ This: key
+ is a: mapping
+ :
+ with a: mapping value.
+ruby: |
+ { 'empty' => {}, 'flow' => { 'one' => 1, 'two' => 2 },
+ 'spanning' => { 'one' => 1, 'two' => 2 },
+ 'block' => { 'first' => 'First entry', 'second' =>
+ { 'key' => 'Subordinate mapping' }, 'third' =>
+ [ 'Subordinate sequence', {}, 'Previous mapping is empty.',
+ { 'A key' => 'value pair in a sequence.', 'A second' => 'key:value pair.' },
+ 'The previous entry is equal to the following one.',
+ { 'A key' => 'value pair in a sequence.', 'A second' => 'key:value pair.' } ],
+ 12.0 => 'This key is a float.', "?\n" => 'This key had to be protected.',
+ "\a" => 'This key had to be escaped.',
+ "This is a multi-line folded key\n" => "Whose value is also multi-line.",
+ 'this also works as a key' => 'with a value at the next line.',
+ [ 'This key', 'is a sequence' ] => [ 'With a sequence value.' ] } }
+ # Couldn't recreate map exactly, so we'll do a detailed check to be sure it's entact
+ obj_y['block'].keys.each { |k|
+ if Hash === k
+ v = obj_y['block'][k]
+ if k['This'] == 'key' and k['is a'] == 'mapping' and v['with a'] == 'mapping value.'
+ obj_r['block'][k] = v
+ end
+ end
+ }
+---
+test: Literal explicit indentation
+yaml: |
+ # Explicit indentation must
+ # be given in all the three
+ # following cases.
+ leading spaces: |2
+ This value starts with four spaces.
+
+ leading line break: |2
+
+ This value starts with a line break.
+
+ leading comment indicator: |2
+ # first line starts with a
+ # character.
+
+ # Explicit indentation may
+ # also be given when it is
+ # not required.
+ redundant: |2
+ This value is indented 2 spaces.
+php: |
+ array(
+ 'leading spaces' => " This value starts with four spaces.\n",
+ 'leading line break' => "\nThis value starts with a line break.\n",
+ 'leading comment indicator' => "# first line starts with a\n# character.\n",
+ 'redundant' => "This value is indented 2 spaces.\n"
+ )
+---
+test: Chomping and keep modifiers
+yaml: |
+ clipped: |
+ This has one newline.
+
+ same as "clipped" above: "This has one newline.\n"
+
+ stripped: |-
+ This has no newline.
+
+ same as "stripped" above: "This has no newline."
+
+ kept: |+
+ This has two newlines.
+
+ same as "kept" above: "This has two newlines.\n\n"
+php: |
+ array(
+ 'clipped' => "This has one newline.\n",
+ 'same as "clipped" above' => "This has one newline.\n",
+ 'stripped' => 'This has no newline.',
+ 'same as "stripped" above' => 'This has no newline.',
+ 'kept' => "This has two newlines.\n\n",
+ 'same as "kept" above' => "This has two newlines.\n\n"
+ )
+---
+test: Literal combinations
+todo: true
+yaml: |
+ empty: |
+
+ literal: |
+ The \ ' " characters may be
+ freely used. Leading white
+ space is significant.
+
+ Line breaks are significant.
+ Thus this value contains one
+ empty line and ends with a
+ single line break, but does
+ not start with one.
+
+ is equal to: "The \\ ' \" characters may \
+ be\nfreely used. Leading white\n space \
+ is significant.\n\nLine breaks are \
+ significant.\nThus this value contains \
+ one\nempty line and ends with a\nsingle \
+ line break, but does\nnot start with one.\n"
+
+ # Comments may follow a block
+ # scalar value. They must be
+ # less indented.
+
+ # Modifiers may be combined in any order.
+ indented and chomped: |2-
+ This has no newline.
+
+ also written as: |-2
+ This has no newline.
+
+ both are equal to: " This has no newline."
+php: |
+ array(
+ 'empty' => '',
+ 'literal' => "The \\ ' \" characters may be\nfreely used. Leading white\n space " +
+ "is significant.\n\nLine breaks are significant.\nThus this value contains one\n" +
+ "empty line and ends with a\nsingle line break, but does\nnot start with one.\n",
+ 'is equal to' => "The \\ ' \" characters may be\nfreely used. Leading white\n space " +
+ "is significant.\n\nLine breaks are significant.\nThus this value contains one\n" +
+ "empty line and ends with a\nsingle line break, but does\nnot start with one.\n",
+ 'indented and chomped' => ' This has no newline.',
+ 'also written as' => ' This has no newline.',
+ 'both are equal to' => ' This has no newline.'
+ )
+---
+test: Folded combinations
+todo: true
+yaml: |
+ empty: >
+
+ one paragraph: >
+ Line feeds are converted
+ to spaces, so this value
+ contains no line breaks
+ except for the final one.
+
+ multiple paragraphs: >2
+
+ An empty line, either
+ at the start or in
+ the value:
+
+ Is interpreted as a
+ line break. Thus this
+ value contains three
+ line breaks.
+
+ indented text: >
+ This is a folded
+ paragraph followed
+ by a list:
+ * first entry
+ * second entry
+ Followed by another
+ folded paragraph,
+ another list:
+
+ * first entry
+
+ * second entry
+
+ And a final folded
+ paragraph.
+
+ above is equal to: |
+ This is a folded paragraph followed by a list:
+ * first entry
+ * second entry
+ Followed by another folded paragraph, another list:
+
+ * first entry
+
+ * second entry
+
+ And a final folded paragraph.
+
+ # Explicit comments may follow
+ # but must be less indented.
+php: |
+ array(
+ 'empty' => '',
+ 'one paragraph' => 'Line feeds are converted to spaces, so this value'.
+ " contains no line breaks except for the final one.\n",
+ 'multiple paragraphs' => "\nAn empty line, either at the start or in the value:\n".
+ "Is interpreted as a line break. Thus this value contains three line breaks.\n",
+ 'indented text' => "This is a folded paragraph followed by a list:\n".
+ " * first entry\n * second entry\nFollowed by another folded paragraph, ".
+ "another list:\n\n * first entry\n\n * second entry\n\nAnd a final folded paragraph.\n",
+ 'above is equal to' => "This is a folded paragraph followed by a list:\n".
+ " * first entry\n * second entry\nFollowed by another folded paragraph, ".
+ "another list:\n\n * first entry\n\n * second entry\n\nAnd a final folded paragraph.\n"
+ )
+---
+test: Single quotes
+todo: true
+yaml: |
+ empty: ''
+ second: '! : \ etc. can be used freely.'
+ third: 'a single quote '' must be escaped.'
+ span: 'this contains
+ six spaces
+
+ and one
+ line break'
+ is same as: "this contains six spaces\nand one line break"
+php: |
+ array(
+ 'empty' => '',
+ 'second' => '! : \\ etc. can be used freely.',
+ 'third' => "a single quote ' must be escaped.",
+ 'span' => "this contains six spaces\nand one line break",
+ 'is same as' => "this contains six spaces\nand one line break"
+ )
+---
+test: Double quotes
+todo: true
+yaml: |
+ empty: ""
+ second: "! : etc. can be used freely."
+ third: "a \" or a \\ must be escaped."
+ fourth: "this value ends with an LF.\n"
+ span: "this contains
+ four \
+ spaces"
+ is equal to: "this contains four spaces"
+php: |
+ array(
+ 'empty' => '',
+ 'second' => '! : etc. can be used freely.',
+ 'third' => 'a " or a \\ must be escaped.',
+ 'fourth' => "this value ends with an LF.\n",
+ 'span' => "this contains four spaces",
+ 'is equal to' => "this contains four spaces"
+ )
+---
+test: Unquoted strings
+todo: true
+yaml: |
+ first: There is no unquoted empty string.
+
+ second: 12 ## This is an integer.
+
+ third: !str 12 ## This is a string.
+
+ span: this contains
+ six spaces
+
+ and one
+ line break
+
+ indicators: this has no comments.
+ #:foo and bar# are
+ both text.
+
+ flow: [ can span
+ lines, # comment
+ like
+ this ]
+
+ note: { one-line keys: but multi-line values }
+
+php: |
+ array(
+ 'first' => 'There is no unquoted empty string.',
+ 'second' => 12,
+ 'third' => '12',
+ 'span' => "this contains six spaces\nand one line break",
+ 'indicators' => "this has no comments. #:foo and bar# are both text.",
+ 'flow' => [ 'can span lines', 'like this' ],
+ 'note' => { 'one-line keys' => 'but multi-line values' }
+ )
+---
+test: Spanning sequences
+todo: true
+yaml: |
+ # The following are equal seqs
+ # with different identities.
+ flow: [ one, two ]
+ spanning: [ one,
+ two ]
+ block:
+ - one
+ - two
+php: |
+ array(
+ 'flow' => [ 'one', 'two' ],
+ 'spanning' => [ 'one', 'two' ],
+ 'block' => [ 'one', 'two' ]
+ )
+---
+test: Flow mappings
+yaml: |
+ # The following are equal maps
+ # with different identities.
+ flow: { one: 1, two: 2 }
+ block:
+ one: 1
+ two: 2
+php: |
+ array(
+ 'flow' => array( 'one' => 1, 'two' => 2 ),
+ 'block' => array( 'one' => 1, 'two' => 2 )
+ )
+---
+test: Representations of 12
+todo: true
+yaml: |
+ - 12 # An integer
+ # The following scalars
+ # are loaded to the
+ # string value '1' '2'.
+ - !str 12
+ - '12'
+ - "12"
+ - "\
+ 1\
+ 2\
+ "
+ # Strings containing paths and regexps can be unquoted:
+ - /foo/bar
+ - d:/foo/bar
+ - foo/bar
+ - /a.*b/
+php: |
+ array( 12, '12', '12', '12', '12', '/foo/bar', 'd:/foo/bar', 'foo/bar', '/a.*b/' )
+---
+test: "Null"
+todo: true
+yaml: |
+ canonical: ~
+
+ english: null
+
+ # This sequence has five
+ # entries, two with values.
+ sparse:
+ - ~
+ - 2nd entry
+ - Null
+ - 4th entry
+ -
+
+ four: This mapping has five keys,
+ only two with values.
+
+php: |
+ array (
+ 'canonical' => null,
+ 'english' => null,
+ 'sparse' => array( null, '2nd entry', null, '4th entry', null ]),
+ 'four' => 'This mapping has five keys, only two with values.'
+ )
+---
+test: Omap
+todo: true
+yaml: |
+ # Explicitly typed dictionary.
+ Bestiary: !omap
+ - aardvark: African pig-like ant eater. Ugly.
+ - anteater: South-American ant eater. Two species.
+ - anaconda: South-American constrictor snake. Scary.
+ # Etc.
+ruby: |
+ {
+ 'Bestiary' => YAML::Omap[
+ 'aardvark', 'African pig-like ant eater. Ugly.',
+ 'anteater', 'South-American ant eater. Two species.',
+ 'anaconda', 'South-American constrictor snake. Scary.'
+ ]
+ }
+
+---
+test: Pairs
+todo: true
+yaml: |
+ # Explicitly typed pairs.
+ tasks: !pairs
+ - meeting: with team.
+ - meeting: with boss.
+ - break: lunch.
+ - meeting: with client.
+ruby: |
+ {
+ 'tasks' => YAML::Pairs[
+ 'meeting', 'with team.',
+ 'meeting', 'with boss.',
+ 'break', 'lunch.',
+ 'meeting', 'with client.'
+ ]
+ }
+
+---
+test: Set
+todo: true
+yaml: |
+ # Explicitly typed set.
+ baseball players: !set
+ Mark McGwire:
+ Sammy Sosa:
+ Ken Griffey:
+ruby: |
+ {
+ 'baseball players' => YAML::Set[
+ 'Mark McGwire', nil,
+ 'Sammy Sosa', nil,
+ 'Ken Griffey', nil
+ ]
+ }
+
+---
+test: Boolean
+yaml: |
+ false: used as key
+ logical: true
+ answer: false
+php: |
+ array(
+ false => 'used as key',
+ 'logical' => true,
+ 'answer' => false
+ )
+---
+test: Integer
+yaml: |
+ canonical: 12345
+ decimal: +12,345
+ octal: 014
+ hexadecimal: 0xC
+php: |
+ array(
+ 'canonical' => 12345,
+ 'decimal' => 12345.0,
+ 'octal' => 12,
+ 'hexadecimal' => 12
+ )
+---
+test: Float
+yaml: |
+ canonical: 1.23015e+3
+ exponential: 12.3015e+02
+ fixed: 1,230.15
+ negative infinity: -.inf
+ not a number: .NaN
+php: |
+ array(
+ 'canonical' => 1230.15,
+ 'exponential' => 1230.15,
+ 'fixed' => 1230.15,
+ 'negative infinity' => log(0),
+ 'not a number' => -log(0)
+ )
+---
+test: Timestamp
+todo: true
+yaml: |
+ canonical: 2001-12-15T02:59:43.1Z
+ valid iso8601: 2001-12-14t21:59:43.10-05:00
+ space separated: 2001-12-14 21:59:43.10 -05:00
+ date (noon UTC): 2002-12-14
+ruby: |
+ array(
+ 'canonical' => YAML::mktime( 2001, 12, 15, 2, 59, 43, 0.10 ),
+ 'valid iso8601' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
+ 'space separated' => YAML::mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
+ 'date (noon UTC)' => Date.new( 2002, 12, 14 )
+ )
+---
+test: Binary
+todo: true
+yaml: |
+ canonical: !binary "\
+ R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\
+ OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\
+ +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\
+ AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs="
+ base64: !binary |
+ R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5
+ OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+
+ +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC
+ AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=
+ description: >
+ The binary value above is a tiny arrow
+ encoded as a gif image.
+ruby-setup: |
+ arrow_gif = "GIF89a\f\000\f\000\204\000\000\377\377\367\365\365\356\351\351\345fff\000\000\000\347\347\347^^^\363\363\355\216\216\216\340\340\340\237\237\237\223\223\223\247\247\247\236\236\236iiiccc\243\243\243\204\204\204\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371\377\376\371!\376\016Made with GIMP\000,\000\000\000\000\f\000\f\000\000\005, \216\2010\236\343@\024\350i\020\304\321\212\010\034\317\200M$z\357\3770\205p\270\2601f\r\e\316\001\303\001\036\020' \202\n\001\000;"
+ruby: |
+ {
+ 'canonical' => arrow_gif,
+ 'base64' => arrow_gif,
+ 'description' => "The binary value above is a tiny arrow encoded as a gif image.\n"
+ }
+
+---
+test: Merge key
+todo: true
+yaml: |
+ ---
+ - &CENTER { x: 1, y: 2 }
+ - &LEFT { x: 0, y: 2 }
+ - &BIG { r: 10 }
+ - &SMALL { r: 1 }
+
+ # All the following maps are equal:
+
+ - # Explicit keys
+ x: 1
+ y: 2
+ r: 10
+ label: center/big
+
+ - # Merge one map
+ << : *CENTER
+ r: 10
+ label: center/big
+
+ - # Merge multiple maps
+ << : [ *CENTER, *BIG ]
+ label: center/big
+
+ - # Override
+ << : [ *BIG, *LEFT, *SMALL ]
+ x: 1
+ label: center/big
+
+ruby-setup: |
+ center = { 'x' => 1, 'y' => 2 }
+ left = { 'x' => 0, 'y' => 2 }
+ big = { 'r' => 10 }
+ small = { 'r' => 1 }
+ node1 = { 'x' => 1, 'y' => 2, 'r' => 10, 'label' => 'center/big' }
+ node2 = center.dup
+ node2.update( { 'r' => 10, 'label' => 'center/big' } )
+ node3 = big.dup
+ node3.update( center )
+ node3.update( { 'label' => 'center/big' } )
+ node4 = small.dup
+ node4.update( left )
+ node4.update( big )
+ node4.update( { 'x' => 1, 'label' => 'center/big' } )
+
+ruby: |
+ [
+ center, left, big, small, node1, node2, node3, node4
+ ]
+
+---
+test: Default key
+todo: true
+yaml: |
+ --- # Old schema
+ link with:
+ - library1.dll
+ - library2.dll
+ --- # New schema
+ link with:
+ - = : library1.dll
+ version: 1.2
+ - = : library2.dll
+ version: 2.3
+ruby: |
+ y = YAML::Stream.new
+ y.add( { 'link with' => [ 'library1.dll', 'library2.dll' ] } )
+ obj_h = Hash[ 'version' => 1.2 ]
+ obj_h.default = 'library1.dll'
+ obj_h2 = Hash[ 'version' => 2.3 ]
+ obj_h2.default = 'library2.dll'
+ y.add( { 'link with' => [ obj_h, obj_h2 ] } )
+documents: 2
+
+---
+test: Special keys
+todo: true
+yaml: |
+ "!": These three keys
+ "&": had to be quoted
+ "=": and are normal strings.
+ # NOTE: the following node should NOT be serialized this way.
+ encoded node :
+ !special '!' : '!type'
+ !special|canonical '&' : 12
+ = : value
+ # The proper way to serialize the above node is as follows:
+ node : !!type &12 value
+ruby: |
+ { '!' => 'These three keys', '&' => 'had to be quoted',
+ '=' => 'and are normal strings.',
+ 'encoded node' => YAML::PrivateType.new( 'type', 'value' ),
+ 'node' => YAML::PrivateType.new( 'type', 'value' ) }
diff --git a/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml b/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml
new file mode 100644
index 0000000..46c8d4a
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/YtsTypeTransfers.yml
@@ -0,0 +1,244 @@
+--- %YAML:1.0
+test: Strings
+brief: >
+ Any group of characters beginning with an
+ alphabetic or numeric character is a string,
+ unless it belongs to one of the groups below
+ (such as an Integer or Time).
+yaml: |
+ String
+php: |
+ 'String'
+---
+test: String characters
+brief: >
+ A string can contain any alphabetic or
+ numeric character, along with many
+ punctuation characters, including the
+ period, dash, space, quotes, exclamation, and
+ question mark.
+yaml: |
+ - What's Yaml?
+ - It's for writing data structures in plain text.
+ - And?
+ - And what? That's not good enough for you?
+ - No, I mean, "And what about Yaml?"
+ - Oh, oh yeah. Uh.. Yaml for Ruby.
+php: |
+ array(
+ "What's Yaml?",
+ "It's for writing data structures in plain text.",
+ "And?",
+ "And what? That's not good enough for you?",
+ "No, I mean, \"And what about Yaml?\"",
+ "Oh, oh yeah. Uh.. Yaml for Ruby."
+ )
+---
+test: Indicators in Strings
+brief: >
+ Be careful using indicators in strings. In particular,
+ the comma, colon, and pound sign must be used carefully.
+yaml: |
+ the colon followed by space is an indicator: but is a string:right here
+ same for the pound sign: here we have it#in a string
+ the comma can, honestly, be used in most cases: [ but not in, inline collections ]
+php: |
+ array(
+ 'the colon followed by space is an indicator' => 'but is a string:right here',
+ 'same for the pound sign' => 'here we have it#in a string',
+ 'the comma can, honestly, be used in most cases' => array('but not in', 'inline collections')
+ )
+---
+test: Forcing Strings
+brief: >
+ Any YAML type can be forced into a string using the
+ explicit !str method.
+yaml: |
+ date string: !str 2001-08-01
+ number string: !str 192
+php: |
+ array(
+ 'date string' => '2001-08-01',
+ 'number string' => '192'
+ )
+---
+test: Single-quoted Strings
+brief: >
+ You can also enclose your strings within single quotes,
+ which allows use of slashes, colons, and other indicators
+ freely. Inside single quotes, you can represent a single
+ quote in your string by using two single quotes next to
+ each other.
+yaml: |
+ all my favorite symbols: '#:!/%.)'
+ a few i hate: '&(*'
+ why do i hate them?: 'it''s very hard to explain'
+ entities: '£ me'
+php: |
+ array(
+ 'all my favorite symbols' => '#:!/%.)',
+ 'a few i hate' => '&(*',
+ 'why do i hate them?' => 'it\'s very hard to explain',
+ 'entities' => '£ me'
+ )
+---
+test: Double-quoted Strings
+brief: >
+ Enclosing strings in double quotes allows you
+ to use escapings to represent ASCII and
+ Unicode characters.
+yaml: |
+ i know where i want my line breaks: "one here\nand another here\n"
+php: |
+ array(
+ 'i know where i want my line breaks' => "one here\nand another here\n"
+ )
+---
+test: Multi-line Quoted Strings
+todo: true
+brief: >
+ Both single- and double-quoted strings may be
+ carried on to new lines in your YAML document.
+ They must be indented a step and indentation
+ is interpreted as a single space.
+yaml: |
+ i want a long string: "so i'm going to
+ let it go on and on to other lines
+ until i end it with a quote."
+php: |
+ array('i want a long string' => "so i'm going to ".
+ "let it go on and on to other lines ".
+ "until i end it with a quote."
+ )
+
+---
+test: Plain scalars
+todo: true
+brief: >
+ Unquoted strings may also span multiple lines, if they
+ are free of YAML space indicators and indented.
+yaml: |
+ - My little toe is broken in two places;
+ - I'm crazy to have skied this way;
+ - I'm not the craziest he's seen, since there was always the German guy
+ who skied for 3 hours on a broken shin bone (just below the kneecap);
+ - Nevertheless, second place is respectable, and he doesn't
+ recommend going for the record;
+ - He's going to put my foot in plaster for a month;
+ - This would impair my skiing ability somewhat for the
+ duration, as can be imagined.
+php: |
+ array(
+ "My little toe is broken in two places;",
+ "I'm crazy to have skied this way;",
+ "I'm not the craziest he's seen, since there was always ".
+ "the German guy who skied for 3 hours on a broken shin ".
+ "bone (just below the kneecap);",
+ "Nevertheless, second place is respectable, and he doesn't ".
+ "recommend going for the record;",
+ "He's going to put my foot in plaster for a month;",
+ "This would impair my skiing ability somewhat for the duration, ".
+ "as can be imagined."
+ )
+---
+test: 'Null'
+brief: >
+ You can use the tilde '~' character for a null value.
+yaml: |
+ name: Mr. Show
+ hosted by: Bob and David
+ date of next season: ~
+php: |
+ array(
+ 'name' => 'Mr. Show',
+ 'hosted by' => 'Bob and David',
+ 'date of next season' => null
+ )
+---
+test: Boolean
+brief: >
+ You can use 'true' and 'false' for Boolean values.
+yaml: |
+ Is Gus a Liar?: true
+ Do I rely on Gus for Sustenance?: false
+php: |
+ array(
+ 'Is Gus a Liar?' => true,
+ 'Do I rely on Gus for Sustenance?' => false
+ )
+---
+test: Integers
+dump_skip: true
+brief: >
+ An integer is a series of numbers, optionally
+ starting with a positive or negative sign. Integers
+ may also contain commas for readability.
+yaml: |
+ zero: 0
+ simple: 12
+ one-thousand: 1,000
+ negative one-thousand: -1,000
+php: |
+ array(
+ 'zero' => 0,
+ 'simple' => 12,
+ 'one-thousand' => 1000.0,
+ 'negative one-thousand' => -1000.0
+ )
+---
+test: Integers as Map Keys
+brief: >
+ An integer can be used a dictionary key.
+yaml: |
+ 1: one
+ 2: two
+ 3: three
+php: |
+ array(
+ 1 => 'one',
+ 2 => 'two',
+ 3 => 'three'
+ )
+---
+test: Floats
+dump_skip: true
+brief: >
+ Floats are represented by numbers with decimals,
+ allowing for scientific notation, as well as
+ positive and negative infinity and "not a number."
+yaml: |
+ a simple float: 2.00
+ larger float: 1,000.09
+ scientific notation: 1.00009e+3
+php: |
+ array(
+ 'a simple float' => 2.0,
+ 'larger float' => 1000.09,
+ 'scientific notation' => 1000.09
+ )
+---
+test: Time
+todo: true
+brief: >
+ You can represent timestamps by using
+ ISO8601 format, or a variation which
+ allows spaces between the date, time and
+ time zone.
+yaml: |
+ iso8601: 2001-12-14t21:59:43.10-05:00
+ space separated: 2001-12-14 21:59:43.10 -05:00
+php: |
+ array(
+ 'iso8601' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" ),
+ 'space separated' => mktime( 2001, 12, 14, 21, 59, 43, 0.10, "-05:00" )
+ )
+---
+test: Date
+todo: true
+brief: >
+ A date can be represented by its year,
+ month and day in ISO8601 order.
+yaml: |
+ 1976-07-31
+php: |
+ date( 1976, 7, 31 )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml b/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml
new file mode 100644
index 0000000..ec456ed
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/embededPhp.yml
@@ -0,0 +1 @@
+value:
diff --git a/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml b/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml
new file mode 100644
index 0000000..6ca044c
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/escapedCharacters.yml
@@ -0,0 +1,155 @@
+test: outside double quotes
+yaml: |
+ \0 \ \a \b \n
+php: |
+ "\\0 \\ \\a \\b \\n"
+---
+test: null
+yaml: |
+ "\0"
+php: |
+ "\x00"
+---
+test: bell
+yaml: |
+ "\a"
+php: |
+ "\x07"
+---
+test: backspace
+yaml: |
+ "\b"
+php: |
+ "\x08"
+---
+test: horizontal tab (1)
+yaml: |
+ "\t"
+php: |
+ "\x09"
+---
+test: horizontal tab (2)
+yaml: |
+ "\ "
+php: |
+ "\x09"
+---
+test: line feed
+yaml: |
+ "\n"
+php: |
+ "\x0a"
+---
+test: vertical tab
+yaml: |
+ "\v"
+php: |
+ "\x0b"
+---
+test: form feed
+yaml: |
+ "\f"
+php: |
+ "\x0c"
+---
+test: carriage return
+yaml: |
+ "\r"
+php: |
+ "\x0d"
+---
+test: escape
+yaml: |
+ "\e"
+php: |
+ "\x1b"
+---
+test: space
+yaml: |
+ "\ "
+php: |
+ "\x20"
+---
+test: slash
+yaml: |
+ "\/"
+php: |
+ "\x2f"
+---
+test: backslash
+yaml: |
+ "\\"
+php: |
+ "\\"
+---
+test: Unicode next line
+yaml: |
+ "\N"
+php: |
+ "\xc2\x85"
+---
+test: Unicode non-breaking space
+yaml: |
+ "\_"
+php: |
+ "\xc2\xa0"
+---
+test: Unicode line separator
+yaml: |
+ "\L"
+php: |
+ "\xe2\x80\xa8"
+---
+test: Unicode paragraph separator
+yaml: |
+ "\P"
+php: |
+ "\xe2\x80\xa9"
+---
+test: Escaped 8-bit Unicode
+yaml: |
+ "\x42"
+php: |
+ "B"
+---
+test: Escaped 16-bit Unicode
+yaml: |
+ "\u20ac"
+php: |
+ "\xe2\x82\xac"
+---
+test: Escaped 32-bit Unicode
+yaml: |
+ "\U00000043"
+php: |
+ "C"
+---
+test: Example 5.13 Escaped Characters
+note: |
+ Currently throws an error parsing first line. Maybe Symfony Yaml doesn't support
+ continuation of string across multiple lines? Keeping test here but disabled.
+todo: true
+yaml: |
+ "Fun with \\
+ \" \a \b \e \f \
+ \n \r \t \v \0 \
+ \ \_ \N \L \P \
+ \x41 \u0041 \U00000041"
+php: |
+ "Fun with \x5C\n\x22 \x07 \x08 \x1B \x0C\n\x0A \x0D \x09 \x0B \x00\n\x20 \xA0 \x85 \xe2\x80\xa8 \xe2\x80\xa9\nA A A"
+---
+test: Double quotes with a line feed
+yaml: |
+ { double: "some value\n \"some quoted string\" and 'some single quotes one'" }
+php: |
+ array(
+ 'double' => "some value\n \"some quoted string\" and 'some single quotes one'"
+ )
+---
+test: Backslashes
+yaml: |
+ { single: 'foo\Var', no-quotes: foo\Var, double: "foo\\Var" }
+php: |
+ array(
+ 'single' => 'foo\Var', 'no-quotes' => 'foo\Var', 'double' => 'foo\Var'
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/index.yml b/vendor/symfony/yaml/Tests/Fixtures/index.yml
new file mode 100644
index 0000000..3216a89
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/index.yml
@@ -0,0 +1,18 @@
+- escapedCharacters
+- sfComments
+- sfCompact
+- sfTests
+- sfObjects
+- sfMergeKey
+- sfQuotes
+- YtsAnchorAlias
+- YtsBasicTests
+- YtsBlockMapping
+- YtsDocumentSeparator
+- YtsErrorTests
+- YtsFlowCollections
+- YtsFoldedScalars
+- YtsNullsAndEmpties
+- YtsSpecificationExamples
+- YtsTypeTransfers
+- unindentedCollections
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml b/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml
new file mode 100644
index 0000000..b72a9b6
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfComments.yml
@@ -0,0 +1,76 @@
+--- %YAML:1.0
+test: Comments at the end of a line
+brief: >
+ Comments at the end of a line
+yaml: |
+ ex1: "foo # bar"
+ ex2: "foo # bar" # comment
+ ex3: 'foo # bar' # comment
+ ex4: foo # comment
+ ex5: foo # comment with tab before
+ ex6: foo#foo # comment here
+ ex7: foo # ignore me # and me
+php: |
+ array('ex1' => 'foo # bar', 'ex2' => 'foo # bar', 'ex3' => 'foo # bar', 'ex4' => 'foo', 'ex5' => 'foo', 'ex6' => 'foo#foo', 'ex7' => 'foo')
+---
+test: Comments in the middle
+brief: >
+ Comments in the middle
+yaml: |
+ foo:
+ # some comment
+ # some comment
+ bar: foo
+ # some comment
+ # some comment
+php: |
+ array('foo' => array('bar' => 'foo'))
+---
+test: Comments on a hash line
+brief: >
+ Comments on a hash line
+yaml: |
+ foo: # a comment
+ foo: bar # a comment
+php: |
+ array('foo' => array('foo' => 'bar'))
+---
+test: 'Value starting with a #'
+brief: >
+ 'Value starting with a #'
+yaml: |
+ foo: '#bar'
+php: |
+ array('foo' => '#bar')
+---
+test: Document starting with a comment and a separator
+brief: >
+ Commenting before document start is allowed
+yaml: |
+ # document comment
+ ---
+ foo: bar # a comment
+php: |
+ array('foo' => 'bar')
+---
+test: Comment containing a colon on a hash line
+brief: >
+ Comment containing a colon on a scalar line
+yaml: 'foo # comment: this is also part of the comment'
+php: |
+ 'foo'
+---
+test: 'Hash key containing a #'
+brief: >
+ 'Hash key containing a #'
+yaml: 'foo#bar: baz'
+php: |
+ array('foo#bar' => 'baz')
+---
+test: 'Hash key ending with a space and a #'
+brief: >
+ 'Hash key ending with a space and a #'
+yaml: |
+ 'foo #': baz
+php: |
+ array('foo #' => 'baz')
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml b/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml
new file mode 100644
index 0000000..1339d23
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfCompact.yml
@@ -0,0 +1,159 @@
+--- %YAML:1.0
+test: Compact notation
+brief: |
+ Compact notation for sets of mappings with single element
+yaml: |
+ ---
+ # products purchased
+ - item : Super Hoop
+ - item : Basketball
+ quantity: 1
+ - item:
+ name: Big Shoes
+ nick: Biggies
+ quantity: 1
+php: |
+ array (
+ array (
+ 'item' => 'Super Hoop',
+ ),
+ array (
+ 'item' => 'Basketball',
+ 'quantity' => 1,
+ ),
+ array (
+ 'item' => array(
+ 'name' => 'Big Shoes',
+ 'nick' => 'Biggies'
+ ),
+ 'quantity' => 1
+ )
+ )
+---
+test: Compact notation combined with inline notation
+brief: |
+ Combinations of compact and inline notation are allowed
+yaml: |
+ ---
+ items:
+ - { item: Super Hoop, quantity: 1 }
+ - [ Basketball, Big Shoes ]
+php: |
+ array (
+ 'items' => array (
+ array (
+ 'item' => 'Super Hoop',
+ 'quantity' => 1,
+ ),
+ array (
+ 'Basketball',
+ 'Big Shoes'
+ )
+ )
+ )
+--- %YAML:1.0
+test: Compact notation
+brief: |
+ Compact notation for sets of mappings with single element
+yaml: |
+ ---
+ # products purchased
+ - item : Super Hoop
+ - item : Basketball
+ quantity: 1
+ - item:
+ name: Big Shoes
+ nick: Biggies
+ quantity: 1
+php: |
+ array (
+ array (
+ 'item' => 'Super Hoop',
+ ),
+ array (
+ 'item' => 'Basketball',
+ 'quantity' => 1,
+ ),
+ array (
+ 'item' => array(
+ 'name' => 'Big Shoes',
+ 'nick' => 'Biggies'
+ ),
+ 'quantity' => 1
+ )
+ )
+---
+test: Compact notation combined with inline notation
+brief: |
+ Combinations of compact and inline notation are allowed
+yaml: |
+ ---
+ items:
+ - { item: Super Hoop, quantity: 1 }
+ - [ Basketball, Big Shoes ]
+php: |
+ array (
+ 'items' => array (
+ array (
+ 'item' => 'Super Hoop',
+ 'quantity' => 1,
+ ),
+ array (
+ 'Basketball',
+ 'Big Shoes'
+ )
+ )
+ )
+--- %YAML:1.0
+test: Compact notation
+brief: |
+ Compact notation for sets of mappings with single element
+yaml: |
+ ---
+ # products purchased
+ - item : Super Hoop
+ - item : Basketball
+ quantity: 1
+ - item:
+ name: Big Shoes
+ nick: Biggies
+ quantity: 1
+php: |
+ array (
+ array (
+ 'item' => 'Super Hoop',
+ ),
+ array (
+ 'item' => 'Basketball',
+ 'quantity' => 1,
+ ),
+ array (
+ 'item' => array(
+ 'name' => 'Big Shoes',
+ 'nick' => 'Biggies'
+ ),
+ 'quantity' => 1
+ )
+ )
+---
+test: Compact notation combined with inline notation
+brief: |
+ Combinations of compact and inline notation are allowed
+yaml: |
+ ---
+ items:
+ - { item: Super Hoop, quantity: 1 }
+ - [ Basketball, Big Shoes ]
+php: |
+ array (
+ 'items' => array (
+ array (
+ 'item' => 'Super Hoop',
+ 'quantity' => 1,
+ ),
+ array (
+ 'Basketball',
+ 'Big Shoes'
+ )
+ )
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml b/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml
new file mode 100644
index 0000000..499446c
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfMergeKey.yml
@@ -0,0 +1,66 @@
+--- %YAML:1.0
+test: Simple In Place Substitution
+brief: >
+ If you want to reuse an entire alias, only overwriting what is different
+ you can use a << in place substitution. This is not part of the official
+ YAML spec, but a widely implemented extension. See the following URL for
+ details: http://yaml.org/type/merge.html
+yaml: |
+ foo: &foo
+ a: Steve
+ b: Clark
+ c: Brian
+ e: notnull
+ bar:
+ a: before
+ d: other
+ e: ~
+ <<: *foo
+ b: new
+ x: Oren
+ c:
+ foo: bar
+ foo: ignore
+ bar: foo
+ bar_inline: {a: before, d: other, <<: *foo, b: new, x: Oren, c: { foo: bar, foo: ignore, bar: foo}}
+ duplicate:
+ foo: bar
+ foo: ignore
+ foo2: &foo2
+ a: Ballmer
+ ding: &dong [ fi, fei, fo, fam]
+ check:
+ <<:
+ - *foo
+ - *dong
+ isit: tested
+ head:
+ <<: [ *foo , *dong , *foo2 ]
+ taz: &taz
+ a: Steve
+ w:
+ p: 1234
+ nested:
+ <<: *taz
+ d: Doug
+ w: &nestedref
+ p: 12345
+ z:
+ <<: *nestedref
+ head_inline: &head_inline { <<: [ *foo , *dong , *foo2 ] }
+ recursive_inline: { <<: *head_inline, c: { <<: *foo2 } }
+php: |
+ array(
+ 'foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'e' => 'notnull'),
+ 'bar' => array('a' => 'before', 'd' => 'other', 'e' => null, 'b' => 'new', 'c' => array('foo' => 'bar', 'bar' => 'foo'), 'x' => 'Oren'),
+ 'bar_inline' => array('a' => 'before', 'd' => 'other', 'b' => 'new', 'c' => array('foo' => 'bar', 'bar' => 'foo'), 'e' => 'notnull', 'x' => 'Oren'),
+ 'duplicate' => array('foo' => 'bar'),
+ 'foo2' => array('a' => 'Ballmer'),
+ 'ding' => array('fi', 'fei', 'fo', 'fam'),
+ 'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'e' => 'notnull', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'),
+ 'head' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'e' => 'notnull', 'fi', 'fei', 'fo', 'fam'),
+ 'taz' => array('a' => 'Steve', 'w' => array('p' => 1234)),
+ 'nested' => array('a' => 'Steve', 'w' => array('p' => 12345), 'd' => 'Doug', 'z' => array('p' => 12345)),
+ 'head_inline' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'e' => 'notnull', 'fi', 'fei', 'fo', 'fam'),
+ 'recursive_inline' => array('a' => 'Steve', 'b' => 'Clark', 'c' => array('a' => 'Ballmer'), 'e' => 'notnull', 'fi', 'fei', 'fo', 'fam'),
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml b/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml
new file mode 100644
index 0000000..ee124b2
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfObjects.yml
@@ -0,0 +1,11 @@
+--- %YAML:1.0
+test: Objects
+brief: >
+ Comments at the end of a line
+yaml: |
+ ex1: "foo # bar"
+ ex2: "foo # bar" # comment
+ ex3: 'foo # bar' # comment
+ ex4: foo # comment
+php: |
+ array('ex1' => 'foo # bar', 'ex2' => 'foo # bar', 'ex3' => 'foo # bar', 'ex4' => 'foo')
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml b/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml
new file mode 100644
index 0000000..7c60bae
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfQuotes.yml
@@ -0,0 +1,33 @@
+--- %YAML:1.0
+test: Some characters at the beginning of a string must be escaped
+brief: >
+ Some characters at the beginning of a string must be escaped
+yaml: |
+ foo: '| bar'
+php: |
+ array('foo' => '| bar')
+---
+test: A key can be a quoted string
+brief: >
+ A key can be a quoted string
+yaml: |
+ "foo1": bar
+ 'foo2': bar
+ "foo \" bar": bar
+ 'foo '' bar': bar
+ 'foo3: ': bar
+ "foo4: ": bar
+ foo5: { "foo \" bar: ": bar, 'foo '' bar: ': bar }
+php: |
+ array(
+ 'foo1' => 'bar',
+ 'foo2' => 'bar',
+ 'foo " bar' => 'bar',
+ 'foo \' bar' => 'bar',
+ 'foo3: ' => 'bar',
+ 'foo4: ' => 'bar',
+ 'foo5' => array(
+ 'foo " bar: ' => 'bar',
+ 'foo \' bar: ' => 'bar',
+ ),
+ )
diff --git a/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml b/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml
new file mode 100644
index 0000000..a427be1
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/sfTests.yml
@@ -0,0 +1,149 @@
+--- %YAML:1.0
+test: Multiple quoted string on one line
+brief: >
+ Multiple quoted string on one line
+yaml: |
+ stripped_title: { name: "foo bar", help: "bar foo" }
+php: |
+ array('stripped_title' => array('name' => 'foo bar', 'help' => 'bar foo'))
+---
+test: Empty sequence
+yaml: |
+ foo: [ ]
+php: |
+ array('foo' => array())
+---
+test: Empty value
+yaml: |
+ foo:
+php: |
+ array('foo' => null)
+---
+test: Inline string parsing
+brief: >
+ Inline string parsing
+yaml: |
+ test: ['complex: string', 'another [string]']
+php: |
+ array('test' => array('complex: string', 'another [string]'))
+---
+test: Boolean
+brief: >
+ Boolean
+yaml: |
+ - false
+ - true
+ - null
+ - ~
+ - 'false'
+ - 'true'
+ - 'null'
+ - '~'
+php: |
+ array(
+ false,
+ true,
+ null,
+ null,
+ 'false',
+ 'true',
+ 'null',
+ '~',
+ )
+---
+test: Empty lines in literal blocks
+brief: >
+ Empty lines in literal blocks
+yaml: |
+ foo:
+ bar: |
+ foo
+
+
+
+ bar
+php: |
+ array('foo' => array('bar' => "foo\n\n\n \nbar\n"))
+---
+test: Empty lines in folded blocks
+brief: >
+ Empty lines in folded blocks
+yaml: |
+ foo:
+ bar: >
+
+ foo
+
+
+ bar
+php: |
+ array('foo' => array('bar' => "\nfoo\n\nbar\n"))
+---
+test: IP addresses
+brief: >
+ IP addresses
+yaml: |
+ foo: 10.0.0.2
+php: |
+ array('foo' => '10.0.0.2')
+---
+test: A sequence with an embedded mapping
+brief: >
+ A sequence with an embedded mapping
+yaml: |
+ - foo
+ - bar: { bar: foo }
+php: |
+ array('foo', array('bar' => array('bar' => 'foo')))
+---
+test: A sequence with an unordered array
+brief: >
+ A sequence with an unordered array
+yaml: |
+ 1: foo
+ 0: bar
+php: |
+ array(1 => 'foo', 0 => 'bar')
+---
+test: Octal
+brief: as in spec example 2.19, octal value is converted
+yaml: |
+ foo: 0123
+php: |
+ array('foo' => 83)
+---
+test: Octal strings
+brief: Octal notation in a string must remain a string
+yaml: |
+ foo: "0123"
+php: |
+ array('foo' => '0123')
+---
+test: Octal strings
+brief: Octal notation in a string must remain a string
+yaml: |
+ foo: '0123'
+php: |
+ array('foo' => '0123')
+---
+test: Octal strings
+brief: Octal notation in a string must remain a string
+yaml: |
+ foo: |
+ 0123
+php: |
+ array('foo' => "0123\n")
+---
+test: Document as a simple hash
+brief: Document as a simple hash
+yaml: |
+ { foo: bar }
+php: |
+ array('foo' => 'bar')
+---
+test: Document as a simple array
+brief: Document as a simple array
+yaml: |
+ [ foo, bar ]
+php: |
+ array('foo', 'bar')
diff --git a/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml b/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml
new file mode 100644
index 0000000..0c96108
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/Fixtures/unindentedCollections.yml
@@ -0,0 +1,82 @@
+--- %YAML:1.0
+test: Unindented collection
+brief: >
+ Unindented collection
+yaml: |
+ collection:
+ - item1
+ - item2
+ - item3
+php: |
+ array('collection' => array('item1', 'item2', 'item3'))
+---
+test: Nested unindented collection (two levels)
+brief: >
+ Nested unindented collection
+yaml: |
+ collection:
+ key:
+ - a
+ - b
+ - c
+php: |
+ array('collection' => array('key' => array('a', 'b', 'c')))
+---
+test: Nested unindented collection (three levels)
+brief: >
+ Nested unindented collection
+yaml: |
+ collection:
+ key:
+ subkey:
+ - one
+ - two
+ - three
+php: |
+ array('collection' => array('key' => array('subkey' => array('one', 'two', 'three'))))
+---
+test: Key/value after unindented collection (1)
+brief: >
+ Key/value after unindented collection (1)
+yaml: |
+ collection:
+ key:
+ - a
+ - b
+ - c
+ foo: bar
+php: |
+ array('collection' => array('key' => array('a', 'b', 'c')), 'foo' => 'bar')
+---
+test: Key/value after unindented collection (at the same level)
+brief: >
+ Key/value after unindented collection
+yaml: |
+ collection:
+ key:
+ - a
+ - b
+ - c
+ foo: bar
+php: |
+ array('collection' => array('key' => array('a', 'b', 'c'), 'foo' => 'bar'))
+---
+test: Shortcut Key after unindented collection
+brief: >
+ Key/value after unindented collection
+yaml: |
+ collection:
+ - key: foo
+ foo: bar
+php: |
+ array('collection' => array(array('key' => 'foo', 'foo' => 'bar')))
+---
+test: Shortcut Key after unindented collection with custom spaces
+brief: >
+ Key/value after unindented collection
+yaml: |
+ collection:
+ - key: foo
+ foo: bar
+php: |
+ array('collection' => array(array('key' => 'foo', 'foo' => 'bar')))
diff --git a/vendor/symfony/yaml/Tests/InlineTest.php b/vendor/symfony/yaml/Tests/InlineTest.php
new file mode 100644
index 0000000..66ef701
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/InlineTest.php
@@ -0,0 +1,506 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Inline;
+
+class InlineTest extends TestCase
+{
+ /**
+ * @dataProvider getTestsForParse
+ */
+ public function testParse($yaml, $value)
+ {
+ $this->assertSame($value, Inline::parse($yaml), sprintf('::parse() converts an inline YAML to a PHP structure (%s)', $yaml));
+ }
+
+ /**
+ * @dataProvider getTestsForParseWithMapObjects
+ */
+ public function testParseWithMapObjects($yaml, $value)
+ {
+ $actual = Inline::parse($yaml, false, false, true);
+
+ $this->assertSame(serialize($value), serialize($actual));
+ }
+
+ /**
+ * @dataProvider getTestsForDump
+ */
+ public function testDump($yaml, $value)
+ {
+ $this->assertEquals($yaml, Inline::dump($value), sprintf('::dump() converts a PHP structure to an inline YAML (%s)', $yaml));
+
+ $this->assertSame($value, Inline::parse(Inline::dump($value)), 'check consistency');
+ }
+
+ public function testDumpNumericValueWithLocale()
+ {
+ $locale = setlocale(LC_NUMERIC, 0);
+ if (false === $locale) {
+ $this->markTestSkipped('Your platform does not support locales.');
+ }
+
+ try {
+ $requiredLocales = array('fr_FR.UTF-8', 'fr_FR.UTF8', 'fr_FR.utf-8', 'fr_FR.utf8', 'French_France.1252');
+ if (false === setlocale(LC_NUMERIC, $requiredLocales)) {
+ $this->markTestSkipped('Could not set any of required locales: '.implode(', ', $requiredLocales));
+ }
+
+ $this->assertEquals('1.2', Inline::dump(1.2));
+ $this->assertContains('fr', strtolower(setlocale(LC_NUMERIC, 0)));
+ setlocale(LC_NUMERIC, $locale);
+ } catch (\Exception $e) {
+ setlocale(LC_NUMERIC, $locale);
+ throw $e;
+ }
+ }
+
+ public function testHashStringsResemblingExponentialNumericsShouldNotBeChangedToINF()
+ {
+ $value = '686e444';
+
+ $this->assertSame($value, Inline::parse(Inline::dump($value)));
+ }
+
+ /**
+ * @group legacy
+ * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
+ */
+ public function testParseScalarWithNonEscapedBlackslashShouldThrowException()
+ {
+ $this->assertSame('Foo\Var', Inline::parse('"Foo\Var"'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseScalarWithNonEscapedBlackslashAtTheEndShouldThrowException()
+ {
+ Inline::parse('"Foo\\"');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseScalarWithIncorrectlyQuotedStringShouldThrowException()
+ {
+ $value = "'don't do somthin' like that'";
+ Inline::parse($value);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseScalarWithIncorrectlyDoubleQuotedStringShouldThrowException()
+ {
+ $value = '"don"t do somthin" like that"';
+ Inline::parse($value);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseInvalidMappingKeyShouldThrowException()
+ {
+ $value = '{ "foo " bar": "bar" }';
+ Inline::parse($value);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseInvalidMappingShouldThrowException()
+ {
+ Inline::parse('[foo] bar');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testParseInvalidSequenceShouldThrowException()
+ {
+ Inline::parse('{ foo: bar } bar');
+ }
+
+ public function testParseScalarWithCorrectlyQuotedStringShouldReturnString()
+ {
+ $value = "'don''t do somthin'' like that'";
+ $expect = "don't do somthin' like that";
+
+ $this->assertSame($expect, Inline::parseScalar($value));
+ }
+
+ /**
+ * @dataProvider getDataForParseReferences
+ */
+ public function testParseReferences($yaml, $expected)
+ {
+ $this->assertSame($expected, Inline::parse($yaml, false, false, false, array('var' => 'var-value')));
+ }
+
+ public function getDataForParseReferences()
+ {
+ return array(
+ 'scalar' => array('*var', 'var-value'),
+ 'list' => array('[ *var ]', array('var-value')),
+ 'list-in-list' => array('[[ *var ]]', array(array('var-value'))),
+ 'map-in-list' => array('[ { key: *var } ]', array(array('key' => 'var-value'))),
+ 'embedded-mapping-in-list' => array('[ key: *var ]', array(array('key' => 'var-value'))),
+ 'map' => array('{ key: *var }', array('key' => 'var-value')),
+ 'list-in-map' => array('{ key: [*var] }', array('key' => array('var-value'))),
+ 'map-in-map' => array('{ foo: { bar: *var } }', array('foo' => array('bar' => 'var-value'))),
+ );
+ }
+
+ public function testParseMapReferenceInSequence()
+ {
+ $foo = array(
+ 'a' => 'Steve',
+ 'b' => 'Clark',
+ 'c' => 'Brian',
+ );
+ $this->assertSame(array($foo), Inline::parse('[*foo]', false, false, false, array('foo' => $foo)));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage A reference must contain at least one character.
+ */
+ public function testParseUnquotedAsterisk()
+ {
+ Inline::parse('{ foo: * }');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage A reference must contain at least one character.
+ */
+ public function testParseUnquotedAsteriskFollowedByAComment()
+ {
+ Inline::parse('{ foo: * #foo }');
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Not quoting the scalar "@foo " starting with "@" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
+ * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
+ */
+ public function testParseUnquotedScalarStartingWithReservedAtIndicator()
+ {
+ Inline::parse('{ foo: @foo }');
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Not quoting the scalar "`foo " starting with "`" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
+ * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
+ */
+ public function testParseUnquotedScalarStartingWithReservedBacktickIndicator()
+ {
+ Inline::parse('{ foo: `foo }');
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Not quoting the scalar "|foo " starting with "|" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
+ * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
+ */
+ public function testParseUnquotedScalarStartingWithLiteralStyleIndicator()
+ {
+ Inline::parse('{ foo: |foo }');
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Not quoting the scalar ">foo " starting with ">" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
+ * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
+ */
+ public function testParseUnquotedScalarStartingWithFoldedStyleIndicator()
+ {
+ Inline::parse('{ foo: >foo }');
+ }
+
+ public function getScalarIndicators()
+ {
+ return array(array('|'), array('>'));
+ }
+
+ /**
+ * @dataProvider getDataForIsHash
+ */
+ public function testIsHash($array, $expected)
+ {
+ $this->assertSame($expected, Inline::isHash($array));
+ }
+
+ public function getDataForIsHash()
+ {
+ return array(
+ array(array(), false),
+ array(array(1, 2, 3), false),
+ array(array(2 => 1, 1 => 2, 0 => 3), true),
+ array(array('foo' => 1, 'bar' => 2), true),
+ );
+ }
+
+ public function getTestsForParse()
+ {
+ return array(
+ array('', ''),
+ array('null', null),
+ array('false', false),
+ array('true', true),
+ array('12', 12),
+ array('-12', -12),
+ array('"quoted string"', 'quoted string'),
+ array("'quoted string'", 'quoted string'),
+ array('12.30e+02', 12.30e+02),
+ array('0x4D2', 0x4D2),
+ array('02333', 02333),
+ array('.Inf', -log(0)),
+ array('-.Inf', log(0)),
+ array("'686e444'", '686e444'),
+ array('686e444', 646e444),
+ array('123456789123456789123456789123456789', '123456789123456789123456789123456789'),
+ array('"foo\r\nbar"', "foo\r\nbar"),
+ array("'foo#bar'", 'foo#bar'),
+ array("'foo # bar'", 'foo # bar'),
+ array("'#cfcfcf'", '#cfcfcf'),
+ array('::form_base.html.twig', '::form_base.html.twig'),
+
+ // Pre-YAML-1.2 booleans
+ array("'y'", 'y'),
+ array("'n'", 'n'),
+ array("'yes'", 'yes'),
+ array("'no'", 'no'),
+ array("'on'", 'on'),
+ array("'off'", 'off'),
+
+ array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)),
+ array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)),
+ array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)),
+ array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)),
+ array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)),
+
+ array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''),
+ array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
+
+ // sequences
+ // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon
+ array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)),
+ array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)),
+ array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
+
+ // mappings
+ array('{foo:bar,bar:foo,false:false,null:null,integer:12}', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
+ array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
+ array('{foo: \'bar\', bar: \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')),
+ array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')),
+ array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', array('foo\'' => 'bar', 'bar"' => 'foo: bar')),
+ array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', array('foo: ' => 'bar', 'bar: ' => 'foo: bar')),
+
+ // nested sequences and mappings
+ array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
+ array('[foo, {bar: foo}]', array('foo', array('bar' => 'foo'))),
+ array('{ foo: {bar: foo} }', array('foo' => array('bar' => 'foo'))),
+ array('{ foo: [bar, foo] }', array('foo' => array('bar', 'foo'))),
+
+ array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))),
+
+ array('[{ foo: {bar: foo} }]', array(array('foo' => array('bar' => 'foo')))),
+
+ array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
+
+ array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))),
+
+ array('[foo, bar: { foo: bar }]', array('foo', '1' => array('bar' => array('foo' => 'bar')))),
+ array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
+ );
+ }
+
+ public function getTestsForParseWithMapObjects()
+ {
+ return array(
+ array('', ''),
+ array('null', null),
+ array('false', false),
+ array('true', true),
+ array('12', 12),
+ array('-12', -12),
+ array('"quoted string"', 'quoted string'),
+ array("'quoted string'", 'quoted string'),
+ array('12.30e+02', 12.30e+02),
+ array('0x4D2', 0x4D2),
+ array('02333', 02333),
+ array('.Inf', -log(0)),
+ array('-.Inf', log(0)),
+ array("'686e444'", '686e444'),
+ array('686e444', 646e444),
+ array('123456789123456789123456789123456789', '123456789123456789123456789123456789'),
+ array('"foo\r\nbar"', "foo\r\nbar"),
+ array("'foo#bar'", 'foo#bar'),
+ array("'foo # bar'", 'foo # bar'),
+ array("'#cfcfcf'", '#cfcfcf'),
+ array('::form_base.html.twig', '::form_base.html.twig'),
+
+ array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)),
+ array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)),
+ array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)),
+ array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)),
+ array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)),
+
+ array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''),
+ array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
+
+ // sequences
+ // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon
+ array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)),
+ array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)),
+ array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
+
+ // mappings
+ array('{foo:bar,bar:foo,false:false,null:null,integer:12}', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
+ array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
+ array('{foo: \'bar\', bar: \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
+ array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
+ array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', (object) array('foo\'' => 'bar', 'bar"' => 'foo: bar')),
+ array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', 'bar: ' => 'foo: bar')),
+
+ // nested sequences and mappings
+ array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
+ array('[foo, {bar: foo}]', array('foo', (object) array('bar' => 'foo'))),
+ array('{ foo: {bar: foo} }', (object) array('foo' => (object) array('bar' => 'foo'))),
+ array('{ foo: [bar, foo] }', (object) array('foo' => array('bar', 'foo'))),
+
+ array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))),
+
+ array('[{ foo: {bar: foo} }]', array((object) array('foo' => (object) array('bar' => 'foo')))),
+
+ array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
+
+ array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', (object) array('bar' => 'foo', 'foo' => array('foo', (object) array('bar' => 'foo'))), array('foo', (object) array('bar' => 'foo')))),
+
+ array('[foo, bar: { foo: bar }]', array('foo', '1' => (object) array('bar' => (object) array('foo' => 'bar')))),
+ array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', (object) array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
+
+ array('{}', new \stdClass()),
+ array('{ foo : bar, bar : {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
+ array('{ foo : [], bar : {} }', (object) array('foo' => array(), 'bar' => new \stdClass())),
+ array('{foo: \'bar\', bar: {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
+ array('{\'foo\': \'bar\', "bar": {}}', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
+ array('{\'foo\': \'bar\', "bar": \'{}\'}', (object) array('foo' => 'bar', 'bar' => '{}')),
+
+ array('[foo, [{}, {}]]', array('foo', array(new \stdClass(), new \stdClass()))),
+ array('[foo, [[], {}]]', array('foo', array(array(), new \stdClass()))),
+ array('[foo, [[{}, {}], {}]]', array('foo', array(array(new \stdClass(), new \stdClass()), new \stdClass()))),
+ array('[foo, {bar: {}}]', array('foo', '1' => (object) array('bar' => new \stdClass()))),
+ );
+ }
+
+ public function getTestsForDump()
+ {
+ return array(
+ array('null', null),
+ array('false', false),
+ array('true', true),
+ array('12', 12),
+ array("'quoted string'", 'quoted string'),
+ array('!!float 1230', 12.30e+02),
+ array('1234', 0x4D2),
+ array('1243', 02333),
+ array('.Inf', -log(0)),
+ array('-.Inf', log(0)),
+ array("'686e444'", '686e444'),
+ array('"foo\r\nbar"', "foo\r\nbar"),
+ array("'foo#bar'", 'foo#bar'),
+ array("'foo # bar'", 'foo # bar'),
+ array("'#cfcfcf'", '#cfcfcf'),
+
+ array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
+
+ array("'-dash'", '-dash'),
+ array("'-'", '-'),
+
+ // Pre-YAML-1.2 booleans
+ array("'y'", 'y'),
+ array("'n'", 'n'),
+ array("'yes'", 'yes'),
+ array("'no'", 'no'),
+ array("'on'", 'on'),
+ array("'off'", 'off'),
+
+ // sequences
+ array('[foo, bar, false, null, 12]', array('foo', 'bar', false, null, 12)),
+ array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
+
+ // mappings
+ array('{ foo: bar, bar: foo, \'false\': false, \'null\': null, integer: 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
+ array('{ foo: bar, bar: \'foo: bar\' }', array('foo' => 'bar', 'bar' => 'foo: bar')),
+
+ // nested sequences and mappings
+ array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
+
+ array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
+
+ array('{ foo: { bar: foo } }', array('foo' => array('bar' => 'foo'))),
+
+ array('[foo, { bar: foo }]', array('foo', array('bar' => 'foo'))),
+
+ array('[foo, { bar: foo, foo: [foo, { bar: foo }] }, [foo, { bar: foo }]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))),
+
+ array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
+
+ array('{ foo: { bar: { 1: 2, baz: 3 } } }', array('foo' => array('bar' => array(1 => 2, 'baz' => 3)))),
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage Malformed inline YAML string: {this, is not, supported}.
+ */
+ public function testNotSupportedMissingValue()
+ {
+ Inline::parse('{this, is not, supported}');
+ }
+
+ public function testVeryLongQuotedStrings()
+ {
+ $longStringWithQuotes = str_repeat("x\r\n\\\"x\"x", 1000);
+
+ $yamlString = Inline::dump(array('longStringWithQuotes' => $longStringWithQuotes));
+ $arrayFromYaml = Inline::parse($yamlString);
+
+ $this->assertEquals($longStringWithQuotes, $arrayFromYaml['longStringWithQuotes']);
+ }
+
+ public function testBooleanMappingKeysAreConvertedToStrings()
+ {
+ $this->assertSame(array('false' => 'foo'), Inline::parse('{false: foo}'));
+ $this->assertSame(array('true' => 'foo'), Inline::parse('{true: foo}'));
+ }
+
+ public function testTheEmptyStringIsAValidMappingKey()
+ {
+ $this->assertSame(array('' => 'foo'), Inline::parse('{ "": foo }'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage Unexpected end of line, expected one of ",}".
+ */
+ public function testUnfinishedInlineMap()
+ {
+ Inline::parse("{abc: 'def'");
+ }
+}
diff --git a/vendor/symfony/yaml/Tests/ParseExceptionTest.php b/vendor/symfony/yaml/Tests/ParseExceptionTest.php
new file mode 100644
index 0000000..b7797fb
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/ParseExceptionTest.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Exception\ParseException;
+
+class ParseExceptionTest extends TestCase
+{
+ public function testGetMessage()
+ {
+ $exception = new ParseException('Error message', 42, 'foo: bar', '/var/www/app/config.yml');
+ if (\PHP_VERSION_ID >= 50400) {
+ $message = 'Error message in "/var/www/app/config.yml" at line 42 (near "foo: bar")';
+ } else {
+ $message = 'Error message in "\\/var\\/www\\/app\\/config.yml" at line 42 (near "foo: bar")';
+ }
+
+ $this->assertEquals($message, $exception->getMessage());
+ }
+
+ public function testGetMessageWithUnicodeInFilename()
+ {
+ $exception = new ParseException('Error message', 42, 'foo: bar', 'äöü.yml');
+ if (\PHP_VERSION_ID >= 50400) {
+ $message = 'Error message in "äöü.yml" at line 42 (near "foo: bar")';
+ } else {
+ $message = 'Error message in "\u00e4\u00f6\u00fc.yml" at line 42 (near "foo: bar")';
+ }
+
+ $this->assertEquals($message, $exception->getMessage());
+ }
+}
diff --git a/vendor/symfony/yaml/Tests/ParserTest.php b/vendor/symfony/yaml/Tests/ParserTest.php
new file mode 100644
index 0000000..0cf9bd4
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/ParserTest.php
@@ -0,0 +1,1300 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Parser;
+use Symfony\Component\Yaml\Yaml;
+
+class ParserTest extends TestCase
+{
+ /** @var Parser */
+ protected $parser;
+
+ protected function setUp()
+ {
+ $this->parser = new Parser();
+ }
+
+ protected function tearDown()
+ {
+ $this->parser = null;
+ }
+
+ /**
+ * @dataProvider getDataFormSpecifications
+ */
+ public function testSpecifications($file, $expected, $yaml, $comment)
+ {
+ $this->assertEquals($expected, var_export($this->parser->parse($yaml), true), $comment);
+ }
+
+ public function getDataFormSpecifications()
+ {
+ $parser = new Parser();
+ $path = __DIR__.'/Fixtures';
+
+ $tests = array();
+ $files = $parser->parse(file_get_contents($path.'/index.yml'));
+ foreach ($files as $file) {
+ $yamls = file_get_contents($path.'/'.$file.'.yml');
+
+ // split YAMLs documents
+ foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) {
+ if (!$yaml) {
+ continue;
+ }
+
+ $test = $parser->parse($yaml);
+ if (isset($test['todo']) && $test['todo']) {
+ // TODO
+ } else {
+ eval('$expected = '.trim($test['php']).';');
+
+ $tests[] = array($file, var_export($expected, true), $test['yaml'], $test['test']);
+ }
+ }
+ }
+
+ return $tests;
+ }
+
+ public function testTabsInYaml()
+ {
+ // test tabs in YAML
+ $yamls = array(
+ "foo:\n bar",
+ "foo:\n bar",
+ "foo:\n bar",
+ "foo:\n bar",
+ );
+
+ foreach ($yamls as $yaml) {
+ try {
+ $content = $this->parser->parse($yaml);
+
+ $this->fail('YAML files must not contain tabs');
+ } catch (\Exception $e) {
+ $this->assertInstanceOf('\Exception', $e, 'YAML files must not contain tabs');
+ $this->assertEquals('A YAML file cannot contain tabs as indentation at line 2 (near "'.strpbrk($yaml, "\t").'").', $e->getMessage(), 'YAML files must not contain tabs');
+ }
+ }
+ }
+
+ public function testEndOfTheDocumentMarker()
+ {
+ $yaml = <<<'EOF'
+--- %YAML:1.0
+foo
+...
+EOF;
+
+ $this->assertEquals('foo', $this->parser->parse($yaml));
+ }
+
+ public function getBlockChompingTests()
+ {
+ $tests = array();
+
+ $yaml = <<<'EOF'
+foo: |-
+ one
+ two
+bar: |-
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo",
+ 'bar' => "one\ntwo",
+ );
+ $tests['Literal block chomping strip with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |-
+ one
+ two
+
+bar: |-
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo",
+ 'bar' => "one\ntwo",
+ );
+ $tests['Literal block chomping strip with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+{}
+
+
+EOF;
+ $expected = array();
+ $tests['Literal block chomping strip with multiple trailing newlines after a 1-liner'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |-
+ one
+ two
+bar: |-
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo",
+ 'bar' => "one\ntwo",
+ );
+ $tests['Literal block chomping strip without trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |
+ one
+ two
+bar: |
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n",
+ 'bar' => "one\ntwo\n",
+ );
+ $tests['Literal block chomping clip with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |
+ one
+ two
+
+bar: |
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n",
+ 'bar' => "one\ntwo\n",
+ );
+ $tests['Literal block chomping clip with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo:
+- bar: |
+ one
+
+ two
+EOF;
+ $expected = array(
+ 'foo' => array(
+ array(
+ 'bar' => "one\n\ntwo",
+ ),
+ ),
+ );
+ $tests['Literal block chomping clip with embedded blank line inside unindented collection'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |
+ one
+ two
+bar: |
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n",
+ 'bar' => "one\ntwo",
+ );
+ $tests['Literal block chomping clip without trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |+
+ one
+ two
+bar: |+
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n",
+ 'bar' => "one\ntwo\n",
+ );
+ $tests['Literal block chomping keep with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |+
+ one
+ two
+
+bar: |+
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n\n",
+ 'bar' => "one\ntwo\n\n",
+ );
+ $tests['Literal block chomping keep with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: |+
+ one
+ two
+bar: |+
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => "one\ntwo\n",
+ 'bar' => "one\ntwo",
+ );
+ $tests['Literal block chomping keep without trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >-
+ one
+ two
+bar: >-
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => 'one two',
+ 'bar' => 'one two',
+ );
+ $tests['Folded block chomping strip with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >-
+ one
+ two
+
+bar: >-
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => 'one two',
+ 'bar' => 'one two',
+ );
+ $tests['Folded block chomping strip with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >-
+ one
+ two
+bar: >-
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => 'one two',
+ 'bar' => 'one two',
+ );
+ $tests['Folded block chomping strip without trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >
+ one
+ two
+bar: >
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => "one two\n",
+ 'bar' => "one two\n",
+ );
+ $tests['Folded block chomping clip with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >
+ one
+ two
+
+bar: >
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => "one two\n",
+ 'bar' => "one two\n",
+ );
+ $tests['Folded block chomping clip with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >
+ one
+ two
+bar: >
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => "one two\n",
+ 'bar' => 'one two',
+ );
+ $tests['Folded block chomping clip without trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >+
+ one
+ two
+bar: >+
+ one
+ two
+
+EOF;
+ $expected = array(
+ 'foo' => "one two\n",
+ 'bar' => "one two\n",
+ );
+ $tests['Folded block chomping keep with single trailing newline'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >+
+ one
+ two
+
+bar: >+
+ one
+ two
+
+
+EOF;
+ $expected = array(
+ 'foo' => "one two\n\n",
+ 'bar' => "one two\n\n",
+ );
+ $tests['Folded block chomping keep with multiple trailing newlines'] = array($expected, $yaml);
+
+ $yaml = <<<'EOF'
+foo: >+
+ one
+ two
+bar: >+
+ one
+ two
+EOF;
+ $expected = array(
+ 'foo' => "one two\n",
+ 'bar' => 'one two',
+ );
+ $tests['Folded block chomping keep without trailing newline'] = array($expected, $yaml);
+
+ return $tests;
+ }
+
+ /**
+ * @dataProvider getBlockChompingTests
+ */
+ public function testBlockChomping($expected, $yaml)
+ {
+ $this->assertSame($expected, $this->parser->parse($yaml));
+ }
+
+ /**
+ * Regression test for issue #7989.
+ *
+ * @see https://github.com/symfony/symfony/issues/7989
+ */
+ public function testBlockLiteralWithLeadingNewlines()
+ {
+ $yaml = <<<'EOF'
+foo: |-
+
+
+ bar
+
+EOF;
+ $expected = array(
+ 'foo' => "\n\nbar",
+ );
+
+ $this->assertSame($expected, $this->parser->parse($yaml));
+ }
+
+ public function testObjectSupportEnabled()
+ {
+ $input = <<<'EOF'
+foo: !!php/object:O:30:"Symfony\Component\Yaml\Tests\B":1:{s:1:"b";s:3:"foo";}
+bar: 1
+EOF;
+ $this->assertEquals(array('foo' => new B(), 'bar' => 1), $this->parser->parse($input, false, true), '->parse() is able to parse objects');
+
+ $input = <<<'EOF'
+foo: !php/object:O:30:"Symfony\Component\Yaml\Tests\B":1:{s:1:"b";s:3:"foo";}
+bar: 1
+EOF;
+ $this->assertEquals(array('foo' => new B(), 'bar' => 1), $this->parser->parse($input, false, true), '->parse() is able to parse objects');
+ }
+
+ /**
+ * @dataProvider invalidDumpedObjectProvider
+ */
+ public function testObjectSupportDisabledButNoExceptions($input)
+ {
+ $this->assertEquals(array('foo' => null, 'bar' => 1), $this->parser->parse($input), '->parse() does not parse objects');
+ }
+
+ /**
+ * @dataProvider getObjectForMapTests
+ */
+ public function testObjectForMap($yaml, $expected)
+ {
+ $this->assertEquals($expected, $this->parser->parse($yaml, false, false, true));
+ }
+
+ public function getObjectForMapTests()
+ {
+ $tests = array();
+
+ $yaml = <<<'EOF'
+foo:
+ fiz: [cat]
+EOF;
+ $expected = new \stdClass();
+ $expected->foo = new \stdClass();
+ $expected->foo->fiz = array('cat');
+ $tests['mapping'] = array($yaml, $expected);
+
+ $yaml = '{ "foo": "bar", "fiz": "cat" }';
+ $expected = new \stdClass();
+ $expected->foo = 'bar';
+ $expected->fiz = 'cat';
+ $tests['inline-mapping'] = array($yaml, $expected);
+
+ $yaml = "foo: bar\nbaz: foobar";
+ $expected = new \stdClass();
+ $expected->foo = 'bar';
+ $expected->baz = 'foobar';
+ $tests['object-for-map-is-applied-after-parsing'] = array($yaml, $expected);
+
+ $yaml = <<<'EOT'
+array:
+ - key: one
+ - key: two
+EOT;
+ $expected = new \stdClass();
+ $expected->array = array();
+ $expected->array[0] = new \stdClass();
+ $expected->array[0]->key = 'one';
+ $expected->array[1] = new \stdClass();
+ $expected->array[1]->key = 'two';
+ $tests['nest-map-and-sequence'] = array($yaml, $expected);
+
+ $yaml = <<<'YAML'
+map:
+ 1: one
+ 2: two
+YAML;
+ $expected = new \stdClass();
+ $expected->map = new \stdClass();
+ $expected->map->{1} = 'one';
+ $expected->map->{2} = 'two';
+ $tests['numeric-keys'] = array($yaml, $expected);
+
+ $yaml = <<<'YAML'
+map:
+ 0: one
+ 1: two
+YAML;
+ $expected = new \stdClass();
+ $expected->map = new \stdClass();
+ $expected->map->{0} = 'one';
+ $expected->map->{1} = 'two';
+ $tests['zero-indexed-numeric-keys'] = array($yaml, $expected);
+
+ return $tests;
+ }
+
+ /**
+ * @dataProvider invalidDumpedObjectProvider
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testObjectsSupportDisabledWithExceptions($yaml)
+ {
+ $this->parser->parse($yaml, true, false);
+ }
+
+ public function invalidDumpedObjectProvider()
+ {
+ $yamlTag = <<<'EOF'
+foo: !!php/object:O:30:"Symfony\Tests\Component\Yaml\B":1:{s:1:"b";s:3:"foo";}
+bar: 1
+EOF;
+ $localTag = <<<'EOF'
+foo: !php/object:O:30:"Symfony\Tests\Component\Yaml\B":1:{s:1:"b";s:3:"foo";}
+bar: 1
+EOF;
+
+ return array(
+ 'yaml-tag' => array($yamlTag),
+ 'local-tag' => array($localTag),
+ );
+ }
+
+ /**
+ * @requires extension iconv
+ */
+ public function testNonUtf8Exception()
+ {
+ $yamls = array(
+ iconv('UTF-8', 'ISO-8859-1', "foo: 'äöüß'"),
+ iconv('UTF-8', 'ISO-8859-15', "euro: '€'"),
+ iconv('UTF-8', 'CP1252', "cp1252: '©ÉÇáñ'"),
+ );
+
+ foreach ($yamls as $yaml) {
+ try {
+ $this->parser->parse($yaml);
+
+ $this->fail('charsets other than UTF-8 are rejected.');
+ } catch (\Exception $e) {
+ $this->assertInstanceOf('Symfony\Component\Yaml\Exception\ParseException', $e, 'charsets other than UTF-8 are rejected.');
+ }
+ }
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testUnindentedCollectionException()
+ {
+ $yaml = <<<'EOF'
+
+collection:
+-item1
+-item2
+-item3
+
+EOF;
+
+ $this->parser->parse($yaml);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testShortcutKeyUnindentedCollectionException()
+ {
+ $yaml = <<<'EOF'
+
+collection:
+- key: foo
+ foo: bar
+
+EOF;
+
+ $this->parser->parse($yaml);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessageRegExp /^Multiple documents are not supported.+/
+ */
+ public function testMultipleDocumentsNotSupportedException()
+ {
+ Yaml::parse(<<<'EOL'
+# Ranking of 1998 home runs
+---
+- Mark McGwire
+- Sammy Sosa
+- Ken Griffey
+
+# Team ranking
+---
+- Chicago Cubs
+- St Louis Cardinals
+EOL
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testSequenceInAMapping()
+ {
+ Yaml::parse(<<<'EOF'
+yaml:
+ hash: me
+ - array stuff
+EOF
+ );
+ }
+
+ public function testSequenceInMappingStartedBySingleDashLine()
+ {
+ $yaml = <<<'EOT'
+a:
+-
+ b:
+ -
+ bar: baz
+- foo
+d: e
+EOT;
+ $expected = array(
+ 'a' => array(
+ array(
+ 'b' => array(
+ array(
+ 'bar' => 'baz',
+ ),
+ ),
+ ),
+ 'foo',
+ ),
+ 'd' => 'e',
+ );
+
+ $this->assertSame($expected, $this->parser->parse($yaml));
+ }
+
+ public function testSequenceFollowedByCommentEmbeddedInMapping()
+ {
+ $yaml = <<<'EOT'
+a:
+ b:
+ - c
+# comment
+ d: e
+EOT;
+ $expected = array(
+ 'a' => array(
+ 'b' => array('c'),
+ 'd' => 'e',
+ ),
+ );
+
+ $this->assertSame($expected, $this->parser->parse($yaml));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ */
+ public function testMappingInASequence()
+ {
+ Yaml::parse(<<<'EOF'
+yaml:
+ - array stuff
+ hash: me
+EOF
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage missing colon
+ */
+ public function testScalarInSequence()
+ {
+ Yaml::parse(<<<'EOF'
+foo:
+ - bar
+"missing colon"
+ foo: bar
+EOF
+ );
+ }
+
+ /**
+ * > It is an error for two equal keys to appear in the same mapping node.
+ * > In such a case the YAML processor may continue, ignoring the second
+ * > `key: value` pair and issuing an appropriate warning. This strategy
+ * > preserves a consistent information model for one-pass and random access
+ * > applications.
+ *
+ * @see http://yaml.org/spec/1.2/spec.html#id2759572
+ * @see http://yaml.org/spec/1.1/#id932806
+ */
+ public function testMappingDuplicateKeyBlock()
+ {
+ $input = <<<'EOD'
+parent:
+ child: first
+ child: duplicate
+parent:
+ child: duplicate
+ child: duplicate
+EOD;
+ $expected = array(
+ 'parent' => array(
+ 'child' => 'first',
+ ),
+ );
+ $this->assertSame($expected, Yaml::parse($input));
+ }
+
+ public function testMappingDuplicateKeyFlow()
+ {
+ $input = <<<'EOD'
+parent: { child: first, child: duplicate }
+parent: { child: duplicate, child: duplicate }
+EOD;
+ $expected = array(
+ 'parent' => array(
+ 'child' => 'first',
+ ),
+ );
+ $this->assertSame($expected, Yaml::parse($input));
+ }
+
+ public function testEmptyValue()
+ {
+ $input = <<<'EOF'
+hash:
+EOF;
+
+ $this->assertEquals(array('hash' => null), Yaml::parse($input));
+ }
+
+ public function testCommentAtTheRootIndent()
+ {
+ $this->assertEquals(array(
+ 'services' => array(
+ 'app.foo_service' => array(
+ 'class' => 'Foo',
+ ),
+ 'app/bar_service' => array(
+ 'class' => 'Bar',
+ ),
+ ),
+ ), Yaml::parse(<<<'EOF'
+# comment 1
+services:
+# comment 2
+ # comment 3
+ app.foo_service:
+ class: Foo
+# comment 4
+ # comment 5
+ app/bar_service:
+ class: Bar
+EOF
+ ));
+ }
+
+ public function testStringBlockWithComments()
+ {
+ $this->assertEquals(array('content' => <<<'EOT'
+# comment 1
+header
+
+ # comment 2
+
+ title
+
+
+footer # comment3
+EOT
+ ), Yaml::parse(<<<'EOF'
+content: |
+ # comment 1
+ header
+
+ # comment 2
+
+ title
+
+
+ footer # comment3
+EOF
+ ));
+ }
+
+ public function testFoldedStringBlockWithComments()
+ {
+ $this->assertEquals(array(array('content' => <<<'EOT'
+# comment 1
+header
+
+ # comment 2
+
+ title
+
+
+footer # comment3
+EOT
+ )), Yaml::parse(<<<'EOF'
+-
+ content: |
+ # comment 1
+ header
+
+ # comment 2
+
+ title
+
+
+ footer # comment3
+EOF
+ ));
+ }
+
+ public function testNestedFoldedStringBlockWithComments()
+ {
+ $this->assertEquals(array(array(
+ 'title' => 'some title',
+ 'content' => <<<'EOT'
+# comment 1
+header
+
+ # comment 2
+
+ title
+
+
+footer # comment3
+EOT
+ )), Yaml::parse(<<<'EOF'
+-
+ title: some title
+ content: |
+ # comment 1
+ header
+
+ # comment 2
+
+ title
+
+
+ footer # comment3
+EOF
+ ));
+ }
+
+ public function testReferenceResolvingInInlineStrings()
+ {
+ $this->assertEquals(array(
+ 'var' => 'var-value',
+ 'scalar' => 'var-value',
+ 'list' => array('var-value'),
+ 'list_in_list' => array(array('var-value')),
+ 'map_in_list' => array(array('key' => 'var-value')),
+ 'embedded_mapping' => array(array('key' => 'var-value')),
+ 'map' => array('key' => 'var-value'),
+ 'list_in_map' => array('key' => array('var-value')),
+ 'map_in_map' => array('foo' => array('bar' => 'var-value')),
+ ), Yaml::parse(<<<'EOF'
+var: &var var-value
+scalar: *var
+list: [ *var ]
+list_in_list: [[ *var ]]
+map_in_list: [ { key: *var } ]
+embedded_mapping: [ key: *var ]
+map: { key: *var }
+list_in_map: { key: [*var] }
+map_in_map: { foo: { bar: *var } }
+EOF
+ ));
+ }
+
+ public function testYamlDirective()
+ {
+ $yaml = <<<'EOF'
+%YAML 1.2
+---
+foo: 1
+bar: 2
+EOF;
+ $this->assertEquals(array('foo' => 1, 'bar' => 2), $this->parser->parse($yaml));
+ }
+
+ public function testFloatKeys()
+ {
+ $yaml = <<<'EOF'
+foo:
+ 1.2: "bar"
+ 1.3: "baz"
+EOF;
+
+ $expected = array(
+ 'foo' => array(
+ '1.2' => 'bar',
+ '1.3' => 'baz',
+ ),
+ );
+
+ $this->assertEquals($expected, $this->parser->parse($yaml));
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation Using a colon in the unquoted mapping value "bar: baz" in line 1 is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
+ * throw ParseException in Symfony 3.0
+ */
+ public function testColonInMappingValueException()
+ {
+ $yaml = <<<'EOF'
+foo: bar: baz
+EOF;
+
+ $this->parser->parse($yaml);
+ }
+
+ public function testColonInMappingValueExceptionNotTriggeredByColonInComment()
+ {
+ $yaml = <<<'EOT'
+foo:
+ bar: foobar # Note: a comment after a colon
+EOT;
+
+ $this->assertSame(array('foo' => array('bar' => 'foobar')), $this->parser->parse($yaml));
+ }
+
+ /**
+ * @dataProvider getCommentLikeStringInScalarBlockData
+ */
+ public function testCommentLikeStringsAreNotStrippedInBlockScalars($yaml, $expectedParserResult)
+ {
+ $this->assertSame($expectedParserResult, $this->parser->parse($yaml));
+ }
+
+ public function getCommentLikeStringInScalarBlockData()
+ {
+ $tests = array();
+
+ $yaml = <<<'EOT'
+pages:
+ -
+ title: some title
+ content: |
+ # comment 1
+ header
+
+ # comment 2
+
+ title
+
+
+ footer # comment3
+EOT;
+ $expected = array(
+ 'pages' => array(
+ array(
+ 'title' => 'some title',
+ 'content' => <<<'EOT'
+# comment 1
+header
+
+ # comment 2
+
+ title
+
+
+footer # comment3
+EOT
+ ,
+ ),
+ ),
+ );
+ $tests[] = array($yaml, $expected);
+
+ $yaml = <<<'EOT'
+test: |
+ foo
+ # bar
+ baz
+collection:
+ - one: |
+ foo
+ # bar
+ baz
+ - two: |
+ foo
+ # bar
+ baz
+EOT;
+ $expected = array(
+ 'test' => <<<'EOT'
+foo
+# bar
+baz
+
+EOT
+ ,
+ 'collection' => array(
+ array(
+ 'one' => <<<'EOT'
+foo
+# bar
+baz
+
+EOT
+ ,
+ ),
+ array(
+ 'two' => <<<'EOT'
+foo
+# bar
+baz
+EOT
+ ,
+ ),
+ ),
+ );
+ $tests[] = array($yaml, $expected);
+
+ $yaml = <<<'EOT'
+foo:
+ bar:
+ scalar-block: >
+ line1
+ line2>
+ baz:
+# comment
+ foobar: ~
+EOT;
+ $expected = array(
+ 'foo' => array(
+ 'bar' => array(
+ 'scalar-block' => "line1 line2>\n",
+ ),
+ 'baz' => array(
+ 'foobar' => null,
+ ),
+ ),
+ );
+ $tests[] = array($yaml, $expected);
+
+ $yaml = <<<'EOT'
+a:
+ b: hello
+# c: |
+# first row
+# second row
+ d: hello
+EOT;
+ $expected = array(
+ 'a' => array(
+ 'b' => 'hello',
+ 'd' => 'hello',
+ ),
+ );
+ $tests[] = array($yaml, $expected);
+
+ return $tests;
+ }
+
+ public function testBlankLinesAreParsedAsNewLinesInFoldedBlocks()
+ {
+ $yaml = <<<'EOT'
+test: >
+ A heading
+
+
+ a list
+ may be a good example
+
+EOT;
+
+ $this->assertSame(
+ array(
+ 'test' => <<<'EOT'
+A heading
+ a list may be a good example
+EOT
+ ,
+ ),
+ $this->parser->parse($yaml)
+ );
+ }
+
+ public function testAdditionallyIndentedLinesAreParsedAsNewLinesInFoldedBlocks()
+ {
+ $yaml = <<<'EOT'
+test: >
+ A heading
+
+
+ a list
+ may be a good example
+
+EOT;
+
+ $this->assertSame(
+ array(
+ 'test' => <<<'EOT'
+A heading
+
+ a list
+ may be a good example
+
+EOT
+ ,
+ ),
+ $this->parser->parse($yaml)
+ );
+ }
+
+ /**
+ * @param $lineNumber
+ * @param $yaml
+ * @dataProvider parserThrowsExceptionWithCorrectLineNumberProvider
+ */
+ public function testParserThrowsExceptionWithCorrectLineNumber($lineNumber, $yaml)
+ {
+ if (method_exists($this, 'expectException')) {
+ $this->expectException('\Symfony\Component\Yaml\Exception\ParseException');
+ $this->expectExceptionMessage(sprintf('Unexpected characters near "," at line %d (near "bar: "123",").', $lineNumber));
+ } else {
+ $this->setExpectedException('\Symfony\Component\Yaml\Exception\ParseException', sprintf('Unexpected characters near "," at line %d (near "bar: "123",").', $lineNumber));
+ }
+
+ $this->parser->parse($yaml);
+ }
+
+ public function parserThrowsExceptionWithCorrectLineNumberProvider()
+ {
+ return array(
+ array(
+ 4,
+ <<<'YAML'
+foo:
+ -
+ # bar
+ bar: "123",
+YAML
+ ),
+ array(
+ 5,
+ <<<'YAML'
+foo:
+ -
+ # bar
+ # bar
+ bar: "123",
+YAML
+ ),
+ array(
+ 8,
+ <<<'YAML'
+foo:
+ -
+ # foobar
+ baz: 123
+bar:
+ -
+ # bar
+ bar: "123",
+YAML
+ ),
+ array(
+ 10,
+ <<<'YAML'
+foo:
+ -
+ # foobar
+ # foobar
+ baz: 123
+bar:
+ -
+ # bar
+ # bar
+ bar: "123",
+YAML
+ ),
+ );
+ }
+
+ public function testCanParseVeryLongValue()
+ {
+ $longStringWithSpaces = str_repeat('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ', 20000);
+ $trickyVal = array('x' => $longStringWithSpaces);
+
+ $yamlString = Yaml::dump($trickyVal);
+ $arrayFromYaml = $this->parser->parse($yamlString);
+
+ $this->assertEquals($trickyVal, $arrayFromYaml);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage Reference "foo" does not exist at line 2
+ */
+ public function testParserCleansUpReferencesBetweenRuns()
+ {
+ $yaml = <<parser->parse($yaml);
+
+ $yaml = <<parser->parse($yaml);
+ }
+
+ public function testParseReferencesOnMergeKeys()
+ {
+ $yaml = << array(
+ 'a' => 'foo',
+ 'b' => 'bar',
+ 'c' => 'baz',
+ ),
+ 'mergekeyderef' => array(
+ 'd' => 'quux',
+ 'b' => 'bar',
+ 'c' => 'baz',
+ ),
+ );
+
+ $this->assertSame($expected, $this->parser->parse($yaml));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Yaml\Exception\ParseException
+ * @expectedExceptionMessage Reference "foo" does not exist
+ */
+ public function testEvalRefException()
+ {
+ $yaml = <<parser->parse($yaml);
+ }
+}
+
+class B
+{
+ public $b = 'foo';
+}
diff --git a/vendor/symfony/yaml/Tests/YamlTest.php b/vendor/symfony/yaml/Tests/YamlTest.php
new file mode 100644
index 0000000..9e776ca
--- /dev/null
+++ b/vendor/symfony/yaml/Tests/YamlTest.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Yaml;
+
+class YamlTest extends TestCase
+{
+ public function testParseAndDump()
+ {
+ $data = array('lorem' => 'ipsum', 'dolor' => 'sit');
+ $yml = Yaml::dump($data);
+ $parsed = Yaml::parse($yml);
+ $this->assertEquals($data, $parsed);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testLegacyParseFromFile()
+ {
+ $filename = __DIR__.'/Fixtures/index.yml';
+ $contents = file_get_contents($filename);
+ $parsedByFilename = Yaml::parse($filename);
+ $parsedByContents = Yaml::parse($contents);
+ $this->assertEquals($parsedByFilename, $parsedByContents);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The indentation must be greater than zero
+ */
+ public function testZeroIndentationThrowsException()
+ {
+ Yaml::dump(array('lorem' => 'ipsum', 'dolor' => 'sit'), 2, 0);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The indentation must be greater than zero
+ */
+ public function testNegativeIndentationThrowsException()
+ {
+ Yaml::dump(array('lorem' => 'ipsum', 'dolor' => 'sit'), 2, -4);
+ }
+}
diff --git a/vendor/symfony/yaml/Unescaper.php b/vendor/symfony/yaml/Unescaper.php
new file mode 100644
index 0000000..d0dbcfa
--- /dev/null
+++ b/vendor/symfony/yaml/Unescaper.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+/**
+ * Unescaper encapsulates unescaping rules for single and double-quoted
+ * YAML strings.
+ *
+ * @author Matthew Lewinski
+ *
+ * @internal
+ */
+class Unescaper
+{
+ /**
+ * Parser and Inline assume UTF-8 encoding, so escaped Unicode characters
+ * must be converted to that encoding.
+ *
+ * @deprecated since version 2.5, to be removed in 3.0
+ *
+ * @internal
+ */
+ const ENCODING = 'UTF-8';
+
+ /**
+ * Regex fragment that matches an escaped character in a double quoted string.
+ */
+ const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)';
+
+ /**
+ * Unescapes a single quoted string.
+ *
+ * @param string $value A single quoted string
+ *
+ * @return string The unescaped string
+ */
+ public function unescapeSingleQuotedString($value)
+ {
+ return str_replace('\'\'', '\'', $value);
+ }
+
+ /**
+ * Unescapes a double quoted string.
+ *
+ * @param string $value A double quoted string
+ *
+ * @return string The unescaped string
+ */
+ public function unescapeDoubleQuotedString($value)
+ {
+ $self = $this;
+ $callback = function ($match) use ($self) {
+ return $self->unescapeCharacter($match[0]);
+ };
+
+ // evaluate the string
+ return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value);
+ }
+
+ /**
+ * Unescapes a character that was found in a double-quoted string.
+ *
+ * @param string $value An escaped character
+ *
+ * @return string The unescaped character
+ *
+ * @internal This method is public to be usable as callback. It should not
+ * be used in user code. Should be changed in 3.0.
+ */
+ public function unescapeCharacter($value)
+ {
+ switch ($value[1]) {
+ case '0':
+ return "\x0";
+ case 'a':
+ return "\x7";
+ case 'b':
+ return "\x8";
+ case 't':
+ return "\t";
+ case "\t":
+ return "\t";
+ case 'n':
+ return "\n";
+ case 'v':
+ return "\xB";
+ case 'f':
+ return "\xC";
+ case 'r':
+ return "\r";
+ case 'e':
+ return "\x1B";
+ case ' ':
+ return ' ';
+ case '"':
+ return '"';
+ case '/':
+ return '/';
+ case '\\':
+ return '\\';
+ case 'N':
+ // U+0085 NEXT LINE
+ return "\xC2\x85";
+ case '_':
+ // U+00A0 NO-BREAK SPACE
+ return "\xC2\xA0";
+ case 'L':
+ // U+2028 LINE SEPARATOR
+ return "\xE2\x80\xA8";
+ case 'P':
+ // U+2029 PARAGRAPH SEPARATOR
+ return "\xE2\x80\xA9";
+ case 'x':
+ return self::utf8chr(hexdec(substr($value, 2, 2)));
+ case 'u':
+ return self::utf8chr(hexdec(substr($value, 2, 4)));
+ case 'U':
+ return self::utf8chr(hexdec(substr($value, 2, 8)));
+ default:
+ @trigger_error('Not escaping a backslash in a double-quoted string is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', E_USER_DEPRECATED);
+
+ return $value;
+ }
+ }
+
+ /**
+ * Get the UTF-8 character for the given code point.
+ *
+ * @param int $c The unicode code point
+ *
+ * @return string The corresponding UTF-8 character
+ */
+ private static function utf8chr($c)
+ {
+ if (0x80 > $c %= 0x200000) {
+ return \chr($c);
+ }
+ if (0x800 > $c) {
+ return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
+ }
+ if (0x10000 > $c) {
+ return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
+ }
+
+ return \chr(0xF0 | $c >> 18).\chr(0x80 | $c >> 12 & 0x3F).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
+ }
+}
diff --git a/vendor/symfony/yaml/Yaml.php b/vendor/symfony/yaml/Yaml.php
new file mode 100644
index 0000000..3f93cba
--- /dev/null
+++ b/vendor/symfony/yaml/Yaml.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Yaml;
+
+use Symfony\Component\Yaml\Exception\ParseException;
+
+/**
+ * Yaml offers convenience methods to load and dump YAML.
+ *
+ * @author Fabien Potencier
+ */
+class Yaml
+{
+ /**
+ * Parses YAML into a PHP value.
+ *
+ * Usage:
+ *
+ * $array = Yaml::parse(file_get_contents('config.yml'));
+ * print_r($array);
+ *
+ * As this method accepts both plain strings and file names as an input,
+ * you must validate the input before calling this method. Passing a file
+ * as an input is a deprecated feature and will be removed in 3.0.
+ *
+ * Note: the ability to pass file names to the Yaml::parse method is deprecated since Symfony 2.2 and will be removed in 3.0. Pass the YAML contents of the file instead.
+ *
+ * @param string $input Path to a YAML file or a string containing YAML
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ * @param bool $objectForMap True if maps should return a stdClass instead of array()
+ *
+ * @return mixed The YAML converted to a PHP value
+ *
+ * @throws ParseException If the YAML is not valid
+ */
+ public static function parse($input, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
+ {
+ // if input is a file, process it
+ $file = '';
+ if (false === strpos($input, "\n") && is_file($input)) {
+ @trigger_error('The ability to pass file names to the '.__METHOD__.' method is deprecated since Symfony 2.2 and will be removed in 3.0. Pass the YAML contents of the file instead.', E_USER_DEPRECATED);
+
+ if (false === is_readable($input)) {
+ throw new ParseException(sprintf('Unable to parse "%s" as the file is not readable.', $input));
+ }
+
+ $file = $input;
+ $input = file_get_contents($file);
+ }
+
+ $yaml = new Parser();
+
+ try {
+ return $yaml->parse($input, $exceptionOnInvalidType, $objectSupport, $objectForMap);
+ } catch (ParseException $e) {
+ if ($file) {
+ $e->setParsedFile($file);
+ }
+
+ throw $e;
+ }
+ }
+
+ /**
+ * Dumps a PHP value to a YAML string.
+ *
+ * The dump method, when supplied with an array, will do its best
+ * to convert the array into friendly YAML.
+ *
+ * @param mixed $input The PHP value
+ * @param int $inline The level where you switch to inline YAML
+ * @param int $indent The amount of spaces to use for indentation of nested nodes
+ * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport True if object support is enabled, false otherwise
+ *
+ * @return string A YAML string representing the original PHP value
+ */
+ public static function dump($input, $inline = 2, $indent = 4, $exceptionOnInvalidType = false, $objectSupport = false)
+ {
+ if ($indent < 1) {
+ throw new \InvalidArgumentException('The indentation must be greater than zero.');
+ }
+
+ $yaml = new Dumper();
+ $yaml->setIndentation($indent);
+
+ return $yaml->dump($input, $inline, 0, $exceptionOnInvalidType, $objectSupport);
+ }
+}
diff --git a/vendor/symfony/yaml/composer.json b/vendor/symfony/yaml/composer.json
new file mode 100644
index 0000000..d073236
--- /dev/null
+++ b/vendor/symfony/yaml/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "symfony/yaml",
+ "type": "library",
+ "description": "Symfony Yaml Component",
+ "keywords": [],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Yaml\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ }
+}
diff --git a/vendor/symfony/yaml/phpunit.xml.dist b/vendor/symfony/yaml/phpunit.xml.dist
new file mode 100644
index 0000000..b5d4d91
--- /dev/null
+++ b/vendor/symfony/yaml/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+ ./Tests
+ ./vendor
+
+
+
+
diff --git a/vendor/topthink/framework/.gitignore b/vendor/topthink/framework/.gitignore
new file mode 100644
index 0000000..92b1f96
--- /dev/null
+++ b/vendor/topthink/framework/.gitignore
@@ -0,0 +1,7 @@
+/vendor
+composer.phar
+composer.lock
+.DS_Store
+Thumbs.db
+/.idea
+/.vscode
\ No newline at end of file
diff --git a/vendor/topthink/framework/.travis.yml b/vendor/topthink/framework/.travis.yml
new file mode 100644
index 0000000..5f9bcb7
--- /dev/null
+++ b/vendor/topthink/framework/.travis.yml
@@ -0,0 +1,34 @@
+dist: xenial
+language: php
+
+matrix:
+ fast_finish: true
+ include:
+ - php: 7.1
+ - php: 7.2
+ - php: 7.3
+
+cache:
+ directories:
+ - $HOME/.composer/cache
+
+services:
+ - memcached
+ - redis-server
+ - mysql
+
+before_install:
+ - echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+ - printf "\n" | pecl install -f redis
+ - travis_retry composer self-update
+ - mysql -e 'CREATE DATABASE test;'
+
+install:
+ - travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest
+
+script:
+ - vendor/bin/phpunit --coverage-clover build/logs/coverage.xml
+
+after_script:
+ - travis_retry wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.xml
\ No newline at end of file
diff --git a/vendor/topthink/framework/CONTRIBUTING.md b/vendor/topthink/framework/CONTRIBUTING.md
new file mode 100644
index 0000000..40705b3
--- /dev/null
+++ b/vendor/topthink/framework/CONTRIBUTING.md
@@ -0,0 +1,119 @@
+如何贡献我的源代码
+===
+
+此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。
+
+## 通过 Github 贡献代码
+
+ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。
+
+参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。
+
+我们希望你贡献的代码符合:
+
+* ThinkPHP 的编码规范
+* 适当的注释,能让其他人读懂
+* 遵循 Apache2 开源协议
+
+**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容**
+
+### 注意事项
+
+* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141);
+* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144);
+* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。
+* 系统会自动在 PHP 7.1 ~ 7.3 上测试修改,请确保你的修改符合 PHP 7.1 ~ 7.3 的语法规范;
+* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests);
+
+## GitHub Issue
+
+GitHub 提供了 Issue 功能,该功能可以用于:
+
+* 提出 bug
+* 提出功能改进
+* 反馈使用体验
+
+该功能不应该用于:
+
+ * 提出修改意见(涉及代码署名和修订追溯问题)
+ * 不友善的言论
+
+## 快速修改
+
+**GitHub 提供了快速编辑文件的功能**
+
+1. 登录 GitHub 帐号;
+2. 浏览项目文件,找到要进行修改的文件;
+3. 点击右上角铅笔图标进行修改;
+4. 填写 `Commit changes` 相关内容(Title 必填);
+5. 提交修改,等待 CI 验证和管理员合并。
+
+**若您需要一次提交大量修改,请继续阅读下面的内容**
+
+## 完整流程
+
+1. `fork`本项目;
+2. 克隆(`clone`)你 `fork` 的项目到本地;
+3. 新建分支(`branch`)并检出(`checkout`)新分支;
+4. 添加本项目到你的本地 git 仓库作为上游(`upstream`);
+5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests);
+6. 变基(衍合 `rebase`)你的分支到上游 master 分支;
+7. `push` 你的本地仓库到 GitHub;
+8. 提交 `pull request`;
+9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`);
+10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。
+
+*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`*
+
+*绝对不可以使用 `git push -f` 强行推送修改到上游*
+
+### 注意事项
+
+* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/);
+* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分);
+* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/)
+
+## 推荐资源
+
+### 开发环境
+
+* XAMPP for Windows 5.5.x
+* WampServer (for Windows)
+* upupw Apache PHP5.4 ( for Windows)
+
+或自行安装
+
+- Apache / Nginx
+- PHP 7.1 ~ 7.3
+- MySQL / MariaDB
+
+*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer*
+
+*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB*
+
+### 编辑器
+
+Sublime Text 3 + phpfmt 插件
+
+phpfmt 插件参数
+
+```json
+{
+ "autocomplete": true,
+ "enable_auto_align": true,
+ "format_on_save": true,
+ "indent_with_space": true,
+ "psr1_naming": false,
+ "psr2": true,
+ "version": 4
+}
+```
+
+或其他 编辑器 / IDE 配合 PSR2 自动格式化工具
+
+### Git GUI
+
+* SourceTree
+* GitHub Desktop
+
+或其他 Git 图形界面客户端
diff --git a/vendor/topthink/framework/LICENSE.txt b/vendor/topthink/framework/LICENSE.txt
new file mode 100644
index 0000000..8e507ac
--- /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/README.md b/vendor/topthink/framework/README.md
new file mode 100644
index 0000000..6583266
--- /dev/null
+++ b/vendor/topthink/framework/README.md
@@ -0,0 +1,86 @@
+![](https://box.kancloud.cn/5a0aaa69a5ff42657b5c4715f3d49221)
+
+ThinkPHP 6.0
+===============
+
+[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=6.0)](https://travis-ci.org/top-think/framework)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/top-think/framework/badges/quality-score.png?b=6.0)](https://scrutinizer-ci.com/g/top-think/framework/?branch=6.0)
+[![Code Coverage](https://scrutinizer-ci.com/g/top-think/framework/badges/coverage.png?b=6.0)](https://scrutinizer-ci.com/g/top-think/framework/?branch=6.0)
+[![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework)
+[![Latest Stable Version](https://poser.pugx.org/topthink/framework/v/stable)](https://packagist.org/packages/topthink/framework)
+[![PHP Version](https://img.shields.io/badge/php-%3E%3D7.1-8892BF.svg)](http://www.php.net/)
+[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework)
+
+ThinkPHP6.0底层架构采用PHP7.1改写和进一步优化。
+
+[官方应用服务市场](https://www.thinkphp.cn/service) | [`ThinkPHP`开发者扶持计划](https://sites.thinkphp.cn/1782366)
+
+## 主要新特性
+
+* 采用`PHP7`强类型(严格模式)
+* 支持更多的`PSR`规范
+* 原生多应用支持
+* 系统服务注入支持
+* ORM作为独立组件使用
+* 增加Filesystem
+* 全新的事件系统
+* 模板引擎分离出核心
+* 内部功能中间件化
+* SESSION机制改进
+* 日志多通道支持
+* 规范扩展接口
+* 更强大的控制台
+* 对Swoole以及协程支持改进
+* 对IDE更加友好
+* 统一和精简大量用法
+
+
+> ThinkPHP6.0的运行环境要求PHP7.1+。
+
+## 安装
+
+~~~
+composer create-project topthink/think tp
+~~~
+
+启动服务
+
+~~~
+cd tp
+php think run
+~~~
+
+然后就可以在浏览器中访问
+
+~~~
+http://localhost:8000
+~~~
+
+如果需要更新框架使用
+~~~
+composer update topthink/framework
+~~~
+
+## 文档
+
+[完全开发手册](https://www.kancloud.cn/manual/thinkphp6_0/content)
+
+## 命名规范
+
+`ThinkPHP6`遵循PSR-2命名规范和PSR-4自动加载规范。
+
+## 参与开发
+
+直接提交PR或者Issue即可
+
+## 版权信息
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2006-2020 by ThinkPHP (http://thinkphp.cn) All rights reserved。
+
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+更多细节参阅 [LICENSE.txt](LICENSE.txt)
diff --git a/vendor/topthink/framework/composer.json b/vendor/topthink/framework/composer.json
new file mode 100644
index 0000000..2128aed
--- /dev/null
+++ b/vendor/topthink/framework/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "topthink/framework",
+ "description": "The ThinkPHP Framework.",
+ "keywords": [
+ "framework",
+ "thinkphp",
+ "ORM"
+ ],
+ "homepage": "http://thinkphp.cn/",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ },
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "league/flysystem": "^1.0",
+ "league/flysystem-cached-adapter": "^1.0",
+ "opis/closure": "^3.1",
+ "psr/log": "~1.0",
+ "psr/container": "~1.0",
+ "psr/simple-cache": "^1.0",
+ "topthink/think-orm": "^2.0",
+ "topthink/think-helper": "^3.1.1"
+ },
+ "require-dev": {
+ "mikey179/vfsstream": "^1.6",
+ "mockery/mockery": "^1.2",
+ "phpunit/phpunit": "^7.0"
+ },
+ "autoload": {
+ "files": [],
+ "psr-4": {
+ "think\\": "src/think/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "think\\tests\\": "tests/"
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "sort-packages": true
+ }
+}
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/phpunit.xml.dist b/vendor/topthink/framework/phpunit.xml.dist
new file mode 100644
index 0000000..211e3bb
--- /dev/null
+++ b/vendor/topthink/framework/phpunit.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+ ./tests
+
+
+
+
+ ./src/think
+
+
+
diff --git a/vendor/topthink/framework/src/helper.php b/vendor/topthink/framework/src/helper.php
new file mode 100644
index 0000000..b1786ce
--- /dev/null
+++ b/vendor/topthink/framework/src/helper.php
@@ -0,0 +1,663 @@
+
+// +----------------------------------------------------------------------
+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')) {
+ /**
+ * 快速获取容器中的实例 支持依赖注入
+ * @param string $name 类名或标识 默认获取当前应用实例
+ * @param array $args 参数
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return 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);
+ } 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/lang/zh-cn.php b/vendor/topthink/framework/src/lang/zh-cn.php
new file mode 100644
index 0000000..aa64486
--- /dev/null
+++ b/vendor/topthink/framework/src/lang/zh-cn.php
@@ -0,0 +1,148 @@
+
+// +----------------------------------------------------------------------
+
+// 核心中文语言包
+return [
+ // 系统错误提示
+ 'Undefined variable' => '未定义变量',
+ 'Undefined index' => '未定义数组索引',
+ 'Undefined offset' => '未定义数组下标',
+ 'Parse error' => '语法解析错误',
+ 'Type error' => '类型错误',
+ 'Fatal error' => '致命错误',
+ 'syntax error' => '语法错误',
+
+ // 框架核心错误提示
+ 'dispatch type not support' => '不支持的调度类型',
+ 'method param miss' => '方法参数错误',
+ 'method not exists' => '方法不存在',
+ 'function not exists' => '函数不存在',
+ 'app not exists' => '应用不存在',
+ 'controller not exists' => '控制器不存在',
+ 'class not exists' => '类不存在',
+ 'property not exists' => '类的属性不存在',
+ 'template not exists' => '模板文件不存在',
+ 'illegal controller name' => '非法的控制器名称',
+ 'illegal action name' => '非法的操作名称',
+ 'url suffix deny' => '禁止的URL后缀访问',
+ 'Undefined cache config' => '缓存配置未定义',
+ 'Route Not Found' => '当前访问路由未定义或不匹配',
+ 'Undefined db config' => '数据库配置未定义',
+ 'Undefined log config' => '日志配置未定义',
+ 'Undefined db type' => '未定义数据库类型',
+ 'variable type error' => '变量类型错误',
+ 'PSR-4 error' => 'PSR-4 规范错误',
+ 'not support type' => '不支持的分页索引字段类型',
+ 'not support total' => '简洁模式下不能获取数据总数',
+ 'not support last' => '简洁模式下不能获取最后一页',
+ 'error session handler' => '错误的SESSION处理器类',
+ 'not allow php tag' => '模板不允许使用PHP语法',
+ 'not support' => '不支持',
+ 'database config error' => '数据库配置信息错误',
+ 'redisd master' => 'Redisd 主服务器错误',
+ 'redisd slave' => 'Redisd 从服务器错误',
+ 'must run at sae' => '必须在SAE运行',
+ 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务',
+ 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务',
+ 'fields not exists' => '数据表字段不存在',
+ 'where express error' => '查询表达式错误',
+ 'no data to update' => '没有任何数据需要更新',
+ 'miss data to insert' => '缺少需要写入的数据',
+ 'miss complex primary data' => '缺少复合主键数据',
+ 'miss update condition' => '缺少更新条件',
+ 'model data Not Found' => '模型数据不存在',
+ 'table data not Found' => '表数据不存在',
+ 'delete without condition' => '没有条件不会执行删除操作',
+ 'miss relation data' => '缺少关联表数据',
+ 'tag attr must' => '模板标签属性必须',
+ 'tag error' => '模板标签错误',
+ 'cache write error' => '缓存写入失败',
+ 'sae mc write error' => 'SAE mc 写入错误',
+ 'route name not exists' => '路由标识不存在(或参数不够)',
+ 'invalid request' => '非法请求',
+ 'bind attr has exists' => '模型的属性已经存在',
+ 'relation data not exists' => '关联数据不存在',
+ 'relation not support' => '关联不支持',
+ 'chunk not support order' => 'Chunk不支持调用order方法',
+ 'route pattern error' => '路由变量规则定义错误',
+ 'route behavior will not support' => '路由行为废弃(使用中间件替代)',
+ 'closure not support cache(true)' => '使用闭包查询不支持cache(true),请指定缓存Key',
+
+ // 上传错误信息
+ 'unknown upload error' => '未知上传错误!',
+ 'file write error' => '文件写入失败!',
+ 'upload temp dir not found' => '找不到临时文件夹!',
+ 'no file to uploaded' => '没有文件被上传!',
+ 'only the portion of file is uploaded' => '文件只有部分被上传!',
+ 'upload File size exceeds the maximum value' => '上传文件大小超过了最大值!',
+ 'upload write error' => '文件上传保存错误!',
+ 'has the same filename: {:filename}' => '存在同名文件:{:filename}',
+ 'upload illegal files' => '非法上传文件',
+ 'illegal image files' => '非法图片文件',
+ 'extensions to upload is not allowed' => '上传文件后缀不允许',
+ 'mimetype to upload is not allowed' => '上传文件MIME类型不允许!',
+ 'filesize not match' => '上传文件大小不符!',
+ 'directory {:path} creation failed' => '目录 {:path} 创建失败!',
+
+ 'The middleware must return Response instance' => '中间件方法必须返回Response对象实例',
+ 'The queue was exhausted, with no response returned' => '中间件队列为空',
+ // Validate Error Message
+ ':attribute require' => ':attribute不能为空',
+ ':attribute must' => ':attribute必须',
+ ':attribute must be numeric' => ':attribute必须是数字',
+ ':attribute must be integer' => ':attribute必须是整数',
+ ':attribute must be float' => ':attribute必须是浮点数',
+ ':attribute must be bool' => ':attribute必须是布尔值',
+ ':attribute not a valid email address' => ':attribute格式不符',
+ ':attribute not a valid mobile' => ':attribute格式不符',
+ ':attribute must be a array' => ':attribute必须是数组',
+ ':attribute must be yes,on or 1' => ':attribute必须是yes、on或者1',
+ ':attribute not a valid datetime' => ':attribute不是一个有效的日期或时间格式',
+ ':attribute not a valid file' => ':attribute不是有效的上传文件',
+ ':attribute not a valid image' => ':attribute不是有效的图像文件',
+ ':attribute must be alpha' => ':attribute只能是字母',
+ ':attribute must be alpha-numeric' => ':attribute只能是字母和数字',
+ ':attribute must be alpha-numeric, dash, underscore' => ':attribute只能是字母、数字和下划线_及破折号-',
+ ':attribute not a valid domain or ip' => ':attribute不是有效的域名或者IP',
+ ':attribute must be chinese' => ':attribute只能是汉字',
+ ':attribute must be chinese or alpha' => ':attribute只能是汉字、字母',
+ ':attribute must be chinese,alpha-numeric' => ':attribute只能是汉字、字母和数字',
+ ':attribute must be chinese,alpha-numeric,underscore, dash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-',
+ ':attribute not a valid url' => ':attribute不是有效的URL地址',
+ ':attribute not a valid ip' => ':attribute不是有效的IP地址',
+ ':attribute must be dateFormat of :rule' => ':attribute必须使用日期格式 :rule',
+ ':attribute must be in :rule' => ':attribute必须在 :rule 范围内',
+ ':attribute be notin :rule' => ':attribute不能在 :rule 范围内',
+ ':attribute must between :1 - :2' => ':attribute只能在 :1 - :2 之间',
+ ':attribute not between :1 - :2' => ':attribute不能在 :1 - :2 之间',
+ 'size of :attribute must be :rule' => ':attribute长度不符合要求 :rule',
+ 'max size of :attribute must be :rule' => ':attribute长度不能超过 :rule',
+ 'min size of :attribute must be :rule' => ':attribute长度不能小于 :rule',
+ ':attribute cannot be less than :rule' => ':attribute日期不能小于 :rule',
+ ':attribute cannot exceed :rule' => ':attribute日期不能超过 :rule',
+ ':attribute not within :rule' => '不在有效期内 :rule',
+ 'access IP is not allowed' => '不允许的IP访问',
+ 'access IP denied' => '禁止的IP访问',
+ ':attribute out of accord with :2' => ':attribute和确认字段:2不一致',
+ ':attribute cannot be same with :2' => ':attribute和比较字段:2不能相同',
+ ':attribute must greater than or equal :rule' => ':attribute必须大于等于 :rule',
+ ':attribute must greater than :rule' => ':attribute必须大于 :rule',
+ ':attribute must less than or equal :rule' => ':attribute必须小于等于 :rule',
+ ':attribute must less than :rule' => ':attribute必须小于 :rule',
+ ':attribute must equal :rule' => ':attribute必须等于 :rule',
+ ':attribute has exists' => ':attribute已存在',
+ ':attribute not conform to the rules' => ':attribute不符合指定规则',
+ 'invalid Request method' => '无效的请求类型',
+ 'invalid token' => '令牌数据无效',
+ 'not conform to the rules' => '规则错误',
+
+ 'record has update' => '记录已经被更新了',
+];
diff --git a/vendor/topthink/framework/src/think/App.php b/vendor/topthink/framework/src/think/App.php
new file mode 100644
index 0000000..1c375fe
--- /dev/null
+++ b/vendor/topthink/framework/src/think/App.php
@@ -0,0 +1,608 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use think\event\AppInit;
+use think\helper\Str;
+use think\initializer\BootService;
+use think\initializer\Error;
+use think\initializer\RegisterService;
+
+/**
+ * App 基础类
+ * @property Route $route
+ * @property Config $config
+ * @property Cache $cache
+ * @property Request $request
+ * @property Http $http
+ * @property Console $console
+ * @property Env $env
+ * @property Event $event
+ * @property Middleware $middleware
+ * @property Log $log
+ * @property Lang $lang
+ * @property Db $db
+ * @property Cookie $cookie
+ * @property Session $session
+ * @property Validate $validate
+ * @property Filesystem $filesystem
+ */
+class App extends Container
+{
+ const VERSION = '6.0.3';
+
+ /**
+ * 应用调试模式
+ * @var bool
+ */
+ protected $appDebug = false;
+
+ /**
+ * 应用开始时间
+ * @var float
+ */
+ protected $beginTime;
+
+ /**
+ * 应用内存初始占用
+ * @var integer
+ */
+ protected $beginMem;
+
+ /**
+ * 当前应用类库命名空间
+ * @var string
+ */
+ protected $namespace = 'app';
+
+ /**
+ * 应用根目录
+ * @var string
+ */
+ protected $rootPath = '';
+
+ /**
+ * 框架目录
+ * @var string
+ */
+ protected $thinkPath = '';
+
+ /**
+ * 应用目录
+ * @var string
+ */
+ protected $appPath = '';
+
+ /**
+ * Runtime目录
+ * @var string
+ */
+ protected $runtimePath = '';
+
+ /**
+ * 路由定义目录
+ * @var string
+ */
+ protected $routePath = '';
+
+ /**
+ * 配置后缀
+ * @var string
+ */
+ protected $configExt = '.php';
+
+ /**
+ * 应用初始化器
+ * @var array
+ */
+ protected $initializers = [
+ Error::class,
+ RegisterService::class,
+ BootService::class,
+ ];
+
+ /**
+ * 注册的系统服务
+ * @var array
+ */
+ protected $services = [];
+
+ /**
+ * 初始化
+ * @var bool
+ */
+ protected $initialized = false;
+
+ /**
+ * 容器绑定标识
+ * @var array
+ */
+ protected $bind = [
+ 'app' => App::class,
+ 'cache' => Cache::class,
+ 'config' => Config::class,
+ 'console' => Console::class,
+ 'cookie' => Cookie::class,
+ 'db' => Db::class,
+ 'env' => Env::class,
+ 'event' => Event::class,
+ 'http' => Http::class,
+ 'lang' => Lang::class,
+ 'log' => Log::class,
+ 'middleware' => Middleware::class,
+ 'request' => Request::class,
+ 'response' => Response::class,
+ 'route' => Route::class,
+ 'session' => Session::class,
+ 'validate' => Validate::class,
+ 'view' => View::class,
+ 'filesystem' => Filesystem::class,
+ 'think\DbManager' => Db::class,
+ 'think\LogManager' => Log::class,
+ 'think\CacheManager' => Cache::class,
+
+ // 接口依赖注入
+ 'Psr\Log\LoggerInterface' => Log::class,
+ ];
+
+ /**
+ * 架构方法
+ * @access public
+ * @param string $rootPath 应用根目录
+ */
+ public function __construct(string $rootPath = '')
+ {
+ $this->thinkPath = dirname(__DIR__) . DIRECTORY_SEPARATOR;
+ $this->rootPath = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
+ $this->appPath = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
+ $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
+
+ if (is_file($this->appPath . 'provider.php')) {
+ $this->bind(include $this->appPath . 'provider.php');
+ }
+
+ static::setInstance($this);
+
+ $this->instance('app', $this);
+ $this->instance('think\Container', $this);
+ }
+
+ /**
+ * 注册服务
+ * @access public
+ * @param Service|string $service 服务
+ * @param bool $force 强制重新注册
+ * @return Service|null
+ */
+ public function register($service, bool $force = false)
+ {
+ $registered = $this->getService($service);
+
+ if ($registered && !$force) {
+ return $registered;
+ }
+
+ if (is_string($service)) {
+ $service = new $service($this);
+ }
+
+ if (method_exists($service, 'register')) {
+ $service->register();
+ }
+
+ if (property_exists($service, 'bind')) {
+ $this->bind($service->bind);
+ }
+
+ $this->services[] = $service;
+ }
+
+ /**
+ * 执行服务
+ * @access public
+ * @param Service $service 服务
+ * @return mixed
+ */
+ public function bootService($service)
+ {
+ if (method_exists($service, 'boot')) {
+ return $this->invoke([$service, 'boot']);
+ }
+ }
+
+ /**
+ * 获取服务
+ * @param string|Service $service
+ * @return Service|null
+ */
+ public function getService($service)
+ {
+ $name = is_string($service) ? $service : get_class($service);
+ return array_values(array_filter($this->services, function ($value) use ($name) {
+ return $value instanceof $name;
+ }, ARRAY_FILTER_USE_BOTH))[0] ?? null;
+ }
+
+ /**
+ * 开启应用调试模式
+ * @access public
+ * @param bool $debug 开启应用调试模式
+ * @return $this
+ */
+ public function debug(bool $debug = true)
+ {
+ $this->appDebug = $debug;
+ return $this;
+ }
+
+ /**
+ * 是否为调试模式
+ * @access public
+ * @return bool
+ */
+ public function isDebug(): bool
+ {
+ return $this->appDebug;
+ }
+
+ /**
+ * 设置应用命名空间
+ * @access public
+ * @param string $namespace 应用命名空间
+ * @return $this
+ */
+ public function setNamespace(string $namespace)
+ {
+ $this->namespace = $namespace;
+ return $this;
+ }
+
+ /**
+ * 获取应用类库命名空间
+ * @access public
+ * @return string
+ */
+ public function getNamespace(): string
+ {
+ return $this->namespace;
+ }
+
+ /**
+ * 获取框架版本
+ * @access public
+ * @return string
+ */
+ public function version(): string
+ {
+ return static::VERSION;
+ }
+
+ /**
+ * 获取应用根目录
+ * @access public
+ * @return string
+ */
+ public function getRootPath(): string
+ {
+ return $this->rootPath;
+ }
+
+ /**
+ * 获取应用基础目录
+ * @access public
+ * @return string
+ */
+ public function getBasePath(): string
+ {
+ return $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 获取当前应用目录
+ * @access public
+ * @return string
+ */
+ public function getAppPath(): string
+ {
+ return $this->appPath;
+ }
+
+ /**
+ * 设置应用目录
+ * @param string $path 应用目录
+ */
+ public function setAppPath(string $path)
+ {
+ $this->appPath = $path;
+ }
+
+ /**
+ * 获取应用运行时目录
+ * @access public
+ * @return string
+ */
+ public function getRuntimePath(): string
+ {
+ return $this->runtimePath;
+ }
+
+ /**
+ * 设置runtime目录
+ * @param string $path 定义目录
+ */
+ public function setRuntimePath(string $path): void
+ {
+ $this->runtimePath = $path;
+ }
+
+ /**
+ * 获取核心框架目录
+ * @access public
+ * @return string
+ */
+ public function getThinkPath(): string
+ {
+ return $this->thinkPath;
+ }
+
+ /**
+ * 获取应用配置目录
+ * @access public
+ * @return string
+ */
+ public function getConfigPath(): string
+ {
+ return $this->rootPath . 'config' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 获取配置后缀
+ * @access public
+ * @return string
+ */
+ public function getConfigExt(): string
+ {
+ return $this->configExt;
+ }
+
+ /**
+ * 获取应用开启时间
+ * @access public
+ * @return float
+ */
+ public function getBeginTime(): float
+ {
+ return $this->beginTime;
+ }
+
+ /**
+ * 获取应用初始内存占用
+ * @access public
+ * @return integer
+ */
+ public function getBeginMem(): int
+ {
+ return $this->beginMem;
+ }
+
+ /**
+ * 初始化应用
+ * @access public
+ * @return $this
+ */
+ public function initialize()
+ {
+ $this->initialized = true;
+
+ $this->beginTime = microtime(true);
+ $this->beginMem = memory_get_usage();
+
+ // 加载环境变量
+ if (is_file($this->rootPath . '.env')) {
+ $this->env->load($this->rootPath . '.env');
+ }
+
+ $this->configExt = $this->env->get('config_ext', '.php');
+
+ $this->debugModeInit();
+
+ // 加载全局初始化文件
+ $this->load();
+
+ // 加载框架默认语言包
+ $langSet = $this->lang->defaultLangSet();
+
+ $this->lang->load($this->thinkPath . 'lang' . DIRECTORY_SEPARATOR . $langSet . '.php');
+
+ // 加载应用默认语言包
+ $this->loadLangPack($langSet);
+
+ // 监听AppInit
+ $this->event->trigger(AppInit::class);
+
+ date_default_timezone_set($this->config->get('app.default_timezone', 'Asia/Shanghai'));
+
+ // 初始化
+ foreach ($this->initializers as $initializer) {
+ $this->make($initializer)->init($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 是否初始化过
+ * @return bool
+ */
+ public function initialized()
+ {
+ return $this->initialized;
+ }
+
+ /**
+ * 加载语言包
+ * @param string $langset 语言
+ * @return void
+ */
+ public function loadLangPack($langset)
+ {
+ if (empty($langset)) {
+ return;
+ }
+
+ // 加载系统语言包
+ $files = glob($this->appPath . 'lang' . DIRECTORY_SEPARATOR . $langset . '.*');
+ $this->lang->load($files);
+
+ // 加载扩展(自定义)语言包
+ $list = $this->config->get('lang.extend_list', []);
+
+ if (isset($list[$langset])) {
+ $this->lang->load($list[$langset]);
+ }
+ }
+
+ /**
+ * 引导应用
+ * @access public
+ * @return void
+ */
+ public function boot(): void
+ {
+ array_walk($this->services, function ($service) {
+ $this->bootService($service);
+ });
+ }
+
+ /**
+ * 加载应用文件和配置
+ * @access protected
+ * @return void
+ */
+ protected function load(): void
+ {
+ $appPath = $this->getAppPath();
+
+ if (is_file($appPath . 'common.php')) {
+ include_once $appPath . 'common.php';
+ }
+
+ include_once $this->thinkPath . 'helper.php';
+
+ $configPath = $this->getConfigPath();
+
+ $files = [];
+
+ if (is_dir($configPath)) {
+ $files = glob($configPath . '*' . $this->configExt);
+ }
+
+ foreach ($files as $file) {
+ $this->config->load($file, pathinfo($file, PATHINFO_FILENAME));
+ }
+
+ if (is_file($appPath . 'event.php')) {
+ $this->loadEvent(include $appPath . 'event.php');
+ }
+
+ if (is_file($appPath . 'service.php')) {
+ $services = include $appPath . 'service.php';
+ foreach ($services as $service) {
+ $this->register($service);
+ }
+ }
+ }
+
+ /**
+ * 调试模式设置
+ * @access protected
+ * @return void
+ */
+ protected function debugModeInit(): void
+ {
+ // 应用调试模式
+ if (!$this->appDebug) {
+ $this->appDebug = $this->env->get('app_debug') ? true : false;
+ ini_set('display_errors', 'Off');
+ }
+
+ if (!$this->runningInConsole()) {
+ //重新申请一块比较大的buffer
+ if (ob_get_level() > 0) {
+ $output = ob_get_clean();
+ }
+ ob_start();
+ if (!empty($output)) {
+ echo $output;
+ }
+ }
+ }
+
+ /**
+ * 注册应用事件
+ * @access protected
+ * @param array $event 事件数据
+ * @return void
+ */
+ public function loadEvent(array $event): void
+ {
+ if (isset($event['bind'])) {
+ $this->event->bind($event['bind']);
+ }
+
+ if (isset($event['listen'])) {
+ $this->event->listenEvents($event['listen']);
+ }
+
+ if (isset($event['subscribe'])) {
+ $this->event->subscribe($event['subscribe']);
+ }
+ }
+
+ /**
+ * 解析应用类的类名
+ * @access public
+ * @param string $layer 层名 controller model ...
+ * @param string $name 类名
+ * @return string
+ */
+ public function parseClass(string $layer, string $name): string
+ {
+ $name = str_replace(['/', '.'], '\\', $name);
+ $array = explode('\\', $name);
+ $class = Str::studly(array_pop($array));
+ $path = $array ? implode('\\', $array) . '\\' : '';
+
+ return $this->namespace . '\\' . $layer . '\\' . $path . $class;
+ }
+
+ /**
+ * 是否运行在命令行下
+ * @return bool
+ */
+ public function runningInConsole()
+ {
+ return php_sapi_name() === 'cli' || php_sapi_name() === 'phpdbg';
+ }
+
+ /**
+ * 获取应用根目录
+ * @access protected
+ * @return string
+ */
+ protected function getDefaultRootPath(): string
+ {
+ return dirname($this->thinkPath, 4) . DIRECTORY_SEPARATOR;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Cache.php b/vendor/topthink/framework/src/think/Cache.php
new file mode 100644
index 0000000..775f71d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Cache.php
@@ -0,0 +1,197 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Psr\SimpleCache\CacheInterface;
+use think\cache\Driver;
+use think\cache\TagSet;
+use think\exception\InvalidArgumentException;
+use think\helper\Arr;
+
+/**
+ * 缓存管理类
+ * @mixin Driver
+ * @mixin \think\cache\driver\File
+ */
+class Cache extends Manager implements CacheInterface
+{
+
+ protected $namespace = '\\think\\cache\\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('cache.' . $name, $default);
+ }
+
+ return $this->app->config->get('cache');
+ }
+
+ /**
+ * 获取驱动配置
+ * @param string $store
+ * @param string $name
+ * @param null $default
+ * @return array
+ */
+ public function getStoreConfig(string $store, string $name = null, $default = null)
+ {
+ if ($config = $this->getConfig("stores.{$store}")) {
+ return Arr::get($config, $name, $default);
+ }
+
+ throw new \InvalidArgumentException("Store [$store] not found.");
+ }
+
+ protected function resolveType(string $name)
+ {
+ return $this->getStoreConfig($name, 'type', 'file');
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ return $this->getStoreConfig($name);
+ }
+
+ /**
+ * 连接或者切换缓存
+ * @access public
+ * @param string $name 连接配置名
+ * @return Driver
+ */
+ public function store(string $name = null)
+ {
+ return $this->driver($name);
+ }
+
+ /**
+ * 清空缓冲池
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ return $this->store()->clear();
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ return $this->store()->get($key, $default);
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|\DateTime $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function set($key, $value, $ttl = null): bool
+ {
+ return $this->store()->set($key, $value, $ttl);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @return bool
+ */
+ public function delete($key): bool
+ {
+ return $this->store()->delete($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @param mixed $default 默认值
+ * @return iterable
+ * @throws InvalidArgumentException
+ */
+ public function getMultiple($keys, $default = null): iterable
+ {
+ return $this->store()->getMultiple($keys, $default);
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param iterable $values 缓存数据
+ * @param null|int|\DateInterval $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function setMultiple($values, $ttl = null): bool
+ {
+ return $this->store()->setMultiple($values, $ttl);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @return bool
+ * @throws InvalidArgumentException
+ */
+ public function deleteMultiple($keys): bool
+ {
+ return $this->store()->deleteMultiple($keys);
+ }
+
+ /**
+ * 判断缓存是否存在
+ * @access public
+ * @param string $key 缓存变量名
+ * @return bool
+ */
+ public function has($key): bool
+ {
+ return $this->store()->has($key);
+ }
+
+ /**
+ * 缓存标签
+ * @access public
+ * @param string|array $name 标签名
+ * @return TagSet
+ */
+ public function tag($name): TagSet
+ {
+ return $this->store()->tag($name);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Config.php b/vendor/topthink/framework/src/think/Config.php
new file mode 100644
index 0000000..930eb67
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Config.php
@@ -0,0 +1,193 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 配置管理类
+ * @package think
+ */
+class Config
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * 配置文件目录
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * 配置文件后缀
+ * @var string
+ */
+ protected $ext;
+
+ /**
+ * 构造方法
+ * @access public
+ */
+ public function __construct(string $path = null, string $ext = '.php')
+ {
+ $this->path = $path ?: '';
+ $this->ext = $ext;
+ }
+
+ public static function __make(App $app)
+ {
+ $path = $app->getConfigPath();
+ $ext = $app->getConfigExt();
+
+ return new static($path, $ext);
+ }
+
+ /**
+ * 加载配置文件(多种格式)
+ * @access public
+ * @param string $file 配置文件名
+ * @param string $name 一级配置名
+ * @return array
+ */
+ public function load(string $file, string $name = ''): array
+ {
+ if (is_file($file)) {
+ $filename = $file;
+ } elseif (is_file($this->path . $file . $this->ext)) {
+ $filename = $this->path . $file . $this->ext;
+ }
+
+ if (isset($filename)) {
+ return $this->parse($filename, $name);
+ }
+
+ return $this->config;
+ }
+
+ /**
+ * 解析配置文件
+ * @access public
+ * @param string $file 配置文件名
+ * @param string $name 一级配置名
+ * @return array
+ */
+ protected function parse(string $file, string $name): array
+ {
+ $type = pathinfo($file, PATHINFO_EXTENSION);
+ $config = [];
+ switch ($type) {
+ case 'php':
+ $config = include $file;
+ break;
+ case 'yml':
+ case 'yaml':
+ if (function_exists('yaml_parse_file')) {
+ $config = yaml_parse_file($file);
+ }
+ break;
+ case 'ini':
+ $config = parse_ini_file($file, true, INI_SCANNER_TYPED) ?: [];
+ break;
+ case 'json':
+ $config = json_decode(file_get_contents($file), true);
+ break;
+ }
+
+ return is_array($config) ? $this->set($config, strtolower($name)) : [];
+ }
+
+ /**
+ * 检测配置是否存在
+ * @access public
+ * @param string $name 配置参数名(支持多级配置 .号分割)
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return !is_null($this->get($name));
+ }
+
+ /**
+ * 获取一级配置
+ * @access protected
+ * @param string $name 一级配置名
+ * @return array
+ */
+ protected function pull(string $name): array
+ {
+ $name = strtolower($name);
+
+ return $this->config[$name] ?? [];
+ }
+
+ /**
+ * 获取配置参数 为空则获取所有配置
+ * @access public
+ * @param string $name 配置参数名(支持多级配置 .号分割)
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get(string $name = null, $default = null)
+ {
+ // 无参数时获取所有
+ if (empty($name)) {
+ return $this->config;
+ }
+
+ if (false === strpos($name, '.')) {
+ return $this->pull($name);
+ }
+
+ $name = explode('.', $name);
+ $name[0] = strtolower($name[0]);
+ $config = $this->config;
+
+ // 按.拆分成多维数组进行判断
+ foreach ($name as $val) {
+ if (isset($config[$val])) {
+ $config = $config[$val];
+ } else {
+ return $default;
+ }
+ }
+
+ return $config;
+ }
+
+ /**
+ * 设置配置参数 name为数组则为批量设置
+ * @access public
+ * @param array $config 配置参数
+ * @param string $name 配置名
+ * @return array
+ */
+ public function set(array $config, string $name = null): array
+ {
+ if (!empty($name)) {
+ if (isset($this->config[$name])) {
+ $result = array_merge($this->config[$name], $config);
+ } else {
+ $result = $config;
+ }
+
+ $this->config[$name] = $result;
+ } else {
+ $result = $this->config = array_merge($this->config, array_change_key_case($config));
+ }
+
+ return $result;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Console.php b/vendor/topthink/framework/src/think/Console.php
new file mode 100644
index 0000000..d727255
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Console.php
@@ -0,0 +1,732 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use InvalidArgumentException;
+use LogicException;
+use think\console\Command;
+use think\console\command\Clear;
+use think\console\command\Help;
+use think\console\command\Help as HelpCommand;
+use think\console\command\Lists;
+use think\console\command\make\Command as MakeCommand;
+use think\console\command\make\Controller;
+use think\console\command\make\Event;
+use think\console\command\make\Listener;
+use think\console\command\make\Middleware;
+use think\console\command\make\Model;
+use think\console\command\make\Service;
+use think\console\command\make\Subscribe;
+use think\console\command\make\Validate;
+use think\console\command\optimize\Route;
+use think\console\command\optimize\Schema;
+use think\console\command\RouteList;
+use think\console\command\RunServer;
+use think\console\command\ServiceDiscover;
+use think\console\command\VendorPublish;
+use think\console\command\Version;
+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;
+use think\console\output\driver\Buffer;
+
+/**
+ * 控制台应用管理类
+ */
+class Console
+{
+
+ protected $app;
+
+ /** @var Command[] */
+ protected $commands = [];
+
+ protected $wantHelps = false;
+
+ protected $catchExceptions = true;
+ protected $autoExit = true;
+ protected $definition;
+ protected $defaultCommand = 'list';
+
+ protected $defaultCommands = [
+ 'help' => Help::class,
+ 'list' => Lists::class,
+ 'clear' => Clear::class,
+ 'make:command' => MakeCommand::class,
+ 'make:controller' => Controller::class,
+ 'make:model' => Model::class,
+ 'make:middleware' => Middleware::class,
+ 'make:validate' => Validate::class,
+ 'make:event' => Event::class,
+ 'make:listener' => Listener::class,
+ 'make:service' => Service::class,
+ 'make:subscribe' => Subscribe::class,
+ 'optimize:route' => Route::class,
+ 'optimize:schema' => Schema::class,
+ 'run' => RunServer::class,
+ 'version' => Version::class,
+ 'route:list' => RouteList::class,
+ 'service:discover' => ServiceDiscover::class,
+ 'vendor:publish' => VendorPublish::class,
+ ];
+
+ /**
+ * 启动器
+ * @var array
+ */
+ protected static $startCallbacks = [];
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+
+ if (!$this->app->initialized()) {
+ $this->app->initialize();
+ }
+
+ $this->definition = $this->getDefaultInputDefinition();
+
+ //加载指令
+ $this->loadCommands();
+
+ $this->start();
+ }
+
+ /**
+ * 添加初始化器
+ * @param Closure $callback
+ */
+ public static function starting(Closure $callback): void
+ {
+ static::$startCallbacks[] = $callback;
+ }
+
+ /**
+ * 清空启动器
+ */
+ public static function flushStartCallbacks(): void
+ {
+ static::$startCallbacks = [];
+ }
+
+ /**
+ * 设置执行用户
+ * @param $user
+ */
+ public static function setUser(string $user): void
+ {
+ if (extension_loaded('posix')) {
+ $user = posix_getpwnam($user);
+
+ if (!empty($user)) {
+ posix_setgid($user['gid']);
+ posix_setuid($user['uid']);
+ }
+ }
+ }
+
+ /**
+ * 启动
+ */
+ protected function start(): void
+ {
+ foreach (static::$startCallbacks as $callback) {
+ $callback($this);
+ }
+ }
+
+ /**
+ * 加载指令
+ * @access protected
+ */
+ protected function loadCommands(): void
+ {
+ $commands = $this->app->config->get('console.commands', []);
+ $commands = array_merge($this->defaultCommands, $commands);
+
+ $this->addCommands($commands);
+ }
+
+ /**
+ * @access public
+ * @param string $command
+ * @param array $parameters
+ * @param string $driver
+ * @return Output|Buffer
+ */
+ public function call(string $command, array $parameters = [], string $driver = 'buffer')
+ {
+ array_unshift($parameters, $command);
+
+ $input = new Input($parameters);
+ $output = new Output($driver);
+
+ $this->setCatchExceptions(false);
+ $this->find($command)->run($input, $output);
+
+ return $output;
+ }
+
+ /**
+ * 执行当前的指令
+ * @access public
+ * @return int
+ * @throws \Exception
+ * @api
+ */
+ public function run()
+ {
+ $input = new Input();
+ $output = new Output();
+
+ $this->configureIO($input, $output);
+
+ try {
+ $exitCode = $this->doRun($input, $output);
+ } catch (\Exception $e) {
+ if (!$this->catchExceptions) {
+ throw $e;
+ }
+
+ $output->renderException($e);
+
+ $exitCode = $e->getCode();
+ if (is_numeric($exitCode)) {
+ $exitCode = (int) $exitCode;
+ if (0 === $exitCode) {
+ $exitCode = 1;
+ }
+ } else {
+ $exitCode = 1;
+ }
+ }
+
+ if ($this->autoExit) {
+ if ($exitCode > 255) {
+ $exitCode = 255;
+ }
+
+ exit($exitCode);
+ }
+
+ return $exitCode;
+ }
+
+ /**
+ * 执行指令
+ * @access public
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ */
+ public function doRun(Input $input, Output $output)
+ {
+ if (true === $input->hasParameterOption(['--version', '-V'])) {
+ $output->writeln($this->getLongVersion());
+
+ return 0;
+ }
+
+ $name = $this->getCommandName($input);
+
+ if (true === $input->hasParameterOption(['--help', '-h'])) {
+ if (!$name) {
+ $name = 'help';
+ $input = new Input(['help']);
+ } else {
+ $this->wantHelps = true;
+ }
+ }
+
+ if (!$name) {
+ $name = $this->defaultCommand;
+ $input = new Input([$this->defaultCommand]);
+ }
+
+ $command = $this->find($name);
+
+ return $this->doRunCommand($command, $input, $output);
+ }
+
+ /**
+ * 设置输入参数定义
+ * @access public
+ * @param InputDefinition $definition
+ */
+ public function setDefinition(InputDefinition $definition): void
+ {
+ $this->definition = $definition;
+ }
+
+ /**
+ * 获取输入参数定义
+ * @access public
+ * @return InputDefinition The InputDefinition instance
+ */
+ public function getDefinition(): InputDefinition
+ {
+ return $this->definition;
+ }
+
+ /**
+ * Gets the help message.
+ * @access public
+ * @return string A help message.
+ */
+ public function getHelp(): string
+ {
+ return $this->getLongVersion();
+ }
+
+ /**
+ * 是否捕获异常
+ * @access public
+ * @param bool $boolean
+ * @api
+ */
+ public function setCatchExceptions(bool $boolean): void
+ {
+ $this->catchExceptions = $boolean;
+ }
+
+ /**
+ * 是否自动退出
+ * @access public
+ * @param bool $boolean
+ * @api
+ */
+ public function setAutoExit(bool $boolean): void
+ {
+ $this->autoExit = $boolean;
+ }
+
+ /**
+ * 获取完整的版本号
+ * @access public
+ * @return string
+ */
+ public function getLongVersion(): string
+ {
+ if ($this->app->version()) {
+ return sprintf('version %s ', $this->app->version());
+ }
+
+ return 'Console Tool ';
+ }
+
+ /**
+ * 添加指令集
+ * @access public
+ * @param array $commands
+ */
+ public function addCommands(array $commands): void
+ {
+ foreach ($commands as $key => $command) {
+ if (is_subclass_of($command, Command::class)) {
+ // 注册指令
+ $this->addCommand($command, is_numeric($key) ? '' : $key);
+ }
+ }
+ }
+
+ /**
+ * 添加一个指令
+ * @access public
+ * @param string|Command $command 指令对象或者指令类名
+ * @param string $name 指令名 留空则自动获取
+ * @return Command|void
+ */
+ public function addCommand($command, string $name = '')
+ {
+ if ($name) {
+ $this->commands[$name] = $command;
+ return;
+ }
+
+ if (is_string($command)) {
+ $command = $this->app->invokeClass($command);
+ }
+
+ $command->setConsole($this);
+
+ if (!$command->isEnabled()) {
+ $command->setConsole(null);
+ return;
+ }
+
+ $command->setApp($this->app);
+
+ if (null === $command->getDefinition()) {
+ throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
+ }
+
+ $this->commands[$command->getName()] = $command;
+
+ foreach ($command->getAliases() as $alias) {
+ $this->commands[$alias] = $command;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 获取指令
+ * @access public
+ * @param string $name 指令名称
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function getCommand(string $name): Command
+ {
+ if (!isset($this->commands[$name])) {
+ throw new InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
+ }
+
+ $command = $this->commands[$name];
+
+ if (is_string($command)) {
+ $command = $this->app->invokeClass($command);
+ /** @var Command $command */
+ $command->setConsole($this);
+ $command->setApp($this->app);
+ }
+
+ if ($this->wantHelps) {
+ $this->wantHelps = false;
+
+ /** @var HelpCommand $helpCommand */
+ $helpCommand = $this->getCommand('help');
+ $helpCommand->setCommand($command);
+
+ return $helpCommand;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 某个指令是否存在
+ * @access public
+ * @param string $name 指令名称
+ * @return bool
+ */
+ public function hasCommand(string $name): bool
+ {
+ return isset($this->commands[$name]);
+ }
+
+ /**
+ * 获取所有的命名空间
+ * @access public
+ * @return array
+ */
+ public function getNamespaces(): array
+ {
+ $namespaces = [];
+ foreach ($this->commands as $key => $command) {
+ if (is_string($command)) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($key));
+ } else {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
+
+ foreach ($command->getAliases() as $alias) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
+ }
+ }
+ }
+
+ return array_values(array_unique(array_filter($namespaces)));
+ }
+
+ /**
+ * 查找注册命名空间中的名称或缩写。
+ * @access public
+ * @param string $namespace
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function findNamespace(string $namespace): string
+ {
+ $allNamespaces = $this->getNamespaces();
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $namespace);
+ $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces);
+
+ if (empty($namespaces)) {
+ $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
+
+ if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new InvalidArgumentException($message);
+ }
+
+ $exact = in_array($namespace, $namespaces, true);
+ if (count($namespaces) > 1 && !$exact) {
+ throw new InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
+ }
+
+ return $exact ? $namespace : reset($namespaces);
+ }
+
+ /**
+ * 查找指令
+ * @access public
+ * @param string $name 名称或者别名
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function find(string $name): Command
+ {
+ $allCommands = array_keys($this->commands);
+
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $name);
+
+ $commands = preg_grep('{^' . $expr . '}', $allCommands);
+
+ if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
+ if (false !== $pos = strrpos($name, ':')) {
+ $this->findNamespace(substr($name, 0, $pos));
+ }
+
+ $message = sprintf('Command "%s" is not defined.', $name);
+
+ if ($alternatives = $this->findAlternatives($name, $allCommands)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new InvalidArgumentException($message);
+ }
+
+ $exact = in_array($name, $commands, true);
+ if (count($commands) > 1 && !$exact) {
+ $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
+
+ throw new InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
+ }
+
+ return $this->getCommand($exact ? $name : reset($commands));
+ }
+
+ /**
+ * 获取所有的指令
+ * @access public
+ * @param string $namespace 命名空间
+ * @return Command[]
+ * @api
+ */
+ public function all(string $namespace = null): array
+ {
+ if (null === $namespace) {
+ return $this->commands;
+ }
+
+ $commands = [];
+ foreach ($this->commands as $name => $command) {
+ if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
+ $commands[$name] = $command;
+ }
+ }
+
+ return $commands;
+ }
+
+ /**
+ * 配置基于用户的参数和选项的输入和输出实例。
+ * @access protected
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ */
+ protected function configureIO(Input $input, Output $output): void
+ {
+ if (true === $input->hasParameterOption(['--ansi'])) {
+ $output->setDecorated(true);
+ } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
+ $output->setDecorated(false);
+ }
+
+ if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
+ $input->setInteractive(false);
+ }
+
+ if (true === $input->hasParameterOption(['--quiet', '-q'])) {
+ $output->setVerbosity(Output::VERBOSITY_QUIET);
+ } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
+ $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
+ } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
+ $output->setVerbosity(Output::VERBOSITY_VERBOSE);
+ }
+ }
+
+ /**
+ * 执行指令
+ * @access protected
+ * @param Command $command 指令实例
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ * @return int
+ * @throws \Exception
+ */
+ protected function doRunCommand(Command $command, Input $input, Output $output)
+ {
+ return $command->run($input, $output);
+ }
+
+ /**
+ * 获取指令的基础名称
+ * @access protected
+ * @param Input $input
+ * @return string
+ */
+ protected function getCommandName(Input $input): string
+ {
+ return $input->getFirstArgument() ?: '';
+ }
+
+ /**
+ * 获取默认输入定义
+ * @access protected
+ * @return InputDefinition
+ */
+ protected function getDefaultInputDefinition(): InputDefinition
+ {
+ return new InputDefinition([
+ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
+ new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
+ new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
+ new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
+ new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
+ new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
+ new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
+ new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
+ ]);
+ }
+
+ /**
+ * 获取可能的建议
+ * @access private
+ * @param array $abbrevs
+ * @return string
+ */
+ private function getAbbreviationSuggestions(array $abbrevs): string
+ {
+ return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
+ }
+
+ /**
+ * 返回命名空间部分
+ * @access public
+ * @param string $name 指令
+ * @param int $limit 部分的命名空间的最大数量
+ * @return string
+ */
+ public function extractNamespace(string $name, int $limit = 0): string
+ {
+ $parts = explode(':', $name);
+ array_pop($parts);
+
+ return implode(':', 0 === $limit ? $parts : array_slice($parts, 0, $limit));
+ }
+
+ /**
+ * 查找可替代的建议
+ * @access private
+ * @param string $name
+ * @param array|\Traversable $collection
+ * @return array
+ */
+ private function findAlternatives(string $name, $collection): array
+ {
+ $threshold = 1e3;
+ $alternatives = [];
+
+ $collectionParts = [];
+ foreach ($collection as $item) {
+ $collectionParts[$item] = explode(':', $item);
+ }
+
+ foreach (explode(':', $name) as $i => $subname) {
+ foreach ($collectionParts as $collectionName => $parts) {
+ $exists = isset($alternatives[$collectionName]);
+ if (!isset($parts[$i]) && $exists) {
+ $alternatives[$collectionName] += $threshold;
+ continue;
+ } elseif (!isset($parts[$i])) {
+ continue;
+ }
+
+ $lev = levenshtein($subname, $parts[$i]);
+ if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
+ $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
+ } elseif ($exists) {
+ $alternatives[$collectionName] += $threshold;
+ }
+ }
+ }
+
+ foreach ($collection as $item) {
+ $lev = levenshtein($name, $item);
+ if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
+ $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
+ }
+ }
+
+ $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
+ return $lev < 2 * $threshold;
+ });
+ asort($alternatives);
+
+ return array_keys($alternatives);
+ }
+
+ /**
+ * 返回所有的命名空间
+ * @access private
+ * @param string $name
+ * @return array
+ */
+ private function extractAllNamespaces(string $name): array
+ {
+ $parts = explode(':', $name, -1);
+ $namespaces = [];
+
+ foreach ($parts as $part) {
+ if (count($namespaces)) {
+ $namespaces[] = end($namespaces) . ':' . $part;
+ } else {
+ $namespaces[] = $part;
+ }
+ }
+
+ return $namespaces;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Container.php b/vendor/topthink/framework/src/think/Container.php
new file mode 100644
index 0000000..320338c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Container.php
@@ -0,0 +1,551 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ArrayAccess;
+use ArrayIterator;
+use Closure;
+use Countable;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Psr\Container\ContainerInterface;
+use ReflectionClass;
+use ReflectionException;
+use ReflectionFunction;
+use ReflectionFunctionAbstract;
+use ReflectionMethod;
+use think\exception\ClassNotFoundException;
+use think\exception\FuncNotFoundException;
+use think\helper\Str;
+
+/**
+ * 容器管理类 支持PSR-11
+ */
+class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable
+{
+ /**
+ * 容器对象实例
+ * @var Container|Closure
+ */
+ protected static $instance;
+
+ /**
+ * 容器中的对象实例
+ * @var array
+ */
+ protected $instances = [];
+
+ /**
+ * 容器绑定标识
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 容器回调
+ * @var array
+ */
+ protected $invokeCallback = [];
+
+ /**
+ * 获取当前容器的实例(单例)
+ * @access public
+ * @return static
+ */
+ public static function getInstance()
+ {
+ if (is_null(static::$instance)) {
+ static::$instance = new static;
+ }
+
+ if (static::$instance instanceof Closure) {
+ return (static::$instance)();
+ }
+
+ return static::$instance;
+ }
+
+ /**
+ * 设置当前容器的实例
+ * @access public
+ * @param object|Closure $instance
+ * @return void
+ */
+ public static function setInstance($instance): void
+ {
+ static::$instance = $instance;
+ }
+
+ /**
+ * 注册一个容器对象回调
+ *
+ * @param string|Closure $abstract
+ * @param Closure|null $callback
+ * @return void
+ */
+ public function resolving($abstract, Closure $callback = null): void
+ {
+ if ($abstract instanceof Closure) {
+ $this->invokeCallback['*'][] = $abstract;
+ return;
+ }
+
+ $abstract = $this->getAlias($abstract);
+
+ $this->invokeCallback[$abstract][] = $callback;
+ }
+
+ /**
+ * 获取容器中的对象实例 不存在则创建
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @param array|true $vars 变量
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return object
+ */
+ public static function pull(string $abstract, array $vars = [], bool $newInstance = false)
+ {
+ return static::getInstance()->make($abstract, $vars, $newInstance);
+ }
+
+ /**
+ * 获取容器中的对象实例
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @return object
+ */
+ public function get($abstract)
+ {
+ if ($this->has($abstract)) {
+ return $this->make($abstract);
+ }
+
+ throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
+ }
+
+ /**
+ * 绑定一个类、闭包、实例、接口实现到容器
+ * @access public
+ * @param string|array $abstract 类标识、接口
+ * @param mixed $concrete 要绑定的类、闭包或者实例
+ * @return $this
+ */
+ public function bind($abstract, $concrete = null)
+ {
+ if (is_array($abstract)) {
+ foreach ($abstract as $key => $val) {
+ $this->bind($key, $val);
+ }
+ } elseif ($concrete instanceof Closure) {
+ $this->bind[$abstract] = $concrete;
+ } elseif (is_object($concrete)) {
+ $this->instance($abstract, $concrete);
+ } else {
+ $abstract = $this->getAlias($abstract);
+
+ $this->bind[$abstract] = $concrete;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 根据别名获取真实类名
+ * @param string $abstract
+ * @return string
+ */
+ public function getAlias(string $abstract): string
+ {
+ if (isset($this->bind[$abstract])) {
+ $bind = $this->bind[$abstract];
+
+ if (is_string($bind)) {
+ return $this->getAlias($bind);
+ }
+ }
+
+ return $abstract;
+ }
+
+ /**
+ * 绑定一个类实例到容器
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @param object $instance 类的实例
+ * @return $this
+ */
+ public function instance(string $abstract, $instance)
+ {
+ $abstract = $this->getAlias($abstract);
+
+ $this->instances[$abstract] = $instance;
+
+ return $this;
+ }
+
+ /**
+ * 判断容器中是否存在类及标识
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @return bool
+ */
+ public function bound(string $abstract): bool
+ {
+ return isset($this->bind[$abstract]) || isset($this->instances[$abstract]);
+ }
+
+ /**
+ * 判断容器中是否存在类及标识
+ * @access public
+ * @param string $name 类名或者标识
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ return $this->bound($name);
+ }
+
+ /**
+ * 判断容器中是否存在对象实例
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @return bool
+ */
+ public function exists(string $abstract): bool
+ {
+ $abstract = $this->getAlias($abstract);
+
+ return isset($this->instances[$abstract]);
+ }
+
+ /**
+ * 创建类的实例 已经存在则直接获取
+ * @access public
+ * @param string $abstract 类名或者标识
+ * @param array $vars 变量
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return mixed
+ */
+ public function make(string $abstract, array $vars = [], bool $newInstance = false)
+ {
+ $abstract = $this->getAlias($abstract);
+
+ if (isset($this->instances[$abstract]) && !$newInstance) {
+ return $this->instances[$abstract];
+ }
+
+ if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
+ $object = $this->invokeFunction($this->bind[$abstract], $vars);
+ } else {
+ $object = $this->invokeClass($abstract, $vars);
+ }
+
+ if (!$newInstance) {
+ $this->instances[$abstract] = $object;
+ }
+
+ return $object;
+ }
+
+ /**
+ * 删除容器中的对象实例
+ * @access public
+ * @param string $name 类名或者标识
+ * @return void
+ */
+ public function delete($name)
+ {
+ $name = $this->getAlias($name);
+
+ if (isset($this->instances[$name])) {
+ unset($this->instances[$name]);
+ }
+ }
+
+ /**
+ * 执行函数或者闭包方法 支持参数调用
+ * @access public
+ * @param string|Closure $function 函数或者闭包
+ * @param array $vars 参数
+ * @return mixed
+ */
+ public function invokeFunction($function, array $vars = [])
+ {
+ try {
+ $reflect = new ReflectionFunction($function);
+ } catch (ReflectionException $e) {
+ throw new FuncNotFoundException("function not exists: {$function}()", $function, $e);
+ }
+
+ $args = $this->bindParams($reflect, $vars);
+
+ return $function(...$args);
+ }
+
+ /**
+ * 调用反射执行类的方法 支持参数绑定
+ * @access public
+ * @param mixed $method 方法
+ * @param array $vars 参数
+ * @param bool $accessible 设置是否可访问
+ * @return mixed
+ */
+ public function invokeMethod($method, array $vars = [], bool $accessible = false)
+ {
+ if (is_array($method)) {
+ [$class, $method] = $method;
+
+ $class = is_object($class) ? $class : $this->invokeClass($class);
+ } else {
+ // 静态方法
+ [$class, $method] = explode('::', $method);
+ }
+
+ try {
+ $reflect = new ReflectionMethod($class, $method);
+ } catch (ReflectionException $e) {
+ $class = is_object($class) ? get_class($class) : $class;
+ throw new FuncNotFoundException('method not exists: ' . $class . '::' . $method . '()', "{$class}::{$method}", $e);
+ }
+
+ $args = $this->bindParams($reflect, $vars);
+
+ if ($accessible) {
+ $reflect->setAccessible($accessible);
+ }
+
+ return $reflect->invokeArgs(is_object($class) ? $class : null, $args);
+ }
+
+ /**
+ * 调用反射执行类的方法 支持参数绑定
+ * @access public
+ * @param object $instance 对象实例
+ * @param mixed $reflect 反射类
+ * @param array $vars 参数
+ * @return mixed
+ */
+ public function invokeReflectMethod($instance, $reflect, array $vars = [])
+ {
+ $args = $this->bindParams($reflect, $vars);
+
+ return $reflect->invokeArgs($instance, $args);
+ }
+
+ /**
+ * 调用反射执行callable 支持参数绑定
+ * @access public
+ * @param mixed $callable
+ * @param array $vars 参数
+ * @param bool $accessible 设置是否可访问
+ * @return mixed
+ */
+ public function invoke($callable, array $vars = [], bool $accessible = false)
+ {
+ if ($callable instanceof Closure) {
+ return $this->invokeFunction($callable, $vars);
+ } elseif (is_string($callable) && false === strpos($callable, '::')) {
+ return $this->invokeFunction($callable, $vars);
+ } else {
+ return $this->invokeMethod($callable, $vars, $accessible);
+ }
+ }
+
+ /**
+ * 调用反射执行类的实例化 支持依赖注入
+ * @access public
+ * @param string $class 类名
+ * @param array $vars 参数
+ * @return mixed
+ */
+ public function invokeClass(string $class, array $vars = [])
+ {
+ try {
+ $reflect = new ReflectionClass($class);
+ } catch (ReflectionException $e) {
+ throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
+ }
+
+ if ($reflect->hasMethod('__make')) {
+ $method = $reflect->getMethod('__make');
+ if ($method->isPublic() && $method->isStatic()) {
+ $args = $this->bindParams($method, $vars);
+ return $method->invokeArgs(null, $args);
+ }
+ }
+
+ $constructor = $reflect->getConstructor();
+
+ $args = $constructor ? $this->bindParams($constructor, $vars) : [];
+
+ $object = $reflect->newInstanceArgs($args);
+
+ $this->invokeAfter($class, $object);
+
+ return $object;
+ }
+
+ /**
+ * 执行invokeClass回调
+ * @access protected
+ * @param string $class 对象类名
+ * @param object $object 容器对象实例
+ * @return void
+ */
+ protected function invokeAfter(string $class, $object): void
+ {
+ if (isset($this->invokeCallback['*'])) {
+ foreach ($this->invokeCallback['*'] as $callback) {
+ $callback($object, $this);
+ }
+ }
+
+ if (isset($this->invokeCallback[$class])) {
+ foreach ($this->invokeCallback[$class] as $callback) {
+ $callback($object, $this);
+ }
+ }
+ }
+
+ /**
+ * 绑定参数
+ * @access protected
+ * @param ReflectionFunctionAbstract $reflect 反射类
+ * @param array $vars 参数
+ * @return array
+ */
+ protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array
+ {
+ if ($reflect->getNumberOfParameters() == 0) {
+ return [];
+ }
+
+ // 判断数组类型 数字数组时按顺序绑定参数
+ reset($vars);
+ $type = key($vars) === 0 ? 1 : 0;
+ $params = $reflect->getParameters();
+ $args = [];
+
+ foreach ($params as $param) {
+ $name = $param->getName();
+ $lowerName = Str::snake($name);
+ $class = $param->getClass();
+
+ if ($class) {
+ $args[] = $this->getObjectParam($class->getName(), $vars);
+ } elseif (1 == $type && !empty($vars)) {
+ $args[] = array_shift($vars);
+ } elseif (0 == $type && isset($vars[$name])) {
+ $args[] = $vars[$name];
+ } elseif (0 == $type && isset($vars[$lowerName])) {
+ $args[] = $vars[$lowerName];
+ } elseif ($param->isDefaultValueAvailable()) {
+ $args[] = $param->getDefaultValue();
+ } else {
+ throw new InvalidArgumentException('method param miss:' . $name);
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * 创建工厂对象实例
+ * @param string $name 工厂类名
+ * @param string $namespace 默认命名空间
+ * @param array $args
+ * @return mixed
+ * @deprecated
+ * @access public
+ */
+ public static function factory(string $name, string $namespace = '', ...$args)
+ {
+ $class = false !== strpos($name, '\\') ? $name : $namespace . ucwords($name);
+
+ return Container::getInstance()->invokeClass($class, $args);
+ }
+
+ /**
+ * 获取对象类型的参数值
+ * @access protected
+ * @param string $className 类名
+ * @param array $vars 参数
+ * @return mixed
+ */
+ protected function getObjectParam(string $className, array &$vars)
+ {
+ $array = $vars;
+ $value = array_shift($array);
+
+ if ($value instanceof $className) {
+ $result = $value;
+ array_shift($vars);
+ } else {
+ $result = $this->make($className);
+ }
+
+ return $result;
+ }
+
+ public function __set($name, $value)
+ {
+ $this->bind($name, $value);
+ }
+
+ public function __get($name)
+ {
+ return $this->get($name);
+ }
+
+ public function __isset($name): bool
+ {
+ return $this->exists($name);
+ }
+
+ public function __unset($name)
+ {
+ $this->delete($name);
+ }
+
+ public function offsetExists($key)
+ {
+ return $this->exists($key);
+ }
+
+ public function offsetGet($key)
+ {
+ return $this->make($key);
+ }
+
+ public function offsetSet($key, $value)
+ {
+ $this->bind($key, $value);
+ }
+
+ public function offsetUnset($key)
+ {
+ $this->delete($key);
+ }
+
+ //Countable
+ public function count()
+ {
+ return count($this->instances);
+ }
+
+ //IteratorAggregate
+ public function getIterator()
+ {
+ return new ArrayIterator($this->instances);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Cookie.php b/vendor/topthink/framework/src/think/Cookie.php
new file mode 100644
index 0000000..e1efd7c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Cookie.php
@@ -0,0 +1,230 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use DateTimeInterface;
+
+/**
+ * Cookie管理类
+ * @package think
+ */
+class Cookie
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // cookie 保存时间
+ 'expire' => 0,
+ // cookie 保存路径
+ 'path' => '/',
+ // cookie 有效域名
+ 'domain' => '',
+ // cookie 启用安全传输
+ 'secure' => false,
+ // httponly设置
+ 'httponly' => false,
+ // samesite 设置,支持 'strict' 'lax'
+ 'samesite' => '',
+ ];
+
+ /**
+ * Cookie写入数据
+ * @var array
+ */
+ protected $cookie = [];
+
+ /**
+ * 当前Request对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * 构造方法
+ * @access public
+ */
+ public function __construct(Request $request, array $config = [])
+ {
+ $this->request = $request;
+ $this->config = array_merge($this->config, array_change_key_case($config));
+ }
+
+ public static function __make(Request $request, Config $config)
+ {
+ return new static($request, $config->get('cookie'));
+ }
+
+ /**
+ * 获取cookie
+ * @access public
+ * @param mixed $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function get(string $name = '', $default = null)
+ {
+ return $this->request->cookie($name, $default);
+ }
+
+ /**
+ * 是否存在Cookie参数
+ * @access public
+ * @param string $name 变量名
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return $this->request->has($name, 'cookie');
+ }
+
+ /**
+ * Cookie 设置
+ *
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数
+ * @return void
+ */
+ public function set(string $name, string $value, $option = null): void
+ {
+ // 参数设置(会覆盖黙认设置)
+ if (!is_null($option)) {
+ if (is_numeric($option) || $option instanceof DateTimeInterface) {
+ $option = ['expire' => $option];
+ }
+
+ $config = array_merge($this->config, array_change_key_case($option));
+ } else {
+ $config = $this->config;
+ }
+
+ if ($config['expire'] instanceof DateTimeInterface) {
+ $expire = $config['expire']->getTimestamp();
+ } else {
+ $expire = !empty($config['expire']) ? time() + intval($config['expire']) : 0;
+ }
+
+ $this->setCookie($name, $value, $expire, $config);
+ }
+
+ /**
+ * Cookie 保存
+ *
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param int $expire 有效期
+ * @param array $option 可选参数
+ * @return void
+ */
+ protected function setCookie(string $name, string $value, int $expire, array $option = []): void
+ {
+ $this->cookie[$name] = [$value, $expire, $option];
+ }
+
+ /**
+ * 永久保存Cookie数据
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数 可能会是 null|integer|string
+ * @return void
+ */
+ public function forever(string $name, string $value = '', $option = null): void
+ {
+ if (is_null($option) || is_numeric($option)) {
+ $option = [];
+ }
+
+ $option['expire'] = 315360000;
+
+ $this->set($name, $value, $option);
+ }
+
+ /**
+ * Cookie删除
+ * @access public
+ * @param string $name cookie名称
+ * @return void
+ */
+ public function delete(string $name): void
+ {
+ $this->setCookie($name, '', time() - 3600, $this->config);
+ }
+
+ /**
+ * 获取cookie保存数据
+ * @access public
+ * @return array
+ */
+ public function getCookie(): array
+ {
+ return $this->cookie;
+ }
+
+ /**
+ * 保存Cookie
+ * @access public
+ * @return void
+ */
+ public function save(): void
+ {
+ foreach ($this->cookie as $name => $val) {
+ [$value, $expire, $option] = $val;
+
+ $this->saveCookie(
+ $name,
+ $value,
+ $expire,
+ $option['path'],
+ $option['domain'],
+ $option['secure'] ? true : false,
+ $option['httponly'] ? true : false,
+ $option['samesite']
+ );
+ }
+ }
+
+ /**
+ * 保存Cookie
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param int $expire cookie过期时间
+ * @param string $path 有效的服务器路径
+ * @param string $domain 有效域名/子域名
+ * @param bool $secure 是否仅仅通过HTTPS
+ * @param bool $httponly 仅可通过HTTP访问
+ * @param string $samesite 防止CSRF攻击和用户追踪
+ * @return void
+ */
+ protected function saveCookie(string $name, string $value, int $expire, string $path, string $domain, bool $secure, bool $httponly, string $samesite): void
+ {
+ if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
+ setcookie($name, $value, [
+ 'expires' => $expire,
+ 'path' => $path,
+ 'domain' => $domain,
+ 'secure' => $secure,
+ 'httponly' => $httponly,
+ 'samesite' => $samesite,
+ ]);
+ } else {
+ setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
+ }
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Db.php b/vendor/topthink/framework/src/think/Db.php
new file mode 100644
index 0000000..a7b24d8
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Db.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 数据库管理类
+ * @package think
+ * @property Config $config
+ */
+class Db extends DbManager
+{
+ /**
+ * @param Event $event
+ * @param Config $config
+ * @param Log $log
+ * @param Cache $cache
+ * @return Db
+ * @codeCoverageIgnore
+ */
+ public static function __make(Event $event, Config $config, Log $log, Cache $cache)
+ {
+ $db = new static();
+ $db->setConfig($config);
+ $db->setEvent($event);
+ $db->setLog($log);
+
+ $store = $db->getConfig('cache_store');
+ $db->setCache($cache->store($store));
+ $db->triggerSql();
+
+ return $db;
+ }
+
+ /**
+ * 注入模型对象
+ * @access public
+ * @return void
+ */
+ protected function modelMaker()
+ {
+ }
+
+ /**
+ * 设置配置对象
+ * @access public
+ * @param Config $config 配置对象
+ * @return void
+ */
+ public function setConfig($config): void
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * 获取配置参数
+ * @access public
+ * @param string $name 配置参数
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(string $name = '', $default = null)
+ {
+ if ('' !== $name) {
+ return $this->config->get('database.' . $name, $default);
+ }
+
+ return $this->config->get('database', []);
+ }
+
+ /**
+ * 设置Event对象
+ * @param Event $event
+ */
+ public function setEvent(Event $event): void
+ {
+ $this->event = $event;
+ }
+
+ /**
+ * 注册回调方法
+ * @access public
+ * @param string $event 事件名
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function event(string $event, callable $callback): void
+ {
+ if ($this->event) {
+ $this->event->listen('db.' . $event, $callback);
+ }
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string $event 事件名
+ * @param mixed $params 传入参数
+ * @param bool $once
+ * @return mixed
+ */
+ public function trigger(string $event, $params = null, bool $once = false)
+ {
+ if ($this->event) {
+ return $this->event->trigger('db.' . $event, $params, $once);
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Env.php b/vendor/topthink/framework/src/think/Env.php
new file mode 100644
index 0000000..1c5772b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Env.php
@@ -0,0 +1,181 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ArrayAccess;
+
+/**
+ * Env管理类
+ * @package think
+ */
+class Env implements ArrayAccess
+{
+ /**
+ * 环境变量数据
+ * @var array
+ */
+ protected $data = [];
+
+ 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) ?: [];
+ $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])) {
+ return $this->data[$name];
+ }
+
+ 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
+ public function offsetSet($name, $value): void
+ {
+ $this->set($name, $value);
+ }
+
+ public function offsetExists($name): bool
+ {
+ return $this->__isset($name);
+ }
+
+ public function offsetUnset($name)
+ {
+ throw new Exception('not support: unset');
+ }
+
+ 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..ebb951b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Event.php
@@ -0,0 +1,263 @@
+
+// +----------------------------------------------------------------------
+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,
+ ];
+
+ /**
+ * 应用对象
+ * @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] ?? [];
+ $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..30c6f25
--- /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..f72394d
--- /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..cd0085e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/File.php
@@ -0,0 +1,187 @@
+
+// +----------------------------------------------------------------------
+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;
+
+ 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();
+ }
+
+ /**
+ * 自动生成文件名
+ * @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((string) microtime(true));
+ break;
+ }
+ }
+ }
+
+ return $this->hashName . '.' . $this->extension();
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Filesystem.php b/vendor/topthink/framework/src/think/Filesystem.php
new file mode 100644
index 0000000..4d68225
--- /dev/null
+++ b/vendor/topthink/framework/src/think/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/framework/src/think/Http.php b/vendor/topthink/framework/src/think/Http.php
new file mode 100644
index 0000000..f9abfc0
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Http.php
@@ -0,0 +1,282 @@
+
+// +----------------------------------------------------------------------
+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 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 路由定义目录
+ * @return string
+ */
+ 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
+ {
+ //自动创建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->initialize();
+
+ // 加载全局中间件
+ $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..3ea9f9c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Lang.php
@@ -0,0 +1,282 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 多语言管理类
+ * @package think
+ */
+class Lang
+{
+ /**
+ * 配置参数
+ * @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(array $config = [])
+ {
+ $this->config = array_merge($this->config, array_change_key_case($config));
+ $this->range = $this->config['default_lang'];
+ }
+
+ public static function __make(Config $config)
+ {
+ return new static($config->get('lang'));
+ }
+
+ /**
+ * 设置当前语言
+ * @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|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;
+ }
+
+ 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 (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;
+ }
+
+ /**
+ * 自动侦测设置获取语言选择
+ * @access public
+ * @param Request $request
+ * @return string
+ */
+ public function detect(Request $request): string
+ {
+ // 自动侦测设置获取语言选择
+ $langSet = '';
+
+ if ($request->get($this->config['detect_var'])) {
+ // url中设置了语言变量
+ $langSet = strtolower($request->get($this->config['detect_var']));
+ } elseif ($request->header($this->config['header_var'])) {
+ // Header中设置了语言变量
+ $langSet = strtolower($request->header($this->config['header_var']));
+ } elseif ($request->cookie($this->config['cookie_var'])) {
+ // Cookie中设置了语言变量
+ $langSet = strtolower($request->cookie($this->config['cookie_var']));
+ } elseif ($request->server('HTTP_ACCEPT_LANGUAGE')) {
+ // 自动侦测浏览器语言
+ $match = preg_match('/^([a-z\d\-]+)/i', $request->server('HTTP_ACCEPT_LANGUAGE'), $matches);
+ if ($match) {
+ $langSet = strtolower($matches[1]);
+ if (isset($this->config['accept_language'][$langSet])) {
+ $langSet = $this->config['accept_language'][$langSet];
+ }
+ }
+ }
+
+ if (empty($this->config['allow_lang_list']) || in_array($langSet, $this->config['allow_lang_list'])) {
+ // 合法的语言
+ $this->range = $langSet;
+ }
+
+ return $this->range;
+ }
+
+ /**
+ * 保存当前语言到Cookie
+ * @access public
+ * @param Cookie $cookie Cookie对象
+ * @return void
+ */
+ public function saveToCookie(Cookie $cookie)
+ {
+ if ($this->config['use_cookie']) {
+ $cookie->set($this->config['cookie_var'], $this->range);
+ }
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Log.php b/vendor/topthink/framework/src/think/Log.php
new file mode 100644
index 0000000..f8dc3c7
--- /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..3bea50d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Manager.php
@@ -0,0 +1,176 @@
+
+// +----------------------------------------------------------------------
+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..c80fd1c
--- /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/Pipeline.php b/vendor/topthink/framework/src/think/Pipeline.php
new file mode 100644
index 0000000..677dcfd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Pipeline.php
@@ -0,0 +1,106 @@
+
+// +----------------------------------------------------------------------
+namespace think;
+
+use Closure;
+use Exception;
+use Throwable;
+
+class Pipeline
+{
+ protected $passable;
+
+ protected $pipes = [];
+
+ protected $exceptionHandler;
+
+ /**
+ * 初始数据
+ * @param $passable
+ * @return $this
+ */
+ public function send($passable)
+ {
+ $this->passable = $passable;
+ return $this;
+ }
+
+ /**
+ * 调用栈
+ * @param $pipes
+ * @return $this
+ */
+ public function through($pipes)
+ {
+ $this->pipes = is_array($pipes) ? $pipes : func_get_args();
+ return $this;
+ }
+
+ /**
+ * 执行
+ * @param Closure $destination
+ * @return mixed
+ */
+ public function then(Closure $destination)
+ {
+ $pipeline = array_reduce(
+ array_reverse($this->pipes),
+ $this->carry(),
+ function ($passable) use ($destination) {
+ try {
+ return $destination($passable);
+ } catch (Throwable | Exception $e) {
+ return $this->handleException($passable, $e);
+ }
+ });
+
+ return $pipeline($this->passable);
+ }
+
+ /**
+ * 设置异常处理器
+ * @param callable $handler
+ * @return $this
+ */
+ public function whenException($handler)
+ {
+ $this->exceptionHandler = $handler;
+ return $this;
+ }
+
+ protected function carry()
+ {
+ return function ($stack, $pipe) {
+ return function ($passable) use ($stack, $pipe) {
+ try {
+ return $pipe($passable, $stack);
+ } catch (Throwable | Exception $e) {
+ return $this->handleException($passable, $e);
+ }
+ };
+ };
+ }
+
+ /**
+ * 异常处理
+ * @param $passable
+ * @param $e
+ * @return mixed
+ */
+ protected function handleException($passable, Throwable $e)
+ {
+ if ($this->exceptionHandler) {
+ return call_user_func($this->exceptionHandler, $passable, $e);
+ }
+ throw $e;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Request.php b/vendor/topthink/framework/src/think/Request.php
new file mode 100644
index 0000000..f68113c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Request.php
@@ -0,0 +1,2148 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ArrayAccess;
+use think\file\UploadedFile;
+use think\route\Rule;
+
+/**
+ * 请求管理类
+ * @package think
+ */
+class Request implements ArrayAccess
+{
+ /**
+ * 兼容PATH_INFO获取
+ * @var array
+ */
+ protected $pathinfoFetch = ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'];
+
+ /**
+ * PATHINFO变量名 用于兼容模式
+ * @var string
+ */
+ protected $varPathinfo = 's';
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $varMethod = '_method';
+
+ /**
+ * 表单ajax伪装变量
+ * @var string
+ */
+ protected $varAjax = '_ajax';
+
+ /**
+ * 表单pjax伪装变量
+ * @var string
+ */
+ protected $varPjax = '_pjax';
+
+ /**
+ * 域名根
+ * @var string
+ */
+ protected $rootDomain = '';
+
+ /**
+ * HTTPS代理标识
+ * @var string
+ */
+ protected $httpsAgentName = '';
+
+ /**
+ * 前端代理服务器IP
+ * @var array
+ */
+ protected $proxyServerIp = [];
+
+ /**
+ * 前端代理服务器真实IP头
+ * @var array
+ */
+ protected $proxyServerIpHeader = ['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP'];
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * 域名(含协议及端口)
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * HOST(含端口)
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * 子域名
+ * @var string
+ */
+ protected $subDomain;
+
+ /**
+ * 泛域名
+ * @var string
+ */
+ protected $panDomain;
+
+ /**
+ * 当前URL地址
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * 基础URL
+ * @var string
+ */
+ protected $baseUrl;
+
+ /**
+ * 当前执行的文件
+ * @var string
+ */
+ protected $baseFile;
+
+ /**
+ * 访问的ROOT地址
+ * @var string
+ */
+ protected $root;
+
+ /**
+ * pathinfo
+ * @var string
+ */
+ protected $pathinfo;
+
+ /**
+ * pathinfo(不含后缀)
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * 当前请求的IP地址
+ * @var string
+ */
+ protected $realIP;
+
+ /**
+ * 当前控制器名
+ * @var string
+ */
+ protected $controller;
+
+ /**
+ * 当前操作名
+ * @var string
+ */
+ protected $action;
+
+ /**
+ * 当前请求参数
+ * @var array
+ */
+ protected $param = [];
+
+ /**
+ * 当前GET参数
+ * @var array
+ */
+ protected $get = [];
+
+ /**
+ * 当前POST参数
+ * @var array
+ */
+ protected $post = [];
+
+ /**
+ * 当前REQUEST参数
+ * @var array
+ */
+ protected $request = [];
+
+ /**
+ * 当前路由对象
+ * @var Rule
+ */
+ protected $rule;
+
+ /**
+ * 当前ROUTE参数
+ * @var array
+ */
+ protected $route = [];
+
+ /**
+ * 中间件传递的参数
+ * @var array
+ */
+ protected $middleware = [];
+
+ /**
+ * 当前PUT参数
+ * @var array
+ */
+ protected $put;
+
+ /**
+ * SESSION对象
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * COOKIE数据
+ * @var array
+ */
+ protected $cookie = [];
+
+ /**
+ * ENV对象
+ * @var Env
+ */
+ protected $env;
+
+ /**
+ * 当前SERVER参数
+ * @var array
+ */
+ protected $server = [];
+
+ /**
+ * 当前FILE参数
+ * @var array
+ */
+ protected $file = [];
+
+ /**
+ * 当前HEADER参数
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 资源类型定义
+ * @var array
+ */
+ protected $mimeType = [
+ 'xml' => 'application/xml,text/xml,application/x-xml',
+ 'json' => 'application/json,text/x-json,application/jsonrequest,text/json',
+ 'js' => 'text/javascript,application/javascript,application/x-javascript',
+ 'css' => 'text/css',
+ 'rss' => 'application/rss+xml',
+ 'yaml' => 'application/x-yaml,text/yaml',
+ 'atom' => 'application/atom+xml',
+ 'pdf' => 'application/pdf',
+ 'text' => 'text/plain',
+ 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*',
+ 'csv' => 'text/csv',
+ 'html' => 'text/html,application/xhtml+xml,*/*',
+ ];
+
+ /**
+ * 当前请求内容
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * 全局过滤规则
+ * @var array
+ */
+ protected $filter;
+
+ /**
+ * php://input内容
+ * @var string
+ */
+ // php://input
+ protected $input;
+
+ /**
+ * 请求安全Key
+ * @var string
+ */
+ protected $secureKey;
+
+ /**
+ * 是否合并Param
+ * @var bool
+ */
+ protected $mergeParam = false;
+
+ /**
+ * 架构函数
+ * @access public
+ */
+ public function __construct()
+ {
+ // 保存 php://input
+ $this->input = file_get_contents('php://input');
+ }
+
+ public static function __make(App $app)
+ {
+ $request = new static();
+
+ if (function_exists('apache_request_headers') && $result = apache_request_headers()) {
+ $header = $result;
+ } else {
+ $header = [];
+ $server = $_SERVER;
+ foreach ($server as $key => $val) {
+ if (0 === strpos($key, 'HTTP_')) {
+ $key = str_replace('_', '-', strtolower(substr($key, 5)));
+ $header[$key] = $val;
+ }
+ }
+ if (isset($server['CONTENT_TYPE'])) {
+ $header['content-type'] = $server['CONTENT_TYPE'];
+ }
+ if (isset($server['CONTENT_LENGTH'])) {
+ $header['content-length'] = $server['CONTENT_LENGTH'];
+ }
+ }
+
+ $request->header = array_change_key_case($header);
+ $request->server = $_SERVER;
+ $request->env = $app->env;
+
+ $inputData = $request->getInputData($request->input);
+
+ $request->get = $_GET;
+ $request->post = $_POST ?: $inputData;
+ $request->put = $inputData;
+ $request->request = $_REQUEST;
+ $request->cookie = $_COOKIE;
+ $request->file = $_FILES ?? [];
+
+ return $request;
+ }
+
+ /**
+ * 设置当前包含协议的域名
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setDomain(string $domain)
+ {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前包含协议的域名
+ * @access public
+ * @param bool $port 是否需要去除端口号
+ * @return string
+ */
+ public function domain(bool $port = false): string
+ {
+ return $this->scheme() . '://' . $this->host($port);
+ }
+
+ /**
+ * 获取当前根域名
+ * @access public
+ * @return string
+ */
+ public function rootDomain(): string
+ {
+ $root = $this->rootDomain;
+
+ if (!$root) {
+ $item = explode('.', $this->host());
+ $count = count($item);
+ $root = $count > 1 ? $item[$count - 2] . '.' . $item[$count - 1] : $item[0];
+ }
+
+ return $root;
+ }
+
+ /**
+ * 设置当前泛域名的值
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setSubDomain(string $domain)
+ {
+ $this->subDomain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前子域名
+ * @access public
+ * @return string
+ */
+ public function subDomain(): string
+ {
+ if (is_null($this->subDomain)) {
+ // 获取当前主域名
+ $rootDomain = $this->rootDomain();
+
+ if ($rootDomain) {
+ $this->subDomain = rtrim(stristr($this->host(), $rootDomain, true), '.');
+ } else {
+ $this->subDomain = '';
+ }
+ }
+
+ return $this->subDomain;
+ }
+
+ /**
+ * 设置当前泛域名的值
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setPanDomain(string $domain)
+ {
+ $this->panDomain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前泛域名的值
+ * @access public
+ * @return string
+ */
+ public function panDomain(): string
+ {
+ return $this->panDomain ?: '';
+ }
+
+ /**
+ * 设置当前完整URL 包括QUERY_STRING
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setUrl(string $url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * 获取当前完整URL 包括QUERY_STRING
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function url(bool $complete = false): string
+ {
+ if ($this->url) {
+ $url = $this->url;
+ } elseif ($this->server('HTTP_X_REWRITE_URL')) {
+ $url = $this->server('HTTP_X_REWRITE_URL');
+ } elseif ($this->server('REQUEST_URI')) {
+ $url = $this->server('REQUEST_URI');
+ } elseif ($this->server('ORIG_PATH_INFO')) {
+ $url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : '');
+ } elseif (isset($_SERVER['argv'][1])) {
+ $url = $_SERVER['argv'][1];
+ } else {
+ $url = '';
+ }
+
+ return $complete ? $this->domain() . $url : $url;
+ }
+
+ /**
+ * 设置当前URL 不含QUERY_STRING
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setBaseUrl(string $url)
+ {
+ $this->baseUrl = $url;
+ return $this;
+ }
+
+ /**
+ * 获取当前URL 不含QUERY_STRING
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function baseUrl(bool $complete = false): string
+ {
+ if (!$this->baseUrl) {
+ $str = $this->url();
+ $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str;
+ }
+
+ return $complete ? $this->domain() . $this->baseUrl : $this->baseUrl;
+ }
+
+ /**
+ * 获取当前执行的文件 SCRIPT_NAME
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function baseFile(bool $complete = false): string
+ {
+ if (!$this->baseFile) {
+ $url = '';
+ if (!$this->isCli()) {
+ $script_name = basename($this->server('SCRIPT_FILENAME'));
+ if (basename($this->server('SCRIPT_NAME')) === $script_name) {
+ $url = $this->server('SCRIPT_NAME');
+ } elseif (basename($this->server('PHP_SELF')) === $script_name) {
+ $url = $this->server('PHP_SELF');
+ } elseif (basename($this->server('ORIG_SCRIPT_NAME')) === $script_name) {
+ $url = $this->server('ORIG_SCRIPT_NAME');
+ } elseif (($pos = strpos($this->server('PHP_SELF'), '/' . $script_name)) !== false) {
+ $url = substr($this->server('SCRIPT_NAME'), 0, $pos) . '/' . $script_name;
+ } elseif ($this->server('DOCUMENT_ROOT') && strpos($this->server('SCRIPT_FILENAME'), $this->server('DOCUMENT_ROOT')) === 0) {
+ $url = str_replace('\\', '/', str_replace($this->server('DOCUMENT_ROOT'), '', $this->server('SCRIPT_FILENAME')));
+ }
+ }
+ $this->baseFile = $url;
+ }
+
+ return $complete ? $this->domain() . $this->baseFile : $this->baseFile;
+ }
+
+ /**
+ * 设置URL访问根地址
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setRoot(string $url)
+ {
+ $this->root = $url;
+ return $this;
+ }
+
+ /**
+ * 获取URL访问根地址
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function root(bool $complete = false): string
+ {
+ if (!$this->root) {
+ $file = $this->baseFile();
+ if ($file && 0 !== strpos($this->url(), $file)) {
+ $file = str_replace('\\', '/', dirname($file));
+ }
+ $this->root = rtrim($file, '/');
+ }
+
+ return $complete ? $this->domain() . $this->root : $this->root;
+ }
+
+ /**
+ * 获取URL访问根目录
+ * @access public
+ * @return string
+ */
+ public function rootUrl(): string
+ {
+ $base = $this->root();
+ $root = strpos($base, '.') ? ltrim(dirname($base), DIRECTORY_SEPARATOR) : $base;
+
+ if ('' != $root) {
+ $root = '/' . ltrim($root, '/');
+ }
+
+ return $root;
+ }
+
+ /**
+ * 设置当前请求的pathinfo
+ * @access public
+ * @param string $pathinfo
+ * @return $this
+ */
+ public function setPathinfo(string $pathinfo)
+ {
+ $this->pathinfo = $pathinfo;
+ return $this;
+ }
+
+ /**
+ * 获取当前请求URL的pathinfo信息(含URL后缀)
+ * @access public
+ * @return string
+ */
+ public function pathinfo(): string
+ {
+ if (is_null($this->pathinfo)) {
+ if (isset($_GET[$this->varPathinfo])) {
+ // 判断URL里面是否有兼容模式参数
+ $pathinfo = $_GET[$this->varPathinfo];
+ unset($_GET[$this->varPathinfo]);
+ unset($this->get[$this->varPathinfo]);
+ } elseif ($this->server('PATH_INFO')) {
+ $pathinfo = $this->server('PATH_INFO');
+ } elseif (false !== strpos(PHP_SAPI, 'cli')) {
+ $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
+ }
+
+ // 分析PATHINFO信息
+ if (!isset($pathinfo)) {
+ foreach ($this->pathinfoFetch as $type) {
+ if ($this->server($type)) {
+ $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
+ substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
+ break;
+ }
+ }
+ }
+
+ if (!empty($pathinfo)) {
+ unset($this->get[$pathinfo], $this->request[$pathinfo]);
+ }
+
+ $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
+ }
+
+ return $this->pathinfo;
+ }
+
+ /**
+ * 当前URL的访问后缀
+ * @access public
+ * @return string
+ */
+ public function ext(): string
+ {
+ return pathinfo($this->pathinfo(), PATHINFO_EXTENSION);
+ }
+
+ /**
+ * 获取当前请求的时间
+ * @access public
+ * @param bool $float 是否使用浮点类型
+ * @return integer|float
+ */
+ public function time(bool $float = false)
+ {
+ return $float ? $this->server('REQUEST_TIME_FLOAT') : $this->server('REQUEST_TIME');
+ }
+
+ /**
+ * 当前请求的资源类型
+ * @access public
+ * @return string
+ */
+ public function type(): string
+ {
+ $accept = $this->server('HTTP_ACCEPT');
+
+ if (empty($accept)) {
+ return '';
+ }
+
+ foreach ($this->mimeType as $key => $val) {
+ $array = explode(',', $val);
+ foreach ($array as $k => $v) {
+ if (stristr($accept, $v)) {
+ return $key;
+ }
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * 设置资源类型
+ * @access public
+ * @param string|array $type 资源类型名
+ * @param string $val 资源类型
+ * @return void
+ */
+ public function mimeType($type, $val = ''): void
+ {
+ if (is_array($type)) {
+ $this->mimeType = array_merge($this->mimeType, $type);
+ } else {
+ $this->mimeType[$type] = $val;
+ }
+ }
+
+ /**
+ * 设置请求类型
+ * @access public
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function setMethod(string $method)
+ {
+ $this->method = strtoupper($method);
+ return $this;
+ }
+
+ /**
+ * 当前的请求类型
+ * @access public
+ * @param bool $origin 是否获取原始请求类型
+ * @return string
+ */
+ public function method(bool $origin = false): string
+ {
+ if ($origin) {
+ // 获取原始请求类型
+ return $this->server('REQUEST_METHOD') ?: 'GET';
+ } elseif (!$this->method) {
+ if (isset($this->post[$this->varMethod])) {
+ $method = strtolower($this->post[$this->varMethod]);
+ if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
+ $this->method = strtoupper($method);
+ $this->{$method} = $this->post;
+ } else {
+ $this->method = 'POST';
+ }
+ unset($this->post[$this->varMethod]);
+ } elseif ($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
+ $this->method = strtoupper($this->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
+ } else {
+ $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
+ }
+ }
+
+ return $this->method;
+ }
+
+ /**
+ * 是否为GET请求
+ * @access public
+ * @return bool
+ */
+ public function isGet(): bool
+ {
+ return $this->method() == 'GET';
+ }
+
+ /**
+ * 是否为POST请求
+ * @access public
+ * @return bool
+ */
+ public function isPost(): bool
+ {
+ return $this->method() == 'POST';
+ }
+
+ /**
+ * 是否为PUT请求
+ * @access public
+ * @return bool
+ */
+ public function isPut(): bool
+ {
+ return $this->method() == 'PUT';
+ }
+
+ /**
+ * 是否为DELTE请求
+ * @access public
+ * @return bool
+ */
+ public function isDelete(): bool
+ {
+ return $this->method() == 'DELETE';
+ }
+
+ /**
+ * 是否为HEAD请求
+ * @access public
+ * @return bool
+ */
+ public function isHead(): bool
+ {
+ return $this->method() == 'HEAD';
+ }
+
+ /**
+ * 是否为PATCH请求
+ * @access public
+ * @return bool
+ */
+ public function isPatch(): bool
+ {
+ return $this->method() == 'PATCH';
+ }
+
+ /**
+ * 是否为OPTIONS请求
+ * @access public
+ * @return bool
+ */
+ public function isOptions(): bool
+ {
+ return $this->method() == 'OPTIONS';
+ }
+
+ /**
+ * 是否为cli
+ * @access public
+ * @return bool
+ */
+ public function isCli(): bool
+ {
+ return PHP_SAPI == 'cli';
+ }
+
+ /**
+ * 是否为cgi
+ * @access public
+ * @return bool
+ */
+ public function isCgi(): bool
+ {
+ return strpos(PHP_SAPI, 'cgi') === 0;
+ }
+
+ /**
+ * 获取当前请求的参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function param($name = '', $default = null, $filter = '')
+ {
+ if (empty($this->mergeParam)) {
+ $method = $this->method(true);
+
+ // 自动获取请求变量
+ switch ($method) {
+ case 'POST':
+ $vars = $this->post(false);
+ break;
+ case 'PUT':
+ case 'DELETE':
+ case 'PATCH':
+ $vars = $this->put(false);
+ break;
+ default:
+ $vars = [];
+ }
+
+ // 当前请求参数和URL地址中的参数合并
+ $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
+
+ $this->mergeParam = true;
+ }
+
+ if (is_array($name)) {
+ return $this->only($name, $this->param, $filter);
+ }
+
+ return $this->input($this->param, $name, $default, $filter);
+ }
+
+ /**
+ * 设置路由变量
+ * @access public
+ * @param Rule $rule 路由对象
+ * @return $this
+ */
+ public function setRule(Rule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+
+ /**
+ * 获取当前路由对象
+ * @access public
+ * @return Rule|null
+ */
+ public function rule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 设置路由变量
+ * @access public
+ * @param array $route 路由变量
+ * @return $this
+ */
+ public function setRoute(array $route)
+ {
+ $this->route = array_merge($this->route, $route);
+ $this->mergeParam = false;
+ return $this;
+ }
+
+ /**
+ * 获取路由参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function route($name = '', $default = null, $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->route, $filter);
+ }
+
+ return $this->input($this->route, $name, $default, $filter);
+ }
+
+ /**
+ * 获取GET参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function get($name = '', $default = null, $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->get, $filter);
+ }
+
+ return $this->input($this->get, $name, $default, $filter);
+ }
+
+ /**
+ * 获取中间件传递的参数
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function middleware($name, $default = null)
+ {
+ return $this->middleware[$name] ?? $default;
+ }
+
+ /**
+ * 获取POST参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function post($name = '', $default = null, $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->post, $filter);
+ }
+
+ return $this->input($this->post, $name, $default, $filter);
+ }
+
+ /**
+ * 获取PUT参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function put($name = '', $default = null, $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->put, $filter);
+ }
+
+ return $this->input($this->put, $name, $default, $filter);
+ }
+
+ protected function getInputData($content): array
+ {
+ $contentType = $this->contentType();
+ if ('application/x-www-form-urlencoded' == $contentType) {
+ parse_str($content, $data);
+ return $data;
+ } elseif (false !== strpos($contentType, 'json')) {
+ return (array) json_decode($content, true);
+ }
+
+ return [];
+ }
+
+ /**
+ * 设置获取DELETE参数
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function delete($name = '', $default = null, $filter = '')
+ {
+ return $this->put($name, $default, $filter);
+ }
+
+ /**
+ * 设置获取PATCH参数
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function patch($name = '', $default = null, $filter = '')
+ {
+ return $this->put($name, $default, $filter);
+ }
+
+ /**
+ * 获取request变量
+ * @access public
+ * @param string|array $name 数据名称
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function request($name = '', $default = null, $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->request, $filter);
+ }
+
+ return $this->input($this->request, $name, $default, $filter);
+ }
+
+ /**
+ * 获取环境变量
+ * @access public
+ * @param string $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function env(string $name = '', string $default = null)
+ {
+ if (empty($name)) {
+ return $this->env->get();
+ } else {
+ $name = strtoupper($name);
+ }
+
+ return $this->env->get($name, $default);
+ }
+
+ /**
+ * 获取session数据
+ * @access public
+ * @param string $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function session(string $name = '', $default = null)
+ {
+ if ('' === $name) {
+ return $this->session->all();
+ }
+ return $this->session->get($name, $default);
+ }
+
+ /**
+ * 获取cookie参数
+ * @access public
+ * @param mixed $name 数据名称
+ * @param string $default 默认值
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function cookie(string $name = '', $default = null, $filter = '')
+ {
+ if (!empty($name)) {
+ $data = $this->getData($this->cookie, $name, $default);
+ } else {
+ $data = $this->cookie;
+ }
+
+ // 解析过滤器
+ $filter = $this->getFilter($filter, $default);
+
+ if (is_array($data)) {
+ array_walk_recursive($data, [$this, 'filterValue'], $filter);
+ } else {
+ $this->filterValue($data, $name, $filter);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取server参数
+ * @access public
+ * @param string $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function server(string $name = '', string $default = '')
+ {
+ if (empty($name)) {
+ return $this->server;
+ } else {
+ $name = strtoupper($name);
+ }
+
+ return $this->server[$name] ?? $default;
+ }
+
+ /**
+ * 获取上传的文件信息
+ * @access public
+ * @param string $name 名称
+ * @return null|array|UploadedFile
+ */
+ public function file(string $name = '')
+ {
+ $files = $this->file;
+ if (!empty($files)) {
+
+ if (strpos($name, '.')) {
+ [$name, $sub] = explode('.', $name);
+ }
+
+ // 处理上传文件
+ $array = $this->dealUploadFile($files, $name);
+
+ if ('' === $name) {
+ // 获取全部文件
+ return $array;
+ } elseif (isset($sub) && isset($array[$name][$sub])) {
+ return $array[$name][$sub];
+ } elseif (isset($array[$name])) {
+ return $array[$name];
+ }
+ }
+ }
+
+ protected function dealUploadFile(array $files, string $name): array
+ {
+ $array = [];
+ foreach ($files as $key => $file) {
+ if (is_array($file['name'])) {
+ $item = [];
+ $keys = array_keys($file);
+ $count = count($file['name']);
+
+ for ($i = 0; $i < $count; $i++) {
+ if ($file['error'][$i] > 0) {
+ if ($name == $key) {
+ $this->throwUploadFileError($file['error'][$i]);
+ } else {
+ continue;
+ }
+ }
+
+ $temp['key'] = $key;
+
+ foreach ($keys as $_key) {
+ $temp[$_key] = $file[$_key][$i];
+ }
+
+ $item[] = new UploadedFile($temp['tmp_name'], $temp['name'], $temp['type'], $temp['error']);
+ }
+
+ $array[$key] = $item;
+ } else {
+ if ($file instanceof File) {
+ $array[$key] = $file;
+ } else {
+ if ($file['error'] > 0) {
+ if ($key == $name) {
+ $this->throwUploadFileError($file['error']);
+ } else {
+ continue;
+ }
+ }
+
+ $array[$key] = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);
+ }
+ }
+ }
+
+ return $array;
+ }
+
+ protected function throwUploadFileError($error)
+ {
+ static $fileUploadErrors = [
+ 1 => 'upload File size exceeds the maximum value',
+ 2 => 'upload File size exceeds the maximum value',
+ 3 => 'only the portion of file is uploaded',
+ 4 => 'no file to uploaded',
+ 6 => 'upload temp dir not found',
+ 7 => 'file write error',
+ ];
+
+ $msg = $fileUploadErrors[$error];
+ throw new Exception($msg, $error);
+ }
+
+ /**
+ * 设置或者获取当前的Header
+ * @access public
+ * @param string $name header名称
+ * @param string $default 默认值
+ * @return string|array
+ */
+ public function header(string $name = '', string $default = null)
+ {
+ if ('' === $name) {
+ return $this->header;
+ }
+
+ $name = str_replace('_', '-', strtolower($name));
+
+ return $this->header[$name] ?? $default;
+ }
+
+ /**
+ * 获取变量 支持过滤和默认值
+ * @access public
+ * @param array $data 数据源
+ * @param string|false $name 字段名
+ * @param mixed $default 默认值
+ * @param string|array $filter 过滤函数
+ * @return mixed
+ */
+ public function input(array $data = [], $name = '', $default = null, $filter = '')
+ {
+ if (false === $name) {
+ // 获取原始数据
+ return $data;
+ }
+
+ $name = (string) $name;
+ if ('' != $name) {
+ // 解析name
+ if (strpos($name, '/')) {
+ [$name, $type] = explode('/', $name);
+ }
+
+ $data = $this->getData($data, $name);
+
+ if (is_null($data)) {
+ return $default;
+ }
+
+ if (is_object($data)) {
+ return $data;
+ }
+ }
+
+ $data = $this->filterData($data, $filter, $name, $default);
+
+ if (isset($type) && $data !== $default) {
+ // 强制类型转换
+ $this->typeCast($data, $type);
+ }
+
+ return $data;
+ }
+
+ protected function filterData($data, $filter, $name, $default)
+ {
+ // 解析过滤器
+ $filter = $this->getFilter($filter, $default);
+
+ if (is_array($data)) {
+ array_walk_recursive($data, [$this, 'filterValue'], $filter);
+ } else {
+ $this->filterValue($data, $name, $filter);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 强制类型转换
+ * @access public
+ * @param mixed $data
+ * @param string $type
+ * @return mixed
+ */
+ private function typeCast(&$data, string $type)
+ {
+ switch (strtolower($type)) {
+ // 数组
+ case 'a':
+ $data = (array) $data;
+ break;
+ // 数字
+ case 'd':
+ $data = (int) $data;
+ break;
+ // 浮点
+ case 'f':
+ $data = (float) $data;
+ break;
+ // 布尔
+ case 'b':
+ $data = (boolean) $data;
+ break;
+ // 字符串
+ case 's':
+ if (is_scalar($data)) {
+ $data = (string) $data;
+ } else {
+ throw new \InvalidArgumentException('variable type error:' . gettype($data));
+ }
+ break;
+ }
+ }
+
+ /**
+ * 获取数据
+ * @access public
+ * @param array $data 数据源
+ * @param string $name 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ protected function getData(array $data, string $name, $default = null)
+ {
+ foreach (explode('.', $name) as $val) {
+ if (isset($data[$val])) {
+ $data = $data[$val];
+ } else {
+ return $default;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 设置或获取当前的过滤规则
+ * @access public
+ * @param mixed $filter 过滤规则
+ * @return mixed
+ */
+ public function filter($filter = null)
+ {
+ if (is_null($filter)) {
+ return $this->filter;
+ }
+
+ $this->filter = $filter;
+
+ return $this;
+ }
+
+ protected function getFilter($filter, $default): array
+ {
+ if (is_null($filter)) {
+ $filter = [];
+ } else {
+ $filter = $filter ?: $this->filter;
+ if (is_string($filter) && false === strpos($filter, '/')) {
+ $filter = explode(',', $filter);
+ } else {
+ $filter = (array) $filter;
+ }
+ }
+
+ $filter[] = $default;
+
+ return $filter;
+ }
+
+ /**
+ * 递归过滤给定的值
+ * @access public
+ * @param mixed $value 键值
+ * @param mixed $key 键名
+ * @param array $filters 过滤方法+默认值
+ * @return mixed
+ */
+ public function filterValue(&$value, $key, $filters)
+ {
+ $default = array_pop($filters);
+
+ foreach ($filters as $filter) {
+ if (is_callable($filter)) {
+ // 调用函数或者方法过滤
+ $value = call_user_func($filter, $value);
+ } elseif (is_scalar($value)) {
+ if (is_string($filter) && false !== strpos($filter, '/')) {
+ // 正则过滤
+ if (!preg_match($filter, $value)) {
+ // 匹配不成功返回默认值
+ $value = $default;
+ break;
+ }
+ } elseif (!empty($filter)) {
+ // filter函数不存在时, 则使用filter_var进行过滤
+ // filter为非整形值时, 调用filter_id取得过滤id
+ $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
+ if (false === $value) {
+ $value = $default;
+ break;
+ }
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 是否存在某个请求参数
+ * @access public
+ * @param string $name 变量名
+ * @param string $type 变量类型
+ * @param bool $checkEmpty 是否检测空值
+ * @return bool
+ */
+ public function has(string $name, string $type = 'param', bool $checkEmpty = false): bool
+ {
+ if (!in_array($type, ['param', 'get', 'post', 'put', 'patch', 'route', 'delete', 'cookie', 'session', 'env', 'request', 'server', 'header', 'file'])) {
+ return false;
+ }
+
+ $param = empty($this->$type) ? $this->$type() : $this->$type;
+
+ if (is_object($param)) {
+ return $param->has($name);
+ }
+
+ // 按.拆分成多维数组进行判断
+ foreach (explode('.', $name) as $val) {
+ if (isset($param[$val])) {
+ $param = $param[$val];
+ } else {
+ return false;
+ }
+ }
+
+ return ($checkEmpty && '' === $param) ? false : true;
+ }
+
+ /**
+ * 获取指定的参数
+ * @access public
+ * @param array $name 变量名
+ * @param mixed $data 数据或者变量类型
+ * @param string|array $filter 过滤方法
+ * @return array
+ */
+ public function only(array $name, $data = 'param', $filter = ''): array
+ {
+ $data = is_array($data) ? $data : $this->$data();
+
+ $item = [];
+ foreach ($name as $key => $val) {
+
+ if (is_int($key)) {
+ $default = null;
+ $key = $val;
+ if (!isset($data[$key])) {
+ continue;
+ }
+ } else {
+ $default = $val;
+ }
+
+ $item[$key] = $this->filterData($data[$key] ?? $default, $filter, $key, $default);
+ }
+
+ return $item;
+ }
+
+ /**
+ * 排除指定参数获取
+ * @access public
+ * @param array $name 变量名
+ * @param string $type 变量类型
+ * @return mixed
+ */
+ public function except(array $name, string $type = 'param'): array
+ {
+ $param = $this->$type();
+
+ foreach ($name as $key) {
+ if (isset($param[$key])) {
+ unset($param[$key]);
+ }
+ }
+
+ return $param;
+ }
+
+ /**
+ * 当前是否ssl
+ * @access public
+ * @return bool
+ */
+ public function isSsl(): bool
+ {
+ if ($this->server('HTTPS') && ('1' == $this->server('HTTPS') || 'on' == strtolower($this->server('HTTPS')))) {
+ return true;
+ } elseif ('https' == $this->server('REQUEST_SCHEME')) {
+ return true;
+ } elseif ('443' == $this->server('SERVER_PORT')) {
+ return true;
+ } elseif ('https' == $this->server('HTTP_X_FORWARDED_PROTO')) {
+ return true;
+ } elseif ($this->httpsAgentName && $this->server($this->httpsAgentName)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 当前是否JSON请求
+ * @access public
+ * @return bool
+ */
+ public function isJson(): bool
+ {
+ $acceptType = $this->type();
+
+ return false !== strpos($acceptType, 'json');
+ }
+
+ /**
+ * 当前是否Ajax请求
+ * @access public
+ * @param bool $ajax true 获取原始ajax请求
+ * @return bool
+ */
+ public function isAjax(bool $ajax = false): bool
+ {
+ $value = $this->server('HTTP_X_REQUESTED_WITH');
+ $result = $value && 'xmlhttprequest' == strtolower($value) ? true : false;
+
+ if (true === $ajax) {
+ return $result;
+ }
+
+ return $this->param($this->varAjax) ? true : $result;
+ }
+
+ /**
+ * 当前是否Pjax请求
+ * @access public
+ * @param bool $pjax true 获取原始pjax请求
+ * @return bool
+ */
+ public function isPjax(bool $pjax = false): bool
+ {
+ $result = !empty($this->server('HTTP_X_PJAX')) ? true : false;
+
+ if (true === $pjax) {
+ return $result;
+ }
+
+ return $this->param($this->varPjax) ? true : $result;
+ }
+
+ /**
+ * 获取客户端IP地址
+ * @access public
+ * @return string
+ */
+ public function ip(): string
+ {
+ if (!empty($this->realIP)) {
+ return $this->realIP;
+ }
+
+ $this->realIP = $this->server('REMOTE_ADDR', '');
+
+ // 如果指定了前端代理服务器IP以及其会发送的IP头
+ // 则尝试获取前端代理服务器发送过来的真实IP
+ $proxyIp = $this->proxyServerIp;
+ $proxyIpHeader = $this->proxyServerIpHeader;
+
+ if (count($proxyIp) > 0 && count($proxyIpHeader) > 0) {
+ // 从指定的HTTP头中依次尝试获取IP地址
+ // 直到获取到一个合法的IP地址
+ foreach ($proxyIpHeader as $header) {
+ $tempIP = $this->server($header);
+
+ if (empty($tempIP)) {
+ continue;
+ }
+
+ $tempIP = trim(explode(',', $tempIP)[0]);
+
+ if (!$this->isValidIP($tempIP)) {
+ $tempIP = null;
+ } else {
+ break;
+ }
+ }
+
+ // tempIP不为空,说明获取到了一个IP地址
+ // 这时我们检查 REMOTE_ADDR 是不是指定的前端代理服务器之一
+ // 如果是的话说明该 IP头 是由前端代理服务器设置的
+ // 否则则是伪装的
+ if (!empty($tempIP)) {
+ $realIPBin = $this->ip2bin($this->realIP);
+
+ foreach ($proxyIp as $ip) {
+ $serverIPElements = explode('/', $ip);
+ $serverIP = $serverIPElements[0];
+ $serverIPPrefix = $serverIPElements[1] ?? 128;
+ $serverIPBin = $this->ip2bin($serverIP);
+
+ // IP类型不符
+ if (strlen($realIPBin) !== strlen($serverIPBin)) {
+ continue;
+ }
+
+ if (strncmp($realIPBin, $serverIPBin, (int) $serverIPPrefix) === 0) {
+ $this->realIP = $tempIP;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$this->isValidIP($this->realIP)) {
+ $this->realIP = '0.0.0.0';
+ }
+
+ return $this->realIP;
+ }
+
+ /**
+ * 检测是否是合法的IP地址
+ *
+ * @param string $ip IP地址
+ * @param string $type IP地址类型 (ipv4, ipv6)
+ *
+ * @return boolean
+ */
+ public function isValidIP(string $ip, string $type = ''): bool
+ {
+ switch (strtolower($type)) {
+ case 'ipv4':
+ $flag = FILTER_FLAG_IPV4;
+ break;
+ case 'ipv6':
+ $flag = FILTER_FLAG_IPV6;
+ break;
+ default:
+ $flag = null;
+ break;
+ }
+
+ return boolval(filter_var($ip, FILTER_VALIDATE_IP, $flag));
+ }
+
+ /**
+ * 将IP地址转换为二进制字符串
+ *
+ * @param string $ip
+ *
+ * @return string
+ */
+ public function ip2bin(string $ip): string
+ {
+ if ($this->isValidIP($ip, 'ipv6')) {
+ $IPHex = str_split(bin2hex(inet_pton($ip)), 4);
+ foreach ($IPHex as $key => $value) {
+ $IPHex[$key] = intval($value, 16);
+ }
+ $IPBin = vsprintf('%016b%016b%016b%016b%016b%016b%016b%016b', $IPHex);
+ } else {
+ $IPHex = str_split(bin2hex(inet_pton($ip)), 2);
+ foreach ($IPHex as $key => $value) {
+ $IPHex[$key] = intval($value, 16);
+ }
+ $IPBin = vsprintf('%08b%08b%08b%08b', $IPHex);
+ }
+
+ return $IPBin;
+ }
+
+ /**
+ * 检测是否使用手机访问
+ * @access public
+ * @return bool
+ */
+ public function isMobile(): bool
+ {
+ if ($this->server('HTTP_VIA') && stristr($this->server('HTTP_VIA'), "wap")) {
+ return true;
+ } elseif ($this->server('HTTP_ACCEPT') && strpos(strtoupper($this->server('HTTP_ACCEPT')), "VND.WAP.WML")) {
+ return true;
+ } elseif ($this->server('HTTP_X_WAP_PROFILE') || $this->server('HTTP_PROFILE')) {
+ return true;
+ } elseif ($this->server('HTTP_USER_AGENT') && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $this->server('HTTP_USER_AGENT'))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 当前URL地址中的scheme参数
+ * @access public
+ * @return string
+ */
+ public function scheme(): string
+ {
+ return $this->isSsl() ? 'https' : 'http';
+ }
+
+ /**
+ * 当前请求URL地址中的query参数
+ * @access public
+ * @return string
+ */
+ public function query(): string
+ {
+ return $this->server('QUERY_STRING', '');
+ }
+
+ /**
+ * 设置当前请求的host(包含端口)
+ * @access public
+ * @param string $host 主机名(含端口)
+ * @return $this
+ */
+ public function setHost(string $host)
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ /**
+ * 当前请求的host
+ * @access public
+ * @param bool $strict true 仅仅获取HOST
+ * @return string
+ */
+ public function host(bool $strict = false): string
+ {
+ if ($this->host) {
+ $host = $this->host;
+ } else {
+ $host = strval($this->server('HTTP_X_FORWARDED_HOST') ?: $this->server('HTTP_HOST'));
+ }
+
+ return true === $strict && strpos($host, ':') ? strstr($host, ':', true) : $host;
+ }
+
+ /**
+ * 当前请求URL地址中的port参数
+ * @access public
+ * @return int
+ */
+ public function port(): int
+ {
+ return (int) ($this->server('HTTP_X_FORWARDED_PORT') ?: $this->server('SERVER_PORT', ''));
+ }
+
+ /**
+ * 当前请求 SERVER_PROTOCOL
+ * @access public
+ * @return string
+ */
+ public function protocol(): string
+ {
+ return $this->server('SERVER_PROTOCOL', '');
+ }
+
+ /**
+ * 当前请求 REMOTE_PORT
+ * @access public
+ * @return int
+ */
+ public function remotePort(): int
+ {
+ return (int) $this->server('REMOTE_PORT', '');
+ }
+
+ /**
+ * 当前请求 HTTP_CONTENT_TYPE
+ * @access public
+ * @return string
+ */
+ public function contentType(): string
+ {
+ $contentType = $this->header('Content-Type');
+
+ if ($contentType) {
+ if (strpos($contentType, ';')) {
+ [$type] = explode(';', $contentType);
+ } else {
+ $type = $contentType;
+ }
+ return trim($type);
+ }
+
+ return '';
+ }
+
+ /**
+ * 获取当前请求的安全Key
+ * @access public
+ * @return string
+ */
+ public function secureKey(): string
+ {
+ if (is_null($this->secureKey)) {
+ $this->secureKey = uniqid('', true);
+ }
+
+ return $this->secureKey;
+ }
+
+ /**
+ * 设置当前的控制器名
+ * @access public
+ * @param string $controller 控制器名
+ * @return $this
+ */
+ public function setController(string $controller)
+ {
+ $this->controller = $controller;
+ return $this;
+ }
+
+ /**
+ * 设置当前的操作名
+ * @access public
+ * @param string $action 操作名
+ * @return $this
+ */
+ public function setAction(string $action)
+ {
+ $this->action = $action;
+ return $this;
+ }
+
+ /**
+ * 获取当前的控制器名
+ * @access public
+ * @param bool $convert 转换为小写
+ * @return string
+ */
+ public function controller(bool $convert = false): string
+ {
+ $name = $this->controller ?: '';
+ return $convert ? strtolower($name) : $name;
+ }
+
+ /**
+ * 获取当前的操作名
+ * @access public
+ * @param bool $convert 转换为小写
+ * @return string
+ */
+ public function action(bool $convert = false): string
+ {
+ $name = $this->action ?: '';
+ return $convert ? strtolower($name) : $name;
+ }
+
+ /**
+ * 设置或者获取当前请求的content
+ * @access public
+ * @return string
+ */
+ public function getContent(): string
+ {
+ if (is_null($this->content)) {
+ $this->content = $this->input;
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * 获取当前请求的php://input
+ * @access public
+ * @return string
+ */
+ public function getInput(): string
+ {
+ return $this->input;
+ }
+
+ /**
+ * 生成请求令牌
+ * @access public
+ * @param string $name 令牌名称
+ * @param mixed $type 令牌生成方法
+ * @return string
+ */
+ public function buildToken(string $name = '__token__', $type = 'md5'): string
+ {
+ $type = is_callable($type) ? $type : 'md5';
+ $token = call_user_func($type, $this->server('REQUEST_TIME_FLOAT'));
+
+ $this->session->set($name, $token);
+
+ return $token;
+ }
+
+ /**
+ * 检查请求令牌
+ * @access public
+ * @param string $token 令牌名称
+ * @param array $data 表单数据
+ * @return bool
+ */
+ public function checkToken(string $token = '__token__', array $data = []): bool
+ {
+ if (in_array($this->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
+ return true;
+ }
+
+ if (!$this->session->has($token)) {
+ // 令牌数据无效
+ return false;
+ }
+ // Header验证
+ if ($this->header('X-CSRF-TOKEN') && $this->session->get($token) === $this->header('X-CSRF-TOKEN')) {
+ // 防止重复提交
+ $this->session->delete($token); // 验证完成销毁session
+ return true;
+ }
+
+ if (empty($data)) {
+ $data = $this->post();
+ }
+
+ // 令牌验证
+ if (isset($data[$token]) && $this->session->get($token) === $data[$token]) {
+ // 防止重复提交
+ $this->session->delete($token); // 验证完成销毁session
+ return true;
+ }
+
+ // 开启TOKEN重置
+ $this->session->delete($token);
+ return false;
+ }
+
+ /**
+ * 设置在中间件传递的数据
+ * @access public
+ * @param array $middleware 数据
+ * @return $this
+ */
+ public function withMiddleware(array $middleware)
+ {
+ $this->middleware = array_merge($this->middleware, $middleware);
+ return $this;
+ }
+
+ /**
+ * 设置GET数据
+ * @access public
+ * @param array $get 数据
+ * @return $this
+ */
+ public function withGet(array $get)
+ {
+ $this->get = $get;
+ return $this;
+ }
+
+ /**
+ * 设置POST数据
+ * @access public
+ * @param array $post 数据
+ * @return $this
+ */
+ public function withPost(array $post)
+ {
+ $this->post = $post;
+ return $this;
+ }
+
+ /**
+ * 设置COOKIE数据
+ * @access public
+ * @param array $cookie 数据
+ * @return $this
+ */
+ public function withCookie(array $cookie)
+ {
+ $this->cookie = $cookie;
+ return $this;
+ }
+
+ /**
+ * 设置SESSION数据
+ * @access public
+ * @param Session $session 数据
+ * @return $this
+ */
+ public function withSession(Session $session)
+ {
+ $this->session = $session;
+ return $this;
+ }
+
+ /**
+ * 设置SERVER数据
+ * @access public
+ * @param array $server 数据
+ * @return $this
+ */
+ public function withServer(array $server)
+ {
+ $this->server = array_change_key_case($server, CASE_UPPER);
+ return $this;
+ }
+
+ /**
+ * 设置HEADER数据
+ * @access public
+ * @param array $header 数据
+ * @return $this
+ */
+ public function withHeader(array $header)
+ {
+ $this->header = array_change_key_case($header);
+ return $this;
+ }
+
+ /**
+ * 设置ENV数据
+ * @access public
+ * @param Env $env 数据
+ * @return $this
+ */
+ public function withEnv(Env $env)
+ {
+ $this->env = $env;
+ return $this;
+ }
+
+ /**
+ * 设置php://input数据
+ * @access public
+ * @param string $input RAW数据
+ * @return $this
+ */
+ public function withInput(string $input)
+ {
+ $this->input = $input;
+ if (!empty($input)) {
+ $inputData = $this->getInputData($input);
+ if (!empty($inputData)) {
+ $this->post = $inputData;
+ $this->put = $inputData;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * 设置文件上传数据
+ * @access public
+ * @param array $files 上传信息
+ * @return $this
+ */
+ public function withFiles(array $files)
+ {
+ $this->file = $files;
+ return $this;
+ }
+
+ /**
+ * 设置ROUTE变量
+ * @access public
+ * @param array $route 数据
+ * @return $this
+ */
+ public function withRoute(array $route)
+ {
+ $this->route = $route;
+ return $this;
+ }
+
+ /**
+ * 设置中间传递数据
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $value 值
+ */
+ public function __set(string $name, $value)
+ {
+ $this->middleware[$name] = $value;
+ }
+
+ /**
+ * 获取中间传递数据的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ return $this->middleware($name);
+ }
+
+ /**
+ * 检测中间传递数据的值
+ * @access public
+ * @param string $name 名称
+ * @return boolean
+ */
+ public function __isset(string $name): bool
+ {
+ return isset($this->middleware[$name]);
+ }
+
+ // ArrayAccess
+ public function offsetExists($name): bool
+ {
+ return $this->has($name);
+ }
+
+ public function offsetGet($name)
+ {
+ return $this->param($name);
+ }
+
+ public function offsetSet($name, $value)
+ {}
+
+ public function offsetUnset($name)
+ {}
+
+}
diff --git a/vendor/topthink/framework/src/think/Response.php b/vendor/topthink/framework/src/think/Response.php
new file mode 100644
index 0000000..c11a28f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Response.php
@@ -0,0 +1,410 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 响应输出基础类
+ * @package think
+ */
+abstract class Response
+{
+ /**
+ * 原始数据
+ * @var mixed
+ */
+ protected $data;
+
+ /**
+ * 当前contentType
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ /**
+ * 字符集
+ * @var string
+ */
+ protected $charset = 'utf-8';
+
+ /**
+ * 状态码
+ * @var integer
+ */
+ protected $code = 200;
+
+ /**
+ * 是否允许请求缓存
+ * @var bool
+ */
+ protected $allowCache = true;
+
+ /**
+ * 输出参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * header参数
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 输出内容
+ * @var string
+ */
+ protected $content = null;
+
+ /**
+ * Cookie对象
+ * @var Cookie
+ */
+ protected $cookie;
+
+ /**
+ * Session对象
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * 初始化
+ * @access protected
+ * @param mixed $data 输出数据
+ * @param int $code 状态码
+ */
+ protected function init($data = '', int $code = 200)
+ {
+ $this->data($data);
+ $this->code = $code;
+
+ $this->contentType($this->contentType, $this->charset);
+ }
+
+ /**
+ * 创建Response对象
+ * @access public
+ * @param mixed $data 输出数据
+ * @param string $type 输出类型
+ * @param int $code 状态码
+ * @return Response
+ */
+ public static function create($data = '', string $type = 'html', int $code = 200): Response
+ {
+ $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type));
+
+ return Container::getInstance()->invokeClass($class, [$data, $code]);
+ }
+
+ /**
+ * 设置Session对象
+ * @access public
+ * @param Session $session Session对象
+ * @return $this
+ */
+ public function setSession(Session $session)
+ {
+ $this->session = $session;
+ return $this;
+ }
+
+ /**
+ * 发送数据到客户端
+ * @access public
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function send(): void
+ {
+ // 处理输出数据
+ $data = $this->getContent();
+
+ if (!headers_sent() && !empty($this->header)) {
+ // 发送状态码
+ http_response_code($this->code);
+ // 发送头部信息
+ foreach ($this->header as $name => $val) {
+ header($name . (!is_null($val) ? ':' . $val : ''));
+ }
+ }
+ if ($this->cookie) {
+ $this->cookie->save();
+ }
+
+ $this->sendData($data);
+
+ if (function_exists('fastcgi_finish_request')) {
+ // 提高页面响应
+ fastcgi_finish_request();
+ }
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data)
+ {
+ return $data;
+ }
+
+ /**
+ * 输出数据
+ * @access protected
+ * @param string $data 要处理的数据
+ * @return void
+ */
+ protected function sendData(string $data): void
+ {
+ echo $data;
+ }
+
+ /**
+ * 输出的参数
+ * @access public
+ * @param mixed $options 输出参数
+ * @return $this
+ */
+ public function options(array $options = [])
+ {
+ $this->options = array_merge($this->options, $options);
+
+ return $this;
+ }
+
+ /**
+ * 输出数据设置
+ * @access public
+ * @param mixed $data 输出数据
+ * @return $this
+ */
+ public function data($data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * 是否允许请求缓存
+ * @access public
+ * @param bool $cache 允许请求缓存
+ * @return $this
+ */
+ public function allowCache(bool $cache)
+ {
+ $this->allowCache = $cache;
+
+ return $this;
+ }
+
+ /**
+ * 是否允许请求缓存
+ * @access public
+ * @return $this
+ */
+ public function isAllowCache()
+ {
+ return $this->allowCache;
+ }
+
+ /**
+ * 设置Cookie
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数
+ * @return $this
+ */
+ public function cookie(string $name, string $value, $option = null)
+ {
+ $this->cookie->set($name, $value, $option);
+
+ return $this;
+ }
+
+ /**
+ * 设置响应头
+ * @access public
+ * @param array $header 参数
+ * @return $this
+ */
+ public function header(array $header = [])
+ {
+ $this->header = array_merge($this->header, $header);
+
+ return $this;
+ }
+
+ /**
+ * 设置页面输出内容
+ * @access public
+ * @param mixed $content
+ * @return $this
+ */
+ public function content($content)
+ {
+ if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+ $content,
+ '__toString',
+ ])
+ ) {
+ throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+ }
+
+ $this->content = (string) $content;
+
+ return $this;
+ }
+
+ /**
+ * 发送HTTP状态
+ * @access public
+ * @param integer $code 状态码
+ * @return $this
+ */
+ public function code(int $code)
+ {
+ $this->code = $code;
+
+ return $this;
+ }
+
+ /**
+ * LastModified
+ * @access public
+ * @param string $time
+ * @return $this
+ */
+ public function lastModified(string $time)
+ {
+ $this->header['Last-Modified'] = $time;
+
+ return $this;
+ }
+
+ /**
+ * Expires
+ * @access public
+ * @param string $time
+ * @return $this
+ */
+ public function expires(string $time)
+ {
+ $this->header['Expires'] = $time;
+
+ return $this;
+ }
+
+ /**
+ * ETag
+ * @access public
+ * @param string $eTag
+ * @return $this
+ */
+ public function eTag(string $eTag)
+ {
+ $this->header['ETag'] = $eTag;
+
+ return $this;
+ }
+
+ /**
+ * 页面缓存控制
+ * @access public
+ * @param string $cache 状态码
+ * @return $this
+ */
+ public function cacheControl(string $cache)
+ {
+ $this->header['Cache-control'] = $cache;
+
+ return $this;
+ }
+
+ /**
+ * 页面输出类型
+ * @access public
+ * @param string $contentType 输出类型
+ * @param string $charset 输出编码
+ * @return $this
+ */
+ public function contentType(string $contentType, string $charset = 'utf-8')
+ {
+ $this->header['Content-Type'] = $contentType . '; charset=' . $charset;
+
+ return $this;
+ }
+
+ /**
+ * 获取头部信息
+ * @access public
+ * @param string $name 头部名称
+ * @return mixed
+ */
+ public function getHeader(string $name = '')
+ {
+ if (!empty($name)) {
+ return $this->header[$name] ?? null;
+ }
+
+ return $this->header;
+ }
+
+ /**
+ * 获取原始数据
+ * @access public
+ * @return mixed
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * 获取输出数据
+ * @access public
+ * @return string
+ */
+ public function getContent(): string
+ {
+ if (null == $this->content) {
+ $content = $this->output($this->data);
+
+ if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+ $content,
+ '__toString',
+ ])
+ ) {
+ throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+ }
+
+ $this->content = (string) $content;
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * 获取状态码
+ * @access public
+ * @return integer
+ */
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Route.php b/vendor/topthink/framework/src/think/Route.php
new file mode 100644
index 0000000..1583d06
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Route.php
@@ -0,0 +1,914 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use think\exception\RouteNotFoundException;
+use think\route\Dispatch;
+use think\route\dispatch\Callback;
+use think\route\dispatch\Url as UrlDispatch;
+use think\route\Domain;
+use think\route\Resource;
+use think\route\Rule;
+use think\route\RuleGroup;
+use think\route\RuleItem;
+use think\route\RuleName;
+use think\route\Url as UrlBuild;
+
+/**
+ * 路由管理类
+ * @package think
+ */
+class Route
+{
+ /**
+ * REST定义
+ * @var array
+ */
+ protected $rest = [
+ 'index' => ['get', '', 'index'],
+ 'create' => ['get', '/create', 'create'],
+ 'edit' => ['get', '//edit', 'edit'],
+ 'read' => ['get', '/', 'read'],
+ 'save' => ['post', '', 'save'],
+ 'update' => ['put', '/', 'update'],
+ 'delete' => ['delete', '/', 'delete'],
+ ];
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // pathinfo分隔符
+ 'pathinfo_depr' => '/',
+ // 是否开启路由延迟解析
+ 'url_lazy_route' => false,
+ // 是否强制使用路由
+ 'url_route_must' => false,
+ // 合并路由规则
+ 'route_rule_merge' => false,
+ // 路由是否完全匹配
+ 'route_complete_match' => false,
+ // 去除斜杠
+ 'remove_slash' => false,
+ // 使用注解路由
+ 'route_annotation' => false,
+ // 默认的路由变量规则
+ 'default_route_pattern' => '[\w\.]+',
+ // URL伪静态后缀
+ 'url_html_suffix' => 'html',
+ // 访问控制器层名称
+ 'controller_layer' => 'controller',
+ // 空控制器名
+ 'empty_controller' => 'Error',
+ // 是否使用控制器后缀
+ 'controller_suffix' => false,
+ // 默认控制器名
+ 'default_controller' => 'Index',
+ // 默认操作名
+ 'default_action' => 'index',
+ // 操作方法后缀
+ 'action_suffix' => '',
+ // 非路由变量是否使用普通参数方式(用于URL生成)
+ 'url_common_param' => true,
+ ];
+
+ /**
+ * 当前应用
+ * @var App
+ */
+ protected $app;
+
+ /**
+ * 请求对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * @var RuleName
+ */
+ protected $ruleName;
+
+ /**
+ * 当前HOST
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * 当前分组对象
+ * @var RuleGroup
+ */
+ protected $group;
+
+ /**
+ * 路由绑定
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 域名对象
+ * @var Domain[]
+ */
+ protected $domains = [];
+
+ /**
+ * 跨域路由规则
+ * @var RuleGroup
+ */
+ protected $cross;
+
+ /**
+ * 路由是否延迟解析
+ * @var bool
+ */
+ protected $lazy = false;
+
+ /**
+ * 路由是否测试模式
+ * @var bool
+ */
+ protected $isTest = false;
+
+ /**
+ * (分组)路由规则是否合并解析
+ * @var bool
+ */
+ protected $mergeRuleRegex = false;
+
+ /**
+ * 是否去除URL最后的斜线
+ * @var bool
+ */
+ protected $removeSlash = false;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->ruleName = new RuleName();
+ $this->setDefaultDomain();
+
+ if (is_file($this->app->getRuntimePath() . 'route.php')) {
+ // 读取路由映射文件
+ $this->import(include $this->app->getRuntimePath() . 'route.php');
+ }
+ }
+
+ protected function init()
+ {
+ $this->config = array_merge($this->config, $this->app->config->get('route'));
+
+ if (!empty($this->config['middleware'])) {
+ $this->app->middleware->import($this->config['middleware'], 'route');
+ }
+
+ $this->lazy($this->config['url_lazy_route']);
+ $this->mergeRuleRegex = $this->config['route_rule_merge'];
+ $this->removeSlash = $this->config['remove_slash'];
+
+ $this->group->removeSlash($this->removeSlash);
+ }
+
+ public function config(string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->config;
+ }
+
+ return $this->config[$name] ?? null;
+ }
+
+ /**
+ * 设置路由域名及分组(包括资源路由)是否延迟解析
+ * @access public
+ * @param bool $lazy 路由是否延迟解析
+ * @return $this
+ */
+ public function lazy(bool $lazy = true)
+ {
+ $this->lazy = $lazy;
+ return $this;
+ }
+
+ /**
+ * 设置路由为测试模式
+ * @access public
+ * @param bool $test 路由是否测试模式
+ * @return void
+ */
+ public function setTestMode(bool $test): void
+ {
+ $this->isTest = $test;
+ }
+
+ /**
+ * 检查路由是否为测试模式
+ * @access public
+ * @return bool
+ */
+ public function isTest(): bool
+ {
+ return $this->isTest;
+ }
+
+ /**
+ * 设置路由域名及分组(包括资源路由)是否合并解析
+ * @access public
+ * @param bool $merge 路由是否合并解析
+ * @return $this
+ */
+ public function mergeRuleRegex(bool $merge = true)
+ {
+ $this->mergeRuleRegex = $merge;
+ $this->group->mergeRuleRegex($merge);
+
+ return $this;
+ }
+
+ /**
+ * 初始化默认域名
+ * @access protected
+ * @return void
+ */
+ protected function setDefaultDomain(): void
+ {
+ // 注册默认域名
+ $domain = new Domain($this);
+
+ $this->domains['-'] = $domain;
+
+ // 默认分组
+ $this->group = $domain;
+ }
+
+ /**
+ * 设置当前分组
+ * @access public
+ * @param RuleGroup $group 域名
+ * @return void
+ */
+ public function setGroup(RuleGroup $group): void
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * 获取指定标识的路由分组 不指定则获取当前分组
+ * @access public
+ * @param string $name 分组标识
+ * @return RuleGroup
+ */
+ public function getGroup(string $name = null)
+ {
+ return $name ? $this->ruleName->getGroup($name) : $this->group;
+ }
+
+ /**
+ * 注册变量规则
+ * @access public
+ * @param array $pattern 变量规则
+ * @return $this
+ */
+ public function pattern(array $pattern)
+ {
+ $this->group->pattern($pattern);
+
+ return $this;
+ }
+
+ /**
+ * 注册路由参数
+ * @access public
+ * @param array $option 参数
+ * @return $this
+ */
+ public function option(array $option)
+ {
+ $this->group->option($option);
+
+ return $this;
+ }
+
+ /**
+ * 注册域名路由
+ * @access public
+ * @param string|array $name 子域名
+ * @param mixed $rule 路由规则
+ * @return Domain
+ */
+ public function domain($name, $rule = null): Domain
+ {
+ // 支持多个域名使用相同路由规则
+ $domainName = is_array($name) ? array_shift($name) : $name;
+
+ if (!isset($this->domains[$domainName])) {
+ $domain = (new Domain($this, $domainName, $rule))
+ ->lazy($this->lazy)
+ ->removeSlash($this->removeSlash)
+ ->mergeRuleRegex($this->mergeRuleRegex);
+
+ $this->domains[$domainName] = $domain;
+ } else {
+ $domain = $this->domains[$domainName];
+ $domain->parseGroupRule($rule);
+ }
+
+ if (is_array($name) && !empty($name)) {
+ foreach ($name as $item) {
+ $this->domains[$item] = $domainName;
+ }
+ }
+
+ // 返回域名对象
+ return $domain;
+ }
+
+ /**
+ * 获取域名
+ * @access public
+ * @return array
+ */
+ public function getDomains(): array
+ {
+ return $this->domains;
+ }
+
+ /**
+ * 获取RuleName对象
+ * @access public
+ * @return RuleName
+ */
+ public function getRuleName(): RuleName
+ {
+ return $this->ruleName;
+ }
+
+ /**
+ * 设置路由绑定
+ * @access public
+ * @param string $bind 绑定信息
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function bind(string $bind, string $domain = null)
+ {
+ $domain = is_null($domain) ? '-' : $domain;
+
+ $this->bind[$domain] = $bind;
+
+ return $this;
+ }
+
+ /**
+ * 读取路由绑定信息
+ * @access public
+ * @return array
+ */
+ public function getBind(): array
+ {
+ return $this->bind;
+ }
+
+ /**
+ * 读取路由绑定
+ * @access public
+ * @param string $domain 域名
+ * @return string|null
+ */
+ public function getDomainBind(string $domain = null)
+ {
+ if (is_null($domain)) {
+ $domain = $this->host;
+ } elseif (false === strpos($domain, '.') && $this->request) {
+ $domain .= '.' . $this->request->rootDomain();
+ }
+
+ if ($this->request) {
+ $subDomain = $this->request->subDomain();
+
+ if (strpos($subDomain, '.')) {
+ $name = '*' . strstr($subDomain, '.');
+ }
+ }
+
+ if (isset($this->bind[$domain])) {
+ $result = $this->bind[$domain];
+ } elseif (isset($name) && isset($this->bind[$name])) {
+ $result = $this->bind[$name];
+ } elseif (!empty($subDomain) && isset($this->bind['*'])) {
+ $result = $this->bind['*'];
+ } else {
+ $result = null;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 读取路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @param string $method 请求类型
+ * @return RuleItem[]
+ */
+ public function getName(string $name = null, string $domain = null, string $method = '*'): array
+ {
+ return $this->ruleName->getName($name, $domain, $method);
+ }
+
+ /**
+ * 批量导入路由标识
+ * @access public
+ * @param array $name 路由标识
+ * @return void
+ */
+ public function import(array $name): void
+ {
+ $this->ruleName->import($name);
+ }
+
+ /**
+ * 注册路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param RuleItem $ruleItem 路由规则
+ * @param bool $first 是否优先
+ * @return void
+ */
+ public function setName(string $name, RuleItem $ruleItem, bool $first = false): void
+ {
+ $this->ruleName->setName($name, $ruleItem, $first);
+ }
+
+ /**
+ * 保存路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param RuleItem $ruleItem RuleItem对象
+ * @return void
+ */
+ public function setRule(string $rule, RuleItem $ruleItem = null): void
+ {
+ $this->ruleName->setRule($rule, $ruleItem);
+ }
+
+ /**
+ * 读取路由
+ * @access public
+ * @param string $rule 路由规则
+ * @return RuleItem[]
+ */
+ public function getRule(string $rule): array
+ {
+ return $this->ruleName->getRule($rule);
+ }
+
+ /**
+ * 读取路由列表
+ * @access public
+ * @return array
+ */
+ public function getRuleList(): array
+ {
+ return $this->ruleName->getRuleList();
+ }
+
+ /**
+ * 清空路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->ruleName->clear();
+
+ if ($this->group) {
+ $this->group->clear();
+ }
+ }
+
+ /**
+ * 注册路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function rule(string $rule, $route = null, string $method = '*'): RuleItem
+ {
+ if ($route instanceof Response) {
+ // 兼容之前的路由到响应对象,感觉不需要,使用场景很少,闭包就能实现
+ $route = function () use ($route) {
+ return $route;
+ };
+ }
+ return $this->group->addRule($rule, $route, $method);
+ }
+
+ /**
+ * 设置跨域有效路由规则
+ * @access public
+ * @param Rule $rule 路由规则
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function setCrossDomainRule(Rule $rule, string $method = '*')
+ {
+ if (!isset($this->cross)) {
+ $this->cross = (new RuleGroup($this))->mergeRuleRegex($this->mergeRuleRegex);
+ }
+
+ $this->cross->addRuleItem($rule, $method);
+
+ return $this;
+ }
+
+ /**
+ * 注册路由分组
+ * @access public
+ * @param string|\Closure $name 分组名称或者参数
+ * @param mixed $route 分组路由
+ * @return RuleGroup
+ */
+ public function group($name, $route = null): RuleGroup
+ {
+ if ($name instanceof Closure) {
+ $route = $name;
+ $name = '';
+ }
+
+ return (new RuleGroup($this, $this->group, $name, $route))
+ ->lazy($this->lazy)
+ ->removeSlash($this->removeSlash)
+ ->mergeRuleRegex($this->mergeRuleRegex);
+ }
+
+ /**
+ * 注册路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function any(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, '*');
+ }
+
+ /**
+ * 注册GET路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function get(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'GET');
+ }
+
+ /**
+ * 注册POST路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function post(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'POST');
+ }
+
+ /**
+ * 注册PUT路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function put(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'PUT');
+ }
+
+ /**
+ * 注册DELETE路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function delete(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'DELETE');
+ }
+
+ /**
+ * 注册PATCH路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function patch(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'PATCH');
+ }
+
+ /**
+ * 注册OPTIONS路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function options(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'OPTIONS');
+ }
+
+ /**
+ * 注册资源路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @return Resource
+ */
+ public function resource(string $rule, string $route): Resource
+ {
+ return (new Resource($this, $this->group, $rule, $route, $this->rest))
+ ->lazy($this->lazy);
+ }
+
+ /**
+ * 注册视图路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $template 路由模板地址
+ * @param array $vars 模板变量
+ * @return RuleItem
+ */
+ public function view(string $rule, string $template = '', array $vars = []): RuleItem
+ {
+ return $this->rule($rule, function () use ($vars, $template) {
+ return Response::create($template, 'view')->assign($vars);
+ }, 'GET');
+ }
+
+ /**
+ * 注册重定向路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @param int $status 状态码
+ * @return RuleItem
+ */
+ public function redirect(string $rule, string $route = '', int $status = 301): RuleItem
+ {
+ return $this->rule($rule, function () use ($status, $route) {
+ return Response::create($route, 'redirect')->code($status);
+ }, '*');
+ }
+
+ /**
+ * rest方法定义和修改
+ * @access public
+ * @param string|array $name 方法名称
+ * @param array|bool $resource 资源
+ * @return $this
+ */
+ public function rest($name, $resource = [])
+ {
+ if (is_array($name)) {
+ $this->rest = $resource ? $name : array_merge($this->rest, $name);
+ } else {
+ $this->rest[$name] = $resource;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取rest方法定义的参数
+ * @access public
+ * @param string $name 方法名称
+ * @return array|null
+ */
+ public function getRest(string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->rest;
+ }
+
+ return $this->rest[$name] ?? null;
+ }
+
+ /**
+ * 注册未匹配路由规则后的处理
+ * @access public
+ * @param string|Closure $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function miss($route, string $method = '*'): RuleItem
+ {
+ return $this->group->miss($route, $method);
+ }
+
+ /**
+ * 路由调度
+ * @param Request $request
+ * @param Closure|bool $withRoute
+ * @return Response
+ */
+ public function dispatch(Request $request, $withRoute = true)
+ {
+ $this->request = $request;
+ $this->host = $this->request->host(true);
+ $this->init();
+
+ if ($withRoute) {
+ //加载路由
+ if ($withRoute instanceof Closure) {
+ $withRoute();
+ }
+ $dispatch = $this->check();
+ } else {
+ $dispatch = $this->url($this->path());
+ }
+
+ $dispatch->init($this->app);
+
+ return $this->app->middleware->pipeline('route')
+ ->send($request)
+ ->then(function () use ($dispatch) {
+ return $dispatch->run();
+ });
+ }
+
+ /**
+ * 检测URL路由
+ * @access public
+ * @return Dispatch|false
+ * @throws RouteNotFoundException
+ */
+ public function check()
+ {
+ // 自动检测域名路由
+ $url = str_replace($this->config['pathinfo_depr'], '|', $this->path());
+
+ $completeMatch = $this->config['route_complete_match'];
+
+ $result = $this->checkDomain()->check($this->request, $url, $completeMatch);
+
+ if (false === $result && !empty($this->cross)) {
+ // 检测跨域路由
+ $result = $this->cross->check($this->request, $url, $completeMatch);
+ }
+
+ if (false !== $result) {
+ return $result;
+ } elseif ($this->config['url_route_must']) {
+ throw new RouteNotFoundException();
+ }
+
+ return $this->url($url);
+ }
+
+ /**
+ * 获取当前请求URL的pathinfo信息(不含URL后缀)
+ * @access protected
+ * @return string
+ */
+ protected function path(): string
+ {
+ $suffix = $this->config['url_html_suffix'];
+ $pathinfo = $this->request->pathinfo();
+
+ if (false === $suffix) {
+ // 禁止伪静态访问
+ $path = $pathinfo;
+ } elseif ($suffix) {
+ // 去除正常的URL后缀
+ $path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
+ } else {
+ // 允许任何后缀访问
+ $path = preg_replace('/\.' . $this->request->ext() . '$/i', '', $pathinfo);
+ }
+
+ return $path;
+ }
+
+ /**
+ * 默认URL解析
+ * @access public
+ * @param string $url URL地址
+ * @return Dispatch
+ */
+ public function url(string $url): Dispatch
+ {
+ if ($this->request->method() == 'OPTIONS') {
+ // 自动响应options请求
+ return new Callback($this->request, $this->group, function () {
+ return Response::create('', 'html', 204)->header(['Allow' => 'GET, POST, PUT, DELETE']);
+ });
+ }
+
+ return new UrlDispatch($this->request, $this->group, $url);
+ }
+
+ /**
+ * 检测域名的路由规则
+ * @access protected
+ * @return Domain
+ */
+ protected function checkDomain(): Domain
+ {
+ $item = false;
+
+ if (count($this->domains) > 1) {
+ // 获取当前子域名
+ $subDomain = $this->request->subDomain();
+
+ $domain = $subDomain ? explode('.', $subDomain) : [];
+ $domain2 = $domain ? array_pop($domain) : '';
+
+ if ($domain) {
+ // 存在三级域名
+ $domain3 = array_pop($domain);
+ }
+
+ if (isset($this->domains[$this->host])) {
+ // 子域名配置
+ $item = $this->domains[$this->host];
+ } elseif (isset($this->domains[$subDomain])) {
+ $item = $this->domains[$subDomain];
+ } elseif (isset($this->domains['*.' . $domain2]) && !empty($domain3)) {
+ // 泛三级域名
+ $item = $this->domains['*.' . $domain2];
+ $panDomain = $domain3;
+ } elseif (isset($this->domains['*']) && !empty($domain2)) {
+ // 泛二级域名
+ if ('www' != $domain2) {
+ $item = $this->domains['*'];
+ $panDomain = $domain2;
+ }
+ }
+
+ if (isset($panDomain)) {
+ // 保存当前泛域名
+ $this->request->setPanDomain($panDomain);
+ }
+ }
+
+ if (false === $item) {
+ // 检测全局域名规则
+ $item = $this->domains['-'];
+ }
+
+ if (is_string($item)) {
+ $item = $this->domains[$item];
+ }
+
+ return $item;
+ }
+
+ /**
+ * URL生成 支持路由反射
+ * @access public
+ * @param string $url 路由地址
+ * @param array $vars 参数 ['a'=>'val1', 'b'=>'val2']
+ * @return UrlBuild
+ */
+ public function buildUrl(string $url = '', array $vars = []): UrlBuild
+ {
+ return $this->app->make(UrlBuild::class, [$this, $this->app, $url, $vars], true);
+ }
+
+ /**
+ * 设置全局的路由分组参数
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return RuleGroup
+ */
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->group, $method], $args);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Service.php b/vendor/topthink/framework/src/think/Service.php
new file mode 100644
index 0000000..c1128ca
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Service.php
@@ -0,0 +1,66 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use think\event\RouteLoaded;
+
+/**
+ * 系统服务基础类
+ * @method void register()
+ * @method void boot()
+ */
+abstract class Service
+{
+ protected $app;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 加载路由
+ * @access protected
+ * @param string $path 路由路径
+ */
+ protected function loadRoutesFrom($path)
+ {
+ $this->registerRoutes(function () use ($path) {
+ include $path;
+ });
+ }
+
+ /**
+ * 注册路由
+ * @param Closure $closure
+ */
+ protected function registerRoutes(Closure $closure)
+ {
+ $this->app->event->listen(RouteLoaded::class, $closure);
+ }
+
+ /**
+ * 添加指令
+ * @access protected
+ * @param array|string $commands 指令
+ */
+ protected function commands($commands)
+ {
+ $commands = is_array($commands) ? $commands : func_get_args();
+
+ Console::starting(function (Console $console) use ($commands) {
+ $console->addCommands($commands);
+ });
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Session.php b/vendor/topthink/framework/src/think/Session.php
new file mode 100644
index 0000000..7bd38a5
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Session.php
@@ -0,0 +1,65 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use think\helper\Arr;
+use think\session\Store;
+
+/**
+ * Session管理类
+ * @package think
+ * @mixin Store
+ */
+class Session extends Manager
+{
+ protected $namespace = '\\think\\session\\driver\\';
+
+ protected function createDriver(string $name)
+ {
+ $handler = parent::createDriver($name);
+
+ return new Store($this->getConfig('name') ?: 'PHPSESSID', $handler, $this->getConfig('serialize'));
+ }
+
+ /**
+ * 获取Session配置
+ * @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('session.' . $name, $default);
+ }
+
+ return $this->app->config->get('session');
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ $config = $this->app->config->get('session', []);
+ Arr::forget($config, 'type');
+ return $config;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver()
+ {
+ return $this->app->config->get('session.type', 'file');
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Validate.php b/vendor/topthink/framework/src/think/Validate.php
new file mode 100644
index 0000000..baca769
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Validate.php
@@ -0,0 +1,1681 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use think\exception\ValidateException;
+use think\helper\Str;
+use think\validate\ValidateRule;
+
+/**
+ * 数据验证类
+ * @package think
+ */
+class Validate
+{
+ /**
+ * 自定义验证类型
+ * @var array
+ */
+ protected $type = [];
+
+ /**
+ * 验证类型别名
+ * @var array
+ */
+ protected $alias = [
+ '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq',
+ ];
+
+ /**
+ * 当前验证规则
+ * @var array
+ */
+ protected $rule = [];
+
+ /**
+ * 验证提示信息
+ * @var array
+ */
+ protected $message = [];
+
+ /**
+ * 验证字段描述
+ * @var array
+ */
+ protected $field = [];
+
+ /**
+ * 默认规则提示
+ * @var array
+ */
+ protected $typeMsg = [
+ 'require' => ':attribute require',
+ 'must' => ':attribute must',
+ 'number' => ':attribute must be numeric',
+ 'integer' => ':attribute must be integer',
+ 'float' => ':attribute must be float',
+ 'boolean' => ':attribute must be bool',
+ 'email' => ':attribute not a valid email address',
+ 'mobile' => ':attribute not a valid mobile',
+ 'array' => ':attribute must be a array',
+ 'accepted' => ':attribute must be yes,on or 1',
+ 'date' => ':attribute not a valid datetime',
+ 'file' => ':attribute not a valid file',
+ 'image' => ':attribute not a valid image',
+ 'alpha' => ':attribute must be alpha',
+ 'alphaNum' => ':attribute must be alpha-numeric',
+ 'alphaDash' => ':attribute must be alpha-numeric, dash, underscore',
+ 'activeUrl' => ':attribute not a valid domain or ip',
+ 'chs' => ':attribute must be chinese',
+ 'chsAlpha' => ':attribute must be chinese or alpha',
+ 'chsAlphaNum' => ':attribute must be chinese,alpha-numeric',
+ 'chsDash' => ':attribute must be chinese,alpha-numeric,underscore, dash',
+ 'url' => ':attribute not a valid url',
+ 'ip' => ':attribute not a valid ip',
+ 'dateFormat' => ':attribute must be dateFormat of :rule',
+ 'in' => ':attribute must be in :rule',
+ 'notIn' => ':attribute be notin :rule',
+ 'between' => ':attribute must between :1 - :2',
+ 'notBetween' => ':attribute not between :1 - :2',
+ 'length' => 'size of :attribute must be :rule',
+ 'max' => 'max size of :attribute must be :rule',
+ 'min' => 'min size of :attribute must be :rule',
+ 'after' => ':attribute cannot be less than :rule',
+ 'before' => ':attribute cannot exceed :rule',
+ 'expire' => ':attribute not within :rule',
+ 'allowIp' => 'access IP is not allowed',
+ 'denyIp' => 'access IP denied',
+ 'confirm' => ':attribute out of accord with :2',
+ 'different' => ':attribute cannot be same with :2',
+ 'egt' => ':attribute must greater than or equal :rule',
+ 'gt' => ':attribute must greater than :rule',
+ 'elt' => ':attribute must less than or equal :rule',
+ 'lt' => ':attribute must less than :rule',
+ 'eq' => ':attribute must equal :rule',
+ 'unique' => ':attribute has exists',
+ 'regex' => ':attribute not conform to the rules',
+ 'method' => 'invalid Request method',
+ 'token' => 'invalid token',
+ 'fileSize' => 'filesize not match',
+ 'fileExt' => 'extensions to upload is not allowed',
+ 'fileMime' => 'mimetype to upload is not allowed',
+ ];
+
+ /**
+ * 当前验证场景
+ * @var string
+ */
+ protected $currentScene;
+
+ /**
+ * 内置正则验证规则
+ * @var array
+ */
+ protected $defaultRegex = [
+ 'alpha' => '/^[A-Za-z]+$/',
+ 'alphaNum' => '/^[A-Za-z0-9]+$/',
+ 'alphaDash' => '/^[A-Za-z0-9\-\_]+$/',
+ 'chs' => '/^[\x{4e00}-\x{9fa5}]+$/u',
+ 'chsAlpha' => '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u',
+ 'chsAlphaNum' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u',
+ 'chsDash' => '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u',
+ 'mobile' => '/^1[3-9]\d{9}$/',
+ 'idCard' => '/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$)/',
+ 'zip' => '/\d{6}/',
+ ];
+
+ /**
+ * Filter_var 规则
+ * @var array
+ */
+ protected $filter = [
+ 'email' => FILTER_VALIDATE_EMAIL,
+ 'ip' => [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6],
+ 'integer' => FILTER_VALIDATE_INT,
+ 'url' => FILTER_VALIDATE_URL,
+ 'macAddr' => FILTER_VALIDATE_MAC,
+ 'float' => FILTER_VALIDATE_FLOAT,
+ ];
+
+ /**
+ * 验证场景定义
+ * @var array
+ */
+ protected $scene = [];
+
+ /**
+ * 验证失败错误信息
+ * @var string|array
+ */
+ protected $error = [];
+
+ /**
+ * 是否批量验证
+ * @var bool
+ */
+ protected $batch = false;
+
+ /**
+ * 验证失败是否抛出异常
+ * @var bool
+ */
+ protected $failException = false;
+
+ /**
+ * 场景需要验证的规则
+ * @var array
+ */
+ protected $only = [];
+
+ /**
+ * 场景需要移除的验证规则
+ * @var array
+ */
+ protected $remove = [];
+
+ /**
+ * 场景需要追加的验证规则
+ * @var array
+ */
+ protected $append = [];
+
+ /**
+ * 验证正则定义
+ * @var array
+ */
+ protected $regex = [];
+
+ /**
+ * Db对象
+ * @var Db
+ */
+ protected $db;
+
+ /**
+ * 语言对象
+ * @var Lang
+ */
+ protected $lang;
+
+ /**
+ * 请求对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * @var Closure[]
+ */
+ protected static $maker = [];
+
+ /**
+ * 构造方法
+ * @access public
+ */
+ public function __construct()
+ {
+ if (!empty(static::$maker)) {
+ foreach (static::$maker as $maker) {
+ call_user_func($maker, $this);
+ }
+ }
+ }
+
+ /**
+ * 设置服务注入
+ * @access public
+ * @param Closure $maker
+ * @return void
+ */
+ public static function maker(Closure $maker)
+ {
+ static::$maker[] = $maker;
+ }
+
+ /**
+ * 设置Lang对象
+ * @access public
+ * @param Lang $lang Lang对象
+ * @return void
+ */
+ public function setLang(Lang $lang)
+ {
+ $this->lang = $lang;
+ }
+
+ /**
+ * 设置Db对象
+ * @access public
+ * @param Db $db Db对象
+ * @return void
+ */
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * 设置Request对象
+ * @access public
+ * @param Request $request Request对象
+ * @return void
+ */
+ public function setRequest(Request $request)
+ {
+ $this->request = $request;
+ }
+
+ /**
+ * 添加字段验证规则
+ * @access protected
+ * @param string|array $name 字段名称或者规则数组
+ * @param mixed $rule 验证规则或者字段描述信息
+ * @return $this
+ */
+ public function rule($name, $rule = '')
+ {
+ if (is_array($name)) {
+ $this->rule = $name + $this->rule;
+ if (is_array($rule)) {
+ $this->field = array_merge($this->field, $rule);
+ }
+ } else {
+ $this->rule[$name] = $rule;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 注册验证(类型)规则
+ * @access public
+ * @param string $type 验证规则类型
+ * @param callable $callback callback方法(或闭包)
+ * @param string $message 验证失败提示信息
+ * @return $this
+ */
+ public function extend(string $type, callable $callback = null, string $message = null)
+ {
+ $this->type[$type] = $callback;
+
+ if ($message) {
+ $this->typeMsg[$type] = $message;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置验证规则的默认提示信息
+ * @access public
+ * @param string|array $type 验证规则类型名称或者数组
+ * @param string $msg 验证提示信息
+ * @return void
+ */
+ public function setTypeMsg($type, string $msg = null): void
+ {
+ if (is_array($type)) {
+ $this->typeMsg = array_merge($this->typeMsg, $type);
+ } else {
+ $this->typeMsg[$type] = $msg;
+ }
+ }
+
+ /**
+ * 设置提示信息
+ * @access public
+ * @param array $message 错误信息
+ * @return Validate
+ */
+ public function message(array $message)
+ {
+ $this->message = array_merge($this->message, $message);
+
+ return $this;
+ }
+
+ /**
+ * 设置验证场景
+ * @access public
+ * @param string $name 场景名
+ * @return $this
+ */
+ public function scene(string $name)
+ {
+ // 设置当前场景
+ $this->currentScene = $name;
+
+ return $this;
+ }
+
+ /**
+ * 判断是否存在某个验证场景
+ * @access public
+ * @param string $name 场景名
+ * @return bool
+ */
+ public function hasScene(string $name): bool
+ {
+ return isset($this->scene[$name]) || method_exists($this, 'scene' . $name);
+ }
+
+ /**
+ * 设置批量验证
+ * @access public
+ * @param bool $batch 是否批量验证
+ * @return $this
+ */
+ public function batch(bool $batch = true)
+ {
+ $this->batch = $batch;
+
+ return $this;
+ }
+
+ /**
+ * 设置验证失败后是否抛出异常
+ * @access protected
+ * @param bool $fail 是否抛出异常
+ * @return $this
+ */
+ public function failException(bool $fail = true)
+ {
+ $this->failException = $fail;
+
+ return $this;
+ }
+
+ /**
+ * 指定需要验证的字段列表
+ * @access public
+ * @param array $fields 字段名
+ * @return $this
+ */
+ public function only(array $fields)
+ {
+ $this->only = $fields;
+
+ return $this;
+ }
+
+ /**
+ * 移除某个字段的验证规则
+ * @access public
+ * @param string|array $field 字段名
+ * @param mixed $rule 验证规则 true 移除所有规则
+ * @return $this
+ */
+ public function remove($field, $rule = null)
+ {
+ if (is_array($field)) {
+ foreach ($field as $key => $rule) {
+ if (is_int($key)) {
+ $this->remove($rule);
+ } else {
+ $this->remove($key, $rule);
+ }
+ }
+ } else {
+ if (is_string($rule)) {
+ $rule = explode('|', $rule);
+ }
+
+ $this->remove[$field] = $rule;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 追加某个字段的验证规则
+ * @access public
+ * @param string|array $field 字段名
+ * @param mixed $rule 验证规则
+ * @return $this
+ */
+ public function append($field, $rule = null)
+ {
+ if (is_array($field)) {
+ foreach ($field as $key => $rule) {
+ $this->append($key, $rule);
+ }
+ } else {
+ if (is_string($rule)) {
+ $rule = explode('|', $rule);
+ }
+
+ $this->append[$field] = $rule;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 数据自动验证
+ * @access public
+ * @param array $data 数据
+ * @param array $rules 验证规则
+ * @return bool
+ */
+ public function check(array $data, array $rules = []): bool
+ {
+ $this->error = [];
+
+ if ($this->currentScene) {
+ $this->getScene($this->currentScene);
+ }
+
+ if (empty($rules)) {
+ // 读取验证规则
+ $rules = $this->rule;
+ }
+
+ foreach ($this->append as $key => $rule) {
+ if (!isset($rules[$key])) {
+ $rules[$key] = $rule;
+ }
+ }
+
+ foreach ($rules as $key => $rule) {
+ // field => 'rule1|rule2...' field => ['rule1','rule2',...]
+ if (strpos($key, '|')) {
+ // 字段|描述 用于指定属性名称
+ [$key, $title] = explode('|', $key);
+ } else {
+ $title = $this->field[$key] ?? $key;
+ }
+
+ // 场景检测
+ if (!empty($this->only) && !in_array($key, $this->only)) {
+ continue;
+ }
+
+ // 获取数据 支持二维数组
+ $value = $this->getDataValue($data, $key);
+
+ // 字段验证
+ if ($rule instanceof Closure) {
+ $result = call_user_func_array($rule, [$value, $data]);
+ } elseif ($rule instanceof ValidateRule) {
+ // 验证因子
+ $result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg());
+ } else {
+ $result = $this->checkItem($key, $value, $rule, $data, $title);
+ }
+
+ if (true !== $result) {
+ // 没有返回true 则表示验证失败
+ if (!empty($this->batch)) {
+ // 批量验证
+ $this->error[$key] = $result;
+ } elseif ($this->failException) {
+ throw new ValidateException($result);
+ } else {
+ $this->error = $result;
+ return false;
+ }
+ }
+ }
+
+ if (!empty($this->error)) {
+ if ($this->failException) {
+ throw new ValidateException($this->error);
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 根据验证规则验证数据
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rules 验证规则
+ * @return bool
+ */
+ public function checkRule($value, $rules): bool
+ {
+ if ($rules instanceof Closure) {
+ return call_user_func_array($rules, [$value]);
+ } elseif ($rules instanceof ValidateRule) {
+ $rules = $rules->getRule();
+ } elseif (is_string($rules)) {
+ $rules = explode('|', $rules);
+ }
+
+ foreach ($rules as $key => $rule) {
+ if ($rule instanceof Closure) {
+ $result = call_user_func_array($rule, [$value]);
+ } else {
+ // 判断验证类型
+ [$type, $rule] = $this->getValidateType($key, $rule);
+
+ $callback = $this->type[$type] ?? [$this, $type];
+
+ $result = call_user_func_array($callback, [$value, $rule]);
+ }
+
+ if (true !== $result) {
+ if ($this->failException) {
+ throw new ValidateException($result);
+ }
+
+ return $result;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 验证单个字段规则
+ * @access protected
+ * @param string $field 字段名
+ * @param mixed $value 字段值
+ * @param mixed $rules 验证规则
+ * @param array $data 数据
+ * @param string $title 字段描述
+ * @param array $msg 提示信息
+ * @return mixed
+ */
+ protected function checkItem(string $field, $value, $rules, $data, string $title = '', array $msg = [])
+ {
+ if (isset($this->remove[$field]) && true === $this->remove[$field] && empty($this->append[$field])) {
+ // 字段已经移除 无需验证
+ return true;
+ }
+
+ // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...]
+ if (is_string($rules)) {
+ $rules = explode('|', $rules);
+ }
+
+ if (isset($this->append[$field])) {
+ // 追加额外的验证规则
+ $rules = array_unique(array_merge($rules, $this->append[$field]), SORT_REGULAR);
+ }
+
+ if (empty($rules)) {
+ return true;
+ }
+
+ $i = 0;
+ foreach ($rules as $key => $rule) {
+ if ($rule instanceof Closure) {
+ $result = call_user_func_array($rule, [$value, $data]);
+ $info = is_numeric($key) ? '' : $key;
+ } else {
+ // 判断验证类型
+ [$type, $rule, $info] = $this->getValidateType($key, $rule);
+
+ if (isset($this->append[$field]) && in_array($info, $this->append[$field])) {
+ } elseif (isset($this->remove[$field]) && in_array($info, $this->remove[$field])) {
+ // 规则已经移除
+ $i++;
+ continue;
+ }
+
+ if (isset($this->type[$type])) {
+ $result = call_user_func_array($this->type[$type], [$value, $rule, $data, $field, $title]);
+ } elseif ('must' == $info || 0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) {
+ $result = call_user_func_array([$this, $type], [$value, $rule, $data, $field, $title]);
+ } else {
+ $result = true;
+ }
+ }
+
+ if (false === $result) {
+ // 验证失败 返回错误信息
+ if (!empty($msg[$i])) {
+ $message = $msg[$i];
+ if (is_string($message) && strpos($message, '{%') === 0) {
+ $message = $this->lang->get(substr($message, 2, -1));
+ }
+ } else {
+ $message = $this->getRuleMsg($field, $title, $info, $rule);
+ }
+
+ return $message;
+ } elseif (true !== $result) {
+ // 返回自定义错误信息
+ if (is_string($result) && false !== strpos($result, ':')) {
+ $result = str_replace(':attribute', $title, $result);
+
+ if (strpos($result, ':rule') && is_scalar($rule)) {
+ $result = str_replace(':rule', (string) $rule, $result);
+ }
+ }
+
+ return $result;
+ }
+ $i++;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取当前验证类型及规则
+ * @access public
+ * @param mixed $key
+ * @param mixed $rule
+ * @return array
+ */
+ protected function getValidateType($key, $rule): array
+ {
+ // 判断验证类型
+ if (!is_numeric($key)) {
+ if (isset($this->alias[$key])) {
+ // 判断别名
+ $key = $this->alias[$key];
+ }
+ return [$key, $rule, $key];
+ }
+
+ if (strpos($rule, ':')) {
+ [$type, $rule] = explode(':', $rule, 2);
+ if (isset($this->alias[$type])) {
+ // 判断别名
+ $type = $this->alias[$type];
+ }
+ $info = $type;
+ } elseif (method_exists($this, $rule)) {
+ $type = $rule;
+ $info = $rule;
+ $rule = '';
+ } else {
+ $type = 'is';
+ $info = $rule;
+ }
+
+ return [$type, $rule, $info];
+ }
+
+ /**
+ * 验证是否和某个字段的值一致
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @param string $field 字段名
+ * @return bool
+ */
+ public function confirm($value, $rule, array $data = [], string $field = ''): bool
+ {
+ if ('' == $rule) {
+ if (strpos($field, '_confirm')) {
+ $rule = strstr($field, '_confirm', true);
+ } else {
+ $rule = $field . '_confirm';
+ }
+ }
+
+ return $this->getDataValue($data, $rule) === $value;
+ }
+
+ /**
+ * 验证是否和某个字段的值是否不同
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function different($value, $rule, array $data = []): bool
+ {
+ return $this->getDataValue($data, $rule) != $value;
+ }
+
+ /**
+ * 验证是否大于等于某个值
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function egt($value, $rule, array $data = []): bool
+ {
+ return $value >= $this->getDataValue($data, $rule);
+ }
+
+ /**
+ * 验证是否大于某个值
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function gt($value, $rule, array $data = []): bool
+ {
+ return $value > $this->getDataValue($data, $rule);
+ }
+
+ /**
+ * 验证是否小于等于某个值
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function elt($value, $rule, array $data = []): bool
+ {
+ return $value <= $this->getDataValue($data, $rule);
+ }
+
+ /**
+ * 验证是否小于某个值
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function lt($value, $rule, array $data = []): bool
+ {
+ return $value < $this->getDataValue($data, $rule);
+ }
+
+ /**
+ * 验证是否等于某个值
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function eq($value, $rule): bool
+ {
+ return $value == $rule;
+ }
+
+ /**
+ * 必须验证
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function must($value, $rule = null): bool
+ {
+ return !empty($value) || '0' == $value;
+ }
+
+ /**
+ * 验证字段值是否为有效格式
+ * @access public
+ * @param mixed $value 字段值
+ * @param string $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function is($value, string $rule, array $data = []): bool
+ {
+ switch (Str::camel($rule)) {
+ case 'require':
+ // 必须
+ $result = !empty($value) || '0' == $value;
+ break;
+ case 'accepted':
+ // 接受
+ $result = in_array($value, ['1', 'on', 'yes']);
+ break;
+ case 'date':
+ // 是否是一个有效日期
+ $result = false !== strtotime($value);
+ break;
+ case 'activeUrl':
+ // 是否为有效的网址
+ $result = checkdnsrr($value);
+ break;
+ case 'boolean':
+ case 'bool':
+ // 是否为布尔值
+ $result = in_array($value, [true, false, 0, 1, '0', '1'], true);
+ break;
+ case 'number':
+ $result = ctype_digit((string) $value);
+ break;
+ case 'alphaNum':
+ $result = ctype_alnum($value);
+ break;
+ case 'array':
+ // 是否为数组
+ $result = is_array($value);
+ break;
+ case 'file':
+ $result = $value instanceof File;
+ break;
+ case 'image':
+ $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]);
+ break;
+ case 'token':
+ $result = $this->token($value, '__token__', $data);
+ break;
+ default:
+ if (isset($this->type[$rule])) {
+ // 注册的验证规则
+ $result = call_user_func_array($this->type[$rule], [$value]);
+ } elseif (function_exists('ctype_' . $rule)) {
+ // ctype验证规则
+ $ctypeFun = 'ctype_' . $rule;
+ $result = $ctypeFun($value);
+ } elseif (isset($this->filter[$rule])) {
+ // Filter_var验证规则
+ $result = $this->filter($value, $this->filter[$rule]);
+ } else {
+ // 正则验证
+ $result = $this->regex($value, $rule);
+ }
+ }
+
+ return $result;
+ }
+
+ // 判断图像类型
+ protected function getImageType($image)
+ {
+ if (function_exists('exif_imagetype')) {
+ return exif_imagetype($image);
+ }
+
+ try {
+ $info = getimagesize($image);
+ return $info ? $info[2] : false;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 验证表单令牌
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function token($value, string $rule, array $data): bool
+ {
+ $rule = !empty($rule) ? $rule : '__token__';
+ return $this->request->checkToken($rule, $data);
+ }
+
+ /**
+ * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function activeUrl(string $value, string $rule = 'MX'): bool
+ {
+ if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) {
+ $rule = 'MX';
+ }
+
+ return checkdnsrr($value, $rule);
+ }
+
+ /**
+ * 验证是否有效IP
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则 ipv4 ipv6
+ * @return bool
+ */
+ public function ip($value, string $rule = 'ipv4'): bool
+ {
+ if (!in_array($rule, ['ipv4', 'ipv6'])) {
+ $rule = 'ipv4';
+ }
+
+ return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]);
+ }
+
+ /**
+ * 检测上传文件后缀
+ * @access public
+ * @param File $file
+ * @param array|string $ext 允许后缀
+ * @return bool
+ */
+ protected function checkExt(File $file, $ext): bool
+ {
+ if (is_string($ext)) {
+ $ext = explode(',', $ext);
+ }
+
+ return in_array(strtolower($file->extension()), $ext);
+ }
+
+ /**
+ * 检测上传文件大小
+ * @access public
+ * @param File $file
+ * @param integer $size 最大大小
+ * @return bool
+ */
+ protected function checkSize(File $file, $size): bool
+ {
+ return $file->getSize() <= (int) $size;
+ }
+
+ /**
+ * 检测上传文件类型
+ * @access public
+ * @param File $file
+ * @param array|string $mime 允许类型
+ * @return bool
+ */
+ protected function checkMime(File $file, $mime): bool
+ {
+ if (is_string($mime)) {
+ $mime = explode(',', $mime);
+ }
+
+ return in_array(strtolower($file->getMime()), $mime);
+ }
+
+ /**
+ * 验证上传文件后缀
+ * @access public
+ * @param mixed $file 上传文件
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function fileExt($file, $rule): bool
+ {
+ if (is_array($file)) {
+ foreach ($file as $item) {
+ if (!($item instanceof File) || !$this->checkExt($item, $rule)) {
+ return false;
+ }
+ }
+ return true;
+ } elseif ($file instanceof File) {
+ return $this->checkExt($file, $rule);
+ }
+
+ return false;
+ }
+
+ /**
+ * 验证上传文件类型
+ * @access public
+ * @param mixed $file 上传文件
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function fileMime($file, $rule): bool
+ {
+ if (is_array($file)) {
+ foreach ($file as $item) {
+ if (!($item instanceof File) || !$this->checkMime($item, $rule)) {
+ return false;
+ }
+ }
+ return true;
+ } elseif ($file instanceof File) {
+ return $this->checkMime($file, $rule);
+ }
+
+ return false;
+ }
+
+ /**
+ * 验证上传文件大小
+ * @access public
+ * @param mixed $file 上传文件
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function fileSize($file, $rule): bool
+ {
+ if (is_array($file)) {
+ foreach ($file as $item) {
+ if (!($item instanceof File) || !$this->checkSize($item, $rule)) {
+ return false;
+ }
+ }
+ return true;
+ } elseif ($file instanceof File) {
+ return $this->checkSize($file, $rule);
+ }
+
+ return false;
+ }
+
+ /**
+ * 验证图片的宽高及类型
+ * @access public
+ * @param mixed $file 上传文件
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function image($file, $rule): bool
+ {
+ if (!($file instanceof File)) {
+ return false;
+ }
+
+ if ($rule) {
+ $rule = explode(',', $rule);
+
+ [$width, $height, $type] = getimagesize($file->getRealPath());
+
+ if (isset($rule[2])) {
+ $imageType = strtolower($rule[2]);
+
+ if ('jpg' == $imageType) {
+ $imageType = 'jpeg';
+ }
+
+ if (image_type_to_extension($type, false) != $imageType) {
+ return false;
+ }
+ }
+
+ [$w, $h] = $rule;
+
+ return $w == $width && $h == $height;
+ }
+
+ return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]);
+ }
+
+ /**
+ * 验证时间和日期是否符合指定格式
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function dateFormat($value, $rule): bool
+ {
+ $info = date_parse_from_format($rule, $value);
+ return 0 == $info['warning_count'] && 0 == $info['error_count'];
+ }
+
+ /**
+ * 验证是否唯一
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名
+ * @param array $data 数据
+ * @param string $field 验证字段名
+ * @return bool
+ */
+ public function unique($value, $rule, array $data = [], string $field = ''): bool
+ {
+ if (is_string($rule)) {
+ $rule = explode(',', $rule);
+ }
+
+ if (false !== strpos($rule[0], '\\')) {
+ // 指定模型类
+ $db = new $rule[0];
+ } else {
+ $db = $this->db->name($rule[0]);
+ }
+
+ $key = $rule[1] ?? $field;
+ $map = [];
+
+ if (strpos($key, '^')) {
+ // 支持多个字段验证
+ $fields = explode('^', $key);
+ foreach ($fields as $key) {
+ if (isset($data[$key])) {
+ $map[] = [$key, '=', $data[$key]];
+ }
+ }
+ } elseif (isset($data[$field])) {
+ $map[] = [$key, '=', $data[$field]];
+ } else {
+ $map = [];
+ }
+
+ $pk = !empty($rule[3]) ? $rule[3] : $db->getPk();
+
+ if (is_string($pk)) {
+ if (isset($rule[2])) {
+ $map[] = [$pk, '<>', $rule[2]];
+ } elseif (isset($data[$pk])) {
+ $map[] = [$pk, '<>', $data[$pk]];
+ }
+ }
+
+ if ($db->where($map)->field($pk)->find()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 使用filter_var方式验证
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function filter($value, $rule): bool
+ {
+ if (is_string($rule) && strpos($rule, ',')) {
+ [$rule, $param] = explode(',', $rule);
+ } elseif (is_array($rule)) {
+ $param = $rule[1] ?? null;
+ $rule = $rule[0];
+ } else {
+ $param = null;
+ }
+
+ return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param);
+ }
+
+ /**
+ * 验证某个字段等于某个值的时候必须
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function requireIf($value, $rule, array $data = []): bool
+ {
+ [$field, $val] = explode(',', $rule);
+
+ if ($this->getDataValue($data, $field) == $val) {
+ return !empty($value) || '0' == $value;
+ }
+
+ return true;
+ }
+
+ /**
+ * 通过回调方法验证某个字段是否必须
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function requireCallback($value, $rule, array $data = []): bool
+ {
+ $result = call_user_func_array([$this, $rule], [$value, $data]);
+
+ if ($result) {
+ return !empty($value) || '0' == $value;
+ }
+
+ return true;
+ }
+
+ /**
+ * 验证某个字段有值的情况下必须
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function requireWith($value, $rule, array $data = []): bool
+ {
+ $val = $this->getDataValue($data, $rule);
+
+ if (!empty($val)) {
+ return !empty($value) || '0' == $value;
+ }
+
+ return true;
+ }
+
+ /**
+ * 验证某个字段没有值的情况下必须
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function requireWithout($value, $rule, array $data = []): bool
+ {
+ $val = $this->getDataValue($data, $rule);
+
+ if (empty($val)) {
+ return !empty($value) || '0' == $value;
+ }
+
+ return true;
+ }
+
+ /**
+ * 验证是否在范围内
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function in($value, $rule): bool
+ {
+ return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+ }
+
+ /**
+ * 验证是否不在某个范围
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function notIn($value, $rule): bool
+ {
+ return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+ }
+
+ /**
+ * between验证数据
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function between($value, $rule): bool
+ {
+ if (is_string($rule)) {
+ $rule = explode(',', $rule);
+ }
+ [$min, $max] = $rule;
+
+ return $value >= $min && $value <= $max;
+ }
+
+ /**
+ * 使用notbetween验证数据
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function notBetween($value, $rule): bool
+ {
+ if (is_string($rule)) {
+ $rule = explode(',', $rule);
+ }
+ [$min, $max] = $rule;
+
+ return $value < $min || $value > $max;
+ }
+
+ /**
+ * 验证数据长度
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function length($value, $rule): bool
+ {
+ if (is_array($value)) {
+ $length = count($value);
+ } elseif ($value instanceof File) {
+ $length = $value->getSize();
+ } else {
+ $length = mb_strlen((string) $value);
+ }
+
+ if (is_string($rule) && strpos($rule, ',')) {
+ // 长度区间
+ [$min, $max] = explode(',', $rule);
+ return $length >= $min && $length <= $max;
+ }
+
+ // 指定长度
+ return $length == $rule;
+ }
+
+ /**
+ * 验证数据最大长度
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function max($value, $rule): bool
+ {
+ if (is_array($value)) {
+ $length = count($value);
+ } elseif ($value instanceof File) {
+ $length = $value->getSize();
+ } else {
+ $length = mb_strlen((string) $value);
+ }
+
+ return $length <= $rule;
+ }
+
+ /**
+ * 验证数据最小长度
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function min($value, $rule): bool
+ {
+ if (is_array($value)) {
+ $length = count($value);
+ } elseif ($value instanceof File) {
+ $length = $value->getSize();
+ } else {
+ $length = mb_strlen((string) $value);
+ }
+
+ return $length >= $rule;
+ }
+
+ /**
+ * 验证日期
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function after($value, $rule, array $data = []): bool
+ {
+ return strtotime($value) >= strtotime($rule);
+ }
+
+ /**
+ * 验证日期
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function before($value, $rule, array $data = []): bool
+ {
+ return strtotime($value) <= strtotime($rule);
+ }
+
+ /**
+ * 验证日期
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function afterWith($value, $rule, array $data = []): bool
+ {
+ $rule = $this->getDataValue($data, $rule);
+ return !is_null($rule) && strtotime($value) >= strtotime($rule);
+ }
+
+ /**
+ * 验证日期
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @param array $data 数据
+ * @return bool
+ */
+ public function beforeWith($value, $rule, array $data = []): bool
+ {
+ $rule = $this->getDataValue($data, $rule);
+ return !is_null($rule) && strtotime($value) <= strtotime($rule);
+ }
+
+ /**
+ * 验证有效期
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function expire($value, $rule): bool
+ {
+ if (is_string($rule)) {
+ $rule = explode(',', $rule);
+ }
+
+ [$start, $end] = $rule;
+
+ if (!is_numeric($start)) {
+ $start = strtotime($start);
+ }
+
+ if (!is_numeric($end)) {
+ $end = strtotime($end);
+ }
+
+ return time() >= $start && time() <= $end;
+ }
+
+ /**
+ * 验证IP许可
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function allowIp($value, $rule): bool
+ {
+ return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+ }
+
+ /**
+ * 验证IP禁用
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则
+ * @return bool
+ */
+ public function denyIp($value, $rule): bool
+ {
+ return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+ }
+
+ /**
+ * 使用正则验证数据
+ * @access public
+ * @param mixed $value 字段值
+ * @param mixed $rule 验证规则 正则规则或者预定义正则名
+ * @return bool
+ */
+ public function regex($value, $rule): bool
+ {
+ if (isset($this->regex[$rule])) {
+ $rule = $this->regex[$rule];
+ } elseif (isset($this->defaultRegex[$rule])) {
+ $rule = $this->defaultRegex[$rule];
+ }
+
+ if (is_string($rule) && 0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) {
+ // 不是正则表达式则两端补上/
+ $rule = '/^' . $rule . '$/';
+ }
+
+ return is_scalar($value) && 1 === preg_match($rule, (string) $value);
+ }
+
+ /**
+ * 获取错误信息
+ * @return array|string
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * 获取数据值
+ * @access protected
+ * @param array $data 数据
+ * @param string $key 数据标识 支持二维
+ * @return mixed
+ */
+ protected function getDataValue(array $data, $key)
+ {
+ if (is_numeric($key)) {
+ $value = $key;
+ } elseif (is_string($key) && strpos($key, '.')) {
+ // 支持多维数组验证
+ foreach (explode('.', $key) as $key) {
+ if (!isset($data[$key])) {
+ $value = null;
+ break;
+ }
+ $value = $data = $data[$key];
+ }
+ } else {
+ $value = $data[$key] ?? null;
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取验证规则的错误提示信息
+ * @access protected
+ * @param string $attribute 字段英文名
+ * @param string $title 字段描述名
+ * @param string $type 验证规则名称
+ * @param mixed $rule 验证规则数据
+ * @return string|array
+ */
+ protected function getRuleMsg(string $attribute, string $title, string $type, $rule)
+ {
+ if (isset($this->message[$attribute . '.' . $type])) {
+ $msg = $this->message[$attribute . '.' . $type];
+ } elseif (isset($this->message[$attribute][$type])) {
+ $msg = $this->message[$attribute][$type];
+ } elseif (isset($this->message[$attribute])) {
+ $msg = $this->message[$attribute];
+ } elseif (isset($this->typeMsg[$type])) {
+ $msg = $this->typeMsg[$type];
+ } elseif (0 === strpos($type, 'require')) {
+ $msg = $this->typeMsg['require'];
+ } else {
+ $msg = $title . $this->lang->get('not conform to the rules');
+ }
+
+ if (is_array($msg)) {
+ return $this->errorMsgIsArray($msg, $rule, $title);
+ }
+
+ return $this->parseErrorMsg($msg, $rule, $title);
+ }
+
+ /**
+ * 获取验证规则的错误提示信息
+ * @access protected
+ * @param string $msg 错误信息
+ * @param mixed $rule 验证规则数据
+ * @param string $title 字段描述名
+ * @return string
+ */
+ protected function parseErrorMsg(string $msg, $rule, string $title)
+ {
+ if (0 === strpos($msg, '{%')) {
+ $msg = $this->lang->get(substr($msg, 2, -1));
+ } elseif ($this->lang->has($msg)) {
+ $msg = $this->lang->get($msg);
+ }
+
+ if (is_array($msg)) {
+ return $this->errorMsgIsArray($msg, $rule, $title);
+ }
+
+ if (is_scalar($rule) && false !== strpos($msg, ':')) {
+ // 变量替换
+ if (is_string($rule) && strpos($rule, ',')) {
+ $array = array_pad(explode(',', $rule), 3, '');
+ } else {
+ $array = array_pad([], 3, '');
+ }
+
+ $msg = str_replace(
+ [':attribute', ':1', ':2', ':3'],
+ [$title, $array[0], $array[1], $array[2]],
+ $msg
+ );
+
+ if (strpos($msg, ':rule')) {
+ $msg = str_replace(':rule', (string) $rule, $msg);
+ }
+ }
+
+ return $msg;
+ }
+
+ /**
+ * 错误信息数组处理
+ * @access protected
+ * @param array $msg 错误信息
+ * @param mixed $rule 验证规则数据
+ * @param string $title 字段描述名
+ * @return array
+ */
+ protected function errorMsgIsArray(array $msg, $rule, string $title)
+ {
+ foreach ($msg as $key => $val) {
+ if (is_string($val)) {
+ $msg[$key] = $this->parseErrorMsg($val, $rule, $title);
+ }
+ }
+ return $msg;
+ }
+
+ /**
+ * 获取数据验证的场景
+ * @access protected
+ * @param string $scene 验证场景
+ * @return void
+ */
+ protected function getScene(string $scene): void
+ {
+ $this->only = $this->append = $this->remove = [];
+
+ if (method_exists($this, 'scene' . $scene)) {
+ call_user_func([$this, 'scene' . $scene]);
+ } elseif (isset($this->scene[$scene])) {
+ // 如果设置了验证适用场景
+ $this->only = $this->scene[$scene];
+ }
+ }
+
+ /**
+ * 动态方法 直接调用is方法进行验证
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return bool
+ */
+ public function __call($method, $args)
+ {
+ if ('is' == strtolower(substr($method, 0, 2))) {
+ $method = substr($method, 2);
+ }
+
+ array_push($args, lcfirst($method));
+
+ return call_user_func_array([$this, 'is'], $args);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/View.php b/vendor/topthink/framework/src/think/View.php
new file mode 100644
index 0000000..ea35896
--- /dev/null
+++ b/vendor/topthink/framework/src/think/View.php
@@ -0,0 +1,187 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use think\helper\Arr;
+
+/**
+ * 视图类
+ * @package think
+ */
+class View extends Manager
+{
+
+ protected $namespace = '\\think\\view\\driver\\';
+
+ /**
+ * 模板变量
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 内容过滤
+ * @var mixed
+ */
+ protected $filter;
+
+ /**
+ * 获取模板引擎
+ * @access public
+ * @param string $type 模板引擎类型
+ * @return $this
+ */
+ public function engine(string $type = null)
+ {
+ return $this->driver($type);
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string|array $name 模板变量
+ * @param mixed $value 变量值
+ * @return $this
+ */
+ public function assign($name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->data = array_merge($this->data, $name);
+ } else {
+ $this->data[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图过滤
+ * @access public
+ * @param Callable $filter 过滤方法或闭包
+ * @return $this
+ */
+ public function filter(callable $filter = null)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * 解析和获取模板内容 用于输出
+ * @access public
+ * @param string $template 模板文件名或者内容
+ * @param array $vars 模板变量
+ * @return string
+ * @throws \Exception
+ */
+ public function fetch(string $template = '', array $vars = []): string
+ {
+ return $this->getContent(function () use ($vars, $template) {
+ $this->engine()->fetch($template, array_merge($this->data, $vars));
+ });
+ }
+
+ /**
+ * 渲染内容输出
+ * @access public
+ * @param string $content 内容
+ * @param array $vars 模板变量
+ * @return string
+ */
+ public function display(string $content, array $vars = []): string
+ {
+ return $this->getContent(function () use ($vars, $content) {
+ $this->engine()->display($content, array_merge($this->data, $vars));
+ });
+ }
+
+ /**
+ * 获取模板引擎渲染内容
+ * @param $callback
+ * @return string
+ * @throws \Exception
+ */
+ protected function getContent($callback): string
+ {
+ // 页面缓存
+ ob_start();
+ ob_implicit_flush(0);
+
+ // 渲染输出
+ try {
+ $callback();
+ } catch (\Exception $e) {
+ ob_end_clean();
+ throw $e;
+ }
+
+ // 获取并清空缓存
+ $content = ob_get_clean();
+
+ if ($this->filter) {
+ $content = call_user_func_array($this->filter, [$content]);
+ }
+
+ return $content;
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string $name 变量名
+ * @param mixed $value 变量值
+ */
+ public function __set($name, $value)
+ {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * 取得模板显示变量的值
+ * @access protected
+ * @param string $name 模板变量
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->data[$name];
+ }
+
+ /**
+ * 检测模板变量是否设置
+ * @access public
+ * @param string $name 模板变量名
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->data[$name]);
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ $config = $this->app->config->get('view', []);
+ Arr::forget($config, 'type');
+ return $config;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver()
+ {
+ return $this->app->config->get('view.type', 'php');
+ }
+
+}
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..1cd8151
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/Driver.php
@@ -0,0 +1,347 @@
+
+// +----------------------------------------------------------------------
+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);
+ }
+
+ /**
+ * 如果不存在则写入缓存
+ * @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)) {
+ return $this->get($name);
+ }
+
+ $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])) {
+ $name = array_map(function ($val) {
+ return $this->getTagKey($val);
+ }, $name);
+ $this->tag[$key] = new TagSet($name, $this);
+ }
+
+ return $this->tag[$key];
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 标签标识
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ return $this->get($tag, []);
+ }
+
+ /**
+ * 获取实际标签名
+ * @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(string $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/TagSet.php b/vendor/topthink/framework/src/think/cache/TagSet.php
new file mode 100644
index 0000000..17f6607
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/TagSet.php
@@ -0,0 +1,130 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache;
+
+/**
+ * 标签集合
+ */
+class TagSet
+{
+ /**
+ * 标签的缓存Key
+ * @var array
+ */
+ protected $tag;
+
+ /**
+ * 缓存句柄
+ * @var Driver
+ */
+ protected $handler;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $tag 缓存标签
+ * @param Driver $cache 缓存对象
+ */
+ public function __construct(array $tag, Driver $cache)
+ {
+ $this->tag = $tag;
+ $this->handler = $cache;
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|\DateTime $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set(string $name, $value, $expire = null): bool
+ {
+ $this->handler->set($name, $value, $expire);
+
+ $this->append($name);
+
+ return true;
+ }
+
+ /**
+ * 追加缓存标识到标签
+ * @access public
+ * @param string $name 缓存变量名
+ * @return void
+ */
+ public function append(string $name): void
+ {
+ $name = $this->handler->getCacheKey($name);
+
+ foreach ($this->tag as $tag) {
+ $this->handler->push($tag, $name);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @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 string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int $expire 有效时间 0为永久
+ * @return mixed
+ */
+ public function remember(string $name, $value, $expire = null)
+ {
+ $result = $this->handler->remember($name, $value, $expire);
+
+ $this->append($name);
+
+ return $result;
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ // 指定标签清除
+ foreach ($this->tag as $tag) {
+ $names = $this->handler->getTagItems($tag);
+
+ $this->handler->clearTag($names);
+ $this->handler->delete($tag);
+ }
+
+ return true;
+ }
+}
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..7a3d60f
--- /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 ['content' => $content, 'expire' => $expire];
+ }
+ }
+
+ /**
+ * 判断缓存是否存在
+ * @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..bfbc5a2
--- /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..3304937
--- /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/cache/driver/Redis.php b/vendor/topthink/framework/src/think/cache/driver/Redis.php
new file mode 100644
index 0000000..3fb33a2
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/Redis.php
@@ -0,0 +1,248 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好
+ * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动
+ *
+ * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis
+ * @author 尘缘 <130775@qq.com>
+ */
+class Redis extends Driver
+{
+ /** @var \Predis\Client|\Redis */
+ protected $handler;
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'password' => '',
+ 'select' => 0,
+ 'timeout' => 0,
+ 'expire' => 0,
+ 'persistent' => false,
+ 'prefix' => '',
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ */
+ public function __construct(array $options = [])
+ {
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ if (extension_loaded('redis')) {
+ $this->handler = new \Redis;
+
+ if ($this->options['persistent']) {
+ $this->handler->pconnect($this->options['host'], (int) $this->options['port'], (int) $this->options['timeout'], 'persistent_id_' . $this->options['select']);
+ } else {
+ $this->handler->connect($this->options['host'], (int) $this->options['port'], (int) $this->options['timeout']);
+ }
+
+ if ('' != $this->options['password']) {
+ $this->handler->auth($this->options['password']);
+ }
+ } elseif (class_exists('\Predis\Client')) {
+ $params = [];
+ foreach ($this->options as $key => $val) {
+ if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication', 'parameters'])) {
+ $params[$key] = $val;
+ unset($this->options[$key]);
+ }
+ }
+
+ if ('' == $this->options['password']) {
+ unset($this->options['password']);
+ }
+
+ $this->handler = new \Predis\Client($this->options, $params);
+
+ $this->options['prefix'] = '';
+ } else {
+ throw new \BadFunctionCallException('not support: redis');
+ }
+
+ if (0 != $this->options['select']) {
+ $this->handler->select($this->options['select']);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ return $this->handler->exists($this->getCacheKey($name)) ? true : false;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null)
+ {
+ $this->readTimes++;
+
+ $value = $this->handler->get($this->getCacheKey($name));
+
+ if (false === $value || is_null($value)) {
+ return $default;
+ }
+
+ return $this->unserialize($value);
+ }
+
+ /**
+ * 写入缓存
+ * @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 ($expire) {
+ $this->handler->setex($key, $expire, $value);
+ } else {
+ $this->handler->set($key, $value);
+ }
+
+ return true;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @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);
+
+ return $this->handler->incrby($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);
+
+ return $this->handler->decrby($key, $step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ $this->writeTimes++;
+
+ $result = $this->handler->del($this->getCacheKey($name));
+ return $result > 0;
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->writeTimes++;
+
+ $this->handler->flushDB();
+ return true;
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys): void
+ {
+ // 指定标签清除
+ $this->handler->del($keys);
+ }
+
+ /**
+ * 追加(数组)缓存数据
+ * @access public
+ * @param string $name 缓存标识
+ * @param mixed $value 数据
+ * @return void
+ */
+ public function push(string $name, $value): void
+ {
+ $this->handler->sAdd($name, $value);
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 缓存标签
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ return $this->handler->sMembers($tag);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/cache/driver/Wincache.php b/vendor/topthink/framework/src/think/cache/driver/Wincache.php
new file mode 100644
index 0000000..c71ae2d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/Wincache.php
@@ -0,0 +1,175 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Wincache缓存驱动
+ */
+class Wincache extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'prefix' => '',
+ 'expire' => 0,
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ * @throws \BadFunctionCallException
+ */
+ public function __construct(array $options = [])
+ {
+ if (!function_exists('wincache_ucache_info')) {
+ throw new \BadFunctionCallException('not support: WinCache');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $this->readTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ return wincache_ucache_exists($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null)
+ {
+ $this->readTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ return wincache_ucache_exists($key) ? $this->unserialize(wincache_ucache_get($key)) : $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 (wincache_ucache_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);
+
+ return wincache_ucache_inc($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);
+
+ return wincache_ucache_dec($key, $step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ $this->writeTimes++;
+
+ return wincache_ucache_delete($this->getCacheKey($name));
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->writeTimes++;
+ return wincache_ucache_clear();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys): void
+ {
+ wincache_ucache_delete($keys);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/Command.php b/vendor/topthink/framework/src/think/console/Command.php
new file mode 100644
index 0000000..1bbcf48
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/Command.php
@@ -0,0 +1,504 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use Exception;
+use InvalidArgumentException;
+use LogicException;
+use think\App;
+use think\Console;
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+abstract class Command
+{
+
+ /** @var Console */
+ private $console;
+ private $name;
+ private $processTitle;
+ private $aliases = [];
+ private $definition;
+ private $help;
+ private $description;
+ private $ignoreValidationErrors = false;
+ private $consoleDefinitionMerged = false;
+ private $consoleDefinitionMergedWithArgs = false;
+ private $synopsis = [];
+ private $usages = [];
+
+ /** @var Input */
+ protected $input;
+
+ /** @var Output */
+ protected $output;
+
+ /** @var App */
+ protected $app;
+
+ /**
+ * 构造方法
+ * @throws LogicException
+ * @api
+ */
+ public function __construct()
+ {
+ $this->definition = new Definition();
+
+ $this->configure();
+
+ if (!$this->name) {
+ throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
+ }
+ }
+
+ /**
+ * 忽略验证错误
+ */
+ public function ignoreValidationErrors(): void
+ {
+ $this->ignoreValidationErrors = true;
+ }
+
+ /**
+ * 设置控制台
+ * @param Console $console
+ */
+ public function setConsole(Console $console = null): void
+ {
+ $this->console = $console;
+ }
+
+ /**
+ * 获取控制台
+ * @return Console
+ * @api
+ */
+ public function getConsole(): Console
+ {
+ return $this->console;
+ }
+
+ /**
+ * 设置app
+ * @param App $app
+ */
+ public function setApp(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 获取app
+ * @return App
+ */
+ public function getApp()
+ {
+ return $this->app;
+ }
+
+ /**
+ * 是否有效
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return true;
+ }
+
+ /**
+ * 配置指令
+ */
+ protected function configure()
+ {
+ }
+
+ /**
+ * 执行指令
+ * @param Input $input
+ * @param Output $output
+ * @return null|int
+ * @throws LogicException
+ * @see setCode()
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ return $this->app->invoke([$this, 'handle']);
+ }
+
+ /**
+ * 用户验证
+ * @param Input $input
+ * @param Output $output
+ */
+ protected function interact(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 初始化
+ * @param Input $input An InputInterface instance
+ * @param Output $output An OutputInterface instance
+ */
+ protected function initialize(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 执行
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ * @throws Exception
+ * @see setCode()
+ * @see execute()
+ */
+ public function run(Input $input, Output $output): int
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ $this->getSynopsis(true);
+ $this->getSynopsis(false);
+
+ $this->mergeConsoleDefinition();
+
+ try {
+ $input->bind($this->definition);
+ } catch (Exception $e) {
+ if (!$this->ignoreValidationErrors) {
+ throw $e;
+ }
+ }
+
+ $this->initialize($input, $output);
+
+ if (null !== $this->processTitle) {
+ if (function_exists('cli_set_process_title')) {
+ if (false === @cli_set_process_title($this->processTitle)) {
+ if ('Darwin' === PHP_OS) {
+ $output->writeln('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS. ');
+ } else {
+ $error = error_get_last();
+ trigger_error($error['message'], E_USER_WARNING);
+ }
+ }
+ } elseif (function_exists('setproctitle')) {
+ setproctitle($this->processTitle);
+ } elseif (Output::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
+ $output->writeln('Install the proctitle PECL to be able to change the process title. ');
+ }
+ }
+
+ if ($input->isInteractive()) {
+ $this->interact($input, $output);
+ }
+
+ $input->validate();
+
+ $statusCode = $this->execute($input, $output);
+
+ return is_numeric($statusCode) ? (int) $statusCode : 0;
+ }
+
+ /**
+ * 合并参数定义
+ * @param bool $mergeArgs
+ */
+ public function mergeConsoleDefinition(bool $mergeArgs = true)
+ {
+ if (null === $this->console
+ || (true === $this->consoleDefinitionMerged
+ && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs))
+ ) {
+ return;
+ }
+
+ if ($mergeArgs) {
+ $currentArguments = $this->definition->getArguments();
+ $this->definition->setArguments($this->console->getDefinition()->getArguments());
+ $this->definition->addArguments($currentArguments);
+ }
+
+ $this->definition->addOptions($this->console->getDefinition()->getOptions());
+
+ $this->consoleDefinitionMerged = true;
+ if ($mergeArgs) {
+ $this->consoleDefinitionMergedWithArgs = true;
+ }
+ }
+
+ /**
+ * 设置参数定义
+ * @param array|Definition $definition
+ * @return Command
+ * @api
+ */
+ public function setDefinition($definition)
+ {
+ if ($definition instanceof Definition) {
+ $this->definition = $definition;
+ } else {
+ $this->definition->setDefinition($definition);
+ }
+
+ $this->consoleDefinitionMerged = false;
+
+ return $this;
+ }
+
+ /**
+ * 获取参数定义
+ * @return Definition
+ * @api
+ */
+ public function getDefinition(): Definition
+ {
+ return $this->definition;
+ }
+
+ /**
+ * 获取当前指令的参数定义
+ * @return Definition
+ */
+ public function getNativeDefinition(): Definition
+ {
+ return $this->getDefinition();
+ }
+
+ /**
+ * 添加参数
+ * @param string $name 名称
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addArgument(string $name, int $mode = null, string $description = '', $default = null)
+ {
+ $this->definition->addArgument(new Argument($name, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 添加选项
+ * @param string $name 选项名称
+ * @param string $shortcut 别名
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addOption(string $name, string $shortcut = null, int $mode = null, string $description = '', $default = null)
+ {
+ $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 设置指令名称
+ * @param string $name
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function setName(string $name)
+ {
+ $this->validateName($name);
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 设置进程名称
+ *
+ * PHP 5.5+ or the proctitle PECL library is required
+ *
+ * @param string $title The process title
+ *
+ * @return $this
+ */
+ public function setProcessTitle($title)
+ {
+ $this->processTitle = $title;
+
+ return $this;
+ }
+
+ /**
+ * 获取指令名称
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 设置描述
+ * @param string $description
+ * @return Command
+ */
+ public function setDescription(string $description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description ?: '';
+ }
+
+ /**
+ * 设置帮助信息
+ * @param string $help
+ * @return Command
+ */
+ public function setHelp(string $help)
+ {
+ $this->help = $help;
+
+ return $this;
+ }
+
+ /**
+ * 获取帮助信息
+ * @return string
+ */
+ public function getHelp(): string
+ {
+ return $this->help ?: '';
+ }
+
+ /**
+ * 描述信息
+ * @return string
+ */
+ public function getProcessedHelp(): string
+ {
+ $name = $this->name;
+
+ $placeholders = [
+ '%command.name%',
+ '%command.full_name%',
+ ];
+ $replacements = [
+ $name,
+ $_SERVER['PHP_SELF'] . ' ' . $name,
+ ];
+
+ return str_replace($placeholders, $replacements, $this->getHelp());
+ }
+
+ /**
+ * 设置别名
+ * @param string[] $aliases
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function setAliases(iterable $aliases)
+ {
+ foreach ($aliases as $alias) {
+ $this->validateName($alias);
+ }
+
+ $this->aliases = $aliases;
+
+ return $this;
+ }
+
+ /**
+ * 获取别名
+ * @return array
+ */
+ public function getAliases(): array
+ {
+ return $this->aliases;
+ }
+
+ /**
+ * 获取简介
+ * @param bool $short 是否简单的
+ * @return string
+ */
+ public function getSynopsis(bool $short = false): string
+ {
+ $key = $short ? 'short' : 'long';
+
+ if (!isset($this->synopsis[$key])) {
+ $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
+ }
+
+ return $this->synopsis[$key];
+ }
+
+ /**
+ * 添加用法介绍
+ * @param string $usage
+ * @return $this
+ */
+ public function addUsage(string $usage)
+ {
+ if (0 !== strpos($usage, $this->name)) {
+ $usage = sprintf('%s %s', $this->name, $usage);
+ }
+
+ $this->usages[] = $usage;
+
+ return $this;
+ }
+
+ /**
+ * 获取用法介绍
+ * @return array
+ */
+ public function getUsages(): array
+ {
+ return $this->usages;
+ }
+
+ /**
+ * 验证指令名称
+ * @param string $name
+ * @throws InvalidArgumentException
+ */
+ private function validateName(string $name)
+ {
+ if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
+ throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+ }
+ }
+
+ /**
+ * 输出表格
+ * @param Table $table
+ * @return string
+ */
+ protected function table(Table $table): string
+ {
+ $content = $table->render();
+ $this->output->writeln($content);
+ return $content;
+ }
+
+}
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..343c0fa
--- /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..a8de861
--- /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..9d0379a
--- /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}$style>");
+ }
+
+ /**
+ * 输出空行
+ * @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/Table.php b/vendor/topthink/framework/src/think/console/Table.php
new file mode 100644
index 0000000..f79d075
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/Table.php
@@ -0,0 +1,300 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+class Table
+{
+ const ALIGN_LEFT = 1;
+ const ALIGN_RIGHT = 0;
+ const ALIGN_CENTER = 2;
+
+ /**
+ * 头信息数据
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 头部对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @var int
+ */
+ protected $headerAlign = 1;
+
+ /**
+ * 表格数据(二维数组)
+ * @var array
+ */
+ protected $rows = [];
+
+ /**
+ * 单元格对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @var int
+ */
+ protected $cellAlign = 1;
+
+ /**
+ * 单元格宽度信息
+ * @var array
+ */
+ protected $colWidth = [];
+
+ /**
+ * 表格输出样式
+ * @var string
+ */
+ protected $style = 'default';
+
+ /**
+ * 表格样式定义
+ * @var array
+ */
+ protected $format = [
+ 'compact' => [],
+ 'default' => [
+ 'top' => ['+', '-', '+', '+'],
+ 'cell' => ['|', ' ', '|', '|'],
+ 'middle' => ['+', '-', '+', '+'],
+ 'bottom' => ['+', '-', '+', '+'],
+ 'cross-top' => ['+', '-', '-', '+'],
+ 'cross-bottom' => ['+', '-', '-', '+'],
+ ],
+ 'markdown' => [
+ 'top' => [' ', ' ', ' ', ' '],
+ 'cell' => ['|', ' ', '|', '|'],
+ 'middle' => ['|', '-', '|', '|'],
+ 'bottom' => [' ', ' ', ' ', ' '],
+ 'cross-top' => ['|', ' ', ' ', '|'],
+ 'cross-bottom' => ['|', ' ', ' ', '|'],
+ ],
+ 'borderless' => [
+ 'top' => ['=', '=', ' ', '='],
+ 'cell' => [' ', ' ', ' ', ' '],
+ 'middle' => ['=', '=', ' ', '='],
+ 'bottom' => ['=', '=', ' ', '='],
+ 'cross-top' => ['=', '=', ' ', '='],
+ 'cross-bottom' => ['=', '=', ' ', '='],
+ ],
+ 'box' => [
+ 'top' => ['┌', '─', '┬', '┐'],
+ 'cell' => ['│', ' ', '│', '│'],
+ 'middle' => ['├', '─', '┼', '┤'],
+ 'bottom' => ['└', '─', '┴', '┘'],
+ 'cross-top' => ['├', '─', '┴', '┤'],
+ 'cross-bottom' => ['├', '─', '┬', '┤'],
+ ],
+ 'box-double' => [
+ 'top' => ['╔', '═', '╤', '╗'],
+ 'cell' => ['║', ' ', '│', '║'],
+ 'middle' => ['╠', '─', '╪', '╣'],
+ 'bottom' => ['╚', '═', '╧', '╝'],
+ 'cross-top' => ['╠', '═', '╧', '╣'],
+ 'cross-bottom' => ['╠', '═', '╤', '╣'],
+ ],
+ ];
+
+ /**
+ * 设置表格头信息 以及对齐方式
+ * @access public
+ * @param array $header 要输出的Header信息
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return void
+ */
+ public function setHeader(array $header, int $align = 1): void
+ {
+ $this->header = $header;
+ $this->headerAlign = $align;
+
+ $this->checkColWidth($header);
+ }
+
+ /**
+ * 设置输出表格数据 及对齐方式
+ * @access public
+ * @param array $rows 要输出的表格数据(二维数组)
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return void
+ */
+ public function setRows(array $rows, int $align = 1): void
+ {
+ $this->rows = $rows;
+ $this->cellAlign = $align;
+
+ foreach ($rows as $row) {
+ $this->checkColWidth($row);
+ }
+ }
+
+ /**
+ * 设置全局单元格对齐方式
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return $this
+ */
+ public function setCellAlign(int $align = 1)
+ {
+ $this->cellAlign = $align;
+ return $this;
+ }
+
+ /**
+ * 检查列数据的显示宽度
+ * @access public
+ * @param mixed $row 行数据
+ * @return void
+ */
+ protected function checkColWidth($row): void
+ {
+ if (is_array($row)) {
+ foreach ($row as $key => $cell) {
+ $width = mb_strwidth((string) $cell);
+ if (!isset($this->colWidth[$key]) || $width > $this->colWidth[$key]) {
+ $this->colWidth[$key] = $width;
+ }
+ }
+ }
+ }
+
+ /**
+ * 增加一行表格数据
+ * @access public
+ * @param mixed $row 行数据
+ * @param bool $first 是否在开头插入
+ * @return void
+ */
+ public function addRow($row, bool $first = false): void
+ {
+ if ($first) {
+ array_unshift($this->rows, $row);
+ } else {
+ $this->rows[] = $row;
+ }
+
+ $this->checkColWidth($row);
+ }
+
+ /**
+ * 设置输出表格的样式
+ * @access public
+ * @param string $style 样式名
+ * @return void
+ */
+ public function setStyle(string $style): void
+ {
+ $this->style = isset($this->format[$style]) ? $style : 'default';
+ }
+
+ /**
+ * 输出分隔行
+ * @access public
+ * @param string $pos 位置
+ * @return string
+ */
+ protected function renderSeparator(string $pos): string
+ {
+ $style = $this->getStyle($pos);
+ $array = [];
+
+ foreach ($this->colWidth as $width) {
+ $array[] = str_repeat($style[1], $width + 2);
+ }
+
+ return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL;
+ }
+
+ /**
+ * 输出表格头部
+ * @access public
+ * @return string
+ */
+ protected function renderHeader(): string
+ {
+ $style = $this->getStyle('cell');
+ $content = $this->renderSeparator('top');
+
+ foreach ($this->header as $key => $header) {
+ $array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign);
+ }
+
+ if (!empty($array)) {
+ $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
+
+ if (!empty($this->rows)) {
+ $content .= $this->renderSeparator('middle');
+ }
+ }
+
+ return $content;
+ }
+
+ protected function getStyle(string $style): array
+ {
+ if ($this->format[$this->style]) {
+ $style = $this->format[$this->style][$style];
+ } else {
+ $style = [' ', ' ', ' ', ' '];
+ }
+
+ return $style;
+ }
+
+ /**
+ * 输出表格
+ * @access public
+ * @param array $dataList 表格数据
+ * @return string
+ */
+ public function render(array $dataList = []): string
+ {
+ if (!empty($dataList)) {
+ $this->setRows($dataList);
+ }
+
+ // 输出头部
+ $content = $this->renderHeader();
+ $style = $this->getStyle('cell');
+
+ if (!empty($this->rows)) {
+ foreach ($this->rows as $row) {
+ if (is_string($row) && '-' === $row) {
+ $content .= $this->renderSeparator('middle');
+ } elseif (is_scalar($row)) {
+ $content .= $this->renderSeparator('cross-top');
+ $width = 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) {
+ return $a + $b;
+ });
+ $array = str_pad($row, $width);
+
+ $content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL;
+ $content .= $this->renderSeparator('cross-bottom');
+ } else {
+ $array = [];
+
+ foreach ($row as $key => $val) {
+ $width = $this->colWidth[$key];
+ // form https://github.com/symfony/console/blob/20c9821c8d1c2189f287dcee709b2f86353ea08f/Helper/Table.php#L467
+ // str_pad won't work properly with multi-byte strings, we need to fix the padding
+ if (false !== $encoding = mb_detect_encoding((string) $val, null, true)) {
+ $width += strlen((string) $val) - mb_strwidth((string) $val, $encoding);
+ }
+ $array[] = ' ' . str_pad((string) $val, $width, ' ', $this->cellAlign);
+ }
+
+ $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
+ }
+ }
+ }
+
+ $content .= $this->renderSeparator('bottom');
+
+ return $content;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/bin/README.md b/vendor/topthink/framework/src/think/console/bin/README.md
new file mode 100644
index 0000000..c515914
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/bin/README.md
@@ -0,0 +1 @@
+console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。
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/Clear.php b/vendor/topthink/framework/src/think/console/command/Clear.php
new file mode 100644
index 0000000..c584f0e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Clear.php
@@ -0,0 +1,85 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class Clear extends Command
+{
+ protected function configure()
+ {
+ // 指令配置
+ $this->setName('clear')
+ ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null)
+ ->addOption('cache', 'c', Option::VALUE_NONE, 'clear cache file')
+ ->addOption('log', 'l', Option::VALUE_NONE, 'clear log file')
+ ->addOption('dir', 'r', Option::VALUE_NONE, 'clear empty dir')
+ ->addOption('expire', 'e', Option::VALUE_NONE, 'clear cache file if cache has expired')
+ ->setDescription('Clear runtime file');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR;
+
+ if ($input->getOption('cache')) {
+ $path = $runtimePath . 'cache';
+ } elseif ($input->getOption('log')) {
+ $path = $runtimePath . 'log';
+ } else {
+ $path = $input->getOption('path') ?: $runtimePath;
+ }
+
+ $rmdir = $input->getOption('dir') ? true : false;
+ // --expire 仅当 --cache 时生效
+ $cache_expire = $input->getOption('expire') && $input->getOption('cache') ? true : false;
+ $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir, $cache_expire);
+
+ $output->writeln("Clear Successed ");
+ }
+
+ protected function clear(string $path, bool $rmdir, bool $cache_expire): void
+ {
+ $files = is_dir($path) ? scandir($path) : [];
+
+ foreach ($files as $file) {
+ if ('.' != $file && '..' != $file && is_dir($path . $file)) {
+ $this->clear($path . $file . DIRECTORY_SEPARATOR, $rmdir, $cache_expire);
+ if ($rmdir) {
+ @rmdir($path . $file);
+ }
+ } elseif ('.gitignore' != $file && is_file($path . $file)) {
+ if ($cache_expire) {
+ if ($this->cacheHasExpired($path . $file)) {
+ unlink($path . $file);
+ }
+ } else {
+ unlink($path . $file);
+ }
+ }
+ }
+ }
+
+ /**
+ * 缓存文件是否已过期
+ * @param $filename string 文件路径
+ * @return bool
+ */
+ protected function cacheHasExpired($filename) {
+ $content = file_get_contents($filename);
+ $expire = (int) substr($content, 8, 12);
+ return 0 != $expire && time() - $expire > filemtime($filename);
+ }
+
+}
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..be065fe
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Help.php
@@ -0,0 +1,69 @@
+
+// +----------------------------------------------------------------------
+
+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..5dc7edc
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Lists.php
@@ -0,0 +1,73 @@
+
+// +----------------------------------------------------------------------
+
+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..398f773
--- /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 = str_replace('app\\', '', $name);
+
+ 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/RouteList.php b/vendor/topthink/framework/src/think/console/command/RouteList.php
new file mode 100644
index 0000000..ccd25bd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/RouteList.php
@@ -0,0 +1,129 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\console\Table;
+use think\event\RouteLoaded;
+
+class RouteList extends Command
+{
+ protected $sortBy = [
+ 'rule' => 0,
+ 'route' => 1,
+ 'method' => 2,
+ 'name' => 3,
+ 'domain' => 4,
+ ];
+
+ protected function configure()
+ {
+ $this->setName('route:list')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->addArgument('style', Argument::OPTIONAL, "the style of the table.", 'default')
+ ->addOption('sort', 's', Option::VALUE_OPTIONAL, 'order by rule name.', 0)
+ ->addOption('more', 'm', Option::VALUE_NONE, 'show route options.')
+ ->setDescription('show route list.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dir = $input->getArgument('dir') ?: '';
+
+ $filename = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($dir ? $dir . DIRECTORY_SEPARATOR : '') . 'route_list.php';
+
+ if (is_file($filename)) {
+ unlink($filename);
+ } elseif (!is_dir(dirname($filename))) {
+ mkdir(dirname($filename), 0755);
+ }
+
+ $content = $this->getRouteList($dir);
+ file_put_contents($filename, 'Route List' . PHP_EOL . $content);
+ }
+
+ protected function getRouteList(string $dir = null): string
+ {
+ $this->app->route->setTestMode(true);
+ $this->app->route->clear();
+
+ if ($dir) {
+ $path = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR;
+ } else {
+ $path = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
+ }
+
+ $files = is_dir($path) ? scandir($path) : [];
+
+ foreach ($files as $file) {
+ if (strpos($file, '.php')) {
+ include $path . $file;
+ }
+ }
+
+ //触发路由载入完成事件
+ $this->app->event->trigger(RouteLoaded::class);
+
+ $table = new Table();
+
+ if ($this->input->hasOption('more')) {
+ $header = ['Rule', 'Route', 'Method', 'Name', 'Domain', 'Option', 'Pattern'];
+ } else {
+ $header = ['Rule', 'Route', 'Method', 'Name'];
+ }
+
+ $table->setHeader($header);
+
+ $routeList = $this->app->route->getRuleList();
+ $rows = [];
+
+ foreach ($routeList as $item) {
+ $item['route'] = $item['route'] instanceof \Closure ? '' : $item['route'];
+
+ if ($this->input->hasOption('more')) {
+ $item = [$item['rule'], $item['route'], $item['method'], $item['name'], $item['domain'], json_encode($item['option']), json_encode($item['pattern'])];
+ } else {
+ $item = [$item['rule'], $item['route'], $item['method'], $item['name']];
+ }
+
+ $rows[] = $item;
+ }
+
+ if ($this->input->getOption('sort')) {
+ $sort = strtolower($this->input->getOption('sort'));
+
+ if (isset($this->sortBy[$sort])) {
+ $sort = $this->sortBy[$sort];
+ }
+
+ uasort($rows, function ($a, $b) use ($sort) {
+ $itemA = $a[$sort] ?? null;
+ $itemB = $b[$sort] ?? null;
+
+ return strcasecmp($itemA, $itemB);
+ });
+ }
+
+ $table->setRows($rows);
+
+ if ($this->input->getArgument('style')) {
+ $style = $this->input->getArgument('style');
+ $table->setStyle($style);
+ }
+
+ return $this->table($table);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/RunServer.php b/vendor/topthink/framework/src/think/console/command/RunServer.php
new file mode 100644
index 0000000..d7cba70
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/RunServer.php
@@ -0,0 +1,57 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class RunServer extends Command
+{
+ public function configure()
+ {
+ $this->setName('run')
+ ->addOption('host', 'H', Option::VALUE_OPTIONAL,
+ 'The host to server the application on', '0.0.0.0')
+ ->addOption('port', 'p', Option::VALUE_OPTIONAL,
+ 'The port to server the application on', 8000)
+ ->addOption('root', 'r', Option::VALUE_OPTIONAL,
+ 'The document root of the application', '')
+ ->setDescription('PHP Built-in Server for ThinkPHP');
+ }
+
+ public function execute(Input $input, Output $output)
+ {
+ $host = $input->getOption('host');
+ $port = $input->getOption('port');
+ $root = $input->getOption('root');
+ if (empty($root)) {
+ $root = $this->app->getRootPath() . 'public';
+ }
+
+ $command = sprintf(
+ 'php -S %s:%d -t %s %s',
+ $host,
+ $port,
+ escapeshellarg($root),
+ escapeshellarg($root . DIRECTORY_SEPARATOR . 'router.php')
+ );
+
+ $output->writeln(sprintf('ThinkPHP Development server is started On ', '0.0.0.0' == $host ? '127.0.0.1' : $host, $port));
+ $output->writeln(sprintf('You can exit with `CTRL-C` '));
+ $output->writeln(sprintf('Document root is: %s', $root));
+ passthru($command);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/ServiceDiscover.php b/vendor/topthink/framework/src/think/console/command/ServiceDiscover.php
new file mode 100644
index 0000000..50b2148
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/ServiceDiscover.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class ServiceDiscover extends Command
+{
+ public function configure()
+ {
+ $this->setName('service:discover')
+ ->setDescription('Discover Services for ThinkPHP');
+ }
+
+ public function execute(Input $input, Output $output)
+ {
+ if (is_file($path = $this->app->getRootPath() . 'vendor/composer/installed.json')) {
+ $packages = json_decode(@file_get_contents($path), true);
+
+ $services = [];
+ foreach ($packages as $package) {
+ if (!empty($package['extra']['think']['services'])) {
+ $services = array_merge($services, (array) $package['extra']['think']['services']);
+ }
+ }
+
+ $header = '// This file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL . 'declare (strict_types = 1);' . PHP_EOL;
+
+ $content = 'app->getRootPath() . 'vendor/services.php', $content);
+
+ $output->writeln('Succeed! ');
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/VendorPublish.php b/vendor/topthink/framework/src/think/console/command/VendorPublish.php
new file mode 100644
index 0000000..e54175b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/VendorPublish.php
@@ -0,0 +1,66 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\input\Option;
+
+class VendorPublish extends Command
+{
+ public function configure()
+ {
+ $this->setName('vendor:publish')
+ ->addOption('force', 'f', Option::VALUE_NONE, 'Overwrite any existing files')
+ ->setDescription('Publish any publishable assets from vendor packages');
+ }
+
+ public function handle()
+ {
+
+ $force = $this->input->getOption('force');
+
+ if (is_file($path = $this->app->getRootPath() . 'vendor/composer/installed.json')) {
+ $packages = json_decode(@file_get_contents($path), true);
+
+ foreach ($packages as $package) {
+ //配置
+ $configDir = $this->app->getConfigPath();
+
+ if (!empty($package['extra']['think']['config'])) {
+
+ $installPath = $this->app->getRootPath() . 'vendor/' . $package['name'] . DIRECTORY_SEPARATOR;
+
+ foreach ((array) $package['extra']['think']['config'] as $name => $file) {
+
+ $target = $configDir . $name . '.php';
+ $source = $installPath . $file;
+
+ if (is_file($target) && !$force) {
+ $this->output->info("File {$target} exist!");
+ continue;
+ }
+
+ if (!is_file($source)) {
+ $this->output->info("File {$source} not exist!");
+ continue;
+ }
+
+ copy($source, $target);
+ }
+ }
+ }
+
+ $this->output->writeln('Succeed! ');
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/Version.php b/vendor/topthink/framework/src/think/console/command/Version.php
new file mode 100644
index 0000000..34f73ea
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Version.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Version extends Command
+{
+ protected function configure()
+ {
+ // 指令配置
+ $this->setName('version')
+ ->setDescription('show thinkphp framework version');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('v' . $this->app->version());
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Command.php b/vendor/topthink/framework/src/think/console/command/make/Command.php
new file mode 100644
index 0000000..c09b271
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Command.php
@@ -0,0 +1,55 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+use think\console\input\Argument;
+
+class Command extends Make
+{
+ protected $type = "Command";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:command')
+ ->addArgument('commandName', Argument::OPTIONAL, "The name of the command")
+ ->setDescription('Create a new command class');
+ }
+
+ protected function buildClass(string $name): string
+ {
+ $commandName = $this->input->getArgument('commandName') ?: strtolower(basename($name));
+ $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
+
+ $class = str_replace($namespace . '\\', '', $name);
+ $stub = file_get_contents($this->getStub());
+
+ return str_replace(['{%commandName%}', '{%className%}', '{%namespace%}', '{%app_namespace%}'], [
+ $commandName,
+ $class,
+ $namespace,
+ $this->app->getNamespace(),
+ ], $stub);
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'command.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\command';
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Controller.php b/vendor/topthink/framework/src/think/console/command/make/Controller.php
new file mode 100644
index 0000000..066c28b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Controller.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+use think\console\input\Option;
+
+class Controller extends Make
+{
+
+ protected $type = "Controller";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:controller')
+ ->addOption('api', null, Option::VALUE_NONE, 'Generate an api controller class.')
+ ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.')
+ ->setDescription('Create a new resource controller class');
+ }
+
+ protected function getStub(): string
+ {
+ $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR;
+
+ if ($this->input->getOption('api')) {
+ return $stubPath . 'controller.api.stub';
+ }
+
+ if ($this->input->getOption('plain')) {
+ return $stubPath . 'controller.plain.stub';
+ }
+
+ return $stubPath . 'controller.stub';
+ }
+
+ protected function getClassName(string $name): string
+ {
+ return parent::getClassName($name) . ($this->app->config->get('route.controller_suffix') ? 'Controller' : '');
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\controller';
+ }
+
+}
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..47282bf
--- /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..fe896ab
--- /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..736dc62
--- /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 '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..be19163
--- /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/Service.php b/vendor/topthink/framework/src/think/console/command/make/Service.php
new file mode 100644
index 0000000..8127109
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Service.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Service extends Make
+{
+ protected $type = "Service";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:service')
+ ->setDescription('Create a new Service class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'service.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\service';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Subscribe.php b/vendor/topthink/framework/src/think/console/command/make/Subscribe.php
new file mode 100644
index 0000000..f0c4723
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Subscribe.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Subscribe extends Make
+{
+ protected $type = "Subscribe";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:subscribe')
+ ->setDescription('Create a new subscribe class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'subscribe.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\subscribe';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Validate.php b/vendor/topthink/framework/src/think/console/command/make/Validate.php
new file mode 100644
index 0000000..a4198f7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Validate.php
@@ -0,0 +1,39 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Validate extends Make
+{
+ protected $type = "Validate";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:validate')
+ ->setDescription('Create a validate class');
+ }
+
+ protected function getStub(): string
+ {
+ $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR;
+
+ return $stubPath . 'validate.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\validate';
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/stubs/command.stub b/vendor/topthink/framework/src/think/console/command/make/stubs/command.stub
new file mode 100644
index 0000000..2656f70
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/stubs/command.stub
@@ -0,0 +1,26 @@
+setName('{%commandName%}')
+ ->setDescription('the {%commandName%} command');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ // 指令输出
+ $output->writeln('{%commandName%}');
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/stubs/controller.api.stub b/vendor/topthink/framework/src/think/console/command/make/stubs/controller.api.stub
new file mode 100644
index 0000000..ec4bc7c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/stubs/controller.api.stub
@@ -0,0 +1,64 @@
+ ['规则1','规则2'...]
+ *
+ * @var array
+ */
+ protected $rule = [];
+
+ /**
+ * 定义错误信息
+ * 格式:'字段名.规则名' => '错误信息'
+ *
+ * @var array
+ */
+ protected $message = [];
+}
diff --git a/vendor/topthink/framework/src/think/console/command/optimize/Route.php b/vendor/topthink/framework/src/think/console/command/optimize/Route.php
new file mode 100644
index 0000000..a0bd828
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/optimize/Route.php
@@ -0,0 +1,67 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\event\RouteLoaded;
+
+class Route extends Command
+{
+ protected function configure()
+ {
+ $this->setName('optimize:route')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->setDescription('Build app route cache.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dir = $input->getArgument('dir') ?: '';
+
+ $path = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($dir ? $dir . DIRECTORY_SEPARATOR : '');
+
+ $filename = $path . 'route.php';
+ if (is_file($filename)) {
+ unlink($filename);
+ }
+
+ file_put_contents($filename, $this->buildRouteCache($dir));
+ $output->writeln('Succeed! ');
+ }
+
+ protected function buildRouteCache(string $dir = null): string
+ {
+ $this->app->route->clear();
+ $this->app->route->lazy(false);
+
+ // 路由检测
+ $path = $this->app->getRootPath() . ($dir ? 'app' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR : '') . 'route' . DIRECTORY_SEPARATOR ;
+
+ $files = is_dir($path) ? scandir($path) : [];
+
+ foreach ($files as $file) {
+ if (strpos($file, '.php')) {
+ include $path . $file;
+ }
+ }
+
+ //触发路由载入完成事件
+ $this->app->event->trigger(RouteLoaded::class);
+
+ $content = 'app->route->getName()) . '\');';
+ return $content;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/command/optimize/Schema.php b/vendor/topthink/framework/src/think/console/command/optimize/Schema.php
new file mode 100644
index 0000000..21d561f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/optimize/Schema.php
@@ -0,0 +1,99 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\db\PDOConnection;
+
+class Schema extends Command
+{
+ protected function configure()
+ {
+ $this->setName('optimize:schema')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->addOption('connection', null, Option::VALUE_REQUIRED, 'connection name .')
+ ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .')
+ ->setDescription('Build database schema cache.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dir = $input->getArgument('dir') ?: '';
+
+ if ($input->hasOption('table')) {
+ $connection = $this->app->db->connect($input->getOption('connection'));
+ if (!$connection instanceof PDOConnection) {
+ $output->error("only PDO connection support schema cache!");
+ return;
+ }
+ $table = $input->getOption('table');
+ if (false === strpos($table, '.')) {
+ $dbName = $connection->getConfig('database');
+ } else {
+ [$dbName, $table] = explode('.', $table);
+ }
+
+ if ($table == '*') {
+ $table = $connection->getTables($dbName);
+ }
+
+ $this->buildDataBaseSchema($connection, (array) $table, $dbName);
+ } else {
+ if ($dir) {
+ $appPath = $this->app->getBasePath() . $dir . DIRECTORY_SEPARATOR;
+ $namespace = 'app\\' . $dir;
+ } else {
+ $appPath = $this->app->getBasePath();
+ $namespace = 'app';
+ }
+
+ $path = $appPath . 'model';
+ $list = is_dir($path) ? scandir($path) : [];
+
+ foreach ($list as $file) {
+ if (0 === strpos($file, '.')) {
+ continue;
+ }
+ $class = '\\' . $namespace . '\\model\\' . pathinfo($file, PATHINFO_FILENAME);
+ $this->buildModelSchema($class);
+ }
+ }
+
+ $output->writeln('Succeed! ');
+ }
+
+ protected function buildModelSchema(string $class): void
+ {
+ $reflect = new \ReflectionClass($class);
+ if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) {
+ /** @var \think\Model $model */
+ $model = new $class;
+ $connection = $model->db()->getConnection();
+ if ($connection instanceof PDOConnection) {
+ $table = $model->getTable();
+ //预读字段信息
+ $connection->getSchemaInfo($table, true);
+ }
+ }
+ }
+
+ protected function buildDataBaseSchema(PDOConnection $connection, array $tables, string $dbName): void
+ {
+ foreach ($tables as $table) {
+ //预读字段信息
+ $connection->getSchemaInfo("{$dbName}.{$table}", true);
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/input/Argument.php b/vendor/topthink/framework/src/think/console/input/Argument.php
new file mode 100644
index 0000000..66e47b4
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/input/Argument.php
@@ -0,0 +1,115 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Argument
+{
+
+ const REQUIRED = 1;
+ const OPTIONAL = 2;
+ const IS_ARRAY = 4;
+
+ private $name;
+ private $mode;
+ private $default;
+ private $description;
+
+ /**
+ * 构造方法
+ * @param string $name 参数名
+ * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL
+ * @param string $description 描述
+ * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(string $name, int $mode = null, string $description = '', $default = null)
+ {
+ if (null === $mode) {
+ $mode = self::OPTIONAL;
+ } elseif (!is_int($mode) || $mode > 7 || $mode < 1) {
+ throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
+ }
+
+ $this->name = $name;
+ $this->mode = $mode;
+ $this->description = $description;
+
+ $this->setDefault($default);
+ }
+
+ /**
+ * 获取参数名
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否必须
+ * @return bool
+ */
+ public function isRequired(): bool
+ {
+ return self::REQUIRED === (self::REQUIRED & $this->mode);
+ }
+
+ /**
+ * 该参数是否接受数组
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
+ }
+
+ /**
+ * 设置默认值
+ * @param mixed $default 默认值
+ * @throws \LogicException
+ */
+ public function setDefault($default = null): void
+ {
+ if (self::REQUIRED === $this->mode && null !== $default) {
+ throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
+ }
+
+ if ($this->isArray()) {
+ if (null === $default) {
+ $default = [];
+ } elseif (!is_array($default)) {
+ throw new \LogicException('A default value for an array argument must be an array.');
+ }
+ }
+
+ $this->default = $default;
+ }
+
+ /**
+ * 获取默认值
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/input/Definition.php b/vendor/topthink/framework/src/think/console/input/Definition.php
new file mode 100644
index 0000000..777eae0
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/input/Definition.php
@@ -0,0 +1,375 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Definition
+{
+
+ /**
+ * @var Argument[]
+ */
+ private $arguments;
+
+ private $requiredCount;
+ private $hasAnArrayArgument = false;
+ private $hasOptional;
+
+ /**
+ * @var Option[]
+ */
+ private $options;
+ private $shortcuts;
+
+ /**
+ * 构造方法
+ * @param array $definition
+ * @api
+ */
+ public function __construct(array $definition = [])
+ {
+ $this->setDefinition($definition);
+ }
+
+ /**
+ * 设置指令的定义
+ * @param array $definition 定义的数组
+ */
+ public function setDefinition(array $definition): void
+ {
+ $arguments = [];
+ $options = [];
+ foreach ($definition as $item) {
+ if ($item instanceof Option) {
+ $options[] = $item;
+ } else {
+ $arguments[] = $item;
+ }
+ }
+
+ $this->setArguments($arguments);
+ $this->setOptions($options);
+ }
+
+ /**
+ * 设置参数
+ * @param Argument[] $arguments 参数数组
+ */
+ public function setArguments(array $arguments = []): void
+ {
+ $this->arguments = [];
+ $this->requiredCount = 0;
+ $this->hasOptional = false;
+ $this->hasAnArrayArgument = false;
+ $this->addArguments($arguments);
+ }
+
+ /**
+ * 添加参数
+ * @param Argument[] $arguments 参数数组
+ * @api
+ */
+ public function addArguments(array $arguments = []): void
+ {
+ if (null !== $arguments) {
+ foreach ($arguments as $argument) {
+ $this->addArgument($argument);
+ }
+ }
+ }
+
+ /**
+ * 添加一个参数
+ * @param Argument $argument 参数
+ * @throws \LogicException
+ */
+ public function addArgument(Argument $argument): void
+ {
+ if (isset($this->arguments[$argument->getName()])) {
+ throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
+ }
+
+ if ($this->hasAnArrayArgument) {
+ throw new \LogicException('Cannot add an argument after an array argument.');
+ }
+
+ if ($argument->isRequired() && $this->hasOptional) {
+ throw new \LogicException('Cannot add a required argument after an optional one.');
+ }
+
+ if ($argument->isArray()) {
+ $this->hasAnArrayArgument = true;
+ }
+
+ if ($argument->isRequired()) {
+ ++$this->requiredCount;
+ } else {
+ $this->hasOptional = true;
+ }
+
+ $this->arguments[$argument->getName()] = $argument;
+ }
+
+ /**
+ * 根据名称或者位置获取参数
+ * @param string|int $name 参数名或者位置
+ * @return Argument 参数
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument($name): Argument
+ {
+ if (!$this->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return $arguments[$name];
+ }
+
+ /**
+ * 根据名称或位置检查是否具有某个参数
+ * @param string|int $name 参数名或者位置
+ * @return bool
+ * @api
+ */
+ public function hasArgument($name): bool
+ {
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return isset($arguments[$name]);
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[] 参数数组
+ */
+ public function getArguments(): array
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * 获取参数数量
+ * @return int
+ */
+ public function getArgumentCount(): int
+ {
+ return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments);
+ }
+
+ /**
+ * 获取必填的参数的数量
+ * @return int
+ */
+ public function getArgumentRequiredCount(): int
+ {
+ return $this->requiredCount;
+ }
+
+ /**
+ * 获取参数默认值
+ * @return array
+ */
+ public function getArgumentDefaults(): array
+ {
+ $values = [];
+ foreach ($this->arguments as $argument) {
+ $values[$argument->getName()] = $argument->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 设置选项
+ * @param Option[] $options 选项数组
+ */
+ public function setOptions(array $options = []): void
+ {
+ $this->options = [];
+ $this->shortcuts = [];
+ $this->addOptions($options);
+ }
+
+ /**
+ * 添加选项
+ * @param Option[] $options 选项数组
+ * @api
+ */
+ public function addOptions(array $options = []): void
+ {
+ foreach ($options as $option) {
+ $this->addOption($option);
+ }
+ }
+
+ /**
+ * 添加一个选项
+ * @param Option $option 选项
+ * @throws \LogicException
+ * @api
+ */
+ public function addOption(Option $option): void
+ {
+ if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
+ throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+ }
+
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ if (isset($this->shortcuts[$shortcut])
+ && !$option->equals($this->options[$this->shortcuts[$shortcut]])
+ ) {
+ throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
+ }
+ }
+ }
+
+ $this->options[$option->getName()] = $option;
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ $this->shortcuts[$shortcut] = $option->getName();
+ }
+ }
+ }
+
+ /**
+ * 根据名称获取选项
+ * @param string $name 选项名
+ * @return Option
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function getOption(string $name): Option
+ {
+ if (!$this->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ return $this->options[$name];
+ }
+
+ /**
+ * 根据名称检查是否有这个选项
+ * @param string $name 选项名
+ * @return bool
+ * @api
+ */
+ public function hasOption(string $name): bool
+ {
+ return isset($this->options[$name]);
+ }
+
+ /**
+ * 获取所有选项
+ * @return Option[]
+ * @api
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ /**
+ * 根据名称检查某个选项是否有短名称
+ * @param string $name 短名称
+ * @return bool
+ */
+ public function hasShortcut(string $name): bool
+ {
+ return isset($this->shortcuts[$name]);
+ }
+
+ /**
+ * 根据短名称获取选项
+ * @param string $shortcut 短名称
+ * @return Option
+ */
+ public function getOptionForShortcut(string $shortcut): Option
+ {
+ return $this->getOption($this->shortcutToName($shortcut));
+ }
+
+ /**
+ * 获取所有选项的默认值
+ * @return array
+ */
+ public function getOptionDefaults(): array
+ {
+ $values = [];
+ foreach ($this->options as $option) {
+ $values[$option->getName()] = $option->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 根据短名称获取选项名
+ * @param string $shortcut 短名称
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ private function shortcutToName(string $shortcut): string
+ {
+ if (!isset($this->shortcuts[$shortcut])) {
+ throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ return $this->shortcuts[$shortcut];
+ }
+
+ /**
+ * 获取该指令的介绍
+ * @param bool $short 是否简洁介绍
+ * @return string
+ */
+ public function getSynopsis(bool $short = false): string
+ {
+ $elements = [];
+
+ if ($short && $this->getOptions()) {
+ $elements[] = '[options]';
+ } elseif (!$short) {
+ foreach ($this->getOptions() as $option) {
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '');
+ }
+
+ $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
+ $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
+ }
+ }
+
+ if (count($elements) && $this->getArguments()) {
+ $elements[] = '[--]';
+ }
+
+ foreach ($this->getArguments() as $argument) {
+ $element = '<' . $argument->getName() . '>';
+ if (!$argument->isRequired()) {
+ $element = '[' . $element . ']';
+ } elseif ($argument->isArray()) {
+ $element .= ' (' . $element . ')';
+ }
+
+ if ($argument->isArray()) {
+ $element .= '...';
+ }
+
+ $elements[] = $element;
+ }
+
+ return implode(' ', $elements);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/input/Option.php b/vendor/topthink/framework/src/think/console/input/Option.php
new file mode 100644
index 0000000..db64bc7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/input/Option.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Option
+{
+
+ const VALUE_NONE = 1;
+ const VALUE_REQUIRED = 2;
+ const VALUE_OPTIONAL = 4;
+ const VALUE_IS_ARRAY = 8;
+
+ private $name;
+ private $shortcut;
+ private $mode;
+ private $default;
+ 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 = null;
+ }
+
+ if (null !== $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()
+ {
+ return $this->shortcut;
+ }
+
+ /**
+ * 获取选项名
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否可以设置值
+ * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false
+ */
+ public function acceptValue()
+ {
+ return $this->isValueRequired() || $this->isValueOptional();
+ }
+
+ /**
+ * 是否必须
+ * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false
+ */
+ public function isValueRequired()
+ {
+ return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
+ }
+
+ /**
+ * 是否可选
+ * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false
+ */
+ public function isValueOptional()
+ {
+ return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
+ }
+
+ /**
+ * 选项值是否接受数组
+ * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false
+ */
+ public function isArray()
+ {
+ 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()
+ {
+ return $this->description;
+ }
+
+ /**
+ * 检查所给选项是否是当前这个
+ * @param Option $option
+ * @return bool
+ */
+ public function equals(Option $option)
+ {
+ 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/Ask.php b/vendor/topthink/framework/src/think/console/output/Ask.php
new file mode 100644
index 0000000..f8968dd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/Ask.php
@@ -0,0 +1,336 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\console\Input;
+use think\console\Output;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+
+class Ask
+{
+ private static $stty;
+
+ private static $shell;
+
+ /** @var Input */
+ protected $input;
+
+ /** @var Output */
+ protected $output;
+
+ /** @var Question */
+ protected $question;
+
+ public function __construct(Input $input, Output $output, Question $question)
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->question = $question;
+ }
+
+ public function run()
+ {
+ if (!$this->input->isInteractive()) {
+ return $this->question->getDefault();
+ }
+
+ if (!$this->question->getValidator()) {
+ return $this->doAsk();
+ }
+
+ $that = $this;
+
+ $interviewer = function () use ($that) {
+ return $that->doAsk();
+ };
+
+ return $this->validateAttempts($interviewer);
+ }
+
+ protected function doAsk()
+ {
+ $this->writePrompt();
+
+ $inputStream = STDIN;
+ $autocomplete = $this->question->getAutocompleterValues();
+
+ if (null === $autocomplete || !$this->hasSttyAvailable()) {
+ $ret = false;
+ if ($this->question->isHidden()) {
+ try {
+ $ret = trim($this->getHiddenResponse($inputStream));
+ } catch (\RuntimeException $e) {
+ if (!$this->question->isHiddenFallback()) {
+ throw $e;
+ }
+ }
+ }
+
+ if (false === $ret) {
+ $ret = fgets($inputStream, 4096);
+ if (false === $ret) {
+ throw new \RuntimeException('Aborted');
+ }
+ $ret = trim($ret);
+ }
+ } else {
+ $ret = trim($this->autocomplete($inputStream));
+ }
+
+ $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault();
+
+ if ($normalizer = $this->question->getNormalizer()) {
+ return $normalizer($ret);
+ }
+
+ return $ret;
+ }
+
+ private function autocomplete($inputStream)
+ {
+ $autocomplete = $this->question->getAutocompleterValues();
+ $ret = '';
+
+ $i = 0;
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -icanon -echo');
+
+ while (!feof($inputStream)) {
+ $c = fread($inputStream, 1);
+
+ if ("\177" === $c) {
+ if (0 === $numMatches && 0 !== $i) {
+ --$i;
+ $this->output->write("\033[1D");
+ }
+
+ if ($i === 0) {
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+ } else {
+ $numMatches = 0;
+ }
+
+ $ret = substr($ret, 0, $i);
+ } elseif ("\033" === $c) {
+ $c .= fread($inputStream, 2);
+
+ if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
+ if ('A' === $c[2] && -1 === $ofs) {
+ $ofs = 0;
+ }
+
+ if (0 === $numMatches) {
+ continue;
+ }
+
+ $ofs += ('A' === $c[2]) ? -1 : 1;
+ $ofs = ($numMatches + $ofs) % $numMatches;
+ }
+ } elseif (ord($c) < 32) {
+ if ("\t" === $c || "\n" === $c) {
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $ret = $matches[$ofs];
+ $this->output->write(substr($ret, $i));
+ $i = strlen($ret);
+ }
+
+ if ("\n" === $c) {
+ $this->output->write($c);
+ break;
+ }
+
+ $numMatches = 0;
+ }
+
+ continue;
+ } else {
+ $this->output->write($c);
+ $ret .= $c;
+ ++$i;
+
+ $numMatches = 0;
+ $ofs = 0;
+
+ foreach ($autocomplete as $value) {
+ if (0 === strpos($value, $ret) && $i !== strlen($value)) {
+ $matches[$numMatches++] = $value;
+ }
+ }
+ }
+
+ $this->output->write("\033[K");
+
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $this->output->write("\0337");
+ $this->output->highlight(substr($matches[$ofs], $i));
+ $this->output->write("\0338");
+ }
+ }
+
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ return $ret;
+ }
+
+ protected function getHiddenResponse($inputStream)
+ {
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ $exe = __DIR__ . '/../bin/hiddeninput.exe';
+
+ $value = rtrim(shell_exec($exe));
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ if ($this->hasSttyAvailable()) {
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -echo');
+ $value = fgets($inputStream, 4096);
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ if (false === $value) {
+ throw new \RuntimeException('Aborted');
+ }
+
+ $value = trim($value);
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ if (false !== $shell = $this->getShell()) {
+ $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
+ $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
+ $value = rtrim(shell_exec($command));
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ throw new \RuntimeException('Unable to hide the response.');
+ }
+
+ protected function validateAttempts($interviewer)
+ {
+ /** @var \Exception $error */
+ $error = null;
+ $attempts = $this->question->getMaxAttempts();
+ while (null === $attempts || $attempts--) {
+ if (null !== $error) {
+ $this->output->error($error->getMessage());
+ }
+
+ try {
+ return call_user_func($this->question->getValidator(), $interviewer());
+ } catch (\Exception $error) {
+ }
+ }
+
+ throw $error;
+ }
+
+ /**
+ * 显示问题的提示信息
+ */
+ protected function writePrompt()
+ {
+ $text = $this->question->getQuestion();
+ $default = $this->question->getDefault();
+
+ switch (true) {
+ case null === $default:
+ $text = sprintf(' %s :', $text);
+
+ break;
+
+ case $this->question instanceof Confirmation:
+ $text = sprintf(' %s (yes/no) [%s ]:', $text, $default ? 'yes' : 'no');
+
+ break;
+
+ case $this->question instanceof Choice && $this->question->isMultiselect():
+ $choices = $this->question->getChoices();
+ $default = explode(',', $default);
+
+ foreach ($default as $key => $value) {
+ $default[$key] = $choices[trim($value)];
+ }
+
+ $text = sprintf(' %s [%s ]:', $text, implode(', ', $default));
+
+ break;
+
+ case $this->question instanceof Choice:
+ $choices = $this->question->getChoices();
+ $text = sprintf(' %s [%s ]:', $text, $choices[$default]);
+
+ break;
+
+ default:
+ $text = sprintf(' %s [%s ]:', $text, $default);
+ }
+
+ $this->output->writeln($text);
+
+ if ($this->question instanceof Choice) {
+ $width = max(array_map('strlen', array_keys($this->question->getChoices())));
+
+ foreach ($this->question->getChoices() as $key => $value) {
+ $this->output->writeln(sprintf(" [%-${width}s ] %s", $key, $value));
+ }
+ }
+
+ $this->output->write(' > ');
+ }
+
+ private function getShell()
+ {
+ if (null !== self::$shell) {
+ return self::$shell;
+ }
+
+ self::$shell = false;
+
+ if (file_exists('/usr/bin/env')) {
+ $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
+ foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
+ if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
+ self::$shell = $sh;
+ break;
+ }
+ }
+ }
+
+ return self::$shell;
+ }
+
+ private function hasSttyAvailable()
+ {
+ if (null !== self::$stty) {
+ return self::$stty;
+ }
+
+ exec('stty 2>&1', $output, $exitcode);
+
+ return self::$stty = $exitcode === 0;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/Descriptor.php b/vendor/topthink/framework/src/think/console/output/Descriptor.php
new file mode 100644
index 0000000..ab4454d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/Descriptor.php
@@ -0,0 +1,323 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\Console;
+use think\console\Command;
+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;
+use think\console\output\descriptor\Console as ConsoleDescription;
+
+class Descriptor
+{
+
+ /**
+ * @var Output
+ */
+ protected $output;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function describe(Output $output, $object, array $options = [])
+ {
+ $this->output = $output;
+
+ switch (true) {
+ case $object instanceof InputArgument:
+ $this->describeInputArgument($object, $options);
+ break;
+ case $object instanceof InputOption:
+ $this->describeInputOption($object, $options);
+ break;
+ case $object instanceof InputDefinition:
+ $this->describeInputDefinition($object, $options);
+ break;
+ case $object instanceof Command:
+ $this->describeCommand($object, $options);
+ break;
+ case $object instanceof Console:
+ $this->describeConsole($object, $options);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
+ }
+ }
+
+ /**
+ * 输出内容
+ * @param string $content
+ * @param bool $decorated
+ */
+ protected function write($content, $decorated = false)
+ {
+ $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW);
+ }
+
+ /**
+ * 描述参数
+ * @param InputArgument $argument
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputArgument(InputArgument $argument, array $options = [])
+ {
+ if (null !== $argument->getDefault()
+ && (!is_array($argument->getDefault())
+ || count($argument->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s] ', $this->formatDefaultValue($argument->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $totalWidth = $options['total_width'] ?? strlen($argument->getName());
+ $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
+
+ $this->writeText(sprintf(" %s %s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options);
+ }
+
+ /**
+ * 描述选项
+ * @param InputOption $option
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputOption(InputOption $option, array $options = [])
+ {
+ if ($option->acceptValue() && null !== $option->getDefault()
+ && (!is_array($option->getDefault())
+ || count($option->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s] ', $this->formatDefaultValue($option->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = '=' . strtoupper($option->getName());
+
+ if ($option->isValueOptional()) {
+ $value = '[' . $value . ']';
+ }
+ }
+
+ $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
+ $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value));
+
+ $spacingWidth = $totalWidth - strlen($synopsis) + 2;
+
+ $this->writeText(sprintf(" %s %s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed) ' : ''), $options);
+ }
+
+ /**
+ * 描述输入
+ * @param InputDefinition $definition
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ {
+ $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
+ foreach ($definition->getArguments() as $argument) {
+ $totalWidth = max($totalWidth, strlen($argument->getName()));
+ }
+
+ if ($definition->getArguments()) {
+ $this->writeText('Arguments: ', $options);
+ $this->writeText("\n");
+ foreach ($definition->getArguments() as $argument) {
+ $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
+ $this->writeText("\n");
+ }
+ }
+
+ if ($definition->getArguments() && $definition->getOptions()) {
+ $this->writeText("\n");
+ }
+
+ if ($definition->getOptions()) {
+ $laterOptions = [];
+
+ $this->writeText('Options: ', $options);
+ foreach ($definition->getOptions() as $option) {
+ if (strlen($option->getShortcut()) > 1) {
+ $laterOptions[] = $option;
+ continue;
+ }
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ foreach ($laterOptions as $option) {
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ }
+ }
+
+ /**
+ * 描述指令
+ * @param Command $command
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeCommand(Command $command, array $options = [])
+ {
+ $command->getSynopsis(true);
+ $command->getSynopsis(false);
+ $command->mergeConsoleDefinition(false);
+
+ $this->writeText('Usage: ', $options);
+ foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $usage, $options);
+ }
+ $this->writeText("\n");
+
+ $definition = $command->getNativeDefinition();
+ if ($definition->getOptions() || $definition->getArguments()) {
+ $this->writeText("\n");
+ $this->describeInputDefinition($definition, $options);
+ $this->writeText("\n");
+ }
+
+ if ($help = $command->getProcessedHelp()) {
+ $this->writeText("\n");
+ $this->writeText('Help: ', $options);
+ $this->writeText("\n");
+ $this->writeText(' ' . str_replace("\n", "\n ", $help), $options);
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * 描述控制台
+ * @param Console $console
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeConsole(Console $console, array $options = [])
+ {
+ $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
+ $description = new ConsoleDescription($console, $describedNamespace);
+
+ if (isset($options['raw_text']) && $options['raw_text']) {
+ $width = $this->getColumnWidth($description->getNamespaces());
+
+ foreach ($description->getCommands() as $command) {
+ $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options);
+ $this->writeText("\n");
+ }
+ } else {
+ if ('' != $help = $console->getHelp()) {
+ $this->writeText("$help\n\n", $options);
+ }
+
+ $this->writeText("Usage: \n", $options);
+ $this->writeText(" command [options] [arguments]\n\n", $options);
+
+ $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options);
+
+ $this->writeText("\n");
+ $this->writeText("\n");
+
+ $width = $this->getColumnWidth($description->getNamespaces());
+
+ if ($describedNamespace) {
+ $this->writeText(sprintf('Available commands for the "%s" namespace: ', $describedNamespace), $options);
+ } else {
+ $this->writeText('Available commands: ', $options);
+ }
+
+ // add commands by namespace
+ foreach ($description->getNamespaces() as $namespace) {
+ if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $namespace['id'] . ' ', $options);
+ }
+
+ foreach ($namespace['commands'] as $name) {
+ $this->writeText("\n");
+ $spacingWidth = $width - strlen($name);
+ $this->writeText(sprintf(" %s %s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)
+ ->getDescription()), $options);
+ }
+ }
+
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function writeText($content, array $options = [])
+ {
+ $this->write(isset($options['raw_text'])
+ && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true);
+ }
+
+ /**
+ * 格式化
+ * @param mixed $default
+ * @return string
+ */
+ private function formatDefaultValue($default)
+ {
+ return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ }
+
+ /**
+ * @param Namespaces[] $namespaces
+ * @return int
+ */
+ private function getColumnWidth(array $namespaces)
+ {
+ $width = 0;
+ foreach ($namespaces as $namespace) {
+ foreach ($namespace['commands'] as $name) {
+ if (strlen($name) > $width) {
+ $width = strlen($name);
+ }
+ }
+ }
+
+ return $width + 2;
+ }
+
+ /**
+ * @param InputOption[] $options
+ * @return int
+ */
+ private function calculateTotalWidthForOptions($options)
+ {
+ $totalWidth = 0;
+ foreach ($options as $option) {
+ $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
+
+ if ($option->acceptValue()) {
+ $valueLength = 1 + strlen($option->getName()); // = + value
+ $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
+
+ $nameLength += $valueLength;
+ }
+ $totalWidth = max($totalWidth, $nameLength);
+ }
+
+ return $totalWidth;
+ }
+}
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..9d582cf
--- /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/Question.php b/vendor/topthink/framework/src/think/console/output/Question.php
new file mode 100644
index 0000000..bd0917d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/Question.php
@@ -0,0 +1,211 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+class Question
+{
+
+ private $question;
+ private $attempts;
+ private $hidden = false;
+ private $hiddenFallback = true;
+ private $autocompleterValues;
+ private $validator;
+ private $default;
+ private $normalizer;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, $default = null)
+ {
+ $this->question = $question;
+ $this->default = $default;
+ }
+
+ /**
+ * 获取问题
+ * @return string
+ */
+ public function getQuestion()
+ {
+ return $this->question;
+ }
+
+ /**
+ * 获取默认答案
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 是否隐藏答案
+ * @return bool
+ */
+ public function isHidden()
+ {
+ return $this->hidden;
+ }
+
+ /**
+ * 隐藏答案
+ * @param bool $hidden
+ * @return Question
+ */
+ public function setHidden($hidden)
+ {
+ if ($this->autocompleterValues) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->hidden = (bool) $hidden;
+
+ return $this;
+ }
+
+ /**
+ * 不能被隐藏是否撤销
+ * @return bool
+ */
+ public function isHiddenFallback()
+ {
+ return $this->hiddenFallback;
+ }
+
+ /**
+ * 设置不能被隐藏的时候的操作
+ * @param bool $fallback
+ * @return Question
+ */
+ public function setHiddenFallback($fallback)
+ {
+ $this->hiddenFallback = (bool) $fallback;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动完成
+ * @return null|array|\Traversable
+ */
+ public function getAutocompleterValues()
+ {
+ return $this->autocompleterValues;
+ }
+
+ /**
+ * 设置自动完成的值
+ * @param null|array|\Traversable $values
+ * @return Question
+ * @throws \InvalidArgumentException
+ * @throws \LogicException
+ */
+ public function setAutocompleterValues($values)
+ {
+ if (is_array($values) && $this->isAssoc($values)) {
+ $values = array_merge(array_keys($values), array_values($values));
+ }
+
+ if (null !== $values && !is_array($values)) {
+ if (!$values instanceof \Traversable || $values instanceof \Countable) {
+ throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.');
+ }
+ }
+
+ if ($this->hidden) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->autocompleterValues = $values;
+
+ return $this;
+ }
+
+ /**
+ * 设置答案的验证器
+ * @param null|callable $validator
+ * @return Question The current instance
+ */
+ public function setValidator($validator)
+ {
+ $this->validator = $validator;
+
+ return $this;
+ }
+
+ /**
+ * 获取验证器
+ * @return null|callable
+ */
+ public function getValidator()
+ {
+ return $this->validator;
+ }
+
+ /**
+ * 设置最大重试次数
+ * @param null|int $attempts
+ * @return Question
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxAttempts($attempts)
+ {
+ if (null !== $attempts && $attempts < 1) {
+ throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.');
+ }
+
+ $this->attempts = $attempts;
+
+ return $this;
+ }
+
+ /**
+ * 获取最大重试次数
+ * @return null|int
+ */
+ public function getMaxAttempts()
+ {
+ return $this->attempts;
+ }
+
+ /**
+ * 设置响应的回调
+ * @param string|\Closure $normalizer
+ * @return Question
+ */
+ public function setNormalizer($normalizer)
+ {
+ $this->normalizer = $normalizer;
+
+ return $this;
+ }
+
+ /**
+ * 获取响应回调
+ * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
+ * @return string|\Closure
+ */
+ public function getNormalizer()
+ {
+ return $this->normalizer;
+ }
+
+ protected function isAssoc($array)
+ {
+ return (bool) count(array_filter(array_keys($array), 'is_string'));
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/descriptor/Console.php b/vendor/topthink/framework/src/think/console/output/descriptor/Console.php
new file mode 100644
index 0000000..0da0cec
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/descriptor/Console.php
@@ -0,0 +1,153 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\descriptor;
+
+use think\Console as ThinkConsole;
+use think\console\Command;
+
+class Console
+{
+
+ const GLOBAL_NAMESPACE = '_global';
+
+ /**
+ * @var ThinkConsole
+ */
+ private $console;
+
+ /**
+ * @var null|string
+ */
+ private $namespace;
+
+ /**
+ * @var array
+ */
+ private $namespaces;
+
+ /**
+ * @var Command[]
+ */
+ private $commands;
+
+ /**
+ * @var Command[]
+ */
+ private $aliases;
+
+ /**
+ * 构造方法
+ * @param ThinkConsole $console
+ * @param string|null $namespace
+ */
+ public function __construct(ThinkConsole $console, $namespace = null)
+ {
+ $this->console = $console;
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @return array
+ */
+ public function getNamespaces(): array
+ {
+ if (null === $this->namespaces) {
+ $this->inspectConsole();
+ }
+
+ return $this->namespaces;
+ }
+
+ /**
+ * @return Command[]
+ */
+ public function getCommands(): array
+ {
+ if (null === $this->commands) {
+ $this->inspectConsole();
+ }
+
+ return $this->commands;
+ }
+
+ /**
+ * @param string $name
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function getCommand(string $name): Command
+ {
+ if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
+ throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name));
+ }
+
+ return $this->commands[$name] ?? $this->aliases[$name];
+ }
+
+ private function inspectConsole(): void
+ {
+ $this->commands = [];
+ $this->namespaces = [];
+
+ $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null);
+ foreach ($this->sortCommands($all) as $namespace => $commands) {
+ $names = [];
+
+ /** @var Command $command */
+ foreach ($commands as $name => $command) {
+ if (is_string($command)) {
+ $command = new $command();
+ }
+
+ if (!$command->getName()) {
+ continue;
+ }
+
+ if ($command->getName() === $name) {
+ $this->commands[$name] = $command;
+ } else {
+ $this->aliases[$name] = $command;
+ }
+
+ $names[] = $name;
+ }
+
+ $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
+ }
+ }
+
+ /**
+ * @param array $commands
+ * @return array
+ */
+ private function sortCommands(array $commands): array
+ {
+ $namespacedCommands = [];
+ foreach ($commands as $name => $command) {
+ $key = $this->console->extractNamespace($name, 1);
+ if (!$key) {
+ $key = self::GLOBAL_NAMESPACE;
+ }
+
+ $namespacedCommands[$key][$name] = $command;
+ }
+ ksort($namespacedCommands);
+
+ foreach ($namespacedCommands as &$commandsSet) {
+ ksort($commandsSet);
+ }
+ // unset reference to keep scope clear
+ unset($commandsSet);
+
+ return $namespacedCommands;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/driver/Buffer.php b/vendor/topthink/framework/src/think/console/output/driver/Buffer.php
new file mode 100644
index 0000000..d878491
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/driver/Buffer.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Buffer
+{
+ /**
+ * @var string
+ */
+ private $buffer = '';
+
+ public function __construct(Output $output)
+ {
+ // do nothing
+ }
+
+ public function fetch()
+ {
+ $content = $this->buffer;
+ $this->buffer = '';
+ return $content;
+ }
+
+ public function write($messages, bool $newline = false, int $options = 0)
+ {
+ $messages = (array) $messages;
+
+ foreach ($messages as $message) {
+ $this->buffer .= $message;
+ }
+ if ($newline) {
+ $this->buffer .= "\n";
+ }
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ // do nothing
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/console/output/driver/Console.php b/vendor/topthink/framework/src/think/console/output/driver/Console.php
new file mode 100644
index 0000000..0b5b9d2
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/driver/Console.php
@@ -0,0 +1,368 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+use think\console\output\Formatter;
+
+class Console
+{
+
+ /** @var Resource */
+ private $stdout;
+
+ /** @var Formatter */
+ private $formatter;
+
+ private $terminalDimensions;
+
+ /** @var Output */
+ private $output;
+
+ public function __construct(Output $output)
+ {
+ $this->output = $output;
+ $this->formatter = new Formatter();
+ $this->stdout = $this->openOutputStream();
+ $decorated = $this->hasColorSupport($this->stdout);
+ $this->formatter->setDecorated($decorated);
+ }
+
+ public function setDecorated($decorated)
+ {
+ $this->formatter->setDecorated($decorated);
+ }
+
+ public function write($messages, bool $newline = false, int $type = 0, $stream = null)
+ {
+ if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) {
+ return;
+ }
+
+ $messages = (array) $messages;
+
+ foreach ($messages as $message) {
+ switch ($type) {
+ case Output::OUTPUT_NORMAL:
+ $message = $this->formatter->format($message);
+ break;
+ case Output::OUTPUT_RAW:
+ break;
+ case Output::OUTPUT_PLAIN:
+ $message = strip_tags($this->formatter->format($message));
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
+ }
+
+ $this->doWrite($message, $newline, $stream);
+ }
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ $stderr = $this->openErrorStream();
+ $decorated = $this->hasColorSupport($stderr);
+ $this->formatter->setDecorated($decorated);
+
+ do {
+ $title = sprintf(' [%s] ', get_class($e));
+
+ $len = $this->stringWidth($title);
+
+ $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
+
+ if (defined('HHVM_VERSION') && $width > 1 << 31) {
+ $width = 1 << 31;
+ }
+ $lines = [];
+ foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
+ foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
+
+ $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4;
+ $lines[] = [$line, $lineLength];
+
+ $len = max($lineLength, $len);
+ }
+ }
+
+ $messages = ['', ''];
+ $messages[] = $emptyLine = sprintf('%s ', str_repeat(' ', $len));
+ $messages[] = sprintf('%s%s ', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))));
+ foreach ($lines as $line) {
+ $messages[] = sprintf(' %s %s ', $line[0], str_repeat(' ', $len - $line[1]));
+ }
+ $messages[] = $emptyLine;
+ $messages[] = '';
+ $messages[] = '';
+
+ $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr);
+
+ if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) {
+ $this->write('Exception trace: ', true, Output::OUTPUT_NORMAL, $stderr);
+
+ // exception related properties
+ $trace = $e->getTrace();
+ array_unshift($trace, [
+ 'function' => '',
+ 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a',
+ 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a',
+ 'args' => [],
+ ]);
+
+ for ($i = 0, $count = count($trace); $i < $count; ++$i) {
+ $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
+ $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
+ $function = $trace[$i]['function'];
+ $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
+ $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
+
+ $this->write(sprintf(' %s%s%s() at %s:%s ', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr);
+ }
+
+ $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+ $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+ }
+ } while ($e = $e->getPrevious());
+
+ }
+
+ /**
+ * 获取终端宽度
+ * @return int|null
+ */
+ protected function getTerminalWidth()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[0];
+ }
+
+ /**
+ * 获取终端高度
+ * @return int|null
+ */
+ protected function getTerminalHeight()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[1];
+ }
+
+ /**
+ * 获取当前终端的尺寸
+ * @return array
+ */
+ public function getTerminalDimensions(): array
+ {
+ if ($this->terminalDimensions) {
+ return $this->terminalDimensions;
+ }
+
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ }
+
+ if ($sttyString = $this->getSttyColumns()) {
+ if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ }
+
+ return [null, null];
+ }
+
+ /**
+ * 获取stty列数
+ * @return string
+ */
+ private function getSttyColumns()
+ {
+ if (!function_exists('proc_open')) {
+ return;
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ return $info;
+ }
+ return;
+ }
+
+ /**
+ * 获取终端模式
+ * @return string x 或 null
+ */
+ private function getMode()
+ {
+ if (!function_exists('proc_open')) {
+ return;
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
+ return $matches[2] . 'x' . $matches[1];
+ }
+ }
+ return;
+ }
+
+ private function stringWidth(string $string): int
+ {
+ if (!function_exists('mb_strwidth')) {
+ return strlen($string);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return strlen($string);
+ }
+
+ return mb_strwidth($string, $encoding);
+ }
+
+ private function splitStringByWidth(string $string, int $width): array
+ {
+ if (!function_exists('mb_strwidth')) {
+ return str_split($string, $width);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return str_split($string, $width);
+ }
+
+ $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+ $lines = [];
+ $line = '';
+ foreach (preg_split('//u', $utf8String) as $char) {
+ if (mb_strwidth($line . $char, 'utf8') <= $width) {
+ $line .= $char;
+ continue;
+ }
+ $lines[] = str_pad($line, $width);
+ $line = $char;
+ }
+ if (strlen($line)) {
+ $lines[] = count($lines) ? str_pad($line, $width) : $line;
+ }
+
+ mb_convert_variables($encoding, 'utf8', $lines);
+
+ return $lines;
+ }
+
+ private function isRunningOS400(): bool
+ {
+ $checks = [
+ function_exists('php_uname') ? php_uname('s') : '',
+ getenv('OSTYPE'),
+ PHP_OS,
+ ];
+ return false !== stripos(implode(';', $checks), 'OS400');
+ }
+
+ /**
+ * 当前环境是否支持写入控制台输出到stdout.
+ *
+ * @return bool
+ */
+ protected function hasStdoutSupport(): bool
+ {
+ return false === $this->isRunningOS400();
+ }
+
+ /**
+ * 当前环境是否支持写入控制台输出到stderr.
+ *
+ * @return bool
+ */
+ protected function hasStderrSupport(): bool
+ {
+ return false === $this->isRunningOS400();
+ }
+
+ /**
+ * @return resource
+ */
+ private function openOutputStream()
+ {
+ if (!$this->hasStdoutSupport()) {
+ return fopen('php://output', 'w');
+ }
+ return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
+ }
+
+ /**
+ * @return resource
+ */
+ private function openErrorStream()
+ {
+ return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
+ }
+
+ /**
+ * 将消息写入到输出。
+ * @param string $message 消息
+ * @param bool $newline 是否另起一行
+ * @param null $stream
+ */
+ protected function doWrite($message, $newline, $stream = null)
+ {
+ if (null === $stream) {
+ $stream = $this->stdout;
+ }
+ if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) {
+ throw new \RuntimeException('Unable to write output.');
+ }
+
+ fflush($stream);
+ }
+
+ /**
+ * 是否支持着色
+ * @param $stream
+ * @return bool
+ */
+ protected function hasColorSupport($stream): bool
+ {
+ if (DIRECTORY_SEPARATOR === '\\') {
+ return
+ '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD
+ || false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ || 'xterm' === getenv('TERM');
+ }
+
+ return function_exists('posix_isatty') && @posix_isatty($stream);
+ }
+
+}
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..28189de
--- /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/console/output/formatter/Stack.php b/vendor/topthink/framework/src/think/console/output/formatter/Stack.php
new file mode 100644
index 0000000..823d337
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/formatter/Stack.php
@@ -0,0 +1,116 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Stack
+{
+
+ /**
+ * @var Style[]
+ */
+ private $styles;
+
+ /**
+ * @var Style
+ */
+ private $emptyStyle;
+
+ /**
+ * 构造方法
+ * @param Style|null $emptyStyle
+ */
+ public function __construct(Style $emptyStyle = null)
+ {
+ $this->emptyStyle = $emptyStyle ?: new Style();
+ $this->reset();
+ }
+
+ /**
+ * 重置堆栈
+ */
+ public function reset(): void
+ {
+ $this->styles = [];
+ }
+
+ /**
+ * 推一个样式进入堆栈
+ * @param Style $style
+ */
+ public function push(Style $style): void
+ {
+ $this->styles[] = $style;
+ }
+
+ /**
+ * 从堆栈中弹出一个样式
+ * @param Style|null $style
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function pop(Style $style = null): Style
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ if (null === $style) {
+ return array_pop($this->styles);
+ }
+
+ /**
+ * @var int $index
+ * @var Style $stackedStyle
+ */
+ foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
+ if ($style->apply('') === $stackedStyle->apply('')) {
+ $this->styles = array_slice($this->styles, 0, $index);
+
+ return $stackedStyle;
+ }
+ }
+
+ throw new \InvalidArgumentException('Incorrectly nested style tag found.');
+ }
+
+ /**
+ * 计算堆栈的当前样式。
+ * @return Style
+ */
+ public function getCurrent(): Style
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ return $this->styles[count($this->styles) - 1];
+ }
+
+ /**
+ * @param Style $emptyStyle
+ * @return Stack
+ */
+ public function setEmptyStyle(Style $emptyStyle)
+ {
+ $this->emptyStyle = $emptyStyle;
+
+ return $this;
+ }
+
+ /**
+ * @return Style
+ */
+ public function getEmptyStyle(): Style
+ {
+ return $this->emptyStyle;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/formatter/Style.php b/vendor/topthink/framework/src/think/console/output/formatter/Style.php
new file mode 100644
index 0000000..e1ca5e5
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/formatter/Style.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Style
+{
+ protected static $availableForegroundColors = [
+ 'black' => ['set' => 30, 'unset' => 39],
+ 'red' => ['set' => 31, 'unset' => 39],
+ 'green' => ['set' => 32, 'unset' => 39],
+ 'yellow' => ['set' => 33, 'unset' => 39],
+ 'blue' => ['set' => 34, 'unset' => 39],
+ 'magenta' => ['set' => 35, 'unset' => 39],
+ 'cyan' => ['set' => 36, 'unset' => 39],
+ 'white' => ['set' => 37, 'unset' => 39],
+ ];
+
+ protected static $availableBackgroundColors = [
+ 'black' => ['set' => 40, 'unset' => 49],
+ 'red' => ['set' => 41, 'unset' => 49],
+ 'green' => ['set' => 42, 'unset' => 49],
+ 'yellow' => ['set' => 43, 'unset' => 49],
+ 'blue' => ['set' => 44, 'unset' => 49],
+ 'magenta' => ['set' => 45, 'unset' => 49],
+ 'cyan' => ['set' => 46, 'unset' => 49],
+ 'white' => ['set' => 47, 'unset' => 49],
+ ];
+
+ protected static $availableOptions = [
+ 'bold' => ['set' => 1, 'unset' => 22],
+ 'underscore' => ['set' => 4, 'unset' => 24],
+ 'blink' => ['set' => 5, 'unset' => 25],
+ 'reverse' => ['set' => 7, 'unset' => 27],
+ 'conceal' => ['set' => 8, 'unset' => 28],
+ ];
+
+ private $foreground;
+ private $background;
+ private $options = [];
+
+ /**
+ * 初始化输出的样式
+ * @param string|null $foreground 字体颜色
+ * @param string|null $background 背景色
+ * @param array $options 格式
+ * @api
+ */
+ public function __construct($foreground = null, $background = null, array $options = [])
+ {
+ if (null !== $foreground) {
+ $this->setForeground($foreground);
+ }
+ if (null !== $background) {
+ $this->setBackground($background);
+ }
+ if (count($options)) {
+ $this->setOptions($options);
+ }
+ }
+
+ /**
+ * 设置字体颜色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setForeground($color = null)
+ {
+ if (null === $color) {
+ $this->foreground = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableForegroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors))));
+ }
+
+ $this->foreground = static::$availableForegroundColors[$color];
+ }
+
+ /**
+ * 设置背景色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setBackground($color = null)
+ {
+ if (null === $color) {
+ $this->background = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableBackgroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
+ }
+
+ $this->background = static::$availableBackgroundColors[$color];
+ }
+
+ /**
+ * 设置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException When the option name isn't defined
+ * @api
+ */
+ public function setOption(string $option): void
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ if (!in_array(static::$availableOptions[$option], $this->options)) {
+ $this->options[] = static::$availableOptions[$option];
+ }
+ }
+
+ /**
+ * 重置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException
+ */
+ public function unsetOption(string $option): void
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ $pos = array_search(static::$availableOptions[$option], $this->options);
+ if (false !== $pos) {
+ unset($this->options[$pos]);
+ }
+ }
+
+ /**
+ * 批量设置字体格式
+ * @param array $options
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = [];
+
+ foreach ($options as $option) {
+ $this->setOption($option);
+ }
+ }
+
+ /**
+ * 应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ public function apply(string $text): string
+ {
+ $setCodes = [];
+ $unsetCodes = [];
+
+ if (null !== $this->foreground) {
+ $setCodes[] = $this->foreground['set'];
+ $unsetCodes[] = $this->foreground['unset'];
+ }
+ if (null !== $this->background) {
+ $setCodes[] = $this->background['set'];
+ $unsetCodes[] = $this->background['unset'];
+ }
+ if (count($this->options)) {
+ foreach ($this->options as $option) {
+ $setCodes[] = $option['set'];
+ $unsetCodes[] = $option['unset'];
+ }
+ }
+
+ if (0 === count($setCodes)) {
+ return $text;
+ }
+
+ return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/question/Choice.php b/vendor/topthink/framework/src/think/console/output/question/Choice.php
new file mode 100644
index 0000000..b9217b0
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/question/Choice.php
@@ -0,0 +1,163 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Choice extends Question
+{
+
+ private $choices;
+ private $multiselect = false;
+ private $prompt = ' > ';
+ private $errorMessage = 'Value "%s" is invalid';
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param array $choices 选项
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, array $choices, $default = null)
+ {
+ parent::__construct($question, $default);
+
+ $this->choices = $choices;
+ $this->setValidator($this->getDefaultValidator());
+ $this->setAutocompleterValues($choices);
+ }
+
+ /**
+ * 可选项
+ * @return array
+ */
+ public function getChoices(): array
+ {
+ return $this->choices;
+ }
+
+ /**
+ * 设置可否多选
+ * @param bool $multiselect
+ * @return self
+ */
+ public function setMultiselect(bool $multiselect)
+ {
+ $this->multiselect = $multiselect;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ public function isMultiselect(): bool
+ {
+ return $this->multiselect;
+ }
+
+ /**
+ * 获取提示
+ * @return string
+ */
+ public function getPrompt(): string
+ {
+ return $this->prompt;
+ }
+
+ /**
+ * 设置提示
+ * @param string $prompt
+ * @return self
+ */
+ public function setPrompt(string $prompt)
+ {
+ $this->prompt = $prompt;
+
+ return $this;
+ }
+
+ /**
+ * 设置错误提示信息
+ * @param string $errorMessage
+ * @return self
+ */
+ public function setErrorMessage(string $errorMessage)
+ {
+ $this->errorMessage = $errorMessage;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ /**
+ * 获取默认的验证方法
+ * @return callable
+ */
+ private function getDefaultValidator()
+ {
+ $choices = $this->choices;
+ $errorMessage = $this->errorMessage;
+ $multiselect = $this->multiselect;
+ $isAssoc = $this->isAssoc($choices);
+
+ return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
+ // Collapse all spaces.
+ $selectedChoices = str_replace(' ', '', $selected);
+
+ if ($multiselect) {
+ // Check for a separated comma values
+ if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $selected));
+ }
+ $selectedChoices = explode(',', $selectedChoices);
+ } else {
+ $selectedChoices = [$selected];
+ }
+
+ $multiselectChoices = [];
+ foreach ($selectedChoices as $value) {
+ $results = [];
+ foreach ($choices as $key => $choice) {
+ if ($choice === $value) {
+ $results[] = $key;
+ }
+ }
+
+ if (count($results) > 1) {
+ throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results)));
+ }
+
+ $result = array_search($value, $choices);
+
+ if (!$isAssoc) {
+ if (!empty($result)) {
+ $result = $choices[$result];
+ } elseif (isset($choices[$value])) {
+ $result = $choices[$value];
+ }
+ } elseif (empty($result) && array_key_exists($value, $choices)) {
+ $result = $value;
+ }
+
+ if (false === $result) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $value));
+ }
+ array_push($multiselectChoices, $result);
+ }
+
+ if ($multiselect) {
+ return $multiselectChoices;
+ }
+
+ return current($multiselectChoices);
+ };
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/question/Confirmation.php b/vendor/topthink/framework/src/think/console/output/question/Confirmation.php
new file mode 100644
index 0000000..3be939d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/question/Confirmation.php
@@ -0,0 +1,57 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Confirmation extends Question
+{
+
+ private $trueAnswerRegex;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param bool $default 默认答案
+ * @param string $trueAnswerRegex 验证正则
+ */
+ public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i')
+ {
+ parent::__construct($question, (bool) $default);
+
+ $this->trueAnswerRegex = $trueAnswerRegex;
+ $this->setNormalizer($this->getDefaultNormalizer());
+ }
+
+ /**
+ * 获取默认的答案回调
+ * @return callable
+ */
+ private function getDefaultNormalizer()
+ {
+ $default = $this->getDefault();
+ $regex = $this->trueAnswerRegex;
+
+ return function ($answer) use ($default, $regex) {
+ if (is_bool($answer)) {
+ return $answer;
+ }
+
+ $answerIsTrue = (bool) preg_match($regex, $answer);
+ if (false === $default) {
+ return $answer && $answerIsTrue;
+ }
+
+ return !$answer || $answerIsTrue;
+ };
+ }
+}
diff --git a/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php b/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php
new file mode 100644
index 0000000..b0a4cdf
--- /dev/null
+++ b/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php
@@ -0,0 +1,88 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+/**
+ * 缓存驱动接口
+ */
+interface CacheHandlerInterface
+{
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name);
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null);
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|\DateTime $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null);
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc(string $name, int $step = 1);
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec(string $name, int $step = 1);
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name);
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear();
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys);
+
+}
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..655a75b
--- /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..e3642e7
--- /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/contract/SessionHandlerInterface.php b/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php
new file mode 100644
index 0000000..2586b16
--- /dev/null
+++ b/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+/**
+ * Session驱动接口
+ */
+interface SessionHandlerInterface
+{
+ public function read(string $sessionId): string;
+ public function delete(string $sessionId): bool;
+ public function write(string $sessionId, string $data): bool;
+}
diff --git a/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php b/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php
new file mode 100644
index 0000000..b2d1479
--- /dev/null
+++ b/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php
@@ -0,0 +1,61 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+/**
+ * 视图驱动接口
+ */
+interface TemplateHandlerInterface
+{
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool;
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void;
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void;
+
+ /**
+ * 配置模板引擎
+ * @access private
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void;
+
+ /**
+ * 获取模板引擎配置
+ * @access public
+ * @param string $name 参数名
+ * @return void
+ */
+ public function getConfig(string $name);
+}
diff --git a/vendor/topthink/framework/src/think/event/AppInit.php b/vendor/topthink/framework/src/think/event/AppInit.php
new file mode 100644
index 0000000..f851934
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/AppInit.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\event;
+
+/**
+ * AppInit事件类
+ */
+class AppInit
+{}
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..15309ec
--- /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..6218bb0
--- /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/LogWrite.php b/vendor/topthink/framework/src/think/event/LogWrite.php
new file mode 100644
index 0000000..c4a0f3d
--- /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/event/RouteLoaded.php b/vendor/topthink/framework/src/think/event/RouteLoaded.php
new file mode 100644
index 0000000..f9fac39
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/RouteLoaded.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\event;
+
+/**
+ * 路由加载完成事件
+ */
+class RouteLoaded
+{
+
+}
diff --git a/vendor/topthink/framework/src/think/exception/ClassNotFoundException.php b/vendor/topthink/framework/src/think/exception/ClassNotFoundException.php
new file mode 100644
index 0000000..5f380d8
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/ClassNotFoundException.php
@@ -0,0 +1,39 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+use Psr\Container\NotFoundExceptionInterface;
+use RuntimeException;
+use Throwable;
+
+class ClassNotFoundException extends RuntimeException implements NotFoundExceptionInterface
+{
+ protected $class;
+
+ public function __construct(string $message, string $class = '', Throwable $previous = null)
+ {
+ $this->message = $message;
+ $this->class = $class;
+
+ parent::__construct($message, 0, $previous);
+ }
+
+ /**
+ * 获取类名
+ * @access public
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+}
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..92e41a1
--- /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..6834970
--- /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..8114673
--- /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..90cd1c1
--- /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..88c3422
--- /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..c39b0f9
--- /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..ce7eeb9
--- /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/exception/RouteNotFoundException.php b/vendor/topthink/framework/src/think/exception/RouteNotFoundException.php
new file mode 100644
index 0000000..80b9b26
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/RouteNotFoundException.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+/**
+ * 路由未定义异常
+ */
+class RouteNotFoundException extends HttpException
+{
+
+ public function __construct()
+ {
+ parent::__construct(404, 'Route Not Found');
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/exception/ValidateException.php b/vendor/topthink/framework/src/think/exception/ValidateException.php
new file mode 100644
index 0000000..b6f82b9
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/ValidateException.php
@@ -0,0 +1,37 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+/**
+ * 数据验证异常
+ */
+class ValidateException extends \RuntimeException
+{
+ protected $error;
+
+ public function __construct($error)
+ {
+ $this->error = $error;
+ $this->message = is_array($error) ? implode(PHP_EOL, $error) : $error;
+ }
+
+ /**
+ * 获取验证错误信息
+ * @access public
+ * @return array|string
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/App.php b/vendor/topthink/framework/src/think/facade/App.php
new file mode 100644
index 0000000..849774b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/App.php
@@ -0,0 +1,59 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\App
+ * @package think\facade
+ * @mixin \think\App
+ * @method static \think\Service|null register(\think\Service|string $service, bool $force = false) 注册服务
+ * @method static mixed bootService(\think\Service $service) 执行服务
+ * @method static \think\Service|null getService(string|\think\Service $service) 获取服务
+ * @method static \think\App debug(bool $debug = true) 开启应用调试模式
+ * @method static bool isDebug() 是否为调试模式
+ * @method static \think\App setNamespace(string $namespace) 设置应用命名空间
+ * @method static string getNamespace() 获取应用类库命名空间
+ * @method static string version() 获取框架版本
+ * @method static string getRootPath() 获取应用根目录
+ * @method static string getBasePath() 获取应用基础目录
+ * @method static string getAppPath() 获取当前应用目录
+ * @method static mixed setAppPath(string $path) 设置应用目录
+ * @method static string getRuntimePath() 获取应用运行时目录
+ * @method static void setRuntimePath(string $path) 设置runtime目录
+ * @method static string getThinkPath() 获取核心框架目录
+ * @method static string getConfigPath() 获取应用配置目录
+ * @method static string getConfigExt() 获取配置后缀
+ * @method static float getBeginTime() 获取应用开启时间
+ * @method static integer getBeginMem() 获取应用初始内存占用
+ * @method static \think\App initialize() 初始化应用
+ * @method static bool initialized() 是否初始化过
+ * @method static void loadLangPack(string $langset) 加载语言包
+ * @method static void boot() 引导应用
+ * @method static void loadEvent(array $event) 注册应用事件
+ * @method static string parseClass(string $layer, string $name) 解析应用类的类名
+ * @method static bool runningInConsole() 是否运行在命令行下
+ */
+class App extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'app';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Cache.php b/vendor/topthink/framework/src/think/facade/Cache.php
new file mode 100644
index 0000000..993bc78
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Cache.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\cache\Driver;
+use think\cache\TagSet;
+
+/**
+ * @see \think\Cache
+ * @package think\facade
+ * @mixin \think\Cache
+ * @method static string|null getDefaultDriver() 默认驱动
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取缓存配置
+ * @method static array getStoreConfig(string $store, string $name = null, null $default = null) 获取驱动配置
+ * @method static Driver store(string $name = null) 连接或者切换缓存
+ * @method static bool clear() 清空缓冲池
+ * @method static mixed get(string $key, mixed $default = null) 读取缓存
+ * @method static bool set(string $key, mixed $value, int|\DateTime $ttl = null) 写入缓存
+ * @method static bool delete(string $key) 删除缓存
+ * @method static iterable getMultiple(iterable $keys, mixed $default = null) 读取缓存
+ * @method static bool setMultiple(iterable $values, null|int|\DateInterval $ttl = null) 写入缓存
+ * @method static bool deleteMultiple(iterable $keys) 删除缓存
+ * @method static bool has(string $key) 判断缓存是否存在
+ * @method static TagSet tag(string|array $name) 缓存标签
+ */
+class Cache extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'cache';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Config.php b/vendor/topthink/framework/src/think/facade/Config.php
new file mode 100644
index 0000000..6202d47
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Config.php
@@ -0,0 +1,37 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Config
+ * @package think\facade
+ * @mixin \think\Config
+ * @method static array load(string $file, string $name = '') 加载配置文件(多种格式)
+ * @method static bool has(string $name) 检测配置是否存在
+ * @method static mixed get(string $name = null, mixed $default = null) 获取配置参数 为空则获取所有配置
+ * @method static array set(array $config, string $name = null) 设置配置参数 name为数组则为批量设置
+ */
+class Config extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'config';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Console.php b/vendor/topthink/framework/src/think/facade/Console.php
new file mode 100644
index 0000000..9401316
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Console.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Definition as InputDefinition;
+use think\console\Output;
+use think\console\output\driver\Buffer;
+
+/**
+ * Class Console
+ * @package think\facade
+ * @mixin \think\Console
+ * @method static Output|Buffer call(string $command, array $parameters = [], string $driver = 'buffer')
+ * @method static int run() 执行当前的指令
+ * @method static int doRun(Input $input, Output $output) 执行指令
+ * @method static void setDefinition(InputDefinition $definition) 设置输入参数定义
+ * @method static InputDefinition The InputDefinition instance getDefinition() 获取输入参数定义
+ * @method static string A help message. getHelp() Gets the help message.
+ * @method static void setCatchExceptions(bool $boolean) 是否捕获异常
+ * @method static void setAutoExit(bool $boolean) 是否自动退出
+ * @method static string getLongVersion() 获取完整的版本号
+ * @method static void addCommands(array $commands) 添加指令集
+ * @method static Command|void addCommand(string|Command $command, string $name = '') 添加一个指令
+ * @method static Command getCommand(string $name) 获取指令
+ * @method static bool hasCommand(string $name) 某个指令是否存在
+ * @method static array getNamespaces() 获取所有的命名空间
+ * @method static string findNamespace(string $namespace) 查找注册命名空间中的名称或缩写。
+ * @method static Command find(string $name) 查找指令
+ * @method static Command[] all(string $namespace = null) 获取所有的指令
+ * @method static string extractNamespace(string $name, int $limit = 0) 返回命名空间部分
+ */
+class Console extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'console';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Cookie.php b/vendor/topthink/framework/src/think/facade/Cookie.php
new file mode 100644
index 0000000..95b0406
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Cookie.php
@@ -0,0 +1,40 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Cookie
+ * @package think\facade
+ * @mixin \think\Cookie
+ * @method static mixed get(mixed $name = '', string $default = null) 获取cookie
+ * @method static bool has(string $name) 是否存在Cookie参数
+ * @method static void set(string $name, string $value, mixed $option = null) Cookie 设置
+ * @method static void forever(string $name, string $value = '', mixed $option = null) 永久保存Cookie数据
+ * @method static void delete(string $name) Cookie删除
+ * @method static array getCookie() 获取cookie保存数据
+ * @method static void save() 保存Cookie
+ */
+class Cookie extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'cookie';
+ }
+}
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..ba088b6
--- /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..afb7805
--- /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/Filesystem.php b/vendor/topthink/framework/src/think/facade/Filesystem.php
new file mode 100644
index 0000000..005d01b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/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 'filesystem';
+ }
+}
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..7c7e42f
--- /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..5ca1655
--- /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..e0d4e0a
--- /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/facade/Request.php b/vendor/topthink/framework/src/think/facade/Request.php
new file mode 100644
index 0000000..1d126f0
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Request.php
@@ -0,0 +1,134 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\file\UploadedFile;
+use think\route\Rule;
+
+/**
+ * @see \think\Request
+ * @package think\facade
+ * @mixin \think\Request
+ * @method static \think\Request setDomain(string $domain) 设置当前包含协议的域名
+ * @method static string domain(bool $port = false) 获取当前包含协议的域名
+ * @method static string rootDomain() 获取当前根域名
+ * @method static \think\Request setSubDomain(string $domain) 设置当前泛域名的值
+ * @method static string subDomain() 获取当前子域名
+ * @method static \think\Request setPanDomain(string $domain) 设置当前泛域名的值
+ * @method static string panDomain() 获取当前泛域名的值
+ * @method static \think\Request setUrl(string $url) 设置当前完整URL 包括QUERY_STRING
+ * @method static string url(bool $complete = false) 获取当前完整URL 包括QUERY_STRING
+ * @method static \think\Request setBaseUrl(string $url) 设置当前URL 不含QUERY_STRING
+ * @method static string baseUrl(bool $complete = false) 获取当前URL 不含QUERY_STRING
+ * @method static string baseFile(bool $complete = false) 获取当前执行的文件 SCRIPT_NAME
+ * @method static \think\Request setRoot(string $url) 设置URL访问根地址
+ * @method static string root(bool $complete = false) 获取URL访问根地址
+ * @method static string rootUrl() 获取URL访问根目录
+ * @method static \think\Request setPathinfo(string $pathinfo) 设置当前请求的pathinfo
+ * @method static string pathinfo() 获取当前请求URL的pathinfo信息(含URL后缀)
+ * @method static string ext() 当前URL的访问后缀
+ * @method static integer|float time(bool $float = false) 获取当前请求的时间
+ * @method static string type() 当前请求的资源类型
+ * @method static void mimeType(string|array $type, string $val = '') 设置资源类型
+ * @method static \think\Request setMethod(string $method) 设置请求类型
+ * @method static string method(bool $origin = false) 当前的请求类型
+ * @method static bool isGet() 是否为GET请求
+ * @method static bool isPost() 是否为POST请求
+ * @method static bool isPut() 是否为PUT请求
+ * @method static bool isDelete() 是否为DELTE请求
+ * @method static bool isHead() 是否为HEAD请求
+ * @method static bool isPatch() 是否为PATCH请求
+ * @method static bool isOptions() 是否为OPTIONS请求
+ * @method static bool isCli() 是否为cli
+ * @method static bool isCgi() 是否为cgi
+ * @method static mixed param(string|array $name = '', mixed $default = null, string|array $filter = '') 获取当前请求的参数
+ * @method static \think\Request setRule(Rule $rule) 设置路由变量
+ * @method static Rule|null rule() 获取当前路由对象
+ * @method static \think\Request setRoute(array $route) 设置路由变量
+ * @method static mixed route(string|array $name = '', mixed $default = null, string|array $filter = '') 获取路由参数
+ * @method static mixed get(string|array $name = '', mixed $default = null, string|array $filter = '') 获取GET参数
+ * @method static mixed middleware(mixed $name, mixed $default = null) 获取中间件传递的参数
+ * @method static mixed post(string|array $name = '', mixed $default = null, string|array $filter = '') 获取POST参数
+ * @method static mixed put(string|array $name = '', mixed $default = null, string|array $filter = '') 获取PUT参数
+ * @method static mixed delete(mixed $name = '', mixed $default = null, string|array $filter = '') 设置获取DELETE参数
+ * @method static mixed patch(mixed $name = '', mixed $default = null, string|array $filter = '') 设置获取PATCH参数
+ * @method static mixed request(string|array $name = '', mixed $default = null, string|array $filter = '') 获取request变量
+ * @method static mixed env(string $name = '', string $default = null) 获取环境变量
+ * @method static mixed session(string $name = '', string $default = null) 获取session数据
+ * @method static mixed cookie(mixed $name = '', string $default = null, string|array $filter = '') 获取cookie参数
+ * @method static mixed server(string $name = '', string $default = '') 获取server参数
+ * @method static null|array|UploadedFile file(string $name = '') 获取上传的文件信息
+ * @method static string|array header(string $name = '', string $default = null) 设置或者获取当前的Header
+ * @method static mixed input(array $data = [], string|false $name = '', mixed $default = null, string|array $filter = '') 获取变量 支持过滤和默认值
+ * @method static mixed filter(mixed $filter = null) 设置或获取当前的过滤规则
+ * @method static mixed filterValue(mixed &$value, mixed $key, array $filters) 递归过滤给定的值
+ * @method static bool has(string $name, string $type = 'param', bool $checkEmpty = false) 是否存在某个请求参数
+ * @method static array only(array $name, mixed $data = 'param', string|array $filter = '') 获取指定的参数
+ * @method static mixed except(array $name, string $type = 'param') 排除指定参数获取
+ * @method static bool isSsl() 当前是否ssl
+ * @method static bool isJson() 当前是否JSON请求
+ * @method static bool isAjax(bool $ajax = false) 当前是否Ajax请求
+ * @method static bool isPjax(bool $pjax = false) 当前是否Pjax请求
+ * @method static string ip() 获取客户端IP地址
+ * @method static boolean isValidIP(string $ip, string $type = '') 检测是否是合法的IP地址
+ * @method static string ip2bin(string $ip) 将IP地址转换为二进制字符串
+ * @method static bool isMobile() 检测是否使用手机访问
+ * @method static string scheme() 当前URL地址中的scheme参数
+ * @method static string query() 当前请求URL地址中的query参数
+ * @method static \think\Request setHost(string $host) 设置当前请求的host(包含端口)
+ * @method static string host(bool $strict = false) 当前请求的host
+ * @method static int port() 当前请求URL地址中的port参数
+ * @method static string protocol() 当前请求 SERVER_PROTOCOL
+ * @method static int remotePort() 当前请求 REMOTE_PORT
+ * @method static string contentType() 当前请求 HTTP_CONTENT_TYPE
+ * @method static string secureKey() 获取当前请求的安全Key
+ * @method static \think\Request setController(string $controller) 设置当前的控制器名
+ * @method static \think\Request setAction(string $action) 设置当前的操作名
+ * @method static string controller(bool $convert = false) 获取当前的控制器名
+ * @method static string action(bool $convert = false) 获取当前的操作名
+ * @method static string getContent() 设置或者获取当前请求的content
+ * @method static string getInput() 获取当前请求的php://input
+ * @method static string buildToken(string $name = '__token__', mixed $type = 'md5') 生成请求令牌
+ * @method static bool checkToken(string $token = '__token__', array $data = []) 检查请求令牌
+ * @method static \think\Request withMiddleware(array $middleware) 设置在中间件传递的数据
+ * @method static \think\Request withGet(array $get) 设置GET数据
+ * @method static \think\Request withPost(array $post) 设置POST数据
+ * @method static \think\Request withCookie(array $cookie) 设置COOKIE数据
+ * @method static \think\Request withSession(Session $session) 设置SESSION数据
+ * @method static \think\Request withServer(array $server) 设置SERVER数据
+ * @method static \think\Request withHeader(array $header) 设置HEADER数据
+ * @method static \think\Request withEnv(Env $env) 设置ENV数据
+ * @method static \think\Request withInput(string $input) 设置php://input数据
+ * @method static \think\Request withFiles(array $files) 设置文件上传数据
+ * @method static \think\Request withRoute(array $route) 设置ROUTE变量
+ * @method static mixed __set(string $name, mixed $value) 设置中间传递数据
+ * @method static mixed __get(string $name) 获取中间传递数据的值
+ * @method static boolean __isset(string $name) 检测中间传递数据的值
+ * @method static bool offsetExists($name)
+ * @method static mixed offsetGet($name)
+ * @method static mixed offsetSet($name, $value)
+ * @method static mixed offsetUnset($name)
+ */
+class Request extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'request';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Route.php b/vendor/topthink/framework/src/think/facade/Route.php
new file mode 100644
index 0000000..8e9d457
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Route.php
@@ -0,0 +1,83 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\route\Dispatch;
+use think\route\Domain;
+use think\route\Rule;
+use think\route\RuleGroup;
+use think\route\RuleItem;
+use think\route\RuleName;
+use think\route\Url as UrlBuild;
+
+/**
+ * @see \think\Route
+ * @package think\facade
+ * @mixin \think\Route
+ * @method static mixed config(string $name = null)
+ * @method static \think\Route lazy(bool $lazy = true) 设置路由域名及分组(包括资源路由)是否延迟解析
+ * @method static void setTestMode(bool $test) 设置路由为测试模式
+ * @method static bool isTest() 检查路由是否为测试模式
+ * @method static \think\Route mergeRuleRegex(bool $merge = true) 设置路由域名及分组(包括资源路由)是否合并解析
+ * @method static void setGroup(RuleGroup $group) 设置当前分组
+ * @method static RuleGroup getGroup(string $name = null) 获取指定标识的路由分组 不指定则获取当前分组
+ * @method static \think\Route pattern(array $pattern) 注册变量规则
+ * @method static \think\Route option(array $option) 注册路由参数
+ * @method static Domain domain(string|array $name, mixed $rule = null) 注册域名路由
+ * @method static array getDomains() 获取域名
+ * @method static RuleName getRuleName() 获取RuleName对象
+ * @method static \think\Route bind(string $bind, string $domain = null) 设置路由绑定
+ * @method static array getBind() 读取路由绑定信息
+ * @method static string|null getDomainBind(string $domain = null) 读取路由绑定
+ * @method static RuleItem[] getName(string $name = null, string $domain = null, string $method = '*') 读取路由标识
+ * @method static void import(array $name) 批量导入路由标识
+ * @method static void setName(string $name, RuleItem $ruleItem, bool $first = false) 注册路由标识
+ * @method static void setRule(string $rule, RuleItem $ruleItem = null) 保存路由规则
+ * @method static RuleItem[] getRule(string $rule) 读取路由
+ * @method static array getRuleList() 读取路由列表
+ * @method static void clear() 清空路由规则
+ * @method static RuleItem rule(string $rule, mixed $route = null, string $method = '*') 注册路由规则
+ * @method static \think\Route setCrossDomainRule(Rule $rule, string $method = '*') 设置跨域有效路由规则
+ * @method static RuleGroup group(string|\Closure $name, mixed $route = null) 注册路由分组
+ * @method static RuleItem any(string $rule, mixed $route) 注册路由
+ * @method static RuleItem get(string $rule, mixed $route) 注册GET路由
+ * @method static RuleItem post(string $rule, mixed $route) 注册POST路由
+ * @method static RuleItem put(string $rule, mixed $route) 注册PUT路由
+ * @method static RuleItem delete(string $rule, mixed $route) 注册DELETE路由
+ * @method static RuleItem patch(string $rule, mixed $route) 注册PATCH路由
+ * @method static RuleItem options(string $rule, mixed $route) 注册OPTIONS路由
+ * @method static Resource resource(string $rule, string $route) 注册资源路由
+ * @method static RuleItem view(string $rule, string $template = '', array $vars = []) 注册视图路由
+ * @method static RuleItem redirect(string $rule, string $route = '', int $status = 301) 注册重定向路由
+ * @method static \think\Route rest(string|array $name, array|bool $resource = []) rest方法定义和修改
+ * @method static array|null getRest(string $name = null) 获取rest方法定义的参数
+ * @method static RuleItem miss(string|Closure $route, string $method = '*') 注册未匹配路由规则后的处理
+ * @method static Response dispatch(\think\Request $request, Closure|bool $withRoute = true) 路由调度
+ * @method static Dispatch|false check() 检测URL路由
+ * @method static Dispatch url(string $url) 默认URL解析
+ * @method static UrlBuild buildUrl(string $url = '', array $vars = []) URL生成 支持路由反射
+ * @method static RuleGroup __call(string $method, array $args) 设置全局的路由分组参数
+ */
+class Route extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'route';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Session.php b/vendor/topthink/framework/src/think/facade/Session.php
new file mode 100644
index 0000000..38f8563
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Session.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Session
+ * @package think\facade
+ * @mixin \think\Session
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取Session配置
+ * @method static string|null getDefaultDriver() 默认驱动
+ */
+class Session extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'session';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Validate.php b/vendor/topthink/framework/src/think/facade/Validate.php
new file mode 100644
index 0000000..7c21267
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Validate.php
@@ -0,0 +1,95 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Validate
+ * @package think\facade
+ * @mixin \think\Validate
+ * @method static void setLang(\think\Lang $lang) 设置Lang对象
+ * @method static void setDb(\think\Db $db) 设置Db对象
+ * @method static void setRequest(\think\Request $request) 设置Request对象
+ * @method static \think\Validate rule(string|array $name, mixed $rule = '') 添加字段验证规则
+ * @method static \think\Validate extend(string $type, callable $callback = null, string $message = null) 注册验证(类型)规则
+ * @method static void setTypeMsg(string|array $type, string $msg = null) 设置验证规则的默认提示信息
+ * @method static Validate message(array $message) 设置提示信息
+ * @method static \think\Validate scene(string $name) 设置验证场景
+ * @method static bool hasScene(string $name) 判断是否存在某个验证场景
+ * @method static \think\Validate batch(bool $batch = true) 设置批量验证
+ * @method static \think\Validate failException(bool $fail = true) 设置验证失败后是否抛出异常
+ * @method static \think\Validate only(array $fields) 指定需要验证的字段列表
+ * @method static \think\Validate remove(string|array $field, mixed $rule = null) 移除某个字段的验证规则
+ * @method static \think\Validate append(string|array $field, mixed $rule = null) 追加某个字段的验证规则
+ * @method static bool check(array $data, array $rules = []) 数据自动验证
+ * @method static bool checkRule(mixed $value, mixed $rules) 根据验证规则验证数据
+ * @method static bool confirm(mixed $value, mixed $rule, array $data = [], string $field = '') 验证是否和某个字段的值一致
+ * @method static bool different(mixed $value, mixed $rule, array $data = []) 验证是否和某个字段的值是否不同
+ * @method static bool egt(mixed $value, mixed $rule, array $data = []) 验证是否大于等于某个值
+ * @method static bool gt(mixed $value, mixed $rule, array $data = []) 验证是否大于某个值
+ * @method static bool elt(mixed $value, mixed $rule, array $data = []) 验证是否小于等于某个值
+ * @method static bool lt(mixed $value, mixed $rule, array $data = []) 验证是否小于某个值
+ * @method static bool eq(mixed $value, mixed $rule) 验证是否等于某个值
+ * @method static bool must(mixed $value, mixed $rule = null) 必须验证
+ * @method static bool is(mixed $value, string $rule, array $data = []) 验证字段值是否为有效格式
+ * @method static bool token(mixed $value, mixed $rule, array $data) 验证表单令牌
+ * @method static bool activeUrl(mixed $value, mixed $rule = 'MX') 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型
+ * @method static bool ip(mixed $value, mixed $rule = 'ipv4') 验证是否有效IP
+ * @method static bool fileExt(mixed $file, mixed $rule) 验证上传文件后缀
+ * @method static bool fileMime(mixed $file, mixed $rule) 验证上传文件类型
+ * @method static bool fileSize(mixed $file, mixed $rule) 验证上传文件大小
+ * @method static bool image(mixed $file, mixed $rule) 验证图片的宽高及类型
+ * @method static bool dateFormat(mixed $value, mixed $rule) 验证时间和日期是否符合指定格式
+ * @method static bool unique(mixed $value, mixed $rule, array $data = [], string $field = '') 验证是否唯一
+ * @method static bool filter(mixed $value, mixed $rule) 使用filter_var方式验证
+ * @method static bool requireIf(mixed $value, mixed $rule, array $data = []) 验证某个字段等于某个值的时候必须
+ * @method static bool requireCallback(mixed $value, mixed $rule, array $data = []) 通过回调方法验证某个字段是否必须
+ * @method static bool requireWith(mixed $value, mixed $rule, array $data = []) 验证某个字段有值的情况下必须
+ * @method static bool requireWithout(mixed $value, mixed $rule, array $data = []) 验证某个字段没有值的情况下必须
+ * @method static bool in(mixed $value, mixed $rule) 验证是否在范围内
+ * @method static bool notIn(mixed $value, mixed $rule) 验证是否不在某个范围
+ * @method static bool between(mixed $value, mixed $rule) between验证数据
+ * @method static bool notBetween(mixed $value, mixed $rule) 使用notbetween验证数据
+ * @method static bool length(mixed $value, mixed $rule) 验证数据长度
+ * @method static bool max(mixed $value, mixed $rule) 验证数据最大长度
+ * @method static bool min(mixed $value, mixed $rule) 验证数据最小长度
+ * @method static bool after(mixed $value, mixed $rule, array $data = []) 验证日期
+ * @method static bool before(mixed $value, mixed $rule, array $data = []) 验证日期
+ * @method static bool afterWith(mixed $value, mixed $rule, array $data = []) 验证日期
+ * @method static bool beforeWith(mixed $value, mixed $rule, array $data = []) 验证日期
+ * @method static bool expire(mixed $value, mixed $rule) 验证有效期
+ * @method static bool allowIp(mixed $value, mixed $rule) 验证IP许可
+ * @method static bool denyIp(mixed $value, mixed $rule) 验证IP禁用
+ * @method static bool regex(mixed $value, mixed $rule) 使用正则验证数据
+ * @method static array|string getError() 获取错误信息
+ * @method static bool __call(string $method, array $args) 动态方法 直接调用is方法进行验证
+ */
+class Validate extends Facade
+{
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance = true;
+
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'validate';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/View.php b/vendor/topthink/framework/src/think/facade/View.php
new file mode 100644
index 0000000..2f480b7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/View.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\View
+ * @package think\facade
+ * @mixin \think\View
+ * @method static \think\View engine(string $type = null) 获取模板引擎
+ * @method static \think\View assign(string|array $name, mixed $value = null) 模板变量赋值
+ * @method static \think\View filter(\think\Callable $filter = null) 视图过滤
+ * @method static string fetch(string $template = '', array $vars = []) 解析和获取模板内容 用于输出
+ * @method static string display(string $content, array $vars = []) 渲染内容输出
+ * @method static mixed __set(string $name, mixed $value) 模板变量赋值
+ * @method static mixed __get(string $name) 取得模板显示变量的值
+ * @method static bool __isset(string $name) 检测模板变量是否设置
+ * @method static string|null getDefaultDriver() 默认驱动
+ */
+class View extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'view';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/file/UploadedFile.php b/vendor/topthink/framework/src/think/file/UploadedFile.php
new file mode 100644
index 0000000..84259db
--- /dev/null
+++ b/vendor/topthink/framework/src/think/file/UploadedFile.php
@@ -0,0 +1,143 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\file;
+
+use think\exception\FileException;
+use think\File;
+
+class UploadedFile extends File
+{
+
+ private $test = false;
+ private $originalName;
+ private $mimeType;
+ private $error;
+
+ public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false)
+ {
+ $this->originalName = $originalName;
+ $this->mimeType = $mimeType ?: 'application/octet-stream';
+ $this->test = $test;
+ $this->error = $error ?: UPLOAD_ERR_OK;
+
+ parent::__construct($path, UPLOAD_ERR_OK === $this->error);
+ }
+
+ public function isValid(): bool
+ {
+ $isOk = UPLOAD_ERR_OK === $this->error;
+
+ return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
+ }
+
+ /**
+ * 上传文件
+ * @access public
+ * @param string $directory 保存路径
+ * @param string|null $name 保存的文件名
+ * @return File
+ */
+ public function move(string $directory, string $name = null): File
+ {
+ if ($this->isValid()) {
+ if ($this->test) {
+ return parent::move($directory, $name);
+ }
+
+ $target = $this->getTargetFile($directory, $name);
+
+ set_error_handler(function ($type, $msg) use (&$error) {
+ $error = $msg;
+ });
+
+ $moved = move_uploaded_file($this->getPathname(), (string) $target);
+ restore_error_handler();
+ if (!$moved) {
+ 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;
+ }
+
+ throw new FileException($this->getErrorMessage());
+ }
+
+ /**
+ * 获取错误信息
+ * @access public
+ * @return string
+ */
+ protected function getErrorMessage(): string
+ {
+ switch ($this->error) {
+ case 1:
+ case 2:
+ $message = 'upload File size exceeds the maximum value';
+ break;
+ case 3:
+ $message = 'only the portion of file is uploaded';
+ break;
+ case 4:
+ $message = 'no file to uploaded';
+ break;
+ case 6:
+ $message = 'upload temp dir not found';
+ break;
+ case 7:
+ $message = 'file write error';
+ break;
+ default:
+ $message = 'unknown upload error';
+ }
+
+ return $message;
+ }
+
+ /**
+ * 获取上传文件类型信息
+ * @return string
+ */
+ public function getOriginalMime(): string
+ {
+ return $this->mimeType;
+ }
+
+ /**
+ * 上传文件名
+ * @return string
+ */
+ public function getOriginalName(): string
+ {
+ return $this->originalName;
+ }
+
+ /**
+ * 获取上传文件扩展名
+ * @return string
+ */
+ public function getOriginalExtension(): string
+ {
+ return pathinfo($this->originalName, PATHINFO_EXTENSION);
+ }
+
+ /**
+ * 获取文件扩展名
+ * @return string
+ */
+ public function extension(): string
+ {
+ return $this->getOriginalExtension();
+ }
+}
diff --git a/vendor/topthink/framework/src/think/filesystem/CacheStore.php b/vendor/topthink/framework/src/think/filesystem/CacheStore.php
new file mode 100644
index 0000000..be31ec7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/filesystem/CacheStore.php
@@ -0,0 +1,54 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\filesystem;
+
+use League\Flysystem\Cached\Storage\AbstractCache;
+use Psr\SimpleCache\CacheInterface;
+
+class CacheStore extends AbstractCache
+{
+ protected $store;
+
+ protected $key;
+
+ protected $expire;
+
+ public function __construct(CacheInterface $store, $key = 'flysystem', $expire = null)
+ {
+ $this->key = $key;
+ $this->store = $store;
+ $this->expire = $expire;
+ }
+
+ /**
+ * Store the cache.
+ */
+ public function save()
+ {
+ $contents = $this->getForStorage();
+
+ $this->store->set($this->key, $contents, $this->expire);
+ }
+
+ /**
+ * Load the cache.
+ */
+ public function load()
+ {
+ $contents = $this->store->get($this->key);
+
+ if (!is_null($contents)) {
+ $this->setFromStorage($contents);
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/filesystem/Driver.php b/vendor/topthink/framework/src/think/filesystem/Driver.php
new file mode 100644
index 0000000..f56728f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/filesystem/Driver.php
@@ -0,0 +1,133 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\filesystem;
+
+use League\Flysystem\AdapterInterface;
+use League\Flysystem\Adapter\AbstractAdapter;
+use League\Flysystem\Cached\CachedAdapter;
+use League\Flysystem\Cached\Storage\Memory as MemoryStore;
+use League\Flysystem\Filesystem;
+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);
+ }
+
+ protected function createCacheStore($config)
+ {
+ if (true === $config) {
+ return new MemoryStore;
+ }
+
+ return new CacheStore(
+ $this->cache->store($config['store']),
+ $config['prefix'] ?? 'flysystem',
+ $config['expire'] ?? null
+ );
+ }
+
+ abstract protected function createAdapter(): AdapterInterface;
+
+ protected function createFilesystem(AdapterInterface $adapter): Filesystem
+ {
+ if (!empty($this->config['cache'])) {
+ $adapter = new CachedAdapter($adapter, $this->createCacheStore($this->config['cache']));
+ }
+
+ $config = array_intersect_key($this->config, array_flip(['visibility', 'disable_asserts', 'url']));
+
+ return new Filesystem($adapter, count($config) > 0 ? $config : null);
+ }
+
+ /**
+ * 获取文件完整路径
+ * @param string $path
+ * @return string
+ */
+ public function path(string $path): string
+ {
+ $adapter = $this->filesystem->getAdapter();
+
+ if ($adapter instanceof AbstractAdapter) {
+ return $adapter->applyPathPrefix($path);
+ }
+
+ return $path;
+ }
+
+ /**
+ * 保存文件
+ * @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->putStream($path, $stream, $options);
+
+ if (is_resource($stream)) {
+ fclose($stream);
+ }
+
+ return $result ? $path : false;
+ }
+
+ public function __call($method, $parameters)
+ {
+ return $this->filesystem->$method(...$parameters);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/filesystem/driver/Local.php b/vendor/topthink/framework/src/think/filesystem/driver/Local.php
new file mode 100644
index 0000000..250eb64
--- /dev/null
+++ b/vendor/topthink/framework/src/think/filesystem/driver/Local.php
@@ -0,0 +1,41 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\filesystem\driver;
+
+use League\Flysystem\AdapterInterface;
+use League\Flysystem\Adapter\Local as LocalAdapter;
+use think\filesystem\Driver;
+
+class Local extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ 'root' => '',
+ ];
+
+ protected function createAdapter(): AdapterInterface
+ {
+ $permissions = $this->config['permissions'] ?? [];
+
+ $links = ($this->config['links'] ?? null) === 'skip'
+ ? LocalAdapter::SKIP_LINKS
+ : LocalAdapter::DISALLOW_LINKS;
+
+ return new LocalAdapter(
+ $this->config['root'], LOCK_EX, $links, $permissions
+ );
+ }
+}
diff --git a/vendor/topthink/framework/src/think/initializer/BootService.php b/vendor/topthink/framework/src/think/initializer/BootService.php
new file mode 100644
index 0000000..bdca28f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/initializer/BootService.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\initializer;
+
+use think\App;
+
+/**
+ * 启动系统服务
+ */
+class BootService
+{
+ public function init(App $app)
+ {
+ $app->boot();
+ }
+}
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..ae13572
--- /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/initializer/RegisterService.php b/vendor/topthink/framework/src/think/initializer/RegisterService.php
new file mode 100644
index 0000000..95ecd05
--- /dev/null
+++ b/vendor/topthink/framework/src/think/initializer/RegisterService.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\initializer;
+
+use think\App;
+use think\service\ModelService;
+use think\service\PaginatorService;
+use think\service\ValidateService;
+
+/**
+ * 注册系统服务
+ */
+class RegisterService
+{
+
+ protected $services = [
+ PaginatorService::class,
+ ValidateService::class,
+ ModelService::class,
+ ];
+
+ public function init(App $app)
+ {
+ $file = $app->getRootPath() . 'vendor/services.php';
+
+ $services = $this->services;
+
+ if (is_file($file)) {
+ $services = array_merge($services, include $file);
+ }
+
+ foreach ($services as $service) {
+ if (class_exists($service)) {
+ $app->register($service);
+ }
+ }
+ }
+}
diff --git a/vendor/topthink/framework/src/think/log/Channel.php b/vendor/topthink/framework/src/think/log/Channel.php
new file mode 100644
index 0000000..75471e6
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/Channel.php
@@ -0,0 +1,282 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\log;
+
+use Psr\Log\LoggerInterface;
+use think\contract\LogHandlerInterface;
+use think\Event;
+use think\event\LogWrite;
+
+class Channel implements LoggerInterface
+{
+ protected $name;
+ protected $logger;
+ protected $event;
+
+ protected $lazy = true;
+ /**
+ * 日志信息
+ * @var array
+ */
+ protected $log = [];
+
+ /**
+ * 关闭日志
+ * @var array
+ */
+ protected $close = false;
+
+ /**
+ * 允许写入类型
+ * @var array
+ */
+ protected $allow = [];
+
+ public function __construct(string $name, LogHandlerInterface $logger, array $allow, bool $lazy = true, Event $event = null)
+ {
+ $this->name = $name;
+ $this->logger = $logger;
+ $this->allow = $allow;
+ $this->lazy = $lazy;
+ $this->event = $event;
+ }
+
+ /**
+ * 关闭通道
+ */
+ public function close()
+ {
+ $this->clear();
+ $this->close = true;
+ }
+
+ /**
+ * 清空日志
+ */
+ public function clear()
+ {
+ $this->log = [];
+ }
+
+ /**
+ * 记录日志信息
+ * @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)
+ {
+ if ($this->close || (!empty($this->allow) && !in_array($type, $this->allow))) {
+ return $this;
+ }
+
+ if (is_string($msg) && !empty($context)) {
+ $replace = [];
+ foreach ($context as $key => $val) {
+ $replace['{' . $key . '}'] = $val;
+ }
+
+ $msg = strtr($msg, $replace);
+ }
+
+ if (!empty($msg) || 0 === $msg) {
+ $this->log[$type][] = $msg;
+ }
+
+ if (!$this->lazy || !$lazy) {
+ $this->save();
+ }
+
+ 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);
+ }
+
+ /**
+ * 获取日志信息
+ * @return array
+ */
+ public function getLog(): array
+ {
+ return $this->log;
+ }
+
+ /**
+ * 保存日志
+ * @return bool
+ */
+ public function save(): bool
+ {
+ $log = $this->log;
+ if ($this->event) {
+ $event = new LogWrite($this->name, $log);
+ $this->event->trigger($event);
+ $log = $event->log;
+ }
+
+ if ($this->logger->save($log)) {
+ $this->clear();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function emergency($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function alert($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function critical($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function error($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function warning($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function notice($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function info($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function debug($message, array $context = [])
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function log($level, $message, array $context = [])
+ {
+ $this->record($message, $level, $context);
+ }
+
+ public function __call($method, $parameters)
+ {
+ $this->log($method, ...$parameters);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/log/ChannelSet.php b/vendor/topthink/framework/src/think/log/ChannelSet.php
new file mode 100644
index 0000000..4b44180
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/ChannelSet.php
@@ -0,0 +1,39 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\log;
+
+use think\Log;
+
+/**
+ * Class ChannelSet
+ * @package think\log
+ * @mixin Channel
+ */
+class ChannelSet
+{
+ protected $log;
+ protected $channels;
+
+ public function __construct(Log $log, array $channels)
+ {
+ $this->log = $log;
+ $this->channels = $channels;
+ }
+
+ public function __call($method, $arguments)
+ {
+ foreach ($this->channels as $channel) {
+ $this->log->channel($channel)->{$method}(...$arguments);
+ }
+ }
+}
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..3e503bc
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/driver/File.php
@@ -0,0 +1,205 @@
+
+// +----------------------------------------------------------------------
+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']) {
+ unlink($files[0]);
+ }
+ } 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/log/driver/Socket.php b/vendor/topthink/framework/src/think/log/driver/Socket.php
new file mode 100644
index 0000000..9b04d29
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/driver/Socket.php
@@ -0,0 +1,306 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\log\driver;
+
+use Psr\Container\NotFoundExceptionInterface;
+use think\App;
+use think\contract\LogHandlerInterface;
+
+/**
+ * github: https://github.com/luofei614/SocketLog
+ * @author luofei614
+ */
+class Socket implements LogHandlerInterface
+{
+ protected $app;
+
+ protected $config = [
+ // socket服务器地址
+ 'host' => 'localhost',
+ // socket服务器端口
+ 'port' => 1116,
+ // 是否显示加载的文件列表
+ 'show_included_files' => false,
+ // 日志强制记录到配置的client_id
+ 'force_client_ids' => [],
+ // 限制允许读取日志的client_id
+ 'allow_client_ids' => [],
+ // 调试开关
+ 'debug' => false,
+ // 输出到浏览器时默认展开的日志级别
+ 'expand_level' => ['debug'],
+ // 日志头渲染回调
+ 'format_head' => null,
+ ];
+
+ protected $css = [
+ 'sql' => 'color:#009bb4;',
+ 'sql_warn' => 'color:#009bb4;font-size:14px;',
+ 'error' => 'color:#f4006b;font-size:14px;',
+ 'page' => 'color:#40e2ff;background:#171717;',
+ 'big' => 'font-size:20px;color:red;',
+ ];
+
+ protected $allowForceClientIds = []; //配置强制推送且被授权的client_id
+
+ protected $clientArg = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param App $app
+ * @param array $config 缓存参数
+ */
+ public function __construct(App $app, array $config = [])
+ {
+ $this->app = $app;
+
+ if (!empty($config)) {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ if (!isset($config['debug'])) {
+ $this->config['debug'] = $app->isDebug();
+ }
+ }
+
+ /**
+ * 调试输出接口
+ * @access public
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function save(array $log = []): bool
+ {
+ if (!$this->check()) {
+ return false;
+ }
+
+ $trace = [];
+
+ if ($this->config['debug']) {
+ if ($this->app->exists('request')) {
+ $currentUri = $this->app->request->url(true);
+ } else {
+ $currentUri = 'cmd:' . implode(' ', $_SERVER['argv'] ?? []);
+ }
+
+ if (!empty($this->config['format_head'])) {
+ try {
+ $currentUri = $this->app->invoke($this->config['format_head'], [$currentUri]);
+ } catch (NotFoundExceptionInterface $notFoundException) {
+ // Ignore exception
+ }
+ }
+
+ // 基本信息
+ $trace[] = [
+ 'type' => 'group',
+ 'msg' => $currentUri,
+ 'css' => $this->css['page'],
+ ];
+ }
+
+ $expandLevel = array_flip($this->config['expand_level']);
+
+ foreach ($log as $type => $val) {
+ $trace[] = [
+ 'type' => isset($expandLevel[$type]) ? 'group' : 'groupCollapsed',
+ 'msg' => '[ ' . $type . ' ]',
+ 'css' => $this->css[$type] ?? '',
+ ];
+
+ foreach ($val as $msg) {
+ if (!is_string($msg)) {
+ $msg = var_export($msg, true);
+ }
+ $trace[] = [
+ 'type' => 'log',
+ 'msg' => $msg,
+ 'css' => '',
+ ];
+ }
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+ }
+
+ if ($this->config['show_included_files']) {
+ $trace[] = [
+ 'type' => 'groupCollapsed',
+ 'msg' => '[ file ]',
+ 'css' => '',
+ ];
+
+ $trace[] = [
+ 'type' => 'log',
+ 'msg' => implode("\n", get_included_files()),
+ 'css' => '',
+ ];
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+ }
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+
+ $tabid = $this->getClientArg('tabid');
+
+ if (!$clientId = $this->getClientArg('client_id')) {
+ $clientId = '';
+ }
+
+ if (!empty($this->allowForceClientIds)) {
+ //强制推送到多个client_id
+ foreach ($this->allowForceClientIds as $forceClientId) {
+ $clientId = $forceClientId;
+ $this->sendToClient($tabid, $clientId, $trace, $forceClientId);
+ }
+ } else {
+ $this->sendToClient($tabid, $clientId, $trace, '');
+ }
+
+ return true;
+ }
+
+ /**
+ * 发送给指定客户端
+ * @access protected
+ * @author Zjmainstay
+ * @param $tabid
+ * @param $clientId
+ * @param $logs
+ * @param $forceClientId
+ */
+ protected function sendToClient($tabid, $clientId, $logs, $forceClientId)
+ {
+ $logs = [
+ 'tabid' => $tabid,
+ 'client_id' => $clientId,
+ 'logs' => $logs,
+ 'force_client_id' => $forceClientId,
+ ];
+
+ $msg = json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
+ $address = '/' . $clientId; //将client_id作为地址, server端通过地址判断将日志发布给谁
+
+ $this->send($this->config['host'], $this->config['port'], $msg, $address);
+ }
+
+ /**
+ * 检测客户授权
+ * @access protected
+ * @return bool
+ */
+ protected function check()
+ {
+ $tabid = $this->getClientArg('tabid');
+
+ //是否记录日志的检查
+ if (!$tabid && !$this->config['force_client_ids']) {
+ return false;
+ }
+
+ //用户认证
+ $allowClientIds = $this->config['allow_client_ids'];
+
+ if (!empty($allowClientIds)) {
+ //通过数组交集得出授权强制推送的client_id
+ $this->allowForceClientIds = array_intersect($allowClientIds, $this->config['force_client_ids']);
+ if (!$tabid && count($this->allowForceClientIds)) {
+ return true;
+ }
+
+ $clientId = $this->getClientArg('client_id');
+ if (!in_array($clientId, $allowClientIds)) {
+ return false;
+ }
+ } else {
+ $this->allowForceClientIds = $this->config['force_client_ids'];
+ }
+
+ return true;
+ }
+
+ /**
+ * 获取客户参数
+ * @access protected
+ * @param string $name
+ * @return string
+ */
+ protected function getClientArg(string $name)
+ {
+ if (!$this->app->exists('request')) {
+ return '';
+ }
+
+ if (empty($this->clientArg)) {
+ if (empty($socketLog = $this->app->request->header('socketlog'))) {
+ if (empty($socketLog = $this->app->request->header('User-Agent'))) {
+ return '';
+ }
+ }
+
+ if (!preg_match('/SocketLog\((.*?)\)/', $socketLog, $match)) {
+ $this->clientArg = ['tabid' => null, 'client_id' => null];
+ return '';
+ }
+ parse_str($match[1] ?? '', $this->clientArg);
+ }
+
+ if (isset($this->clientArg[$name])) {
+ return $this->clientArg[$name];
+ }
+
+ return '';
+ }
+
+ /**
+ * @access protected
+ * @param string $host - $host of socket server
+ * @param int $port - $port of socket server
+ * @param string $message - 发送的消息
+ * @param string $address - 地址
+ * @return bool
+ */
+ protected function send($host, $port, $message = '', $address = '/')
+ {
+ $url = 'http://' . $host . ':' . $port . $address;
+ $ch = curl_init();
+
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+ $headers = [
+ "Content-Type: application/json;charset=UTF-8",
+ ];
+
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header
+
+ return curl_exec($ch);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/middleware/AllowCrossDomain.php b/vendor/topthink/framework/src/think/middleware/AllowCrossDomain.php
new file mode 100644
index 0000000..d2515bd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/AllowCrossDomain.php
@@ -0,0 +1,63 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\Config;
+use think\Request;
+use think\Response;
+
+/**
+ * 跨域请求支持
+ */
+class AllowCrossDomain
+{
+ protected $cookieDomain;
+
+ protected $header = [
+ 'Access-Control-Allow-Credentials' => 'true',
+ 'Access-Control-Max-Age' => 1800,
+ 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
+ ];
+
+ public function __construct(Config $config)
+ {
+ $this->cookieDomain = $config->get('cookie.domain', '');
+ }
+
+ /**
+ * 允许跨域请求
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param array $header
+ * @return Response
+ */
+ public function handle($request, Closure $next, ?array $header = [])
+ {
+ $header = !empty($header) ? array_merge($this->header, $header) : $this->header;
+
+ if (!isset($header['Access-Control-Allow-Origin'])) {
+ $origin = $request->header('origin');
+
+ if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain))) {
+ $header['Access-Control-Allow-Origin'] = $origin;
+ } else {
+ $header['Access-Control-Allow-Origin'] = '*';
+ }
+ }
+
+ return $next($request)->header($header);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php b/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php
new file mode 100644
index 0000000..fdae36d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php
@@ -0,0 +1,163 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\Cache;
+use think\Config;
+use think\Request;
+use think\Response;
+
+/**
+ * 请求缓存处理
+ */
+class CheckRequestCache
+{
+ /**
+ * 缓存对象
+ * @var Cache
+ */
+ protected $cache;
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // 请求缓存规则 true为自动规则
+ 'request_cache_key' => true,
+ // 请求缓存有效期
+ 'request_cache_expire' => null,
+ // 全局请求缓存排除规则
+ 'request_cache_except' => [],
+ // 请求缓存的Tag
+ 'request_cache_tag' => '',
+ ];
+
+ public function __construct(Cache $cache, Config $config)
+ {
+ $this->cache = $cache;
+ $this->config = array_merge($this->config, $config->get('route'));
+ }
+
+ /**
+ * 设置当前地址的请求缓存
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param mixed $cache
+ * @return Response
+ */
+ public function handle($request, Closure $next, $cache = null)
+ {
+ if ($request->isGet() && false !== $cache) {
+ $cache = $cache ?: $this->getRequestCache($request);
+
+ if ($cache) {
+ if (is_array($cache)) {
+ [$key, $expire, $tag] = $cache;
+ } else {
+ $key = str_replace('|', '/', $request->url());
+ $expire = $cache;
+ $tag = null;
+ }
+
+ if (strtotime($request->server('HTTP_IF_MODIFIED_SINCE', '')) + $expire > $request->server('REQUEST_TIME')) {
+ // 读取缓存
+ return Response::create()->code(304);
+ } elseif (($hit = $this->cache->get($key)) !== null) {
+ [$content, $header, $when] = $hit;
+ if (null === $expire || $when + $expire > $request->server('REQUEST_TIME')) {
+ return Response::create($content)->header($header);
+ }
+ }
+ }
+ }
+
+ $response = $next($request);
+
+ if (isset($key) && 200 == $response->getCode() && $response->isAllowCache()) {
+ $header = $response->getHeader();
+ $header['Cache-Control'] = 'max-age=' . $expire . ',must-revalidate';
+ $header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
+ $header['Expires'] = gmdate('D, d M Y H:i:s', time() + $expire) . ' GMT';
+
+ $this->cache->tag($tag)->set($key, [$response->getContent(), $header, time()], $expire);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 读取当前地址的请求缓存信息
+ * @access protected
+ * @param Request $request
+ * @return mixed
+ */
+ protected function getRequestCache($request)
+ {
+ $key = $this->config['request_cache_key'];
+ $expire = $this->config['request_cache_expire'];
+ $except = $this->config['request_cache_except'];
+ $tag = $this->config['request_cache_tag'];
+
+ if ($key instanceof \Closure) {
+ $key = call_user_func($key, $request);
+ }
+
+ if (false === $key) {
+ // 关闭当前缓存
+ return;
+ }
+
+ foreach ($except as $rule) {
+ if (0 === stripos($request->url(), $rule)) {
+ return;
+ }
+ }
+
+ if (true === $key) {
+ // 自动缓存功能
+ $key = '__URL__';
+ } elseif (strpos($key, '|')) {
+ [$key, $fun] = explode('|', $key);
+ }
+
+ // 特殊规则替换
+ if (false !== strpos($key, '__')) {
+ $key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
+ }
+
+ if (false !== strpos($key, ':')) {
+ $param = $request->param();
+ foreach ($param as $item => $val) {
+ if (is_string($val) && false !== strpos($key, ':' . $item)) {
+ $key = str_replace(':' . $item, $val, $key);
+ }
+ }
+ } elseif (strpos($key, ']')) {
+ if ('[' . $request->ext() . ']' == $key) {
+ // 缓存某个后缀的请求
+ $key = md5($request->url());
+ } else {
+ return;
+ }
+ }
+
+ if (isset($fun)) {
+ $key = $fun($key);
+ }
+
+ return [$key, $expire, $tag];
+ }
+}
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..d2709a7
--- /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..72e9362
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/LoadLangPack.php
@@ -0,0 +1,61 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\App;
+use think\Lang;
+use think\Request;
+use think\Response;
+
+/**
+ * 多语言加载
+ */
+class LoadLangPack
+{
+ protected $app;
+
+ protected $lang;
+
+ public function __construct(App $app, Lang $lang)
+ {
+ $this->app = $app;
+ $this->lang = $lang;
+ }
+
+ /**
+ * 路由初始化(路由规则注册)
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle($request, Closure $next)
+ {
+ // 自动侦测当前语言
+ $langset = $this->lang->detect($request);
+
+ if ($this->lang->defaultLangSet() != $langset) {
+ // 加载系统语言包
+ $this->lang->load([
+ $this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php',
+ ]);
+
+ $this->app->LoadLangPack($langset);
+ }
+
+ $this->lang->saveToCookie($this->app->cookie);
+
+ return $next($request);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/middleware/SessionInit.php b/vendor/topthink/framework/src/think/middleware/SessionInit.php
new file mode 100644
index 0000000..9c581c3
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/SessionInit.php
@@ -0,0 +1,80 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\App;
+use think\Request;
+use think\Response;
+use think\Session;
+
+/**
+ * Session初始化
+ */
+class SessionInit
+{
+
+ /** @var App */
+ protected $app;
+
+ /** @var Session */
+ protected $session;
+
+ public function __construct(App $app, Session $session)
+ {
+ $this->app = $app;
+ $this->session = $session;
+ }
+
+ /**
+ * Session初始化
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle($request, Closure $next)
+ {
+ // Session初始化
+ $varSessionId = $this->app->config->get('session.var_session_id');
+ $cookieName = $this->session->getName();
+
+ if ($varSessionId && $request->request($varSessionId)) {
+ $sessionId = $request->request($varSessionId);
+ } else {
+ $sessionId = $request->cookie($cookieName);
+ }
+
+ if ($sessionId) {
+ $this->session->setId($sessionId);
+ }
+
+ $this->session->init();
+
+ $request->withSession($this->session);
+
+ /** @var Response $response */
+ $response = $next($request);
+
+ $response->setSession($this->session);
+
+ $this->app->cookie->set($cookieName, $this->session->getId());
+
+ return $response;
+ }
+
+ public function end(Response $response)
+ {
+ $this->session->save();
+ }
+}
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..c55538a
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/File.php
@@ -0,0 +1,158 @@
+
+// +----------------------------------------------------------------------
+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);
+ }
+
+ 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..a577364
--- /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..933d9f8
--- /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..e362aca
--- /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/response/Redirect.php b/vendor/topthink/framework/src/think/response/Redirect.php
new file mode 100644
index 0000000..23ad702
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/Redirect.php
@@ -0,0 +1,98 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Request;
+use think\Response;
+use think\Session;
+
+/**
+ * Redirect Response
+ */
+class Redirect extends Response
+{
+
+ protected $request;
+
+ public function __construct(Cookie $cookie, Request $request, Session $session, $data = '', int $code = 302)
+ {
+ $this->init((string) $data, $code);
+
+ $this->cookie = $cookie;
+ $this->request = $request;
+ $this->session = $session;
+
+ $this->cacheControl('no-cache,must-revalidate');
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ */
+ protected function output($data): string
+ {
+ $this->header['Location'] = $data;
+
+ return '';
+ }
+
+ /**
+ * 重定向传值(通过Session)
+ * @access protected
+ * @param string|array $name 变量名或者数组
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function with($name, $value = null)
+ {
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $this->session->flash($key, $val);
+ }
+ } else {
+ $this->session->flash($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 记住当前url后跳转
+ * @access public
+ * @return $this
+ */
+ public function remember()
+ {
+ $this->session->set('redirect_url', $this->request->url());
+
+ return $this;
+ }
+
+ /**
+ * 跳转到上次记住的url
+ * @access public
+ * @return $this
+ */
+ public function restore()
+ {
+ if ($this->session->has('redirect_url')) {
+ $this->data = $this->session->get('redirect_url');
+ $this->session->delete('redirect_url');
+ }
+
+ return $this;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/response/View.php b/vendor/topthink/framework/src/think/response/View.php
new file mode 100644
index 0000000..7095349
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/View.php
@@ -0,0 +1,149 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+use think\View as BaseView;
+
+/**
+ * View Response
+ */
+class View extends Response
+{
+ /**
+ * 输出参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 输出变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 输出过滤
+ * @var mixed
+ */
+ protected $filter;
+
+ /**
+ * 输出type
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ /**
+ * View对象
+ * @var BaseView
+ */
+ protected $view;
+
+ /**
+ * 是否内容渲染
+ * @var bool
+ */
+ protected $isContent = false;
+
+ public function __construct(Cookie $cookie, BaseView $view, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+
+ $this->cookie = $cookie;
+ $this->view = $view;
+ }
+
+ /**
+ * 设置是否为内容渲染
+ * @access public
+ * @param bool $content
+ * @return $this
+ */
+ public function isContent(bool $content = true)
+ {
+ $this->isContent = $content;
+ return $this;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ */
+ protected function output($data): string
+ {
+ // 渲染模板输出
+ return $this->view->filter($this->filter)
+ ->fetch($data, $this->vars, $this->isContent);
+ }
+
+ /**
+ * 获取视图变量
+ * @access public
+ * @param string $name 模板变量
+ * @return mixed
+ */
+ public function getVars(string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->vars;
+ } else {
+ return $this->vars[$name] ?? null;
+ }
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string|array $name 模板变量
+ * @param mixed $value 变量值
+ * @return $this
+ */
+ public function assign($name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->vars = array_merge($this->vars, $name);
+ } else {
+ $this->vars[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图内容过滤
+ * @access public
+ * @param callable $filter
+ * @return $this
+ */
+ public function filter(callable $filter = null)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * 检查模板是否存在
+ * @access public
+ * @param string $name 模板名
+ * @return bool
+ */
+ public function exists(string $name): bool
+ {
+ return $this->view->exists($name);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/response/Xml.php b/vendor/topthink/framework/src/think/response/Xml.php
new file mode 100644
index 0000000..e777c55
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/Xml.php
@@ -0,0 +1,127 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Collection;
+use think\Cookie;
+use think\Model;
+use think\Response;
+
+/**
+ * XML Response
+ */
+class Xml extends Response
+{
+ // 输出参数
+ protected $options = [
+ // 根节点名
+ 'root_node' => 'think',
+ // 根节点属性
+ 'root_attr' => '',
+ //数字索引的子节点名
+ 'item_node' => 'item',
+ // 数字索引子节点key转换的属性名
+ 'item_key' => 'id',
+ // 数据编码
+ 'encoding' => 'utf-8',
+ ];
+
+ protected $contentType = 'text/xml';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data): string
+ {
+ if (is_string($data)) {
+ if (0 !== strpos($data, 'options['encoding'];
+ $xml = "";
+ $data = $xml . $data;
+ }
+ return $data;
+ }
+
+ // XML数据转换
+ return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']);
+ }
+
+ /**
+ * XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $root 根节点名
+ * @param string $item 数字索引的子节点名
+ * @param mixed $attr 根节点属性
+ * @param string $id 数字索引子节点key转换的属性名
+ * @param string $encoding 数据编码
+ * @return string
+ */
+ protected function xmlEncode($data, string $root, string $item, $attr, string $id, string $encoding): string
+ {
+ if (is_array($attr)) {
+ $array = [];
+ foreach ($attr as $key => $value) {
+ $array[] = "{$key}=\"{$value}\"";
+ }
+ $attr = implode(' ', $array);
+ }
+
+ $attr = trim($attr);
+ $attr = empty($attr) ? '' : " {$attr}";
+ $xml = "";
+ $xml .= "<{$root}{$attr}>";
+ $xml .= $this->dataToXml($data, $item, $id);
+ $xml .= "{$root}>";
+
+ return $xml;
+ }
+
+ /**
+ * 数据XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $item 数字索引时的节点名称
+ * @param string $id 数字索引key转换为的属性名
+ * @return string
+ */
+ protected function dataToXml($data, string $item, string $id): string
+ {
+ $xml = $attr = '';
+
+ if ($data instanceof Collection || $data instanceof Model) {
+ $data = $data->toArray();
+ }
+
+ foreach ($data as $key => $val) {
+ if (is_numeric($key)) {
+ $id && $attr = " {$id}=\"{$key}\"";
+ $key = $item;
+ }
+ $xml .= "<{$key}{$attr}>";
+ $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val;
+ $xml .= "{$key}>";
+ }
+
+ return $xml;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/Dispatch.php b/vendor/topthink/framework/src/think/route/Dispatch.php
new file mode 100644
index 0000000..a631e83
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Dispatch.php
@@ -0,0 +1,257 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\App;
+use think\Container;
+use think\Request;
+use think\Response;
+use think\Validate;
+
+/**
+ * 路由调度基础类
+ */
+abstract class Dispatch
+{
+ /**
+ * 应用对象
+ * @var \think\App
+ */
+ protected $app;
+
+ /**
+ * 请求对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * 路由规则
+ * @var Rule
+ */
+ protected $rule;
+
+ /**
+ * 调度信息
+ * @var mixed
+ */
+ protected $dispatch;
+
+ /**
+ * 路由变量
+ * @var array
+ */
+ protected $param;
+
+ public function __construct(Request $request, Rule $rule, $dispatch, array $param = [])
+ {
+ $this->request = $request;
+ $this->rule = $rule;
+ $this->dispatch = $dispatch;
+ $this->param = $param;
+ }
+
+ public function init(App $app)
+ {
+ $this->app = $app;
+
+ // 执行路由后置操作
+ $this->doRouteAfter();
+ }
+
+ /**
+ * 执行路由调度
+ * @access public
+ * @return mixed
+ */
+ public function run(): Response
+ {
+ if ($this->rule instanceof RuleItem && $this->request->method() == 'OPTIONS' && $this->rule->isAutoOptions()) {
+ $rules = $this->rule->getRouter()->getRule($this->rule->getRule());
+ $allow = [];
+ foreach ($rules as $item) {
+ $allow[] = strtoupper($item->getMethod());
+ }
+
+ return Response::create('', 'html', 204)->header(['Allow' => implode(', ', $allow)]);
+ }
+
+ $data = $this->exec();
+ return $this->autoResponse($data);
+ }
+
+ protected function autoResponse($data): Response
+ {
+ if ($data instanceof Response) {
+ $response = $data;
+ } elseif (!is_null($data)) {
+ // 默认自动识别响应输出类型
+ $type = $this->request->isJson() ? 'json' : 'html';
+ $response = Response::create($data, $type);
+ } else {
+ $data = ob_get_clean();
+
+ $content = false === $data ? '' : $data;
+ $status = '' === $content && $this->request->isJson() ? 204 : 200;
+ $response = Response::create($content, 'html', $status);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 检查路由后置操作
+ * @access protected
+ * @return void
+ */
+ protected function doRouteAfter(): void
+ {
+ $option = $this->rule->getOption();
+
+ // 添加中间件
+ if (!empty($option['middleware'])) {
+ $this->app->middleware->import($option['middleware'], 'route');
+ }
+
+ if (!empty($option['append'])) {
+ $this->param = array_merge($this->param, $option['append']);
+ }
+
+ // 绑定模型数据
+ if (!empty($option['model'])) {
+ $this->createBindModel($option['model'], $this->param);
+ }
+
+ // 记录当前请求的路由规则
+ $this->request->setRule($this->rule);
+
+ // 记录路由变量
+ $this->request->setRoute($this->param);
+
+ // 数据自动验证
+ if (isset($option['validate'])) {
+ $this->autoValidate($option['validate']);
+ }
+ }
+
+ /**
+ * 路由绑定模型实例
+ * @access protected
+ * @param array $bindModel 绑定模型
+ * @param array $matches 路由变量
+ * @return void
+ */
+ protected function createBindModel(array $bindModel, array $matches): void
+ {
+ foreach ($bindModel as $key => $val) {
+ if ($val instanceof \Closure) {
+ $result = $this->app->invokeFunction($val, $matches);
+ } else {
+ $fields = explode('&', $key);
+
+ if (is_array($val)) {
+ [$model, $exception] = $val;
+ } else {
+ $model = $val;
+ $exception = true;
+ }
+
+ $where = [];
+ $match = true;
+
+ foreach ($fields as $field) {
+ if (!isset($matches[$field])) {
+ $match = false;
+ break;
+ } else {
+ $where[] = [$field, '=', $matches[$field]];
+ }
+ }
+
+ if ($match) {
+ $result = $model::where($where)->failException($exception)->find();
+ }
+ }
+
+ if (!empty($result)) {
+ // 注入容器
+ $this->app->instance(get_class($result), $result);
+ }
+ }
+ }
+
+ /**
+ * 验证数据
+ * @access protected
+ * @param array $option
+ * @return void
+ * @throws \think\exception\ValidateException
+ */
+ protected function autoValidate(array $option): void
+ {
+ [$validate, $scene, $message, $batch] = $option;
+
+ if (is_array($validate)) {
+ // 指定验证规则
+ $v = new Validate();
+ $v->rule($validate);
+ } else {
+ // 调用验证器
+ $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+
+ $v = new $class();
+
+ if (!empty($scene)) {
+ $v->scene($scene);
+ }
+ }
+
+ /** @var Validate $v */
+ $v->message($message)
+ ->batch($batch)
+ ->failException(true)
+ ->check($this->request->param());
+ }
+
+ public function getDispatch()
+ {
+ return $this->dispatch;
+ }
+
+ public function getParam(): array
+ {
+ return $this->param;
+ }
+
+ abstract public function exec();
+
+ public function __sleep()
+ {
+ return ['rule', 'dispatch', 'param', 'controller', 'actionName'];
+ }
+
+ public function __wakeup()
+ {
+ $this->app = Container::pull('app');
+ $this->request = $this->app->request;
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'dispatch' => $this->dispatch,
+ 'param' => $this->param,
+ 'rule' => $this->rule,
+ ];
+ }
+}
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..e711e5d
--- /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/route/Resource.php b/vendor/topthink/framework/src/think/route/Resource.php
new file mode 100644
index 0000000..cce93cd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Resource.php
@@ -0,0 +1,251 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\Route;
+
+/**
+ * 资源路由类
+ */
+class Resource extends RuleGroup
+{
+ /**
+ * 资源路由名称
+ * @var string
+ */
+ protected $resource;
+
+ /**
+ * 资源路由地址
+ * @var string
+ */
+ protected $route;
+
+ /**
+ * REST方法定义
+ * @var array
+ */
+ protected $rest = [];
+
+ /**
+ * 模型绑定
+ * @var array
+ */
+ protected $model = [];
+
+ /**
+ * 数据验证
+ * @var array
+ */
+ protected $validate = [];
+
+ /**
+ * 中间件
+ * @var array
+ */
+ protected $middleware = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 资源名称
+ * @param string $route 路由地址
+ * @param array $rest 资源定义
+ */
+ public function __construct(Route $router, RuleGroup $parent = null, string $name = '', string $route = '', array $rest = [])
+ {
+ $name = ltrim($name, '/');
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->resource = $name;
+ $this->route = $route;
+ $this->name = strpos($name, '.') ? strstr($name, '.', true) : $name;
+
+ $this->setFullName();
+
+ // 资源路由默认为完整匹配
+ $this->option['complete_match'] = true;
+
+ $this->rest = $rest;
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+
+ if ($router->isTest()) {
+ $this->buildResourceRule();
+ }
+ }
+
+ /**
+ * 生成资源路由规则
+ * @access protected
+ * @return void
+ */
+ protected function buildResourceRule(): void
+ {
+ $rule = $this->resource;
+ $option = $this->option;
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if (strpos($rule, '.')) {
+ // 注册嵌套资源路由
+ $array = explode('.', $rule);
+ $last = array_pop($array);
+ $item = [];
+
+ foreach ($array as $val) {
+ $item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';
+ }
+
+ $rule = implode('/', $item) . '/' . $last;
+ }
+
+ $prefix = substr($rule, strlen($this->name) + 1);
+
+ // 注册资源路由
+ foreach ($this->rest as $key => $val) {
+ if ((isset($option['only']) && !in_array($key, $option['only']))
+ || (isset($option['except']) && in_array($key, $option['except']))) {
+ continue;
+ }
+
+ if (isset($last) && strpos($val[1], '') && isset($option['var'][$last])) {
+ $val[1] = str_replace('', '<' . $option['var'][$last] . '>', $val[1]);
+ } elseif (strpos($val[1], '') && isset($option['var'][$rule])) {
+ $val[1] = str_replace('', '<' . $option['var'][$rule] . '>', $val[1]);
+ }
+
+ $ruleItem = $this->addRule(trim($prefix . $val[1], '/'), $this->route . '/' . $val[2], $val[0]);
+
+ foreach (['model', 'validate', 'middleware'] as $name) {
+ if (isset($this->$name[$key])) {
+ call_user_func_array([$ruleItem, $name], (array) $this->$name[$key]);
+ }
+
+ }
+ }
+
+ $this->router->setGroup($origin);
+ }
+
+ /**
+ * 设置资源允许
+ * @access public
+ * @param array $only 资源允许
+ * @return $this
+ */
+ public function only(array $only)
+ {
+ return $this->setOption('only', $only);
+ }
+
+ /**
+ * 设置资源排除
+ * @access public
+ * @param array $except 排除资源
+ * @return $this
+ */
+ public function except(array $except)
+ {
+ return $this->setOption('except', $except);
+ }
+
+ /**
+ * 设置资源路由的变量
+ * @access public
+ * @param array $vars 资源变量
+ * @return $this
+ */
+ public function vars(array $vars)
+ {
+ return $this->setOption('var', $vars);
+ }
+
+ /**
+ * 绑定资源验证
+ * @access public
+ * @param array|string $name 资源类型或者验证信息
+ * @param array|string $validate 验证信息
+ * @return $this
+ */
+ public function withValidate($name, $validate = [])
+ {
+ if (is_array($name)) {
+ $this->validate = array_merge($this->validate, $name);
+ } else {
+ $this->validate[$name] = $validate;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定资源模型
+ * @access public
+ * @param array|string $name 资源类型或者模型绑定
+ * @param array|string $model 模型绑定
+ * @return $this
+ */
+ public function withModel($name, $model = [])
+ {
+ if (is_array($name)) {
+ $this->model = array_merge($this->model, $name);
+ } else {
+ $this->model[$name] = $model;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定资源模型
+ * @access public
+ * @param array|string $name 资源类型或者中间件定义
+ * @param array|string $middleware 中间件定义
+ * @return $this
+ */
+ public function withMiddleware($name, $middleware = [])
+ {
+ if (is_array($name)) {
+ $this->middleware = array_merge($this->middleware, $name);
+ } else {
+ $this->middleware[$name] = $middleware;
+ }
+
+ return $this;
+ }
+
+ /**
+ * rest方法定义和修改
+ * @access public
+ * @param array|string $name 方法名称
+ * @param array|bool $resource 资源
+ * @return $this
+ */
+ public function rest($name, $resource = [])
+ {
+ if (is_array($name)) {
+ $this->rest = $resource ? $name : array_merge($this->rest, $name);
+ } else {
+ $this->rest[$name] = $resource;
+ }
+
+ return $this;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/route/Rule.php b/vendor/topthink/framework/src/think/route/Rule.php
new file mode 100644
index 0000000..eb638b1
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Rule.php
@@ -0,0 +1,893 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use think\Container;
+use think\middleware\AllowCrossDomain;
+use think\middleware\CheckRequestCache;
+use think\middleware\FormTokenCheck;
+use think\Request;
+use think\Route;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+
+/**
+ * 路由规则基础类
+ */
+abstract class Rule
+{
+ /**
+ * 路由标识
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 所在域名
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * 路由对象
+ * @var Route
+ */
+ protected $router;
+
+ /**
+ * 路由所属分组
+ * @var RuleGroup
+ */
+ protected $parent;
+
+ /**
+ * 路由规则
+ * @var mixed
+ */
+ protected $rule;
+
+ /**
+ * 路由地址
+ * @var string|Closure
+ */
+ protected $route;
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * 路由变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 路由参数
+ * @var array
+ */
+ protected $option = [];
+
+ /**
+ * 路由变量规则
+ * @var array
+ */
+ protected $pattern = [];
+
+ /**
+ * 需要和分组合并的路由参数
+ * @var array
+ */
+ protected $mergeOptions = ['model', 'append', 'middleware'];
+
+ abstract public function check(Request $request, string $url, bool $completeMatch = false);
+
+ /**
+ * 设置路由参数
+ * @access public
+ * @param array $option 参数
+ * @return $this
+ */
+ public function option(array $option)
+ {
+ $this->option = array_merge($this->option, $option);
+
+ return $this;
+ }
+
+ /**
+ * 设置单个路由参数
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function setOption(string $name, $value)
+ {
+ $this->option[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 注册变量规则
+ * @access public
+ * @param array $pattern 变量规则
+ * @return $this
+ */
+ public function pattern(array $pattern)
+ {
+ $this->pattern = array_merge($this->pattern, $pattern);
+
+ return $this;
+ }
+
+ /**
+ * 设置标识
+ * @access public
+ * @param string $name 标识名
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 获取路由对象
+ * @access public
+ * @return Route
+ */
+ public function getRouter(): Route
+ {
+ return $this->router;
+ }
+
+ /**
+ * 获取Name
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 获取当前路由规则
+ * @access public
+ * @return mixed
+ */
+ public function getRule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 获取当前路由地址
+ * @access public
+ * @return mixed
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
+
+ /**
+ * 获取当前路由的变量
+ * @access public
+ * @return array
+ */
+ public function getVars(): array
+ {
+ return $this->vars;
+ }
+
+ /**
+ * 获取Parent对象
+ * @access public
+ * @return $this|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取路由所在域名
+ * @access public
+ * @return string
+ */
+ public function getDomain(): string
+ {
+ return $this->domain ?: $this->parent->getDomain();
+ }
+
+ /**
+ * 获取路由参数
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function config(string $name = '')
+ {
+ return $this->router->config($name);
+ }
+
+ /**
+ * 获取变量规则定义
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function getPattern(string $name = '')
+ {
+ if ('' === $name) {
+ return $this->pattern;
+ }
+
+ return $this->pattern[$name] ?? null;
+ }
+
+ /**
+ * 获取路由参数定义
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getOption(string $name = '', $default = null)
+ {
+ if ('' === $name) {
+ return $this->option;
+ }
+
+ return $this->option[$name] ?? $default;
+ }
+
+ /**
+ * 获取当前路由的请求类型
+ * @access public
+ * @return string
+ */
+ public function getMethod(): string
+ {
+ return strtolower($this->method);
+ }
+
+ /**
+ * 设置路由请求类型
+ * @access public
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function method(string $method)
+ {
+ return $this->setOption('method', strtolower($method));
+ }
+
+ /**
+ * 检查后缀
+ * @access public
+ * @param string $ext URL后缀
+ * @return $this
+ */
+ public function ext(string $ext = '')
+ {
+ return $this->setOption('ext', $ext);
+ }
+
+ /**
+ * 检查禁止后缀
+ * @access public
+ * @param string $ext URL后缀
+ * @return $this
+ */
+ public function denyExt(string $ext = '')
+ {
+ return $this->setOption('deny_ext', $ext);
+ }
+
+ /**
+ * 检查域名
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function domain(string $domain)
+ {
+ $this->domain = $domain;
+ return $this->setOption('domain', $domain);
+ }
+
+ /**
+ * 设置参数过滤检查
+ * @access public
+ * @param array $filter 参数过滤
+ * @return $this
+ */
+ public function filter(array $filter)
+ {
+ $this->option['filter'] = $filter;
+
+ return $this;
+ }
+
+ /**
+ * 绑定模型
+ * @access public
+ * @param array|string|Closure $var 路由变量名 多个使用 & 分割
+ * @param string|Closure $model 绑定模型类
+ * @param bool $exception 是否抛出异常
+ * @return $this
+ */
+ public function model($var, $model = null, bool $exception = true)
+ {
+ if ($var instanceof Closure) {
+ $this->option['model'][] = $var;
+ } elseif (is_array($var)) {
+ $this->option['model'] = $var;
+ } elseif (is_null($model)) {
+ $this->option['model']['id'] = [$var, true];
+ } else {
+ $this->option['model'][$var] = [$model, $exception];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 附加路由隐式参数
+ * @access public
+ * @param array $append 追加参数
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ $this->option['append'] = $append;
+
+ return $this;
+ }
+
+ /**
+ * 绑定验证
+ * @access public
+ * @param mixed $validate 验证器类
+ * @param string $scene 验证场景
+ * @param array $message 验证提示
+ * @param bool $batch 批量验证
+ * @return $this
+ */
+ public function validate($validate, string $scene = null, array $message = [], bool $batch = false)
+ {
+ $this->option['validate'] = [$validate, $scene, $message, $batch];
+
+ return $this;
+ }
+
+ /**
+ * 指定路由中间件
+ * @access public
+ * @param string|array|Closure $middleware 中间件
+ * @param mixed $params 参数
+ * @return $this
+ */
+ public function middleware($middleware, ...$params)
+ {
+ if (empty($params) && is_array($middleware)) {
+ $this->option['middleware'] = $middleware;
+ } else {
+ foreach ((array) $middleware as $item) {
+ $this->option['middleware'][] = [$item, $params];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 允许跨域
+ * @access public
+ * @param array $header 自定义Header
+ * @return $this
+ */
+ public function allowCrossDomain(array $header = [])
+ {
+ return $this->middleware(AllowCrossDomain::class, $header);
+ }
+
+ /**
+ * 表单令牌验证
+ * @access public
+ * @param string $token 表单令牌token名称
+ * @return $this
+ */
+ public function token(string $token = '__token__')
+ {
+ return $this->middleware(FormTokenCheck::class, $token);
+ }
+
+ /**
+ * 设置路由缓存
+ * @access public
+ * @param array|string $cache 缓存
+ * @return $this
+ */
+ public function cache($cache)
+ {
+ return $this->middleware(CheckRequestCache::class, $cache);
+ }
+
+ /**
+ * 检查URL分隔符
+ * @access public
+ * @param string $depr URL分隔符
+ * @return $this
+ */
+ public function depr(string $depr)
+ {
+ return $this->setOption('param_depr', $depr);
+ }
+
+ /**
+ * 设置需要合并的路由参数
+ * @access public
+ * @param array $option 路由参数
+ * @return $this
+ */
+ public function mergeOptions(array $option = [])
+ {
+ $this->mergeOptions = array_merge($this->mergeOptions, $option);
+ return $this;
+ }
+
+ /**
+ * 检查是否为HTTPS请求
+ * @access public
+ * @param bool $https 是否为HTTPS
+ * @return $this
+ */
+ public function https(bool $https = true)
+ {
+ return $this->setOption('https', $https);
+ }
+
+ /**
+ * 检查是否为JSON请求
+ * @access public
+ * @param bool $json 是否为JSON
+ * @return $this
+ */
+ public function json(bool $json = true)
+ {
+ return $this->setOption('json', $json);
+ }
+
+ /**
+ * 检查是否为AJAX请求
+ * @access public
+ * @param bool $ajax 是否为AJAX
+ * @return $this
+ */
+ public function ajax(bool $ajax = true)
+ {
+ return $this->setOption('ajax', $ajax);
+ }
+
+ /**
+ * 检查是否为PJAX请求
+ * @access public
+ * @param bool $pjax 是否为PJAX
+ * @return $this
+ */
+ public function pjax(bool $pjax = true)
+ {
+ return $this->setOption('pjax', $pjax);
+ }
+
+ /**
+ * 路由到一个模板地址 需要额外传入的模板变量
+ * @access public
+ * @param array $view 视图
+ * @return $this
+ */
+ public function view(array $view = [])
+ {
+ return $this->setOption('view', $view);
+ }
+
+ /**
+ * 设置路由完整匹配
+ * @access public
+ * @param bool $match 是否完整匹配
+ * @return $this
+ */
+ public function completeMatch(bool $match = true)
+ {
+ return $this->setOption('complete_match', $match);
+ }
+
+ /**
+ * 是否去除URL最后的斜线
+ * @access public
+ * @param bool $remove 是否去除最后斜线
+ * @return $this
+ */
+ public function removeSlash(bool $remove = true)
+ {
+ return $this->setOption('remove_slash', $remove);
+ }
+
+ /**
+ * 设置路由规则全局有效
+ * @access public
+ * @return $this
+ */
+ public function crossDomainRule()
+ {
+ if ($this instanceof RuleGroup) {
+ $method = '*';
+ } else {
+ $method = $this->method;
+ }
+
+ $this->router->setCrossDomainRule($this, $method);
+
+ return $this;
+ }
+
+ /**
+ * 合并分组参数
+ * @access public
+ * @return array
+ */
+ public function mergeGroupOptions(): array
+ {
+ $parentOption = $this->parent->getOption();
+ // 合并分组参数
+ foreach ($this->mergeOptions as $item) {
+ if (isset($parentOption[$item]) && isset($this->option[$item])) {
+ $this->option[$item] = array_merge($parentOption[$item], $this->option[$item]);
+ }
+ }
+
+ $this->option = array_merge($parentOption, $this->option);
+
+ return $this->option;
+ }
+
+ /**
+ * 解析匹配到的规则路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param array $matches 匹配的变量
+ * @return Dispatch
+ */
+ public function parseRule(Request $request, string $rule, $route, string $url, array $option = [], array $matches = []): Dispatch
+ {
+ if (is_string($route) && isset($option['prefix'])) {
+ // 路由地址前缀
+ $route = $option['prefix'] . $route;
+ }
+
+ // 替换路由地址中的变量
+ if (is_string($route) && !empty($matches)) {
+ $search = $replace = [];
+
+ foreach ($matches as $key => $value) {
+ $search[] = '<' . $key . '>';
+ $replace[] = $value;
+
+ $search[] = ':' . $key;
+ $replace[] = $value;
+ }
+
+ $route = str_replace($search, $replace, $route);
+ }
+
+ // 解析额外参数
+ $count = substr_count($rule, '/');
+ $url = array_slice(explode('|', $url), $count + 1);
+ $this->parseUrlParams(implode('|', $url), $matches);
+
+ $this->vars = $matches;
+
+ // 发起路由调度
+ return $this->dispatch($request, $route, $option);
+ }
+
+ /**
+ * 发起路由调度
+ * @access protected
+ * @param Request $request Request对象
+ * @param mixed $route 路由地址
+ * @param array $option 路由参数
+ * @return Dispatch
+ */
+ protected function dispatch(Request $request, $route, array $option): Dispatch
+ {
+ if (is_subclass_of($route, Dispatch::class)) {
+ $result = new $route($request, $this, $route, $this->vars);
+ } elseif ($route instanceof Closure) {
+ // 执行闭包
+ $result = new CallbackDispatch($request, $this, $route, $this->vars);
+ } elseif (false !== strpos($route, '@') || false !== strpos($route, '::')) {
+ // 路由到类的方法
+ $route = str_replace('::', '@', $route);
+ $result = $this->dispatchMethod($request, $route);
+ } else {
+ // 路由到控制器/操作
+ $result = $this->dispatchController($request, $route);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析URL地址为 模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return CallbackDispatch
+ */
+ protected function dispatchMethod(Request $request, string $route): CallbackDispatch
+ {
+ $path = $this->parseUrlPath($route);
+
+ $route = str_replace('/', '@', implode('/', $path));
+ $method = strpos($route, '@') ? explode('@', $route) : $route;
+
+ return new CallbackDispatch($request, $this, $method, $this->vars);
+ }
+
+ /**
+ * 解析URL地址为 模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return ControllerDispatch
+ */
+ protected function dispatchController(Request $request, string $route): ControllerDispatch
+ {
+ $path = $this->parseUrlPath($route);
+
+ $action = array_pop($path);
+ $controller = !empty($path) ? array_pop($path) : null;
+
+ // 路由到模块/控制器/操作
+ return new ControllerDispatch($request, $this, [$controller, $action], $this->vars);
+ }
+
+ /**
+ * 路由检查
+ * @access protected
+ * @param array $option 路由参数
+ * @param Request $request Request对象
+ * @return bool
+ */
+ protected function checkOption(array $option, Request $request): bool
+ {
+ // 请求类型检测
+ if (!empty($option['method'])) {
+ if (is_string($option['method']) && false === stripos($option['method'], $request->method())) {
+ return false;
+ }
+ }
+
+ // AJAX PJAX 请求检查
+ foreach (['ajax', 'pjax', 'json'] as $item) {
+ if (isset($option[$item])) {
+ $call = 'is' . $item;
+ if ($option[$item] && !$request->$call() || !$option[$item] && $request->$call()) {
+ return false;
+ }
+ }
+ }
+
+ // 伪静态后缀检测
+ if ($request->url() != '/' && ((isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|'))
+ || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')))) {
+ return false;
+ }
+
+ // 域名检查
+ if ((isset($option['domain']) && !in_array($option['domain'], [$request->host(true), $request->subDomain()]))) {
+ return false;
+ }
+
+ // HTTPS检查
+ if ((isset($option['https']) && $option['https'] && !$request->isSsl())
+ || (isset($option['https']) && !$option['https'] && $request->isSsl())) {
+ return false;
+ }
+
+ // 请求参数检查
+ if (isset($option['filter'])) {
+ foreach ($option['filter'] as $name => $value) {
+ if ($request->param($name, '', null) != $value) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 解析URL地址中的参数Request对象
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $var 变量
+ * @return void
+ */
+ protected function parseUrlParams(string $url, array &$var = []): void
+ {
+ if ($url) {
+ preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
+ $var[$match[1]] = strip_tags($match[2]);
+ }, $url);
+ }
+ }
+
+ /**
+ * 解析URL的pathinfo参数
+ * @access public
+ * @param string $url URL地址
+ * @return array
+ */
+ public function parseUrlPath(string $url): array
+ {
+ // 分隔符替换 确保路由定义使用统一的分隔符
+ $url = str_replace('|', '/', $url);
+ $url = trim($url, '/');
+
+ if (strpos($url, '/')) {
+ // [控制器/操作]
+ $path = explode('/', $url);
+ } else {
+ $path = [$url];
+ }
+
+ return $path;
+ }
+
+ /**
+ * 生成路由的正则规则
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $match 匹配的变量
+ * @param array $pattern 路由变量规则
+ * @param array $option 路由参数
+ * @param bool $completeMatch 路由是否完全匹配
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildRuleRegex(string $rule, array $match, array $pattern = [], array $option = [], bool $completeMatch = false, string $suffix = ''): string
+ {
+ foreach ($match as $name) {
+ $replace[] = $this->buildNameRegex($name, $pattern, $suffix);
+ }
+
+ // 是否区分 / 地址访问
+ if ('/' != $rule) {
+ if (!empty($option['remove_slash'])) {
+ $rule = rtrim($rule, '/');
+ } elseif (substr($rule, -1) == '/') {
+ $rule = rtrim($rule, '/');
+ $hasSlash = true;
+ }
+ }
+
+ $regex = str_replace(array_unique($match), array_unique($replace), $rule);
+ $regex = str_replace([')?/', ')/', ')?-', ')-', '\\\\/'], [')\/', ')\/', ')\-', ')\-', '\/'], $regex);
+
+ if (isset($hasSlash)) {
+ $regex .= '\/';
+ }
+
+ return $regex . ($completeMatch ? '$' : '');
+ }
+
+ /**
+ * 生成路由变量的正则规则
+ * @access protected
+ * @param string $name 路由变量
+ * @param array $pattern 变量规则
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildNameRegex(string $name, array $pattern, string $suffix): string
+ {
+ $optional = '';
+ $slash = substr($name, 0, 1);
+
+ if (in_array($slash, ['/', '-'])) {
+ $prefix = '\\' . $slash;
+ $name = substr($name, 1);
+ $slash = substr($name, 0, 1);
+ } else {
+ $prefix = '';
+ }
+
+ if ('<' != $slash) {
+ return $prefix . preg_quote($name, '/');
+ }
+
+ if (strpos($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = '?';
+ } elseif (strpos($name, '>')) {
+ $name = substr($name, 1, -1);
+ }
+
+ if (isset($pattern[$name])) {
+ $nameRule = $pattern[$name];
+ if (0 === strpos($nameRule, '/') && '/' == substr($nameRule, -1)) {
+ $nameRule = substr($nameRule, 1, -1);
+ }
+ } else {
+ $nameRule = $this->router->config('default_route_pattern');
+ }
+
+ return '(' . $prefix . '(?<' . $name . $suffix . '>' . $nameRule . '))' . $optional;
+ }
+
+ /**
+ * 设置路由参数
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return $this
+ */
+ public function __call($method, $args)
+ {
+ if (count($args) > 1) {
+ $args[0] = $args;
+ }
+ array_unshift($args, $method);
+
+ return call_user_func_array([$this, 'setOption'], $args);
+ }
+
+ public function __sleep()
+ {
+ return ['name', 'rule', 'route', 'method', 'vars', 'option', 'pattern'];
+ }
+
+ public function __wakeup()
+ {
+ $this->router = Container::pull('route');
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'name' => $this->name,
+ 'rule' => $this->rule,
+ 'route' => $this->route,
+ 'method' => $this->method,
+ 'vars' => $this->vars,
+ 'option' => $this->option,
+ 'pattern' => $this->pattern,
+ ];
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/RuleGroup.php b/vendor/topthink/framework/src/think/route/RuleGroup.php
new file mode 100644
index 0000000..322e0fc
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/RuleGroup.php
@@ -0,0 +1,511 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use think\Container;
+use think\Exception;
+use think\Request;
+use think\Route;
+
+/**
+ * 路由分组类
+ */
+class RuleGroup extends Rule
+{
+ /**
+ * 分组路由(包括子分组)
+ * @var array
+ */
+ protected $rules = [];
+
+ /**
+ * 分组路由规则
+ * @var mixed
+ */
+ protected $rule;
+
+ /**
+ * MISS路由
+ * @var RuleItem
+ */
+ protected $miss;
+
+ /**
+ * 完整名称
+ * @var string
+ */
+ protected $fullName;
+
+ /**
+ * 分组别名
+ * @var string
+ */
+ protected $alias;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 分组名称
+ * @param mixed $rule 分组路由
+ */
+ public function __construct(Route $router, RuleGroup $parent = null, string $name = '', $rule = null)
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->rule = $rule;
+ $this->name = trim($name, '/');
+
+ $this->setFullName();
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+
+ if ($router->isTest()) {
+ $this->lazy(false);
+ }
+ }
+
+ /**
+ * 设置分组的路由规则
+ * @access public
+ * @return void
+ */
+ protected function setFullName(): void
+ {
+ if (false !== strpos($this->name, ':')) {
+ $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
+ }
+
+ if ($this->parent && $this->parent->getFullName()) {
+ $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
+ } else {
+ $this->fullName = $this->name;
+ }
+
+ if ($this->name) {
+ $this->router->getRuleName()->setGroup($this->name, $this);
+ }
+ }
+
+ /**
+ * 获取所属域名
+ * @access public
+ * @return string
+ */
+ public function getDomain(): string
+ {
+ return $this->domain ?: '-';
+ }
+
+ /**
+ * 获取分组别名
+ * @access public
+ * @return string
+ */
+ public function getAlias(): string
+ {
+ return $this->alias ?: '';
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check(Request $request, string $url, bool $completeMatch = false)
+ {
+ // 检查分组有效性
+ if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
+ return false;
+ }
+
+ // 解析分组路由
+ if ($this instanceof Resource) {
+ $this->buildResourceRule();
+ } else {
+ $this->parseGroupRule($this->rule);
+ }
+
+ // 获取当前路由规则
+ $method = strtolower($request->method());
+ $rules = $this->getRules($method);
+
+ if ($this->parent) {
+ // 合并分组参数
+ $this->mergeGroupOptions();
+ // 合并分组变量规则
+ $this->pattern = array_merge($this->parent->getPattern(), $this->pattern);
+ }
+
+ if (isset($this->option['complete_match'])) {
+ $completeMatch = $this->option['complete_match'];
+ }
+
+ if (!empty($this->option['merge_rule_regex'])) {
+ // 合并路由正则规则进行路由匹配检查
+ $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+
+ // 检查分组路由
+ foreach ($rules as $key => $item) {
+ $result = $item[1]->check($request, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+
+ if ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) {
+ // 未匹配所有路由的路由规则处理
+ $result = $this->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->mergeGroupOptions());
+ } else {
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 分组URL匹配检查
+ * @access protected
+ * @param string $url URL
+ * @return bool
+ */
+ protected function checkUrl(string $url): bool
+ {
+ if ($this->fullName) {
+ $pos = strpos($this->fullName, '<');
+
+ if (false !== $pos) {
+ $str = substr($this->fullName, 0, $pos);
+ } else {
+ $str = $this->fullName;
+ }
+
+ if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 设置路由分组别名
+ * @access public
+ * @param string $alias 路由分组别名
+ * @return $this
+ */
+ public function alias(string $alias)
+ {
+ $this->alias = $alias;
+ $this->router->getRuleName()->setGroup($alias, $this);
+
+ return $this;
+ }
+
+ /**
+ * 延迟解析分组的路由规则
+ * @access public
+ * @param bool $lazy 路由是否延迟解析
+ * @return $this
+ */
+ public function lazy(bool $lazy = true)
+ {
+ if (!$lazy) {
+ $this->parseGroupRule($this->rule);
+ $this->rule = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 解析分组和域名的路由规则及绑定
+ * @access public
+ * @param mixed $rule 路由规则
+ * @return void
+ */
+ public function parseGroupRule($rule): void
+ {
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if ($rule instanceof \Closure) {
+ Container::getInstance()->invokeFunction($rule);
+ } elseif (is_string($rule) && $rule) {
+ $this->router->bind($rule, $this->domain);
+ }
+
+ $this->router->setGroup($origin);
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param array $rules 路由规则
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ protected function checkMergeRuleRegex(Request $request, array &$rules, string $url, bool $completeMatch)
+ {
+ $depr = $this->router->config('pathinfo_depr');
+ $url = $depr . str_replace('|', $depr, $url);
+ $regex = [];
+ $items = [];
+
+ foreach ($rules as $key => $val) {
+ $item = $val[1];
+ if ($item instanceof RuleItem) {
+ $rule = $depr . str_replace('/', $depr, $item->getRule());
+ if ($depr == $rule && $depr != $url) {
+ unset($rules[$key]);
+ continue;
+ }
+
+ $complete = $item->getOption('complete_match', $completeMatch);
+
+ if (false === strpos($rule, '<')) {
+ if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
+ return $item->checkRule($request, $url, []);
+ }
+
+ unset($rules[$key]);
+ continue;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ unset($rules[$key]);
+ continue;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ unset($rules[$key]);
+ $pattern = array_merge($this->getPattern(), $item->getPattern());
+ $option = array_merge($this->getOption(), $item->getOption());
+
+ $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
+ $items[$key] = $item;
+ }
+ }
+ }
+
+ if (empty($regex)) {
+ return false;
+ }
+
+ try {
+ $result = preg_match('/^(?:' . implode('|', $regex) . ')/u', $url, $match);
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ if ($result) {
+ $var = [];
+ foreach ($match as $key => $val) {
+ if (is_string($key) && '' !== $val) {
+ [$name, $pos] = explode('_THINK_', $key);
+
+ $var[$name] = $val;
+ }
+ }
+
+ if (!isset($pos)) {
+ foreach ($regex as $key => $item) {
+ if (0 === strpos(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
+ $pos = $key;
+ break;
+ }
+ }
+ }
+
+ $rule = $items[$pos]->getRule();
+ $array = $this->router->getRule($rule);
+
+ foreach ($array as $item) {
+ if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
+ $result = $item->checkRule($request, $url, $var);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取分组的MISS路由
+ * @access public
+ * @return RuleItem|null
+ */
+ public function getMissRule(): ? RuleItem
+ {
+ return $this->miss;
+ }
+
+ /**
+ * 注册MISS路由
+ * @access public
+ * @param string|Closure $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function miss($route, string $method = '*') : RuleItem
+ {
+ // 创建路由规则实例
+ $ruleItem = new RuleItem($this->router, $this, null, '', $route, strtolower($method));
+
+ $ruleItem->setMiss();
+ $this->miss = $ruleItem;
+
+ return $ruleItem;
+ }
+
+ /**
+ * 添加分组下的路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function addRule(string $rule, $route = null, string $method = '*'): RuleItem
+ {
+ // 读取路由标识
+ if (is_string($route)) {
+ $name = $route;
+ } else {
+ $name = null;
+ }
+
+ $method = strtolower($method);
+
+ if ('' === $rule || '/' === $rule) {
+ $rule .= '$';
+ }
+
+ // 创建路由规则实例
+ $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method);
+
+ $this->addRuleItem($ruleItem, $method);
+
+ return $ruleItem;
+ }
+
+ /**
+ * 注册分组下的路由规则
+ * @access public
+ * @param Rule $rule 路由规则
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function addRuleItem(Rule $rule, string $method = '*')
+ {
+ if (strpos($method, '|')) {
+ $rule->method($method);
+ $method = '*';
+ }
+
+ $this->rules[] = [$method, $rule];
+
+ if ($rule instanceof RuleItem && 'options' != $method) {
+ $this->rules[] = ['options', $rule->setAutoOptions()];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置分组的路由前缀
+ * @access public
+ * @param string $prefix 路由前缀
+ * @return $this
+ */
+ public function prefix(string $prefix)
+ {
+ if ($this->parent && $this->parent->getOption('prefix')) {
+ $prefix = $this->parent->getOption('prefix') . $prefix;
+ }
+
+ return $this->setOption('prefix', $prefix);
+ }
+
+ /**
+ * 合并分组的路由规则正则
+ * @access public
+ * @param bool $merge 是否合并
+ * @return $this
+ */
+ public function mergeRuleRegex(bool $merge = true)
+ {
+ return $this->setOption('merge_rule_regex', $merge);
+ }
+
+ /**
+ * 获取完整分组Name
+ * @access public
+ * @return string
+ */
+ public function getFullName(): string
+ {
+ return $this->fullName ?: '';
+ }
+
+ /**
+ * 获取分组的路由规则
+ * @access public
+ * @param string $method 请求类型
+ * @return array
+ */
+ public function getRules(string $method = ''): array
+ {
+ if ('' === $method) {
+ return $this->rules;
+ }
+
+ return array_filter($this->rules, function ($item) use ($method) {
+ return $method == $item[0] || $item[0] == '*';
+ });
+ }
+
+ /**
+ * 清空分组下的路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->rules = [];
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/RuleItem.php b/vendor/topthink/framework/src/think/route/RuleItem.php
new file mode 100644
index 0000000..302f36c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/RuleItem.php
@@ -0,0 +1,330 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\Exception;
+use think\Request;
+use think\Route;
+
+/**
+ * 路由规则类
+ */
+class RuleItem extends Rule
+{
+ /**
+ * 是否为MISS规则
+ * @var bool
+ */
+ protected $miss = false;
+
+ /**
+ * 是否为额外自动注册的OPTIONS规则
+ * @var bool
+ */
+ protected $autoOption = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由实例
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 路由标识
+ * @param string $rule 路由规则
+ * @param string|\Closure $route 路由地址
+ * @param string $method 请求类型
+ */
+ public function __construct(Route $router, RuleGroup $parent, string $name = null, string $rule = '', $route = null, string $method = '*')
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->name = $name;
+ $this->route = $route;
+ $this->method = $method;
+
+ $this->setRule($rule);
+
+ $this->router->setRule($this->rule, $this);
+ }
+
+ /**
+ * 设置当前路由规则为MISS路由
+ * @access public
+ * @return $this
+ */
+ public function setMiss()
+ {
+ $this->miss = true;
+ return $this;
+ }
+
+ /**
+ * 判断当前路由规则是否为MISS路由
+ * @access public
+ * @return bool
+ */
+ public function isMiss(): bool
+ {
+ return $this->miss;
+ }
+
+ /**
+ * 设置当前路由为自动注册OPTIONS
+ * @access public
+ * @return $this
+ */
+ public function setAutoOptions()
+ {
+ $this->autoOption = true;
+ return $this;
+ }
+
+ /**
+ * 判断当前路由规则是否为自动注册的OPTIONS路由
+ * @access public
+ * @return bool
+ */
+ public function isAutoOptions(): bool
+ {
+ return $this->autoOption;
+ }
+
+ /**
+ * 获取当前路由的URL后缀
+ * @access public
+ * @return string|null
+ */
+ public function getSuffix()
+ {
+ if (isset($this->option['ext'])) {
+ $suffix = $this->option['ext'];
+ } elseif ($this->parent->getOption('ext')) {
+ $suffix = $this->parent->getOption('ext');
+ } else {
+ $suffix = null;
+ }
+
+ return $suffix;
+ }
+
+ /**
+ * 路由规则预处理
+ * @access public
+ * @param string $rule 路由规则
+ * @return void
+ */
+ public function setRule(string $rule): void
+ {
+ if ('$' == substr($rule, -1, 1)) {
+ // 是否完整匹配
+ $rule = substr($rule, 0, -1);
+
+ $this->option['complete_match'] = true;
+ }
+
+ $rule = '/' != $rule ? ltrim($rule, '/') : '';
+
+ if ($this->parent && $prefix = $this->parent->getFullName()) {
+ $rule = $prefix . ($rule ? '/' . ltrim($rule, '/') : '');
+ }
+
+ if (false !== strpos($rule, ':')) {
+ $this->rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $rule);
+ } else {
+ $this->rule = $rule;
+ }
+
+ // 生成路由标识的快捷访问
+ $this->setRuleName();
+ }
+
+ /**
+ * 设置别名
+ * @access public
+ * @param string $name
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+ $this->setRuleName(true);
+
+ return $this;
+ }
+
+ /**
+ * 设置路由标识 用于URL反解生成
+ * @access protected
+ * @param bool $first 是否插入开头
+ * @return void
+ */
+ protected function setRuleName(bool $first = false): void
+ {
+ if ($this->name) {
+ $this->router->setName($this->name, $this, $first);
+ }
+ }
+
+ /**
+ * 检测路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $match 匹配路由变量
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function checkRule(Request $request, string $url, $match = null, bool $completeMatch = false)
+ {
+ // 检查参数有效性
+ if (!$this->checkOption($this->option, $request)) {
+ return false;
+ }
+
+ // 合并分组参数
+ $option = $this->mergeGroupOptions();
+
+ $url = $this->urlSuffixCheck($request, $url, $option);
+
+ if (is_null($match)) {
+ $match = $this->match($url, $option, $completeMatch);
+ }
+
+ if (false !== $match) {
+ return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match);
+ }
+
+ return false;
+ }
+
+ /**
+ * 检测路由(含路由匹配)
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check(Request $request, string $url, bool $completeMatch = false)
+ {
+ return $this->checkRule($request, $url, null, $completeMatch);
+ }
+
+ /**
+ * URL后缀及Slash检查
+ * @access protected
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $option 路由参数
+ * @return string
+ */
+ protected function urlSuffixCheck(Request $request, string $url, array $option = []): string
+ {
+ // 是否区分 / 地址访问
+ if (!empty($option['remove_slash']) && '/' != $this->rule) {
+ $this->rule = rtrim($this->rule, '/');
+ $url = rtrim($url, '|');
+ }
+
+ if (isset($option['ext'])) {
+ // 路由ext参数 优先于系统配置的URL伪静态后缀参数
+ $url = preg_replace('/\.(' . $request->ext() . ')$/i', '', $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * 检测URL和规则路由是否匹配
+ * @access private
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return array|false
+ */
+ private function match(string $url, array $option, bool $completeMatch)
+ {
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ $depr = $this->router->config('pathinfo_depr');
+ $pattern = array_merge($this->parent->getPattern(), $this->pattern);
+
+ // 检查完整规则定义
+ if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . ($completeMatch ? '$' : '') . '/', str_replace('|', $depr, $url))) {
+ return false;
+ }
+
+ $var = [];
+ $url = $depr . str_replace('|', $depr, $url);
+ $rule = $depr . str_replace('/', $depr, $this->rule);
+
+ if ($depr == $rule && $depr != $url) {
+ return false;
+ }
+
+ if (false === strpos($rule, '<')) {
+ if (0 === strcasecmp($rule, $url) || (!$completeMatch && 0 === strncasecmp($rule . $depr, $url . $depr, strlen($rule . $depr)))) {
+ return $var;
+ }
+ return false;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']?<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ return false;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ $regex = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $completeMatch);
+
+ try {
+ if (!preg_match('/^' . $regex . '/u', $url, $match)) {
+ return false;
+ }
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ foreach ($match as $key => $val) {
+ if (is_string($key)) {
+ $var[$key] = $val;
+ }
+ }
+ }
+
+ // 成功匹配后返回URL中的动态变量数组
+ return $var;
+ }
+
+ /**
+ * 设置路由所属分组(用于注解路由)
+ * @access public
+ * @param string $name 分组名称或者标识
+ * @return $this
+ */
+ public function group(string $name)
+ {
+ $group = $this->router->getRuleName()->getGroup($name);
+
+ if ($group) {
+ $this->parent = $group;
+ $this->setRule($this->rule);
+ }
+
+ return $this;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/RuleName.php b/vendor/topthink/framework/src/think/route/RuleName.php
new file mode 100644
index 0000000..4ffdd21
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/RuleName.php
@@ -0,0 +1,195 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+/**
+ * 路由标识管理类
+ */
+class RuleName
+{
+ /**
+ * 路由标识
+ * @var array
+ */
+ protected $item = [];
+
+ /**
+ * 路由规则
+ * @var array
+ */
+ protected $rule = [];
+
+ /**
+ * 路由分组
+ * @var array
+ */
+ protected $group = [];
+
+ /**
+ * 注册路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param RuleItem $ruleItem 路由规则
+ * @param bool $first 是否优先
+ * @return void
+ */
+ public function setName(string $name, RuleItem $ruleItem, bool $first = false): void
+ {
+ $name = strtolower($name);
+ if ($first && isset($this->item[$name])) {
+ array_unshift($this->item[$name], $ruleItem);
+ } else {
+ $this->item[$name][] = $ruleItem;
+ }
+ }
+
+ /**
+ * 注册路由分组标识
+ * @access public
+ * @param string $name 路由分组标识
+ * @param RuleGroup $group 路由分组
+ * @return void
+ */
+ public function setGroup(string $name, RuleGroup $group): void
+ {
+ $this->group[strtolower($name)] = $group;
+ }
+
+ /**
+ * 注册路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param RuleItem $ruleItem 路由
+ * @return void
+ */
+ public function setRule(string $rule, RuleItem $ruleItem): void
+ {
+ $route = $ruleItem->getRoute();
+
+ if (is_string($route)) {
+ $this->rule[$rule][$route] = $ruleItem;
+ } else {
+ $this->rule[$rule][] = $ruleItem;
+ }
+ }
+
+ /**
+ * 根据路由规则获取路由对象(列表)
+ * @access public
+ * @param string $rule 路由标识
+ * @return RuleItem[]
+ */
+ public function getRule(string $rule): array
+ {
+ return $this->rule[$rule] ?? [];
+ }
+
+ /**
+ * 根据路由分组标识获取分组
+ * @access public
+ * @param string $name 路由分组标识
+ * @return RuleGroup|null
+ */
+ public function getGroup(string $name)
+ {
+ return $this->group[strtolower($name)] ?? null;
+ }
+
+ /**
+ * 清空路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->item = [];
+ $this->rule = [];
+ }
+
+ /**
+ * 获取全部路由列表
+ * @access public
+ * @return array
+ */
+ public function getRuleList(): array
+ {
+ $list = [];
+
+ foreach ($this->rule as $rule => $rules) {
+ foreach ($rules as $item) {
+ $val = [];
+
+ foreach (['method', 'rule', 'name', 'route', 'domain', 'pattern', 'option'] as $param) {
+ $call = 'get' . $param;
+ $val[$param] = $item->$call();
+ }
+
+ if ($item->isMiss()) {
+ $val['rule'] .= '';
+ }
+
+ $list[] = $val;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * 导入路由标识
+ * @access public
+ * @param array $item 路由标识
+ * @return void
+ */
+ public function import(array $item): void
+ {
+ $this->item = $item;
+ }
+
+ /**
+ * 根据路由标识获取路由信息(用于URL生成)
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @param string $method 请求类型
+ * @return array
+ */
+ public function getName(string $name = null, string $domain = null, string $method = '*'): array
+ {
+ if (is_null($name)) {
+ return $this->item;
+ }
+
+ $name = strtolower($name);
+ $method = strtolower($method);
+ $result = [];
+
+ if (isset($this->item[$name])) {
+ if (is_null($domain)) {
+ $result = $this->item[$name];
+ } else {
+ foreach ($this->item[$name] as $item) {
+ $itemDomain = $item->getDomain();
+ $itemMethod = $item->getMethod();
+
+ if (($itemDomain == $domain || '-' == $itemDomain) && ('*' == $itemMethod || '*' == $method || $method == $itemMethod)) {
+ $result[] = $item;
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/route/Url.php b/vendor/topthink/framework/src/think/route/Url.php
new file mode 100644
index 0000000..d2165b8
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Url.php
@@ -0,0 +1,512 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\App;
+use think\Route;
+
+/**
+ * 路由地址生成
+ */
+class Url
+{
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ /**
+ * 路由对象
+ * @var Route
+ */
+ protected $route;
+
+ /**
+ * URL变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 路由URL
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * URL 根地址
+ * @var string
+ */
+ protected $root = '';
+
+ /**
+ * HTTPS
+ * @var bool
+ */
+ protected $https;
+
+ /**
+ * URL后缀
+ * @var string|bool
+ */
+ protected $suffix = true;
+
+ /**
+ * URL域名
+ * @var string|bool
+ */
+ protected $domain = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param string $url URL地址
+ * @param array $vars 参数
+ */
+ public function __construct(Route $route, App $app, string $url = '', array $vars = [])
+ {
+ $this->route = $route;
+ $this->app = $app;
+ $this->url = $url;
+ $this->vars = $vars;
+ }
+
+ /**
+ * 设置URL参数
+ * @access public
+ * @param array $vars URL参数
+ * @return $this
+ */
+ public function vars(array $vars = [])
+ {
+ $this->vars = $vars;
+ return $this;
+ }
+
+ /**
+ * 设置URL后缀
+ * @access public
+ * @param string|bool $suffix URL后缀
+ * @return $this
+ */
+ public function suffix($suffix)
+ {
+ $this->suffix = $suffix;
+ return $this;
+ }
+
+ /**
+ * 设置URL域名(或者子域名)
+ * @access public
+ * @param string|bool $domain URL域名
+ * @return $this
+ */
+ public function domain($domain)
+ {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ /**
+ * 设置URL 根地址
+ * @access public
+ * @param string $root URL root
+ * @return $this
+ */
+ public function root(string $root)
+ {
+ $this->root = $root;
+ return $this;
+ }
+
+ /**
+ * 设置是否使用HTTPS
+ * @access public
+ * @param bool $https
+ * @return $this
+ */
+ public function https(bool $https = true)
+ {
+ $this->https = $https;
+ return $this;
+ }
+
+ /**
+ * 检测域名
+ * @access protected
+ * @param string $url URL
+ * @param string|true $domain 域名
+ * @return string
+ */
+ protected function parseDomain(string &$url, $domain): string
+ {
+ if (!$domain) {
+ return '';
+ }
+
+ $request = $this->app->request;
+ $rootDomain = $request->rootDomain();
+
+ if (true === $domain) {
+ // 自动判断域名
+ $domain = $request->host();
+ $domains = $this->route->getDomains();
+
+ if (!empty($domains)) {
+ $routeDomain = array_keys($domains);
+ foreach ($routeDomain as $domainPrefix) {
+ if (0 === strpos($domainPrefix, '*.') && strpos($domain, ltrim($domainPrefix, '*.')) !== false) {
+ foreach ($domains as $key => $rule) {
+ $rule = is_array($rule) ? $rule[0] : $rule;
+ if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) {
+ $url = ltrim($url, $rule);
+ $domain = $key;
+
+ // 生成对应子域名
+ if (!empty($rootDomain)) {
+ $domain .= $rootDomain;
+ }
+ break;
+ } elseif (false !== strpos($key, '*')) {
+ if (!empty($rootDomain)) {
+ $domain .= $rootDomain;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+ } elseif (false === strpos($domain, '.') && 0 !== strpos($domain, $rootDomain)) {
+ $domain .= '.' . $rootDomain;
+ }
+
+ if (false !== strpos($domain, '://')) {
+ $scheme = '';
+ } else {
+ $scheme = $this->https || $request->isSsl() ? 'https://' : 'http://';
+ }
+
+ return $scheme . $domain;
+ }
+
+ /**
+ * 解析URL后缀
+ * @access protected
+ * @param string|bool $suffix 后缀
+ * @return string
+ */
+ protected function parseSuffix($suffix): string
+ {
+ if ($suffix) {
+ $suffix = true === $suffix ? $this->route->config('url_html_suffix') : $suffix;
+
+ if (is_string($suffix) && $pos = strpos($suffix, '|')) {
+ $suffix = substr($suffix, 0, $pos);
+ }
+ }
+
+ return (empty($suffix) || 0 === strpos($suffix, '.')) ? (string) $suffix : '.' . $suffix;
+ }
+
+ /**
+ * 直接解析URL地址
+ * @access protected
+ * @param string $url URL
+ * @param string|bool $domain Domain
+ * @return string
+ */
+ protected function parseUrl(string $url, &$domain): string
+ {
+ $request = $this->app->request;
+
+ if (0 === strpos($url, '/')) {
+ // 直接作为路由地址解析
+ $url = substr($url, 1);
+ } elseif (false !== strpos($url, '\\')) {
+ // 解析到类
+ $url = ltrim(str_replace('\\', '/', $url), '/');
+ } elseif (0 === strpos($url, '@')) {
+ // 解析到控制器
+ $url = substr($url, 1);
+ } elseif ('' === $url) {
+ $url = $request->controller() . '/' . $request->action();
+ } else {
+ $controller = $request->controller();
+
+ $path = explode('/', $url);
+ $action = array_pop($path);
+ $controller = empty($path) ? $controller : array_pop($path);
+
+ $url = $controller . '/' . $action;
+ }
+
+ return $url;
+ }
+
+ /**
+ * 分析路由规则中的变量
+ * @access protected
+ * @param string $rule 路由规则
+ * @return array
+ */
+ protected function parseVar(string $rule): array
+ {
+ // 提取路由规则中的变量
+ $var = [];
+
+ if (preg_match_all('/<\w+\??>/', $rule, $matches)) {
+ foreach ($matches[0] as $name) {
+ $optional = false;
+
+ if (strpos($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = true;
+ } else {
+ $name = substr($name, 1, -1);
+ }
+
+ $var[$name] = $optional ? 2 : 1;
+ }
+ }
+
+ return $var;
+ }
+
+ /**
+ * 匹配路由地址
+ * @access protected
+ * @param array $rule 路由规则
+ * @param array $vars 路由变量
+ * @param mixed $allowDomain 允许域名
+ * @return array
+ */
+ protected function getRuleUrl(array $rule, array &$vars = [], $allowDomain = ''): array
+ {
+ $request = $this->app->request;
+ if (is_string($allowDomain) && false === strpos($allowDomain, '.')) {
+ $allowDomain .= '.' . $request->rootDomain();
+ }
+ $port = $request->port();
+
+ foreach ($rule as $item) {
+ $url = $item->getRule();
+ $pattern = $this->parseVar($url);
+ $domain = $item->getDomain();
+ $suffix = $item->getSuffix();
+
+ if ('-' == $domain) {
+ $domain = is_string($allowDomain) ? $allowDomain : $request->host(true);
+ }
+
+ if (is_string($allowDomain) && $domain != $allowDomain) {
+ continue;
+ }
+
+ if ($port && !in_array($port, [80, 443])) {
+ $domain .= ':' . $port;
+ }
+
+ if (empty($pattern)) {
+ return [rtrim($url, '?/-'), $domain, $suffix];
+ }
+
+ $type = $this->route->config('url_common_param');
+ $keys = [];
+
+ foreach ($pattern as $key => $val) {
+ if (isset($vars[$key])) {
+ $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? $vars[$key] : urlencode((string) $vars[$key]), $url);
+ $keys[] = $key;
+ $url = str_replace(['/?', '-?'], ['/', '-'], $url);
+ $result = [rtrim($url, '?/-'), $domain, $suffix];
+ } elseif (2 == $val) {
+ $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url);
+ $url = str_replace(['/?', '-?'], ['/', '-'], $url);
+ $result = [rtrim($url, '?/-'), $domain, $suffix];
+ } else {
+ $result = null;
+ $keys = [];
+ break;
+ }
+ }
+
+ $vars = array_diff_key($vars, array_flip($keys));
+
+ if (isset($result)) {
+ return $result;
+ }
+ }
+
+ return [];
+ }
+
+ public function build()
+ {
+ // 解析URL
+ $url = $this->url;
+ $suffix = $this->suffix;
+ $domain = $this->domain;
+ $request = $this->app->request;
+ $vars = $this->vars;
+
+ if (0 === strpos($url, '[') && $pos = strpos($url, ']')) {
+ // [name] 表示使用路由命名标识生成URL
+ $name = substr($url, 1, $pos - 1);
+ $url = 'name' . substr($url, $pos + 1);
+ }
+
+ if (false === strpos($url, '://') && 0 !== strpos($url, '/')) {
+ $info = parse_url($url);
+ $url = !empty($info['path']) ? $info['path'] : '';
+
+ if (isset($info['fragment'])) {
+ // 解析锚点
+ $anchor = $info['fragment'];
+
+ if (false !== strpos($anchor, '?')) {
+ // 解析参数
+ [$anchor, $info['query']] = explode('?', $anchor, 2);
+ }
+
+ if (false !== strpos($anchor, '@')) {
+ // 解析域名
+ [$anchor, $domain] = explode('@', $anchor, 2);
+ }
+ } elseif (strpos($url, '@') && false === strpos($url, '\\')) {
+ // 解析域名
+ [$url, $domain] = explode('@', $url, 2);
+ }
+ }
+
+ if ($url) {
+ $checkName = isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : '');
+ $checkDomain = $domain && is_string($domain) ? $domain : null;
+
+ $rule = $this->route->getName($checkName, $checkDomain);
+
+ if (empty($rule) && isset($info['query'])) {
+ $rule = $this->route->getName($url, $checkDomain);
+ // 解析地址里面参数 合并到vars
+ parse_str($info['query'], $params);
+ $vars = array_merge($params, $vars);
+ unset($info['query']);
+ }
+ }
+
+ if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
+ // 匹配路由命名标识
+ $url = $match[0];
+
+ if ($domain && !empty($match[1])) {
+ $domain = $match[1];
+ }
+
+ if (!is_null($match[2])) {
+ $suffix = $match[2];
+ }
+ } elseif (!empty($rule) && isset($name)) {
+ throw new \InvalidArgumentException('route name not exists:' . $name);
+ } else {
+ // 检测URL绑定
+ $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
+
+ if ($bind && 0 === strpos($url, $bind)) {
+ $url = substr($url, strlen($bind) + 1);
+ } else {
+ $binds = $this->route->getBind();
+
+ foreach ($binds as $key => $val) {
+ if (is_string($val) && 0 === strpos($url, $val) && substr_count($val, '/') > 1) {
+ $url = substr($url, strlen($val) + 1);
+ $domain = $key;
+ break;
+ }
+ }
+ }
+
+ // 路由标识不存在 直接解析
+ $url = $this->parseUrl($url, $domain);
+
+ if (isset($info['query'])) {
+ // 解析地址里面参数 合并到vars
+ parse_str($info['query'], $params);
+ $vars = array_merge($params, $vars);
+ }
+ }
+
+ // 还原URL分隔符
+ $depr = $this->route->config('pathinfo_depr');
+ $url = str_replace('/', $depr, $url);
+
+ $file = $request->baseFile();
+ if ($file && 0 !== strpos($request->url(), $file)) {
+ $file = str_replace('\\', '/', dirname($file));
+ }
+
+ $url = rtrim($file, '/') . '/' . $url;
+
+ // URL后缀
+ if ('/' == substr($url, -1) || '' == $url) {
+ $suffix = '';
+ } else {
+ $suffix = $this->parseSuffix($suffix);
+ }
+
+ // 锚点
+ $anchor = !empty($anchor) ? '#' . $anchor : '';
+
+ // 参数组装
+ if (!empty($vars)) {
+ // 添加参数
+ if ($this->route->config('url_common_param')) {
+ $vars = http_build_query($vars);
+ $url .= $suffix . '?' . $vars . $anchor;
+ } else {
+ foreach ($vars as $var => $val) {
+ $val = (string) $val;
+ if ('' !== $val) {
+ $url .= $depr . $var . $depr . urlencode($val);
+ }
+ }
+
+ $url .= $suffix . $anchor;
+ }
+ } else {
+ $url .= $suffix . $anchor;
+ }
+
+ // 检测域名
+ $domain = $this->parseDomain($url, $domain);
+
+ // URL组装
+ return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
+ }
+
+ public function __toString()
+ {
+ return $this->build();
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'url' => $this->url,
+ 'vars' => $this->vars,
+ 'suffix' => $this->suffix,
+ 'domain' => $this->domain,
+ ];
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/dispatch/Callback.php b/vendor/topthink/framework/src/think/route/dispatch/Callback.php
new file mode 100644
index 0000000..e8a4e5d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/dispatch/Callback.php
@@ -0,0 +1,30 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route\dispatch;
+
+use think\route\Dispatch;
+
+/**
+ * Callback Dispatcher
+ */
+class Callback extends Dispatch
+{
+ public function exec()
+ {
+ // 执行回调方法
+ $vars = array_merge($this->request->param(), $this->param);
+
+ return $this->app->invoke($this->dispatch, $vars);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/route/dispatch/Controller.php b/vendor/topthink/framework/src/think/route/dispatch/Controller.php
new file mode 100644
index 0000000..e91bcd7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/dispatch/Controller.php
@@ -0,0 +1,183 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route\dispatch;
+
+use ReflectionClass;
+use ReflectionException;
+use ReflectionMethod;
+use think\App;
+use think\exception\ClassNotFoundException;
+use think\exception\HttpException;
+use think\helper\Str;
+use think\route\Dispatch;
+
+/**
+ * Controller Dispatcher
+ */
+class Controller extends Dispatch
+{
+ /**
+ * 控制器名
+ * @var string
+ */
+ protected $controller;
+
+ /**
+ * 操作名
+ * @var string
+ */
+ protected $actionName;
+
+ public function init(App $app)
+ {
+ parent::init($app);
+
+ $result = $this->dispatch;
+
+ if (is_string($result)) {
+ $result = explode('/', $result);
+ }
+
+ // 获取控制器名
+ $controller = strip_tags($result[0] ?: $this->rule->config('default_controller'));
+
+ if (strpos($controller, '.')) {
+ $pos = strrpos($controller, '.');
+ $this->controller = substr($controller, 0, $pos) . '.' . Str::studly(substr($controller, $pos + 1));
+ } else {
+ $this->controller = Str::studly($controller);
+ }
+
+ // 获取操作名
+ $this->actionName = strip_tags($result[1] ?: $this->rule->config('default_action'));
+
+ // 设置当前请求的控制器、操作
+ $this->request
+ ->setController($this->controller)
+ ->setAction($this->actionName);
+ }
+
+ public function exec()
+ {
+ try {
+ // 实例化控制器
+ $instance = $this->controller($this->controller);
+ } catch (ClassNotFoundException $e) {
+ throw new HttpException(404, 'controller not exists:' . $e->getClass());
+ }
+
+ // 注册控制器中间件
+ $this->registerControllerMiddleware($instance);
+
+ return $this->app->middleware->pipeline('controller')
+ ->send($this->request)
+ ->then(function () use ($instance) {
+ // 获取当前操作名
+ $suffix = $this->rule->config('action_suffix');
+ $action = $this->actionName . $suffix;
+
+ if (is_callable([$instance, $action])) {
+ $vars = $this->request->param();
+ try {
+ $reflect = new ReflectionMethod($instance, $action);
+ // 严格获取当前操作方法名
+ $actionName = $reflect->getName();
+ if ($suffix) {
+ $actionName = substr($actionName, 0, -strlen($suffix));
+ }
+
+ $this->request->setAction($actionName);
+ } catch (ReflectionException $e) {
+ $reflect = new ReflectionMethod($instance, '__call');
+ $vars = [$action, $vars];
+ $this->request->setAction($action);
+ }
+ } else {
+ // 操作不存在
+ throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
+ }
+
+ $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
+
+ return $this->autoResponse($data);
+ });
+ }
+
+ /**
+ * 使用反射机制注册控制器中间件
+ * @access public
+ * @param object $controller 控制器实例
+ * @return void
+ */
+ protected function registerControllerMiddleware($controller): void
+ {
+ $class = new ReflectionClass($controller);
+
+ if ($class->hasProperty('middleware')) {
+ $reflectionProperty = $class->getProperty('middleware');
+ $reflectionProperty->setAccessible(true);
+
+ $middlewares = $reflectionProperty->getValue($controller);
+
+ foreach ($middlewares as $key => $val) {
+ if (!is_int($key)) {
+ if (isset($val['only']) && !in_array($this->request->action(true), array_map(function ($item) {
+ return strtolower($item);
+ }, is_string($val['only']) ? explode(",", $val['only']) : $val['only']))) {
+ continue;
+ } elseif (isset($val['except']) && in_array($this->request->action(true), array_map(function ($item) {
+ return strtolower($item);
+ }, is_string($val['except']) ? explode(',', $val['except']) : $val['except']))) {
+ continue;
+ } else {
+ $val = $key;
+ }
+ }
+
+ if (is_string($val) && strpos($val, ':')) {
+ $val = explode(':', $val);
+ if (count($val) > 1) {
+ $val = [$val[0], array_slice($val, 1)];
+ }
+ }
+
+ $this->app->middleware->controller($val);
+ }
+ }
+ }
+
+ /**
+ * 实例化访问控制器
+ * @access public
+ * @param string $name 资源地址
+ * @return object
+ * @throws ClassNotFoundException
+ */
+ public function controller(string $name)
+ {
+ $suffix = $this->rule->config('controller_suffix') ? 'Controller' : '';
+
+ $controllerLayer = $this->rule->config('controller_layer') ?: 'controller';
+ $emptyController = $this->rule->config('empty_controller') ?: 'Error';
+
+ $class = $this->app->parseClass($controllerLayer, $name . $suffix);
+
+ if (class_exists($class)) {
+ return $this->app->make($class, [], true);
+ } elseif ($emptyController && class_exists($emptyClass = $this->app->parseClass($controllerLayer, $emptyController . $suffix))) {
+ return $this->app->make($emptyClass, [], true);
+ }
+
+ throw new ClassNotFoundException('class not exists:' . $class, $class);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/route/dispatch/Url.php b/vendor/topthink/framework/src/think/route/dispatch/Url.php
new file mode 100644
index 0000000..d7958e0
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/dispatch/Url.php
@@ -0,0 +1,118 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route\dispatch;
+
+use think\exception\HttpException;
+use think\helper\Str;
+use think\Request;
+use think\route\Rule;
+
+/**
+ * Url Dispatcher
+ */
+class Url extends Controller
+{
+
+ public function __construct(Request $request, Rule $rule, $dispatch)
+ {
+ $this->request = $request;
+ $this->rule = $rule;
+ // 解析默认的URL规则
+ $dispatch = $this->parseUrl($dispatch);
+
+ parent::__construct($request, $rule, $dispatch, $this->param);
+ }
+
+ /**
+ * 解析URL地址
+ * @access protected
+ * @param string $url URL
+ * @return array
+ */
+ protected function parseUrl(string $url): array
+ {
+ $depr = $this->rule->config('pathinfo_depr');
+ $bind = $this->rule->getRouter()->getDomainBind();
+
+ if ($bind && preg_match('/^[a-z]/is', $bind)) {
+ $bind = str_replace('/', $depr, $bind);
+ // 如果有域名绑定
+ $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
+ }
+
+ $path = $this->rule->parseUrlPath($url);
+ if (empty($path)) {
+ return [null, null];
+ }
+
+ // 解析控制器
+ $controller = !empty($path) ? array_shift($path) : null;
+
+ if ($controller && !preg_match('/^[A-Za-z0-9][\w|\.]*$/', $controller)) {
+ throw new HttpException(404, 'controller not exists:' . $controller);
+ }
+
+ // 解析操作
+ $action = !empty($path) ? array_shift($path) : null;
+ $var = [];
+
+ // 解析额外参数
+ if ($path) {
+ preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
+ $var[$match[1]] = strip_tags($match[2]);
+ }, implode('|', $path));
+ }
+
+ $panDomain = $this->request->panDomain();
+ if ($panDomain && $key = array_search('*', $var)) {
+ // 泛域名赋值
+ $var[$key] = $panDomain;
+ }
+
+ // 设置当前请求的参数
+ $this->param = $var;
+
+ // 封装路由
+ $route = [$controller, $action];
+
+ if ($this->hasDefinedRoute($route)) {
+ throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
+ }
+
+ return $route;
+ }
+
+ /**
+ * 检查URL是否已经定义过路由
+ * @access protected
+ * @param array $route 路由信息
+ * @return bool
+ */
+ protected function hasDefinedRoute(array $route): bool
+ {
+ [$controller, $action] = $route;
+
+ // 检查地址是否被定义过路由
+ $name = strtolower(Str::studly($controller) . '/' . $action);
+
+ $host = $this->request->host(true);
+ $method = $this->request->method();
+
+ if ($this->rule->getRouter()->getName($name, $host, $method)) {
+ return true;
+ }
+
+ return false;
+ }
+
+}
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..ca446f5
--- /dev/null
+++ b/vendor/topthink/framework/src/think/service/ModelService.php
@@ -0,0 +1,47 @@
+
+// +----------------------------------------------------------------------
+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'));
+ }
+
+ });
+ }
+}
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..4f8022d
--- /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/service/ValidateService.php b/vendor/topthink/framework/src/think/service/ValidateService.php
new file mode 100644
index 0000000..0093c73
--- /dev/null
+++ b/vendor/topthink/framework/src/think/service/ValidateService.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\service;
+
+use think\Service;
+use think\Validate;
+
+/**
+ * 验证服务类
+ */
+class ValidateService extends Service
+{
+ public function boot()
+ {
+ Validate::maker(function (Validate $validate) {
+ $validate->setLang($this->app->lang);
+ $validate->setDb($this->app->db);
+ $validate->setRequest($this->app->request);
+ });
+ }
+}
diff --git a/vendor/topthink/framework/src/think/session/Store.php b/vendor/topthink/framework/src/think/session/Store.php
new file mode 100644
index 0000000..211726b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/session/Store.php
@@ -0,0 +1,340 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\session;
+
+use think\contract\SessionHandlerInterface;
+use think\helper\Arr;
+
+class Store
+{
+
+ /**
+ * Session数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 是否初始化
+ * @var bool
+ */
+ protected $init = null;
+
+ /**
+ * 记录Session name
+ * @var string
+ */
+ protected $name = 'PHPSESSID';
+
+ /**
+ * 记录Session Id
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * @var SessionHandlerInterface
+ */
+ protected $handler;
+
+ /** @var array */
+ protected $serialize = [];
+
+ public function __construct($name, SessionHandlerInterface $handler, array $serialize = null)
+ {
+ $this->name = $name;
+ $this->handler = $handler;
+
+ if (!empty($serialize)) {
+ $this->serialize = $serialize;
+ }
+
+ $this->setId();
+ }
+
+ /**
+ * 设置数据
+ * @access public
+ * @param array $data
+ * @return void
+ */
+ public function setData(array $data): void
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * session初始化
+ * @access public
+ * @return void
+ */
+ public function init(): void
+ {
+ // 读取缓存数据
+ $data = $this->handler->read($this->getId());
+
+ if (!empty($data)) {
+ $this->data = array_merge($this->data, $this->unserialize($data));
+ }
+
+ $this->init = true;
+ }
+
+ /**
+ * 设置SessionName
+ * @access public
+ * @param string $name session_name
+ * @return void
+ */
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * 获取sessionName
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * session_id设置
+ * @access public
+ * @param string $id session_id
+ * @return void
+ */
+ public function setId($id = null): void
+ {
+ $this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
+ }
+
+ /**
+ * 获取session_id
+ * @access public
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * 获取所有数据
+ * @return array
+ */
+ public function all(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * session设置
+ * @access public
+ * @param string $name session名称
+ * @param mixed $value session值
+ * @return void
+ */
+ public function set(string $name, $value): void
+ {
+ Arr::set($this->data, $name, $value);
+ }
+
+ /**
+ * session获取
+ * @access public
+ * @param string $name session名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get(string $name, $default = null)
+ {
+ return Arr::get($this->data, $name, $default);
+ }
+
+ /**
+ * session获取并删除
+ * @access public
+ * @param string $name session名称
+ * @return mixed
+ */
+ public function pull(string $name)
+ {
+ return Arr::pull($this->data, $name);
+ }
+
+ /**
+ * 添加数据到一个session数组
+ * @access public
+ * @param string $key
+ * @param mixed $value
+ * @return void
+ */
+ public function push(string $key, $value): void
+ {
+ $array = $this->get($key, []);
+
+ $array[] = $value;
+
+ $this->set($key, $array);
+ }
+
+ /**
+ * 判断session数据
+ * @access public
+ * @param string $name session名称
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return Arr::has($this->data, $name);
+ }
+
+ /**
+ * 删除session数据
+ * @access public
+ * @param string $name session名称
+ * @return void
+ */
+ public function delete(string $name): void
+ {
+ Arr::forget($this->data, $name);
+ }
+
+ /**
+ * 清空session数据
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->data = [];
+ }
+
+ /**
+ * 销毁session
+ */
+ public function destroy(): void
+ {
+ $this->clear();
+
+ $this->regenerate(true);
+ }
+
+ /**
+ * 重新生成session id
+ * @param bool $destroy
+ */
+ public function regenerate(bool $destroy = false): void
+ {
+ if ($destroy) {
+ $this->handler->delete($this->getId());
+ }
+
+ $this->setId();
+ }
+
+ /**
+ * 保存session数据
+ * @access public
+ * @return void
+ */
+ public function save(): void
+ {
+ $this->clearFlashData();
+
+ $sessionId = $this->getId();
+
+ if (!empty($this->data)) {
+ $data = $this->serialize($this->data);
+
+ $this->handler->write($sessionId, $data);
+ } else {
+ $this->handler->delete($sessionId);
+ }
+
+ $this->init = false;
+ }
+
+ /**
+ * session设置 下一次请求有效
+ * @access public
+ * @param string $name session名称
+ * @param mixed $value session值
+ * @return void
+ */
+ public function flash(string $name, $value): void
+ {
+ $this->set($name, $value);
+ $this->push('__flash__.__next__', $name);
+ $this->set('__flash__.__current__', Arr::except($this->get('__flash__.__current__', []), $name));
+ }
+
+ /**
+ * 将本次闪存数据推迟到下次请求
+ *
+ * @return void
+ */
+ public function reflash(): void
+ {
+ $keys = $this->get('__flash__.__current__', []);
+ $values = array_unique(array_merge($this->get('__flash__.__next__', []), $keys));
+ $this->set('__flash__.__next__', $values);
+ $this->set('__flash__.__current__', []);
+ }
+
+ /**
+ * 清空当前请求的session数据
+ * @access public
+ * @return void
+ */
+ public function clearFlashData(): void
+ {
+ Arr::forget($this->data, $this->get('__flash__.__current__', []));
+ if (!empty($next = $this->get('__flash__.__next__', []))) {
+ $this->set('__flash__.__current__', $next);
+ } else {
+ $this->delete('__flash__.__current__');
+ }
+ $this->delete('__flash__.__next__');
+ }
+
+ /**
+ * 序列化数据
+ * @access protected
+ * @param mixed $data
+ * @return string
+ */
+ protected function serialize($data): string
+ {
+ $serialize = $this->serialize[0] ?? 'serialize';
+
+ return $serialize($data);
+ }
+
+ /**
+ * 反序列化数据
+ * @access protected
+ * @param string $data
+ * @return array
+ */
+ protected function unserialize(string $data): array
+ {
+ $unserialize = $this->serialize[1] ?? 'unserialize';
+
+ return (array) $unserialize($data);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/session/driver/Cache.php b/vendor/topthink/framework/src/think/session/driver/Cache.php
new file mode 100644
index 0000000..f19c876
--- /dev/null
+++ b/vendor/topthink/framework/src/think/session/driver/Cache.php
@@ -0,0 +1,50 @@
+
+// +----------------------------------------------------------------------
+namespace think\session\driver;
+
+use Psr\SimpleCache\CacheInterface;
+use think\contract\SessionHandlerInterface;
+use think\helper\Arr;
+
+class Cache implements SessionHandlerInterface
+{
+
+ /** @var CacheInterface */
+ protected $handler;
+
+ /** @var integer */
+ protected $expire;
+
+ /** @var string */
+ protected $prefix;
+
+ public function __construct(\think\Cache $cache, array $config = [])
+ {
+ $this->handler = $cache->store(Arr::get($config, 'store'));
+ $this->expire = Arr::get($config, 'expire', 1440);
+ $this->prefix = Arr::get($config, 'prefix', '');
+ }
+
+ public function read(string $sessionId): string
+ {
+ return (string) $this->handler->get($this->prefix . $sessionId);
+ }
+
+ public function delete(string $sessionId): bool
+ {
+ return $this->handler->delete($this->prefix . $sessionId);
+ }
+
+ public function write(string $sessionId, string $data): bool
+ {
+ return $this->handler->set($this->prefix . $sessionId, $data, $this->expire);
+ }
+}
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..3bb7cc5
--- /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->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . '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/src/think/validate/ValidateRule.php b/vendor/topthink/framework/src/think/validate/ValidateRule.php
new file mode 100644
index 0000000..5451783
--- /dev/null
+++ b/vendor/topthink/framework/src/think/validate/ValidateRule.php
@@ -0,0 +1,172 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\validate;
+
+/**
+ * Class ValidateRule
+ * @package think\validate
+ * @method ValidateRule confirm(mixed $rule, string $msg = '') static 验证是否和某个字段的值一致
+ * @method ValidateRule different(mixed $rule, string $msg = '') static 验证是否和某个字段的值是否不同
+ * @method ValidateRule egt(mixed $rule, string $msg = '') static 验证是否大于等于某个值
+ * @method ValidateRule gt(mixed $rule, string $msg = '') static 验证是否大于某个值
+ * @method ValidateRule elt(mixed $rule, string $msg = '') static 验证是否小于等于某个值
+ * @method ValidateRule lt(mixed $rule, string $msg = '') static 验证是否小于某个值
+ * @method ValidateRule eg(mixed $rule, string $msg = '') static 验证是否等于某个值
+ * @method ValidateRule in(mixed $rule, string $msg = '') static 验证是否在范围内
+ * @method ValidateRule notIn(mixed $rule, string $msg = '') static 验证是否不在某个范围
+ * @method ValidateRule between(mixed $rule, string $msg = '') static 验证是否在某个区间
+ * @method ValidateRule notBetween(mixed $rule, string $msg = '') static 验证是否不在某个区间
+ * @method ValidateRule length(mixed $rule, string $msg = '') static 验证数据长度
+ * @method ValidateRule max(mixed $rule, string $msg = '') static 验证数据最大长度
+ * @method ValidateRule min(mixed $rule, string $msg = '') static 验证数据最小长度
+ * @method ValidateRule after(mixed $rule, string $msg = '') static 验证日期
+ * @method ValidateRule before(mixed $rule, string $msg = '') static 验证日期
+ * @method ValidateRule expire(mixed $rule, string $msg = '') static 验证有效期
+ * @method ValidateRule allowIp(mixed $rule, string $msg = '') static 验证IP许可
+ * @method ValidateRule denyIp(mixed $rule, string $msg = '') static 验证IP禁用
+ * @method ValidateRule regex(mixed $rule, string $msg = '') static 使用正则验证数据
+ * @method ValidateRule token(mixed $rule='__token__', string $msg = '') static 验证表单令牌
+ * @method ValidateRule is(mixed $rule, string $msg = '') static 验证字段值是否为有效格式
+ * @method ValidateRule isRequire(mixed $rule = null, string $msg = '') static 验证字段必须
+ * @method ValidateRule isNumber(mixed $rule = null, string $msg = '') static 验证字段值是否为数字
+ * @method ValidateRule isArray(mixed $rule = null, string $msg = '') static 验证字段值是否为数组
+ * @method ValidateRule isInteger(mixed $rule = null, string $msg = '') static 验证字段值是否为整形
+ * @method ValidateRule isFloat(mixed $rule = null, string $msg = '') static 验证字段值是否为浮点数
+ * @method ValidateRule isMobile(mixed $rule = null, string $msg = '') static 验证字段值是否为手机
+ * @method ValidateRule isIdCard(mixed $rule = null, string $msg = '') static 验证字段值是否为身份证号码
+ * @method ValidateRule isChs(mixed $rule = null, string $msg = '') static 验证字段值是否为中文
+ * @method ValidateRule isChsDash(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母及下划线
+ * @method ValidateRule isChsAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为中文和字母
+ * @method ValidateRule isChsAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母和数字
+ * @method ValidateRule isDate(mixed $rule = null, string $msg = '') static 验证字段值是否为有效格式
+ * @method ValidateRule isBool(mixed $rule = null, string $msg = '') static 验证字段值是否为布尔值
+ * @method ValidateRule isAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为字母
+ * @method ValidateRule isAlphaDash(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和下划线
+ * @method ValidateRule isAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和数字
+ * @method ValidateRule isAccepted(mixed $rule = null, string $msg = '') static 验证字段值是否为yes, on, 或是 1
+ * @method ValidateRule isEmail(mixed $rule = null, string $msg = '') static 验证字段值是否为有效邮箱格式
+ * @method ValidateRule isUrl(mixed $rule = null, string $msg = '') static 验证字段值是否为有效URL地址
+ * @method ValidateRule activeUrl(mixed $rule, string $msg = '') static 验证是否为合格的域名或者IP
+ * @method ValidateRule ip(mixed $rule, string $msg = '') static 验证是否有效IP
+ * @method ValidateRule fileExt(mixed $rule, string $msg = '') static 验证文件后缀
+ * @method ValidateRule fileMime(mixed $rule, string $msg = '') static 验证文件类型
+ * @method ValidateRule fileSize(mixed $rule, string $msg = '') static 验证文件大小
+ * @method ValidateRule image(mixed $rule, string $msg = '') static 验证图像文件
+ * @method ValidateRule method(mixed $rule, string $msg = '') static 验证请求类型
+ * @method ValidateRule dateFormat(mixed $rule, string $msg = '') static 验证时间和日期是否符合指定格式
+ * @method ValidateRule unique(mixed $rule, string $msg = '') static 验证是否唯一
+ * @method ValidateRule behavior(mixed $rule, string $msg = '') static 使用行为类验证
+ * @method ValidateRule filter(mixed $rule, string $msg = '') static 使用filter_var方式验证
+ * @method ValidateRule requireIf(mixed $rule, string $msg = '') static 验证某个字段等于某个值的时候必须
+ * @method ValidateRule requireCallback(mixed $rule, string $msg = '') static 通过回调方法验证某个字段是否必须
+ * @method ValidateRule requireWith(mixed $rule, string $msg = '') static 验证某个字段有值的情况下必须
+ * @method ValidateRule must(mixed $rule = null, string $msg = '') static 必须验证
+ */
+class ValidateRule
+{
+ // 验证字段的名称
+ protected $title;
+
+ // 当前验证规则
+ protected $rule = [];
+
+ // 验证提示信息
+ protected $message = [];
+
+ /**
+ * 添加验证因子
+ * @access protected
+ * @param string $name 验证名称
+ * @param mixed $rule 验证规则
+ * @param string $msg 提示信息
+ * @return $this
+ */
+ protected function addItem(string $name, $rule = null, string $msg = '')
+ {
+ if ($rule || 0 === $rule) {
+ $this->rule[$name] = $rule;
+ } else {
+ $this->rule[] = $name;
+ }
+
+ $this->message[] = $msg;
+
+ return $this;
+ }
+
+ /**
+ * 获取验证规则
+ * @access public
+ * @return array
+ */
+ public function getRule(): array
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 获取验证字段名称
+ * @access public
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return $this->title ?: '';
+ }
+
+ /**
+ * 获取验证提示
+ * @access public
+ * @return array
+ */
+ public function getMsg(): array
+ {
+ return $this->message;
+ }
+
+ /**
+ * 设置验证字段名称
+ * @access public
+ * @return $this
+ */
+ public function title(string $title)
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ public function __call($method, $args)
+ {
+ if ('is' == strtolower(substr($method, 0, 2))) {
+ $method = substr($method, 2);
+ }
+
+ array_unshift($args, lcfirst($method));
+
+ return call_user_func_array([$this, 'addItem'], $args);
+ }
+
+ public static function __callStatic($method, $args)
+ {
+ $rule = new static();
+
+ if ('is' == strtolower(substr($method, 0, 2))) {
+ $method = substr($method, 2);
+ }
+
+ array_unshift($args, lcfirst($method));
+
+ return call_user_func_array([$rule, 'addItem'], $args);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/view/driver/Php.php b/vendor/topthink/framework/src/think/view/driver/Php.php
new file mode 100644
index 0000000..06d045b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/view/driver/Php.php
@@ -0,0 +1,191 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\view\driver;
+
+use RuntimeException;
+use think\App;
+use think\contract\TemplateHandlerInterface;
+use think\helper\Str;
+
+/**
+ * PHP原生模板驱动
+ */
+class Php implements TemplateHandlerInterface
+{
+ protected $template;
+ protected $content;
+ protected $app;
+
+ // 模板引擎参数
+ protected $config = [
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+ 'auto_rule' => 1,
+ // 视图目录名
+ 'view_dir_name' => 'view',
+ // 应用模板路径
+ 'view_path' => '',
+ // 模板文件后缀
+ 'view_suffix' => 'php',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ ];
+
+ public function __construct(App $app, array $config = [])
+ {
+ $this->app = $app;
+ $this->config = array_merge($this->config, (array) $config);
+ }
+
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ return is_file($template);
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ // 模板不存在 抛出异常
+ if (!is_file($template)) {
+ throw new RuntimeException('template not exists:' . $template);
+ }
+
+ $this->template = $template;
+
+ extract($data, EXTR_OVERWRITE);
+
+ include $this->template;
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void
+ {
+ $this->content = $content;
+
+ extract($data, EXTR_OVERWRITE);
+ eval('?>' . $this->content);
+ }
+
+ /**
+ * 自动定位模板文件
+ * @access private
+ * @param string $template 模板文件规则
+ * @return string
+ */
+ private function parseTemplate(string $template): string
+ {
+ $request = $this->app->request;
+
+ // 获取视图根目录
+ if (strpos($template, '@')) {
+ // 跨应用调用
+ [$app, $template] = explode('@', $template);
+ }
+
+ if ($this->config['view_path'] && !isset($app)) {
+ $path = $this->config['view_path'];
+ } else {
+ $appName = isset($app) ? $app : $this->app->http->getName();
+ $view = $this->config['view_dir_name'];
+
+ if (is_dir($this->app->getAppPath() . $view)) {
+ $path = isset($app) ? $this->app->getBasePath() . ($appName ? $appName . DIRECTORY_SEPARATOR : '') . $view . DIRECTORY_SEPARATOR : $this->app->getAppPath() . $view . DIRECTORY_SEPARATOR;
+ } else {
+ $path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . ($appName ? $appName . DIRECTORY_SEPARATOR : '');
+ }
+ }
+
+ $depr = $this->config['view_depr'];
+
+ if (0 !== strpos($template, '/')) {
+ $template = str_replace(['/', ':'], $depr, $template);
+ $controller = $request->controller();
+ if (strpos($controller, '.')) {
+ $pos = strrpos($controller, '.');
+ $controller = substr($controller, 0, $pos) . '.' . Str::snake(substr($controller, $pos + 1));
+ } else {
+ $controller = Str::snake($controller);
+ }
+
+ if ($controller) {
+ if ('' == $template) {
+ // 如果模板文件名为空 按照默认规则定位
+ if (2 == $this->config['auto_rule']) {
+ $template = $request->action(true);
+ } elseif (3 == $this->config['auto_rule']) {
+ $template = $request->action();
+ } else {
+ $template = Str::snake($request->action());
+ }
+
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ } elseif (false === strpos($template, $depr)) {
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ }
+ }
+ } else {
+ $template = str_replace(['/', ':'], $depr, substr($template, 1));
+ }
+
+ return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ /**
+ * 配置模板引擎
+ * @access private
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 获取模板引擎配置
+ * @access public
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getConfig(string $name)
+ {
+ return $this->config[$name] ?? null;
+ }
+}
diff --git a/vendor/topthink/framework/src/tpl/think_exception.tpl b/vendor/topthink/framework/src/tpl/think_exception.tpl
new file mode 100644
index 0000000..85baae4
--- /dev/null
+++ b/vendor/topthink/framework/src/tpl/think_exception.tpl
@@ -0,0 +1,502 @@
+'.end($names).'';
+ }
+}
+
+if (!function_exists('parse_file')) {
+ function parse_file($file, $line)
+ {
+ return ''.basename($file)." line {$line}".' ';
+ }
+}
+
+if (!function_exists('parse_args')) {
+ function parse_args($args)
+ {
+ $result = [];
+ foreach ($args as $key => $item) {
+ switch (true) {
+ case is_object($item):
+ $value = sprintf('object (%s)', parse_class(get_class($item)));
+ break;
+ case is_array($item):
+ if (count($item) > 3) {
+ $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
+ } else {
+ $value = sprintf('[%s]', parse_args($item));
+ }
+ break;
+ case is_string($item):
+ if (strlen($item) > 20) {
+ $value = sprintf(
+ '\'%s... \'',
+ htmlentities($item),
+ htmlentities(substr($item, 0, 20))
+ );
+ } else {
+ $value = sprintf("'%s'", htmlentities($item));
+ }
+ break;
+ case is_int($item):
+ case is_float($item):
+ $value = $item;
+ break;
+ case is_null($item):
+ $value = 'null ';
+ break;
+ case is_bool($item):
+ $value = '' . ($item ? 'true' : 'false') . ' ';
+ break;
+ case is_resource($item):
+ $value = 'resource ';
+ break;
+ default:
+ $value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
+ break;
+ }
+
+ $result[] = is_int($key) ? $value : "'{$key}' => {$value}";
+ }
+
+ return implode(', ', $result);
+ }
+}
+if (!function_exists('echo_value')) {
+ function echo_value($val)
+ {
+ if (is_array($val) || is_object($val)) {
+ echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
+ } elseif (is_bool($val)) {
+ echo $val ? 'true' : 'false';
+ } elseif (is_scalar($val)) {
+ echo htmlentities($val);
+ } else {
+ echo 'Resource';
+ }
+ }
+}
+?>
+
+
+
+
+ 系统发生错误
+
+
+
+
+
+ $trace) { ?>
+
+
+
+
+
+
+
Call Stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Exception Datas
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Environment Variables
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/topthink/framework/tests/AppTest.php b/vendor/topthink/framework/tests/AppTest.php
new file mode 100644
index 0000000..3ecdf61
--- /dev/null
+++ b/vendor/topthink/framework/tests/AppTest.php
@@ -0,0 +1,215 @@
+ 'class',
+ ];
+
+ public function register()
+ {
+
+ }
+
+ public function boot()
+ {
+
+ }
+}
+
+/**
+ * @property array initializers
+ */
+class AppTest extends TestCase
+{
+ /** @var App */
+ protected $app;
+
+ protected function setUp()
+ {
+ $this->app = new App();
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ public function testService()
+ {
+ $this->app->register(stdClass::class);
+
+ $this->assertInstanceOf(stdClass::class, $this->app->getService(stdClass::class));
+
+ $service = m::mock(SomeService::class);
+
+ $service->shouldReceive('register')->once();
+
+ $this->app->register($service);
+
+ $this->assertEquals($service, $this->app->getService(SomeService::class));
+
+ $service2 = m::mock(SomeService::class);
+
+ $service2->shouldReceive('register')->once();
+
+ $this->app->register($service2);
+
+ $this->assertEquals($service, $this->app->getService(SomeService::class));
+
+ $this->app->register($service2, true);
+
+ $this->assertEquals($service2, $this->app->getService(SomeService::class));
+
+ $service->shouldReceive('boot')->once();
+ $service2->shouldReceive('boot')->once();
+
+ $this->app->boot();
+ }
+
+ public function testDebug()
+ {
+ $this->app->debug(false);
+
+ $this->assertFalse($this->app->isDebug());
+
+ $this->app->debug(true);
+
+ $this->assertTrue($this->app->isDebug());
+ }
+
+ public function testNamespace()
+ {
+ $namespace = 'test';
+
+ $this->app->setNamespace($namespace);
+
+ $this->assertEquals($namespace, $this->app->getNamespace());
+ }
+
+ public function testVersion()
+ {
+ $this->assertEquals(App::VERSION, $this->app->version());
+ }
+
+ public function testPath()
+ {
+ $rootPath = __DIR__ . DIRECTORY_SEPARATOR;
+
+ $app = new App($rootPath);
+
+ $this->assertEquals($rootPath, $app->getRootPath());
+
+ $this->assertEquals(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $app->getThinkPath());
+
+ $this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getAppPath());
+
+ $appPath = $rootPath . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
+ $app->setAppPath($appPath);
+ $this->assertEquals($appPath, $app->getAppPath());
+
+ $this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getBasePath());
+
+ $this->assertEquals($rootPath . 'config' . DIRECTORY_SEPARATOR, $app->getConfigPath());
+
+ $this->assertEquals($rootPath . 'runtime' . DIRECTORY_SEPARATOR, $app->getRuntimePath());
+
+ $runtimePath = $rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
+ $app->setRuntimePath($runtimePath);
+ $this->assertEquals($runtimePath, $app->getRuntimePath());
+ }
+
+ /**
+ * @param vfsStreamDirectory $root
+ * @param bool $debug
+ * @return App
+ */
+ protected function prepareAppForInitialize(vfsStreamDirectory $root, $debug = true)
+ {
+ $rootPath = $root->url() . DIRECTORY_SEPARATOR;
+
+ $app = new App($rootPath);
+
+ $initializer = m::mock();
+ $initializer->shouldReceive('init')->once()->with($app);
+
+ $app->instance($initializer->mockery_getName(), $initializer);
+
+ (function () use ($initializer) {
+ $this->initializers = [$initializer->mockery_getName()];
+ })->call($app);
+
+ $env = m::mock(Env::class);
+ $env->shouldReceive('load')->once()->with($rootPath . '.env');
+ $env->shouldReceive('get')->once()->with('config_ext', '.php')->andReturn('.php');
+ $env->shouldReceive('get')->once()->with('app_debug')->andReturn($debug);
+
+ $event = m::mock(Event::class);
+ $event->shouldReceive('trigger')->once()->with(AppInit::class);
+ $event->shouldReceive('bind')->once()->with([]);
+ $event->shouldReceive('listenEvents')->once()->with([]);
+ $event->shouldReceive('subscribe')->once()->with([]);
+
+ $app->instance('env', $env);
+ $app->instance('event', $event);
+
+ return $app;
+ }
+
+ public function testInitialize()
+ {
+ $root = vfsStream::setup('rootDir', null, [
+ '.env' => '',
+ 'app' => [
+ 'common.php' => '',
+ 'event.php' => '[],"listen"=>[],"subscribe"=>[]];',
+ 'provider.php' => ' [
+ 'app.php' => 'prepareAppForInitialize($root, true);
+
+ $app->debug(false);
+
+ $app->initialize();
+
+ $this->assertIsInt($app->getBeginMem());
+ $this->assertIsFloat($app->getBeginTime());
+
+ $this->assertTrue($app->initialized());
+ }
+
+ public function testFactory()
+ {
+ $this->assertInstanceOf(stdClass::class, App::factory(stdClass::class));
+
+ $this->expectException(ClassNotFoundException::class);
+
+ App::factory('SomeClass');
+ }
+
+ public function testParseClass()
+ {
+ $this->assertEquals('app\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
+ $this->app->setNamespace('app2');
+ $this->assertEquals('app2\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
+ }
+
+}
diff --git a/vendor/topthink/framework/tests/CacheTest.php b/vendor/topthink/framework/tests/CacheTest.php
new file mode 100644
index 0000000..4e40888
--- /dev/null
+++ b/vendor/topthink/framework/tests/CacheTest.php
@@ -0,0 +1,149 @@
+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->cache = new Cache($this->app);
+ }
+
+ public function testGetConfig()
+ {
+ $config = [
+ 'default' => 'file',
+ ];
+
+ $this->config->shouldReceive('get')->with('cache')->andReturn($config);
+
+ $this->assertEquals($config, $this->cache->getConfig());
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->cache->getStoreConfig('foo');
+ }
+
+ public function testCacheManagerInstances()
+ {
+ $this->config->shouldReceive('get')->with("cache.stores.single", null)->andReturn(['type' => 'file']);
+
+ $channel1 = $this->cache->store('single');
+ $channel2 = $this->cache->store('single');
+
+ $this->assertSame($channel1, $channel2);
+ }
+
+ public function testFileCache()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("cache.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("cache.stores.file", null)->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->cache->set('foo', 5);
+ $this->cache->inc('foo');
+ $this->assertEquals(6, $this->cache->get('foo'));
+ $this->cache->dec('foo', 2);
+ $this->assertEquals(4, $this->cache->get('foo'));
+
+ $this->cache->set('bar', true);
+ $this->assertTrue($this->cache->get('bar'));
+
+ $this->cache->set('baz', null);
+ $this->assertNull($this->cache->get('baz'));
+
+ $this->assertTrue($this->cache->has('baz'));
+ $this->cache->delete('baz');
+ $this->assertFalse($this->cache->has('baz'));
+ $this->assertNull($this->cache->get('baz'));
+ $this->assertFalse($this->cache->get('baz', false));
+
+ $this->assertTrue($root->hasChildren());
+ $this->cache->clear();
+ $this->assertFalse($root->hasChildren());
+
+ //tags
+ $this->cache->tag('foo')->set('bar', 'foobar');
+ $this->assertEquals('foobar', $this->cache->get('bar'));
+ $this->cache->tag('foo')->clear();
+ $this->assertFalse($this->cache->has('bar'));
+
+ //multiple
+ $this->cache->setMultiple(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']]);
+ $this->assertEquals(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']], $this->cache->getMultiple(['foo', 'foobar']));
+ $this->assertTrue($this->cache->deleteMultiple(['foo', 'foobar']));
+ }
+
+ public function testRedisCache()
+ {
+ if (extension_loaded('redis')) {
+ return;
+ }
+ $this->config->shouldReceive('get')->with("cache.default", null)->andReturn('redis');
+ $this->config->shouldReceive('get')->with("cache.stores.redis", null)->andReturn(['type' => 'redis']);
+
+ $redis = m::mock('overload:\Predis\Client');
+
+ $redis->shouldReceive("set")->once()->with('foo', 5)->andReturnTrue();
+ $redis->shouldReceive("incrby")->once()->with('foo', 1)->andReturnTrue();
+ $redis->shouldReceive("decrby")->once()->with('foo', 2)->andReturnTrue();
+ $redis->shouldReceive("get")->once()->with('foo')->andReturn('6');
+ $redis->shouldReceive("get")->once()->with('foo')->andReturn('4');
+ $redis->shouldReceive("set")->once()->with('bar', serialize(true))->andReturnTrue();
+ $redis->shouldReceive("set")->once()->with('baz', serialize(null))->andReturnTrue();
+ $redis->shouldReceive("del")->once()->with('baz')->andReturnTrue();
+ $redis->shouldReceive("flushDB")->once()->andReturnTrue();
+ $redis->shouldReceive("set")->once()->with('bar', serialize('foobar'))->andReturnTrue();
+ $redis->shouldReceive("sAdd")->once()->with('tag:' . md5('foo'), 'bar')->andReturnTrue();
+ $redis->shouldReceive("sMembers")->once()->with('tag:' . md5('foo'))->andReturn(['bar']);
+ $redis->shouldReceive("del")->once()->with(['bar'])->andReturnTrue();
+ $redis->shouldReceive("del")->once()->with('tag:' . md5('foo'))->andReturnTrue();
+
+ $this->cache->set('foo', 5);
+ $this->cache->inc('foo');
+ $this->assertEquals(6, $this->cache->get('foo'));
+ $this->cache->dec('foo', 2);
+ $this->assertEquals(4, $this->cache->get('foo'));
+
+ $this->cache->set('bar', true);
+ $this->cache->set('baz', null);
+ $this->cache->delete('baz');
+ $this->cache->clear();
+
+ //tags
+ $this->cache->tag('foo')->set('bar', 'foobar');
+ $this->cache->tag('foo')->clear();
+ }
+}
diff --git a/vendor/topthink/framework/tests/ConfigTest.php b/vendor/topthink/framework/tests/ConfigTest.php
new file mode 100644
index 0000000..cab4759
--- /dev/null
+++ b/vendor/topthink/framework/tests/ConfigTest.php
@@ -0,0 +1,46 @@
+setContent(" 'value1','key2'=>'value2'];");
+ $root->addChild($file);
+
+ $config = new Config();
+
+ $config->load($file->url(), 'test');
+
+ $this->assertEquals('value1', $config->get('test.key1'));
+ $this->assertEquals('value2', $config->get('test.key2'));
+
+ $this->assertSame(['key1' => 'value1', 'key2' => 'value2'], $config->get('test'));
+ }
+
+ public function testSetAndGet()
+ {
+ $config = new Config();
+
+ $config->set([
+ 'key1' => 'value1',
+ 'key2' => [
+ 'key3' => 'value3',
+ ],
+ ], 'test');
+
+ $this->assertTrue($config->has('test.key1'));
+ $this->assertEquals('value1', $config->get('test.key1'));
+ $this->assertEquals('value3', $config->get('test.key2.key3'));
+
+ $this->assertEquals(['key3' => 'value3'], $config->get('test.key2'));
+ $this->assertFalse($config->has('test.key3'));
+ $this->assertEquals('none', $config->get('test.key3', 'none'));
+ }
+}
diff --git a/vendor/topthink/framework/tests/ContainerTest.php b/vendor/topthink/framework/tests/ContainerTest.php
new file mode 100644
index 0000000..ec5adb6
--- /dev/null
+++ b/vendor/topthink/framework/tests/ContainerTest.php
@@ -0,0 +1,313 @@
+name = $name;
+ }
+
+ public function some(Container $container)
+ {
+ }
+
+ protected function protectionFun()
+ {
+ return true;
+ }
+
+ public static function test(Container $container)
+ {
+ return $container;
+ }
+
+ public static function __make()
+ {
+ return new self('Taylor');
+ }
+}
+
+class SomeClass
+{
+ public $container;
+
+ public $count = 0;
+
+ public function __construct(Container $container)
+ {
+ $this->container = $container;
+ }
+}
+
+class ContainerTest extends TestCase
+{
+ protected function tearDown(): void
+ {
+ Container::setInstance(null);
+ }
+
+ public function testClosureResolution()
+ {
+ $container = new Container;
+
+ Container::setInstance($container);
+
+ $container->bind('name', function () {
+ return 'Taylor';
+ });
+
+ $this->assertEquals('Taylor', $container->make('name'));
+
+ $this->assertEquals('Taylor', Container::pull('name'));
+ }
+
+ public function testGet()
+ {
+ $container = new Container;
+
+ $this->expectException(ClassNotFoundException::class);
+ $this->expectExceptionMessage('class not exists: name');
+ $container->get('name');
+
+ $container->bind('name', function () {
+ return 'Taylor';
+ });
+
+ $this->assertSame('Taylor', $container->get('name'));
+ }
+
+ public function testExist()
+ {
+ $container = new Container;
+
+ $container->bind('name', function () {
+ return 'Taylor';
+ });
+
+ $this->assertFalse($container->exists("name"));
+
+ $container->make('name');
+
+ $this->assertTrue($container->exists('name'));
+ }
+
+ public function testInstance()
+ {
+ $container = new Container;
+
+ $container->bind('name', function () {
+ return 'Taylor';
+ });
+
+ $this->assertEquals('Taylor', $container->get('name'));
+
+ $container->bind('name2', Taylor::class);
+
+ $object = new stdClass();
+
+ $this->assertFalse($container->exists('name2'));
+
+ $container->instance('name2', $object);
+
+ $this->assertTrue($container->exists('name2'));
+
+ $this->assertTrue($container->exists(Taylor::class));
+
+ $this->assertEquals($object, $container->make(Taylor::class));
+
+ unset($container->name1);
+
+ $this->assertFalse($container->exists('name1'));
+
+ $container->delete('name2');
+
+ $this->assertFalse($container->exists('name2'));
+
+ foreach ($container as $class => $instance) {
+
+ }
+ }
+
+ public function testBind()
+ {
+ $container = new Container;
+
+ $object = new stdClass();
+
+ $container->bind(['name' => Taylor::class]);
+
+ $container->bind('name2', $object);
+
+ $container->bind('name3', Taylor::class);
+
+ $container->name4 = $object;
+
+ $container['name5'] = $object;
+
+ $this->assertTrue(isset($container->name4));
+
+ $this->assertTrue(isset($container['name5']));
+
+ $this->assertInstanceOf(Taylor::class, $container->get('name'));
+
+ $this->assertSame($object, $container->get('name2'));
+
+ $this->assertSame($object, $container->name4);
+
+ $this->assertSame($object, $container['name5']);
+
+ $this->assertInstanceOf(Taylor::class, $container->get('name3'));
+
+ unset($container['name']);
+
+ $this->assertFalse(isset($container['name']));
+
+ unset($container->name3);
+
+ $this->assertFalse(isset($container->name3));
+ }
+
+ public function testAutoConcreteResolution()
+ {
+ $container = new Container;
+
+ $taylor = $container->make(Taylor::class);
+
+ $this->assertInstanceOf(Taylor::class, $taylor);
+ $this->assertAttributeSame('Taylor', 'name', $taylor);
+ }
+
+ public function testGetAndSetInstance()
+ {
+ $this->assertInstanceOf(Container::class, Container::getInstance());
+
+ $object = new stdClass();
+
+ Container::setInstance($object);
+
+ $this->assertSame($object, Container::getInstance());
+
+ Container::setInstance(function () {
+ return $this;
+ });
+
+ $this->assertSame($this, Container::getInstance());
+ }
+
+ public function testResolving()
+ {
+ $container = new Container();
+ $container->bind(Container::class, $container);
+
+ $container->resolving(function (SomeClass $taylor, Container $container) {
+ $taylor->count++;
+ });
+ $container->resolving(SomeClass::class, function (SomeClass $taylor, Container $container) {
+ $taylor->count++;
+ });
+
+ /** @var SomeClass $someClass */
+ $someClass = $container->invokeClass(SomeClass::class);
+ $this->assertEquals(2, $someClass->count);
+ }
+
+ public function testInvokeFunctionWithoutMethodThrowsException()
+ {
+ $this->expectException(FuncNotFoundException::class);
+ $this->expectExceptionMessage('function not exists: ContainerTestCallStub()');
+ $container = new Container();
+ $container->invokeFunction('ContainerTestCallStub', []);
+ }
+
+ public function testInvokeProtectionMethod()
+ {
+ $container = new Container();
+ $this->assertTrue($container->invokeMethod([Taylor::class, 'protectionFun'], [], true));
+ }
+
+ public function testInvoke()
+ {
+ $container = new Container();
+
+ Container::setInstance($container);
+
+ $container->bind(Container::class, $container);
+
+ $stub = $this->createMock(Taylor::class);
+
+ $stub->expects($this->once())->method('some')->with($container)->will($this->returnSelf());
+
+ $container->invokeMethod([$stub, 'some']);
+
+ $this->assertEquals('48', $container->invoke('ord', ['0']));
+
+ $this->assertSame($container, $container->invoke(Taylor::class . '::test', []));
+
+ $this->assertSame($container, $container->invokeMethod(Taylor::class . '::test'));
+
+ $reflect = new ReflectionMethod($container, 'exists');
+
+ $this->assertTrue($container->invokeReflectMethod($container, $reflect, [Container::class]));
+
+ $this->assertSame($container, $container->invoke(function (Container $container) {
+ return $container;
+ }));
+
+ $this->assertSame($container, $container->invoke(Taylor::class . '::test'));
+
+ $object = $container->invokeClass(SomeClass::class);
+ $this->assertInstanceOf(SomeClass::class, $object);
+ $this->assertSame($container, $object->container);
+
+ $stdClass = new stdClass();
+
+ $container->invoke(function (Container $container, stdClass $stdObject, $key1, $lowKey, $key2 = 'default') use ($stdClass) {
+ $this->assertEquals('value1', $key1);
+ $this->assertEquals('default', $key2);
+ $this->assertEquals('value2', $lowKey);
+ $this->assertSame($stdClass, $stdObject);
+ return $container;
+ }, ['some' => $stdClass, 'key1' => 'value1', 'low_key' => 'value2']);
+ }
+
+ public function testInvokeMethodNotExists()
+ {
+ $container = $this->resolveContainer();
+ $this->expectException(FuncNotFoundException::class);
+
+ $container->invokeMethod([SomeClass::class, 'any']);
+ }
+
+ public function testInvokeClassNotExists()
+ {
+ $container = new Container();
+
+ Container::setInstance($container);
+
+ $container->bind(Container::class, $container);
+
+ $this->expectExceptionObject(new ClassNotFoundException('class not exists: SomeClass'));
+
+ $container->invokeClass('SomeClass');
+ }
+
+ protected function resolveContainer()
+ {
+ $container = new Container();
+
+ Container::setInstance($container);
+ return $container;
+ }
+
+}
diff --git a/vendor/topthink/framework/tests/DbTest.php b/vendor/topthink/framework/tests/DbTest.php
new file mode 100644
index 0000000..a8e4e40
--- /dev/null
+++ b/vendor/topthink/framework/tests/DbTest.php
@@ -0,0 +1,49 @@
+shouldReceive('get')->with('database.cache_store', null)->andReturn(null);
+ $cache->shouldReceive('store')->with(null)->andReturn($store);
+
+ $db = Db::__make($event, $config, $log, $cache);
+
+ $config->shouldReceive('get')->with('database.foo', null)->andReturn('foo');
+ $this->assertEquals('foo', $db->getConfig('foo'));
+
+ $config->shouldReceive('get')->with('database', [])->andReturn([]);
+ $this->assertEquals([], $db->getConfig());
+
+ $callback = function () {
+ };
+ $event->shouldReceive('listen')->with('db.some', $callback);
+ $db->event('some', $callback);
+
+ $event->shouldReceive('trigger')->with('db.some', null, false);
+ $db->trigger('some');
+ }
+
+}
diff --git a/vendor/topthink/framework/tests/EnvTest.php b/vendor/topthink/framework/tests/EnvTest.php
new file mode 100644
index 0000000..c5b018c
--- /dev/null
+++ b/vendor/topthink/framework/tests/EnvTest.php
@@ -0,0 +1,82 @@
+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'));
+
+ $this->assertSame(['KEY1' => 'value1', 'KEY2' => 'value2'], $env->get());
+ }
+
+ 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..4eab68c
--- /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/FilesystemTest.php b/vendor/topthink/framework/tests/FilesystemTest.php
new file mode 100644
index 0000000..56019e8
--- /dev/null
+++ b/vendor/topthink/framework/tests/FilesystemTest.php
@@ -0,0 +1,131 @@
+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'));
+ }
+
+ public function testCache()
+ {
+ $this->config->shouldReceive('get')->with('filesystem.disks.local', null)->andReturn([
+ 'type' => 'local',
+ 'root' => $this->root->url(),
+ 'cache' => true,
+ ]);
+
+ $this->assertInstanceOf(Local::class, $this->filesystem->disk());
+
+ $this->config->shouldReceive('get')->with('filesystem.disks.cache', null)->andReturn([
+ 'type' => NullDriver::class,
+ 'root' => $this->root->url(),
+ 'cache' => [
+ 'store' => 'flysystem',
+ ],
+ ]);
+
+ $cache = m::mock(Cache::class);
+
+ $cacheDriver = m::mock(File::class);
+
+ $cache->shouldReceive('store')->once()->with('flysystem')->andReturn($cacheDriver);
+
+ $this->app->shouldReceive('make')->with(Cache::class)->andReturn($cache);
+
+ $cacheDriver->shouldReceive('get')->with('flysystem')->once()->andReturn(null);
+
+ $cacheDriver->shouldReceive('set')->withAnyArgs();
+
+ $this->filesystem->disk('cache')->put('test.txt', 'aa');
+ }
+
+ public function testPutFile()
+ {
+ $root = vfsStream::setup('rootDir', null, [
+ 'foo.jpg' => 'hello',
+ ]);
+
+ $this->config->shouldReceive('get')->with('filesystem.disks.local', null)->andReturn([
+ 'type' => NullDriver::class,
+ 'root' => $root->url(),
+ 'cache' => true,
+ ]);
+
+ $file = m::mock(\think\File::class);
+
+ $file->shouldReceive('hashName')->with(null)->once()->andReturn('foo.jpg');
+
+ $file->shouldReceive('getRealPath')->once()->andReturn($root->getChild('foo.jpg')->url());
+
+ $this->filesystem->putFile('test', $file);
+ }
+}
+
+class NullDriver extends Driver
+{
+ protected function createAdapter(): AdapterInterface
+ {
+ return new NullAdapter();
+ }
+}
diff --git a/vendor/topthink/framework/tests/HttpTest.php b/vendor/topthink/framework/tests/HttpTest.php
new file mode 100644
index 0000000..4240c52
--- /dev/null
+++ b/vendor/topthink/framework/tests/HttpTest.php
@@ -0,0 +1,154 @@
+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);
+
+ $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..6566a82
--- /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..677588e
--- /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..e0fc81a
--- /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/framework/tests/RouteTest.php b/vendor/topthink/framework/tests/RouteTest.php
new file mode 100644
index 0000000..3388999
--- /dev/null
+++ b/vendor/topthink/framework/tests/RouteTest.php
@@ -0,0 +1,285 @@
+prepareApp();
+ $this->route = new Route($this->app);
+ }
+
+ /**
+ * @param $path
+ * @param string $method
+ * @param string $host
+ * @return m\Mock|Request
+ */
+ protected function makeRequest($path, $method = 'GET', $host = 'localhost')
+ {
+ $request = m::mock(Request::class)->makePartial();
+ $request->shouldReceive('host')->andReturn($host);
+ $request->shouldReceive('pathinfo')->andReturn($path);
+ $request->shouldReceive('url')->andReturn('/' . $path);
+ $request->shouldReceive('method')->andReturn(strtoupper($method));
+ return $request;
+ }
+
+ public function testSimpleRequest()
+ {
+ $this->route->get('foo', function () {
+ return 'get-foo';
+ });
+
+ $this->route->put('foo', function () {
+ return 'put-foo';
+ });
+
+ $this->route->group(function () {
+ $this->route->post('foo', function () {
+ return 'post-foo';
+ });
+ });
+
+ $request = $this->makeRequest('foo', 'post');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(200, $response->getCode());
+ $this->assertEquals('post-foo', $response->getContent());
+
+ $request = $this->makeRequest('foo', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(200, $response->getCode());
+ $this->assertEquals('get-foo', $response->getContent());
+ }
+
+ public function testOptionsRequest()
+ {
+ $this->route->get('foo', function () {
+ return 'get-foo';
+ });
+
+ $this->route->put('foo', function () {
+ return 'put-foo';
+ });
+
+ $this->route->group(function () {
+ $this->route->post('foo', function () {
+ return 'post-foo';
+ });
+ });
+ $this->route->group('abc', function () {
+ $this->route->post('foo/:id', function () {
+ return 'post-abc-foo';
+ });
+ });
+
+ $this->route->post('foo/:id', function () {
+ return 'post-abc-foo';
+ });
+
+ $this->route->resource('bar', 'SomeClass');
+
+ $request = $this->makeRequest('foo', 'options');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(204, $response->getCode());
+ $this->assertEquals('GET, PUT, POST', $response->getHeader('Allow'));
+
+ $request = $this->makeRequest('bar', 'options');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(204, $response->getCode());
+ $this->assertEquals('GET, POST', $response->getHeader('Allow'));
+
+ $request = $this->makeRequest('bar/1', 'options');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(204, $response->getCode());
+ $this->assertEquals('GET, PUT, DELETE', $response->getHeader('Allow'));
+
+ $request = $this->makeRequest('xxxx', 'options');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(204, $response->getCode());
+ $this->assertEquals('GET, POST, PUT, DELETE', $response->getHeader('Allow'));
+ }
+
+ public function testAllowCrossDomain()
+ {
+ $this->route->get('foo', function () {
+ return 'get-foo';
+ })->allowCrossDomain(['some' => 'bar']);
+
+ $request = $this->makeRequest('foo', 'get');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals('bar', $response->getHeader('some'));
+ $this->assertArrayHasKey('Access-Control-Allow-Credentials', $response->getHeader());
+
+ $request = $this->makeRequest('foo2', 'options');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals(204, $response->getCode());
+ $this->assertArrayHasKey('Access-Control-Allow-Credentials', $response->getHeader());
+ $this->assertEquals('GET, POST, PUT, DELETE', $response->getHeader('Allow'));
+ }
+
+ public function testControllerDispatch()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::Mock(\stdClass::class);
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ public function testEmptyControllerDispatch()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::Mock(\stdClass::class);
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Error')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ protected function createMiddleware($times = 1)
+ {
+ $middleware = m::mock(Str::random(5));
+ $middleware->shouldReceive('handle')->times($times)->andReturnUsing(function ($request, Closure $next) {
+ return $next($request);
+ });
+ $this->app->shouldReceive('make')->with($middleware->mockery_getName())->andReturn($middleware);
+
+ return $middleware;
+ }
+
+ public function testControllerWithMiddleware()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::mock(FooClass::class);
+
+ $controller->middleware = [
+ $this->createMiddleware()->mockery_getName() . ":params1:params2",
+ $this->createMiddleware(0)->mockery_getName() => ['except' => 'bar'],
+ $this->createMiddleware()->mockery_getName() => ['only' => 'bar'],
+ ];
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->once()->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ public function testUrlDispatch()
+ {
+ $controller = m::mock(FooClass::class);
+ $controller->shouldReceive('index')->andReturn('bar');
+
+ $this->app->shouldReceive('parseClass')->once()->with('controller', 'Foo')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ public function testRedirectDispatch()
+ {
+ $this->route->redirect('foo', 'http://localhost', 302);
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+
+ $this->assertInstanceOf(Redirect::class, $response);
+ $this->assertEquals(302, $response->getCode());
+ $this->assertEquals('http://localhost', $response->getData());
+ }
+
+ public function testViewDispatch()
+ {
+ $this->route->view('foo', 'index/hello', ['city' => 'shanghai']);
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+
+ $this->assertInstanceOf(View::class, $response);
+ $this->assertEquals(['city' => 'shanghai'], $response->getVars());
+ $this->assertEquals('index/hello', $response->getData());
+ }
+
+ public function testResponseDispatch()
+ {
+ $this->route->get('hello/:name', response()
+ ->data('Hello,ThinkPHP')
+ ->code(200)
+ ->contentType('text/plain'));
+
+ $request = $this->makeRequest('hello/some');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals('Hello,ThinkPHP', $response->getContent());
+ $this->assertEquals(200, $response->getCode());
+ }
+
+ public function testDomainBindResponse()
+ {
+ $this->route->domain('test', function () {
+ $this->route->get('/', function () {
+ return 'Hello,ThinkPHP';
+ });
+ });
+
+ $request = $this->makeRequest('', 'get', 'test.domain.com');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals('Hello,ThinkPHP', $response->getContent());
+ $this->assertEquals(200, $response->getCode());
+ }
+
+}
+
+class FooClass
+{
+ public $middleware = [];
+
+ public function bar()
+ {
+
+ }
+}
diff --git a/vendor/topthink/framework/tests/SessionTest.php b/vendor/topthink/framework/tests/SessionTest.php
new file mode 100644
index 0000000..4bbdec2
--- /dev/null
+++ b/vendor/topthink/framework/tests/SessionTest.php
@@ -0,0 +1,225 @@
+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);
+ $handlerClass = "\\think\\session\\driver\\Test" . Str::random(10);
+ $this->config->shouldReceive("get")->with("session.type", "file")->andReturn($handlerClass);
+ $this->session = new Session($this->app);
+
+ $this->handler = m::mock('overload:' . $handlerClass, SessionHandlerInterface::class);
+ }
+
+ public function testLoadData()
+ {
+ $data = [
+ "bar" => 'foo',
+ ];
+
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive("read")->once()->with($id)->andReturn(serialize($data));
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->assertEquals('foo', $this->session->get('bar'));
+ $this->assertTrue($this->session->has('bar'));
+ $this->assertFalse($this->session->has('foo'));
+
+ $this->session->set('foo', 'bar');
+ $this->assertTrue($this->session->has('foo'));
+
+ $this->assertEquals('bar', $this->session->pull('foo'));
+ $this->assertFalse($this->session->has('foo'));
+ }
+
+ public function testSave()
+ {
+
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
+
+ $this->handler->shouldReceive('write')->once()->with($id, serialize([
+ "bar" => 'foo',
+ ]))->andReturnTrue();
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->session->set('bar', 'foo');
+
+ $this->session->save();
+ }
+
+ public function testFlash()
+ {
+ $this->session->flash('foo', 'bar');
+ $this->session->flash('bar', 0);
+ $this->session->flash('baz', true);
+
+ $this->assertTrue($this->session->has('foo'));
+ $this->assertEquals('bar', $this->session->get('foo'));
+ $this->assertEquals(0, $this->session->get('bar'));
+ $this->assertTrue($this->session->get('baz'));
+
+ $this->session->clearFlashData();
+
+ $this->assertTrue($this->session->has('foo'));
+ $this->assertEquals('bar', $this->session->get('foo'));
+ $this->assertEquals(0, $this->session->get('bar'));
+
+ $this->session->clearFlashData();
+
+ $this->assertFalse($this->session->has('foo'));
+ $this->assertNull($this->session->get('foo'));
+
+ $this->session->flash('foo', 'bar');
+ $this->assertTrue($this->session->has('foo'));
+ $this->session->clearFlashData();
+ $this->session->reflash();
+ $this->session->clearFlashData();
+
+ $this->assertTrue($this->session->has('foo'));
+ }
+
+ public function testClear()
+ {
+ $this->session->set('bar', 'foo');
+ $this->assertEquals('foo', $this->session->get('bar'));
+ $this->session->clear();
+ $this->assertFalse($this->session->has('foo'));
+ }
+
+ public function testSetName()
+ {
+ $this->session->setName('foo');
+ $this->assertEquals('foo', $this->session->getName());
+ }
+
+ public function testDestroy()
+ {
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
+ $this->handler->shouldReceive('delete')->once()->with($id)->andReturnTrue();
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->session->set('bar', 'foo');
+
+ $this->session->destroy();
+
+ $this->assertFalse($this->session->has('bar'));
+
+ $this->assertNotEquals($id, $this->session->getId());
+ }
+
+ public function testFileHandler()
+ {
+ $root = vfsStream::setup();
+
+ vfsStream::newFile('bar')
+ ->at($root)
+ ->lastModified(time());
+
+ vfsStream::newFile('bar')
+ ->at(vfsStream::newDirectory("foo")->at($root))
+ ->lastModified(100);
+
+ $this->assertTrue($root->hasChild("bar"));
+ $this->assertTrue($root->hasChild("foo/bar"));
+
+ $handler = new TestFileHandle($this->app, [
+ 'path' => $root->url(),
+ 'gc_probability' => 1,
+ 'gc_divisor' => 1,
+ ]);
+
+ $this->assertTrue($root->hasChild("bar"));
+ $this->assertFalse($root->hasChild("foo/bar"));
+
+ $id = md5(uniqid());
+ $handler->write($id, "bar");
+
+ $this->assertTrue($root->hasChild("sess_{$id}"));
+
+ $this->assertEquals("bar", $handler->read($id));
+
+ $handler->delete($id);
+
+ $this->assertFalse($root->hasChild("sess_{$id}"));
+ }
+
+ public function testCacheHandler()
+ {
+ $id = md5(uniqid());
+
+ $cache = m::mock(\think\Cache::class);
+
+ $store = m::mock(Driver::class);
+
+ $cache->shouldReceive('store')->once()->with('redis')->andReturn($store);
+
+ $handler = new Cache($cache, ['store' => 'redis']);
+
+ $store->shouldReceive("set")->with($id, "bar", 1440)->once()->andReturnTrue();
+ $handler->write($id, "bar");
+
+ $store->shouldReceive("get")->with($id)->once()->andReturn("bar");
+ $this->assertEquals("bar", $handler->read($id));
+
+ $store->shouldReceive("delete")->with($id)->once()->andReturnTrue();
+ $handler->delete($id);
+ }
+}
+
+class TestFileHandle extends File
+{
+ protected function writeFile($path, $content): bool
+ {
+ return (bool) file_put_contents($path, $content);
+ }
+}
diff --git a/vendor/topthink/framework/tests/ViewTest.php b/vendor/topthink/framework/tests/ViewTest.php
new file mode 100644
index 0000000..9412207
--- /dev/null
+++ b/vendor/topthink/framework/tests/ViewTest.php
@@ -0,0 +1,127 @@
+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->view = new View($this->app);
+ }
+
+ public function testAssignData()
+ {
+ $this->view->assign('foo', 'bar');
+ $this->view->assign(['baz' => 'boom']);
+ $this->view->qux = "corge";
+
+ $this->assertEquals('bar', $this->view->foo);
+ $this->assertEquals('boom', $this->view->baz);
+ $this->assertEquals('corge', $this->view->qux);
+ $this->assertTrue(isset($this->view->qux));
+ }
+
+ public function testRender()
+ {
+ $this->config->shouldReceive("get")->with("view.type", 'php')->andReturn(TestTemplate::class);
+
+ $this->view->filter(function ($content) {
+ return $content;
+ });
+
+ $this->assertEquals("fetch", $this->view->fetch('foo'));
+ $this->assertEquals("display", $this->view->display('foo'));
+ }
+
+}
+
+class TestTemplate implements TemplateHandlerInterface
+{
+
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool
+ {
+ return true;
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void
+ {
+ echo "fetch";
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void
+ {
+ echo "display";
+ }
+
+ /**
+ * 配置模板引擎
+ * @access private
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void
+ {
+ // TODO: Implement config() method.
+ }
+
+ /**
+ * 获取模板引擎配置
+ * @access public
+ * @param string $name 参数名
+ * @return void
+ */
+ public function getConfig(string $name)
+ {
+ // TODO: Implement getConfig() method.
+ }
+}
diff --git a/vendor/topthink/framework/tests/bootstrap.php b/vendor/topthink/framework/tests/bootstrap.php
new file mode 100644
index 0000000..ce93b97
--- /dev/null
+++ b/vendor/topthink/framework/tests/bootstrap.php
@@ -0,0 +1,3 @@
+ 以下类库都在`\\think\\helper`命名空间下
+
+## Str
+
+> 字符串操作
+
+```
+// 检查字符串中是否包含某些字符串
+Str::contains($haystack, $needles)
+
+// 检查字符串是否以某些字符串结尾
+Str::endsWith($haystack, $needles)
+
+// 获取指定长度的随机字母数字组合的字符串
+Str::random($length = 16)
+
+// 字符串转小写
+Str::lower($value)
+
+// 字符串转大写
+Str::upper($value)
+
+// 获取字符串的长度
+Str::length($value)
+
+// 截取字符串
+Str::substr($string, $start, $length = null)
+
+```
\ No newline at end of file
diff --git a/vendor/topthink/think-helper/composer.json b/vendor/topthink/think-helper/composer.json
new file mode 100644
index 0000000..4a31a6e
--- /dev/null
+++ b/vendor/topthink/think-helper/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "topthink/think-helper",
+ "description": "The ThinkPHP6 Helper Package",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ },
+ "files": [
+ "src/helper.php"
+ ]
+ }
+}
diff --git a/vendor/topthink/think-helper/src/Collection.php b/vendor/topthink/think-helper/src/Collection.php
new file mode 100644
index 0000000..d6074ba
--- /dev/null
+++ b/vendor/topthink/think-helper/src/Collection.php
@@ -0,0 +1,651 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use JsonSerializable;
+use think\contract\Arrayable;
+use think\contract\Jsonable;
+use think\helper\Arr;
+
+/**
+ * 数据集管理类
+ */
+class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Arrayable, Jsonable
+{
+ /**
+ * 数据集数据
+ * @var array
+ */
+ protected $items = [];
+
+ public function __construct($items = [])
+ {
+ $this->items = $this->convertToArray($items);
+ }
+
+ public static function make($items = [])
+ {
+ return new static($items);
+ }
+
+ /**
+ * 是否为空
+ * @access public
+ * @return bool
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->items);
+ }
+
+ public function toArray(): array
+ {
+ return array_map(function ($value) {
+ return $value instanceof Arrayable ? $value->toArray() : $value;
+ }, $this->items);
+ }
+
+ public function all(): array
+ {
+ return $this->items;
+ }
+
+ /**
+ * 合并数组
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @return static
+ */
+ public function merge($items)
+ {
+ return new static(array_merge($this->items, $this->convertToArray($items)));
+ }
+
+ /**
+ * 按指定键整理数据
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 键名
+ * @return array
+ */
+ public function dictionary($items = null, string &$indexKey = null)
+ {
+ if ($items instanceof self) {
+ $items = $items->all();
+ }
+
+ $items = is_null($items) ? $this->items : $items;
+
+ if ($items && empty($indexKey)) {
+ $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk();
+ }
+
+ if (isset($indexKey) && is_string($indexKey)) {
+ return array_column($items, null, $indexKey);
+ }
+
+ return $items;
+ }
+
+ /**
+ * 比较数组,返回差集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 指定比较的键名
+ * @return static
+ */
+ public function diff($items, string $indexKey = null)
+ {
+ if ($this->isEmpty() || is_scalar($this->items[0])) {
+ return new static(array_diff($this->items, $this->convertToArray($items)));
+ }
+
+ $diff = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (!isset($dictionary[$item[$indexKey]])) {
+ $diff[] = $item;
+ }
+ }
+ }
+
+ return new static($diff);
+ }
+
+ /**
+ * 比较数组,返回交集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 指定比较的键名
+ * @return static
+ */
+ public function intersect($items, string $indexKey = null)
+ {
+ if ($this->isEmpty() || is_scalar($this->items[0])) {
+ return new static(array_diff($this->items, $this->convertToArray($items)));
+ }
+
+ $intersect = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (isset($dictionary[$item[$indexKey]])) {
+ $intersect[] = $item;
+ }
+ }
+ }
+
+ return new static($intersect);
+ }
+
+ /**
+ * 交换数组中的键和值
+ *
+ * @access public
+ * @return static
+ */
+ public function flip()
+ {
+ return new static(array_flip($this->items));
+ }
+
+ /**
+ * 返回数组中所有的键名
+ *
+ * @access public
+ * @return static
+ */
+ public function keys()
+ {
+ return new static(array_keys($this->items));
+ }
+
+ /**
+ * 返回数组中所有的值组成的新 Collection 实例
+ * @access public
+ * @return static
+ */
+ public function values()
+ {
+ return new static(array_values($this->items));
+ }
+
+ /**
+ * 删除数组的最后一个元素(出栈)
+ *
+ * @access public
+ * @return mixed
+ */
+ public function pop()
+ {
+ return array_pop($this->items);
+ }
+
+ /**
+ * 通过使用用户自定义函数,以字符串返回数组
+ *
+ * @access public
+ * @param callable $callback 调用方法
+ * @param mixed $initial
+ * @return mixed
+ */
+ public function reduce(callable $callback, $initial = null)
+ {
+ return array_reduce($this->items, $callback, $initial);
+ }
+
+ /**
+ * 以相反的顺序返回数组。
+ *
+ * @access public
+ * @return static
+ */
+ public function reverse()
+ {
+ return new static(array_reverse($this->items));
+ }
+
+ /**
+ * 删除数组中首个元素,并返回被删除元素的值
+ *
+ * @access public
+ * @return mixed
+ */
+ public function shift()
+ {
+ return array_shift($this->items);
+ }
+
+ /**
+ * 在数组结尾插入一个元素
+ * @access public
+ * @param mixed $value 元素
+ * @param string $key KEY
+ * @return void
+ */
+ public function push($value, string $key = null): void
+ {
+ if (is_null($key)) {
+ $this->items[] = $value;
+ } else {
+ $this->items[$key] = $value;
+ }
+ }
+
+ /**
+ * 把一个数组分割为新的数组块.
+ *
+ * @access public
+ * @param int $size 块大小
+ * @param bool $preserveKeys
+ * @return static
+ */
+ public function chunk(int $size, bool $preserveKeys = false)
+ {
+ $chunks = [];
+
+ foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) {
+ $chunks[] = new static($chunk);
+ }
+
+ return new static($chunks);
+ }
+
+ /**
+ * 在数组开头插入一个元素
+ * @access public
+ * @param mixed $value 元素
+ * @param string $key KEY
+ * @return void
+ */
+ public function unshift($value, string $key = null): void
+ {
+ if (is_null($key)) {
+ array_unshift($this->items, $value);
+ } else {
+ $this->items = [$key => $value] + $this->items;
+ }
+ }
+
+ /**
+ * 给每个元素执行个回调
+ *
+ * @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;
+ }
+
+ /**
+ * 用回调函数处理数组中的元素
+ * @access public
+ * @param callable|null $callback 回调
+ * @return static
+ */
+ public function map(callable $callback)
+ {
+ return new static(array_map($callback, $this->items));
+ }
+
+ /**
+ * 用回调函数过滤数组中的元素
+ * @access public
+ * @param callable|null $callback 回调
+ * @return static
+ */
+ public function filter(callable $callback = null)
+ {
+ if ($callback) {
+ return new static(array_filter($this->items, $callback));
+ }
+
+ return new static(array_filter($this->items));
+ }
+
+ /**
+ * 根据字段条件过滤数组中的元素
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $operator 操作符
+ * @param mixed $value 数据
+ * @return static
+ */
+ public function where(string $field, $operator, $value = null)
+ {
+ if (is_null($value)) {
+ $value = $operator;
+ $operator = '=';
+ }
+
+ return $this->filter(function ($data) use ($field, $operator, $value) {
+ if (strpos($field, '.')) {
+ [$field, $relation] = explode('.', $field);
+
+ $result = $data[$field][$relation] ?? null;
+ } else {
+ $result = $data[$field] ?? null;
+ }
+
+ switch (strtolower($operator)) {
+ case '===':
+ return $result === $value;
+ case '!==':
+ return $result !== $value;
+ case '!=':
+ case '<>':
+ return $result != $value;
+ case '>':
+ return $result > $value;
+ case '>=':
+ return $result >= $value;
+ case '<':
+ return $result < $value;
+ case '<=':
+ return $result <= $value;
+ case 'like':
+ return is_string($result) && false !== strpos($result, $value);
+ case 'not like':
+ return is_string($result) && false === strpos($result, $value);
+ case 'in':
+ return is_scalar($result) && in_array($result, $value, true);
+ case 'not in':
+ return is_scalar($result) && !in_array($result, $value, true);
+ case 'between':
+ [$min, $max] = is_string($value) ? explode(',', $value) : $value;
+ return is_scalar($result) && $result >= $min && $result <= $max;
+ case 'not between':
+ [$min, $max] = is_string($value) ? explode(',', $value) : $value;
+ return is_scalar($result) && $result > $max || $result < $min;
+ case '==':
+ case '=':
+ default:
+ return $result == $value;
+ }
+ });
+ }
+
+ /**
+ * LIKE过滤
+ * @access public
+ * @param string $field 字段名
+ * @param string $value 数据
+ * @return static
+ */
+ public function whereLike(string $field, string $value)
+ {
+ return $this->where($field, 'like', $value);
+ }
+
+ /**
+ * NOT LIKE过滤
+ * @access public
+ * @param string $field 字段名
+ * @param string $value 数据
+ * @return static
+ */
+ public function whereNotLike(string $field, string $value)
+ {
+ return $this->where($field, 'not like', $value);
+ }
+
+ /**
+ * IN过滤
+ * @access public
+ * @param string $field 字段名
+ * @param array $value 数据
+ * @return static
+ */
+ public function whereIn(string $field, array $value)
+ {
+ return $this->where($field, 'in', $value);
+ }
+
+ /**
+ * NOT IN过滤
+ * @access public
+ * @param string $field 字段名
+ * @param array $value 数据
+ * @return static
+ */
+ public function whereNotIn(string $field, array $value)
+ {
+ return $this->where($field, 'not in', $value);
+ }
+
+ /**
+ * BETWEEN 过滤
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $value 数据
+ * @return static
+ */
+ public function whereBetween(string $field, $value)
+ {
+ return $this->where($field, 'between', $value);
+ }
+
+ /**
+ * NOT BETWEEN 过滤
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $value 数据
+ * @return static
+ */
+ public function whereNotBetween(string $field, $value)
+ {
+ return $this->where($field, 'not between', $value);
+ }
+
+ /**
+ * 返回数据中指定的一列
+ * @access public
+ * @param string|null $columnKey 键名
+ * @param string|null $indexKey 作为索引值的列
+ * @return array
+ */
+ public function column(?string $columnKey, string $indexKey = null)
+ {
+ return array_column($this->items, $columnKey, $indexKey);
+ }
+
+ /**
+ * 对数组排序
+ *
+ * @access public
+ * @param callable|null $callback 回调
+ * @return static
+ */
+ public function sort(callable $callback = null)
+ {
+ $items = $this->items;
+
+ $callback = $callback ?: function ($a, $b) {
+ return $a == $b ? 0 : (($a < $b) ? -1 : 1);
+ };
+
+ uasort($items, $callback);
+
+ return new static($items);
+ }
+
+ /**
+ * 指定字段排序
+ * @access public
+ * @param string $field 排序字段
+ * @param string $order 排序
+ * @return $this
+ */
+ public function order(string $field, string $order = 'asc')
+ {
+ return $this->sort(function ($a, $b) use ($field, $order) {
+ $fieldA = $a[$field] ?? null;
+ $fieldB = $b[$field] ?? null;
+
+ return 'desc' == strtolower($order) ? $fieldB > $fieldA : $fieldA > $fieldB;
+ });
+ }
+
+ /**
+ * 将数组打乱
+ *
+ * @access public
+ * @return static
+ */
+ public function shuffle()
+ {
+ $items = $this->items;
+
+ shuffle($items);
+
+ return new static($items);
+ }
+
+ /**
+ * 获取最第一个单元数据
+ *
+ * @access public
+ * @param callable|null $callback
+ * @param null $default
+ * @return mixed
+ */
+ public function first(callable $callback = null, $default = null)
+ {
+ return Arr::first($this->items, $callback, $default);
+ }
+
+ /**
+ * 获取最后一个单元数据
+ *
+ * @access public
+ * @param callable|null $callback
+ * @param null $default
+ * @return mixed
+ */
+ public function last(callable $callback = null, $default = null)
+ {
+ return Arr::last($this->items, $callback, $default);
+ }
+
+ /**
+ * 截取数组
+ *
+ * @access public
+ * @param int $offset 起始位置
+ * @param int $length 截取长度
+ * @param bool $preserveKeys preserveKeys
+ * @return static
+ */
+ public function slice(int $offset, int $length = null, bool $preserveKeys = false)
+ {
+ return new static(array_slice($this->items, $offset, $length, $preserveKeys));
+ }
+
+ // ArrayAccess
+ public function offsetExists($offset)
+ {
+ return array_key_exists($offset, $this->items);
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->items[$offset];
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ if (is_null($offset)) {
+ $this->items[] = $value;
+ } else {
+ $this->items[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->items[$offset]);
+ }
+
+ //Countable
+ public function count()
+ {
+ return count($this->items);
+ }
+
+ //IteratorAggregate
+ public function getIterator()
+ {
+ return new ArrayIterator($this->items);
+ }
+
+ //JsonSerializable
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * 转换当前数据集为JSON字符串
+ * @access public
+ * @param integer $options json参数
+ * @return string
+ */
+ public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
+ {
+ return json_encode($this->toArray(), $options);
+ }
+
+ public function __toString()
+ {
+ return $this->toJson();
+ }
+
+ /**
+ * 转换成数组
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @return array
+ */
+ protected function convertToArray($items): array
+ {
+ if ($items instanceof self) {
+ return $items->all();
+ }
+
+ return (array) $items;
+ }
+}
diff --git a/vendor/topthink/think-helper/src/contract/Arrayable.php b/vendor/topthink/think-helper/src/contract/Arrayable.php
new file mode 100644
index 0000000..373b6d6
--- /dev/null
+++ b/vendor/topthink/think-helper/src/contract/Arrayable.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-helper/src/helper/Arr.php b/vendor/topthink/think-helper/src/helper/Arr.php
new file mode 100644
index 0000000..a0929f6
--- /dev/null
+++ b/vendor/topthink/think-helper/src/helper/Arr.php
@@ -0,0 +1,634 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\helper;
+
+use ArrayAccess;
+use InvalidArgumentException;
+use think\Collection;
+
+class Arr
+{
+
+ /**
+ * Determine whether the given value is array accessible.
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ public static function accessible($value)
+ {
+ return is_array($value) || $value instanceof ArrayAccess;
+ }
+
+ /**
+ * Add an element to an array using "dot" notation if it doesn't exist.
+ *
+ * @param array $array
+ * @param string $key
+ * @param mixed $value
+ * @return array
+ */
+ public static function add($array, $key, $value)
+ {
+ if (is_null(static::get($array, $key))) {
+ static::set($array, $key, $value);
+ }
+
+ return $array;
+ }
+
+ /**
+ * Collapse an array of arrays into a single array.
+ *
+ * @param array $array
+ * @return array
+ */
+ public static function collapse($array)
+ {
+ $results = [];
+
+ foreach ($array as $values) {
+ if ($values instanceof Collection) {
+ $values = $values->all();
+ } elseif (!is_array($values)) {
+ continue;
+ }
+
+ $results = array_merge($results, $values);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Cross join the given arrays, returning all possible permutations.
+ *
+ * @param array ...$arrays
+ * @return array
+ */
+ public static function crossJoin(...$arrays)
+ {
+ $results = [[]];
+
+ foreach ($arrays as $index => $array) {
+ $append = [];
+
+ foreach ($results as $product) {
+ foreach ($array as $item) {
+ $product[$index] = $item;
+
+ $append[] = $product;
+ }
+ }
+
+ $results = $append;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Divide an array into two arrays. One with keys and the other with values.
+ *
+ * @param array $array
+ * @return array
+ */
+ public static function divide($array)
+ {
+ return [array_keys($array), array_values($array)];
+ }
+
+ /**
+ * Flatten a multi-dimensional associative array with dots.
+ *
+ * @param array $array
+ * @param string $prepend
+ * @return array
+ */
+ public static function dot($array, $prepend = '')
+ {
+ $results = [];
+
+ foreach ($array as $key => $value) {
+ if (is_array($value) && !empty($value)) {
+ $results = array_merge($results, static::dot($value, $prepend . $key . '.'));
+ } else {
+ $results[$prepend . $key] = $value;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get all of the given array except for a specified array of keys.
+ *
+ * @param array $array
+ * @param array|string $keys
+ * @return array
+ */
+ public static function except($array, $keys)
+ {
+ static::forget($array, $keys);
+
+ return $array;
+ }
+
+ /**
+ * Determine if the given key exists in the provided array.
+ *
+ * @param \ArrayAccess|array $array
+ * @param string|int $key
+ * @return bool
+ */
+ public static function exists($array, $key)
+ {
+ if ($array instanceof ArrayAccess) {
+ return $array->offsetExists($key);
+ }
+
+ return array_key_exists($key, $array);
+ }
+
+ /**
+ * Return the first element in an array passing a given truth test.
+ *
+ * @param array $array
+ * @param callable|null $callback
+ * @param mixed $default
+ * @return mixed
+ */
+ public static function first($array, callable $callback = null, $default = null)
+ {
+ if (is_null($callback)) {
+ if (empty($array)) {
+ return value($default);
+ }
+
+ foreach ($array as $item) {
+ return $item;
+ }
+ }
+
+ foreach ($array as $key => $value) {
+ if (call_user_func($callback, $value, $key)) {
+ return $value;
+ }
+ }
+
+ return value($default);
+ }
+
+ /**
+ * Return the last element in an array passing a given truth test.
+ *
+ * @param array $array
+ * @param callable|null $callback
+ * @param mixed $default
+ * @return mixed
+ */
+ public static function last($array, callable $callback = null, $default = null)
+ {
+ if (is_null($callback)) {
+ return empty($array) ? value($default) : end($array);
+ }
+
+ return static::first(array_reverse($array, true), $callback, $default);
+ }
+
+ /**
+ * Flatten a multi-dimensional array into a single level.
+ *
+ * @param array $array
+ * @param int $depth
+ * @return array
+ */
+ public static function flatten($array, $depth = INF)
+ {
+ $result = [];
+
+ foreach ($array as $item) {
+ $item = $item instanceof Collection ? $item->all() : $item;
+
+ if (!is_array($item)) {
+ $result[] = $item;
+ } elseif ($depth === 1) {
+ $result = array_merge($result, array_values($item));
+ } else {
+ $result = array_merge($result, static::flatten($item, $depth - 1));
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Remove one or many array items from a given array using "dot" notation.
+ *
+ * @param array $array
+ * @param array|string $keys
+ * @return void
+ */
+ public static function forget(&$array, $keys)
+ {
+ $original = &$array;
+
+ $keys = (array) $keys;
+
+ if (count($keys) === 0) {
+ return;
+ }
+
+ foreach ($keys as $key) {
+ // if the exact key exists in the top-level, remove it
+ if (static::exists($array, $key)) {
+ unset($array[$key]);
+
+ continue;
+ }
+
+ $parts = explode('.', $key);
+
+ // clean up before each pass
+ $array = &$original;
+
+ while (count($parts) > 1) {
+ $part = array_shift($parts);
+
+ if (isset($array[$part]) && is_array($array[$part])) {
+ $array = &$array[$part];
+ } else {
+ continue 2;
+ }
+ }
+
+ unset($array[array_shift($parts)]);
+ }
+ }
+
+ /**
+ * Get an item from an array using "dot" notation.
+ *
+ * @param \ArrayAccess|array $array
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ public static function get($array, $key, $default = null)
+ {
+ if (!static::accessible($array)) {
+ return value($default);
+ }
+
+ if (is_null($key)) {
+ return $array;
+ }
+
+ if (static::exists($array, $key)) {
+ return $array[$key];
+ }
+
+ if (strpos($key, '.') === false) {
+ return $array[$key] ?? value($default);
+ }
+
+ foreach (explode('.', $key) as $segment) {
+ if (static::accessible($array) && static::exists($array, $segment)) {
+ $array = $array[$segment];
+ } else {
+ return value($default);
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * Check if an item or items exist in an array using "dot" notation.
+ *
+ * @param \ArrayAccess|array $array
+ * @param string|array $keys
+ * @return bool
+ */
+ public static function has($array, $keys)
+ {
+ $keys = (array) $keys;
+
+ if (!$array || $keys === []) {
+ return false;
+ }
+
+ foreach ($keys as $key) {
+ $subKeyArray = $array;
+
+ if (static::exists($array, $key)) {
+ continue;
+ }
+
+ foreach (explode('.', $key) as $segment) {
+ if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) {
+ $subKeyArray = $subKeyArray[$segment];
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determines if an array is associative.
+ *
+ * An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
+ *
+ * @param array $array
+ * @return bool
+ */
+ public static function isAssoc(array $array)
+ {
+ $keys = array_keys($array);
+
+ return array_keys($keys) !== $keys;
+ }
+
+ /**
+ * Get a subset of the items from the given array.
+ *
+ * @param array $array
+ * @param array|string $keys
+ * @return array
+ */
+ public static function only($array, $keys)
+ {
+ return array_intersect_key($array, array_flip((array) $keys));
+ }
+
+ /**
+ * Pluck an array of values from an array.
+ *
+ * @param array $array
+ * @param string|array $value
+ * @param string|array|null $key
+ * @return array
+ */
+ public static function pluck($array, $value, $key = null)
+ {
+ $results = [];
+
+ [$value, $key] = static::explodePluckParameters($value, $key);
+
+ foreach ($array as $item) {
+ $itemValue = data_get($item, $value);
+
+ // If the key is "null", we will just append the value to the array and keep
+ // looping. Otherwise we will key the array using the value of the key we
+ // received from the developer. Then we'll return the final array form.
+ if (is_null($key)) {
+ $results[] = $itemValue;
+ } else {
+ $itemKey = data_get($item, $key);
+
+ if (is_object($itemKey) && method_exists($itemKey, '__toString')) {
+ $itemKey = (string) $itemKey;
+ }
+
+ $results[$itemKey] = $itemValue;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Explode the "value" and "key" arguments passed to "pluck".
+ *
+ * @param string|array $value
+ * @param string|array|null $key
+ * @return array
+ */
+ protected static function explodePluckParameters($value, $key)
+ {
+ $value = is_string($value) ? explode('.', $value) : $value;
+
+ $key = is_null($key) || is_array($key) ? $key : explode('.', $key);
+
+ return [$value, $key];
+ }
+
+ /**
+ * Push an item onto the beginning of an array.
+ *
+ * @param array $array
+ * @param mixed $value
+ * @param mixed $key
+ * @return array
+ */
+ public static function prepend($array, $value, $key = null)
+ {
+ if (is_null($key)) {
+ array_unshift($array, $value);
+ } else {
+ $array = [$key => $value] + $array;
+ }
+
+ return $array;
+ }
+
+ /**
+ * Get a value from the array, and remove it.
+ *
+ * @param array $array
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ public static function pull(&$array, $key, $default = null)
+ {
+ $value = static::get($array, $key, $default);
+
+ static::forget($array, $key);
+
+ return $value;
+ }
+
+ /**
+ * Get one or a specified number of random values from an array.
+ *
+ * @param array $array
+ * @param int|null $number
+ * @return mixed
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function random($array, $number = null)
+ {
+ $requested = is_null($number) ? 1 : $number;
+
+ $count = count($array);
+
+ if ($requested > $count) {
+ throw new InvalidArgumentException(
+ "You requested {$requested} items, but there are only {$count} items available."
+ );
+ }
+
+ if (is_null($number)) {
+ return $array[array_rand($array)];
+ }
+
+ if ((int) $number === 0) {
+ return [];
+ }
+
+ $keys = array_rand($array, $number);
+
+ $results = [];
+
+ foreach ((array) $keys as $key) {
+ $results[] = $array[$key];
+ }
+
+ return $results;
+ }
+
+ /**
+ * Set an array item to a given value using "dot" notation.
+ *
+ * If no key is given to the method, the entire array will be replaced.
+ *
+ * @param array $array
+ * @param string $key
+ * @param mixed $value
+ * @return array
+ */
+ public static function set(&$array, $key, $value)
+ {
+ if (is_null($key)) {
+ return $array = $value;
+ }
+
+ $keys = explode('.', $key);
+
+ while (count($keys) > 1) {
+ $key = array_shift($keys);
+
+ // If the key doesn't exist at this depth, we will just create an empty array
+ // to hold the next value, allowing us to create the arrays to hold final
+ // values at the correct depth. Then we'll keep digging into the array.
+ if (!isset($array[$key]) || !is_array($array[$key])) {
+ $array[$key] = [];
+ }
+
+ $array = &$array[$key];
+ }
+
+ $array[array_shift($keys)] = $value;
+
+ return $array;
+ }
+
+ /**
+ * Shuffle the given array and return the result.
+ *
+ * @param array $array
+ * @param int|null $seed
+ * @return array
+ */
+ public static function shuffle($array, $seed = null)
+ {
+ if (is_null($seed)) {
+ shuffle($array);
+ } else {
+ srand($seed);
+
+ usort($array, function () {
+ return rand(-1, 1);
+ });
+ }
+
+ return $array;
+ }
+
+ /**
+ * Sort the array using the given callback or "dot" notation.
+ *
+ * @param array $array
+ * @param callable|string|null $callback
+ * @return array
+ */
+ public static function sort($array, $callback = null)
+ {
+ return Collection::make($array)->sort($callback)->all();
+ }
+
+ /**
+ * Recursively sort an array by keys and values.
+ *
+ * @param array $array
+ * @return array
+ */
+ public static function sortRecursive($array)
+ {
+ foreach ($array as &$value) {
+ if (is_array($value)) {
+ $value = static::sortRecursive($value);
+ }
+ }
+
+ if (static::isAssoc($array)) {
+ ksort($array);
+ } else {
+ sort($array);
+ }
+
+ return $array;
+ }
+
+ /**
+ * Convert the array into a query string.
+ *
+ * @param array $array
+ * @return string
+ */
+ public static function query($array)
+ {
+ return http_build_query($array, null, '&', PHP_QUERY_RFC3986);
+ }
+
+ /**
+ * Filter the array using the given callback.
+ *
+ * @param array $array
+ * @param callable $callback
+ * @return array
+ */
+ public static function where($array, callable $callback)
+ {
+ return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
+ }
+
+ /**
+ * If the given value is not an array and not null, wrap it in one.
+ *
+ * @param mixed $value
+ * @return array
+ */
+ public static function wrap($value)
+ {
+ if (is_null($value)) {
+ return [];
+ }
+
+ return is_array($value) ? $value : [$value];
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-helper/src/helper/Str.php b/vendor/topthink/think-helper/src/helper/Str.php
new file mode 100644
index 0000000..7745007
--- /dev/null
+++ b/vendor/topthink/think-helper/src/helper/Str.php
@@ -0,0 +1,234 @@
+
+// +----------------------------------------------------------------------
+namespace think\helper;
+
+class Str
+{
+
+ protected static $snakeCache = [];
+
+ protected static $camelCache = [];
+
+ protected static $studlyCache = [];
+
+ /**
+ * 检查字符串中是否包含某些字符串
+ * @param string $haystack
+ * @param string|array $needles
+ * @return bool
+ */
+ public static function contains(string $haystack, $needles): bool
+ {
+ foreach ((array) $needles as $needle) {
+ if ('' != $needle && mb_strpos($haystack, $needle) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 检查字符串是否以某些字符串结尾
+ *
+ * @param string $haystack
+ * @param string|array $needles
+ * @return bool
+ */
+ public static function endsWith(string $haystack, $needles): bool
+ {
+ foreach ((array) $needles as $needle) {
+ if ((string) $needle === static::substr($haystack, -static::length($needle))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 检查字符串是否以某些字符串开头
+ *
+ * @param string $haystack
+ * @param string|array $needles
+ * @return bool
+ */
+ public static function startsWith(string $haystack, $needles): bool
+ {
+ foreach ((array) $needles as $needle) {
+ if ('' != $needle && mb_strpos($haystack, $needle) === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取指定长度的随机字母数字组合的字符串
+ *
+ * @param int $length
+ * @param int $type
+ * @param string $addChars
+ * @return string
+ */
+ public static function random(int $length = 6, int $type = null, string $addChars = ''): string
+ {
+ $str = '';
+ switch ($type) {
+ case 0:
+ $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars;
+ break;
+ case 1:
+ $chars = str_repeat('0123456789', 3);
+ break;
+ case 2:
+ $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars;
+ break;
+ case 3:
+ $chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars;
+ break;
+ case 4:
+ $chars = "们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书" . $addChars;
+ break;
+ default:
+ $chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars;
+ break;
+ }
+ if ($length > 10) {
+ $chars = $type == 1 ? str_repeat($chars, $length) : str_repeat($chars, 5);
+ }
+ if ($type != 4) {
+ $chars = str_shuffle($chars);
+ $str = substr($chars, 0, $length);
+ } else {
+ for ($i = 0; $i < $length; $i++) {
+ $str .= mb_substr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1);
+ }
+ }
+ return $str;
+ }
+
+ /**
+ * 字符串转小写
+ *
+ * @param string $value
+ * @return string
+ */
+ public static function lower(string $value): string
+ {
+ return mb_strtolower($value, 'UTF-8');
+ }
+
+ /**
+ * 字符串转大写
+ *
+ * @param string $value
+ * @return string
+ */
+ public static function upper(string $value): string
+ {
+ return mb_strtoupper($value, 'UTF-8');
+ }
+
+ /**
+ * 获取字符串的长度
+ *
+ * @param string $value
+ * @return int
+ */
+ public static function length(string $value): int
+ {
+ return mb_strlen($value);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param string $string
+ * @param int $start
+ * @param int|null $length
+ * @return string
+ */
+ public static function substr(string $string, int $start, int $length = null): string
+ {
+ return mb_substr($string, $start, $length, 'UTF-8');
+ }
+
+ /**
+ * 驼峰转下划线
+ *
+ * @param string $value
+ * @param string $delimiter
+ * @return string
+ */
+ public static function snake(string $value, string $delimiter = '_'): string
+ {
+ $key = $value;
+
+ if (isset(static::$snakeCache[$key][$delimiter])) {
+ return static::$snakeCache[$key][$delimiter];
+ }
+
+ if (!ctype_lower($value)) {
+ $value = preg_replace('/\s+/u', '', $value);
+
+ $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
+ }
+
+ return static::$snakeCache[$key][$delimiter] = $value;
+ }
+
+ /**
+ * 下划线转驼峰(首字母小写)
+ *
+ * @param string $value
+ * @return string
+ */
+ public static function camel(string $value): string
+ {
+ if (isset(static::$camelCache[$value])) {
+ return static::$camelCache[$value];
+ }
+
+ return static::$camelCache[$value] = lcfirst(static::studly($value));
+ }
+
+ /**
+ * 下划线转驼峰(首字母大写)
+ *
+ * @param string $value
+ * @return string
+ */
+ public static function studly(string $value): string
+ {
+ $key = $value;
+
+ if (isset(static::$studlyCache[$key])) {
+ return static::$studlyCache[$key];
+ }
+
+ $value = ucwords(str_replace(['-', '_'], ' ', $value));
+
+ return static::$studlyCache[$key] = str_replace(' ', '', $value);
+ }
+
+ /**
+ * 转为首字母大写的标题格式
+ *
+ * @param string $value
+ * @return string
+ */
+ public static function title(string $value): string
+ {
+ return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
+ }
+}
diff --git a/vendor/topthink/think-image/.gitignore b/vendor/topthink/think-image/.gitignore
new file mode 100644
index 0000000..e2974aa
--- /dev/null
+++ b/vendor/topthink/think-image/.gitignore
@@ -0,0 +1,4 @@
+/vendor/
+/thinkphp/
+/composer.lock
+/.idea/
\ No newline at end of file
diff --git a/vendor/topthink/think-image/.travis.yml b/vendor/topthink/think-image/.travis.yml
new file mode 100644
index 0000000..75b4c97
--- /dev/null
+++ b/vendor/topthink/think-image/.travis.yml
@@ -0,0 +1,22 @@
+language: php
+
+php:
+ - 5.4
+ - 5.5
+ - 5.6
+ - 7.0
+ - hhvm
+
+matrix:
+ allow_failures:
+ - php: 7.0
+ - php: hhvm
+
+before_script:
+ - composer self-update
+ - composer install --prefer-source --no-interaction --dev
+
+script: phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/vendor/topthink/think-image/LICENSE b/vendor/topthink/think-image/LICENSE
new file mode 100644
index 0000000..c0ee812
--- /dev/null
+++ b/vendor/topthink/think-image/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-image/README.md b/vendor/topthink/think-image/README.md
new file mode 100644
index 0000000..68eb804
--- /dev/null
+++ b/vendor/topthink/think-image/README.md
@@ -0,0 +1,29 @@
+# The ThinkPHP5 Image Package
+
+[![Build Status](https://img.shields.io/travis/top-think/think-image.svg)](https://travis-ci.org/top-think/think-image)
+[![Coverage Status](https://img.shields.io/codecov/c/github/top-think/think-image.svg)](https://codecov.io/github/top-think/think-image)
+[![Downloads](https://img.shields.io/github/downloads/top-think/think-image/total.svg)](https://github.com/top-think/think-image/releases)
+[![Releases](https://img.shields.io/github/release/top-think/think-image.svg)](https://github.com/top-think/think-image/releases/latest)
+[![Releases Downloads](https://img.shields.io/github/downloads/top-think/think-image/latest/total.svg)](https://github.com/top-think/think-image/releases/latest)
+[![Packagist Status](https://img.shields.io/packagist/v/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image)
+[![Packagist Downloads](https://img.shields.io/packagist/dt/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image)
+
+## 安装
+
+> composer require topthink/think-image
+
+## 使用
+
+~~~
+$image = \think\Image::open('./image.jpg');
+或者
+$image = \think\Image::open(request()->file('image'));
+
+
+$image->crop(...)
+ ->thumb(...)
+ ->water(...)
+ ->text(....)
+ ->save(..);
+
+~~~
\ No newline at end of file
diff --git a/vendor/topthink/think-image/composer.json b/vendor/topthink/think-image/composer.json
new file mode 100644
index 0000000..b06cdd5
--- /dev/null
+++ b/vendor/topthink/think-image/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "topthink/think-image",
+ "description": "The ThinkPHP5 Image Package",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "ext-gd": "*"
+ },
+ "require-dev": {
+ "topthink/framework": "^5.0",
+ "phpunit/phpunit": "4.8.*"
+ },
+ "config": {
+ "preferred-install": "dist"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ }
+ }
+}
diff --git a/vendor/topthink/think-image/phpunit.xml b/vendor/topthink/think-image/phpunit.xml
new file mode 100644
index 0000000..ecbff6c
--- /dev/null
+++ b/vendor/topthink/think-image/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ ./tests/
+
+
+
+
+
+
diff --git a/vendor/topthink/think-image/src/Image.php b/vendor/topthink/think-image/src/Image.php
new file mode 100644
index 0000000..6f5814d
--- /dev/null
+++ b/vendor/topthink/think-image/src/Image.php
@@ -0,0 +1,610 @@
+
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\image\Exception as ImageException;
+use think\image\gif\Gif;
+
+class Image
+{
+
+ /* 缩略图相关常量定义 */
+ const THUMB_SCALING = 1; //常量,标识缩略图等比例缩放类型
+ const THUMB_FILLED = 2; //常量,标识缩略图缩放后填充类型
+ const THUMB_CENTER = 3; //常量,标识缩略图居中裁剪类型
+ const THUMB_NORTHWEST = 4; //常量,标识缩略图左上角裁剪类型
+ const THUMB_SOUTHEAST = 5; //常量,标识缩略图右下角裁剪类型
+ const THUMB_FIXED = 6; //常量,标识缩略图固定尺寸缩放类型
+ /* 水印相关常量定义 */
+ const WATER_NORTHWEST = 1; //常量,标识左上角水印
+ const WATER_NORTH = 2; //常量,标识上居中水印
+ const WATER_NORTHEAST = 3; //常量,标识右上角水印
+ const WATER_WEST = 4; //常量,标识左居中水印
+ const WATER_CENTER = 5; //常量,标识居中水印
+ const WATER_EAST = 6; //常量,标识右居中水印
+ const WATER_SOUTHWEST = 7; //常量,标识左下角水印
+ const WATER_SOUTH = 8; //常量,标识下居中水印
+ const WATER_SOUTHEAST = 9; //常量,标识右下角水印
+ /* 翻转相关常量定义 */
+ const FLIP_X = 1; //X轴翻转
+ const FLIP_Y = 2; //Y轴翻转
+
+ /**
+ * 图像资源对象
+ *
+ * @var resource
+ */
+ protected $im;
+
+ /** @var Gif */
+ protected $gif;
+
+ /**
+ * 图像信息,包括 width, height, type, mime, size
+ *
+ * @var array
+ */
+ protected $info;
+
+ protected function __construct(\SplFileInfo $file)
+ {
+ //获取图像信息
+ $info = @getimagesize($file->getPathname());
+
+ //检测图像合法性
+ if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
+ throw new ImageException('Illegal image file');
+ }
+
+ //设置图像信息
+ $this->info = [
+ 'width' => $info[0],
+ 'height' => $info[1],
+ 'type' => image_type_to_extension($info[2], false),
+ 'mime' => $info['mime'],
+ ];
+
+ //打开图像
+ if ('gif' == $this->info['type']) {
+ $this->gif = new Gif($file->getPathname());
+ $this->im = @imagecreatefromstring($this->gif->image());
+ } else {
+ $fun = "imagecreatefrom{$this->info['type']}";
+ $this->im = @$fun($file->getPathname());
+ }
+
+ if (empty($this->im)) {
+ throw new ImageException('Failed to create image resources!');
+ }
+
+ }
+
+ /**
+ * 打开一个图片文件
+ * @param \SplFileInfo|string $file
+ * @return Image
+ */
+ public static function open($file)
+ {
+ if (is_string($file)) {
+ $file = new \SplFileInfo($file);
+ }
+ if (!$file->isFile()) {
+ throw new ImageException('image file not exist');
+ }
+ return new self($file);
+ }
+
+ /**
+ * 保存图像
+ * @param string $pathname 图像保存路径名称
+ * @param null|string $type 图像类型
+ * @param int $quality 图像质量
+ * @param bool $interlace 是否对JPEG类型图像设置隔行扫描
+ * @return $this
+ */
+ public function save($pathname, $type = null, $quality = 80, $interlace = true)
+ {
+ //自动获取图像类型
+ if (is_null($type)) {
+ $type = $this->info['type'];
+ } else {
+ $type = strtolower($type);
+ }
+ //保存图像
+ if ('jpeg' == $type || 'jpg' == $type) {
+ //JPEG图像设置隔行扫描
+ imageinterlace($this->im, $interlace);
+ imagejpeg($this->im, $pathname, $quality);
+ } elseif ('gif' == $type && !empty($this->gif)) {
+ $this->gif->save($pathname);
+ } elseif ('png' == $type) {
+ //设定保存完整的 alpha 通道信息
+ imagesavealpha($this->im, true);
+ //ImagePNG生成图像的质量范围从0到9的
+ imagepng($this->im, $pathname, min((int) ($quality / 10), 9));
+ } else {
+ $fun = 'image' . $type;
+ $fun($this->im, $pathname);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 返回图像宽度
+ * @return int 图像宽度
+ */
+ public function width()
+ {
+ return $this->info['width'];
+ }
+
+ /**
+ * 返回图像高度
+ * @return int 图像高度
+ */
+ public function height()
+ {
+ return $this->info['height'];
+ }
+
+ /**
+ * 返回图像类型
+ * @return string 图像类型
+ */
+ public function type()
+ {
+ return $this->info['type'];
+ }
+
+ /**
+ * 返回图像MIME类型
+ * @return string 图像MIME类型
+ */
+ public function mime()
+ {
+ return $this->info['mime'];
+ }
+
+ /**
+ * 返回图像尺寸数组 0 - 图像宽度,1 - 图像高度
+ * @return array 图像尺寸
+ */
+ public function size()
+ {
+ return [$this->info['width'], $this->info['height']];
+ }
+
+ /**
+ * 旋转图像
+ * @param int $degrees 顺时针旋转的度数
+ * @return $this
+ */
+ public function rotate($degrees = 90)
+ {
+ do {
+ $img = imagerotate($this->im, -$degrees, imagecolorallocatealpha($this->im, 0, 0, 0, 127));
+ imagedestroy($this->im);
+ $this->im = $img;
+ } while (!empty($this->gif) && $this->gifNext());
+
+ $this->info['width'] = imagesx($this->im);
+ $this->info['height'] = imagesy($this->im);
+
+ return $this;
+ }
+
+ /**
+ * 翻转图像
+ * @param integer $direction 翻转轴,X或者Y
+ * @return $this
+ */
+ public function flip($direction = self::FLIP_X)
+ {
+ //原图宽度和高度
+ $w = $this->info['width'];
+ $h = $this->info['height'];
+
+ do {
+
+ $img = imagecreatetruecolor($w, $h);
+
+ switch ($direction) {
+ case self::FLIP_X:
+ for ($y = 0; $y < $h; $y++) {
+ imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1);
+ }
+ break;
+ case self::FLIP_Y:
+ for ($x = 0; $x < $w; $x++) {
+ imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h);
+ }
+ break;
+ default:
+ throw new ImageException('不支持的翻转类型');
+ }
+
+ imagedestroy($this->im);
+ $this->im = $img;
+
+ } while (!empty($this->gif) && $this->gifNext());
+
+ return $this;
+ }
+
+ /**
+ * 裁剪图像
+ *
+ * @param integer $w 裁剪区域宽度
+ * @param integer $h 裁剪区域高度
+ * @param integer $x 裁剪区域x坐标
+ * @param integer $y 裁剪区域y坐标
+ * @param integer $width 图像保存宽度
+ * @param integer $height 图像保存高度
+ *
+ * @return $this
+ */
+ public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null)
+ {
+ //设置保存尺寸
+ empty($width) && $width = $w;
+ empty($height) && $height = $h;
+ do {
+ //创建新图像
+ $img = imagecreatetruecolor($width, $height);
+ // 调整默认颜色
+ $color = imagecolorallocate($img, 255, 255, 255);
+ imagefill($img, 0, 0, $color);
+ //裁剪
+ imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h);
+ imagedestroy($this->im); //销毁原图
+ //设置新图像
+ $this->im = $img;
+ } while (!empty($this->gif) && $this->gifNext());
+ $this->info['width'] = (int) $width;
+ $this->info['height'] = (int) $height;
+ return $this;
+ }
+
+ /**
+ * 生成缩略图
+ *
+ * @param integer $width 缩略图最大宽度
+ * @param integer $height 缩略图最大高度
+ * @param int $type 缩略图裁剪类型
+ *
+ * @return $this
+ */
+ public function thumb($width, $height, $type = self::THUMB_SCALING)
+ {
+ //原图宽度和高度
+ $w = $this->info['width'];
+ $h = $this->info['height'];
+ /* 计算缩略图生成的必要参数 */
+ switch ($type) {
+ /* 等比例缩放 */
+ case self::THUMB_SCALING:
+ //原图尺寸小于缩略图尺寸则不进行缩略
+ if ($w < $width && $h < $height) {
+ return $this;
+ }
+ //计算缩放比例
+ $scale = min($width / $w, $height / $h);
+ //设置缩略图的坐标及宽度和高度
+ $x = $y = 0;
+ $width = $w * $scale;
+ $height = $h * $scale;
+ break;
+ /* 居中裁剪 */
+ case self::THUMB_CENTER:
+ //计算缩放比例
+ $scale = max($width / $w, $height / $h);
+ //设置缩略图的坐标及宽度和高度
+ $w = $width / $scale;
+ $h = $height / $scale;
+ $x = ($this->info['width'] - $w) / 2;
+ $y = ($this->info['height'] - $h) / 2;
+ break;
+ /* 左上角裁剪 */
+ case self::THUMB_NORTHWEST:
+ //计算缩放比例
+ $scale = max($width / $w, $height / $h);
+ //设置缩略图的坐标及宽度和高度
+ $x = $y = 0;
+ $w = $width / $scale;
+ $h = $height / $scale;
+ break;
+ /* 右下角裁剪 */
+ case self::THUMB_SOUTHEAST:
+ //计算缩放比例
+ $scale = max($width / $w, $height / $h);
+ //设置缩略图的坐标及宽度和高度
+ $w = $width / $scale;
+ $h = $height / $scale;
+ $x = $this->info['width'] - $w;
+ $y = $this->info['height'] - $h;
+ break;
+ /* 填充 */
+ case self::THUMB_FILLED:
+ //计算缩放比例
+ if ($w < $width && $h < $height) {
+ $scale = 1;
+ } else {
+ $scale = min($width / $w, $height / $h);
+ }
+ //设置缩略图的坐标及宽度和高度
+ $neww = $w * $scale;
+ $newh = $h * $scale;
+ $x = $this->info['width'] - $w;
+ $y = $this->info['height'] - $h;
+ $posx = ($width - $w * $scale) / 2;
+ $posy = ($height - $h * $scale) / 2;
+ do {
+ //创建新图像
+ $img = imagecreatetruecolor($width, $height);
+ // 调整默认颜色
+ $color = imagecolorallocate($img, 255, 255, 255);
+ imagefill($img, 0, 0, $color);
+ //裁剪
+ imagecopyresampled($img, $this->im, $posx, $posy, $x, $y, $neww, $newh, $w, $h);
+ imagedestroy($this->im); //销毁原图
+ $this->im = $img;
+ } while (!empty($this->gif) && $this->gifNext());
+ $this->info['width'] = (int) $width;
+ $this->info['height'] = (int) $height;
+ return $this;
+ /* 固定 */
+ case self::THUMB_FIXED:
+ $x = $y = 0;
+ break;
+ default:
+ throw new ImageException('不支持的缩略图裁剪类型');
+ }
+ /* 裁剪图像 */
+ return $this->crop($w, $h, $x, $y, $width, $height);
+ }
+
+ /**
+ * 添加水印
+ *
+ * @param string $source 水印图片路径
+ * @param int $locate 水印位置
+ * @param int $alpha 透明度
+ * @return $this
+ */
+ public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100)
+ {
+ if (!is_file($source)) {
+ throw new ImageException('水印图像不存在');
+ }
+ //获取水印图像信息
+ $info = getimagesize($source);
+ if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
+ throw new ImageException('非法水印文件');
+ }
+ //创建水印图像资源
+ $fun = 'imagecreatefrom' . image_type_to_extension($info[2], false);
+ $water = $fun($source);
+ //设定水印图像的混色模式
+ imagealphablending($water, true);
+ /* 设定水印位置 */
+ switch ($locate) {
+ /* 右下角水印 */
+ case self::WATER_SOUTHEAST:
+ $x = $this->info['width'] - $info[0];
+ $y = $this->info['height'] - $info[1];
+ break;
+ /* 左下角水印 */
+ case self::WATER_SOUTHWEST:
+ $x = 0;
+ $y = $this->info['height'] - $info[1];
+ break;
+ /* 左上角水印 */
+ case self::WATER_NORTHWEST:
+ $x = $y = 0;
+ break;
+ /* 右上角水印 */
+ case self::WATER_NORTHEAST:
+ $x = $this->info['width'] - $info[0];
+ $y = 0;
+ break;
+ /* 居中水印 */
+ case self::WATER_CENTER:
+ $x = ($this->info['width'] - $info[0]) / 2;
+ $y = ($this->info['height'] - $info[1]) / 2;
+ break;
+ /* 下居中水印 */
+ case self::WATER_SOUTH:
+ $x = ($this->info['width'] - $info[0]) / 2;
+ $y = $this->info['height'] - $info[1];
+ break;
+ /* 右居中水印 */
+ case self::WATER_EAST:
+ $x = $this->info['width'] - $info[0];
+ $y = ($this->info['height'] - $info[1]) / 2;
+ break;
+ /* 上居中水印 */
+ case self::WATER_NORTH:
+ $x = ($this->info['width'] - $info[0]) / 2;
+ $y = 0;
+ break;
+ /* 左居中水印 */
+ case self::WATER_WEST:
+ $x = 0;
+ $y = ($this->info['height'] - $info[1]) / 2;
+ break;
+ default:
+ /* 自定义水印坐标 */
+ if (is_array($locate)) {
+ list($x, $y) = $locate;
+ } else {
+ throw new ImageException('不支持的水印位置类型');
+ }
+ }
+ do {
+ //添加水印
+ $src = imagecreatetruecolor($info[0], $info[1]);
+ // 调整默认颜色
+ $color = imagecolorallocate($src, 255, 255, 255);
+ imagefill($src, 0, 0, $color);
+ imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]);
+ imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]);
+ imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha);
+ //销毁零时图片资源
+ imagedestroy($src);
+ } while (!empty($this->gif) && $this->gifNext());
+ //销毁水印资源
+ imagedestroy($water);
+ return $this;
+ }
+
+ /**
+ * 图像添加文字
+ *
+ * @param string $text 添加的文字
+ * @param string $font 字体路径
+ * @param integer $size 字号
+ * @param string $color 文字颜色
+ * @param int $locate 文字写入位置
+ * @param integer $offset 文字相对当前位置的偏移量
+ * @param integer $angle 文字倾斜角度
+ *
+ * @return $this
+ * @throws ImageException
+ */
+ public function text($text, $font, $size, $color = '#00000000',
+ $locate = self::WATER_SOUTHEAST, $offset = 0, $angle = 0) {
+
+ if (!is_file($font)) {
+ throw new ImageException("不存在的字体文件:{$font}");
+ }
+ //获取文字信息
+ $info = imagettfbbox($size, $angle, $font, $text);
+ $minx = min($info[0], $info[2], $info[4], $info[6]);
+ $maxx = max($info[0], $info[2], $info[4], $info[6]);
+ $miny = min($info[1], $info[3], $info[5], $info[7]);
+ $maxy = max($info[1], $info[3], $info[5], $info[7]);
+ /* 计算文字初始坐标和尺寸 */
+ $x = $minx;
+ $y = abs($miny);
+ $w = $maxx - $minx;
+ $h = $maxy - $miny;
+ /* 设定文字位置 */
+ switch ($locate) {
+ /* 右下角文字 */
+ case self::WATER_SOUTHEAST:
+ $x += $this->info['width'] - $w;
+ $y += $this->info['height'] - $h;
+ break;
+ /* 左下角文字 */
+ case self::WATER_SOUTHWEST:
+ $y += $this->info['height'] - $h;
+ break;
+ /* 左上角文字 */
+ case self::WATER_NORTHWEST:
+ // 起始坐标即为左上角坐标,无需调整
+ break;
+ /* 右上角文字 */
+ case self::WATER_NORTHEAST:
+ $x += $this->info['width'] - $w;
+ break;
+ /* 居中文字 */
+ case self::WATER_CENTER:
+ $x += ($this->info['width'] - $w) / 2;
+ $y += ($this->info['height'] - $h) / 2;
+ break;
+ /* 下居中文字 */
+ case self::WATER_SOUTH:
+ $x += ($this->info['width'] - $w) / 2;
+ $y += $this->info['height'] - $h;
+ break;
+ /* 右居中文字 */
+ case self::WATER_EAST:
+ $x += $this->info['width'] - $w;
+ $y += ($this->info['height'] - $h) / 2;
+ break;
+ /* 上居中文字 */
+ case self::WATER_NORTH:
+ $x += ($this->info['width'] - $w) / 2;
+ break;
+ /* 左居中文字 */
+ case self::WATER_WEST:
+ $y += ($this->info['height'] - $h) / 2;
+ break;
+ default:
+ /* 自定义文字坐标 */
+ if (is_array($locate)) {
+ list($posx, $posy) = $locate;
+ $x += $posx;
+ $y += $posy;
+ } else {
+ throw new ImageException('不支持的文字位置类型');
+ }
+ }
+ /* 设置偏移量 */
+ if (is_array($offset)) {
+ $offset = array_map('intval', $offset);
+ list($ox, $oy) = $offset;
+ } else {
+ $offset = intval($offset);
+ $ox = $oy = $offset;
+ }
+ /* 设置颜色 */
+ if (is_string($color) && 0 === strpos($color, '#')) {
+ $color = str_split(substr($color, 1), 2);
+ $color = array_map('hexdec', $color);
+ if (empty($color[3]) || $color[3] > 127) {
+ $color[3] = 0;
+ }
+ } elseif (!is_array($color)) {
+ throw new ImageException('错误的颜色值');
+ }
+ do {
+ /* 写入文字 */
+ $col = imagecolorallocatealpha($this->im, $color[0], $color[1], $color[2], $color[3]);
+ imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text);
+ } while (!empty($this->gif) && $this->gifNext());
+ return $this;
+ }
+
+ /**
+ * 切换到GIF的下一帧并保存当前帧
+ */
+ protected function gifNext()
+ {
+ ob_start();
+ ob_implicit_flush(0);
+ imagegif($this->im);
+ $img = ob_get_clean();
+ $this->gif->image($img);
+ $next = $this->gif->nextImage();
+ if ($next) {
+ imagedestroy($this->im);
+ $this->im = imagecreatefromstring($next);
+ return $next;
+ } else {
+ imagedestroy($this->im);
+ $this->im = imagecreatefromstring($this->gif->image());
+ return false;
+ }
+ }
+
+ /**
+ * 析构方法,用于销毁图像资源
+ */
+ public function __destruct()
+ {
+ empty($this->im) || imagedestroy($this->im);
+ }
+
+}
diff --git a/vendor/topthink/think-image/src/image/Exception.php b/vendor/topthink/think-image/src/image/Exception.php
new file mode 100644
index 0000000..58ca2ee
--- /dev/null
+++ b/vendor/topthink/think-image/src/image/Exception.php
@@ -0,0 +1,18 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\image;
+
+
+class Exception extends \RuntimeException
+{
+
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/src/image/gif/Decoder.php b/vendor/topthink/think-image/src/image/gif/Decoder.php
new file mode 100644
index 0000000..2c8c233
--- /dev/null
+++ b/vendor/topthink/think-image/src/image/gif/Decoder.php
@@ -0,0 +1,207 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\image\gif;
+
+
+class Decoder
+{
+ public $GIF_buffer = [];
+ public $GIF_arrays = [];
+ public $GIF_delays = [];
+ public $GIF_stream = "";
+ public $GIF_string = "";
+ public $GIF_bfseek = 0;
+ public $GIF_screen = [];
+ public $GIF_global = [];
+ public $GIF_sorted;
+ public $GIF_colorS;
+ public $GIF_colorC;
+ public $GIF_colorF;
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFDecoder ( $GIF_pointer )
+ ::
+ */
+ public function __construct($GIF_pointer)
+ {
+ $this->GIF_stream = $GIF_pointer;
+ $this->getByte(6); // GIF89a
+ $this->getByte(7); // Logical Screen Descriptor
+ $this->GIF_screen = $this->GIF_buffer;
+ $this->GIF_colorF = $this->GIF_buffer[4] & 0x80 ? 1 : 0;
+ $this->GIF_sorted = $this->GIF_buffer[4] & 0x08 ? 1 : 0;
+ $this->GIF_colorC = $this->GIF_buffer[4] & 0x07;
+ $this->GIF_colorS = 2 << $this->GIF_colorC;
+ if (1 == $this->GIF_colorF) {
+ $this->getByte(3 * $this->GIF_colorS);
+ $this->GIF_global = $this->GIF_buffer;
+ }
+
+ for ($cycle = 1; $cycle;) {
+ if ($this->getByte(1)) {
+ switch ($this->GIF_buffer[0]) {
+ case 0x21:
+ $this->readExtensions();
+ break;
+ case 0x2C:
+ $this->readDescriptor();
+ break;
+ case 0x3B:
+ $cycle = 0;
+ break;
+ }
+ } else {
+ $cycle = 0;
+ }
+ }
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFReadExtension ( )
+ ::
+ */
+ public function readExtensions()
+ {
+ $this->getByte(1);
+ for (; ;) {
+ $this->getByte(1);
+ if (($u = $this->GIF_buffer[0]) == 0x00) {
+ break;
+ }
+ $this->getByte($u);
+ /*
+ * 07.05.2007.
+ * Implemented a new line for a new function
+ * to determine the originaly delays between
+ * frames.
+ *
+ */
+ if (4 == $u) {
+ $this->GIF_delays[] = ($this->GIF_buffer[1] | $this->GIF_buffer[2] << 8);
+ }
+ }
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFReadExtension ( )
+ ::
+ */
+ public function readDescriptor()
+ {
+ $this->getByte(9);
+ $GIF_screen = $this->GIF_buffer;
+ $GIF_colorF = $this->GIF_buffer[8] & 0x80 ? 1 : 0;
+ if ($GIF_colorF) {
+ $GIF_code = $this->GIF_buffer[8] & 0x07;
+ $GIF_sort = $this->GIF_buffer[8] & 0x20 ? 1 : 0;
+ } else {
+ $GIF_code = $this->GIF_colorC;
+ $GIF_sort = $this->GIF_sorted;
+ }
+ $GIF_size = 2 << $GIF_code;
+ $this->GIF_screen[4] &= 0x70;
+ $this->GIF_screen[4] |= 0x80;
+ $this->GIF_screen[4] |= $GIF_code;
+ if ($GIF_sort) {
+ $this->GIF_screen[4] |= 0x08;
+ }
+ $this->GIF_string = "GIF87a";
+ $this->putByte($this->GIF_screen);
+ if (1 == $GIF_colorF) {
+ $this->getByte(3 * $GIF_size);
+ $this->putByte($this->GIF_buffer);
+ } else {
+ $this->putByte($this->GIF_global);
+ }
+ $this->GIF_string .= chr(0x2C);
+ $GIF_screen[8] &= 0x40;
+ $this->putByte($GIF_screen);
+ $this->getByte(1);
+ $this->putByte($this->GIF_buffer);
+ for (; ;) {
+ $this->getByte(1);
+ $this->putByte($this->GIF_buffer);
+ if (($u = $this->GIF_buffer[0]) == 0x00) {
+ break;
+ }
+ $this->getByte($u);
+ $this->putByte($this->GIF_buffer);
+ }
+ $this->GIF_string .= chr(0x3B);
+ /*
+ Add frames into $GIF_stream array...
+ */
+ $this->GIF_arrays[] = $this->GIF_string;
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFGetByte ( $len )
+ ::
+ */
+ public function getByte($len)
+ {
+ $this->GIF_buffer = [];
+ for ($i = 0; $i < $len; $i++) {
+ if ($this->GIF_bfseek > strlen($this->GIF_stream)) {
+ return 0;
+ }
+ $this->GIF_buffer[] = ord($this->GIF_stream{$this->GIF_bfseek++});
+ }
+ return 1;
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFPutByte ( $bytes )
+ ::
+ */
+ public function putByte($bytes)
+ {
+ for ($i = 0; $i < count($bytes); $i++) {
+ $this->GIF_string .= chr($bytes[$i]);
+ }
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: PUBLIC FUNCTIONS
+ ::
+ ::
+ :: GIFGetFrames ( )
+ ::
+ */
+ public function getFrames()
+ {
+ return ($this->GIF_arrays);
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFGetDelays ( )
+ ::
+ */
+ public function getDelays()
+ {
+ return ($this->GIF_delays);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/src/image/gif/Encoder.php b/vendor/topthink/think-image/src/image/gif/Encoder.php
new file mode 100644
index 0000000..9d898db
--- /dev/null
+++ b/vendor/topthink/think-image/src/image/gif/Encoder.php
@@ -0,0 +1,222 @@
+
+// +----------------------------------------------------------------------
+namespace think\image\gif;
+
+class Encoder
+{
+ public $GIF = "GIF89a"; /* GIF header 6 bytes */
+ public $VER = "GIFEncoder V2.05"; /* Encoder version */
+ public $BUF = [];
+ public $LOP = 0;
+ public $DIS = 2;
+ public $COL = -1;
+ public $IMG = -1;
+ public $ERR = [
+ 'ERR00' => "Does not supported function for only one image!",
+ 'ERR01' => "Source is not a GIF image!",
+ 'ERR02' => "Unintelligible flag ",
+ 'ERR03' => "Does not make animation from animated GIF source",
+ ];
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFEncoder...
+ ::
+ */
+ public function __construct(
+ $GIF_src, $GIF_dly, $GIF_lop, $GIF_dis,
+ $GIF_red, $GIF_grn, $GIF_blu, $GIF_mod
+ )
+ {
+ if (!is_array($GIF_src)) {
+ printf("%s: %s", $this->VER, $this->ERR['ERR00']);
+ exit(0);
+ }
+ $this->LOP = ($GIF_lop > -1) ? $GIF_lop : 0;
+ $this->DIS = ($GIF_dis > -1) ? (($GIF_dis < 3) ? $GIF_dis : 3) : 2;
+ $this->COL = ($GIF_red > -1 && $GIF_grn > -1 && $GIF_blu > -1) ?
+ ($GIF_red | ($GIF_grn << 8) | ($GIF_blu << 16)) : -1;
+ for ($i = 0; $i < count($GIF_src); $i++) {
+ if (strtolower($GIF_mod) == "url") {
+ $this->BUF[] = fread(fopen($GIF_src[$i], "rb"), filesize($GIF_src[$i]));
+ } else if (strtolower($GIF_mod) == "bin") {
+ $this->BUF[] = $GIF_src[$i];
+ } else {
+ printf("%s: %s ( %s )!", $this->VER, $this->ERR['ERR02'], $GIF_mod);
+ exit(0);
+ }
+ if (substr($this->BUF[$i], 0, 6) != "GIF87a" && substr($this->BUF[$i], 0, 6) != "GIF89a") {
+ printf("%s: %d %s", $this->VER, $i, $this->ERR['ERR01']);
+ exit(0);
+ }
+ for ($j = (13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07))), $k = true; $k; $j++) {
+ switch ($this->BUF[$i]{$j}) {
+ case "!":
+ if ((substr($this->BUF[$i], ($j + 3), 8)) == "NETSCAPE") {
+ printf("%s: %s ( %s source )!", $this->VER, $this->ERR['ERR03'], ($i + 1));
+ exit(0);
+ }
+ break;
+ case ";":
+ $k = false;
+ break;
+ }
+ }
+ }
+ $this->addHeader();
+ for ($i = 0; $i < count($this->BUF); $i++) {
+ $this->addFrames($i, $GIF_dly[$i]);
+ }
+ $this->addFooter();
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFAddHeader...
+ ::
+ */
+ public function addHeader()
+ {
+ if (ord($this->BUF[0]{10}) & 0x80) {
+ $cmap = 3 * (2 << (ord($this->BUF[0]{10}) & 0x07));
+ $this->GIF .= substr($this->BUF[0], 6, 7);
+ $this->GIF .= substr($this->BUF[0], 13, $cmap);
+ $this->GIF .= "!\377\13NETSCAPE2.0\3\1" . $this->word($this->LOP) . "\0";
+ }
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFAddFrames...
+ ::
+ */
+ public function addFrames($i, $d)
+ {
+ $Locals_img = '';
+ $Locals_str = 13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07));
+ $Locals_end = strlen($this->BUF[$i]) - $Locals_str - 1;
+ $Locals_tmp = substr($this->BUF[$i], $Locals_str, $Locals_end);
+ $Global_len = 2 << (ord($this->BUF[0]{10}) & 0x07);
+ $Locals_len = 2 << (ord($this->BUF[$i]{10}) & 0x07);
+ $Global_rgb = substr($this->BUF[0], 13,
+ 3 * (2 << (ord($this->BUF[0]{10}) & 0x07)));
+ $Locals_rgb = substr($this->BUF[$i], 13,
+ 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07)));
+ $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 0) .
+ chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . "\x0\x0";
+ if ($this->COL > -1 && ord($this->BUF[$i]{10}) & 0x80) {
+ for ($j = 0; $j < (2 << (ord($this->BUF[$i]{10}) & 0x07)); $j++) {
+ if (
+ ord($Locals_rgb{3 * $j + 0}) == (($this->COL >> 16) & 0xFF) &&
+ ord($Locals_rgb{3 * $j + 1}) == (($this->COL >> 8) & 0xFF) &&
+ ord($Locals_rgb{3 * $j + 2}) == (($this->COL >> 0) & 0xFF)
+ ) {
+ $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 1) .
+ chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . chr($j) . "\x0";
+ break;
+ }
+ }
+ }
+ switch ($Locals_tmp{0}) {
+ case "!":
+ /**
+ * @var string $Locals_img ;
+ */
+ $Locals_img = substr($Locals_tmp, 8, 10);
+ $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18);
+ break;
+ case ",":
+ $Locals_img = substr($Locals_tmp, 0, 10);
+ $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10);
+ break;
+ }
+ if (ord($this->BUF[$i]{10}) & 0x80 && $this->IMG > -1) {
+ if ($Global_len == $Locals_len) {
+ if ($this->blockCompare($Global_rgb, $Locals_rgb, $Global_len)) {
+ $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp);
+ } else {
+ $byte = ord($Locals_img{9});
+ $byte |= 0x80;
+ $byte &= 0xF8;
+ $byte |= (ord($this->BUF[0]{10}) & 0x07);
+ $Locals_img{9} = chr($byte);
+ $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp);
+ }
+ } else {
+ $byte = ord($Locals_img{9});
+ $byte |= 0x80;
+ $byte &= 0xF8;
+ $byte |= (ord($this->BUF[$i]{10}) & 0x07);
+ $Locals_img{9} = chr($byte);
+ $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp);
+ }
+ } else {
+ $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp);
+ }
+ $this->IMG = 1;
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFAddFooter...
+ ::
+ */
+ public function addFooter()
+ {
+ $this->GIF .= ";";
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFBlockCompare...
+ ::
+ */
+ public function blockCompare($GlobalBlock, $LocalBlock, $Len)
+ {
+ for ($i = 0; $i < $Len; $i++) {
+ if (
+ $GlobalBlock{3 * $i + 0} != $LocalBlock{3 * $i + 0} ||
+ $GlobalBlock{3 * $i + 1} != $LocalBlock{3 * $i + 1} ||
+ $GlobalBlock{3 * $i + 2} != $LocalBlock{3 * $i + 2}
+ ) {
+ return (0);
+ }
+ }
+ return (1);
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GIFWord...
+ ::
+ */
+ public function word($int)
+ {
+ return (chr($int & 0xFF) . chr(($int >> 8) & 0xFF));
+ }
+
+ /*
+ :::::::::::::::::::::::::::::::::::::::::::::::::::
+ ::
+ :: GetAnimation...
+ ::
+ */
+ public function getAnimation()
+ {
+ return ($this->GIF);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/src/image/gif/Gif.php b/vendor/topthink/think-image/src/image/gif/Gif.php
new file mode 100644
index 0000000..46fc9bf
--- /dev/null
+++ b/vendor/topthink/think-image/src/image/gif/Gif.php
@@ -0,0 +1,88 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\image\gif;
+
+class Gif
+{
+ /**
+ * GIF帧列表
+ *
+ * @var array
+ */
+ private $frames = [];
+ /**
+ * 每帧等待时间列表
+ *
+ * @var array
+ */
+ private $delays = [];
+
+ /**
+ * 构造方法,用于解码GIF图片
+ *
+ * @param string $src GIF图片数据
+ * @param string $mod 图片数据类型
+ * @throws \Exception
+ */
+ public function __construct($src = null, $mod = 'url')
+ {
+ if (!is_null($src)) {
+ if ('url' == $mod && is_file($src)) {
+ $src = file_get_contents($src);
+ }
+ /* 解码GIF图片 */
+ try {
+ $de = new Decoder($src);
+ $this->frames = $de->getFrames();
+ $this->delays = $de->getDelays();
+ } catch (\Exception $e) {
+ throw new \Exception("解码GIF图片出错");
+ }
+ }
+ }
+
+ /**
+ * 设置或获取当前帧的数据
+ *
+ * @param string $stream 二进制数据流
+ * @return mixed 获取到的数据
+ */
+ public function image($stream = null)
+ {
+ if (is_null($stream)) {
+ $current = current($this->frames);
+ return false === $current ? reset($this->frames) : $current;
+ }
+ $this->frames[key($this->frames)] = $stream;
+ }
+
+ /**
+ * 将当前帧移动到下一帧
+ *
+ * @return string 当前帧数据
+ */
+ public function nextImage()
+ {
+ return next($this->frames);
+ }
+
+ /**
+ * 编码并保存当前GIF图片
+ *
+ * @param string $pathname 图片名称
+ */
+ public function save($pathname)
+ {
+ $gif = new Encoder($this->frames, $this->delays, 0, 2, 0, 0, 0, 'bin');
+ file_put_contents($pathname, $gif->getAnimation());
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/CropTest.php b/vendor/topthink/think-image/tests/CropTest.php
new file mode 100644
index 0000000..48d6c73
--- /dev/null
+++ b/vendor/topthink/think-image/tests/CropTest.php
@@ -0,0 +1,67 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class CropTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/crop.jpg';
+ $image = Image::open($this->getJpeg());
+
+ $image->crop(200, 200, 100, 100, 300, 300)->save($pathname);
+
+ $this->assertEquals(300, $image->width());
+ $this->assertEquals(300, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testPng()
+ {
+ $pathname = TEST_PATH . 'tmp/crop.png';
+ $image = Image::open($this->getPng());
+
+ $image->crop(200, 200, 100, 100, 300, 300)->save($pathname);
+
+ $this->assertEquals(300, $image->width());
+ $this->assertEquals(300, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/crop.gif';
+ $image = Image::open($this->getGif());
+
+ $image->crop(200, 200, 100, 100, 300, 300)->save($pathname);
+
+ $this->assertEquals(300, $image->width());
+ $this->assertEquals(300, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/FlipTest.php b/vendor/topthink/think-image/tests/FlipTest.php
new file mode 100644
index 0000000..21b80b9
--- /dev/null
+++ b/vendor/topthink/think-image/tests/FlipTest.php
@@ -0,0 +1,43 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class FlipTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/flip.jpg';
+ $image = Image::open($this->getJpeg());
+ $image->flip()->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/flip.gif';
+ $image = Image::open($this->getGif());
+ $image->flip(Image::FLIP_Y)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/InfoTest.php b/vendor/topthink/think-image/tests/InfoTest.php
new file mode 100644
index 0000000..9f7cb3c
--- /dev/null
+++ b/vendor/topthink/think-image/tests/InfoTest.php
@@ -0,0 +1,60 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class InfoTest extends TestCase
+{
+
+ public function testOpen()
+ {
+ $this->setExpectedException("\\think\\image\\Exception");
+ Image::open('');
+ }
+
+ public function testIllegal()
+ {
+ $this->setExpectedException("\\think\\image\\Exception", 'Illegal image file');
+ Image::open(TEST_PATH . 'images/test.bmp');
+ }
+
+ public function testJpeg()
+ {
+ $image = Image::open($this->getJpeg());
+ $this->assertEquals(800, $image->width());
+ $this->assertEquals(600, $image->height());
+ $this->assertEquals('jpeg', $image->type());
+ $this->assertEquals('image/jpeg', $image->mime());
+ $this->assertEquals([800, 600], $image->size());
+ }
+
+
+ public function testPng()
+ {
+ $image = Image::open($this->getPng());
+ $this->assertEquals(800, $image->width());
+ $this->assertEquals(600, $image->height());
+ $this->assertEquals('png', $image->type());
+ $this->assertEquals('image/png', $image->mime());
+ $this->assertEquals([800, 600], $image->size());
+ }
+
+ public function testGif()
+ {
+ $image = Image::open($this->getGif());
+ $this->assertEquals(380, $image->width());
+ $this->assertEquals(216, $image->height());
+ $this->assertEquals('gif', $image->type());
+ $this->assertEquals('image/gif', $image->mime());
+ $this->assertEquals([380, 216], $image->size());
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/RotateTest.php b/vendor/topthink/think-image/tests/RotateTest.php
new file mode 100644
index 0000000..85ecd79
--- /dev/null
+++ b/vendor/topthink/think-image/tests/RotateTest.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class RotateTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/rotate.jpg';
+ $image = Image::open($this->getJpeg());
+ $image->rotate(90)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/rotate.gif';
+ $image = Image::open($this->getGif());
+ $image->rotate(90)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/TestCase.php b/vendor/topthink/think-image/tests/TestCase.php
new file mode 100644
index 0000000..888ace8
--- /dev/null
+++ b/vendor/topthink/think-image/tests/TestCase.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace tests;
+
+use think\File;
+
+abstract class TestCase extends \PHPUnit_Framework_TestCase
+{
+
+ protected function getJpeg()
+ {
+ return new File(TEST_PATH . 'images/test.jpg');
+ }
+
+ protected function getPng()
+ {
+ return new File(TEST_PATH . 'images/test.png');
+ }
+
+ protected function getGif()
+ {
+ return new File(TEST_PATH . 'images/test.gif');
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/TextTest.php b/vendor/topthink/think-image/tests/TextTest.php
new file mode 100644
index 0000000..992c427
--- /dev/null
+++ b/vendor/topthink/think-image/tests/TextTest.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class TextTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/text.jpg';
+ $image = Image::open($this->getJpeg());
+
+ $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testPng()
+ {
+ $pathname = TEST_PATH . 'tmp/text.png';
+ $image = Image::open($this->getPng());
+
+ $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/text.gif';
+ $image = Image::open($this->getGif());
+
+ $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/ThumbTest.php b/vendor/topthink/think-image/tests/ThumbTest.php
new file mode 100644
index 0000000..7f461db
--- /dev/null
+++ b/vendor/topthink/think-image/tests/ThumbTest.php
@@ -0,0 +1,284 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class ThumbTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/thumb.jpg';
+
+ //1
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //2
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(150, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //3
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //4
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //5
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //6
+ $image = Image::open($this->getJpeg());
+
+ $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+
+ public function testPng()
+ {
+ $pathname = TEST_PATH . 'tmp/thumb.png';
+
+ //1
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //2
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(150, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //3
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //4
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //5
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //6
+ $image = Image::open($this->getPng());
+
+ $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/thumb.gif';
+
+ //1
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //2
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(113, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //3
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //4
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //5
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+
+ //6
+ $image = Image::open($this->getGif());
+
+ $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname);
+
+ $this->assertEquals(200, $image->width());
+ $this->assertEquals(200, $image->height());
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/WaterTest.php b/vendor/topthink/think-image/tests/WaterTest.php
new file mode 100644
index 0000000..3daa614
--- /dev/null
+++ b/vendor/topthink/think-image/tests/WaterTest.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+namespace tests;
+
+use think\Image;
+
+class WaterTest extends TestCase
+{
+ public function testJpeg()
+ {
+ $pathname = TEST_PATH . 'tmp/water.jpg';
+ $image = Image::open($this->getJpeg());
+
+ $image->water(TEST_PATH . 'images/test.gif')->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testPng()
+ {
+ $pathname = TEST_PATH . 'tmp/water.png';
+ $image = Image::open($this->getPng());
+
+ $image->water(TEST_PATH . 'images/test.gif')->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+
+ public function testGif()
+ {
+ $pathname = TEST_PATH . 'tmp/water.gif';
+ $image = Image::open($this->getGif());
+
+ $image->water(TEST_PATH . 'images/test.jpg')->save($pathname);
+
+ $file = new \SplFileInfo($pathname);
+
+ $this->assertTrue($file->isFile());
+
+ @unlink($pathname);
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/autoload.php b/vendor/topthink/think-image/tests/autoload.php
new file mode 100644
index 0000000..1110027
--- /dev/null
+++ b/vendor/topthink/think-image/tests/autoload.php
@@ -0,0 +1,15 @@
+
+// +----------------------------------------------------------------------
+define('TEST_PATH', __DIR__ . '/');
+// 加载框架基础文件
+require __DIR__ . '/../thinkphp/base.php';
+\think\Loader::addNamespace('tests', TEST_PATH);
+\think\Loader::addNamespace('think', __DIR__ . '/../src/');
\ No newline at end of file
diff --git a/vendor/topthink/think-image/tests/images/test.bmp b/vendor/topthink/think-image/tests/images/test.bmp
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/topthink/think-image/tests/images/test.gif b/vendor/topthink/think-image/tests/images/test.gif
new file mode 100644
index 0000000..c6d5472
Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.gif differ
diff --git a/vendor/topthink/think-image/tests/images/test.jpg b/vendor/topthink/think-image/tests/images/test.jpg
new file mode 100644
index 0000000..4bb6549
Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.jpg differ
diff --git a/vendor/topthink/think-image/tests/images/test.png b/vendor/topthink/think-image/tests/images/test.png
new file mode 100644
index 0000000..f4830e3
Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.png differ
diff --git a/vendor/topthink/think-image/tests/images/test.ttf b/vendor/topthink/think-image/tests/images/test.ttf
new file mode 100644
index 0000000..4f985c8
Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.ttf differ
diff --git a/vendor/topthink/think-image/tests/tmp/.gitignore b/vendor/topthink/think-image/tests/tmp/.gitignore
new file mode 100644
index 0000000..a3a0c8b
--- /dev/null
+++ b/vendor/topthink/think-image/tests/tmp/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/vendor/topthink/think-orm/.gitignore b/vendor/topthink/think-orm/.gitignore
new file mode 100644
index 0000000..7412cf8
--- /dev/null
+++ b/vendor/topthink/think-orm/.gitignore
@@ -0,0 +1,3 @@
+.idea
+composer.lock
+vendor
diff --git a/vendor/topthink/think-orm/LICENSE b/vendor/topthink/think-orm/LICENSE
new file mode 100644
index 0000000..c0ee812
--- /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/README.md b/vendor/topthink/think-orm/README.md
new file mode 100644
index 0000000..4cbdbd7
--- /dev/null
+++ b/vendor/topthink/think-orm/README.md
@@ -0,0 +1,27 @@
+# ThinkORM
+
+基于PHP7.1+ 和PDO实现的ORM,支持多数据库,2.0版本主要特性包括:
+
+* 基于PDO和PHP强类型实现
+* 支持原生查询和查询构造器
+* 自动参数绑定和预查询
+* 简洁易用的查询功能
+* 强大灵活的模型用法
+* 支持预载入关联查询和延迟关联查询
+* 支持多数据库及动态切换
+* 支持`MongoDb`
+* 支持分布式及事务
+* 支持断点重连
+* 支持`JSON`查询
+* 支持数据库日志
+* 支持`PSR-16`缓存及`PSR-3`日志规范
+
+
+## 安装
+~~~
+composer require topthink/think-orm
+~~~
+
+## 文档
+
+详细参考 [ThinkORM开发指南](https://www.kancloud.cn/manual/think-orm/content)
diff --git a/vendor/topthink/think-orm/composer.json b/vendor/topthink/think-orm/composer.json
new file mode 100644
index 0000000..57dfdeb
--- /dev/null
+++ b/vendor/topthink/think-orm/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "topthink/think-orm",
+ "description": "think orm",
+ "keywords": [
+ "orm",
+ "database"
+ ],
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "ext-json": "*",
+ "psr/simple-cache": "^1.0",
+ "psr/log": "~1.0",
+ "topthink/think-helper":"^3.1"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ },
+ "files": []
+ }
+}
diff --git a/vendor/topthink/think-orm/src/DbManager.php b/vendor/topthink/think-orm/src/DbManager.php
new file mode 100644
index 0000000..489308e
--- /dev/null
+++ b/vendor/topthink/think-orm/src/DbManager.php
@@ -0,0 +1,396 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Psr\SimpleCache\CacheInterface;
+use think\db\BaseQuery;
+use think\db\ConnectionInterface;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * Class DbManager
+ * @package think
+ * @mixin BaseQuery
+ * @mixin Query
+ */
+class DbManager
+{
+ /**
+ * 数据库连接实例
+ * @var array
+ */
+ protected $instance = [];
+
+ /**
+ * 数据库配置
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * Event对象或者数组
+ * @var array|object
+ */
+ protected $event;
+
+ /**
+ * SQL监听
+ * @var array
+ */
+ protected $listen = [];
+
+ /**
+ * SQL日志
+ * @var array
+ */
+ protected $dbLog = [];
+
+ /**
+ * 查询次数
+ * @var int
+ */
+ protected $queryTimes = 0;
+
+ /**
+ * 查询缓存对象
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * 查询日志对象
+ * @var LoggerInterface
+ */
+ protected $log;
+
+ /**
+ * 架构函数
+ * @access public
+ */
+ public function __construct()
+ {
+ $this->modelMaker();
+ }
+
+ /**
+ * 注入模型对象
+ * @access public
+ * @return void
+ */
+ protected function modelMaker()
+ {
+ $this->triggerSql();
+
+ Model::setDb($this);
+
+ if (is_object($this->event)) {
+ Model::setEvent($this->event);
+ }
+
+ Model::maker(function (Model $model) {
+ $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
+
+ if (is_null($isAutoWriteTimestamp)) {
+ // 自动写入时间戳
+ $model->isAutoWriteTimestamp($this->getConfig('auto_timestamp', true));
+ }
+
+ $dateFormat = $model->getDateFormat();
+
+ if (is_null($dateFormat)) {
+ // 设置时间戳格式
+ $model->setDateFormat($this->getConfig('datetime_format', 'Y-m-d H:i:s'));
+ }
+ });
+ }
+
+ /**
+ * 监听SQL
+ * @access protected
+ * @return void
+ */
+ protected function triggerSql(): void
+ {
+ // 监听SQL
+ $this->listen(function ($sql, $time, $master) {
+ if (0 === strpos($sql, 'CONNECT:')) {
+ $this->log($sql);
+ return;
+ }
+
+ // 记录SQL
+ if (is_bool($master)) {
+ // 分布式记录当前操作的主从
+ $master = $master ? 'master|' : 'slave|';
+ } else {
+ $master = '';
+ }
+
+ $this->log($sql . ' [ ' . $master . 'RunTime:' . $time . 's ]');
+ });
+ }
+
+ /**
+ * 初始化配置参数
+ * @access public
+ * @param array $config 连接配置
+ * @return void
+ */
+ public function setConfig($config): void
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * 设置缓存对象
+ * @access public
+ * @param CacheInterface $cache 缓存对象
+ * @return void
+ */
+ public function setCache(CacheInterface $cache): void
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * 设置日志对象
+ * @access public
+ * @param LoggerInterface $log 日志对象
+ * @return void
+ */
+ public function setLog(LoggerInterface $log): void
+ {
+ $this->log = $log;
+ }
+
+ /**
+ * 记录SQL日志
+ * @access protected
+ * @param string $log SQL日志信息
+ * @param string $type 日志类型
+ * @return void
+ */
+ public function log(string $log, string $type = 'sql')
+ {
+ if ($this->log) {
+ $this->log->log($type, $log);
+ } else {
+ $this->dbLog[$type][] = $log;
+ }
+ }
+
+ /**
+ * 获得查询日志(没有设置日志对象使用)
+ * @access public
+ * @param bool $clear 是否清空
+ * @return array
+ */
+ public function getDbLog(bool $clear = false): array
+ {
+ $logs = $this->dbLog;
+ if ($clear) {
+ $this->dbLog = [];
+ }
+
+ return $logs;
+ }
+
+ /**
+ * 获取配置参数
+ * @access public
+ * @param string $name 配置参数
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(string $name = '', $default = null)
+ {
+ if ('' === $name) {
+ return $this->config;
+ }
+
+ return $this->config[$name] ?? $default;
+ }
+
+ /**
+ * 创建/切换数据库连接查询
+ * @access public
+ * @param string|null $name 连接配置标识
+ * @param bool $force 强制重新连接
+ * @return ConnectionInterface
+ */
+ public function connect(string $name = null, bool $force = false)
+ {
+ return $this->instance($name, $force);
+ }
+
+ /**
+ * 创建数据库连接实例
+ * @access protected
+ * @param string|null $name 连接标识
+ * @param bool $force 强制重新连接
+ * @return ConnectionInterface
+ */
+ protected function instance(string $name = null, bool $force = false): ConnectionInterface
+ {
+ if (empty($name)) {
+ $name = $this->getConfig('default', 'mysql');
+ }
+
+ if ($force || !isset($this->instance[$name])) {
+ $this->instance[$name] = $this->createConnection($name);
+ }
+
+ return $this->instance[$name];
+ }
+
+ /**
+ * 获取连接配置
+ * @param string $name
+ * @return array
+ */
+ protected function getConnectionConfig(string $name): array
+ {
+ $connections = $this->getConfig('connections');
+ if (!isset($connections[$name])) {
+ throw new InvalidArgumentException('Undefined db config:' . $name);
+ }
+
+ return $connections[$name];
+ }
+
+ /**
+ * 创建连接
+ * @param $name
+ * @return ConnectionInterface
+ */
+ protected function createConnection(string $name): ConnectionInterface
+ {
+ $config = $this->getConnectionConfig($name);
+
+ $type = !empty($config['type']) ? $config['type'] : 'mysql';
+
+ if (false !== strpos($type, '\\')) {
+ $class = $type;
+ } else {
+ $class = '\\think\\db\\connector\\' . ucfirst($type);
+ }
+
+ /** @var ConnectionInterface $connection */
+ $connection = new $class($config);
+ $connection->setDb($this);
+
+ if ($this->cache) {
+ $connection->setCache($this->cache);
+ }
+
+ return $connection;
+ }
+
+ /**
+ * 使用表达式设置数据
+ * @access public
+ * @param string $value 表达式
+ * @return Raw
+ */
+ public function raw(string $value): Raw
+ {
+ return new Raw($value);
+ }
+
+ /**
+ * 更新查询次数
+ * @access public
+ * @return void
+ */
+ public function updateQueryTimes(): void
+ {
+ $this->queryTimes++;
+ }
+
+ /**
+ * 重置查询次数
+ * @access public
+ * @return void
+ */
+ public function clearQueryTimes(): void
+ {
+ $this->queryTimes = 0;
+ }
+
+ /**
+ * 获得查询次数
+ * @access public
+ * @return integer
+ */
+ public function getQueryTimes(): int
+ {
+ return $this->queryTimes;
+ }
+
+ /**
+ * 监听SQL执行
+ * @access public
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function listen(callable $callback): void
+ {
+ $this->listen[] = $callback;
+ }
+
+ /**
+ * 获取监听SQL执行
+ * @access public
+ * @return array
+ */
+ public function getListen(): array
+ {
+ return $this->listen;
+ }
+
+ /**
+ * 注册回调方法
+ * @access public
+ * @param string $event 事件名
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function event(string $event, callable $callback): void
+ {
+ $this->event[$event][] = $callback;
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string $event 事件名
+ * @param mixed $params 传入参数
+ * @return mixed
+ */
+ public function trigger(string $event, $params = null)
+ {
+ if (isset($this->event[$event])) {
+ foreach ($this->event[$event] as $callback) {
+ call_user_func_array($callback, [$this]);
+ }
+ }
+ }
+
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->connect(), $method], $args);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/Model.php b/vendor/topthink/think-orm/src/Model.php
new file mode 100644
index 0000000..7d92bd2
--- /dev/null
+++ b/vendor/topthink/think-orm/src/Model.php
@@ -0,0 +1,1060 @@
+
+// +----------------------------------------------------------------------
+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 更新条件
+ * @return Model
+ */
+ public function newInstance(array $data = [], $where = null): 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;
+
+ 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->set = [];
+ $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 && !isset($data[$this->updateTime])) {
+ // 自动写入更新时间
+ $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
+ $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 ($this->autoWriteTimestamp) {
+ if ($this->createTime && !isset($this->data[$this->createTime])) {
+ $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime);
+ }
+
+ if ($this->updateTime && !isset($this->data[$this->updateTime])) {
+ $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
+ }
+ }
+
+ if (false === $this->trigger('BeforeInsert')) {
+ return false;
+ }
+
+ $this->checkData();
+
+ // 检查允许字段
+ $allowFields = $this->checkAllowFields();
+
+ $db = $this->db();
+
+ $db->transaction(function () use ($sequence, $allowFields, $db) {
+ $result = $db->strict(false)
+ ->field($allowFields)
+ ->replace($this->replace)
+ ->sequence($sequence)
+ ->insert($this->data, true);
+
+ // 获取自动增长主键
+ if ($result) {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) {
+ $this->data[$pk] = $result;
+ }
+ }
+
+ // 关联写入
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationInsert();
+ }
+ });
+
+ // 标记数据已经存在
+ $this->exists = true;
+
+ // 新增回调
+ $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->relation[$name]);
+ }
+
+ // ArrayAccess
+ public function offsetSet($name, $value)
+ {
+ $this->setAttr($name, $value);
+ }
+
+ public function offsetExists($name): bool
+ {
+ return $this->__isset($name);
+ }
+
+ public function offsetUnset($name)
+ {
+ $this->__unset($name);
+ }
+
+ 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);
+ }
+
+ if ('withattr' == strtolower($method)) {
+ return call_user_func_array([$this, 'withAttribute'], $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..29ebf1f
--- /dev/null
+++ b/vendor/topthink/think-orm/src/Paginator.php
@@ -0,0 +1,518 @@
+
+// +----------------------------------------------------------------------
+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]', $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;
+ }
+
+ 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
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator($this->items->all());
+ }
+
+ /**
+ * Whether a offset exists
+ * @access public
+ * @param mixed $offset
+ * @return bool
+ */
+ public function offsetExists($offset)
+ {
+ return $this->items->offsetExists($offset);
+ }
+
+ /**
+ * Offset to retrieve
+ * @access public
+ * @param mixed $offset
+ * @return mixed
+ */
+ public function offsetGet($offset)
+ {
+ return $this->items->offsetGet($offset);
+ }
+
+ /**
+ * Offset to set
+ * @access public
+ * @param mixed $offset
+ * @param mixed $value
+ */
+ public function offsetSet($offset, $value)
+ {
+ $this->items->offsetSet($offset, $value);
+ }
+
+ /**
+ * Offset to unset
+ * @access public
+ * @param mixed $offset
+ * @return void
+ * @since 5.0.0
+ */
+ 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
+ */
+ 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/BaseQuery.php b/vendor/topthink/think-orm/src/db/BaseQuery.php
new file mode 100644
index 0000000..447b749
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/BaseQuery.php
@@ -0,0 +1,1282 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException as Exception;
+use think\db\exception\ModelNotFoundException;
+use think\helper\Str;
+use think\Model;
+use think\Paginator;
+
+/**
+ * 数据查询基础类
+ */
+abstract class BaseQuery
+{
+ use concern\TimeFieldQuery;
+ use concern\AggregateQuery;
+ use concern\ModelRelationQuery;
+ use concern\ResultOperation;
+ use concern\Transaction;
+ use concern\WhereQuery;
+
+ /**
+ * 当前数据库连接对象
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * 当前数据表名称(不含前缀)
+ * @var string
+ */
+ protected $name = '';
+
+ /**
+ * 当前数据表主键
+ * @var string|array
+ */
+ protected $pk;
+
+ /**
+ * 当前数据表自增主键
+ * @var string
+ */
+ protected $autoinc;
+
+ /**
+ * 当前数据表前缀
+ * @var string
+ */
+ protected $prefix = '';
+
+ /**
+ * 当前查询参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param ConnectionInterface $connection 数据库连接对象
+ */
+ public function __construct(ConnectionInterface $connection)
+ {
+ $this->connection = $connection;
+
+ $this->prefix = $this->connection->getConfig('prefix');
+ }
+
+ /**
+ * 利用__call方法实现一些特殊的Model方法
+ * @access public
+ * @param string $method 方法名称
+ * @param array $args 调用参数
+ * @return mixed
+ * @throws Exception
+ */
+ public function __call(string $method, array $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]);
+ } elseif (strtolower(substr($method, 0, 7)) == 'whereor') {
+ $name = Str::snake(substr($method, 7));
+ array_unshift($args, $name);
+ return call_user_func_array([$this, 'whereOr'], $args);
+ } elseif (strtolower(substr($method, 0, 5)) == 'where') {
+ $name = Str::snake(substr($method, 5));
+ array_unshift($args, $name);
+ return call_user_func_array([$this, 'where'], $args);
+ } elseif ($this->model && method_exists($this->model, 'scope' . $method)) {
+ // 动态调用命名范围
+ $method = 'scope' . $method;
+ array_unshift($args, $this);
+
+ call_user_func_array([$this->model, $method], $args);
+ return $this;
+ } else {
+ throw new Exception('method not exist:' . static::class . '->' . $method);
+ }
+ }
+
+ /**
+ * 创建一个新的查询对象
+ * @access public
+ * @return BaseQuery
+ */
+ public function newQuery(): BaseQuery
+ {
+ $query = new static($this->connection);
+
+ if ($this->model) {
+ $query->model($this->model);
+ }
+
+ if (isset($this->options['table'])) {
+ $query->table($this->options['table']);
+ } else {
+ $query->name($this->name);
+ }
+
+ if (isset($this->options['json'])) {
+ $query->json($this->options['json'], $this->options['json_assoc']);
+ }
+
+ if (isset($this->options['field_type'])) {
+ $query->setFieldType($this->options['field_type']);
+ }
+
+ return $query;
+ }
+
+ /**
+ * 获取当前的数据库Connection对象
+ * @access public
+ * @return ConnectionInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * 指定当前数据表名(不含前缀)
+ * @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 ?: $this->model->getName();
+ }
+
+ /**
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $name 参数名称
+ * @return mixed
+ */
+ public function getConfig(string $name = '')
+ {
+ return $this->connection->getConfig($name);
+ }
+
+ /**
+ * 得到当前或者指定名称的数据表
+ * @access public
+ * @param string $name 不含前缀的数据表名字
+ * @return mixed
+ */
+ public function getTable(string $name = '')
+ {
+ if (empty($name) && isset($this->options['table'])) {
+ return $this->options['table'];
+ }
+
+ $name = $name ?: $this->name;
+
+ return $this->prefix . Str::snake($name);
+ }
+
+ /**
+ * 设置字段类型信息
+ * @access public
+ * @param array $type 字段类型信息
+ * @return $this
+ */
+ public function setFieldType(array $type)
+ {
+ $this->options['field_type'] = $type;
+ return $this;
+ }
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string
+ {
+ return $this->connection->getLastSql();
+ }
+
+ /**
+ * 获取返回或者影响的记录数
+ * @access public
+ * @return integer
+ */
+ public function getNumRows(): int
+ {
+ return $this->connection->getNumRows();
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param string $sequence 自增序列名
+ * @return mixed
+ */
+ public function getLastInsID(string $sequence = null)
+ {
+ return $this->connection->getLastInsID($this, $sequence);
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function value(string $field, $default = null)
+ {
+ return $this->connection->value($this, $field, $default);
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param string $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(string $field, string $key = ''): array
+ {
+ return $this->connection->column($this, $field, $key);
+ }
+
+ /**
+ * 查询SQL组装 union
+ * @access public
+ * @param mixed $union UNION
+ * @param boolean $all 是否适用UNION ALL
+ * @return $this
+ */
+ public function union($union, bool $all = false)
+ {
+ $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION';
+
+ if (is_array($union)) {
+ $this->options['union'] = array_merge($this->options['union'], $union);
+ } else {
+ $this->options['union'][] = $union;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 查询SQL组装 union all
+ * @access public
+ * @param mixed $union UNION数据
+ * @return $this
+ */
+ public function unionAll($union)
+ {
+ return $this->union($union, true);
+ }
+
+ /**
+ * 指定查询字段
+ * @access public
+ * @param mixed $field 字段信息
+ * @return $this
+ */
+ public function field($field)
+ {
+ if (empty($field)) {
+ return $this;
+ } elseif ($field instanceof Raw) {
+ $this->options['field'][] = $field;
+ return $this;
+ }
+
+ if (is_string($field)) {
+ if (preg_match('/[\<\'\"\(]/', $field)) {
+ return $this->fieldRaw($field);
+ }
+
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ if (true === $field) {
+ // 获取全部字段
+ $fields = $this->getTableFields();
+ $field = $fields ?: ['*'];
+ }
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field);
+
+ return $this;
+ }
+
+ /**
+ * 指定要排除的查询字段
+ * @access public
+ * @param array|string $field 要排除的字段
+ * @return $this
+ */
+ public function withoutField($field)
+ {
+ if (empty($field)) {
+ return $this;
+ }
+
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ // 字段排除
+ $fields = $this->getTableFields();
+ $field = $fields ? array_diff($fields, $field) : $field;
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field);
+
+ return $this;
+ }
+
+ /**
+ * 指定其它数据表的查询字段
+ * @access public
+ * @param mixed $field 字段信息
+ * @param string $tableName 数据表名
+ * @param string $prefix 字段前缀
+ * @param string $alias 别名前缀
+ * @return $this
+ */
+ public function tableField($field, string $tableName, string $prefix = '', string $alias = '')
+ {
+ if (empty($field)) {
+ return $this;
+ }
+
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ if (true === $field) {
+ // 获取全部字段
+ $fields = $this->getTableFields($tableName);
+ $field = $fields ?: ['*'];
+ }
+
+ // 添加统一的前缀
+ $prefix = $prefix ?: $tableName;
+ foreach ($field as $key => &$val) {
+ if (is_numeric($key) && $alias) {
+ $field[$prefix . '.' . $val] = $alias . $val;
+ unset($field[$key]);
+ } elseif (is_numeric($key)) {
+ $val = $prefix . '.' . $val;
+ }
+ }
+
+ if (isset($this->options['field'])) {
+ $field = array_merge((array) $this->options['field'], $field);
+ }
+
+ $this->options['field'] = array_unique($field);
+
+ return $this;
+ }
+
+ /**
+ * 设置数据
+ * @access public
+ * @param array $data 数据
+ * @return $this
+ */
+ public function data(array $data)
+ {
+ $this->options['data'] = $data;
+
+ return $this;
+ }
+
+ /**
+ * 去除查询参数
+ * @access public
+ * @param string $option 参数名 留空去除所有参数
+ * @return $this
+ */
+ public function removeOption(string $option = '')
+ {
+ if ('' === $option) {
+ $this->options = [];
+ $this->bind = [];
+ } elseif (isset($this->options[$option])) {
+ unset($this->options[$option]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定查询数量
+ * @access public
+ * @param int $offset 起始位置
+ * @param int $length 查询数量
+ * @return $this
+ */
+ public function limit(int $offset, int $length = null)
+ {
+ $this->options['limit'] = $offset . ($length ? ',' . $length : '');
+
+ return $this;
+ }
+
+ /**
+ * 指定分页
+ * @access public
+ * @param int $page 页数
+ * @param int $listRows 每页数量
+ * @return $this
+ */
+ public function page(int $page, int $listRows = null)
+ {
+ $this->options['page'] = [$page, $listRows];
+
+ return $this;
+ }
+
+ /**
+ * 指定当前操作的数据表
+ * @access public
+ * @param mixed $table 表名
+ * @return $this
+ */
+ public function table($table)
+ {
+ if (is_string($table)) {
+ if (strpos($table, ')')) {
+ // 子查询
+ } elseif (false === strpos($table, ',')) {
+ if (strpos($table, ' ')) {
+ [$item, $alias] = explode(' ', $table);
+ $table = [];
+ $this->alias([$item => $alias]);
+ $table[$item] = $alias;
+ }
+ } else {
+ $tables = explode(',', $table);
+ $table = [];
+
+ foreach ($tables as $item) {
+ $item = trim($item);
+ if (strpos($item, ' ')) {
+ [$item, $alias] = explode(' ', $item);
+ $this->alias([$item => $alias]);
+ $table[$item] = $alias;
+ } else {
+ $table[] = $item;
+ }
+ }
+ }
+ } elseif (is_array($table)) {
+ $tables = $table;
+ $table = [];
+
+ foreach ($tables as $key => $val) {
+ if (is_numeric($key)) {
+ $table[] = $val;
+ } else {
+ $this->alias([$key => $val]);
+ $table[$key] = $val;
+ }
+ }
+ }
+
+ $this->options['table'] = $table;
+
+ return $this;
+ }
+
+ /**
+ * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc'])
+ * @access public
+ * @param string|array|Raw $field 排序字段
+ * @param string $order 排序
+ * @return $this
+ */
+ public function order($field, string $order = '')
+ {
+ if (empty($field)) {
+ return $this;
+ } elseif ($field instanceof Raw) {
+ $this->options['order'][] = $field;
+ return $this;
+ }
+
+ if (is_string($field)) {
+ if (!empty($this->options['via'])) {
+ $field = $this->options['via'] . '.' . $field;
+ }
+ if (strpos($field, ',')) {
+ $field = array_map('trim', explode(',', $field));
+ } else {
+ $field = empty($order) ? $field : [$field => $order];
+ }
+ } elseif (!empty($this->options['via'])) {
+ foreach ($field as $key => $val) {
+ if (is_numeric($key)) {
+ $field[$key] = $this->options['via'] . '.' . $val;
+ } else {
+ $field[$this->options['via'] . '.' . $key] = $val;
+ unset($field[$key]);
+ }
+ }
+ }
+
+ if (!isset($this->options['order'])) {
+ $this->options['order'] = [];
+ }
+
+ if (is_array($field)) {
+ $this->options['order'] = array_merge($this->options['order'], $field);
+ } else {
+ $this->options['order'][] = $field;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 分页查询
+ * @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']);
+
+ $bind = $this->bind;
+ $total = $this->count();
+ $results = $this->options($options)->bind($bind)->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 int|array $listRows 每页数量或者分页配置
+ * @param string $key 分页索引键
+ * @param string $sort 索引键排序 asc|desc
+ * @return Paginator
+ * @throws Exception
+ */
+ public function paginateX($listRows = null, string $key = null, string $sort = null): Paginator
+ {
+ $defaultConfig = [
+ 'query' => [], //url额外参数
+ 'fragment' => '', //url锚点
+ 'var_page' => 'page', //分页变量
+ 'list_rows' => 15, //每页数量
+ ];
+
+ $config = is_array($listRows) ? array_merge($defaultConfig, $listRows) : $defaultConfig;
+ $listRows = is_int($listRows) ? $listRows : (int) $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();
+
+ $key = $key ?: $this->getPk();
+ $options = $this->getOptions();
+
+ if (is_null($sort)) {
+ $order = $options['order'] ?? '';
+ if (!empty($order)) {
+ $sort = $order[$key] ?? 'desc';
+ } else {
+ $this->order($key, 'desc');
+ $sort = 'desc';
+ }
+ } else {
+ $this->order($key, $sort);
+ }
+
+ $newOption = $options;
+ unset($newOption['field'], $newOption['page']);
+
+ $data = $this->newQuery()
+ ->options($newOption)
+ ->field($key)
+ ->where(true)
+ ->order($key, $sort)
+ ->limit(1)
+ ->find();
+
+ $result = $data[$key];
+
+ if (is_numeric($result)) {
+ $lastId = 'asc' == $sort ? ($result - 1) + ($page - 1) * $listRows : ($result + 1) - ($page - 1) * $listRows;
+ } else {
+ throw new Exception('not support type');
+ }
+
+ $results = $this->when($lastId, function ($query) use ($key, $sort, $lastId) {
+ $query->where($key, 'asc' == $sort ? '>' : '<', $lastId);
+ })
+ ->limit($listRows)
+ ->select();
+
+ $this->options($options);
+
+ return Paginator::make($results, $listRows, $page, null, true, $config);
+ }
+
+ /**
+ * 根据最后ID查询更多N个数据
+ * @access public
+ * @param int $limit LIMIT
+ * @param int|string $lastId LastId
+ * @param string $key 分页索引键 默认为主键
+ * @param string $sort 索引键排序 asc|desc
+ * @return array
+ * @throws Exception
+ */
+ public function more(int $limit, $lastId = null, string $key = null, string $sort = null): array
+ {
+ $key = $key ?: $this->getPk();
+
+ if (is_null($sort)) {
+ $order = $this->getOptions('order');
+ if (!empty($order)) {
+ $sort = $order[$key] ?? 'desc';
+ } else {
+ $this->order($key, 'desc');
+ $sort = 'desc';
+ }
+ } else {
+ $this->order($key, $sort);
+ }
+
+ $result = $this->when($lastId, function ($query) use ($key, $sort, $lastId) {
+ $query->where($key, 'asc' == $sort ? '>' : '<', $lastId);
+ })->limit($limit)->select();
+
+ $last = $result->last();
+
+ $result->first();
+
+ return [
+ 'data' => $result,
+ 'lastId' => $last[$key],
+ ];
+ }
+
+ /**
+ * 查询缓存
+ * @access public
+ * @param mixed $key 缓存key
+ * @param integer|\DateTime $expire 缓存有效期
+ * @param string|array $tag 缓存标签
+ * @return $this
+ */
+ public function cache($key = true, $expire = null, $tag = null)
+ {
+ if (false === $key || !$this->getConnection()->getCache()) {
+ return $this;
+ }
+
+ if ($key instanceof \DateTimeInterface || $key instanceof \DateInterval || (is_int($key) && is_null($expire))) {
+ $expire = $key;
+ $key = true;
+ }
+
+ $this->options['cache'] = [$key, $expire, $tag];
+
+ return $this;
+ }
+
+ /**
+ * 指定查询lock
+ * @access public
+ * @param bool|string $lock 是否lock
+ * @return $this
+ */
+ public function lock($lock = false)
+ {
+ $this->options['lock'] = $lock;
+
+ if ($lock) {
+ $this->options['master'] = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定数据表别名
+ * @access public
+ * @param array|string $alias 数据表别名
+ * @return $this
+ */
+ public function alias($alias)
+ {
+ if (is_array($alias)) {
+ $this->options['alias'] = $alias;
+ } else {
+ $table = $this->getTable();
+
+ $this->options['alias'][$table] = $alias;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置从主服务器读取数据
+ * @access public
+ * @param bool $readMaster 是否从主服务器读取
+ * @return $this
+ */
+ public function master(bool $readMaster = true)
+ {
+ $this->options['master'] = $readMaster;
+ return $this;
+ }
+
+ /**
+ * 设置是否严格检查字段名
+ * @access public
+ * @param bool $strict 是否严格检查字段
+ * @return $this
+ */
+ public function strict(bool $strict = true)
+ {
+ $this->options['strict'] = $strict;
+ return $this;
+ }
+
+ /**
+ * 设置自增序列名
+ * @access public
+ * @param string $sequence 自增序列名
+ * @return $this
+ */
+ public function sequence(string $sequence = null)
+ {
+ $this->options['sequence'] = $sequence;
+ return $this;
+ }
+
+ /**
+ * 设置JSON字段信息
+ * @access public
+ * @param array $json JSON字段
+ * @param bool $assoc 是否取出数组
+ * @return $this
+ */
+ public function json(array $json = [], bool $assoc = false)
+ {
+ $this->options['json'] = $json;
+ $this->options['json_assoc'] = $assoc;
+ return $this;
+ }
+
+ /**
+ * 指定数据表主键
+ * @access public
+ * @param string|array $pk 主键
+ * @return $this
+ */
+ public function pk($pk)
+ {
+ $this->pk = $pk;
+ return $this;
+ }
+
+ /**
+ * 查询参数批量赋值
+ * @access protected
+ * @param array $options 表达式参数
+ * @return $this
+ */
+ protected function options(array $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * 获取当前的查询参数
+ * @access public
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getOptions(string $name = '')
+ {
+ if ('' === $name) {
+ return $this->options;
+ }
+
+ return $this->options[$name] ?? null;
+ }
+
+ /**
+ * 设置当前的查询参数
+ * @access public
+ * @param string $option 参数名
+ * @param mixed $value 参数值
+ * @return $this
+ */
+ public function setOption(string $option, $value)
+ {
+ $this->options[$option] = $value;
+ return $this;
+ }
+
+ /**
+ * 设置当前字段添加的表别名
+ * @access public
+ * @param string $via 临时表别名
+ * @return $this
+ */
+ public function via(string $via = '')
+ {
+ $this->options['via'] = $via;
+
+ return $this;
+ }
+
+ /**
+ * 保存记录 自动判断insert或者update
+ * @access public
+ * @param array $data 数据
+ * @param bool $forceInsert 是否强制insert
+ * @return integer
+ */
+ public function save(array $data = [], bool $forceInsert = false)
+ {
+ if ($forceInsert) {
+ return $this->insert($data);
+ }
+
+ $this->options['data'] = array_merge($this->options['data'] ?? [], $data);
+
+ if (!empty($this->options['where'])) {
+ $isUpdate = true;
+ } else {
+ $isUpdate = $this->parseUpdateData($this->options['data']);
+ }
+
+ return $isUpdate ? $this->update() : $this->insert();
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param array $data 数据
+ * @param boolean $getLastInsID 返回自增主键
+ * @return integer|string
+ */
+ public function insert(array $data = [], bool $getLastInsID = false)
+ {
+ if (!empty($data)) {
+ $this->options['data'] = $data;
+ }
+
+ return $this->connection->insert($this, $getLastInsID);
+ }
+
+ /**
+ * 插入记录并获取自增ID
+ * @access public
+ * @param array $data 数据
+ * @return integer|string
+ */
+ public function insertGetId(array $data)
+ {
+ return $this->insert($data, true);
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param array $dataSet 数据集
+ * @param integer $limit 每次写入数据限制
+ * @return integer
+ */
+ public function insertAll(array $dataSet = [], int $limit = 0): int
+ {
+ if (empty($dataSet)) {
+ $dataSet = $this->options['data'] ?? [];
+ }
+
+ if (empty($limit) && !empty($this->options['limit']) && is_numeric($this->options['limit'])) {
+ $limit = (int) $this->options['limit'];
+ }
+
+ return $this->connection->insertAll($this, $dataSet, $limit);
+ }
+
+ /**
+ * 通过Select方式插入记录
+ * @access public
+ * @param array $fields 要插入的数据表字段名
+ * @param string $table 要插入的数据表名
+ * @return integer
+ */
+ public function selectInsert(array $fields, string $table): int
+ {
+ return $this->connection->selectInsert($this, $fields, $table);
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param mixed $data 数据
+ * @return integer
+ * @throws Exception
+ */
+ public function update(array $data = []): int
+ {
+ if (!empty($data)) {
+ $this->options['data'] = array_merge($this->options['data'] ?? [], $data);
+ }
+
+ if (empty($this->options['where'])) {
+ $this->parseUpdateData($this->options['data']);
+ }
+
+ if (empty($this->options['where']) && $this->model) {
+ $this->where($this->model->getWhere());
+ }
+
+ if (empty($this->options['where'])) {
+ // 如果没有任何更新条件则不执行
+ throw new Exception('miss update condition');
+ }
+
+ return $this->connection->update($this);
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 表达式 true 表示强制删除
+ * @return int
+ * @throws Exception
+ */
+ public function delete($data = null): int
+ {
+ if (!is_null($data) && true !== $data) {
+ // AR模式分析主键条件
+ $this->parsePkWhere($data);
+ }
+
+ if (empty($this->options['where']) && $this->model) {
+ $this->where($this->model->getWhere());
+ }
+
+ if (true !== $data && empty($this->options['where'])) {
+ // 如果条件为空 不进行删除操作 除非设置 1=1
+ throw new Exception('delete without condition');
+ }
+
+ if (!empty($this->options['soft_delete'])) {
+ // 软删除
+ list($field, $condition) = $this->options['soft_delete'];
+ if ($condition) {
+ unset($this->options['soft_delete']);
+ $this->options['data'] = [$field => $condition];
+
+ return $this->connection->update($this);
+ }
+ }
+
+ $this->options['data'] = $data;
+
+ return $this->connection->delete($this);
+ }
+
+ /**
+ * 查找记录
+ * @access public
+ * @param mixed $data 数据
+ * @return Collection
+ * @throws Exception
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function select($data = null): Collection
+ {
+ if (!is_null($data)) {
+ // 主键条件分析
+ $this->parsePkWhere($data);
+ }
+
+ $resultSet = $this->connection->select($this);
+
+ // 返回结果处理
+ if (!empty($this->options['fail']) && count($resultSet) == 0) {
+ $this->throwNotFound();
+ }
+
+ // 数据列表读取后的处理
+ if (!empty($this->model)) {
+ // 生成模型对象
+ $resultSet = $this->resultSetToModelCollection($resultSet);
+ } else {
+ $this->resultSet($resultSet);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param mixed $data 查询数据
+ * @return array|Model|null
+ * @throws Exception
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function find($data = null)
+ {
+ if (!is_null($data)) {
+ // AR模式分析主键条件
+ $this->parsePkWhere($data);
+ }
+
+ if (empty($this->options['where']) && empty($this->options['order'])) {
+ $result = [];
+ } else {
+ $result = $this->connection->find($this);
+ }
+
+ // 数据处理
+ if (empty($result)) {
+ return $this->resultToEmpty();
+ }
+
+ if (!empty($this->model)) {
+ // 返回模型对象
+ $this->resultToModel($result, $this->options);
+ } else {
+ $this->result($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 分析表达式(可用于查询或者写入操作)
+ * @access public
+ * @return array
+ */
+ public function parseOptions(): array
+ {
+ $options = $this->getOptions();
+
+ // 获取数据表
+ if (empty($options['table'])) {
+ $options['table'] = $this->getTable();
+ }
+
+ if (!isset($options['where'])) {
+ $options['where'] = [];
+ } elseif (isset($options['view'])) {
+ // 视图查询条件处理
+ $this->parseView($options);
+ }
+
+ if (!isset($options['field'])) {
+ $options['field'] = '*';
+ }
+
+ foreach (['data', 'order', 'join', 'union'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ }
+
+ if (!isset($options['strict'])) {
+ $options['strict'] = $this->connection->getConfig('fields_strict');
+ }
+
+ foreach (['master', 'lock', 'fetch_sql', 'array', 'distinct', 'procedure'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = false;
+ }
+ }
+
+ foreach (['group', 'having', 'limit', 'force', 'comment', 'partition', 'duplicate', 'extra'] as $name) {
+ if (!isset($options[$name])) {
+ $options[$name] = '';
+ }
+ }
+
+ if (isset($options['page'])) {
+ // 根据页数计算limit
+ [$page, $listRows] = $options['page'];
+ $page = $page > 0 ? $page : 1;
+ $listRows = $listRows ?: (is_numeric($options['limit']) ? $options['limit'] : 20);
+ $offset = $listRows * ($page - 1);
+ $options['limit'] = $offset . ',' . $listRows;
+ }
+
+ $this->options = $options;
+
+ return $options;
+ }
+
+ /**
+ * 分析数据是否存在更新条件
+ * @access public
+ * @param array $data 数据
+ * @return bool
+ * @throws Exception
+ */
+ public function parseUpdateData(&$data): bool
+ {
+ $pk = $this->getPk();
+ $isUpdate = false;
+ // 如果存在主键数据 则自动作为更新条件
+ if (is_string($pk) && isset($data[$pk])) {
+ $this->where($pk, '=', $data[$pk]);
+ $this->options['key'] = $data[$pk];
+ unset($data[$pk]);
+ $isUpdate = true;
+ } elseif (is_array($pk)) {
+ foreach ($pk as $field) {
+ if (isset($data[$field])) {
+ $this->where($field, '=', $data[$field]);
+ $isUpdate = true;
+ } else {
+ // 如果缺少复合主键数据则不执行
+ throw new Exception('miss complex primary data');
+ }
+ unset($data[$field]);
+ }
+ }
+
+ return $isUpdate;
+ }
+
+ /**
+ * 把主键值转换为查询条件 支持复合主键
+ * @access public
+ * @param array|string $data 主键数据
+ * @return void
+ * @throws Exception
+ */
+ public function parsePkWhere($data): void
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk)) {
+ // 获取数据表
+ if (empty($this->options['table'])) {
+ $this->options['table'] = $this->getTable();
+ }
+
+ $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table'];
+
+ if (!empty($this->options['alias'][$table])) {
+ $alias = $this->options['alias'][$table];
+ }
+
+ $key = isset($alias) ? $alias . '.' . $pk : $pk;
+ // 根据主键查询
+ if (is_array($data)) {
+ $this->where($key, 'in', $data);
+ } else {
+ $this->where($key, '=', $data);
+ $this->options['key'] = $data;
+ }
+ }
+ }
+
+ /**
+ * 获取模型的更新条件
+ * @access protected
+ * @param array $options 查询参数
+ */
+ protected function getModelUpdateCondition(array $options)
+ {
+ return $options['where']['AND'] ?? null;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/Builder.php b/vendor/topthink/think-orm/src/db/Builder.php
new file mode 100644
index 0000000..3019b20
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Builder.php
@@ -0,0 +1,1303 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Closure;
+use PDO;
+use think\db\exception\DbException as Exception;
+
+/**
+ * Db Builder
+ */
+abstract class Builder
+{
+ /**
+ * Connection对象
+ * @var ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * 查询表达式映射
+ * @var array
+ */
+ protected $exp = ['NOTLIKE' => 'NOT LIKE', 'NOTIN' => 'NOT IN', 'NOTBETWEEN' => 'NOT BETWEEN', 'NOTEXISTS' => 'NOT EXISTS', 'NOTNULL' => 'NOT NULL', 'NOTBETWEEN TIME' => 'NOT BETWEEN TIME'];
+
+ /**
+ * 查询表达式解析
+ * @var array
+ */
+ protected $parser = [
+ 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='],
+ 'parseLike' => ['LIKE', 'NOT LIKE'],
+ 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'],
+ 'parseIn' => ['NOT IN', 'IN'],
+ 'parseExp' => ['EXP'],
+ 'parseNull' => ['NOT NULL', 'NULL'],
+ 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'],
+ 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'],
+ 'parseExists' => ['NOT EXISTS', 'EXISTS'],
+ 'parseColumn' => ['COLUMN'],
+ ];
+
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE%EXTRA% %TABLE% SET %SET%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * 架构函数
+ * @access public
+ * @param ConnectionInterface $connection 数据库连接对象实例
+ */
+ public function __construct(ConnectionInterface $connection)
+ {
+ $this->connection = $connection;
+ }
+
+ /**
+ * 获取当前的连接对象实例
+ * @access public
+ * @return ConnectionInterface
+ */
+ public function getConnection(): ConnectionInterface
+ {
+ return $this->connection;
+ }
+
+ /**
+ * 注册查询表达式解析
+ * @access public
+ * @param string $name 解析方法
+ * @param array $parser 匹配表达式数据
+ * @return $this
+ */
+ public function bindParser(string $name, array $parser)
+ {
+ $this->parser[$name] = $parser;
+ return $this;
+ }
+
+ /**
+ * 数据分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $data 数据
+ * @param array $fields 字段信息
+ * @param array $bind 参数绑定
+ * @return array
+ */
+ protected function parseData(Query $query, array $data = [], array $fields = [], array $bind = []): array
+ {
+ if (empty($data)) {
+ return [];
+ }
+
+ $options = $query->getOptions();
+
+ // 获取绑定信息
+ if (empty($bind)) {
+ $bind = $query->getFieldsBindType();
+ }
+
+ if (empty($fields)) {
+ if ('*' == $options['field']) {
+ $fields = array_keys($bind);
+ } else {
+ $fields = $options['field'];
+ }
+ }
+
+ $result = [];
+
+ foreach ($data as $key => $val) {
+ $item = $this->parseKey($query, $key, true);
+
+ if ($val instanceof Raw) {
+ $result[$item] = $this->parseRaw($query, $val);
+ continue;
+ } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $query->getFieldType($key))) {
+ $val = json_encode($val);
+ }
+
+ if (false !== strpos($key, '->')) {
+ [$key, $name] = explode('->', $key, 2);
+ $item = $this->parseKey($query, $key);
+ $result[$item] = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key . '->' . $name, $val, $bind) . ')';
+ } elseif (false === strpos($key, '.') && !in_array($key, $fields, true)) {
+ if ($options['strict']) {
+ throw new Exception('fields not exists:[' . $key . ']');
+ }
+ } elseif (is_null($val)) {
+ $result[$item] = 'NULL';
+ } elseif (is_array($val) && !empty($val) && is_string($val[0])) {
+ switch (strtoupper($val[0])) {
+ case 'INC':
+ $result[$item] = $item . ' + ' . floatval($val[1]);
+ break;
+ case 'DEC':
+ $result[$item] = $item . ' - ' . floatval($val[1]);
+ break;
+ }
+ } elseif (is_scalar($val)) {
+ // 过滤非标量数据
+ $result[$item] = $this->parseDataBind($query, $key, $val, $bind);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 数据绑定处理
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key 字段名
+ * @param mixed $data 数据
+ * @param array $bind 绑定数据
+ * @return string
+ */
+ protected function parseDataBind(Query $query, string $key, $data, array $bind = []): string
+ {
+ if ($data instanceof Raw) {
+ return $this->parseRaw($query, $data);
+ }
+
+ $name = $query->bindValue($data, $bind[$key] ?? PDO::PARAM_STR);
+
+ return ':' . $name;
+ }
+
+ /**
+ * 字段名分析
+ * @access public
+ * @param Query $query 查询对象
+ * @param mixed $key 字段名
+ * @param bool $strict 严格检测
+ * @return string
+ */
+ public function parseKey(Query $query, $key, bool $strict = false): string
+ {
+ return $key;
+ }
+
+ /**
+ * 查询额外参数分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $extra 额外参数
+ * @return string
+ */
+ protected function parseExtra(Query $query, string $extra): string
+ {
+ return preg_match('/^[\w]+$/i', $extra) ? ' ' . strtoupper($extra) : '';
+ }
+
+ /**
+ * field分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $fields 字段名
+ * @return string
+ */
+ protected function parseField(Query $query, $fields): string
+ {
+ if (is_array($fields)) {
+ // 支持 'field1'=>'field2' 这样的字段别名定义
+ $array = [];
+
+ foreach ($fields as $key => $field) {
+ if ($field instanceof Raw) {
+ $array[] = $this->parseRaw($query, $field);
+ } elseif (!is_numeric($key)) {
+ $array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
+ } else {
+ $array[] = $this->parseKey($query, $field);
+ }
+ }
+
+ $fieldsStr = implode(',', $array);
+ } else {
+ $fieldsStr = '*';
+ }
+
+ return $fieldsStr;
+ }
+
+ /**
+ * table分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $tables 表名
+ * @return string
+ */
+ protected function parseTable(Query $query, $tables): string
+ {
+ $item = [];
+ $options = $query->getOptions();
+
+ foreach ((array) $tables as $key => $table) {
+ if ($table instanceof Raw) {
+ $item[] = $this->parseRaw($query, $table);
+ } elseif (!is_numeric($key)) {
+ $item[] = $this->parseKey($query, $key) . ' ' . $this->parseKey($query, $table);
+ } elseif (isset($options['alias'][$table])) {
+ $item[] = $this->parseKey($query, $table) . ' ' . $this->parseKey($query, $options['alias'][$table]);
+ } else {
+ $item[] = $this->parseKey($query, $table);
+ }
+ }
+
+ return implode(',', $item);
+ }
+
+ /**
+ * where分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $where 查询条件
+ * @return string
+ */
+ protected function parseWhere(Query $query, array $where): string
+ {
+ $options = $query->getOptions();
+ $whereStr = $this->buildWhere($query, $where);
+
+ if (!empty($options['soft_delete'])) {
+ // 附加软删除条件
+ [$field, $condition] = $options['soft_delete'];
+
+ $binds = $query->getFieldsBindType();
+ $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
+ $whereStr = $whereStr . $this->parseWhereItem($query, $field, $condition, $binds);
+ }
+
+ return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
+ }
+
+ /**
+ * 生成查询条件SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param mixed $where 查询条件
+ * @return string
+ */
+ public function buildWhere(Query $query, array $where): string
+ {
+ if (empty($where)) {
+ $where = [];
+ }
+
+ $whereStr = '';
+
+ $binds = $query->getFieldsBindType();
+
+ foreach ($where as $logic => $val) {
+ $str = $this->parseWhereLogic($query, $logic, $val, $binds);
+
+ $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($logic) + 1) : implode(' ', $str);
+ }
+
+ return $whereStr;
+ }
+
+ /**
+ * 不同字段使用相同查询条件(AND)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $logic Logic
+ * @param array $val 查询条件
+ * @param array $binds 参数绑定
+ * @return array
+ */
+ protected function parseWhereLogic(Query $query, string $logic, array $val, array $binds = []): array
+ {
+ $where = [];
+ foreach ($val as $value) {
+ if ($value instanceof Raw) {
+ $where[] = ' ' . $logic . ' ( ' . $this->parseRaw($query, $value) . ' )';
+ continue;
+ }
+
+ if (is_array($value)) {
+ if (key($value) !== 0) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
+ $field = array_shift($value);
+ } elseif (true === $value) {
+ $where[] = ' ' . $logic . ' 1 ';
+ continue;
+ } elseif (!($value instanceof Closure)) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
+
+ if ($value instanceof Closure) {
+ // 使用闭包查询
+ $whereClosureStr = $this->parseClosureWhere($query, $value, $logic);
+ if ($whereClosureStr) {
+ $where[] = $whereClosureStr;
+ }
+ } elseif (is_array($field)) {
+ $where[] = $this->parseMultiWhereField($query, $value, $field, $logic, $binds);
+ } elseif ($field instanceof Raw) {
+ $where[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $binds);
+ } elseif (strpos($field, '|')) {
+ $where[] = $this->parseFieldsOr($query, $value, $field, $logic, $binds);
+ } elseif (strpos($field, '&')) {
+ $where[] = $this->parseFieldsAnd($query, $value, $field, $logic, $binds);
+ } else {
+ // 对字段使用表达式查询
+ $field = is_string($field) ? $field : '';
+ $where[] = ' ' . $logic . ' ' . $this->parseWhereItem($query, $field, $value, $binds);
+ }
+ }
+
+ return $where;
+ }
+
+ /**
+ * 不同字段使用相同查询条件(AND)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param string $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseFieldsAnd(Query $query, $value, string $field, string $logic, array $binds): string
+ {
+ $item = [];
+
+ foreach (explode('&', $field) as $k) {
+ $item[] = $this->parseWhereItem($query, $k, $value, $binds);
+ }
+
+ return ' ' . $logic . ' ( ' . implode(' AND ', $item) . ' )';
+ }
+
+ /**
+ * 不同字段使用相同查询条件(OR)
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param string $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseFieldsOr(Query $query, $value, string $field, string $logic, array $binds): string
+ {
+ $item = [];
+
+ foreach (explode('|', $field) as $k) {
+ $item[] = $this->parseWhereItem($query, $k, $value, $binds);
+ }
+
+ return ' ' . $logic . ' ( ' . implode(' OR ', $item) . ' )';
+ }
+
+ /**
+ * 闭包查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param Closure $value 查询条件
+ * @param string $logic Logic
+ * @return string
+ */
+ protected function parseClosureWhere(Query $query, Closure $value, string $logic): string
+ {
+ $newQuery = $query->newQuery();
+ $value($newQuery);
+ $whereClosure = $this->buildWhere($newQuery, $newQuery->getOptions('where') ?: []);
+
+ if (!empty($whereClosure)) {
+ $query->bind($newQuery->getBind(false));
+ $where = ' ' . $logic . ' ( ' . $whereClosure . ' )';
+ }
+
+ return $where ?? '';
+ }
+
+ /**
+ * 复合条件查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value 查询条件
+ * @param mixed $field 查询字段
+ * @param string $logic Logic
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseMultiWhereField(Query $query, $value, $field, string $logic, array $binds): string
+ {
+ array_unshift($value, $field);
+
+ $where = [];
+ foreach ($value as $item) {
+ $where[] = $this->parseWhereItem($query, array_shift($item), $item, $binds);
+ }
+
+ return ' ' . $logic . ' ( ' . implode(' AND ', $where) . ' )';
+ }
+
+ /**
+ * where子单元分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $field 查询字段
+ * @param array $val 查询条件
+ * @param array $binds 参数绑定
+ * @return string
+ */
+ protected function parseWhereItem(Query $query, $field, array $val, array $binds = []): string
+ {
+ // 字段分析
+ $key = $field ? $this->parseKey($query, $field, true) : '';
+
+ list($exp, $value) = $val;
+
+ // 检测操作符
+ if (!is_string($exp)) {
+ throw new Exception('where express error:' . var_export($exp, true));
+ }
+
+ $exp = strtoupper($exp);
+ if (isset($this->exp[$exp])) {
+ $exp = $this->exp[$exp];
+ }
+
+ if (is_string($field) && 'LIKE' != $exp) {
+ $bindType = $binds[$field] ?? PDO::PARAM_STR;
+ } else {
+ $bindType = PDO::PARAM_STR;
+ }
+
+ if ($value instanceof Raw) {
+
+ } elseif (is_object($value) && method_exists($value, '__toString')) {
+ // 对象数据写入
+ $value = $value->__toString();
+ }
+
+ if (is_scalar($value) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) {
+ if (is_string($value) && 0 === strpos($value, ':') && $query->isBind(substr($value, 1))) {
+ } else {
+ $name = $query->bindValue($value, $bindType);
+ $value = ':' . $name;
+ }
+ }
+
+ // 解析查询表达式
+ foreach ($this->parser as $fun => $parse) {
+ if (in_array($exp, $parse)) {
+ return $this->$fun($query, $key, $exp, $value, $field, $bindType, $val[2] ?? 'AND');
+ }
+ }
+
+ throw new Exception('where express error:' . $exp);
+ }
+
+ /**
+ * 模糊查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
+ * @param string $logic
+ * @return string
+ */
+ protected function parseLike(Query $query, string $key, string $exp, $value, $field, int $bindType, string $logic): string
+ {
+ // 模糊匹配
+ if (is_array($value)) {
+ $array = [];
+ foreach ($value as $item) {
+ $name = $query->bindValue($item, PDO::PARAM_STR);
+ $array[] = $key . ' ' . $exp . ' :' . $name;
+ }
+
+ $whereStr = '(' . implode(' ' . strtoupper($logic) . ' ', $array) . ')';
+ } else {
+ $whereStr = $key . ' ' . $exp . ' ' . $value;
+ }
+
+ return $whereStr;
+ }
+
+ /**
+ * 表达式查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseExp(Query $query, string $key, string $exp, Raw $value, string $field, int $bindType): string
+ {
+ // 表达式查询
+ return '( ' . $key . ' ' . $this->parseRaw($query, $value) . ' )';
+ }
+
+ /**
+ * 表达式查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param array $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseColumn(Query $query, string $key, $exp, array $value, string $field, int $bindType): string
+ {
+ // 字段比较查询
+ [$op, $field] = $value;
+
+ if (!in_array(trim($op), ['=', '<>', '>', '>=', '<', '<='])) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
+
+ return '( ' . $key . ' ' . $op . ' ' . $this->parseKey($query, $field, true) . ' )';
+ }
+
+ /**
+ * Null查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseNull(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ // NULL 查询
+ return $key . ' IS ' . $exp;
+ }
+
+ /**
+ * 范围查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseBetween(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ // BETWEEN 查询
+ $data = is_array($value) ? $value : explode(',', $value);
+
+ $min = $query->bindValue($data[0], $bindType);
+ $max = $query->bindValue($data[1], $bindType);
+
+ return $key . ' ' . $exp . ' :' . $min . ' AND :' . $max . ' ';
+ }
+
+ /**
+ * Exists查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseExists(Query $query, string $key, string $exp, $value, string $field, int $bindType): string
+ {
+ // EXISTS 查询
+ if ($value instanceof Closure) {
+ $value = $this->parseClosure($query, $value, false);
+ } elseif ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ } else {
+ throw new Exception('where express error:' . $value);
+ }
+
+ return $exp . ' ( ' . $value . ' )';
+ }
+
+ /**
+ * 时间比较查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseTime(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ return $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($query, $value, $field, $bindType);
+ }
+
+ /**
+ * 大小比较查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseCompare(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ if (is_array($value)) {
+ throw new Exception('where express error:' . $exp . var_export($value, true));
+ }
+
+ // 比较运算
+ if ($value instanceof Closure) {
+ $value = $this->parseClosure($query, $value);
+ }
+
+ if ('=' == $exp && is_null($value)) {
+ return $key . ' IS NULL';
+ }
+
+ return $key . ' ' . $exp . ' ' . $value;
+ }
+
+ /**
+ * 时间范围查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseBetweenTime(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ if (is_string($value)) {
+ $value = explode(',', $value);
+ }
+
+ return $key . ' ' . substr($exp, 0, -4)
+ . $this->parseDateTime($query, $value[0], $field, $bindType)
+ . ' AND '
+ . $this->parseDateTime($query, $value[1], $field, $bindType);
+
+ }
+
+ /**
+ * IN查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseIn(Query $query, string $key, string $exp, $value, $field, int $bindType): string
+ {
+ // IN 查询
+ if ($value instanceof Closure) {
+ $value = $this->parseClosure($query, $value, false);
+ } elseif ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ } else {
+ $value = array_unique(is_array($value) ? $value : explode(',', $value));
+ $array = [];
+
+ foreach ($value as $v) {
+ $name = $query->bindValue($v, $bindType);
+ $array[] = ':' . $name;
+ }
+
+ if (count($array) == 1) {
+ return $key . ('IN' == $exp ? ' = ' : ' <> ') . $array[0];
+ } else {
+ $zone = implode(',', $array);
+ $value = empty($zone) ? "''" : $zone;
+ }
+ }
+
+ return $key . ' ' . $exp . ' (' . $value . ')';
+ }
+
+ /**
+ * 闭包子查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param \Closure $call
+ * @param bool $show
+ * @return string
+ */
+ protected function parseClosure(Query $query, Closure $call, bool $show = true): string
+ {
+ $newQuery = $query->newQuery()->removeOption();
+ $call($newQuery);
+
+ return $newQuery->buildSql($show);
+ }
+
+ /**
+ * 日期时间条件解析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value
+ * @param string $key
+ * @param integer $bindType
+ * @return string
+ */
+ protected function parseDateTime(Query $query, $value, string $key, int $bindType): string
+ {
+ $options = $query->getOptions();
+
+ // 获取时间字段类型
+ if (strpos($key, '.')) {
+ [$table, $key] = explode('.', $key);
+
+ if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) {
+ $table = $pos;
+ }
+ } else {
+ $table = $options['table'];
+ }
+
+ $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);
+ }
+ }
+ }
+
+ $name = $query->bindValue($value, $bindType);
+
+ return ':' . $name;
+ }
+
+ /**
+ * limit分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $limit
+ * @return string
+ */
+ protected function parseLimit(Query $query, string $limit): string
+ {
+ return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : '';
+ }
+
+ /**
+ * join分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $join
+ * @return string
+ */
+ protected function parseJoin(Query $query, array $join): string
+ {
+ $joinStr = '';
+
+ foreach ($join as $item) {
+ [$table, $type, $on] = $item;
+
+ if (strpos($on, '=')) {
+ [$val1, $val2] = explode('=', $on, 2);
+
+ $condition = $this->parseKey($query, $val1) . '=' . $this->parseKey($query, $val2);
+ } else {
+ $condition = $on;
+ }
+
+ $table = $this->parseTable($query, $table);
+
+ $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . $condition;
+ }
+
+ return $joinStr;
+ }
+
+ /**
+ * order分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $order
+ * @return string
+ */
+ protected function parseOrder(Query $query, array $order): string
+ {
+ $array = [];
+ foreach ($order as $key => $val) {
+ if ($val instanceof Raw) {
+ $array[] = $this->parseRaw($query, $val);
+ } elseif (is_array($val) && preg_match('/^[\w\.]+$/', $key)) {
+ $array[] = $this->parseOrderField($query, $key, $val);
+ } elseif ('[rand]' == $val) {
+ $array[] = $this->parseRand($query);
+ } elseif (is_string($val)) {
+ if (is_numeric($key)) {
+ list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
+ } else {
+ $sort = $val;
+ }
+
+ if (preg_match('/^[\w\.]+$/', $key)) {
+ $sort = strtoupper($sort);
+ $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
+ $array[] = $this->parseKey($query, $key, true) . $sort;
+ } else {
+ throw new Exception('order express error:' . $key);
+ }
+ }
+ }
+
+ return empty($array) ? '' : ' ORDER BY ' . implode(',', $array);
+ }
+
+ /**
+ * 分析Raw对象
+ * @access protected
+ * @param Query $query 查询对象
+ * @param Raw $raw Raw对象
+ * @return string
+ */
+ protected function parseRaw(Query $query, Raw $raw): string
+ {
+ $sql = $raw->getValue();
+ $bind = $raw->getBind();
+
+ if ($bind) {
+ $query->bindParams($sql, $bind);
+ }
+
+ return $sql;
+ }
+
+ /**
+ * 随机排序
+ * @access protected
+ * @param Query $query 查询对象
+ * @return string
+ */
+ protected function parseRand(Query $query): string
+ {
+ return '';
+ }
+
+ /**
+ * orderField分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param array $val
+ * @return string
+ */
+ protected function parseOrderField(Query $query, string $key, array $val): string
+ {
+ if (isset($val['sort'])) {
+ $sort = $val['sort'];
+ unset($val['sort']);
+ } else {
+ $sort = '';
+ }
+
+ $sort = strtoupper($sort);
+ $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
+ $bind = $query->getFieldsBindType();
+
+ foreach ($val as $item) {
+ $val[] = $this->parseDataBind($query, $key, $item, $bind);
+ }
+
+ return 'field(' . $this->parseKey($query, $key, true) . ',' . implode(',', $val) . ')' . $sort;
+ }
+
+ /**
+ * group分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $group
+ * @return string
+ */
+ protected function parseGroup(Query $query, $group): string
+ {
+ if (empty($group)) {
+ return '';
+ }
+
+ if (is_string($group)) {
+ $group = explode(',', $group);
+ }
+
+ $val = [];
+ foreach ($group as $key) {
+ $val[] = $this->parseKey($query, $key);
+ }
+
+ return ' GROUP BY ' . implode(',', $val);
+ }
+
+ /**
+ * having分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $having
+ * @return string
+ */
+ protected function parseHaving(Query $query, string $having): string
+ {
+ return !empty($having) ? ' HAVING ' . $having : '';
+ }
+
+ /**
+ * comment分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $comment
+ * @return string
+ */
+ protected function parseComment(Query $query, string $comment): string
+ {
+ if (false !== strpos($comment, '*/')) {
+ $comment = strstr($comment, '*/', true);
+ }
+
+ return !empty($comment) ? ' /* ' . $comment . ' */' : '';
+ }
+
+ /**
+ * distinct分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $distinct
+ * @return string
+ */
+ protected function parseDistinct(Query $query, bool $distinct): string
+ {
+ return !empty($distinct) ? ' DISTINCT ' : '';
+ }
+
+ /**
+ * union分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $union
+ * @return string
+ */
+ protected function parseUnion(Query $query, array $union): string
+ {
+ if (empty($union)) {
+ return '';
+ }
+
+ $type = $union['type'];
+ unset($union['type']);
+
+ foreach ($union as $u) {
+ if ($u instanceof Closure) {
+ $sql[] = $type . ' ' . $this->parseClosure($query, $u);
+ } elseif (is_string($u)) {
+ $sql[] = $type . ' ( ' . $u . ' )';
+ }
+ }
+
+ return ' ' . implode(' ', $sql);
+ }
+
+ /**
+ * index分析,可在操作链中指定需要强制使用的索引
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $index
+ * @return string
+ */
+ protected function parseForce(Query $query, $index): string
+ {
+ if (empty($index)) {
+ return '';
+ }
+
+ if (is_array($index)) {
+ $index = join(',', $index);
+ }
+
+ return sprintf(" FORCE INDEX ( %s ) ", $index);
+ }
+
+ /**
+ * 设置锁机制
+ * @access protected
+ * @param Query $query 查询对象
+ * @param bool|string $lock
+ * @return string
+ */
+ protected function parseLock(Query $query, $lock = false): string
+ {
+ if (is_bool($lock)) {
+ return $lock ? ' FOR UPDATE ' : '';
+ }
+
+ if (is_string($lock) && !empty($lock)) {
+ return ' ' . trim($lock) . ' ';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * 生成查询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%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $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 '';
+ }
+
+ $fields = array_keys($data);
+ $values = array_values($data);
+
+ return str_replace(
+ ['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
+ [
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
+ $this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
+ implode(' , ', $fields),
+ implode(' , ', $values),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->insertSql);
+ }
+
+ /**
+ * 生成insertall SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $dataSet 数据集
+ * @return string
+ */
+ public function insertAll(Query $query, array $dataSet): string
+ {
+ $options = $query->getOptions();
+
+ // 获取绑定信息
+ $bind = $query->getFieldsBindType();
+
+ // 获取合法的字段
+ if ('*' == $options['field']) {
+ $allowFields = array_keys($bind);
+ } else {
+ $allowFields = $options['field'];
+ }
+
+ $fields = [];
+ $values = [];
+
+ foreach ($dataSet as $k => $data) {
+ $data = $this->parseData($query, $data, $allowFields, $bind);
+
+ $values[] = 'SELECT ' . implode(',', array_values($data));
+
+ if (!isset($insertFields)) {
+ $insertFields = array_keys($data);
+ }
+ }
+
+ foreach ($insertFields as $field) {
+ $fields[] = $this->parseKey($query, $field);
+ }
+
+ return str_replace(
+ ['%INSERT%', '%TABLE%', '%EXTRA%', '%FIELD%', '%DATA%', '%COMMENT%'],
+ [
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
+ $this->parseTable($query, $options['table']),
+ $this->parseExtra($query, $options['extra']),
+ implode(' , ', $fields),
+ implode(' UNION ALL ', $values),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->insertAllSql);
+ }
+
+ /**
+ * 生成slect insert SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $fields 数据
+ * @param string $table 数据表
+ * @return string
+ */
+ public function selectInsert(Query $query, array $fields, string $table): string
+ {
+ foreach ($fields as &$field) {
+ $field = $this->parseKey($query, $field, true);
+ }
+
+ return 'INSERT INTO ' . $this->parseTable($query, $table) . ' (' . implode(',', $fields) . ') ' . $this->select($query);
+ }
+
+ /**
+ * 生成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[] = $key . ' = ' . $val;
+ }
+
+ return str_replace(
+ ['%TABLE%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $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%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $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);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/CacheItem.php b/vendor/topthink/think-orm/src/db/CacheItem.php
new file mode 100644
index 0000000..2ea6d1c
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/CacheItem.php
@@ -0,0 +1,209 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use DateInterval;
+use DateTime;
+use DateTimeInterface;
+use think\db\exception\InvalidArgumentException;
+
+/**
+ * CacheItem实现类
+ */
+class CacheItem
+{
+ /**
+ * 缓存Key
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * 缓存内容
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * 过期时间
+ * @var int|DateTimeInterface
+ */
+ protected $expire;
+
+ /**
+ * 缓存tag
+ * @var string
+ */
+ protected $tag;
+
+ /**
+ * 缓存是否命中
+ * @var bool
+ */
+ protected $isHit = false;
+
+ public function __construct(string $key = null)
+ {
+ $this->key = $key;
+ }
+
+ /**
+ * 为此缓存项设置「键」
+ * @access public
+ * @param string $key
+ * @return $this
+ */
+ public function setKey(string $key)
+ {
+ $this->key = $key;
+ return $this;
+ }
+
+ /**
+ * 返回当前缓存项的「键」
+ * @access public
+ * @return string
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * 返回当前缓存项的有效期
+ * @access public
+ * @return DateTimeInterface|int|null
+ */
+ public function getExpire()
+ {
+ if ($this->expire instanceof DateTimeInterface) {
+ return $this->expire;
+ }
+
+ return $this->expire ? $this->expire - time() : null;
+ }
+
+ /**
+ * 获取缓存Tag
+ * @access public
+ * @return string|array
+ */
+ public function getTag()
+ {
+ return $this->tag;
+ }
+
+ /**
+ * 凭借此缓存项的「键」从缓存系统里面取出缓存项
+ * @access public
+ * @return mixed
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * 确认缓存项的检查是否命中
+ * @access public
+ * @return bool
+ */
+ public function isHit(): bool
+ {
+ return $this->isHit;
+ }
+
+ /**
+ * 为此缓存项设置「值」
+ * @access public
+ * @param mixed $value
+ * @return $this
+ */
+ public function set($value)
+ {
+ $this->value = $value;
+ $this->isHit = true;
+ return $this;
+ }
+
+ /**
+ * 为此缓存项设置所属标签
+ * @access public
+ * @param string|array $tag
+ * @return $this
+ */
+ public function tag($tag = null)
+ {
+ $this->tag = $tag;
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的有效期
+ * @access public
+ * @param mixed $expire
+ * @return $this
+ */
+ public function expire($expire)
+ {
+ if (is_null($expire)) {
+ $this->expire = null;
+ } elseif (is_numeric($expire) || $expire instanceof DateInterval) {
+ $this->expiresAfter($expire);
+ } elseif ($expire instanceof DateTimeInterface) {
+ $this->expire = $expire;
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的准确过期时间点
+ * @access public
+ * @param DateTimeInterface $expiration
+ * @return $this
+ */
+ public function expiresAt($expiration)
+ {
+ if ($expiration instanceof DateTimeInterface) {
+ $this->expire = $expiration;
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置缓存项的过期时间
+ * @access public
+ * @param int|DateInterval $timeInterval
+ * @return $this
+ * @throws InvalidArgumentException
+ */
+ public function expiresAfter($timeInterval)
+ {
+ if ($timeInterval instanceof DateInterval) {
+ $this->expire = (int) DateTime::createFromFormat('U', (string) time())->add($timeInterval)->format('U');
+ } elseif (is_numeric($timeInterval)) {
+ $this->expire = $timeInterval + time();
+ } else {
+ throw new InvalidArgumentException('not support datetime');
+ }
+
+ return $this;
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/Connection.php b/vendor/topthink/think-orm/src/db/Connection.php
new file mode 100644
index 0000000..feee038
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Connection.php
@@ -0,0 +1,332 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Psr\SimpleCache\CacheInterface;
+use think\DbManager;
+
+/**
+ * 数据库连接基础类
+ */
+abstract class Connection implements ConnectionInterface
+{
+
+ /**
+ * 当前SQL指令
+ * @var string
+ */
+ protected $queryStr = '';
+
+ /**
+ * 返回或者影响记录数
+ * @var int
+ */
+ protected $numRows = 0;
+
+ /**
+ * 事务指令数
+ * @var int
+ */
+ protected $transTimes = 0;
+
+ /**
+ * 错误信息
+ * @var string
+ */
+ protected $error = '';
+
+ /**
+ * 数据库连接ID 支持多个连接
+ * @var array
+ */
+ protected $links = [];
+
+ /**
+ * 当前连接ID
+ * @var object
+ */
+ protected $linkID;
+
+ /**
+ * 当前读连接ID
+ * @var object
+ */
+ protected $linkRead;
+
+ /**
+ * 当前写连接ID
+ * @var object
+ */
+ protected $linkWrite;
+
+ /**
+ * 数据表信息
+ * @var array
+ */
+ protected $info = [];
+
+ /**
+ * 查询开始时间
+ * @var float
+ */
+ protected $queryStartTime;
+
+ /**
+ * Builder对象
+ * @var Builder
+ */
+ protected $builder;
+
+ /**
+ * Db对象
+ * @var DbManager
+ */
+ protected $db;
+
+ /**
+ * 是否读取主库
+ * @var bool
+ */
+ protected $readMaster = false;
+
+ /**
+ * 数据库连接参数配置
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * 缓存对象
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * 架构函数 读取数据库配置信息
+ * @access public
+ * @param array $config 数据库配置数组
+ */
+ public function __construct(array $config = [])
+ {
+ if (!empty($config)) {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ // 创建Builder对象
+ $class = $this->getBuilderClass();
+
+ $this->builder = new $class($this);
+ }
+
+ /**
+ * 获取当前的builder实例对象
+ * @access public
+ * @return Builder
+ */
+ public function getBuilder()
+ {
+ return $this->builder;
+ }
+
+
+ /**
+ * 创建查询对象
+ */
+ public function newQuery()
+ {
+ $class = $this->getQueryClass();
+
+ /** @var BaseQuery $query */
+ $query = new $class($this);
+
+ $timeRule = $this->db->getConfig('time_query_rule');
+ if (!empty($timeRule)) {
+ $query->timeRule($timeRule);
+ }
+
+ return $query;
+ }
+
+ /**
+ * 指定表名开始查询
+ * @param $table
+ * @return BaseQuery
+ */
+ public function table($table)
+ {
+ return $this->newQuery()->table($table);
+ }
+
+ /**
+ * 指定表名开始查询(不带前缀)
+ * @param $name
+ * @return BaseQuery
+ */
+ public function name($name)
+ {
+ return $this->newQuery()->name($name);
+ }
+
+ /**
+ * 设置当前的数据库Db对象
+ * @access public
+ * @param DbManager $db
+ * @return void
+ */
+ public function setDb(DbManager $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * 设置当前的缓存对象
+ * @access public
+ * @param CacheInterface $cache
+ * @return void
+ */
+ public function setCache(CacheInterface $cache)
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * 获取当前的缓存对象
+ * @access public
+ * @return CacheInterface|null
+ */
+ public function getCache()
+ {
+ return $this->cache;
+ }
+
+ /**
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $config 配置名称
+ * @return mixed
+ */
+ public function getConfig(string $config = '')
+ {
+ if ('' === $config) {
+ return $this->config;
+ }
+
+ return $this->config[$config] ?? null;
+ }
+
+ /**
+ * 数据库SQL监控
+ * @access protected
+ * @param string $sql 执行的SQL语句 留空自动获取
+ * @param bool $master 主从标记
+ * @return void
+ */
+ protected function trigger(string $sql = '', bool $master = false): void
+ {
+ $listen = $this->db->getListen();
+
+ if (!empty($listen)) {
+ $runtime = number_format((microtime(true) - $this->queryStartTime), 6);
+ $sql = $sql ?: $this->getLastsql();
+
+ if (empty($this->config['deploy'])) {
+ $master = null;
+ }
+
+ foreach ($listen as $callback) {
+ if (is_callable($callback)) {
+ $callback($sql, $runtime, $master);
+ }
+ }
+ }
+ }
+
+ /**
+ * 缓存数据
+ * @access protected
+ * @param CacheItem $cacheItem 缓存Item
+ */
+ protected function cacheData(CacheItem $cacheItem)
+ {
+ if ($cacheItem->getTag() && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($cacheItem->getTag())->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
+ } else {
+ $this->cache->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
+ }
+ }
+
+ /**
+ * 分析缓存Key
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param string $method 查询方法
+ * @return string
+ */
+ protected function getCacheKey(BaseQuery $query, string $method = ''): string
+ {
+ if (!empty($query->getOptions('key')) && empty($method)) {
+ $key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
+ } else {
+ $key = $query->getQueryGuid();
+ }
+
+ return $key;
+ }
+
+ /**
+ * 分析缓存
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param array $cache 缓存信息
+ * @param string $method 查询方法
+ * @return CacheItem
+ */
+ protected function parseCache(BaseQuery $query, array $cache, string $method = ''): CacheItem
+ {
+ [$key, $expire, $tag] = $cache;
+
+ if ($key instanceof CacheItem) {
+ $cacheItem = $key;
+ } else {
+ if (true === $key) {
+ $key = $this->getCacheKey($query, $method);
+ }
+
+ $cacheItem = new CacheItem($key);
+ $cacheItem->expire($expire);
+ $cacheItem->tag($tag);
+ }
+
+ return $cacheItem;
+ }
+
+ /**
+ * 获取返回或者影响的记录数
+ * @access public
+ * @return integer
+ */
+ public function getNumRows(): int
+ {
+ return $this->numRows;
+ }
+
+ /**
+ * 析构方法
+ * @access public
+ */
+ public function __destruct()
+ {
+ // 关闭连接
+ $this->close();
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/ConnectionInterface.php b/vendor/topthink/think-orm/src/db/ConnectionInterface.php
new file mode 100644
index 0000000..b6280f9
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/ConnectionInterface.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Psr\SimpleCache\CacheInterface;
+use think\DbManager;
+
+/**
+ * Connection interface
+ */
+interface ConnectionInterface
+{
+ /**
+ * 获取当前连接器类对应的Query类
+ * @access public
+ * @return string
+ */
+ public function getQueryClass(): string;
+
+ /**
+ * 指定表名开始查询
+ * @param $table
+ * @return BaseQuery
+ */
+ public function table($table);
+
+ /**
+ * 指定表名开始查询(不带前缀)
+ * @param $name
+ * @return BaseQuery
+ */
+ public function name($name);
+
+ /**
+ * 连接数据库方法
+ * @access public
+ * @param array $config 接参数
+ * @param integer $linkNum 连接序号
+ * @return mixed
+ */
+ public function connect(array $config = [], $linkNum = 0);
+
+ /**
+ * 设置当前的数据库Db对象
+ * @access public
+ * @param DbManager $db
+ * @return void
+ */
+ public function setDb(DbManager $db);
+
+ /**
+ * 设置当前的缓存对象
+ * @access public
+ * @param CacheInterface $cache
+ * @return void
+ */
+ public function setCache(CacheInterface $cache);
+
+ /**
+ * 获取数据库的配置参数
+ * @access public
+ * @param string $config 配置名称
+ * @return mixed
+ */
+ public function getConfig(string $config = '');
+
+ /**
+ * 关闭数据库(或者重新连接)
+ * @access public
+ * @return $this
+ */
+ public function close();
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ */
+ public function find(BaseQuery $query): array;
+
+ /**
+ * 查找记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ */
+ public function select(BaseQuery $query): array;
+
+ /**
+ * 插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
+ */
+ public function insert(BaseQuery $query, bool $getLastInsID = false);
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param mixed $dataSet 数据集
+ * @return integer
+ */
+ public function insertAll(BaseQuery $query, array $dataSet = []): int;
+
+ /**
+ * 更新记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return integer
+ */
+ public function update(BaseQuery $query): int;
+
+ /**
+ * 删除记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ */
+ public function delete(BaseQuery $query): int;
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function value(BaseQuery $query, string $field, $default = null);
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, string $column, string $key = ''): array;
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ */
+ public function transaction(callable $callback);
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ */
+ public function startTrans();
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ */
+ public function commit();
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ */
+ public function rollback();
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string;
+
+}
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..b07f698
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Fetch.php
@@ -0,0 +1,494 @@
+
+// +----------------------------------------------------------------------
+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
+ $bind = $this->query->getBind();
+ $subSql = $this->query->options($options)->field('count(' . $field . ') AS think_count')->bind($bind)->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..7429142
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Mongo.php
@@ -0,0 +1,712 @@
+
+// +----------------------------------------------------------------------
+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'] 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['projection'])) {
+ $options['projection'] = [];
+ }
+
+ 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..8ae7b43
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/PDOConnection.php
@@ -0,0 +1,1746 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Closure;
+use PDO;
+use PDOStatement;
+use think\db\exception\BindParamException;
+use think\db\exception\DbException;
+use think\db\exception\PDOException;
+
+/**
+ * 数据库连接基础类
+ */
+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' => [],
+ ];
+
+ /**
+ * 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',
+ ];
+
+ /**
+ * 绑定参数
+ * @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);
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName 数据表名称
+ * @return array
+ */
+ abstract public function getFields(string $tableName);
+
+ /**
+ * 取得数据库的表信息
+ * @access public
+ * @param string $dbName 数据库名称
+ * @return array
+ */
+ abstract public function getTables(string $dbName);
+
+ /**
+ * 对返数据表字段信息进行大小写转换出来
+ * @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 === strpos($type, 'set') || 0 === strpos($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 === strpos($type, 'timestamp')) {
+ $result = 'timestamp';
+ } elseif (0 === strpos($type, 'datetime')) {
+ $result = 'datetime';
+ } elseif (0 === strpos($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 \think\Model $model 模型对象实例
+ * @param array $condition 查询条件
+ * @return \Generator
+ */
+ 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 BindParamException
+ * @throws \PDOException
+ */
+ 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 BindParamException
+ * @throws \PDOException
+ */
+ 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 BindParamException
+ * @throws \PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ 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);
+
+ if (isset($cacheItem) && $resultSet) {
+ // 缓存数据集
+ $cacheItem->set($resultSet);
+ $this->cacheData($cacheItem);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return \PDOStatement
+ */
+ 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 BindParamException
+ * @throws \PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
+ {
+ $this->initConnect($this->readMaster ?: $master);
+
+ // 记录SQL语句
+ $this->queryStr = $sql;
+
+ $this->bind = $bind;
+
+ $this->db->updateQueryTimes();
+
+ try {
+ $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->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 BindParamException
+ * @throws \PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ 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;
+ }
+
+ 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
+ {
+ // 事件回调
+ $result = $this->db->trigger('before_find', $query);
+
+ if (!$result) {
+ // 执行查询
+ $resultSet = $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query, true);
+ });
+
+ $result = $resultSet[0] ?? [];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 使用游标查询记录
+ * @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
+ {
+ $resultSet = $this->db->trigger('before_select', $query);
+
+ if (!$resultSet) {
+ // 执行查询操作
+ $resultSet = $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query);
+ });
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 插入记录
+ * @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, false);
+
+ return $force ? (float) $result : $result;
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, string $column, string $key = ''): array
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['field'])) {
+ $query->removeOption('field');
+ }
+
+ if ($key && '*' != $column) {
+ $field = $key . ',' . $column;
+ } else {
+ $field = $column;
+ }
+
+ $field = array_map('trim', explode(',', $field));
+
+ $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 (empty($resultSet)) {
+ $result = [];
+ } elseif (('*' == $column || strpos($column, ',')) && $key) {
+ $result = array_column($resultSet, null, $key);
+ } else {
+ if (empty($key)) {
+ $key = null;
+ }
+
+ if (strpos($column, ',')) {
+ $column = null;
+ } elseif (strpos($column, ' ')) {
+ $column = substr(strrchr(trim($column), ' '), 1);
+ } elseif (strpos($column, '.')) {
+ [$alias, $column] = explode('.', $column);
+ }
+
+ if (is_string($key) && strpos($key, '.')) {
+ [$alias, $key] = explode('.', $key);
+ }
+
+ $result = array_column($resultSet, $column, $key);
+ }
+
+ 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 = is_array($val) ? $val[0] : $val;
+ $type = is_array($val) ? $val[1] : PDO::PARAM_STR;
+
+ if ((self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) && is_string($value)) {
+ $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
+ {
+ $this->initConnect(true);
+
+ ++$this->transTimes;
+
+ try {
+ if (1 == $this->transTimes) {
+ $this->linkID->beginTransaction();
+ } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
+ $this->linkID->exec(
+ $this->parseSavepoint('trans' . $this->transTimes)
+ );
+ }
+ $this->reConnectTimes = 0;
+ } catch (\Exception $e) {
+ if ($this->reConnectTimes < 4 && $this->isBreak($e)) {
+ --$this->transTimes;
+ ++$this->reConnectTimes;
+ $this->close()->startTrans();
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function commit(): void
+ {
+ $this->initConnect(true);
+
+ if (1 == $this->transTimes) {
+ $this->linkID->commit();
+ }
+
+ --$this->transTimes;
+ }
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function rollback(): void
+ {
+ $this->initConnect(true);
+
+ 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->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);
+ }
+
+ /**
+ * 启动XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function startTransXa(string $xid)
+ {}
+
+ /**
+ * 预编译XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function prepareXa(string $xid)
+ {}
+
+ /**
+ * 提交XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function commitXa(string $xid)
+ {}
+
+ /**
+ * 回滚XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function rollbackXa(string $xid)
+ {}
+}
diff --git a/vendor/topthink/think-orm/src/db/Query.php b/vendor/topthink/think-orm/src/db/Query.php
new file mode 100644
index 0000000..80e01cd
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Query.php
@@ -0,0 +1,451 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use PDOStatement;
+use think\helper\Str;
+
+/**
+ * PDO数据查询类
+ */
+class Query extends BaseQuery
+{
+ use concern\JoinAndViewQuery;
+ use concern\ParamsBind;
+ use concern\TableFieldInfo;
+
+ /**
+ * 表达式方式指定Field排序
+ * @access public
+ * @param string $field 排序字段
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function orderRaw(string $field, array $bind = [])
+ {
+ $this->options['order'][] = new Raw($field, $bind);
+
+ return $this;
+ }
+
+ /**
+ * 表达式方式指定查询字段
+ * @access public
+ * @param string $field 字段名
+ * @return $this
+ */
+ public function fieldRaw(string $field)
+ {
+ $this->options['field'][] = new Raw($field);
+
+ return $this;
+ }
+
+ /**
+ * 指定Field排序 orderField('id',[1,2,3],'desc')
+ * @access public
+ * @param string $field 排序字段
+ * @param array $values 排序值
+ * @param string $order 排序 desc/asc
+ * @return $this
+ */
+ public function orderField(string $field, array $values, string $order = '')
+ {
+ if (!empty($values)) {
+ $values['sort'] = $order;
+
+ $this->options['order'][$field] = $values;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 随机排序
+ * @access public
+ * @return $this
+ */
+ public function orderRand()
+ {
+ $this->options['order'][] = '[rand]';
+ return $this;
+ }
+
+ /**
+ * 使用表达式设置数据
+ * @access public
+ * @param string $field 字段名
+ * @param string $value 字段值
+ * @return $this
+ */
+ public function exp(string $field, string $value)
+ {
+ $this->options['data'][$field] = new Raw($value);
+ return $this;
+ }
+
+ /**
+ * 表达式方式指定当前操作的数据表
+ * @access public
+ * @param mixed $table 表名
+ * @return $this
+ */
+ public function tableRaw(string $table)
+ {
+ $this->options['table'] = new Raw($table);
+
+ return $this;
+ }
+
+ /**
+ * 获取执行的SQL语句而不进行实际的查询
+ * @access public
+ * @param bool $fetch 是否返回sql
+ * @return $this|Fetch
+ */
+ public function fetchSql(bool $fetch = true)
+ {
+ $this->options['fetch_sql'] = $fetch;
+
+ if ($fetch) {
+ return new Fetch($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 批处理执行SQL语句
+ * 批处理的指令都认为是execute操作
+ * @access public
+ * @param array $sql SQL批处理指令
+ * @return bool
+ */
+ public function batchQuery(array $sql = []): bool
+ {
+ return $this->connection->batchQuery($this, $sql);
+ }
+
+ /**
+ * USING支持 用于多表删除
+ * @access public
+ * @param mixed $using USING
+ * @return $this
+ */
+ public function using($using)
+ {
+ $this->options['using'] = $using;
+ return $this;
+ }
+
+ /**
+ * 存储过程调用
+ * @access public
+ * @param bool $procedure 是否为存储过程查询
+ * @return $this
+ */
+ public function procedure(bool $procedure = true)
+ {
+ $this->options['procedure'] = $procedure;
+ return $this;
+ }
+
+ /**
+ * 指定group查询
+ * @access public
+ * @param string|array $group GROUP
+ * @return $this
+ */
+ public function group($group)
+ {
+ $this->options['group'] = $group;
+ return $this;
+ }
+
+ /**
+ * 指定having查询
+ * @access public
+ * @param string $having having
+ * @return $this
+ */
+ public function having(string $having)
+ {
+ $this->options['having'] = $having;
+ return $this;
+ }
+
+ /**
+ * 指定distinct查询
+ * @access public
+ * @param bool $distinct 是否唯一
+ * @return $this
+ */
+ public function distinct(bool $distinct = true)
+ {
+ $this->options['distinct'] = $distinct;
+ return $this;
+ }
+
+ /**
+ * 指定强制索引
+ * @access public
+ * @param string $force 索引名称
+ * @return $this
+ */
+ public function force(string $force)
+ {
+ $this->options['force'] = $force;
+ return $this;
+ }
+
+ /**
+ * 查询注释
+ * @access public
+ * @param string $comment 注释
+ * @return $this
+ */
+ public function comment(string $comment)
+ {
+ $this->options['comment'] = $comment;
+ return $this;
+ }
+
+ /**
+ * 设置是否REPLACE
+ * @access public
+ * @param bool $replace 是否使用REPLACE写入数据
+ * @return $this
+ */
+ public function replace(bool $replace = true)
+ {
+ $this->options['replace'] = $replace;
+ return $this;
+ }
+
+ /**
+ * 设置当前查询所在的分区
+ * @access public
+ * @param string|array $partition 分区名称
+ * @return $this
+ */
+ public function partition($partition)
+ {
+ $this->options['partition'] = $partition;
+ return $this;
+ }
+
+ /**
+ * 设置DUPLICATE
+ * @access public
+ * @param array|string|Raw $duplicate DUPLICATE信息
+ * @return $this
+ */
+ public function duplicate($duplicate)
+ {
+ $this->options['duplicate'] = $duplicate;
+ return $this;
+ }
+
+ /**
+ * 设置查询的额外参数
+ * @access public
+ * @param string $extra 额外信息
+ * @return $this
+ */
+ public function extra(string $extra)
+ {
+ $this->options['extra'] = $extra;
+ return $this;
+ }
+
+ /**
+ * 创建子查询SQL
+ * @access public
+ * @param bool $sub 是否添加括号
+ * @return string
+ * @throws Exception
+ */
+ public function buildSql(bool $sub = true): string
+ {
+ return $sub ? '( ' . $this->fetchSql()->select() . ' )' : $this->fetchSql()->select();
+ }
+
+ /**
+ * 获取当前数据表的主键
+ * @access public
+ * @return string|array
+ */
+ public function getPk()
+ {
+ if (empty($this->pk)) {
+ $this->pk = $this->connection->getPk($this->getTable());
+ }
+
+ return $this->pk;
+ }
+
+ /**
+ * 指定数据表自增主键
+ * @access public
+ * @param string $autoinc 自增键
+ * @return $this
+ */
+ public function autoinc(string $autoinc)
+ {
+ $this->autoinc = $autoinc;
+ return $this;
+ }
+
+ /**
+ * 获取当前数据表的自增主键
+ * @access public
+ * @return string|null
+ */
+ public function getAutoInc()
+ {
+ $tableName = $this->getTable();
+
+ if (empty($this->autoinc) && $tableName) {
+ $this->autoinc = $this->connection->getAutoInc($tableName);
+ }
+
+ return $this->autoinc;
+ }
+
+ /**
+ * 字段值增长
+ * @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)
+ {
+ $this->options['data'][$field] = ['DEC', $step];
+ return $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)) . serialize($this->getBind(false)));
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @return PDOStatement
+ */
+ public function getPdo(): PDOStatement
+ {
+ return $this->connection->pdo($this);
+ }
+
+ /**
+ * 使用游标查找记录
+ * @access public
+ * @param mixed $data 数据
+ * @return \Generator
+ */
+ public function cursor($data = null)
+ {
+ if (!is_null($data)) {
+ // 主键条件分析
+ $this->parsePkWhere($data);
+ }
+
+ $this->options['data'] = $data;
+
+ $connection = clone $this->connection;
+
+ return $connection->cursor($this);
+ }
+
+ /**
+ * 分批数据返回处理
+ * @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']);
+ }
+
+ $bind = $this->bind;
+
+ 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->bind($bind)->order($column, $order)->select();
+ }
+
+ return true;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/Raw.php b/vendor/topthink/think-orm/src/db/Raw.php
new file mode 100644
index 0000000..34d4c41
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Raw.php
@@ -0,0 +1,71 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+/**
+ * SQL Raw
+ */
+class Raw
+{
+ /**
+ * 查询表达式
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * 参数绑定
+ *
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 创建一个查询表达式
+ *
+ * @param string $value
+ * @param array $bind
+ * @return void
+ */
+ public function __construct(string $value, array $bind = [])
+ {
+ $this->value = $value;
+ $this->bind = $bind;
+ }
+
+ /**
+ * 获取表达式
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ /**
+ * 获取参数绑定
+ *
+ * @return string
+ */
+ public function getBind(): array
+ {
+ return $this->bind;
+ }
+
+ public function __toString()
+ {
+ return (string) $this->value;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/Where.php b/vendor/topthink/think-orm/src/db/Where.php
new file mode 100644
index 0000000..19898ac
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Where.php
@@ -0,0 +1,182 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use ArrayAccess;
+
+/**
+ * 数组查询对象
+ */
+class Where implements ArrayAccess
+{
+ /**
+ * 查询表达式
+ * @var array
+ */
+ protected $where = [];
+
+ /**
+ * 是否需要把查询条件两边增加括号
+ * @var bool
+ */
+ protected $enclose = false;
+
+ /**
+ * 创建一个查询表达式
+ *
+ * @param array $where 查询条件数组
+ * @param bool $enclose 是否增加括号
+ */
+ public function __construct(array $where = [], bool $enclose = false)
+ {
+ $this->where = $where;
+ $this->enclose = $enclose;
+ }
+
+ /**
+ * 设置是否添加括号
+ * @access public
+ * @param bool $enclose
+ * @return $this
+ */
+ public function enclose(bool $enclose = true)
+ {
+ $this->enclose = $enclose;
+ return $this;
+ }
+
+ /**
+ * 解析为Query对象可识别的查询条件数组
+ * @access public
+ * @return array
+ */
+ public function parse(): array
+ {
+ $where = [];
+
+ foreach ($this->where as $key => $val) {
+ if ($val instanceof Raw) {
+ $where[] = [$key, 'exp', $val];
+ } elseif (is_null($val)) {
+ $where[] = [$key, 'NULL', ''];
+ } elseif (is_array($val)) {
+ $where[] = $this->parseItem($key, $val);
+ } else {
+ $where[] = [$key, '=', $val];
+ }
+ }
+
+ return $this->enclose ? [$where] : $where;
+ }
+
+ /**
+ * 分析查询表达式
+ * @access protected
+ * @param string $field 查询字段
+ * @param array $where 查询条件
+ * @return array
+ */
+ protected function parseItem(string $field, array $where = []): array
+ {
+ $op = $where[0];
+ $condition = $where[1] ?? null;
+
+ if (is_array($op)) {
+ // 同一字段多条件查询
+ array_unshift($where, $field);
+ } elseif (is_null($condition)) {
+ if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
+ // null查询
+ $where = [$field, $op, ''];
+ } elseif (is_null($op) || '=' == $op) {
+ $where = [$field, 'NULL', ''];
+ } elseif ('<>' == $op) {
+ $where = [$field, 'NOTNULL', ''];
+ } else {
+ // 字段相等查询
+ $where = [$field, '=', $op];
+ }
+ } else {
+ $where = [$field, $op, $condition];
+ }
+
+ return $where;
+ }
+
+ /**
+ * 修改器 设置数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @param mixed $value 值
+ * @return void
+ */
+ public function __set($name, $value)
+ {
+ $this->where[$name] = $value;
+ }
+
+ /**
+ * 获取器 获取数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->where[$name] ?? null;
+ }
+
+ /**
+ * 检测数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->where[$name]);
+ }
+
+ /**
+ * 销毁数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return void
+ */
+ public function __unset($name)
+ {
+ unset($this->where[$name]);
+ }
+
+ // ArrayAccess
+ public function offsetSet($name, $value)
+ {
+ $this->__set($name, $value);
+ }
+
+ public function offsetExists($name)
+ {
+ return $this->__isset($name);
+ }
+
+ public function offsetUnset($name)
+ {
+ $this->__unset($name);
+ }
+
+ public function offsetGet($name)
+ {
+ return $this->__get($name);
+ }
+
+}
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..93f7498
--- /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..6bdfc46
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Mysql.php
@@ -0,0 +1,421 @@
+
+// +----------------------------------------------------------------------
+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 ('*' == $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[] = $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 'json_extract(' . $this->parseKey($query, $field) . ', \'$' . (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..0fdd81d
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Oracle.php
@@ -0,0 +1,95 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\db\Query;
+
+/**
+ * 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 string $strict
+ * @return string
+ */
+ public function parseKey(Query $query, $key, bool $strict = false): string
+ {
+ $key = trim($key);
+
+ if (strpos($key, '->') && false === strpos($key, '(')) {
+ // JSON字段支持
+ [$field, $name] = explode($key, '->');
+ $key = $field . '."' . $name . '"';
+ }
+
+ 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/builder/Pgsql.php b/vendor/topthink/think-orm/src/db/builder/Pgsql.php
new file mode 100644
index 0000000..041af5f
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Pgsql.php
@@ -0,0 +1,118 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * Pgsql数据库驱动
+ */
+class Pgsql extends Builder
+{
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+
+ /**
+ * limit分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $limit
+ * @return string
+ */
+ public function parseLimit(Query $query, string $limit): string
+ {
+ $limitStr = '';
+
+ if (!empty($limit)) {
+ $limit = explode(',', $limit);
+ if (count($limit) > 1) {
+ $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' ';
+ } else {
+ $limitStr .= ' LIMIT ' . $limit[0] . ' ';
+ }
+ }
+
+ return $limitStr;
+ }
+
+ /**
+ * 字段和表名处理
+ * @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);
+ $key = '"' . $field . '"' . '->>\'' . $name . '\'';
+ } elseif (strpos($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 ('*' != $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 'RANDOM()';
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/builder/Sqlite.php b/vendor/topthink/think-orm/src/db/builder/Sqlite.php
new file mode 100644
index 0000000..fceb534
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Sqlite.php
@@ -0,0 +1,97 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * Sqlite数据库驱动
+ */
+class Sqlite extends Builder
+{
+ /**
+ * limit
+ * @access public
+ * @param Query $query 查询对象
+ * @param mixed $limit
+ * @return string
+ */
+ public function parseLimit(Query $query, string $limit): string
+ {
+ $limitStr = '';
+
+ if (!empty($limit)) {
+ $limit = explode(',', $limit);
+ if (count($limit) > 1) {
+ $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' ';
+ } else {
+ $limitStr .= ' LIMIT ' . $limit[0] . ' ';
+ }
+ }
+
+ return $limitStr;
+ }
+
+ /**
+ * 随机排序
+ * @access protected
+ * @param Query $query 查询对象
+ * @return string
+ */
+ protected function parseRand(Query $query): string
+ {
+ return 'RANDOM()';
+ }
+
+ /**
+ * 字段和表名处理
+ * @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, '.')) {
+ [$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 (isset($table)) {
+ $key = $table . '.' . $key;
+ }
+
+ return $key;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/builder/Sqlsrv.php b/vendor/topthink/think-orm/src/db/builder/Sqlsrv.php
new file mode 100644
index 0000000..fc36651
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Sqlsrv.php
@@ -0,0 +1,184 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\db\exception\DbException as Exception;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * Sqlsrv数据库驱动
+ */
+class Sqlsrv extends Builder
+{
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%';
+ /**
+ * SELECT INSERT SQL表达式
+ * @var string
+ */
+ protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%';
+
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+
+ /**
+ * order分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $order
+ * @return string
+ */
+ protected function parseOrder(Query $query, array $order): string
+ {
+ if (empty($order)) {
+ return ' ORDER BY rand()';
+ }
+
+ $array = [];
+
+ foreach ($order as $key => $val) {
+ if ($val instanceof Raw) {
+ $array[] = $this->parseRaw($query, $val);
+ } elseif ('[rand]' == $val) {
+ $array[] = $this->parseRand($query);
+ } else {
+ if (is_numeric($key)) {
+ [$key, $sort] = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
+ } else {
+ $sort = $val;
+ }
+
+ $sort = in_array(strtolower($sort), ['asc', 'desc'], true) ? ' ' . $sort : '';
+ $array[] = $this->parseKey($query, $key, true) . $sort;
+ }
+ }
+
+ return ' ORDER BY ' . implode(',', $array);
+ }
+
+ /**
+ * 随机排序
+ * @access protected
+ * @param Query $query 查询对象
+ * @return string
+ */
+ protected function parseRand(Query $query): string
+ {
+ return 'rand()';
+ }
+
+ /**
+ * 字段和表名处理
+ * @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, '.') && !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;
+ }
+
+ /**
+ * limit
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $limit
+ * @return string
+ */
+ protected function parseLimit(Query $query, string $limit): string
+ {
+ if (empty($limit)) {
+ return '';
+ }
+
+ $limit = explode(',', $limit);
+
+ if (count($limit) > 1) {
+ $limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')';
+ } else {
+ $limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ")";
+ }
+
+ return 'WHERE ' . $limitStr;
+ }
+
+ public function selectInsert(Query $query, array $fields, string $table): string
+ {
+ $this->selectSql = $this->selectInsertSql;
+
+ return parent::selectInsert($query, $fields, $table);
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php b/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php
new file mode 100644
index 0000000..f63b973
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php
@@ -0,0 +1,107 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use think\db\Raw;
+
+/**
+ * 聚合查询
+ */
+trait AggregateQuery
+{
+ /**
+ * 聚合查询
+ * @access protected
+ * @param string $aggregate 聚合方法
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ protected function aggregate(string $aggregate, $field, bool $force = false)
+ {
+ return $this->connection->aggregate($this, $aggregate, $field, $force);
+ }
+
+ /**
+ * COUNT查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return int
+ */
+ public function count(string $field = '*'): int
+ {
+ if (!empty($this->options['group'])) {
+ // 支持GROUP
+ $options = $this->getOptions();
+ $subSql = $this->options($options)
+ ->field('count(' . $field . ') AS think_count')
+ ->bind($this->bind)
+ ->buildSql();
+
+ $query = $this->newQuery()->table([$subSql => '_group_count_']);
+
+ $count = $query->aggregate('COUNT', '*');
+ } else {
+ $count = $this->aggregate('COUNT', $field);
+ }
+
+ return (int) $count;
+ }
+
+ /**
+ * SUM查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return float
+ */
+ public function sum($field): float
+ {
+ return $this->aggregate('SUM', $field, true);
+ }
+
+ /**
+ * MIN查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function min($field, bool $force = true)
+ {
+ return $this->aggregate('MIN', $field, $force);
+ }
+
+ /**
+ * MAX查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function max($field, bool $force = true)
+ {
+ return $this->aggregate('MAX', $field, $force);
+ }
+
+ /**
+ * AVG查询
+ * @access public
+ * @param string|Raw $field 字段名
+ * @return float
+ */
+ public function avg($field): float
+ {
+ return $this->aggregate('AVG', $field, true);
+ }
+
+}
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..abc7135
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php
@@ -0,0 +1,229 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use think\db\Raw;
+use think\helper\Str;
+
+/**
+ * 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..288e069
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php
@@ -0,0 +1,524 @@
+
+// +----------------------------------------------------------------------
+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($relation)) {
+ $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)) {
+ $this->options['with_attr'] = $name;
+ } else {
+ $this->options['with_attr'][$name] = $callback;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 关联预载入 In方式
+ * @access public
+ * @param array|string $with 关联方法名称
+ * @return $this
+ */
+ public function with($with)
+ {
+ if (!empty($with)) {
+ $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($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 (!$subQuery) {
+ $this->options['with_count'][] = [$relations, $aggregate, $field];
+ } else {
+ 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 (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);
+ }
+
+ /**
+ * 查询数据转换为模型数据集对象
+ * @access protected
+ * @param array $resultSet 数据集
+ * @return ModelCollection
+ */
+ protected function resultSetToModelCollection(array $resultSet): ModelCollection
+ {
+ if (empty($resultSet)) {
+ return $this->model->toCollection();
+ }
+
+ // 检查动态获取器
+ if (!empty($this->options['with_attr'])) {
+ foreach ($this->options['with_attr'] as $name => $val) {
+ if (strpos($name, '.')) {
+ [$relation, $field] = explode('.', $name);
+
+ $withRelationAttr[$relation][$field] = $val;
+ unset($this->options['with_attr'][$name]);
+ }
+ }
+ }
+
+ $withRelationAttr = $withRelationAttr ?? [];
+
+ foreach ($resultSet as $key => &$result) {
+ // 数据转换为模型对象
+ $this->resultToModel($result, $this->options, true, $withRelationAttr);
+ }
+
+ if (!empty($this->options['with'])) {
+ // 预载入
+ $result->eagerlyResultSet($resultSet, $this->options['with'], $withRelationAttr, false, $this->options['with_cache'] ?? false);
+ }
+
+ if (!empty($this->options['with_join'])) {
+ // 预载入
+ $result->eagerlyResultSet($resultSet, $this->options['with_join'], $withRelationAttr, true, $this->options['with_cache'] ?? false);
+ }
+
+ // 模型数据集转换
+ return $this->model->toCollection($resultSet);
+ }
+
+ /**
+ * 查询数据转换为模型对象
+ * @access protected
+ * @param array $result 查询数据
+ * @param array $options 查询参数
+ * @param bool $resultSet 是否为数据集查询
+ * @param array $withRelationAttr 关联字段获取器
+ * @return void
+ */
+ protected function resultToModel(array &$result, array $options = [], bool $resultSet = false, array $withRelationAttr = []): void
+ {
+ // 动态获取器
+ if (!empty($options['with_attr']) && empty($withRelationAttr)) {
+ foreach ($options['with_attr'] as $name => $val) {
+ if (strpos($name, '.')) {
+ [$relation, $field] = explode('.', $name);
+
+ $withRelationAttr[$relation][$field] = $val;
+ unset($options['with_attr'][$name]);
+ }
+ }
+ }
+
+ // JSON 数据处理
+ if (!empty($options['json'])) {
+ $this->jsonResult($result, $options['json'], $options['json_assoc'], $withRelationAttr);
+ }
+
+ $result = $this->model
+ ->newInstance($result, $resultSet ? null : $this->getModelUpdateCondition($options));
+
+ // 动态获取器
+ if (!empty($options['with_attr'])) {
+ $result->withAttribute($options['with_attr']);
+ }
+
+ // 输出属性控制
+ if (!empty($options['visible'])) {
+ $result->visible($options['visible']);
+ } elseif (!empty($options['hidden'])) {
+ $result->hidden($options['hidden']);
+ }
+
+ if (!empty($options['append'])) {
+ $result->append($options['append']);
+ }
+
+ // 关联查询
+ if (!empty($options['relation'])) {
+ $result->relationQuery($options['relation'], $withRelationAttr);
+ }
+
+ // 预载入查询
+ if (!$resultSet && !empty($options['with'])) {
+ $result->eagerlyResult($result, $options['with'], $withRelationAttr, false, $options['with_cache'] ?? false);
+ }
+
+ // JOIN预载入查询
+ if (!$resultSet && !empty($options['with_join'])) {
+ $result->eagerlyResult($result, $options['with_join'], $withRelationAttr, true, $options['with_cache'] ?? false);
+ }
+
+ // 关联统计
+ if (!empty($options['with_count'])) {
+ foreach ($options['with_count'] as $val) {
+ $result->relationCount($this, (array) $val[0], $val[1], $val[2], false);
+ }
+ }
+ }
+
+}
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..1b67c5c
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/ParamsBind.php
@@ -0,0 +1,106 @@
+
+// +----------------------------------------------------------------------
+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 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/concern/ResultOperation.php b/vendor/topthink/think-orm/src/db/concern/ResultOperation.php
new file mode 100644
index 0000000..e05df8e
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/ResultOperation.php
@@ -0,0 +1,247 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use Closure;
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\db\Query;
+use think\helper\Str;
+use think\Model;
+
+/**
+ * 查询数据处理
+ */
+trait ResultOperation
+{
+ /**
+ * 是否允许返回空数据(或空模型)
+ * @access public
+ * @param bool $allowEmpty 是否允许为空
+ * @return $this
+ */
+ public function allowEmpty(bool $allowEmpty = true)
+ {
+ $this->options['allow_empty'] = $allowEmpty;
+ return $this;
+ }
+
+ /**
+ * 设置查询数据不存在是否抛出异常
+ * @access public
+ * @param bool $fail 数据不存在是否抛出异常
+ * @return $this
+ */
+ public function failException(bool $fail = true)
+ {
+ $this->options['fail'] = $fail;
+ return $this;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function result(array &$result): void
+ {
+ if (!empty($this->options['json'])) {
+ $this->jsonResult($result, $this->options['json'], true);
+ }
+
+ if (!empty($this->options['with_attr'])) {
+ $this->getResultAttr($result, $this->options['with_attr']);
+ }
+
+ $this->filterResult($result);
+ }
+
+ /**
+ * 处理数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @return void
+ */
+ protected function resultSet(array &$resultSet): void
+ {
+ if (!empty($this->options['json'])) {
+ foreach ($resultSet as &$result) {
+ $this->jsonResult($result, $this->options['json'], true);
+ }
+ }
+
+ if (!empty($this->options['with_attr'])) {
+ foreach ($resultSet as &$result) {
+ $this->getResultAttr($result, $this->options['with_attr']);
+ }
+ }
+
+ if (!empty($this->options['visible']) || !empty($this->options['hidden'])) {
+ foreach ($resultSet as &$result) {
+ $this->filterResult($result);
+ }
+ }
+
+ // 返回Collection对象
+ $resultSet = new Collection($resultSet);
+ }
+
+ /**
+ * 处理数据的可见和隐藏
+ * @access protected
+ * @param array $result 查询数据
+ * @return void
+ */
+ protected function filterResult(&$result): void
+ {
+ $array = [];
+ if (!empty($this->options['visible'])) {
+ foreach ($this->options['visible'] as $key) {
+ $array[] = $key;
+ }
+ $result = array_intersect_key($result, array_flip($array));
+ } elseif (!empty($this->options['hidden'])) {
+ foreach ($this->options['hidden'] as $key) {
+ $array[] = $key;
+ }
+ $result = array_diff_key($result, array_flip($array));
+ }
+ }
+
+ /**
+ * 使用获取器处理数据
+ * @access protected
+ * @param array $result 查询数据
+ * @param array $withAttr 字段获取器
+ * @return void
+ */
+ protected function getResultAttr(array &$result, array $withAttr = []): void
+ {
+ foreach ($withAttr as $name => $closure) {
+ $name = Str::snake($name);
+
+ if (strpos($name, '.')) {
+ // 支持JSON字段 获取器定义
+ [$key, $field] = explode('.', $name);
+
+ if (isset($result[$key])) {
+ $result[$key][$field] = $closure($result[$key][$field] ?? null, $result[$key]);
+ }
+ } else {
+ $result[$name] = $closure($result[$name] ?? null, $result);
+ }
+ }
+ }
+
+ /**
+ * 处理空数据
+ * @access protected
+ * @return array|Model|null
+ * @throws DbException
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ protected function resultToEmpty()
+ {
+ if (!empty($this->options['fail'])) {
+ $this->throwNotFound();
+ } elseif (!empty($this->options['allow_empty'])) {
+ return !empty($this->model) ? $this->model->newInstance() : [];
+ }
+ }
+
+ /**
+ * 查找单条记录 不存在返回空数据(或者空模型)
+ * @access public
+ * @param mixed $data 数据
+ * @return array|Model
+ */
+ public function findOrEmpty($data = null)
+ {
+ return $this->allowEmpty(true)->find($data);
+ }
+
+ /**
+ * JSON字段数据转换
+ * @access protected
+ * @param array $result 查询数据
+ * @param array $json JSON字段
+ * @param bool $assoc 是否转换为数组
+ * @param array $withRelationAttr 关联获取器
+ * @return void
+ */
+ protected function jsonResult(array &$result, array $json = [], bool $assoc = false, array $withRelationAttr = []): void
+ {
+ foreach ($json as $name) {
+ if (!isset($result[$name])) {
+ continue;
+ }
+
+ $result[$name] = json_decode($result[$name], true);
+
+ if (isset($withRelationAttr[$name])) {
+ foreach ($withRelationAttr[$name] as $key => $closure) {
+ $result[$name][$key] = $closure($result[$name][$key] ?? null, $result[$name]);
+ }
+ }
+
+ if (!$assoc) {
+ $result[$name] = (object) $result[$name];
+ }
+ }
+ }
+
+ /**
+ * 查询失败 抛出异常
+ * @access protected
+ * @return void
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ protected function throwNotFound(): void
+ {
+ if (!empty($this->model)) {
+ $class = get_class($this->model);
+ throw new ModelNotFoundException('model data Not Found:' . $class, $class, $this->options);
+ }
+
+ $table = $this->getTable();
+ throw new DataNotFoundException('table data not Found:' . $table, $table, $this->options);
+ }
+
+ /**
+ * 查找多条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|Closure $data 数据
+ * @return array|Model
+ */
+ public function selectOrFail($data = null)
+ {
+ return $this->failException(true)->select($data);
+ }
+
+ /**
+ * 查找单条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|Closure $data 数据
+ * @return array|Model
+ */
+ public function findOrFail($data = null)
+ {
+ return $this->failException(true)->find($data);
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php b/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php
new file mode 100644
index 0000000..6fab680
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php
@@ -0,0 +1,99 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+/**
+ * 数据字段信息
+ */
+trait TableFieldInfo
+{
+
+ /**
+ * 获取数据表字段信息
+ * @access public
+ * @param string $tableName 数据表名
+ * @return array
+ */
+ public function getTableFields($tableName = ''): array
+ {
+ if ('' == $tableName) {
+ $tableName = $this->getTable();
+ }
+
+ return $this->connection->getTableFields($tableName);
+ }
+
+ /**
+ * 获取详细字段类型信息
+ * @access public
+ * @param string $tableName 数据表名称
+ * @return array
+ */
+ public function getFields(string $tableName = ''): array
+ {
+ return $this->connection->getFields($tableName ?: $this->getTable());
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @return array
+ */
+ public function getFieldsType(): array
+ {
+ if (!empty($this->options['field_type'])) {
+ return $this->options['field_type'];
+ }
+
+ return $this->connection->getFieldsType($this->getTable());
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return string|null
+ */
+ public function getFieldType(string $field)
+ {
+ $fieldType = $this->getFieldsType();
+
+ return $fieldType[$field] ?? null;
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @return array
+ */
+ public function getFieldsBindType(): array
+ {
+ $fieldType = $this->getFieldsType();
+
+ return array_map([$this->connection, 'getFieldBindType'], $fieldType);
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return int
+ */
+ public function getFieldBindType(string $field): int
+ {
+ $fieldType = $this->getFieldType($field);
+
+ return $this->connection->getFieldBindType($fieldType ?: '');
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php b/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php
new file mode 100644
index 0000000..c96a6a4
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php
@@ -0,0 +1,214 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+/**
+ * 时间查询支持
+ */
+trait TimeFieldQuery
+{
+ /**
+ * 日期查询表达式
+ * @var array
+ */
+ protected $timeRule = [
+ 'today' => ['today', 'tomorrow -1second'],
+ 'yesterday' => ['yesterday', 'today -1second'],
+ 'week' => ['this week 00:00:00', 'next week 00:00:00 -1second'],
+ 'last week' => ['last week 00:00:00', 'this week 00:00:00 -1second'],
+ 'month' => ['first Day of this month 00:00:00', 'first Day of next month 00:00:00 -1second'],
+ 'last month' => ['first Day of last month 00:00:00', 'first Day of this month 00:00:00 -1second'],
+ 'year' => ['this year 1/1', 'next year 1/1 -1second'],
+ 'last year' => ['last year 1/1', 'this year 1/1 -1second'],
+ ];
+
+ /**
+ * 添加日期或者时间查询规则
+ * @access public
+ * @param array $rule 时间表达式
+ * @return $this
+ */
+ public function timeRule(array $rule)
+ {
+ $this->timeRule = array_merge($this->timeRule, $rule);
+ return $this;
+ }
+
+ /**
+ * 查询日期或者时间
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $op 比较运算符或者表达式
+ * @param string|array $range 比较范围
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereTime(string $field, string $op, $range = null, string $logic = 'AND')
+ {
+ if (is_null($range)) {
+ if (isset($this->timeRule[$op])) {
+ $range = $this->timeRule[$op];
+ } else {
+ $range = $op;
+ }
+ $op = is_array($range) ? 'between' : '>=';
+ }
+
+ return $this->parseWhereExp($logic, $field, strtolower($op) . ' time', $range, [], true);
+ }
+
+ /**
+ * 查询某个时间间隔数据
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $start 开始时间
+ * @param string $interval 时间间隔单位 day/month/year/week/hour/minute/second
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereTimeInterval(string $field, string $start, string $interval = 'day', int $step = 1, string $logic = 'AND')
+ {
+ $startTime = strtotime($start);
+ $endTime = strtotime(($step > 0 ? '+' : '-') . abs($step) . ' ' . $interval . (abs($step) > 1 ? 's' : ''), $startTime);
+
+ return $this->whereTime($field, 'between', $step > 0 ? [$startTime, $endTime - 1] : [$endTime, $startTime - 1], $logic);
+ }
+
+ /**
+ * 查询月数据 whereMonth('time_field', '2018-1')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $month 月份信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereMonth(string $field, string $month = 'this month', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($month, ['this month', 'last month'])) {
+ $month = date('Y-m', strtotime($month));
+ }
+
+ return $this->whereTimeInterval($field, $month, 'month', $step, $logic);
+ }
+
+ /**
+ * 查询周数据 whereWeek('time_field', '2018-1-1') 从2018-1-1开始的一周数据
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $week 周信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereWeek(string $field, string $week = 'this week', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($week, ['this week', 'last week'])) {
+ $week = date('Y-m-d', strtotime($week));
+ }
+
+ return $this->whereTimeInterval($field, $week, 'week', $step, $logic);
+ }
+
+ /**
+ * 查询年数据 whereYear('time_field', '2018')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $year 年份信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereYear(string $field, string $year = 'this year', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($year, ['this year', 'last year'])) {
+ $year = date('Y', strtotime($year));
+ }
+
+ return $this->whereTimeInterval($field, $year . '-1-1', 'year', $step, $logic);
+ }
+
+ /**
+ * 查询日数据 whereDay('time_field', '2018-1-1')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string $day 日期信息
+ * @param int $step 间隔
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereDay(string $field, string $day = 'today', int $step = 1, string $logic = 'AND')
+ {
+ if (in_array($day, ['today', 'yesterday'])) {
+ $day = date('Y-m-d', strtotime($day));
+ }
+
+ return $this->whereTimeInterval($field, $day, 'day', $step, $logic);
+ }
+
+ /**
+ * 查询日期或者时间范围 whereBetweenTime('time_field', '2018-1-1','2018-1-15')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string|int $startTime 开始时间
+ * @param string|int $endTime 结束时间
+ * @param string $logic AND OR
+ * @return $this
+ */
+ public function whereBetweenTime(string $field, $startTime, $endTime, string $logic = 'AND')
+ {
+ return $this->whereTime($field, 'between', [$startTime, $endTime], $logic);
+ }
+
+ /**
+ * 查询日期或者时间范围 whereNotBetweenTime('time_field', '2018-1-1','2018-1-15')
+ * @access public
+ * @param string $field 日期字段名
+ * @param string|int $startTime 开始时间
+ * @param string|int $endTime 结束时间
+ * @return $this
+ */
+ public function whereNotBetweenTime(string $field, $startTime, $endTime)
+ {
+ return $this->whereTime($field, '<', $startTime)
+ ->whereTime($field, '>', $endTime);
+ }
+
+ /**
+ * 查询当前时间在两个时间字段范围 whereBetweenTimeField('start_time', 'end_time')
+ * @access public
+ * @param string $startField 开始时间字段
+ * @param string $endField 结束时间字段
+ * @return $this
+ */
+ public function whereBetweenTimeField(string $startField, string $endField)
+ {
+ return $this->whereTime($startField, '<=', time())
+ ->whereTime($endField, '>=', time());
+ }
+
+ /**
+ * 查询当前时间不在两个时间字段范围 whereNotBetweenTimeField('start_time', 'end_time')
+ * @access public
+ * @param string $startField 开始时间字段
+ * @param string $endField 结束时间字段
+ * @return $this
+ */
+ public function whereNotBetweenTimeField(string $startField, string $endField)
+ {
+ return $this->whereTime($startField, '>', time())
+ ->whereTime($endField, '<', time(), 'OR');
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/concern/Transaction.php b/vendor/topthink/think-orm/src/db/concern/Transaction.php
new file mode 100644
index 0000000..3fe2d51
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/Transaction.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use think\db\BaseQuery;
+
+/**
+ * 事务支持
+ */
+trait Transaction
+{
+
+ /**
+ * 执行数据库Xa事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @param array $dbs 多个查询对象或者连接对象
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transactionXa($callback, array $dbs = [])
+ {
+ $xid = uniqid('xa');
+
+ if (empty($dbs)) {
+ $dbs[] = $this->getConnection();
+ }
+
+ foreach ($dbs as $key => $db) {
+ if ($db instanceof BaseQuery) {
+ $db = $db->getConnection();
+
+ $dbs[$key] = $db;
+ }
+
+ $db->startTransXa($xid);
+ }
+
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = call_user_func_array($callback, [$this]);
+ }
+
+ foreach ($dbs as $db) {
+ $db->prepareXa($xid);
+ }
+
+ foreach ($dbs as $db) {
+ $db->commitXa($xid);
+ }
+
+ return $result;
+ } catch (\Exception | \Throwable $e) {
+ foreach ($dbs as $db) {
+ $db->rollbackXa($xid);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ */
+ public function transaction(callable $callback)
+ {
+ return $this->connection->transaction($callback);
+ }
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ */
+ public function startTrans(): void
+ {
+ $this->connection->startTrans();
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function commit(): void
+ {
+ $this->connection->commit();
+ }
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function rollback(): void
+ {
+ $this->connection->rollback();
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/concern/WhereQuery.php b/vendor/topthink/think-orm/src/db/concern/WhereQuery.php
new file mode 100644
index 0000000..dd7e3ac
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/WhereQuery.php
@@ -0,0 +1,532 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\concern;
+
+use Closure;
+use think\db\BaseQuery;
+use think\db\Raw;
+
+trait WhereQuery
+{
+ /**
+ * 指定AND查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function where($field, $op = null, $condition = null)
+ {
+ if ($field instanceof $this) {
+ $this->parseQueryWhere($field);
+ return $this;
+ } elseif (true === $field || 1 === $field) {
+ $this->options['where']['AND'][] = true;
+ return $this;
+ }
+
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('AND', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 解析Query对象查询条件
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return void
+ */
+ protected function parseQueryWhere(BaseQuery $query): void
+ {
+ $this->options['where'] = $query->getOptions('where');
+
+ if ($query->getOptions('via')) {
+ $via = $query->getOptions('via');
+ foreach ($this->options['where'] as $logic => &$where) {
+ foreach ($where as $key => &$val) {
+ if (is_array($val) && !strpos($val[0], '.')) {
+ $val[0] = $via . '.' . $val[0];
+ }
+ }
+ }
+ }
+
+ $this->bind($query->getBind(false));
+ }
+
+ /**
+ * 指定OR查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function whereOr($field, $op = null, $condition = null)
+ {
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('OR', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 指定XOR查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function whereXor($field, $op = null, $condition = null)
+ {
+ $param = func_get_args();
+ array_shift($param);
+ return $this->parseWhereExp('XOR', $field, $op, $condition, $param);
+ }
+
+ /**
+ * 指定Null查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNull(string $field, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NULL', null, [], true);
+ }
+
+ /**
+ * 指定NotNull查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotNull(string $field, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOTNULL', null, [], true);
+ }
+
+ /**
+ * 指定Exists查询条件
+ * @access public
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereExists($condition, string $logic = 'AND')
+ {
+ if (is_string($condition)) {
+ $condition = new Raw($condition);
+ }
+
+ $this->options['where'][strtoupper($logic)][] = ['', 'EXISTS', $condition];
+ return $this;
+ }
+
+ /**
+ * 指定NotExists查询条件
+ * @access public
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotExists($condition, string $logic = 'AND')
+ {
+ if (is_string($condition)) {
+ $condition = new Raw($condition);
+ }
+
+ $this->options['where'][strtoupper($logic)][] = ['', 'NOT EXISTS', $condition];
+ return $this;
+ }
+
+ /**
+ * 指定In查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereIn(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'IN', $condition, [], true);
+ }
+
+ /**
+ * 指定NotIn查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotIn(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT IN', $condition, [], true);
+ }
+
+ /**
+ * 指定Like查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereLike(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'LIKE', $condition, [], true);
+ }
+
+ /**
+ * 指定NotLike查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotLike(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT LIKE', $condition, [], true);
+ }
+
+ /**
+ * 指定Between查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereBetween(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'BETWEEN', $condition, [], true);
+ }
+
+ /**
+ * 指定NotBetween查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereNotBetween(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'NOT BETWEEN', $condition, [], true);
+ }
+
+ /**
+ * 指定FIND_IN_SET查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param mixed $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereFindInSet(string $field, $condition, string $logic = 'AND')
+ {
+ return $this->parseWhereExp($logic, $field, 'FIND IN SET', $condition, [], true);
+ }
+
+ /**
+ * 比较两个字段
+ * @access public
+ * @param string $field1 查询字段
+ * @param string $operator 比较操作符
+ * @param string $field2 比较字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereColumn(string $field1, string $operator, string $field2 = null, string $logic = 'AND')
+ {
+ if (is_null($field2)) {
+ $field2 = $operator;
+ $operator = '=';
+ }
+
+ return $this->parseWhereExp($logic, $field1, 'COLUMN', [$operator, $field2], [], true);
+ }
+
+ /**
+ * 设置软删除字段及条件
+ * @access public
+ * @param string $field 查询字段
+ * @param mixed $condition 查询条件
+ * @return $this
+ */
+ public function useSoftDelete(string $field, $condition = null)
+ {
+ if ($field) {
+ $this->options['soft_delete'] = [$field, $condition];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定Exp查询条件
+ * @access public
+ * @param mixed $field 查询字段
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereExp(string $field, string $where, array $bind = [], string $logic = 'AND')
+ {
+ $this->options['where'][$logic][] = [$field, 'EXP', new Raw($where, $bind)];
+
+ return $this;
+ }
+
+ /**
+ * 指定字段Raw查询
+ * @access public
+ * @param string $field 查询字段表达式
+ * @param mixed $op 查询表达式
+ * @param string $condition 查询条件
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereFieldRaw(string $field, $op, $condition = null, string $logic = 'AND')
+ {
+ if (is_null($condition)) {
+ $condition = $op;
+ $op = '=';
+ }
+
+ $this->options['where'][$logic][] = [new Raw($field), $op, $condition];
+ return $this;
+ }
+
+ /**
+ * 指定表达式查询条件
+ * @access public
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function whereRaw(string $where, array $bind = [], string $logic = 'AND')
+ {
+ $this->options['where'][$logic][] = new Raw($where, $bind);
+
+ return $this;
+ }
+
+ /**
+ * 指定表达式查询条件 OR
+ * @access public
+ * @param string $where 查询条件
+ * @param array $bind 参数绑定
+ * @return $this
+ */
+ public function whereOrRaw(string $where, array $bind = [])
+ {
+ return $this->whereRaw($where, $bind, 'OR');
+ }
+
+ /**
+ * 分析查询表达式
+ * @access protected
+ * @param string $logic 查询逻辑 and or xor
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @param array $param 查询参数
+ * @param bool $strict 严格模式
+ * @return $this
+ */
+ protected function parseWhereExp(string $logic, $field, $op, $condition, array $param = [], bool $strict = false)
+ {
+ $logic = strtoupper($logic);
+
+ if (is_string($field) && !empty($this->options['via']) && false === strpos($field, '.')) {
+ $field = $this->options['via'] . '.' . $field;
+ }
+
+ if ($field instanceof Raw) {
+ return $this->whereRaw($field, is_array($op) ? $op : [], $logic);
+ } elseif ($strict) {
+ // 使用严格模式查询
+ if ('=' == $op) {
+ $where = $this->whereEq($field, $condition);
+ } else {
+ $where = [$field, $op, $condition, $logic];
+ }
+ } elseif (is_array($field)) {
+ // 解析数组批量查询
+ return $this->parseArrayWhereItems($field, $logic);
+ } elseif ($field instanceof Closure) {
+ $where = $field;
+ } elseif (is_string($field)) {
+ if (preg_match('/[,=\<\'\"\(\s]/', $field)) {
+ return $this->whereRaw($field, is_array($op) ? $op : [], $logic);
+ } elseif (is_string($op) && strtolower($op) == 'exp') {
+ $bind = isset($param[2]) && is_array($param[2]) ? $param[2] : [];
+ return $this->whereExp($field, $condition, $bind, $logic);
+ }
+
+ $where = $this->parseWhereItem($logic, $field, $op, $condition, $param);
+ }
+
+ if (!empty($where)) {
+ $this->options['where'][$logic][] = $where;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 分析查询表达式
+ * @access protected
+ * @param string $logic 查询逻辑 and or xor
+ * @param mixed $field 查询字段
+ * @param mixed $op 查询表达式
+ * @param mixed $condition 查询条件
+ * @param array $param 查询参数
+ * @return array
+ */
+ protected function parseWhereItem(string $logic, $field, $op, $condition, array $param = []): array
+ {
+ if (is_array($op)) {
+ // 同一字段多条件查询
+ array_unshift($param, $field);
+ $where = $param;
+ } elseif ($field && is_null($condition)) {
+ if (is_string($op) && in_array(strtoupper($op), ['NULL', 'NOTNULL', 'NOT NULL'], true)) {
+ // null查询
+ $where = [$field, $op, ''];
+ } elseif ('=' === $op || is_null($op)) {
+ $where = [$field, 'NULL', ''];
+ } elseif ('<>' === $op) {
+ $where = [$field, 'NOTNULL', ''];
+ } else {
+ // 字段相等查询
+ $where = $this->whereEq($field, $op);
+ }
+ } elseif (is_string($op) && in_array(strtoupper($op), ['EXISTS', 'NOT EXISTS', 'NOTEXISTS'], true)) {
+ $where = [$field, $op, is_string($condition) ? new Raw($condition) : $condition];
+ } else {
+ $where = $field ? [$field, $op, $condition, $param[2] ?? null] : [];
+ }
+
+ return $where;
+ }
+
+ /**
+ * 相等查询的主键处理
+ * @access protected
+ * @param string $field 字段名
+ * @param mixed $value 字段值
+ * @return array
+ */
+ protected function whereEq(string $field, $value): array
+ {
+ if ($this->getPk() == $field) {
+ $this->options['key'] = $value;
+ }
+
+ return [$field, '=', $value];
+ }
+
+ /**
+ * 数组批量查询
+ * @access protected
+ * @param array $field 批量查询
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ protected function parseArrayWhereItems(array $field, string $logic)
+ {
+ if (key($field) !== 0) {
+ $where = [];
+ foreach ($field as $key => $val) {
+ if ($val instanceof Raw) {
+ $where[] = [$key, 'exp', $val];
+ } else {
+ $where[] = is_null($val) ? [$key, 'NULL', ''] : [$key, is_array($val) ? 'IN' : '=', $val];
+ }
+ }
+ } else {
+ // 数组批量查询
+ $where = $field;
+ }
+
+ if (!empty($where)) {
+ $this->options['where'][$logic] = isset($this->options['where'][$logic]) ?
+ array_merge($this->options['where'][$logic], $where) : $where;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 去除某个查询条件
+ * @access public
+ * @param string $field 查询字段
+ * @param string $logic 查询逻辑 and or xor
+ * @return $this
+ */
+ public function removeWhereField(string $field, string $logic = 'AND')
+ {
+ $logic = strtoupper($logic);
+
+ if (isset($this->options['where'][$logic])) {
+ foreach ($this->options['where'][$logic] as $key => $val) {
+ if (is_array($val) && $val[0] == $field) {
+ unset($this->options['where'][$logic][$key]);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 条件查询
+ * @access public
+ * @param mixed $condition 满足条件(支持闭包)
+ * @param Closure|array $query 满足条件后执行的查询表达式(闭包或数组)
+ * @param Closure|array $otherwise 不满足条件后执行
+ * @return $this
+ */
+ public function when($condition, $query, $otherwise = null)
+ {
+ if ($condition instanceof Closure) {
+ $condition = $condition($this);
+ }
+
+ if ($condition) {
+ if ($query instanceof Closure) {
+ $query($this, $condition);
+ } elseif (is_array($query)) {
+ $this->where($query);
+ }
+ } elseif ($otherwise) {
+ if ($otherwise instanceof Closure) {
+ $otherwise($this, $condition);
+ } elseif (is_array($otherwise)) {
+ $this->where($otherwise);
+ }
+ }
+
+ return $this;
+ }
+}
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..d061e83
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Mongo.php
@@ -0,0 +1,1167 @@
+
+// +----------------------------------------------------------------------
+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\DbException as Exception;
+use think\db\Mongo as Query;
+
+/**
+ * Mongo数据库驱动
+ */
+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
+ {
+ $resultSet = $this->db->trigger('before_select', $query);
+
+ if (!$resultSet) {
+ $resultSet = $this->mongoQuery($query, function ($query) {
+ return $this->builder->select($query);
+ });
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 查找单条记录
+ * @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
+ {
+ // 事件回调
+ $result = $this->db->trigger('before_find', $query);
+
+ if (!$result) {
+ // 执行查询
+ $resultSet = $this->mongoQuery($query, function ($query) {
+ return $this->builder->select($query, true);
+ });
+
+ $result = $resultSet[0] ?? [];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 得到某个字段的值
+ * @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 string $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, string $field, string $key = ''): array
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['projection'])) {
+ $query->removeOption('projection');
+ }
+
+ 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..4274429
--- /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)
+ {
+ $this->initConnect(true);
+ $this->linkID->exec("XA START '$xid'");
+ }
+
+ /**
+ * 预编译XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function prepareXa(string $xid)
+ {
+ $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)
+ {
+ $this->initConnect(true);
+ $this->linkID->exec("XA COMMIT '$xid'");
+ }
+
+ /**
+ * 回滚XA事务
+ * @access public
+ * @param string $xid XA事务id
+ * @return void
+ */
+ public function rollbackXa(string $xid)
+ {
+ $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..7f54ba0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Oracle.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+
+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 = '" . strtoupper($tableName) . "' ) b where table_name = '" . strtoupper($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 $sequence 自增序列名
+ * @return mixed
+ */
+ public function getLastInsID(BaseQuery $query, string $sequence = null)
+ {
+ $pdo = $this->linkID->query("select {$sequence}.currval as id from dual");
+ $result = $pdo->fetchColumn();
+
+ return $result;
+ }
+
+ protected function supportSavepoint(): bool
+ {
+ return true;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/connector/Pgsql.php b/vendor/topthink/think-orm/src/db/connector/Pgsql.php
new file mode 100644
index 0000000..da5e725
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Pgsql.php
@@ -0,0 +1,108 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\connector;
+
+use PDO;
+use think\db\PDOConnection;
+
+/**
+ * Pgsql数据库驱动
+ */
+class Pgsql extends PDOConnection
+{
+
+ /**
+ * 默认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连接的dsn信息
+ * @access protected
+ * @param array $config 连接信息
+ * @return string
+ */
+ protected function parseDsn(array $config): string
+ {
+ $dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname'];
+
+ if (!empty($config['hostport'])) {
+ $dsn .= ';port=' . $config['hostport'];
+ }
+
+ return $dsn;
+ }
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName
+ * @return array
+ */
+ public function getFields(string $tableName): array
+ {
+ [$tableName] = explode(' ', $tableName);
+ $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $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' => (bool) ('' !== $val['null']),
+ 'default' => $val['default'],
+ 'primary' => !empty($val['key']),
+ 'autoinc' => (0 === strpos($val['extra'], 'nextval(')),
+ ];
+ }
+ }
+
+ return $this->fieldCase($info);
+ }
+
+ /**
+ * 取得数据库的表信息
+ * @access public
+ * @param string $dbName
+ * @return array
+ */
+ public function getTables(string $dbName = ''): array
+ {
+ $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'";
+ $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;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/connector/Sqlite.php b/vendor/topthink/think-orm/src/db/connector/Sqlite.php
new file mode 100644
index 0000000..c19c9f0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Sqlite.php
@@ -0,0 +1,96 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\connector;
+
+use PDO;
+use think\db\PDOConnection;
+
+/**
+ * Sqlite数据库驱动
+ */
+class Sqlite extends PDOConnection
+{
+
+ /**
+ * 解析pdo连接的dsn信息
+ * @access protected
+ * @param array $config 连接信息
+ * @return string
+ */
+ protected function parseDsn(array $config): string
+ {
+ $dsn = 'sqlite:' . $config['database'];
+
+ return $dsn;
+ }
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName
+ * @return array
+ */
+ public function getFields(string $tableName): array
+ {
+ [$tableName] = explode(' ', $tableName);
+ $sql = 'PRAGMA table_info( ' . $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['name']] = [
+ 'name' => $val['name'],
+ 'type' => $val['type'],
+ 'notnull' => 1 === $val['notnull'],
+ 'default' => $val['dflt_value'],
+ 'primary' => '1' == $val['pk'],
+ 'autoinc' => '1' == $val['pk'],
+ ];
+ }
+ }
+
+ return $this->fieldCase($info);
+ }
+
+ /**
+ * 取得数据库的表信息
+ * @access public
+ * @param string $dbName
+ * @return array
+ */
+ public function getTables(string $dbName = ''): array
+ {
+ $sql = "SELECT name FROM sqlite_master WHERE type='table' "
+ . "UNION ALL SELECT name FROM sqlite_temp_master "
+ . "WHERE type='table' ORDER BY name";
+
+ $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;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/connector/Sqlsrv.php b/vendor/topthink/think-orm/src/db/connector/Sqlsrv.php
new file mode 100644
index 0000000..fa4caa0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Sqlsrv.php
@@ -0,0 +1,122 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\connector;
+
+use PDO;
+use think\db\PDOConnection;
+
+/**
+ * Sqlsrv数据库驱动
+ */
+class Sqlsrv extends PDOConnection
+{
+ /**
+ * 默认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连接的dsn信息
+ * @access protected
+ * @param array $config 连接信息
+ * @return string
+ */
+ protected function parseDsn(array $config): string
+ {
+ $dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname'];
+
+ if (!empty($config['hostport'])) {
+ $dsn .= ',' . $config['hostport'];
+ }
+
+ return $dsn;
+ }
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName
+ * @return array
+ */
+ public function getFields(string $tableName): array
+ {
+ [$tableName] = explode(' ', $tableName);
+
+ $sql = "SELECT column_name, data_type, column_default, is_nullable
+ FROM information_schema.tables AS t
+ JOIN information_schema.columns AS c
+ ON t.table_catalog = c.table_catalog
+ AND t.table_schema = c.table_schema
+ AND t.table_name = c.table_name
+ WHERE t.table_name = '$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['column_name']] = [
+ 'name' => $val['column_name'],
+ 'type' => $val['data_type'],
+ 'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes
+ 'default' => $val['column_default'],
+ 'primary' => false,
+ 'autoinc' => false,
+ ];
+ }
+ }
+
+ $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'";
+ $pdo = $this->linkID->query($sql);
+ $result = $pdo->fetch(PDO::FETCH_ASSOC);
+
+ if ($result) {
+ $info[$result['column_name']]['primary'] = true;
+ }
+
+ return $this->fieldCase($info);
+ }
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $dbName
+ * @return array
+ */
+ public function getTables(string $dbName = ''): array
+ {
+ $sql = "SELECT TABLE_NAME
+ FROM INFORMATION_SCHEMA.TABLES
+ WHERE TABLE_TYPE = 'BASE TABLE'
+ ";
+
+ $pdo = $this->getPDOStatement($sql);
+ $result = $pdo->fetchAll(PDO::FETCH_ASSOC);
+ $info = [];
+
+ foreach ($result as $key => $val) {
+ $info[$key] = current($val);
+ }
+
+ return $info;
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/connector/pgsql.sql b/vendor/topthink/think-orm/src/db/connector/pgsql.sql
new file mode 100644
index 0000000..f940052
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/pgsql.sql
@@ -0,0 +1,117 @@
+CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS
+$BODY$
+DECLARE
+ v_type varchar;
+BEGIN
+ IF a_type='int8' THEN
+ v_type:='bigint';
+ ELSIF a_type='int4' THEN
+ v_type:='integer';
+ ELSIF a_type='int2' THEN
+ v_type:='smallint';
+ ELSIF a_type='bpchar' THEN
+ v_type:='char';
+ ELSE
+ v_type:=a_type;
+ END IF;
+ RETURN v_type;
+END;
+$BODY$
+LANGUAGE PLPGSQL;
+
+CREATE TYPE "public"."tablestruct" AS (
+ "fields_key_name" varchar(100),
+ "fields_name" VARCHAR(200),
+ "fields_type" VARCHAR(20),
+ "fields_length" BIGINT,
+ "fields_not_null" VARCHAR(10),
+ "fields_default" VARCHAR(500),
+ "fields_comment" VARCHAR(1000)
+);
+
+CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
+$body$
+DECLARE
+ v_ret tablestruct;
+ v_oid oid;
+ v_sql varchar;
+ v_rec RECORD;
+ v_key varchar;
+BEGIN
+ SELECT
+ pg_class.oid INTO v_oid
+ FROM
+ pg_class
+ INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name)
+ WHERE
+ pg_class.relname=a_table_name;
+ IF NOT FOUND THEN
+ RETURN;
+ END IF;
+
+ v_sql='
+ SELECT
+ pg_attribute.attname AS fields_name,
+ pg_attribute.attnum AS fields_index,
+ pgsql_type(pg_type.typname::varchar) AS fields_type,
+ pg_attribute.atttypmod-4 as fields_length,
+ CASE WHEN pg_attribute.attnotnull THEN ''not null''
+ ELSE ''''
+ END AS fields_not_null,
+ pg_attrdef.adsrc AS fields_default,
+ pg_description.description AS fields_comment
+ FROM
+ pg_attribute
+ INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid
+ INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid
+ LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum
+ LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum
+ WHERE
+ pg_attribute.attnum > 0
+ AND attisdropped <> ''t''
+ AND pg_class.oid = ' || v_oid || '
+ ORDER BY pg_attribute.attnum' ;
+
+ FOR v_rec IN EXECUTE v_sql LOOP
+ v_ret.fields_name=v_rec.fields_name;
+ v_ret.fields_type=v_rec.fields_type;
+ IF v_rec.fields_length > 0 THEN
+ v_ret.fields_length:=v_rec.fields_length;
+ ELSE
+ v_ret.fields_length:=NULL;
+ END IF;
+ v_ret.fields_not_null=v_rec.fields_not_null;
+ v_ret.fields_default=v_rec.fields_default;
+ v_ret.fields_comment=v_rec.fields_comment;
+ SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name;
+ IF FOUND THEN
+ v_ret.fields_key_name=v_key;
+ ELSE
+ v_ret.fields_key_name='';
+ END IF;
+ RETURN NEXT v_ret;
+ END LOOP;
+ RETURN ;
+END;
+$body$
+LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
+
+COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar)
+IS '获得表信息';
+
+---重载一个函数
+CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS
+$body$
+DECLARE
+ v_ret tablestruct;
+BEGIN
+ FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP
+ RETURN NEXT v_ret;
+ END LOOP;
+ RETURN;
+END;
+$body$
+LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER;
+
+COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar)
+IS '获得表信息';
\ No newline at end of file
diff --git a/vendor/topthink/think-orm/src/db/exception/BindParamException.php b/vendor/topthink/think-orm/src/db/exception/BindParamException.php
new file mode 100644
index 0000000..b921566
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/BindParamException.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+/**
+ * PDO参数绑定异常
+ */
+class BindParamException extends DbException
+{
+
+ /**
+ * BindParamException constructor.
+ * @access public
+ * @param string $message
+ * @param array $config
+ * @param string $sql
+ * @param array $bind
+ * @param int $code
+ */
+ public function __construct(string $message, array $config, string $sql, array $bind, int $code = 10502)
+ {
+ $this->setData('Bind Param', $bind);
+ parent::__construct($message, $config, $sql, $code);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/DataNotFoundException.php b/vendor/topthink/think-orm/src/db/exception/DataNotFoundException.php
new file mode 100644
index 0000000..a55b795
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/DataNotFoundException.php
@@ -0,0 +1,43 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+class DataNotFoundException extends DbException
+{
+ protected $table;
+
+ /**
+ * DbException constructor.
+ * @access public
+ * @param string $message
+ * @param string $table
+ * @param array $config
+ */
+ public function __construct(string $message, string $table = '', array $config = [])
+ {
+ $this->message = $message;
+ $this->table = $table;
+
+ $this->setData('Database Config', $config);
+ }
+
+ /**
+ * 获取数据表名
+ * @access public
+ * @return string
+ */
+ public function getTable()
+ {
+ return $this->table;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/DbException.php b/vendor/topthink/think-orm/src/db/exception/DbException.php
new file mode 100644
index 0000000..42a7079
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/DbException.php
@@ -0,0 +1,81 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+use Exception;
+
+/**
+ * Database相关异常处理类
+ */
+class DbException extends Exception
+{
+ /**
+ * DbException constructor.
+ * @access public
+ * @param string $message
+ * @param array $config
+ * @param string $sql
+ * @param int $code
+ */
+ public function __construct(string $message, array $config = [], string $sql = '', int $code = 10500)
+ {
+ $this->message = $message;
+ $this->code = $code;
+
+ $this->setData('Database Status', [
+ 'Error Code' => $code,
+ 'Error Message' => $message,
+ 'Error SQL' => $sql,
+ ]);
+
+ unset($config['username'], $config['password']);
+ $this->setData('Database Config', $config);
+ }
+
+ /**
+ * 保存异常页面显示的额外Debug数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 设置异常额外的Debug数据
+ * 数据将会显示为下面的格式
+ *
+ * Exception Data
+ * --------------------------------------------------
+ * Label 1
+ * key1 value1
+ * key2 value2
+ * Label 2
+ * key1 value1
+ * key2 value2
+ *
+ * @param string $label 数据分类,用于异常页面显示
+ * @param array $data 需要显示的数据,必须为关联数组
+ */
+ final protected function setData($label, array $data)
+ {
+ $this->data[$label] = $data;
+ }
+
+ /**
+ * 获取异常额外Debug数据
+ * 主要用于输出到异常页面便于调试
+ * @return array 由setData设置的Debug数据
+ */
+ final public function getData()
+ {
+ return $this->data;
+ }
+}
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..ae2a531
--- /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..405f889
--- /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..4e78393
--- /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..eb83c66
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/PDOException.php
@@ -0,0 +1,41 @@
+
+// +----------------------------------------------------------------------
+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;
+
+ $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($exception->getMessage(), $config, $sql, $code);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/facade/Db.php b/vendor/topthink/think-orm/src/facade/Db.php
new file mode 100644
index 0000000..8e38bfa
--- /dev/null
+++ b/vendor/topthink/think-orm/src/facade/Db.php
@@ -0,0 +1,86 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+if (class_exists('think\Facade')) {
+ class Facade extends \think\Facade
+ {}
+} else {
+ 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);
+ }
+ }
+}
+
+/**
+ * @see \think\DbManager
+ * @mixin \think\DbManager
+ */
+class Db extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'think\DbManager';
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/Collection.php b/vendor/topthink/think-orm/src/model/Collection.php
new file mode 100644
index 0000000..03c4aa0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/Collection.php
@@ -0,0 +1,250 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model;
+
+use think\Collection as BaseCollection;
+use think\Model;
+use think\Paginator;
+
+/**
+ * 模型数据集类
+ */
+class Collection extends BaseCollection
+{
+ /**
+ * 延迟预载入关联查询
+ * @access public
+ * @param array|string $relation 关联
+ * @param mixed $cache 关联缓存
+ * @return $this
+ */
+ public function load($relation, $cache = false)
+ {
+ if (!$this->isEmpty()) {
+ $item = current($this->items);
+ $item->eagerlyResultSet($this->items, (array) $relation, [], false, $cache);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 删除数据集的数据
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ $this->each(function (Model $model) {
+ $model->delete();
+ });
+
+ return true;
+ }
+
+ /**
+ * 更新数据
+ * @access public
+ * @param array $data 数据数组
+ * @param array $allowField 允许字段
+ * @return bool
+ */
+ public function update(array $data, array $allowField = []): bool
+ {
+ $this->each(function (Model $model) use ($data, $allowField) {
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ $model->save($data);
+ });
+
+ return true;
+ }
+
+ /**
+ * 设置需要隐藏的输出属性
+ * @access public
+ * @param array $hidden 属性列表
+ * @return $this
+ */
+ public function hidden(array $hidden)
+ {
+ $this->each(function (Model $model) use ($hidden) {
+ $model->hidden($hidden);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置需要输出的属性
+ * @access public
+ * @param array $visible
+ * @return $this
+ */
+ public function visible(array $visible)
+ {
+ $this->each(function (Model $model) use ($visible) {
+ $model->visible($visible);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置需要追加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @return $this
+ */
+ public function append(array $append)
+ {
+ $this->each(function (Model $model) use ($append) {
+ $model->append($append);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置父模型
+ * @access public
+ * @param Model $parent 父模型
+ * @return $this
+ */
+ public function setParent(Model $parent)
+ {
+ $this->each(function (Model $model) use ($parent) {
+ $model->setParent($parent);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置数据字段获取器
+ * @access public
+ * @param string|array $name 字段名
+ * @param callable $callback 闭包获取器
+ * @return $this
+ */
+ public function withAttr($name, $callback = null)
+ {
+ $this->each(function (Model $model) use ($name, $callback) {
+ $model->withAttribute($name, $callback);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 绑定(一对一)关联属性到当前模型
+ * @access protected
+ * @param string $relation 关联名称
+ * @param array $attrs 绑定属性
+ * @return $this
+ * @throws Exception
+ */
+ public function bindAttr(string $relation, array $attrs = [])
+ {
+ $this->each(function (Model $model) use ($relation, $attrs) {
+ $model->bindAttr($relation, $attrs);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 按指定键整理数据
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 键名
+ * @return array
+ */
+ public function dictionary($items = null, string &$indexKey = null)
+ {
+ if ($items instanceof self || $items instanceof Paginator) {
+ $items = $items->all();
+ }
+
+ $items = is_null($items) ? $this->items : $items;
+
+ if ($items && empty($indexKey)) {
+ $indexKey = $items[0]->getPk();
+ }
+
+ if (isset($indexKey) && is_string($indexKey)) {
+ return array_column($items, null, $indexKey);
+ }
+
+ return $items;
+ }
+
+ /**
+ * 比较数据集,返回差集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 指定比较的键名
+ * @return static
+ */
+ public function diff($items, string $indexKey = null)
+ {
+ if ($this->isEmpty()) {
+ return new static($items);
+ }
+
+ $diff = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (!isset($dictionary[$item[$indexKey]])) {
+ $diff[] = $item;
+ }
+ }
+ }
+
+ return new static($diff);
+ }
+
+ /**
+ * 比较数据集,返回交集
+ *
+ * @access public
+ * @param mixed $items 数据
+ * @param string $indexKey 指定比较的键名
+ * @return static
+ */
+ public function intersect($items, string $indexKey = null)
+ {
+ if ($this->isEmpty()) {
+ return new static([]);
+ }
+
+ $intersect = [];
+ $dictionary = $this->dictionary($items, $indexKey);
+
+ if (is_string($indexKey)) {
+ foreach ($this->items as $item) {
+ if (isset($dictionary[$item[$indexKey]])) {
+ $intersect[] = $item;
+ }
+ }
+ }
+
+ return new static($intersect);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/Pivot.php b/vendor/topthink/think-orm/src/model/Pivot.php
new file mode 100644
index 0000000..b29ec47
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/Pivot.php
@@ -0,0 +1,53 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model;
+
+use think\Model;
+
+/**
+ * 多对多中间表模型类
+ */
+class Pivot extends Model
+{
+
+ /**
+ * 父模型
+ * @var Model
+ */
+ public $parent;
+
+ /**
+ * 是否时间自动写入
+ * @var bool
+ */
+ protected $autoWriteTimestamp = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $data 数据
+ * @param Model $parent 上级模型
+ * @param string $table 中间数据表名
+ */
+ public function __construct(array $data = [], Model $parent = null, string $table = '')
+ {
+ $this->parent = $parent;
+
+ if (is_null($this->name)) {
+ $this->name = $table;
+ }
+
+ parent::__construct($data);
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/model/Relation.php b/vendor/topthink/think-orm/src/model/Relation.php
new file mode 100644
index 0000000..ea1e2b9
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/Relation.php
@@ -0,0 +1,258 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model;
+
+use Closure;
+use ReflectionFunction;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\Model;
+
+/**
+ * 模型关联基础类
+ * @package think\model
+ * @mixin Query
+ */
+abstract class Relation
+{
+ /**
+ * 父模型对象
+ * @var Model
+ */
+ protected $parent;
+
+ /**
+ * 当前关联的模型类名
+ * @var string
+ */
+ protected $model;
+
+ /**
+ * 关联模型查询对象
+ * @var Query
+ */
+ protected $query;
+
+ /**
+ * 关联表外键
+ * @var string
+ */
+ protected $foreignKey;
+
+ /**
+ * 关联表主键
+ * @var string
+ */
+ protected $localKey;
+
+ /**
+ * 是否执行关联基础查询
+ * @var bool
+ */
+ protected $baseQuery;
+
+ /**
+ * 是否为自关联
+ * @var bool
+ */
+ protected $selfRelation = false;
+
+ /**
+ * 关联数据数量限制
+ * @var int
+ */
+ protected $withLimit;
+
+ /**
+ * 关联数据字段限制
+ * @var array
+ */
+ protected $withField;
+
+ /**
+ * 获取关联的所属模型
+ * @access public
+ * @return Model
+ */
+ public function getParent(): Model
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取当前的关联模型类的Query实例
+ * @access public
+ * @return Query
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @access public
+ * @return Model
+ */
+ public function getModel(): Model
+ {
+ return $this->query->getModel();
+ }
+
+ /**
+ * 当前关联是否为自关联
+ * @access public
+ * @return bool
+ */
+ public function isSelfRelation(): bool
+ {
+ return $this->selfRelation;
+ }
+
+ /**
+ * 封装关联数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @param Model $parent 父模型
+ * @return mixed
+ */
+ protected function resultSetBuild(array $resultSet, Model $parent = null)
+ {
+ return (new $this->model)->toCollection($resultSet)->setParent($parent);
+ }
+
+ protected function getQueryFields(string $model)
+ {
+ $fields = $this->query->getOptions('field');
+ return $this->getRelationQueryFields($fields, $model);
+ }
+
+ protected function getRelationQueryFields($fields, string $model)
+ {
+ if (empty($fields) || '*' == $fields) {
+ return $model . '.*';
+ }
+
+ if (is_string($fields)) {
+ $fields = explode(',', $fields);
+ }
+
+ foreach ($fields as &$field) {
+ if (false === strpos($field, '.')) {
+ $field = $model . '.' . $field;
+ }
+ }
+
+ return $fields;
+ }
+
+ protected function getQueryWhere(array &$where, string $relation): void
+ {
+ foreach ($where as $key => &$val) {
+ if (is_string($key)) {
+ $where[] = [false === strpos($key, '.') ? $relation . '.' . $key : $key, '=', $val];
+ unset($where[$key]);
+ } elseif (isset($val[0]) && false === strpos($val[0], '.')) {
+ $val[0] = $relation . '.' . $val[0];
+ }
+ }
+ }
+
+ /**
+ * 更新数据
+ * @access public
+ * @param array $data 更新数据
+ * @return integer
+ */
+ public function update(array $data = []): int
+ {
+ return $this->query->update($data);
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 表达式 true 表示强制删除
+ * @return int
+ * @throws Exception
+ * @throws PDOException
+ */
+ public function delete($data = null): int
+ {
+ return $this->query->delete($data);
+ }
+
+ /**
+ * 限制关联数据的数量
+ * @access public
+ * @param int $limit 关联数量限制
+ * @return $this
+ */
+ public function withLimit(int $limit)
+ {
+ $this->withLimit = $limit;
+ return $this;
+ }
+
+ /**
+ * 限制关联数据的字段
+ * @access public
+ * @param array $field 关联字段限制
+ * @return $this
+ */
+ public function withField(array $field)
+ {
+ $this->withField = $field;
+ return $this;
+ }
+
+ /**
+ * 判断闭包的参数类型
+ * @access protected
+ * @return mixed
+ */
+ protected function getClosureType(Closure $closure)
+ {
+ $reflect = new ReflectionFunction($closure);
+ $params = $reflect->getParameters();
+
+ if (!empty($params)) {
+ $type = $params[0]->getType();
+ return is_null($type) || Relation::class == $type->getName() ? $this : $this->query;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery(): void
+ {}
+
+ public function __call($method, $args)
+ {
+ if ($this->query) {
+ // 执行基础查询
+ $this->baseQuery();
+
+ $result = call_user_func_array([$this->query, $method], $args);
+
+ return $result === $this->query ? $this : $result;
+ }
+
+ throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/Attribute.php b/vendor/topthink/think-orm/src/model/concern/Attribute.php
new file mode 100644
index 0000000..979086c
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/Attribute.php
@@ -0,0 +1,651 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use InvalidArgumentException;
+use think\db\Raw;
+use think\helper\Str;
+use think\model\Relation;
+
+/**
+ * 模型数据处理
+ */
+trait Attribute
+{
+ /**
+ * 数据表主键 复合主键使用数组定义
+ * @var string|array
+ */
+ protected $pk = 'id';
+
+ /**
+ * 数据表字段信息 留空则自动获取
+ * @var array
+ */
+ protected $schema = [];
+
+ /**
+ * 当前允许写入的字段
+ * @var array
+ */
+ protected $field = [];
+
+ /**
+ * 字段自动类型转换
+ * @var array
+ */
+ protected $type = [];
+
+ /**
+ * 数据表废弃字段
+ * @var array
+ */
+ protected $disuse = [];
+
+ /**
+ * 数据表只读字段
+ * @var array
+ */
+ protected $readonly = [];
+
+ /**
+ * 当前模型数据
+ * @var array
+ */
+ private $data = [];
+
+ /**
+ * 原始数据
+ * @var array
+ */
+ private $origin = [];
+
+ /**
+ * JSON数据表字段
+ * @var array
+ */
+ protected $json = [];
+
+ /**
+ * JSON数据表字段类型
+ * @var array
+ */
+ protected $jsonType = [];
+
+ /**
+ * JSON数据取出是否需要转换为数组
+ * @var bool
+ */
+ protected $jsonAssoc = false;
+
+ /**
+ * 是否严格字段大小写
+ * @var bool
+ */
+ protected $strict = true;
+
+ /**
+ * 修改器执行记录
+ * @var array
+ */
+ private $set = [];
+
+ /**
+ * 动态获取器
+ * @var array
+ */
+ private $withAttr = [];
+
+ /**
+ * 获取模型对象的主键
+ * @access public
+ * @return string|array
+ */
+ public function getPk()
+ {
+ return $this->pk;
+ }
+
+ /**
+ * 判断一个字段名是否为主键字段
+ * @access public
+ * @param string $key 名称
+ * @return bool
+ */
+ protected function isPk(string $key): bool
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && $pk == $key) {
+ return true;
+ } elseif (is_array($pk) && in_array($key, $pk)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取模型对象的主键值
+ * @access public
+ * @return mixed
+ */
+ public function getKey()
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && array_key_exists($pk, $this->data)) {
+ return $this->data[$pk];
+ }
+
+ return;
+ }
+
+ /**
+ * 设置允许写入的字段
+ * @access public
+ * @param array $field 允许写入的字段
+ * @return $this
+ */
+ public function allowField(array $field)
+ {
+ $this->field = $field;
+
+ return $this;
+ }
+
+ /**
+ * 设置只读字段
+ * @access public
+ * @param array $field 只读字段
+ * @return $this
+ */
+ public function readOnly(array $field)
+ {
+ $this->readonly = $field;
+
+ return $this;
+ }
+
+ /**
+ * 获取实际的字段名
+ * @access protected
+ * @param string $name 字段名
+ * @return string
+ */
+ protected function getRealFieldName(string $name): string
+ {
+ return $this->strict ? $name : Str::snake($name);
+ }
+
+ /**
+ * 设置数据对象值
+ * @access public
+ * @param array $data 数据
+ * @param bool $set 是否调用修改器
+ * @param array $allow 允许的字段名
+ * @return $this
+ */
+ public function data(array $data, bool $set = false, array $allow = [])
+ {
+ // 清空数据
+ $this->data = [];
+
+ // 废弃字段
+ foreach ($this->disuse as $key) {
+ if (array_key_exists($key, $data)) {
+ unset($data[$key]);
+ }
+ }
+
+ if (!empty($allow)) {
+ $result = [];
+ foreach ($allow as $name) {
+ if (isset($data[$name])) {
+ $result[$name] = $data[$name];
+ }
+ }
+ $data = $result;
+ }
+
+ if ($set) {
+ // 数据对象赋值
+ $this->setAttrs($data);
+ } else {
+ $this->data = $data;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 批量追加数据对象值
+ * @access public
+ * @param array $data 数据
+ * @param bool $set 是否需要进行数据处理
+ * @return $this
+ */
+ public function appendData(array $data, bool $set = false)
+ {
+ if ($set) {
+ $this->setAttrs($data);
+ } else {
+ $this->data = array_merge($this->data, $data);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取对象原始数据 如果不存在指定字段返回null
+ * @access public
+ * @param string $name 字段名 留空获取全部
+ * @return mixed
+ */
+ public function getOrigin(string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->origin;
+ }
+
+ return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
+ }
+
+ /**
+ * 获取对象原始数据 如果不存在指定字段返回false
+ * @access public
+ * @param string $name 字段名 留空获取全部
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ public function getData(string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->data;
+ }
+
+ $fieldName = $this->getRealFieldName($name);
+
+ if (array_key_exists($fieldName, $this->data)) {
+ return $this->data[$fieldName];
+ } elseif (array_key_exists($fieldName, $this->relation)) {
+ return $this->relation[$fieldName];
+ }
+
+ throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
+ }
+
+ /**
+ * 获取变化的数据 并排除只读数据
+ * @access public
+ * @return array
+ */
+ public function getChangedData(): array
+ {
+ $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
+ if ((empty($a) || empty($b)) && $a !== $b) {
+ return 1;
+ }
+
+ return is_object($a) || $a != $b ? 1 : 0;
+ });
+
+ // 只读字段不允许更新
+ foreach ($this->readonly as $key => $field) {
+ if (isset($data[$field])) {
+ unset($data[$field]);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 直接设置数据对象值
+ * @access public
+ * @param string $name 属性名
+ * @param mixed $value 值
+ * @return void
+ */
+ public function set(string $name, $value): void
+ {
+ $name = $this->getRealFieldName($name);
+
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * 通过修改器 批量设置数据对象值
+ * @access public
+ * @param array $data 数据
+ * @return void
+ */
+ public function setAttrs(array $data): void
+ {
+ // 进行数据处理
+ foreach ($data as $key => $value) {
+ $this->setAttr($key, $value, $data);
+ }
+ }
+
+ /**
+ * 通过修改器 设置数据对象值
+ * @access public
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
+ * @return void
+ */
+ public function setAttr(string $name, $value, array $data = []): void
+ {
+ $name = $this->getRealFieldName($name);
+
+ if (isset($this->set[$name])) {
+ return;
+ }
+
+ if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
+ // 自动写入的时间戳字段
+ $value = $this->autoWriteTimestamp();
+ } else {
+ // 检测修改器
+ $method = 'set' . Str::studly($name) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ $array = $this->data;
+
+ $value = $this->$method($value, array_merge($this->data, $data));
+
+ $this->set[$name] = true;
+ if (is_null($value) && $array !== $this->data) {
+ return;
+ }
+ } elseif (isset($this->type[$name])) {
+ // 类型转换
+ $value = $this->writeTransform($value, $this->type[$name]);
+ }
+ }
+
+ // 设置数据对象属性
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * 数据写入 类型转换
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
+ * @return mixed
+ */
+ protected function writeTransform($value, $type)
+ {
+ if (is_null($value)) {
+ return;
+ }
+
+ if ($value instanceof Raw) {
+ return $value;
+ }
+
+ if (is_array($type)) {
+ [$type, $param] = $type;
+ } elseif (strpos($type, ':')) {
+ [$type, $param] = explode(':', $type, 2);
+ }
+
+ switch ($type) {
+ case 'integer':
+ $value = (int) $value;
+ break;
+ case 'float':
+ if (empty($param)) {
+ $value = (float) $value;
+ } else {
+ $value = (float) number_format($value, (int) $param, '.', '');
+ }
+ break;
+ case 'boolean':
+ $value = (bool) $value;
+ break;
+ case 'timestamp':
+ if (!is_numeric($value)) {
+ $value = strtotime($value);
+ }
+ break;
+ case 'datetime':
+ $value = is_numeric($value) ? $value : strtotime($value);
+ $value = $this->formatDateTime('Y-m-d H:i:s.u', $value, true);
+ break;
+ case 'object':
+ if (is_object($value)) {
+ $value = json_encode($value, JSON_FORCE_OBJECT);
+ }
+ break;
+ case 'array':
+ $value = (array) $value;
+ case 'json':
+ $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
+ $value = json_encode($value, $option);
+ break;
+ case 'serialize':
+ $value = serialize($value);
+ break;
+ default:
+ if (is_object($value) && false !== strpos($type, '\\') && method_exists($value, '__toString')) {
+ // 对象类型
+ $value = $value->__toString();
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取器 获取数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ public function getAttr(string $name)
+ {
+ try {
+ $relation = false;
+ $value = $this->getData($name);
+ } catch (InvalidArgumentException $e) {
+ $relation = $this->isRelationAttr($name);
+ $value = null;
+ }
+
+ return $this->getValue($name, $value, $relation);
+ }
+
+ /**
+ * 获取经过获取器处理后的数据对象的值
+ * @access protected
+ * @param string $name 字段名称
+ * @param mixed $value 字段值
+ * @param bool|string $relation 是否为关联属性或者关联名
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ protected function getValue(string $name, $value, $relation = false)
+ {
+ // 检测属性获取器
+ $fieldName = $this->getRealFieldName($name);
+ $method = 'get' . Str::studly($name) . 'Attr';
+
+ if (isset($this->withAttr[$fieldName])) {
+ if ($relation) {
+ $value = $this->getRelationValue($relation);
+ }
+
+ if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
+ $value = $this->getJsonValue($fieldName, $value);
+ } else {
+ $closure = $this->withAttr[$fieldName];
+ $value = $closure($value, $this->data);
+ }
+ } elseif (method_exists($this, $method)) {
+ if ($relation) {
+ $value = $this->getRelationValue($relation);
+ }
+
+ $value = $this->$method($value, $this->data);
+ } elseif (isset($this->type[$fieldName])) {
+ // 类型转换
+ $value = $this->readTransform($value, $this->type[$fieldName]);
+ } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
+ $value = $this->getTimestampValue($value);
+ } elseif ($relation) {
+ $value = $this->getRelationValue($relation);
+ // 保存关联对象值
+ $this->relation[$name] = $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取JSON字段属性值
+ * @access protected
+ * @param string $name 属性名
+ * @param mixed $value JSON数据
+ * @return mixed
+ */
+ protected function getJsonValue($name, $value)
+ {
+ foreach ($this->withAttr[$name] as $key => $closure) {
+ if ($this->jsonAssoc) {
+ $value[$key] = $closure($value[$key], $value);
+ } else {
+ $value->$key = $closure($value->$key, $value);
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取关联属性值
+ * @access protected
+ * @param string $relation 关联名
+ * @return mixed
+ */
+ protected function getRelationValue(string $relation)
+ {
+ $modelRelation = $this->$relation();
+
+ return $modelRelation instanceof Relation ? $this->getRelationData($modelRelation) : null;
+ }
+
+ /**
+ * 数据读取 类型转换
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
+ * @return mixed
+ */
+ protected function readTransform($value, $type)
+ {
+ if (is_null($value)) {
+ return;
+ }
+
+ if (is_array($type)) {
+ [$type, $param] = $type;
+ } elseif (strpos($type, ':')) {
+ [$type, $param] = explode(':', $type, 2);
+ }
+
+ switch ($type) {
+ case 'integer':
+ $value = (int) $value;
+ break;
+ case 'float':
+ if (empty($param)) {
+ $value = (float) $value;
+ } else {
+ $value = (float) number_format($value, (int) $param, '.', '');
+ }
+ break;
+ case 'boolean':
+ $value = (bool) $value;
+ break;
+ case 'timestamp':
+ if (!is_null($value)) {
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = $this->formatDateTime($format, $value, true);
+ }
+ break;
+ case 'datetime':
+ if (!is_null($value)) {
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = $this->formatDateTime($format, $value);
+ }
+ break;
+ case 'json':
+ $value = json_decode($value, true);
+ break;
+ case 'array':
+ $value = empty($value) ? [] : json_decode($value, true);
+ break;
+ case 'object':
+ $value = empty($value) ? new \stdClass() : json_decode($value);
+ break;
+ case 'serialize':
+ try {
+ $value = unserialize($value);
+ } catch (\Exception $e) {
+ $value = null;
+ }
+ break;
+ default:
+ if (false !== strpos($type, '\\')) {
+ // 对象类型
+ $value = new $type($value);
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 设置数据字段获取器
+ * @access public
+ * @param string|array $name 字段名
+ * @param callable $callback 闭包获取器
+ * @return $this
+ */
+ public function withAttribute($name, callable $callback = null)
+ {
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $this->withAttribute($key, $val);
+ }
+ } else {
+ $name = $this->getRealFieldName($name);
+
+ if (strpos($name, '.')) {
+ [$name, $key] = explode('.', $name);
+
+ $this->withAttr[$name][$key] = $callback;
+ } else {
+ $this->withAttr[$name] = $callback;
+ }
+ }
+
+ return $this;
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/Conversion.php b/vendor/topthink/think-orm/src/model/concern/Conversion.php
new file mode 100644
index 0000000..d300628
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/Conversion.php
@@ -0,0 +1,278 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\Collection;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
+use think\Model;
+use think\model\Collection as ModelCollection;
+use think\model\relation\OneToOne;
+
+/**
+ * 模型数据转换处理
+ */
+trait Conversion
+{
+ /**
+ * 数据输出显示的属性
+ * @var array
+ */
+ protected $visible = [];
+
+ /**
+ * 数据输出隐藏的属性
+ * @var array
+ */
+ protected $hidden = [];
+
+ /**
+ * 数据输出需要追加的属性
+ * @var array
+ */
+ protected $append = [];
+
+ /**
+ * 数据集对象名
+ * @var string
+ */
+ protected $resultSetType;
+
+ /**
+ * 设置需要附加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ $this->append = $append;
+
+ return $this;
+ }
+
+ /**
+ * 设置附加关联对象的属性
+ * @access public
+ * @param string $attr 关联属性
+ * @param string|array $append 追加属性名
+ * @return $this
+ * @throws Exception
+ */
+ public function appendRelationAttr(string $attr, array $append)
+ {
+ $relation = Str::camel($attr);
+
+ if (isset($this->relation[$relation])) {
+ $model = $this->relation[$relation];
+ } else {
+ $model = $this->getRelationData($this->$relation());
+ }
+
+ if ($model instanceof Model) {
+ foreach ($append as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ if (isset($this->data[$key])) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $this->data[$key] = $model->$attr;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置需要隐藏的输出属性
+ * @access public
+ * @param array $hidden 属性列表
+ * @return $this
+ */
+ public function hidden(array $hidden = [])
+ {
+ $this->hidden = $hidden;
+
+ return $this;
+ }
+
+ /**
+ * 设置需要输出的属性
+ * @access public
+ * @param array $visible
+ * @return $this
+ */
+ public function visible(array $visible = [])
+ {
+ $this->visible = $visible;
+
+ return $this;
+ }
+
+ /**
+ * 转换当前模型对象为数组
+ * @access public
+ * @return array
+ */
+ public function toArray(): array
+ {
+ $item = [];
+ $hasVisible = false;
+
+ foreach ($this->visible as $key => $val) {
+ if (is_string($val)) {
+ if (strpos($val, '.')) {
+ [$relation, $name] = explode('.', $val);
+ $this->visible[$relation][] = $name;
+ } else {
+ $this->visible[$val] = true;
+ $hasVisible = true;
+ }
+ unset($this->visible[$key]);
+ }
+ }
+
+ foreach ($this->hidden as $key => $val) {
+ if (is_string($val)) {
+ if (strpos($val, '.')) {
+ [$relation, $name] = explode('.', $val);
+ $this->hidden[$relation][] = $name;
+ } else {
+ $this->hidden[$val] = true;
+ }
+ unset($this->hidden[$key]);
+ }
+ }
+
+ // 合并关联数据
+ $data = array_merge($this->data, $this->relation);
+
+ foreach ($data as $key => $val) {
+ if ($val instanceof Model || $val instanceof ModelCollection) {
+ // 关联模型对象
+ if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
+ $val->visible($this->visible[$key]);
+ } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
+ $val->hidden($this->hidden[$key]);
+ }
+ // 关联模型对象
+ if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
+ $item[$key] = $val->toArray();
+ }
+ } elseif (isset($this->visible[$key])) {
+ $item[$key] = $this->getAttr($key);
+ } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
+ $item[$key] = $this->getAttr($key);
+ }
+ }
+
+ // 追加属性(必须定义获取器)
+ foreach ($this->append as $key => $name) {
+ $this->appendAttrToArray($item, $key, $name);
+ }
+
+ return $item;
+ }
+
+ protected function appendAttrToArray(array &$item, $key, $name)
+ {
+ if (is_array($name)) {
+ // 追加关联对象属性
+ $relation = $this->getRelation($key, true);
+ $item[$key] = $relation ? $relation->append($name)
+ ->toArray() : [];
+ } elseif (strpos($name, '.')) {
+ [$key, $attr] = explode('.', $name);
+ // 追加关联对象属性
+ $relation = $this->getRelation($key, true);
+ $item[$key] = $relation ? $relation->append([$attr])
+ ->toArray() : [];
+ } else {
+ $value = $this->getAttr($name);
+ $item[$name] = $value;
+
+ $this->getBindAttr($name, $value, $item);
+ }
+ }
+
+ protected function getBindAttr(string $name, $value, array &$item = [])
+ {
+ $relation = $this->isRelationAttr($name);
+ if (!$relation) {
+ return false;
+ }
+
+ $modelRelation = $this->$relation();
+
+ if ($modelRelation instanceof OneToOne) {
+ $bindAttr = $modelRelation->getBindAttr();
+
+ if (!empty($bindAttr)) {
+ unset($item[$name]);
+ }
+
+ foreach ($bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+
+ if (isset($item[$key])) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $item[$key] = $value ? $value->getAttr($attr) : null;
+ }
+ }
+ }
+
+ /**
+ * 转换当前模型对象为JSON字符串
+ * @access public
+ * @param integer $options json参数
+ * @return string
+ */
+ public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
+ {
+ return json_encode($this->toArray(), $options);
+ }
+
+ public function __toString()
+ {
+ return $this->toJson();
+ }
+
+ // JsonSerializable
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * 转换数据集为数据集对象
+ * @access public
+ * @param array|Collection $collection 数据集
+ * @param string $resultSetType 数据集类
+ * @return Collection
+ */
+ public function toCollection(iterable $collection = [], string $resultSetType = null): Collection
+ {
+ $resultSetType = $resultSetType ?: $this->resultSetType;
+
+ if ($resultSetType && false !== strpos($resultSetType, '\\')) {
+ $collection = new $resultSetType($collection);
+ } else {
+ $collection = new ModelCollection($collection);
+ }
+
+ return $collection;
+ }
+
+}
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..694cd0d
--- /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(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..f6ab024
--- /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/concern/RelationShip.php b/vendor/topthink/think-orm/src/model/concern/RelationShip.php
new file mode 100644
index 0000000..ae91e1c
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/RelationShip.php
@@ -0,0 +1,841 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
+use think\Model;
+use think\model\Relation;
+use think\model\relation\BelongsTo;
+use think\model\relation\BelongsToMany;
+use think\model\relation\HasMany;
+use think\model\relation\HasManyThrough;
+use think\model\relation\HasOne;
+use think\model\relation\HasOneThrough;
+use think\model\relation\MorphMany;
+use think\model\relation\MorphOne;
+use think\model\relation\MorphTo;
+use think\model\relation\MorphToMany;
+use think\model\relation\OneToOne;
+
+/**
+ * 模型关联处理
+ */
+trait RelationShip
+{
+ /**
+ * 父关联模型对象
+ * @var object
+ */
+ private $parent;
+
+ /**
+ * 模型关联数据
+ * @var array
+ */
+ private $relation = [];
+
+ /**
+ * 关联写入定义信息
+ * @var array
+ */
+ private $together = [];
+
+ /**
+ * 关联自动写入信息
+ * @var array
+ */
+ protected $relationWrite = [];
+
+ /**
+ * 设置父关联对象
+ * @access public
+ * @param Model $model 模型对象
+ * @return $this
+ */
+ public function setParent(Model $model)
+ {
+ $this->parent = $model;
+
+ return $this;
+ }
+
+ /**
+ * 获取父关联对象
+ * @access public
+ * @return Model
+ */
+ public function getParent(): Model
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取当前模型的关联模型数据
+ * @access public
+ * @param string $name 关联方法名
+ * @param bool $auto 不存在是否自动获取
+ * @return mixed
+ */
+ public function getRelation(string $name = null, bool $auto = false)
+ {
+ if (is_null($name)) {
+ return $this->relation;
+ }
+
+ if (array_key_exists($name, $this->relation)) {
+ return $this->relation[$name];
+ } elseif ($auto) {
+ $relation = Str::camel($name);
+ return $this->getRelationValue($relation);
+ }
+ }
+
+ /**
+ * 设置关联数据对象值
+ * @access public
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
+ * @return $this
+ */
+ public function setRelation(string $name, $value, array $data = [])
+ {
+ // 检测修改器
+ $method = 'set' . Str::studly($name) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ $value = $this->$method($value, array_merge($this->data, $data));
+ }
+
+ $this->relation[$this->getRealFieldName($name)] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 查询当前模型的关联数据
+ * @access public
+ * @param array $relations 关联名
+ * @param array $withRelationAttr 关联获取器
+ * @return void
+ */
+ public function relationQuery(array $relations, array $withRelationAttr = []): void
+ {
+ foreach ($relations as $key => $relation) {
+ $subRelation = '';
+ $closure = null;
+
+ if ($relation instanceof Closure) {
+ // 支持闭包查询过滤关联条件
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ [$relation, $subRelation] = explode('.', $relation, 2);
+ }
+
+ $method = Str::camel($relation);
+ $relationName = Str::snake($relation);
+
+ $relationResult = $this->$method();
+
+ if (isset($withRelationAttr[$relationName])) {
+ $relationResult->withAttr($withRelationAttr[$relationName]);
+ }
+
+ $this->relation[$relation] = $relationResult->getRelation($subRelation, $closure);
+ }
+ }
+
+ /**
+ * 关联数据写入
+ * @access public
+ * @param array $relation 关联
+ * @return $this
+ */
+ public function together(array $relation)
+ {
+ $this->together = $relation;
+
+ $this->checkAutoRelationWrite();
+
+ return $this;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Query
+ */
+ public static function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = '', Query $query = null): Query
+ {
+ return (new static())
+ ->$relation()
+ ->has($operator, $count, $id, $joinType, $query);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Query
+ */
+ public static function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = '', Query $query = null): Query
+ {
+ return (new static())
+ ->$relation()
+ ->hasWhere($where, $fields, $joinType, $query);
+ }
+
+ /**
+ * 预载入关联查询 JOIN方式
+ * @access public
+ * @param Query $query Query对象
+ * @param string $relation 关联方法名
+ * @param mixed $field 字段
+ * @param string $joinType JOIN类型
+ * @param Closure $closure 闭包
+ * @param bool $first
+ * @return bool
+ */
+ public function eagerly(Query $query, string $relation, $field, string $joinType = '', Closure $closure = null, bool $first = false): bool
+ {
+ $relation = Str::camel($relation);
+ $class = $this->$relation();
+
+ if ($class instanceof OneToOne) {
+ $class->eagerly($query, $relation, $field, $joinType, $closure, $first);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 预载入关联查询 返回数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 关联名
+ * @param array $withRelationAttr 关联获取器
+ * @param bool $join 是否为JOIN方式
+ * @param mixed $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResultSet(array &$resultSet, array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
+ {
+ foreach ($relations as $key => $relation) {
+ $subRelation = [];
+ $closure = null;
+
+ if ($relation instanceof Closure) {
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ [$relation, $subRelation] = explode('.', $relation, 2);
+
+ $subRelation = [$subRelation];
+ }
+
+ $relationName = $relation;
+ $relation = Str::camel($relation);
+
+ $relationResult = $this->$relation();
+
+ if (isset($withRelationAttr[$relationName])) {
+ $relationResult->withAttr($withRelationAttr[$relationName]);
+ }
+
+ if (is_scalar($cache)) {
+ $relationCache = [$cache];
+ } else {
+ $relationCache = $cache[$relationName] ?? $cache;
+ }
+
+ $relationResult->eagerlyResultSet($resultSet, $relationName, $subRelation, $closure, $relationCache, $join);
+ }
+ }
+
+ /**
+ * 预载入关联查询 返回模型对象
+ * @access public
+ * @param Model $result 数据对象
+ * @param array $relations 关联
+ * @param array $withRelationAttr 关联获取器
+ * @param bool $join 是否为JOIN方式
+ * @param mixed $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResult(Model $result, array $relations, array $withRelationAttr = [], bool $join = false, $cache = false): void
+ {
+ foreach ($relations as $key => $relation) {
+ $subRelation = [];
+ $closure = null;
+
+ if ($relation instanceof Closure) {
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ [$relation, $subRelation] = explode('.', $relation, 2);
+
+ $subRelation = [$subRelation];
+ }
+
+ $relationName = $relation;
+ $relation = Str::camel($relation);
+
+ $relationResult = $this->$relation();
+
+ if (isset($withRelationAttr[$relationName])) {
+ $relationResult->withAttr($withRelationAttr[$relationName]);
+ }
+
+ if (is_scalar($cache)) {
+ $relationCache = [$cache];
+ } else {
+ $relationCache = $cache[$relationName] ?? [];
+ }
+
+ $relationResult->eagerlyResult($result, $relationName, $subRelation, $closure, $relationCache, $join);
+ }
+ }
+
+ /**
+ * 绑定(一对一)关联属性到当前模型
+ * @access protected
+ * @param string $relation 关联名称
+ * @param array $attrs 绑定属性
+ * @return $this
+ * @throws Exception
+ */
+ public function bindAttr(string $relation, array $attrs = [])
+ {
+ $relation = $this->getRelation($relation);
+
+ foreach ($attrs as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ $value = $this->getOrigin($key);
+
+ if (!is_null($value)) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $this->set($key, $relation ? $relation->$attr : null);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $relations 关联名
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param bool $useSubQuery 子查询
+ * @return void
+ */
+ public function relationCount(Query $query, array $relations, string $aggregate = 'sum', string $field = '*', bool $useSubQuery = true): void
+ {
+ foreach ($relations as $key => $relation) {
+ $closure = $name = null;
+
+ if ($relation instanceof Closure) {
+ $closure = $relation;
+ $relation = $key;
+ } elseif (is_string($key)) {
+ $name = $relation;
+ $relation = $key;
+ }
+
+ $relation = Str::camel($relation);
+
+ if ($useSubQuery) {
+ $count = $this->$relation()->getRelationCountQuery($closure, $aggregate, $field, $name);
+ } else {
+ $count = $this->$relation()->relationCount($this, $closure, $aggregate, $field, $name);
+ }
+
+ if (empty($name)) {
+ $name = Str::snake($relation) . '_' . $aggregate;
+ }
+
+ if ($useSubQuery) {
+ $query->field(['(' . $count . ')' => $name]);
+ } else {
+ $this->setAttr($name, $count);
+ }
+ }
+ }
+
+ /**
+ * HAS ONE 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
+ * @return HasOne
+ */
+ public function hasOne(string $model, string $foreignKey = '', string $localKey = ''): HasOne
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new HasOne($this, $model, $foreignKey, $localKey);
+ }
+
+ /**
+ * BELONGS TO 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
+ * @return BelongsTo
+ */
+ public function belongsTo(string $model, string $foreignKey = '', string $localKey = ''): BelongsTo
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName());
+ $localKey = $localKey ?: (new $model)->getPk();
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $relation = Str::snake($trace[1]['function']);
+
+ return new BelongsTo($this, $model, $foreignKey, $localKey, $relation);
+ }
+
+ /**
+ * HAS MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
+ * @return HasMany
+ */
+ public function hasMany(string $model, string $foreignKey = '', string $localKey = ''): HasMany
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new HasMany($this, $model, $foreignKey, $localKey);
+ }
+
+ /**
+ * HAS MANY 远程关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ * @param string $throughPk 中间表主键
+ * @return HasManyThrough
+ */
+ public function hasManyThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasManyThrough
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $through = $this->parseModel($through);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+ $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
+ $throughPk = $throughPk ?: (new $through)->getPk();
+
+ return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
+ }
+
+ /**
+ * HAS ONE 远程关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ * @param string $throughPk 中间表主键
+ * @return HasOneThrough
+ */
+ public function hasOneThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = '', string $throughPk = ''): HasOneThrough
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $through = $this->parseModel($through);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+ $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
+ $throughPk = $throughPk ?: (new $through)->getPk();
+
+ return new HasOneThrough($this, $model, $through, $foreignKey, $throughKey, $localKey, $throughPk);
+ }
+
+ /**
+ * BELONGS TO MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $middle 中间表/模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型关联键
+ * @return BelongsToMany
+ */
+ public function belongsToMany(string $model, string $middle = '', string $foreignKey = '', string $localKey = ''): BelongsToMany
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $name = Str::snake(class_basename($model));
+ $middle = $middle ?: Str::snake($this->name) . '_' . $name;
+ $foreignKey = $foreignKey ?: $name . '_id';
+ $localKey = $localKey ?: $this->getForeignKey($this->name);
+
+ return new BelongsToMany($this, $model, $middle, $foreignKey, $localKey);
+ }
+
+ /**
+ * MORPH One 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
+ * @return MorphOne
+ */
+ public function morphOne(string $model, $morph = null, string $type = ''): MorphOne
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+
+ if (is_null($morph)) {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $morph = Str::snake($trace[1]['function']);
+ }
+
+ if (is_array($morph)) {
+ [$morphType, $foreignKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ $type = $type ?: get_class($this);
+
+ return new MorphOne($this, $model, $foreignKey, $morphType, $type);
+ }
+
+ /**
+ * MORPH MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
+ * @return MorphMany
+ */
+ public function morphMany(string $model, $morph = null, string $type = ''): MorphMany
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+
+ if (is_null($morph)) {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $morph = Str::snake($trace[1]['function']);
+ }
+
+ $type = $type ?: get_class($this);
+
+ if (is_array($morph)) {
+ [$morphType, $foreignKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ return new MorphMany($this, $model, $foreignKey, $morphType, $type);
+ }
+
+ /**
+ * MORPH TO 关联定义
+ * @access public
+ * @param string|array $morph 多态字段信息
+ * @param array $alias 多态别名定义
+ * @return MorphTo
+ */
+ public function morphTo($morph = null, array $alias = []): MorphTo
+ {
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+ $relation = Str::snake($trace[1]['function']);
+
+ if (is_null($morph)) {
+ $morph = $relation;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ [$morphType, $foreignKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
+ }
+
+ /**
+ * MORPH TO MANY关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $middle 中间表名/模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $localKey 当前模型关联键
+ * @return MorphToMany
+ */
+ public function morphToMany(string $model, string $middle, $morph = null, string $localKey = null): MorphToMany
+ {
+ if (is_null($morph)) {
+ $morph = $middle;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ [$morphType, $morphKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $morphKey = $morph . '_id';
+ }
+
+ $model = $this->parseModel($model);
+ $name = Str::snake(class_basename($model));
+ $localKey = $localKey ?: $this->getForeignKey($name);
+
+ return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $localKey);
+ }
+
+ /**
+ * MORPH BY MANY关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $middle 中间表名/模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $foreignKey 关联外键
+ * @return MorphToMany
+ */
+ public function morphByMany(string $model, string $middle, $morph = null, string $foreignKey = null): MorphToMany
+ {
+ if (is_null($morph)) {
+ $morph = $middle;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ [$morphType, $morphKey] = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $morphKey = $morph . '_id';
+ }
+
+ $model = $this->parseModel($model);
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new MorphToMany($this, $model, $middle, $morphType, $morphKey, $foreignKey, true);
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
+ * @return string
+ */
+ protected function parseModel(string $model): string
+ {
+ if (false === strpos($model, '\\')) {
+ $path = explode('\\', static::class);
+ array_pop($path);
+ array_push($path, Str::studly($model));
+ $model = implode('\\', $path);
+ }
+
+ return $model;
+ }
+
+ /**
+ * 获取模型的默认外键名
+ * @access protected
+ * @param string $name 模型名
+ * @return string
+ */
+ protected function getForeignKey(string $name): string
+ {
+ if (strpos($name, '\\')) {
+ $name = class_basename($name);
+ }
+
+ return Str::snake($name) . '_id';
+ }
+
+ /**
+ * 检查属性是否为关联属性 如果是则返回关联方法名
+ * @access protected
+ * @param string $attr 关联属性名
+ * @return string|false
+ */
+ protected function isRelationAttr(string $attr)
+ {
+ $relation = Str::camel($attr);
+
+ if ((method_exists($this, $relation) && !method_exists('think\Model', $relation)) || isset(static::$macro[static::class][$relation])) {
+ return $relation;
+ }
+
+ return false;
+ }
+
+ /**
+ * 智能获取关联模型数据
+ * @access protected
+ * @param Relation $modelRelation 模型关联对象
+ * @return mixed
+ */
+ protected function getRelationData(Relation $modelRelation)
+ {
+ if ($this->parent && !$modelRelation->isSelfRelation()
+ && get_class($this->parent) == get_class($modelRelation->getModel())) {
+ return $this->parent;
+ }
+
+ // 获取关联数据
+ return $modelRelation->getRelation();
+ }
+
+ /**
+ * 关联数据自动写入检查
+ * @access protected
+ * @return void
+ */
+ protected function checkAutoRelationWrite(): void
+ {
+ foreach ($this->together as $key => $name) {
+ if (is_array($name)) {
+ if (key($name) === 0) {
+ $this->relationWrite[$key] = [];
+ // 绑定关联属性
+ foreach ($name as $val) {
+ if (isset($this->data[$val])) {
+ $this->relationWrite[$key][$val] = $this->data[$val];
+ }
+ }
+ } else {
+ // 直接传入关联数据
+ $this->relationWrite[$key] = $name;
+ }
+ } elseif (isset($this->relation[$name])) {
+ $this->relationWrite[$name] = $this->relation[$name];
+ } elseif (isset($this->data[$name])) {
+ $this->relationWrite[$name] = $this->data[$name];
+ unset($this->data[$name]);
+ }
+ }
+ }
+
+ /**
+ * 自动关联数据更新(针对一对一关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationUpdate(): void
+ {
+ foreach ($this->relationWrite as $name => $val) {
+ if ($val instanceof Model) {
+ $val->exists(true)->save();
+ } else {
+ $model = $this->getRelation($name, true);
+
+ if ($model instanceof Model) {
+ $model->exists(true)->save($val);
+ }
+ }
+ }
+ }
+
+ /**
+ * 自动关联数据写入(针对一对一关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationInsert(): void
+ {
+ foreach ($this->relationWrite as $name => $val) {
+ $method = Str::camel($name);
+ $this->$method()->save($val);
+ }
+ }
+
+ /**
+ * 自动关联数据删除(支持一对一及一对多关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationDelete(): void
+ {
+ foreach ($this->relationWrite as $key => $name) {
+ $name = is_numeric($key) ? $name : $key;
+ $result = $this->getRelation($name, true);
+
+ if ($result instanceof Model) {
+ $result->delete();
+ } elseif ($result instanceof Collection) {
+ foreach ($result as $model) {
+ $model->delete();
+ }
+ }
+ }
+ }
+
+ /**
+ * 移除当前模型的关联属性
+ * @access public
+ * @return $this
+ */
+ public function removeRelation()
+ {
+ $this->relation = [];
+ return $this;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/SoftDelete.php b/vendor/topthink/think-orm/src/model/concern/SoftDelete.php
new file mode 100644
index 0000000..3c26286
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/SoftDelete.php
@@ -0,0 +1,248 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\BaseQuery as Query;
+use think\Model;
+
+/**
+ * 数据软删除
+ * @mixin Model
+ */
+trait SoftDelete
+{
+ /**
+ * 是否包含软删除数据
+ * @var bool
+ */
+ protected $withTrashed = false;
+
+ /**
+ * 判断当前实例是否被软删除
+ * @access public
+ * @return bool
+ */
+ public function trashed(): bool
+ {
+ $field = $this->getDeleteTimeField();
+
+ if ($field && !empty($this->getOrigin($field))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 查询软删除数据
+ * @access public
+ * @return Query
+ */
+ public static function withTrashed(): Query
+ {
+ $model = new static();
+
+ return $model->withTrashedData(true)->db();
+ }
+
+ /**
+ * 是否包含软删除数据
+ * @access protected
+ * @param bool $withTrashed 是否包含软删除数据
+ * @return $this
+ */
+ protected function withTrashedData(bool $withTrashed)
+ {
+ $this->withTrashed = $withTrashed;
+ return $this;
+ }
+
+ /**
+ * 只查询软删除数据
+ * @access public
+ * @return Query
+ */
+ public static function onlyTrashed(): Query
+ {
+ $model = new static();
+ $field = $model->getDeleteTimeField(true);
+
+ if ($field) {
+ return $model
+ ->db()
+ ->useSoftDelete($field, $model->getWithTrashedExp());
+ }
+
+ return $model->db();
+ }
+
+ /**
+ * 获取软删除数据的查询条件
+ * @access protected
+ * @return array
+ */
+ protected function getWithTrashedExp(): array
+ {
+ return is_null($this->defaultSoftDelete) ? ['notnull', ''] : ['<>', $this->defaultSoftDelete];
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
+ return false;
+ }
+
+ $name = $this->getDeleteTimeField();
+
+ if ($name && !$this->isForce()) {
+ // 软删除
+ $this->set($name, $this->autoWriteTimestamp($name));
+
+ $result = $this->exists()->withEvent(false)->save();
+
+ $this->withEvent(true);
+ } else {
+ // 读取更新条件
+ $where = $this->getWhere();
+
+ // 删除当前模型数据
+ $result = $this->db()
+ ->where($where)
+ ->removeOption('soft_delete')
+ ->delete();
+
+ $this->lazySave(false);
+ }
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+
+ $this->trigger('AfterDelete');
+
+ $this->exists(false);
+
+ return true;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 主键列表 支持闭包查询条件
+ * @param bool $force 是否强制删除
+ * @return bool
+ */
+ public static function destroy($data, bool $force = false): bool
+ {
+ // 包含软删除数据
+ $query = (new static())->withTrashedData(true)->db(false);
+
+ if (is_array($data) && key($data) !== 0) {
+ $query->where($data);
+ $data = null;
+ } elseif ($data instanceof \Closure) {
+ call_user_func_array($data, [ & $query]);
+ $data = null;
+ } elseif (is_null($data)) {
+ return false;
+ }
+
+ $resultSet = $query->select($data);
+
+ foreach ($resultSet as $result) {
+ $result->force($force)->delete();
+ }
+
+ return true;
+ }
+
+ /**
+ * 恢复被软删除的记录
+ * @access public
+ * @param array $where 更新条件
+ * @return bool
+ */
+ public function restore($where = []): bool
+ {
+ $name = $this->getDeleteTimeField();
+
+ if (!$name || false === $this->trigger('BeforeRestore')) {
+ return false;
+ }
+
+ if (empty($where)) {
+ $pk = $this->getPk();
+ if (is_string($pk)) {
+ $where[] = [$pk, '=', $this->getData($pk)];
+ }
+ }
+
+ // 恢复删除
+ $this->db(false)
+ ->where($where)
+ ->useSoftDelete($name, $this->getWithTrashedExp())
+ ->update([$name => $this->defaultSoftDelete]);
+
+ $this->trigger('AfterRestore');
+
+ return true;
+ }
+
+ /**
+ * 获取软删除字段
+ * @access protected
+ * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
+ * @return string|false
+ */
+ protected function getDeleteTimeField(bool $read = false)
+ {
+ $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time';
+
+ if (false === $field) {
+ return false;
+ }
+
+ if (false === strpos($field, '.')) {
+ $field = '__TABLE__.' . $field;
+ }
+
+ if (!$read && strpos($field, '.')) {
+ $array = explode('.', $field);
+ $field = array_pop($array);
+ }
+
+ return $field;
+ }
+
+ /**
+ * 查询的时候默认排除软删除数据
+ * @access protected
+ * @param Query $query
+ * @return void
+ */
+ protected function withNoTrashed(Query $query): void
+ {
+ $field = $this->getDeleteTimeField(true);
+
+ if ($field) {
+ $condition = is_null($this->defaultSoftDelete) ? ['null', ''] : ['=', $this->defaultSoftDelete];
+ $query->useSoftDelete($field, $condition);
+ }
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/TimeStamp.php b/vendor/topthink/think-orm/src/model/concern/TimeStamp.php
new file mode 100644
index 0000000..6d624f2
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/TimeStamp.php
@@ -0,0 +1,208 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use DateTime;
+
+/**
+ * 自动时间戳
+ */
+trait TimeStamp
+{
+ /**
+ * 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
+ * @var bool|string
+ */
+ protected $autoWriteTimestamp;
+
+ /**
+ * 创建时间字段 false表示关闭
+ * @var false|string
+ */
+ protected $createTime = 'create_time';
+
+ /**
+ * 更新时间字段 false表示关闭
+ * @var false|string
+ */
+ protected $updateTime = 'update_time';
+
+ /**
+ * 时间字段显示格式
+ * @var string
+ */
+ protected $dateFormat;
+
+ /**
+ * 是否需要自动写入时间字段
+ * @access public
+ * @param bool|string $auto
+ * @return $this
+ */
+ public function isAutoWriteTimestamp($auto)
+ {
+ $this->autoWriteTimestamp = $this->checkTimeFieldType($auto);
+
+ return $this;
+ }
+
+ /**
+ * 检测时间字段的实际类型
+ * @access public
+ * @param bool|string $type
+ * @return mixed
+ */
+ protected function checkTimeFieldType($type)
+ {
+ if (true === $type) {
+ if (isset($this->type[$this->createTime])) {
+ $type = $this->type[$this->createTime];
+ } elseif (isset($this->schema[$this->createTime]) && in_array($this->schema[$this->createTime], ['datetime', 'date', 'timestamp', 'int'])) {
+ $type = $this->schema[$this->createTime];
+ } else {
+ $type = $this->getFieldType($this->createTime);
+ }
+ }
+
+ return $type;
+ }
+
+ /**
+ * 获取自动写入时间字段
+ * @access public
+ * @return bool|string
+ */
+ public function getAutoWriteTimestamp()
+ {
+ return $this->autoWriteTimestamp;
+ }
+
+ /**
+ * 设置时间字段格式化
+ * @access public
+ * @param string|false $format
+ * @return $this
+ */
+ public function setDateFormat($format)
+ {
+ $this->dateFormat = $format;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动写入时间字段
+ * @access public
+ * @return string|false
+ */
+ public function getDateFormat()
+ {
+ return $this->dateFormat;
+ }
+
+ /**
+ * 自动写入时间戳
+ * @access protected
+ * @return mixed
+ */
+ protected function autoWriteTimestamp()
+ {
+ // 检测时间字段类型
+ $type = $this->checkTimeFieldType($this->autoWriteTimestamp);
+
+ return is_string($type) ? $this->getTimeTypeValue($type) : time();
+ }
+
+ /**
+ * 获取指定类型的时间字段值
+ * @access protected
+ * @param string $type 时间字段类型
+ * @return mixed
+ */
+ protected function getTimeTypeValue(string $type)
+ {
+ $value = time();
+
+ switch ($type) {
+ case 'datetime':
+ case 'date':
+ case 'timestamp':
+ $value = $this->formatDateTime('Y-m-d H:i:s.u');
+ break;
+ default:
+ if (false !== strpos($type, '\\')) {
+ // 对象数据写入
+ $obj = new $type();
+ if (method_exists($obj, '__toString')) {
+ // 对象数据写入
+ $value = $obj->__toString();
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 时间日期字段格式化处理
+ * @access protected
+ * @param mixed $format 日期格式
+ * @param mixed $time 时间日期表达式
+ * @param bool $timestamp 时间表达式是否为时间戳
+ * @return mixed
+ */
+ protected function formatDateTime($format, $time = 'now', bool $timestamp = false)
+ {
+ if (empty($time)) {
+ return;
+ }
+
+ if (false === $format) {
+ return $time;
+ } elseif (false !== strpos($format, '\\')) {
+ return new $format($time);
+ }
+
+ if ($time instanceof DateTime) {
+ $dateTime = $time;
+ } elseif ($timestamp) {
+ $dateTime = new DateTime();
+ $dateTime->setTimestamp((int) $time);
+ } else {
+ $dateTime = new DateTime($time);
+ }
+
+ return $dateTime->format($format);
+ }
+
+ /**
+ * 获取时间字段值
+ * @access protected
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function getTimestampValue($value)
+ {
+ $type = $this->checkTimeFieldType($this->autoWriteTimestamp);
+
+ if (is_string($type) && in_array(strtolower($type), [
+ 'datetime', 'date', 'timestamp',
+ ])) {
+ $value = $this->formatDateTime($this->dateFormat, $value);
+ } else {
+ $value = $this->formatDateTime($this->dateFormat, $value, true);
+ }
+
+ return $value;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/relation/BelongsTo.php b/vendor/topthink/think-orm/src/model/relation/BelongsTo.php
new file mode 100644
index 0000000..5573ce8
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/BelongsTo.php
@@ -0,0 +1,331 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\relation;
+
+use Closure;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
+use think\Model;
+
+/**
+ * BelongsTo关联类
+ */
+class BelongsTo extends OneToOne
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
+ * @param string $relation 关联名
+ */
+ public function __construct(Model $parent, string $model, string $foreignKey, string $localKey, string $relation = null)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+ $this->query = (new $model)->db();
+ $this->relation = $relation;
+
+ 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)
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
+ $foreignKey = $this->foreignKey;
+
+ $relationModel = $this->query
+ ->removeWhereField($this->localKey)
+ ->where($this->localKey, $this->parent->$foreignKey)
+ ->relation($subRelation)
+ ->find();
+
+ if ($relationModel) {
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($this->parent, $relationModel);
+ }
+
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ 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 = '*', &$name = ''): string
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->query
+ ->whereExp($this->localKey, '=' . $this->parent->getTable() . '.' . $this->foreignKey)
+ ->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)
+ {
+ $foreignKey = $this->foreignKey;
+
+ if (!isset($result->$foreignKey)) {
+ return 0;
+ }
+
+ if ($closure) {
+ $closure($this->getClosureType($closure), $name);
+ }
+
+ return $this->query
+ ->where($this->localKey, '=', $result->$foreignKey)
+ ->$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 . '.' . $localKey)
+ ->whereExp($model . '.' . $foreignKey, '=' . $relation . '.' . $localKey)
+ ->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()->alias($model);
+
+ return $query->field($fields)
+ ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $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->$foreignKey)) {
+ $range[] = $result->$foreignKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $this->query->removeWhereField($localKey);
+
+ $data = $this->eagerlyWhere([
+ [$localKey, 'in', $range],
+ ], $localKey, $subRelation, $closure, $cache);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ // 关联模型
+ if (!isset($data[$result->$foreignKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$foreignKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @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($localKey);
+
+ $data = $this->eagerlyWhere([
+ [$localKey, '=', $result->$foreignKey],
+ ], $localKey, $subRelation, $closure, $cache);
+
+ // 关联模型
+ if (!isset($data[$result->$foreignKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$foreignKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+
+ /**
+ * 添加关联数据
+ * @access public
+ * @param Model $model关联模型对象
+ * @return Model
+ */
+ public function associate(Model $model): Model
+ {
+ $this->parent->setAttr($this->foreignKey, $model->getKey());
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, $model);
+ }
+
+ /**
+ * 注销关联数据
+ * @access public
+ * @return Model
+ */
+ public function dissociate(): Model
+ {
+ $foreignKey = $this->foreignKey;
+
+ $this->parent->setAttr($foreignKey, null);
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, null);
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery(): void
+ {
+ if (empty($this->baseQuery)) {
+ if (isset($this->parent->{$this->foreignKey})) {
+ // 关联查询带入关联条件
+ $this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
+ }
+
+ $this->baseQuery = true;
+ }
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php b/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php
new file mode 100644
index 0000000..90113df
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php
@@ -0,0 +1,677 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\db\Raw;
+use think\Model;
+use think\model\Pivot;
+use think\model\Relation;
+use think\Paginator;
+
+/**
+ * 多对多关联类
+ */
+class BelongsToMany extends Relation
+{
+ /**
+ * 中间表表名
+ * @var string
+ */
+ protected $middle;
+
+ /**
+ * 中间表模型名称
+ * @var string
+ */
+ protected $pivotName;
+
+ /**
+ * 中间表模型对象
+ * @var Pivot
+ */
+ protected $pivot;
+
+ /**
+ * 中间表数据名称
+ * @var string
+ */
+ protected $pivotDataName = 'pivot';
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $middle 中间表/模型名
+ * @param string $foreignKey 关联模型外键
+ * @param string $localKey 当前模型关联键
+ */
+ public function __construct(Model $parent, string $model, string $middle, string $foreignKey, string $localKey)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+
+ if (false !== strpos($middle, '\\')) {
+ $this->pivotName = $middle;
+ $this->middle = class_basename($middle);
+ } else {
+ $this->middle = $middle;
+ }
+
+ $this->query = (new $model)->db();
+ $this->pivot = $this->newPivot();
+ }
+
+ /**
+ * 设置中间表模型
+ * @access public
+ * @param $pivot
+ * @return $this
+ */
+ public function pivot(string $pivot)
+ {
+ $this->pivotName = $pivot;
+ return $this;
+ }
+
+ /**
+ * 设置中间表数据名称
+ * @access public
+ * @param string $name
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->pivotDataName = $name;
+ return $this;
+ }
+
+ /**
+ * 实例化中间表模型
+ * @access public
+ * @param $data
+ * @return Pivot
+ * @throws Exception
+ */
+ protected function newPivot(array $data = []): Pivot
+ {
+ $class = $this->pivotName ?: Pivot::class;
+ $pivot = new $class($data, $this->parent, $this->middle);
+
+ if ($pivot instanceof Pivot) {
+ return $pivot;
+ } else {
+ throw new Exception('pivot model must extends: \think\model\Pivot');
+ }
+ }
+
+ /**
+ * 合成中间表模型
+ * @access protected
+ * @param array|Collection|Paginator $models
+ */
+ protected function hydratePivot(iterable $models)
+ {
+ foreach ($models as $model) {
+ $pivot = [];
+
+ foreach ($model->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ [$name, $attr] = explode('__', $key, 2);
+
+ if ('pivot' == $name) {
+ $pivot[$attr] = $val;
+ unset($model->$key);
+ }
+ }
+ }
+
+ $model->setRelation($this->pivotDataName, $this->newPivot($pivot));
+ }
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @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));
+ }
+
+ $result = $this->relation($subRelation)
+ ->select()
+ ->setParent(clone $this->parent);
+
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载select方法
+ * @access public
+ * @param mixed $data
+ * @return Collection
+ */
+ public function select($data = null): Collection
+ {
+ $this->baseQuery();
+ $result = $this->query->select($data);
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载paginate方法
+ * @access public
+ * @param int|array $listRows
+ * @param int|bool $simple
+ * @return Paginator
+ */
+ public function paginate($listRows = null, $simple = false): Paginator
+ {
+ $this->baseQuery();
+ $result = $this->query->paginate($listRows, $simple);
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载find方法
+ * @access public
+ * @param mixed $data
+ * @return Model
+ */
+ public function find($data = null)
+ {
+ $this->baseQuery();
+ $result = $this->query->find($data);
+
+ if ($result && !$result->isEmpty()) {
+ $this->hydratePivot([$result]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Model
+ */
+ public function has(string $operator = '>=', $count = 1, $id = '*', string $joinType = 'INNER', Query $query = null)
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @param Query $query Query对象
+ * @return Query
+ * @throws Exception
+ */
+ public function hasWhere($where = [], $fields = null, string $joinType = '', Query $query = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 设置中间表的查询条件
+ * @access public
+ * @param string $field
+ * @param string $op
+ * @param mixed $condition
+ * @return $this
+ */
+ public function wherePivot($field, $op = null, $condition = null)
+ {
+ $this->query->where('pivot.' . $field, $op, $condition);
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @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;
+ $pk = $resultSet[0]->getPk();
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$pk)) {
+ $range[] = $result->$pk;
+ }
+ }
+
+ if (!empty($range)) {
+ // 查询关联数据
+ $data = $this->eagerlyManyToMany([
+ ['pivot.' . $localKey, 'in', $range],
+ ], $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],
+ ], $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],
+ ])->$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()),
+ ],
+ ])->fetchSql()->$aggregate($field);
+ }
+
+ /**
+ * 多对多 关联模型预查询
+ * @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));
+ }
+
+ // 预载入关联查询 支持嵌套预载入
+ $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 ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
+ continue;
+ }
+
+ $set->setRelation($this->pivotDataName, $this->newPivot($pivot));
+
+ $data[$key][] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 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();
+ $fields = $this->getQueryFields($tableName);
+
+ if ($this->withLimit) {
+ $this->query->limit($this->withLimit);
+ }
+
+ $this->query
+ ->field($fields)
+ ->tableField(true, $table, 'pivot', 'pivot__')
+ ->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $this->query->getPk())
+ ->where($condition);
+
+ return $this->query;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param array $pivot 中间表额外数据
+ * @return array|Pivot
+ */
+ public function save($data, array $pivot = [])
+ {
+ // 保存关联表/中间表数据
+ return $this->attach($data, $pivot);
+ }
+
+ /**
+ * 批量保存当前关联数据对象
+ * @access public
+ * @param iterable $dataSet 数据集
+ * @param array $pivot 中间表额外数据
+ * @param bool $samePivot 额外数据是否相同
+ * @return array|false
+ */
+ public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
+ {
+ $result = [];
+
+ foreach ($dataSet as $key => $data) {
+ if (!$samePivot) {
+ $pivotData = $pivot[$key] ?? [];
+ } else {
+ $pivotData = $pivot;
+ }
+
+ $result[] = $this->attach($data, $pivotData);
+ }
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * 附加关联的一个中间表数据
+ * @access public
+ * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
+ * @param array $pivot 中间表额外数据
+ * @return array|Pivot
+ * @throws Exception
+ */
+ 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();
+
+ $ids = (array) $id;
+ 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->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 = [];
+ $pivot[] = [$this->localKey, '=', $this->parent->getKey()];
+
+ 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())
+ ->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;
+
+ // 关联查询
+ $condition = ['pivot.' . $localKey, '=', $this->parent->getKey()];
+
+ $this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
+
+ $this->baseQuery = true;
+ }
+ }
+
+}
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..22cf5ae
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasMany.php
@@ -0,0 +1,367 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+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 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));
+ }
+
+ if ($this->withLimit) {
+ $this->query->limit($this->withLimit);
+ }
+
+ 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));
+ }
+
+ $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 ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->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)
+ {
+ $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()->alias($model);
+
+ return $query->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..baac72f
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasManyThrough.php
@@ -0,0 +1,382 @@
+
+// +----------------------------------------------------------------------
+
+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();
+
+ if ($this->withLimit) {
+ $this->query->limit($this->withLimit);
+ }
+
+ 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()->alias($model);
+
+ return $query->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));
+ }
+
+ $list = $this->query
+ ->where($this->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 ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->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();
+ $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..47e3a10
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasOne.php
@@ -0,0 +1,300 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\relation;
+
+use Closure;
+use think\db\BaseQuery as Query;
+use think\helper\Str;
+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);
+ }
+
+ 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()->alias($model);
+
+ return $query->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 = null;
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @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 = null;
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @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..b76af76
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasOneThrough.php
@@ -0,0 +1,163 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\helper\Str;
+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);
+ }
+
+ 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 = null;
+ } 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 = null;
+ } 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..17fd82b
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphMany.php
@@ -0,0 +1,353 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\Collection;
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+use think\helper\Str;
+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();
+
+ if ($this->withLimit) {
+ $this->query->limit($this->withLimit);
+ }
+
+ 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));
+ }
+
+ $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 ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->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)
+ {
+ $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;
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @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..803de16
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphOne.php
@@ -0,0 +1,280 @@
+
+// +----------------------------------------------------------------------
+
+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;
+
+/**
+ * 多态一对一关联类
+ */
+class MorphOne 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 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);
+ }
+
+ 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 = null;
+ } else {
+ $relationModel = $data[$result->$pk];
+ $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
+ {
+ $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 = null;
+ }
+
+ $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)
+ {
+ $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;
+ }
+ }
+
+}
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..eaa9890
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphTo.php
@@ -0,0 +1,332 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use Closure;
+use think\db\exception\DbException as Exception;
+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;
+
+ /**
+ * 架构函数
+ * @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 = (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)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
+ * @return string
+ */
+ 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()
+ {
+ 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;
+ $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 = (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);
+ }
+
+}
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..70db26a
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php
@@ -0,0 +1,458 @@
+
+// +----------------------------------------------------------------------
+
+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 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);
+
+ $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();
+ $fields = $this->getQueryFields($tableName);
+
+ if ($this->withLimit) {
+ $this->query->limit($this->withLimit);
+ }
+
+ $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));
+ }
+
+ // 预载入关联查询 支持嵌套预载入
+ $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 ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->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);
+ }
+ } else if (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } else if ($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;
+ } else if (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } else if ($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;
+ } else if (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;
+ }
+ }
+
+}
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..99c7161
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/OneToOne.php
@@ -0,0 +1,332 @@
+
+// +----------------------------------------------------------------------
+
+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) {
+ $joinOn = $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey;
+ } else {
+ $joinOn = $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey;
+ }
+
+ if ($closure) {
+ // 执行闭包查询
+ $closure($this->getClosureType($closure));
+
+ // 使用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 = new $this->model;
+ // 保存关联表数据
+ $data[$this->foreignKey] = $this->parent->{$this->localKey};
+
+ return $model->replace($replace)->save($data) ? $model : false;
+ }
+
+ /**
+ * 绑定关联表的属性到父模型属性
+ * @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);
+ }
+
+ if ($this->query->getOptions('order')) {
+ $this->query->group($key);
+ }
+
+ $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/src/paginator/driver/Bootstrap.php b/vendor/topthink/think-orm/src/paginator/driver/Bootstrap.php
new file mode 100644
index 0000000..22761c5
--- /dev/null
+++ b/vendor/topthink/think-orm/src/paginator/driver/Bootstrap.php
@@ -0,0 +1,209 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\paginator\driver;
+
+use think\Paginator;
+
+/**
+ * Bootstrap 分页驱动
+ */
+class Bootstrap extends Paginator
+{
+
+ /**
+ * 上一页按钮
+ * @param string $text
+ * @return string
+ */
+ protected function getPreviousButton(string $text = "«"): string
+ {
+
+ if ($this->currentPage() <= 1) {
+ return $this->getDisabledTextWrapper($text);
+ }
+
+ $url = $this->url(
+ $this->currentPage() - 1
+ );
+
+ return $this->getPageLinkWrapper($url, $text);
+ }
+
+ /**
+ * 下一页按钮
+ * @param string $text
+ * @return string
+ */
+ protected function getNextButton(string $text = '»'): string
+ {
+ if (!$this->hasMore) {
+ return $this->getDisabledTextWrapper($text);
+ }
+
+ $url = $this->url($this->currentPage() + 1);
+
+ return $this->getPageLinkWrapper($url, $text);
+ }
+
+ /**
+ * 页码按钮
+ * @return string
+ */
+ protected function getLinks(): string
+ {
+ if ($this->simple) {
+ return '';
+ }
+
+ $block = [
+ 'first' => null,
+ 'slider' => null,
+ 'last' => null,
+ ];
+
+ $side = 3;
+ $window = $side * 2;
+
+ if ($this->lastPage < $window + 6) {
+ $block['first'] = $this->getUrlRange(1, $this->lastPage);
+ } elseif ($this->currentPage <= $window) {
+ $block['first'] = $this->getUrlRange(1, $window + 2);
+ $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+ } elseif ($this->currentPage > ($this->lastPage - $window)) {
+ $block['first'] = $this->getUrlRange(1, 2);
+ $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
+ } else {
+ $block['first'] = $this->getUrlRange(1, 2);
+ $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
+ $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+ }
+
+ $html = '';
+
+ if (is_array($block['first'])) {
+ $html .= $this->getUrlLinks($block['first']);
+ }
+
+ if (is_array($block['slider'])) {
+ $html .= $this->getDots();
+ $html .= $this->getUrlLinks($block['slider']);
+ }
+
+ if (is_array($block['last'])) {
+ $html .= $this->getDots();
+ $html .= $this->getUrlLinks($block['last']);
+ }
+
+ return $html;
+ }
+
+ /**
+ * 渲染分页html
+ * @return mixed
+ */
+ public function render()
+ {
+ if ($this->hasPages()) {
+ if ($this->simple) {
+ return sprintf(
+ '',
+ $this->getPreviousButton(),
+ $this->getNextButton()
+ );
+ } else {
+ return sprintf(
+ '',
+ $this->getPreviousButton(),
+ $this->getLinks(),
+ $this->getNextButton()
+ );
+ }
+ }
+ }
+
+ /**
+ * 生成一个可点击的按钮
+ *
+ * @param string $url
+ * @param string $page
+ * @return string
+ */
+ protected function getAvailablePageWrapper(string $url, string $page): string
+ {
+ return '' . $page . ' ';
+ }
+
+ /**
+ * 生成一个禁用的按钮
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function getDisabledTextWrapper(string $text): string
+ {
+ return '' . $text . ' ';
+ }
+
+ /**
+ * 生成一个激活的按钮
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function getActivePageWrapper(string $text): string
+ {
+ return '' . $text . ' ';
+ }
+
+ /**
+ * 生成省略号按钮
+ *
+ * @return string
+ */
+ protected function getDots(): string
+ {
+ return $this->getDisabledTextWrapper('...');
+ }
+
+ /**
+ * 批量生成页码按钮.
+ *
+ * @param array $urls
+ * @return string
+ */
+ protected function getUrlLinks(array $urls): string
+ {
+ $html = '';
+
+ foreach ($urls as $page => $url) {
+ $html .= $this->getPageLinkWrapper($url, $page);
+ }
+
+ return $html;
+ }
+
+ /**
+ * 生成普通页码按钮
+ *
+ * @param string $url
+ * @param string $page
+ * @return string
+ */
+ protected function getPageLinkWrapper(string $url, string $page): string
+ {
+ if ($this->currentPage() == $page) {
+ return $this->getActivePageWrapper($page);
+ }
+
+ return $this->getAvailablePageWrapper($url, $page);
+ }
+}
diff --git a/vendor/topthink/think-template/.gitignore b/vendor/topthink/think-template/.gitignore
new file mode 100644
index 0000000..1da8522
--- /dev/null
+++ b/vendor/topthink/think-template/.gitignore
@@ -0,0 +1 @@
+.idea
diff --git a/vendor/topthink/think-template/LICENSE b/vendor/topthink/think-template/LICENSE
new file mode 100644
index 0000000..c0ee812
--- /dev/null
+++ b/vendor/topthink/think-template/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-template/README.md b/vendor/topthink/think-template/README.md
new file mode 100644
index 0000000..713156c
--- /dev/null
+++ b/vendor/topthink/think-template/README.md
@@ -0,0 +1,70 @@
+# ThinkTemplate
+
+基于XML和标签库的编译型模板引擎
+
+## 主要特性
+
+- 支持XML标签库和普通标签的混合定义;
+- 支持直接使用PHP代码书写;
+- 支持文件包含;
+- 支持多级标签嵌套;
+- 支持布局模板功能;
+- 一次编译多次运行,编译和运行效率非常高;
+- 模板文件和布局模板更新,自动更新模板缓存;
+- 系统变量无需赋值直接输出;
+- 支持多维数组的快速输出;
+- 支持模板变量的默认值;
+- 支持页面代码去除Html空白;
+- 支持变量组合调节器和格式化功能;
+- 允许定义模板禁用函数和禁用PHP语法;
+- 通过标签库方式扩展;
+
+## 安装
+
+~~~php
+composer require topthink/think-template
+~~~
+
+## 用法示例
+
+
+~~~php
+ './template/',
+ 'cache_path' => './runtime/',
+ 'view_suffix' => 'html',
+];
+
+$template = new Template($config);
+// 模板变量赋值
+$template->assign(['name' => 'think']);
+// 读取模板文件渲染输出
+$template->fetch('index');
+// 完整模板文件渲染
+$template->fetch('./template/test.php');
+// 渲染内容输出
+$template->display($content);
+~~~
+
+支持静态调用
+
+~~~
+use think\facade\Template;
+
+Template::config([
+ 'view_path' => './template/',
+ 'cache_path' => './runtime/',
+ 'view_suffix' => 'html',
+]);
+Template::assign(['name' => 'think']);
+Template::fetch('index',['name' => 'think']);
+Template::display($content,['name' => 'think']);
+~~~
+
+详细用法参考[开发手册](https://www.kancloud.cn/manual/think-template/content)
\ No newline at end of file
diff --git a/vendor/topthink/think-template/composer.json b/vendor/topthink/think-template/composer.json
new file mode 100644
index 0000000..1dab208
--- /dev/null
+++ b/vendor/topthink/think-template/composer.json
@@ -0,0 +1,20 @@
+{
+ "name": "topthink/think-template",
+ "description": "the php template engine",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "psr/simple-cache": "^1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ }
+ }
+}
\ No newline at end of file
diff --git a/vendor/topthink/think-template/src/Template.php b/vendor/topthink/think-template/src/Template.php
new file mode 100644
index 0000000..8436d55
--- /dev/null
+++ b/vendor/topthink/think-template/src/Template.php
@@ -0,0 +1,1320 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Exception;
+use Psr\SimpleCache\CacheInterface;
+
+/**
+ * ThinkPHP分离出来的模板引擎
+ * 支持XML标签和普通标签的模板解析
+ * 编译型模板引擎 支持动态缓存
+ */
+class Template
+{
+ /**
+ * 模板变量
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 模板配置参数
+ * @var array
+ */
+ protected $config = [
+ 'view_path' => '', // 模板路径
+ 'view_suffix' => 'html', // 默认模板文件后缀
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ 'cache_path' => '',
+ 'cache_suffix' => 'php', // 默认模板缓存后缀
+ 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数
+ 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码
+ 'tpl_begin' => '{', // 模板引擎普通标签开始标记
+ 'tpl_end' => '}', // 模板引擎普通标签结束标记
+ 'strip_space' => false, // 是否去除模板文件里面的html空格与换行
+ 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译
+ 'compile_type' => 'file', // 模板编译类型
+ 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变
+ 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒)
+ 'layout_on' => false, // 布局模板开关
+ 'layout_name' => 'layout', // 布局模板入口文件
+ 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识
+ 'taglib_begin' => '{', // 标签库标签开始标记
+ 'taglib_end' => '}', // 标签库标签结束标记
+ 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测
+ 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序
+ 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔
+ 'display_cache' => false, // 模板渲染缓存
+ 'cache_id' => '', // 模板缓存ID
+ 'tpl_replace_string' => [],
+ 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别
+ 'default_filter' => 'htmlentities', // 默认过滤方法 用于普通标签输出
+ ];
+
+ /**
+ * 保留内容信息
+ * @var array
+ */
+ private $literal = [];
+
+ /**
+ * 扩展解析规则
+ * @var array
+ */
+ private $extend = [];
+
+ /**
+ * 模板包含信息
+ * @var array
+ */
+ private $includeFile = [];
+
+ /**
+ * 模板存储对象
+ * @var object
+ */
+ protected $storage;
+
+ /**
+ * 查询缓存对象
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $config
+ */
+ public function __construct(array $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+
+ $this->config['taglib_begin_origin'] = $this->config['taglib_begin'];
+ $this->config['taglib_end_origin'] = $this->config['taglib_end'];
+
+ $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/');
+ $this->config['taglib_end'] = preg_quote($this->config['taglib_end'], '/');
+ $this->config['tpl_begin'] = preg_quote($this->config['tpl_begin'], '/');
+ $this->config['tpl_end'] = preg_quote($this->config['tpl_end'], '/');
+
+ // 初始化模板编译存储器
+ $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File';
+ $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type);
+
+ $this->storage = new $class();
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param array $vars 模板变量
+ * @return $this
+ */
+ public function assign(array $vars = [])
+ {
+ $this->data = array_merge($this->data, $vars);
+ return $this;
+ }
+
+ /**
+ * 模板引擎参数赋值
+ * @access public
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set($name, $value)
+ {
+ $this->config[$name] = $value;
+ }
+
+ /**
+ * 设置缓存对象
+ * @access public
+ * @param CacheInterface $cache 缓存对象
+ * @return void
+ */
+ public function setCache(CacheInterface $cache): void
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * 模板引擎配置
+ * @access public
+ * @param array $config
+ * @return $this
+ */
+ public function config(array $config)
+ {
+ $this->config = array_merge($this->config, $config);
+ return $this;
+ }
+
+ /**
+ * 获取模板引擎配置项
+ * @access public
+ * @param string $name
+ * @return mixed
+ */
+ public function getConfig(string $name)
+ {
+ return $this->config[$name] ?? null;
+ }
+
+ /**
+ * 模板变量获取
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function get(string $name = '')
+ {
+ if ('' == $name) {
+ return $this->data;
+ }
+
+ $data = $this->data;
+
+ foreach (explode('.', $name) as $key => $val) {
+ if (isset($data[$val])) {
+ $data = $data[$val];
+ } else {
+ $data = null;
+ break;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 扩展模板解析规则
+ * @access public
+ * @param string $rule 解析规则
+ * @param callable $callback 解析规则
+ * @return void
+ */
+ public function extend(string $rule, callable $callback = null): void
+ {
+ $this->extend[$rule] = $callback;
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $vars 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $vars = []): void
+ {
+ if ($vars) {
+ $this->data = array_merge($this->data, $vars);
+ }
+
+ if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
+ // 读取渲染缓存
+ if ($this->cache->has($this->config['cache_id'])) {
+ echo $this->cache->get($this->config['cache_id']);
+ return;
+ }
+ }
+
+ $template = $this->parseTemplateFile($template);
+
+ if ($template) {
+ $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
+
+ if (!$this->checkCache($cacheFile)) {
+ // 缓存无效 重新模板编译
+ $content = file_get_contents($template);
+ $this->compiler($content, $cacheFile);
+ }
+
+ // 页面缓存
+ ob_start();
+ ob_implicit_flush(0);
+
+ // 读取编译存储
+ $this->storage->read($cacheFile, $this->data);
+
+ // 获取并清空缓存
+ $content = ob_get_clean();
+
+ if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
+ // 缓存页面输出
+ $this->cache->set($this->config['cache_id'], $content, $this->config['cache_time']);
+ }
+
+ echo $content;
+ }
+ }
+
+ /**
+ * 检查编译缓存是否存在
+ * @access public
+ * @param string $cacheId 缓存的id
+ * @return boolean
+ */
+ public function isCache(string $cacheId): bool
+ {
+ if ($cacheId && $this->cache && $this->config['display_cache']) {
+ // 缓存页面输出
+ return $this->cache->has($cacheId);
+ }
+
+ return false;
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $content 模板内容
+ * @param array $vars 模板变量
+ * @return void
+ */
+ public function display(string $content, array $vars = []): void
+ {
+ if ($vars) {
+ $this->data = array_merge($this->data, $vars);
+ }
+
+ $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
+
+ if (!$this->checkCache($cacheFile)) {
+ // 缓存无效 模板编译
+ $this->compiler($content, $cacheFile);
+ }
+
+ // 读取编译存储
+ $this->storage->read($cacheFile, $this->data);
+ }
+
+ /**
+ * 设置布局
+ * @access public
+ * @param mixed $name 布局模板名称 false 则关闭布局
+ * @param string $replace 布局模板内容替换标识
+ * @return $this
+ */
+ public function layout($name, string $replace = '')
+ {
+ if (false === $name) {
+ // 关闭布局
+ $this->config['layout_on'] = false;
+ } else {
+ // 开启布局
+ $this->config['layout_on'] = true;
+
+ // 名称必须为字符串
+ if (is_string($name)) {
+ $this->config['layout_name'] = $name;
+ }
+
+ if (!empty($replace)) {
+ $this->config['layout_item'] = $replace;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 检查编译缓存是否有效
+ * 如果无效则需要重新编译
+ * @access private
+ * @param string $cacheFile 缓存文件名
+ * @return bool
+ */
+ private function checkCache(string $cacheFile): bool
+ {
+ if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, "r")) {
+ return false;
+ }
+
+ // 读取第一行
+ preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches);
+
+ if (!isset($matches[1])) {
+ return false;
+ }
+
+ $includeFile = unserialize($matches[1]);
+
+ if (!is_array($includeFile)) {
+ return false;
+ }
+
+ // 检查模板文件是否有更新
+ foreach ($includeFile as $path => $time) {
+ if (is_file($path) && filemtime($path) > $time) {
+ // 模板文件如果有更新则缓存需要更新
+ return false;
+ }
+ }
+
+ // 检查编译存储是否有效
+ return $this->storage->check($cacheFile, $this->config['cache_time']);
+ }
+
+ /**
+ * 编译模板文件内容
+ * @access private
+ * @param string $content 模板内容
+ * @param string $cacheFile 缓存文件名
+ * @return void
+ */
+ private function compiler(string &$content, string $cacheFile): void
+ {
+ // 判断是否启用布局
+ if ($this->config['layout_on']) {
+ if (false !== strpos($content, '{__NOLAYOUT__}')) {
+ // 可以单独定义不使用布局
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ } else {
+ // 读取布局模板
+ $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
+
+ if ($layoutFile) {
+ // 替换布局的主体内容
+ $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
+ }
+ }
+ } else {
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ }
+
+ // 模板解析
+ $this->parse($content);
+
+ if ($this->config['strip_space']) {
+ /* 去除html空格与换行 */
+ $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
+ $replace = ['><', '>'];
+ $content = preg_replace($find, $replace, $content);
+ }
+
+ // 优化生成的php代码
+ $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content);
+
+ // 模板过滤输出
+ $replace = $this->config['tpl_replace_string'];
+ $content = str_replace(array_keys($replace), array_values($replace), $content);
+
+ // 添加安全代码及模板引用记录
+ $content = 'includeFile) . '*/ ?>' . "\n" . $content;
+ // 编译存储
+ $this->storage->write($cacheFile, $content);
+
+ $this->includeFile = [];
+ }
+
+ /**
+ * 模板解析入口
+ * 支持普通标签和TagLib解析 支持自定义标签库
+ * @access public
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ public function parse(string &$content): void
+ {
+ // 内容为空不解析
+ if (empty($content)) {
+ return;
+ }
+
+ // 替换literal标签内容
+ $this->parseLiteral($content);
+
+ // 解析继承
+ $this->parseExtend($content);
+
+ // 解析布局
+ $this->parseLayout($content);
+
+ // 检查include语法
+ $this->parseInclude($content);
+
+ // 替换包含文件中literal标签内容
+ $this->parseLiteral($content);
+
+ // 检查PHP语法
+ $this->parsePhp($content);
+
+ // 获取需要引入的标签库列表
+ // 标签库只需要定义一次,允许引入多个一次
+ // 一般放在文件的最前面
+ // 格式:
+ // 当TAGLIB_LOAD配置为true时才会进行检测
+ if ($this->config['taglib_load']) {
+ $tagLibs = $this->getIncludeTagLib($content);
+
+ if (!empty($tagLibs)) {
+ // 对导入的TagLib进行解析
+ foreach ($tagLibs as $tagLibName) {
+ $this->parseTagLib($tagLibName, $content);
+ }
+ }
+ }
+
+ // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
+ if ($this->config['taglib_pre_load']) {
+ $tagLibs = explode(',', $this->config['taglib_pre_load']);
+
+ foreach ($tagLibs as $tag) {
+ $this->parseTagLib($tag, $content);
+ }
+ }
+
+ // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
+ $tagLibs = explode(',', $this->config['taglib_build_in']);
+
+ foreach ($tagLibs as $tag) {
+ $this->parseTagLib($tag, $content, true);
+ }
+
+ // 解析普通模板标签 {$tagName}
+ $this->parseTag($content);
+
+ // 还原被替换的Literal标签
+ $this->parseLiteral($content, true);
+ }
+
+ /**
+ * 检查PHP语法
+ * @access private
+ * @param string $content 要解析的模板内容
+ * @return void
+ * @throws Exception
+ */
+ private function parsePhp(string &$content): void
+ {
+ // 短标签的情况要将' . "\n", $content);
+
+ // PHP语法检查
+ if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) {
+ // 替换Layout标签
+ $content = str_replace($matches[0], '', $content);
+ // 解析Layout标签
+ $array = $this->parseAttr($matches[0]);
+
+ if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {
+ // 读取布局模板
+ $layoutFile = $this->parseTemplateFile($array['name']);
+
+ if ($layoutFile) {
+ $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];
+ // 替换布局的主体内容
+ $content = str_replace($replace, $content, file_get_contents($layoutFile));
+ }
+ }
+ } else {
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ }
+ }
+
+ /**
+ * 解析模板中的include标签
+ * @access private
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseInclude(string &$content): void
+ {
+ $regex = $this->getRegex('include');
+ $func = function ($template) use (&$func, &$regex, &$content) {
+ if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $array = $this->parseAttr($match[0]);
+ $file = $array['file'];
+ unset($array['file']);
+
+ // 分析模板文件名并读取内容
+ $parseStr = $this->parseTemplateName($file);
+
+ foreach ($array as $k => $v) {
+ // 以$开头字符串转换成模板变量
+ if (0 === strpos($v, '$')) {
+ $v = $this->get(substr($v, 1));
+ }
+
+ $parseStr = str_replace('[' . $k . ']', $v, $parseStr);
+ }
+
+ $content = str_replace($match[0], $parseStr, $content);
+ // 再次对包含文件进行模板分析
+ $func($parseStr);
+ }
+ unset($matches);
+ }
+ };
+
+ // 替换模板中的include标签
+ $func($content);
+ }
+
+ /**
+ * 解析模板中的extend标签
+ * @access private
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseExtend(string &$content): void
+ {
+ $regex = $this->getRegex('extend');
+ $array = $blocks = $baseBlocks = [];
+ $extend = '';
+
+ $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
+ if (preg_match($regex, $template, $matches)) {
+ if (!isset($array[$matches['name']])) {
+ $array[$matches['name']] = 1;
+ // 读取继承模板
+ $extend = $this->parseTemplateName($matches['name']);
+
+ // 递归检查继承
+ $func($extend);
+
+ // 取得block标签内容
+ $blocks = array_merge($blocks, $this->parseBlock($template));
+
+ return;
+ }
+ } else {
+ // 取得顶层模板block标签内容
+ $baseBlocks = $this->parseBlock($template, true);
+
+ if (empty($extend)) {
+ // 无extend标签但有block标签的情况
+ $extend = $template;
+ }
+ }
+ };
+
+ $func($content);
+
+ if (!empty($extend)) {
+ if ($baseBlocks) {
+ $children = [];
+ foreach ($baseBlocks as $name => $val) {
+ $replace = $val['content'];
+
+ if (!empty($children[$name])) {
+ // 如果包含有子block标签
+ foreach ($children[$name] as $key) {
+ $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace);
+ }
+ }
+
+ if (isset($blocks[$name])) {
+ // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
+ $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);
+
+ if (!empty($val['parent'])) {
+ // 如果不是最顶层的block标签
+ $parent = $val['parent'];
+
+ if (isset($blocks[$parent])) {
+ $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']);
+ }
+
+ $blocks[$name]['content'] = $replace;
+ $children[$parent][] = $name;
+
+ continue;
+ }
+ } elseif (!empty($val['parent'])) {
+ // 如果子标签没有被继承则用原值
+ $children[$val['parent']][] = $name;
+ $blocks[$name] = $val;
+ }
+
+ if (!$val['parent']) {
+ // 替换模板中的顶级block标签
+ $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend);
+ }
+ }
+ }
+
+ $content = $extend;
+ unset($blocks, $baseBlocks);
+ }
+ }
+
+ /**
+ * 替换页面中的literal标签
+ * @access private
+ * @param string $content 模板内容
+ * @param boolean $restore 是否为还原
+ * @return void
+ */
+ private function parseLiteral(string &$content, bool $restore = false): void
+ {
+ $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal');
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+ if (!$restore) {
+ $count = count($this->literal);
+
+ // 替换literal标签
+ foreach ($matches as $match) {
+ $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
+ $content = str_replace($match[0], "", $content);
+ $count++;
+ }
+ } else {
+ // 还原literal标签
+ foreach ($matches as $match) {
+ $content = str_replace($match[0], $this->literal[$match[1]], $content);
+ }
+
+ // 清空literal记录
+ $this->literal = [];
+ }
+
+ unset($matches);
+ }
+ }
+
+ /**
+ * 获取模板中的block标签
+ * @access private
+ * @param string $content 模板内容
+ * @param boolean $sort 是否排序
+ * @return array
+ */
+ private function parseBlock(string &$content, bool $sort = false): array
+ {
+ $regex = $this->getRegex('block');
+ $result = [];
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $right = $keys = [];
+
+ foreach ($matches as $match) {
+ if (empty($match['name'][0])) {
+ if (count($right) > 0) {
+ $tag = array_pop($right);
+ $start = $tag['offset'] + strlen($tag['tag']);
+ $length = $match[0][1] - $start;
+
+ $result[$tag['name']] = [
+ 'begin' => $tag['tag'],
+ 'content' => substr($content, $start, $length),
+ 'end' => $match[0][0],
+ 'parent' => count($right) ? end($right)['name'] : '',
+ ];
+
+ $keys[$tag['name']] = $match[0][1];
+ }
+ } else {
+ // 标签头压入栈
+ $right[] = [
+ 'name' => $match[2][0],
+ 'offset' => $match[0][1],
+ 'tag' => $match[0][0],
+ ];
+ }
+ }
+
+ unset($right, $matches);
+
+ if ($sort) {
+ // 按block标签结束符在模板中的位置排序
+ array_multisort($keys, $result);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 搜索模板页面中包含的TagLib库
+ * 并返回列表
+ * @access private
+ * @param string $content 模板内容
+ * @return array|null
+ */
+ private function getIncludeTagLib(string &$content)
+ {
+ // 搜索是否有TagLib标签
+ if (preg_match($this->getRegex('taglib'), $content, $matches)) {
+ // 替换TagLib标签
+ $content = str_replace($matches[0], '', $content);
+
+ return explode(',', $matches['name']);
+ }
+ }
+
+ /**
+ * TagLib库解析
+ * @access public
+ * @param string $tagLib 要解析的标签库
+ * @param string $content 要解析的模板内容
+ * @param boolean $hide 是否隐藏标签库前缀
+ * @return void
+ */
+ public function parseTagLib(string $tagLib, string &$content, bool $hide = false): void
+ {
+ if (false !== strpos($tagLib, '\\')) {
+ // 支持指定标签库的命名空间
+ $className = $tagLib;
+ $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1);
+ } else {
+ $className = '\\think\\template\\taglib\\' . ucwords($tagLib);
+ }
+
+ $tLib = new $className($this);
+
+ $tLib->parseTag($content, $hide ? '' : $tagLib);
+ }
+
+ /**
+ * 分析标签属性
+ * @access public
+ * @param string $str 属性字符串
+ * @param string $name 不为空时返回指定的属性名
+ * @return array
+ */
+ public function parseAttr(string $str, string $name = null): array
+ {
+ $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is';
+ $array = [];
+
+ if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $array[$match['name']] = $match['value'];
+ }
+ unset($matches);
+ }
+
+ if (!empty($name) && isset($array[$name])) {
+ return $array[$name];
+ }
+
+ return $array;
+ }
+
+ /**
+ * 模板标签解析
+ * 格式: {TagName:args [|content] }
+ * @access private
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseTag(string &$content): void
+ {
+ $regex = $this->getRegex('tag');
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $str = stripslashes($match[1]);
+ $flag = substr($str, 0, 1);
+
+ switch ($flag) {
+ case '$':
+ // 解析模板变量 格式 {$varName}
+ // 是否带有?号
+ if (false !== $pos = strpos($str, '?')) {
+ $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
+ $name = $array[0];
+
+ $this->parseVar($name);
+ //$this->parseVarFunction($name);
+
+ $str = trim(substr($str, $pos + 1));
+ $this->parseVar($str);
+ $first = substr($str, 0, 1);
+
+ if (strpos($name, ')')) {
+ // $name为对象或是自动识别,或者含有函数
+ if (isset($array[1])) {
+ $this->parseVar($array[2]);
+ $name .= $array[1] . $array[2];
+ }
+
+ switch ($first) {
+ case '?':
+ $this->parseVarFunction($name);
+ $str = '';
+ break;
+ case '=':
+ $str = '';
+ break;
+ default:
+ $str = '';
+ }
+ } else {
+ if (isset($array[1])) {
+ $express = true;
+ $this->parseVar($array[2]);
+ $express = $name . $array[1] . $array[2];
+ } else {
+ $express = false;
+ }
+
+ if (in_array($first, ['?', '=', ':'])) {
+ $str = trim(substr($str, 1));
+ if ('$' == substr($str, 0, 1)) {
+ $str = $this->parseVarFunction($str);
+ }
+ }
+
+ // $name为数组
+ switch ($first) {
+ case '?':
+ // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx
+ $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>';
+ break;
+ case '=':
+ // {$varname?='xxx'} $varname为真时才输出xxx
+ $str = '';
+ break;
+ case ':':
+ // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx
+ $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>';
+ break;
+ default:
+ if (strpos($str, ':')) {
+ // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b
+ $array = explode(':', $str, 2);
+
+ $array[0] = '$' == substr(trim($array[0]), 0, 1) ? $this->parseVarFunction($array[0]) : $array[0];
+ $array[1] = '$' == substr(trim($array[1]), 0, 1) ? $this->parseVarFunction($array[1]) : $array[1];
+
+ $str = implode(' : ', $array);
+ }
+ $str = '';
+ }
+ }
+ } else {
+ $this->parseVar($str);
+ $this->parseVarFunction($str);
+ $str = '';
+ }
+ break;
+ case ':':
+ // 输出某个函数的结果
+ $str = substr($str, 1);
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '~':
+ // 执行某个函数
+ $str = substr($str, 1);
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '-':
+ case '+':
+ // 输出计算
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '/':
+ // 注释标签
+ $flag2 = substr($str, 1, 1);
+ if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) {
+ $str = '';
+ }
+ break;
+ default:
+ // 未识别的标签直接返回
+ $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end'];
+ break;
+ }
+
+ $content = str_replace($match[0], $str, $content);
+ }
+
+ unset($matches);
+ }
+ }
+
+ /**
+ * 模板变量解析,支持使用函数
+ * 格式: {$varname|function1|function2=arg1,arg2}
+ * @access public
+ * @param string $varStr 变量数据
+ * @return void
+ */
+ public function parseVar(string &$varStr): void
+ {
+ $varStr = trim($varStr);
+
+ if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
+ static $_varParseList = [];
+
+ while ($matches[0]) {
+ $match = array_pop($matches[0]);
+
+ //如果已经解析过该变量字串,则直接返回变量值
+ if (isset($_varParseList[$match[0]])) {
+ $parseStr = $_varParseList[$match[0]];
+ } else {
+ if (strpos($match[0], '.')) {
+ $vars = explode('.', $match[0]);
+ $first = array_shift($vars);
+
+ if (isset($this->extend[$first])) {
+ $callback = $this->extend[$first];
+ $parseStr = $callback($vars);
+ } elseif ('$Request' == $first) {
+ // 输出请求变量
+ $parseStr = $this->parseRequestVar($vars);
+ } elseif ('$Think' == $first) {
+ // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
+ $parseStr = $this->parseThinkVar($vars);
+ } else {
+ switch ($this->config['tpl_var_identify']) {
+ case 'array': // 识别为数组
+ $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']';
+ break;
+ case 'obj': // 识别为对象
+ $parseStr = $first . '->' . implode('->', $vars);
+ break;
+ default: // 自动判断数组或对象
+ $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')';
+ }
+ }
+ } else {
+ $parseStr = str_replace(':', '->', $match[0]);
+ }
+
+ $_varParseList[$match[0]] = $parseStr;
+ }
+
+ $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0]));
+ }
+ unset($matches);
+ }
+ }
+
+ /**
+ * 对模板中使用了函数的变量进行解析
+ * 格式 {$varname|function1|function2=arg1,arg2}
+ * @access public
+ * @param string $varStr 变量字符串
+ * @param bool $autoescape 自动转义
+ * @return string
+ */
+ public function parseVarFunction(string &$varStr, bool $autoescape = true): string
+ {
+ if (!$autoescape && false === strpos($varStr, '|')) {
+ return $varStr;
+ } elseif ($autoescape && !preg_match('/\|(\s)?raw(\||\s)?/i', $varStr)) {
+ $varStr .= '|' . $this->config['default_filter'];
+ }
+
+ static $_varFunctionList = [];
+
+ $_key = md5($varStr);
+
+ //如果已经解析过该变量字串,则直接返回变量值
+ if (isset($_varFunctionList[$_key])) {
+ $varStr = $_varFunctionList[$_key];
+ } else {
+ $varArray = explode('|', $varStr);
+
+ // 取得变量名称
+ $name = trim(array_shift($varArray));
+
+ // 对变量使用函数
+ $length = count($varArray);
+
+ // 取得模板禁止使用函数列表
+ $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']);
+
+ for ($i = 0; $i < $length; $i++) {
+ $args = explode('=', $varArray[$i], 2);
+
+ // 模板函数过滤
+ $fun = trim($args[0]);
+ if (in_array($fun, $template_deny_funs)) {
+ continue;
+ }
+
+ switch (strtolower($fun)) {
+ case 'raw':
+ break;
+ case 'date':
+ $name = 'date(' . $args[1] . ',!is_numeric(' . $name . ')? strtotime(' . $name . ') : ' . $name . ')';
+ break;
+ case 'first':
+ $name = 'current(' . $name . ')';
+ break;
+ case 'last':
+ $name = 'end(' . $name . ')';
+ break;
+ case 'upper':
+ $name = 'strtoupper(' . $name . ')';
+ break;
+ case 'lower':
+ $name = 'strtolower(' . $name . ')';
+ break;
+ case 'format':
+ $name = 'sprintf(' . $args[1] . ',' . $name . ')';
+ break;
+ case 'default': // 特殊模板函数
+ if (false === strpos($name, '(')) {
+ $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')';
+ } else {
+ $name = '(' . $name . ' ?: ' . $args[1] . ')';
+ }
+ break;
+ default: // 通用模板函数
+ if (isset($args[1])) {
+ if (strstr($args[1], '###')) {
+ $args[1] = str_replace('###', $name, $args[1]);
+ $name = "$fun($args[1])";
+ } else {
+ $name = "$fun($name,$args[1])";
+ }
+ } else {
+ if (!empty($args[0])) {
+ $name = "$fun($name)";
+ }
+ }
+ }
+ }
+
+ $_varFunctionList[$_key] = $name;
+ $varStr = $name;
+ }
+ return $varStr;
+ }
+
+ /**
+ * 请求变量解析
+ * 格式 以 $Request. 打头的变量属于请求变量
+ * @access public
+ * @param array $vars 变量数组
+ * @return string
+ */
+ public function parseRequestVar(array $vars): string
+ {
+ $type = strtoupper(trim(array_shift($vars)));
+ $param = implode('.', $vars);
+
+ switch ($type) {
+ case 'SERVER':
+ $parseStr = '$_SERVER[\'' . $param . '\']';
+ break;
+ case 'GET':
+ $parseStr = '$_GET[\'' . $param . '\']';
+ break;
+ case 'POST':
+ $parseStr = '$_POST[\'' . $param . '\']';
+ break;
+ case 'COOKIE':
+ $parseStr = '$_COOKIE[\'' . $param . '\']';
+ break;
+ case 'SESSION':
+ $parseStr = '$_SESSION[\'' . $param . '\']';
+ break;
+ case 'ENV':
+ $parseStr = '$_ENV[\'' . $param . '\']';
+ break;
+ case 'REQUEST':
+ $parseStr = '$_REQUEST[\'' . $param . '\']';
+ break;
+ default:
+ $parseStr = '\'\'';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * 特殊模板变量解析
+ * 格式 以 $Think. 打头的变量属于特殊模板变量
+ * @access public
+ * @param array $vars 变量数组
+ * @return string
+ */
+ public function parseThinkVar(array $vars): string
+ {
+ $type = strtoupper(trim(array_shift($vars)));
+ $param = implode('.', $vars);
+
+ switch ($type) {
+ case 'CONST':
+ $parseStr = strtoupper($param);
+ break;
+ case 'NOW':
+ $parseStr = "date('Y-m-d g:i a',time())";
+ break;
+ case 'LDELIM':
+ $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\'';
+ break;
+ case 'RDELIM':
+ $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\'';
+ break;
+ default:
+ $parseStr = defined($type) ? $type : '\'\'';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * 分析加载的模板文件并读取内容 支持多个模板文件读取
+ * @access private
+ * @param string $templateName 模板文件名
+ * @return string
+ */
+ private function parseTemplateName(string $templateName): string
+ {
+ $array = explode(',', $templateName);
+ $parseStr = '';
+
+ foreach ($array as $templateName) {
+ if (empty($templateName)) {
+ continue;
+ }
+
+ if (0 === strpos($templateName, '$')) {
+ //支持加载变量文件名
+ $templateName = $this->get(substr($templateName, 1));
+ }
+
+ $template = $this->parseTemplateFile($templateName);
+
+ if ($template) {
+ // 获取模板文件内容
+ $parseStr .= file_get_contents($template);
+ }
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * 解析模板文件名
+ * @access private
+ * @param string $template 文件名
+ * @return string
+ */
+ private function parseTemplateFile(string $template): string
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+
+ if (0 !== strpos($template, '/')) {
+ $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
+ } else {
+ $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1));
+ }
+
+ $template = $this->config['view_path'] . $template . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ if (is_file($template)) {
+ // 记录模板文件的更新时间
+ $this->includeFile[$template] = filemtime($template);
+
+ return $template;
+ }
+
+ throw new Exception('template not exists:' . $template);
+ }
+
+ /**
+ * 按标签生成正则
+ * @access private
+ * @param string $tagName 标签名
+ * @return string
+ */
+ private function getRegex(string $tagName): string
+ {
+ $regex = '';
+ if ('tag' == $tagName) {
+ $begin = $this->config['tpl_begin'];
+ $end = $this->config['tpl_end'];
+
+ if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {
+ $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end;
+ } else {
+ $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;
+ }
+ } else {
+ $begin = $this->config['taglib_begin'];
+ $end = $this->config['taglib_end'];
+ $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
+
+ switch ($tagName) {
+ case 'block':
+ if ($single) {
+ $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end;
+ } else {
+ $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end;
+ }
+ break;
+ case 'literal':
+ if ($single) {
+ $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')';
+ $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)';
+ $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+ } else {
+ $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')';
+ $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)';
+ $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+ }
+ break;
+ case 'restoreliteral':
+ $regex = '';
+ break;
+ case 'include':
+ $name = 'file';
+ case 'taglib':
+ case 'layout':
+ case 'extend':
+ if (empty($name)) {
+ $name = 'name';
+ }
+ if ($single) {
+ $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end;
+ } else {
+ $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end;
+ }
+ break;
+ }
+ }
+
+ return '/' . $regex . '/is';
+ }
+
+ public function __debugInfo()
+ {
+ $data = get_object_vars($this);
+ unset($data['storage']);
+
+ return $data;
+ }
+}
diff --git a/vendor/topthink/think-template/src/facade/Template.php b/vendor/topthink/think-template/src/facade/Template.php
new file mode 100644
index 0000000..5121863
--- /dev/null
+++ b/vendor/topthink/think-template/src/facade/Template.php
@@ -0,0 +1,83 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+if (class_exists('think\Facade')) {
+ class Facade extends \think\Facade
+ {}
+} else {
+ class Facade
+ {
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance;
+
+ protected static $instance;
+
+ /**
+ * 获取当前Facade对应类名
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {}
+
+ /**
+ * 创建Facade实例
+ * @static
+ * @access protected
+ * @return object
+ */
+ protected static function createFacade()
+ {
+ $class = static::getFacadeClass() ?: 'think\Template';
+
+ if (static::$alwaysNewInstance) {
+ 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);
+ }
+ }
+}
+
+/**
+ * @see \think\Template
+ * @mixin \think\Template
+ */
+class Template extends Facade
+{
+ protected static $alwaysNewInstance = true;
+
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'think\Template';
+ }
+}
diff --git a/vendor/topthink/think-template/src/template/TagLib.php b/vendor/topthink/think-template/src/template/TagLib.php
new file mode 100644
index 0000000..45df79e
--- /dev/null
+++ b/vendor/topthink/think-template/src/template/TagLib.php
@@ -0,0 +1,349 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template;
+
+use Exception;
+use think\Template;
+
+/**
+ * ThinkPHP标签库TagLib解析基类
+ * @category Think
+ * @package Think
+ * @subpackage Template
+ * @author liu21st
+ */
+class TagLib
+{
+
+ /**
+ * 标签库定义XML文件
+ * @var string
+ * @access protected
+ */
+ protected $xml = '';
+ protected $tags = []; // 标签定义
+ /**
+ * 标签库名称
+ * @var string
+ * @access protected
+ */
+ protected $tagLib = '';
+
+ /**
+ * 标签库标签列表
+ * @var array
+ * @access protected
+ */
+ protected $tagList = [];
+
+ /**
+ * 标签库分析数组
+ * @var array
+ * @access protected
+ */
+ protected $parse = [];
+
+ /**
+ * 标签库是否有效
+ * @var bool
+ * @access protected
+ */
+ protected $valid = false;
+
+ /**
+ * 当前模板对象
+ * @var object
+ * @access protected
+ */
+ protected $tpl;
+
+ protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < '];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Template $template 模板引擎对象
+ */
+ public function __construct(Template $template)
+ {
+ $this->tpl = $template;
+ }
+
+ /**
+ * 按签标库替换页面中的标签
+ * @access public
+ * @param string $content 模板内容
+ * @param string $lib 标签库名
+ * @return void
+ */
+ public function parseTag(string &$content, string $lib = ''): void
+ {
+ $tags = [];
+ $lib = $lib ? strtolower($lib) . ':' : '';
+
+ foreach ($this->tags as $name => $val) {
+ $close = !isset($val['close']) || $val['close'] ? 1 : 0;
+ $tags[$close][$lib . $name] = $name;
+ if (isset($val['alias'])) {
+ // 别名设置
+ $array = (array) $val['alias'];
+ foreach (explode(',', $array[0]) as $v) {
+ $tags[$close][$lib . $v] = $name;
+ }
+ }
+ }
+
+ // 闭合标签
+ if (!empty($tags[1])) {
+ $nodes = [];
+ $regex = $this->getRegex(array_keys($tags[1]), 1);
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $right = [];
+ foreach ($matches as $match) {
+ if ('' == $match[1][0]) {
+ $name = strtolower($match[2][0]);
+ // 如果有没闭合的标签头则取出最后一个
+ if (!empty($right[$name])) {
+ // $match[0][1]为标签结束符在模板中的位置
+ $nodes[$match[0][1]] = [
+ 'name' => $name,
+ 'begin' => array_pop($right[$name]), // 标签开始符
+ 'end' => $match[0], // 标签结束符
+ ];
+ }
+ } else {
+ // 标签头压入栈
+ $right[strtolower($match[1][0])][] = $match[0];
+ }
+ }
+ unset($right, $matches);
+ // 按标签在模板中的位置从后向前排序
+ krsort($nodes);
+ }
+
+ $break = '';
+ if ($nodes) {
+ $beginArray = [];
+ // 标签替换 从后向前
+ foreach ($nodes as $pos => $node) {
+ // 对应的标签名
+ $name = $tags[1][$node['name']];
+ $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : '';
+
+ // 解析标签属性
+ $attrs = $this->parseAttr($node['begin'][0], $name, $alias);
+ $method = 'tag' . $name;
+
+ // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾
+ $replace = explode($break, $this->$method($attrs, $break));
+
+ if (count($replace) > 1) {
+ while ($beginArray) {
+ $begin = end($beginArray);
+ // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签
+ if ($node['end'][1] > $begin['pos']) {
+ break;
+ } else {
+ // 不为子标签时,取出栈中最后一个标签头
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ // 替换标签尾部
+ $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0]));
+ // 把标签头压入栈
+ $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]];
+ }
+ }
+
+ while ($beginArray) {
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ }
+ // 自闭合标签
+ if (!empty($tags[0])) {
+ $regex = $this->getRegex(array_keys($tags[0]), 0);
+ $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) {
+ // 对应的标签名
+ $name = $tags[0][strtolower($matches[1])];
+ $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : '';
+ // 解析标签属性
+ $attrs = $this->parseAttr($matches[0], $name, $alias);
+ $method = 'tag' . $name;
+ return $this->$method($attrs, '');
+ }, $content);
+ }
+ }
+
+ /**
+ * 按标签生成正则
+ * @access public
+ * @param array|string $tags 标签名
+ * @param boolean $close 是否为闭合标签
+ * @return string
+ */
+ public function getRegex($tags, bool $close): string
+ {
+ $begin = $this->tpl->getConfig('taglib_begin');
+ $end = $this->tpl->getConfig('taglib_end');
+ $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
+ $tagName = is_array($tags) ? implode('|', $tags) : $tags;
+
+ if ($single) {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end;
+ }
+ } else {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end;
+ }
+ }
+
+ return '/' . $regex . '/is';
+ }
+
+ /**
+ * 分析标签属性 正则方式
+ * @access public
+ * @param string $str 标签属性字符串
+ * @param string $name 标签名
+ * @param string $alias 别名
+ * @return array
+ */
+ public function parseAttr(string $str, string $name, string $alias = ''): array
+ {
+ $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is';
+ $result = [];
+
+ if (preg_match_all($regex, $str, $matches)) {
+ foreach ($matches['name'] as $key => $val) {
+ $result[$val] = $matches['value'][$key];
+ }
+
+ if (!isset($this->tags[$name])) {
+ // 检测是否存在别名定义
+ foreach ($this->tags as $key => $val) {
+ if (isset($val['alias'])) {
+ $array = (array) $val['alias'];
+ if (in_array($name, explode(',', $array[0]))) {
+ $tag = $val;
+ $type = !empty($array[1]) ? $array[1] : 'type';
+ $result[$type] = $name;
+ break;
+ }
+ }
+ }
+ } else {
+ $tag = $this->tags[$name];
+ // 设置了标签别名
+ if (!empty($alias) && isset($tag['alias'])) {
+ $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type';
+ $result[$type] = $alias;
+ }
+ }
+
+ if (!empty($tag['must'])) {
+ $must = explode(',', $tag['must']);
+ foreach ($must as $name) {
+ if (!isset($result[$name])) {
+ throw new Exception('tag attr must:' . $name);
+ }
+ }
+ }
+ } else {
+ // 允许直接使用表达式的标签
+ if (!empty($this->tags[$name]['expression'])) {
+ static $_taglibs;
+ if (!isset($_taglibs[$name])) {
+ $_taglibs[$name][0] = strlen($this->tpl->getConfig('taglib_begin_origin') . $name);
+ $_taglibs[$name][1] = strlen($this->tpl->getConfig('taglib_end_origin'));
+ }
+ $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]);
+ // 清除自闭合标签尾部/
+ $result['expression'] = rtrim($result['expression'], '/');
+ $result['expression'] = trim($result['expression']);
+ } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) {
+ throw new Exception('tag error:' . $name);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析条件表达式
+ * @access public
+ * @param string $condition 表达式标签内容
+ * @return string
+ */
+ public function parseCondition(string $condition): string
+ {
+ if (strpos($condition, ':')) {
+ $condition = ' ' . substr(strstr($condition, ':'), 1);
+ }
+
+ $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition);
+ $this->tpl->parseVar($condition);
+
+ return $condition;
+ }
+
+ /**
+ * 自动识别构建变量
+ * @access public
+ * @param string $name 变量描述
+ * @return string
+ */
+ public function autoBuildVar(string &$name): string
+ {
+ $flag = substr($name, 0, 1);
+
+ if (':' == $flag) {
+ // 以:开头为函数调用,解析前去掉:
+ $name = substr($name, 1);
+ } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) {
+ // XXX: 这句的写法可能还需要改进
+ // 常量不需要解析
+ if (defined($name)) {
+ return $name;
+ }
+
+ // 不以$开头并且也不是常量,自动补上$前缀
+ $name = '$' . $name;
+ }
+
+ $this->tpl->parseVar($name);
+ $this->tpl->parseVarFunction($name, false);
+
+ return $name;
+ }
+
+ /**
+ * 获取标签列表
+ * @access public
+ * @return array
+ */
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+}
diff --git a/vendor/topthink/think-template/src/template/driver/File.php b/vendor/topthink/think-template/src/template/driver/File.php
new file mode 100644
index 0000000..3f2a3a6
--- /dev/null
+++ b/vendor/topthink/think-template/src/template/driver/File.php
@@ -0,0 +1,83 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\driver;
+
+use Exception;
+
+class File
+{
+ protected $cacheFile;
+
+ /**
+ * 写入编译缓存
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param string $content 缓存的内容
+ * @return void
+ */
+ public function write(string $cacheFile, string $content): void
+ {
+ // 检测模板目录
+ $dir = dirname($cacheFile);
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ // 生成模板缓存文件
+ if (false === file_put_contents($cacheFile, $content)) {
+ throw new Exception('cache write error:' . $cacheFile, 11602);
+ }
+ }
+
+ /**
+ * 读取编译编译
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param array $vars 变量数组
+ * @return void
+ */
+ public function read(string $cacheFile, array $vars = []): void
+ {
+ $this->cacheFile = $cacheFile;
+
+ if (!empty($vars) && is_array($vars)) {
+ // 模板阵列变量分解成为独立变量
+ extract($vars, EXTR_OVERWRITE);
+ }
+
+ //载入模版缓存文件
+ include $this->cacheFile;
+ }
+
+ /**
+ * 检查编译缓存是否有效
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param int $cacheTime 缓存时间
+ * @return bool
+ */
+ public function check(string $cacheFile, int $cacheTime): bool
+ {
+ // 缓存文件不存在, 直接返回false
+ if (!file_exists($cacheFile)) {
+ return false;
+ }
+
+ if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) {
+ // 缓存是否在有效期
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/vendor/topthink/think-template/src/template/exception/TemplateNotFoundException.php b/vendor/topthink/think-template/src/template/exception/TemplateNotFoundException.php
new file mode 100644
index 0000000..0c45696
--- /dev/null
+++ b/vendor/topthink/think-template/src/template/exception/TemplateNotFoundException.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\exception;
+
+class TemplateNotFoundException extends \RuntimeException
+{
+ protected $template;
+
+ public function __construct(string $message, string $template = '')
+ {
+ $this->message = $message;
+ $this->template = $template;
+ }
+
+ /**
+ * 获取模板文件
+ * @access public
+ * @return string
+ */
+ public function getTemplate(): string
+ {
+ return $this->template;
+ }
+}
diff --git a/vendor/topthink/think-template/src/template/taglib/Cx.php b/vendor/topthink/think-template/src/template/taglib/Cx.php
new file mode 100644
index 0000000..32d71c8
--- /dev/null
+++ b/vendor/topthink/think-template/src/template/taglib/Cx.php
@@ -0,0 +1,715 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\taglib;
+
+use think\template\TagLib;
+
+/**
+ * CX标签库解析类
+ * @category Think
+ * @package Think
+ * @subpackage Driver.Taglib
+ * @author liu21st
+ */
+class Cx extends Taglib
+{
+
+ // 标签定义
+ protected $tags = [
+ // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次
+ 'php' => ['attr' => ''],
+ 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'],
+ 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true],
+ 'if' => ['attr' => 'condition', 'expression' => true],
+ 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true],
+ 'else' => ['attr' => '', 'close' => 0],
+ 'switch' => ['attr' => 'name', 'expression' => true],
+ 'case' => ['attr' => 'value,break', 'expression' => true],
+ 'default' => ['attr' => '', 'close' => 0],
+ 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']],
+ 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']],
+ 'empty' => ['attr' => 'name'],
+ 'notempty' => ['attr' => 'name'],
+ 'present' => ['attr' => 'name'],
+ 'notpresent' => ['attr' => 'name'],
+ 'defined' => ['attr' => 'name'],
+ 'notdefined' => ['attr' => 'name'],
+ 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']],
+ 'assign' => ['attr' => 'name,value', 'close' => 0],
+ 'define' => ['attr' => 'name,value', 'close' => 0],
+ 'for' => ['attr' => 'start,end,name,comparison,step'],
+ 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true],
+ 'function' => ['attr' => 'name,vars,use,call'],
+ ];
+
+ /**
+ * php标签解析
+ * 格式:
+ * {php}echo $name{/php}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPhp(array $tag, string $content): string
+ {
+ $parseStr = '';
+ return $parseStr;
+ }
+
+ /**
+ * volist标签解析 循环输出数据集
+ * 格式:
+ * {volist name="userList" id="user" empty=""}
+ * {user.username}
+ * {user.email}
+ * {/volist}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagVolist(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $id = $tag['id'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $key = !empty($tag['key']) ? $tag['key'] : 'i';
+ $mod = isset($tag['mod']) ? $tag['mod'] : '2';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+ // 允许使用函数设定数据集 {$vo.name}
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= '$_result=' . $name . ';';
+ $name = '$_result';
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $parseStr .= ' $__LIST__ = ' . $name . ';';
+ }
+
+ $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+ $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): ';
+ $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );';
+ $parseStr .= '++$' . $key . ';?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * foreach标签解析 循环输出数据集
+ * 格式:
+ * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""}
+ * {user.username}
+ * {/foreach}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagForeach(array $tag, string $content): string
+ {
+ // 直接使用表达式
+ if (!empty($tag['expression'])) {
+ $expression = ltrim(rtrim($tag['expression'], ')'), '(');
+ $expression = $this->autoBuildVar($expression);
+ $parseStr = '';
+ $parseStr .= $content;
+ $parseStr .= '';
+ return $parseStr;
+ }
+
+ $name = $tag['name'];
+ $key = !empty($tag['key']) ? $tag['key'] : 'key';
+ $item = !empty($tag['id']) ? $tag['id'] : $tag['item'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= $var . '=' . $name . '; ';
+ $name = $var;
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): ';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ if (!isset($var)) {
+ $var = '$_' . uniqid();
+ }
+ $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $var = &$name;
+ }
+
+ $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ $parseStr .= '$' . $index . '=0; ';
+ }
+
+ $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ if (isset($tag['mod'])) {
+ $mod = (int) $tag['mod'];
+ $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); ';
+ }
+ $parseStr .= '++$' . $index . '; ';
+ }
+
+ $parseStr .= '?>';
+ // 循环体中的内容
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * if标签解析
+ * 格式:
+ * {if condition=" $a eq 1"}
+ * {elseif condition="$a eq 2" /}
+ * {else /}
+ * {/if}
+ * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || &&
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagIf(array $tag, string $content): string
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * elseif标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagElseif(array $tag, string $content): string
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * else标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @return string
+ */
+ public function tagElse(array $tag): string
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * switch标签解析
+ * 格式:
+ * {switch name="a.name"}
+ * {case value="1" break="false"}1{/case}
+ * {case value="2" }2{/case}
+ * {default /}other
+ * {/switch}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagSwitch(array $tag, string $content): string
+ {
+ $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * case标签解析 需要配合switch才有效
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCase(array $tag, string $content): string
+ {
+ $value = isset($tag['expression']) ? $tag['expression'] : $tag['value'];
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $value = 'case ' . $value . ':';
+ } elseif (strpos($value, '|')) {
+ $values = explode('|', $value);
+ $value = '';
+ foreach ($values as $val) {
+ $value .= 'case "' . addslashes($val) . '":';
+ }
+ } else {
+ $value = 'case "' . $value . '":';
+ }
+
+ $parseStr = '' . $content;
+ $isBreak = isset($tag['break']) ? $tag['break'] : '';
+
+ if ('' == $isBreak || $isBreak) {
+ $parseStr .= '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * default标签解析 需要配合switch才有效
+ * 使用: {default /}ddfdf
+ * @access public
+ * @param array $tag 标签属性
+ * @return string
+ */
+ public function tagDefault(array $tag): string
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * compare标签解析
+ * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq
+ * 格式: {compare name="" type="eq" value="" }content{/compare}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCompare(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ } else {
+ $value = '\'' . $value . '\'';
+ }
+
+ switch ($type) {
+ case 'equal':
+ $type = 'eq';
+ break;
+ case 'notequal':
+ $type = 'neq';
+ break;
+ }
+ $type = $this->parseCondition(' ' . $type . ' ');
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * range标签解析
+ * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外
+ * 格式: {range name="var|function" value="val" type='in|notin' }content{/range}
+ * example: {range name="a" value="1,2,3" type='in' }content{/range}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagRange(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型
+
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')';
+ } else {
+ $value = '"' . $value . '"';
+ $str = 'explode(\',\',' . $value . ')';
+ }
+
+ if ('between' == $type) {
+ $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . '';
+ } elseif ('notbetween' == $type) {
+ $parseStr = '$_RANGE_VAR_[1]):?>' . $content . '';
+ } else {
+ $fun = ('in' == $type) ? 'in_array' : '!in_array';
+ $parseStr = '' . $content . '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * present标签解析
+ * 如果某个变量已经设置 则输出内容
+ * 格式: {present name="" }content{/present}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPresent(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notpresent标签解析
+ * 如果某个变量没有设置,则输出内容
+ * 格式: {notpresent name="" }content{/notpresent}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotpresent(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * empty标签解析
+ * 如果某个变量为empty 则输出内容
+ * 格式: {empty name="" }content{/empty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagEmpty(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty())): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notempty标签解析
+ * 如果某个变量不为empty 则输出内容
+ * 格式: {notempty name="" }content{/notempty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotempty(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty()))): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否已经定义了该常量
+ * {defined name='TXT'}已定义{/defined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagDefined(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否没有定义了该常量
+ * {notdefined name='TXT'}已定义{/notdefined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagNotdefined(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * load 标签解析 {load file="/static/js/base.js" /}
+ * 格式:{load file="/static/css/base.css" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagLoad(array $tag, string $content): string
+ {
+ $file = isset($tag['file']) ? $tag['file'] : $tag['href'];
+ $type = isset($tag['type']) ? strtolower($tag['type']) : '';
+
+ $parseStr = '';
+ $endStr = '';
+
+ // 判断是否存在加载条件 允许使用函数判断(默认为isset)
+ if (isset($tag['value'])) {
+ $name = $tag['value'];
+ $name = $this->autoBuildVar($name);
+ $name = 'isset(' . $name . ')';
+ $parseStr .= '';
+ $endStr = '';
+ }
+
+ // 文件方式导入
+ $array = explode(',', $file);
+
+ foreach ($array as $val) {
+ $type = strtolower(substr(strrchr($val, '.'), 1));
+ switch ($type) {
+ case 'js':
+ $parseStr .= '';
+ break;
+ case 'css':
+ $parseStr .= ' ';
+ break;
+ case 'php':
+ $parseStr .= '';
+ break;
+ }
+ }
+
+ return $parseStr . $endStr;
+ }
+
+ /**
+ * assign标签解析
+ * 在模板中给某个变量赋值 支持变量赋值
+ * 格式: {assign name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagAssign(array $tag, string $content): string
+ {
+ $name = $this->autoBuildVar($tag['name']);
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * define标签解析
+ * 在模板中定义常量 支持变量赋值
+ * 格式: {define name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagDefine(array $tag, string $content): string
+ {
+ $name = '\'' . $tag['name'] . '\'';
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * for标签解析
+ * 格式:
+ * {for start="" end="" comparison="" step="" name=""}
+ * content
+ * {/for}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFor(array $tag, string $content): string
+ {
+ //设置默认值
+ $start = 0;
+ $end = 0;
+ $step = 1;
+ $comparison = 'lt';
+ $name = 'i';
+ $rand = rand(); //添加随机数,防止嵌套变量冲突
+
+ //获取属性
+ foreach ($tag as $key => $value) {
+ $value = trim($value);
+ $flag = substr($value, 0, 1);
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ }
+
+ switch ($key) {
+ case 'start':
+ $start = $value;
+ break;
+ case 'end':
+ $end = $value;
+ break;
+ case 'step':
+ $step = $value;
+ break;
+ case 'comparison':
+ $comparison = $value;
+ break;
+ case 'name':
+ $name = $value;
+ break;
+ }
+ }
+
+ $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * url函数的tag标签
+ * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagUrl(array $tag, string $content): string
+ {
+ $url = isset($tag['link']) ? $tag['link'] : '';
+ $vars = isset($tag['vars']) ? $tag['vars'] : '';
+ $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true';
+ $domain = isset($tag['domain']) ? $tag['domain'] : 'false';
+
+ return '';
+ }
+
+ /**
+ * function标签解析 匿名函数,可实现递归
+ * 使用:
+ * {function name="func" vars="$data" call="$list" use="&$a,&$b"}
+ * {if is_array($data)}
+ * {foreach $data as $val}
+ * {~func($val) /}
+ * {/foreach}
+ * {else /}
+ * {$data}
+ * {/if}
+ * {/function}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFunction(array $tag, string $content): string
+ {
+ $name = !empty($tag['name']) ? $tag['name'] : 'func';
+ $vars = !empty($tag['vars']) ? $tag['vars'] : '';
+ $call = !empty($tag['call']) ? $tag['call'] : '';
+ $use = ['&$' . $name];
+
+ if (!empty($tag['use'])) {
+ foreach (explode(',', $tag['use']) as $val) {
+ $use[] = '&' . ltrim(trim($val), '&');
+ }
+ }
+
+ $parseStr = '' . $content . '' : '?>';
+
+ return $parseStr;
+ }
+}
diff --git a/vendor/topthink/think-trace/.gitignore b/vendor/topthink/think-trace/.gitignore
new file mode 100644
index 0000000..1da8522
--- /dev/null
+++ b/vendor/topthink/think-trace/.gitignore
@@ -0,0 +1 @@
+.idea
diff --git a/vendor/topthink/think-trace/LICENSE b/vendor/topthink/think-trace/LICENSE
new file mode 100644
index 0000000..29f81d8
--- /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/README.md b/vendor/topthink/think-trace/README.md
new file mode 100644
index 0000000..284c28e
--- /dev/null
+++ b/vendor/topthink/think-trace/README.md
@@ -0,0 +1,15 @@
+# think-trace
+
+用于ThinkPHP6+的页面Trace扩展,支持Html页面和浏览器控制台两种方式输出。
+
+## 安装
+
+~~~
+composer require topthink/think-trace
+~~~
+
+## 配置
+
+安装后config目录下会自带trace.php配置文件。
+
+type参数用于指定trace类型,支持html和console两种方式。
\ No newline at end of file
diff --git a/vendor/topthink/think-trace/composer.json b/vendor/topthink/think-trace/composer.json
new file mode 100644
index 0000000..8776aa2
--- /dev/null
+++ b/vendor/topthink/think-trace/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "topthink/think-trace",
+ "description": "thinkphp debug trace",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "topthink/framework": "^6.0.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\trace\\": "src"
+ }
+ },
+ "extra": {
+ "think":{
+ "services":[
+ "think\\trace\\Service"
+ ],
+ "config":{
+ "trace": "src/config.php"
+ }
+ }
+ },
+ "minimum-stability": "dev"
+}
diff --git a/vendor/topthink/think-trace/src/Console.php b/vendor/topthink/think-trace/src/Console.php
new file mode 100644
index 0000000..01140b5
--- /dev/null
+++ b/vendor/topthink/think-trace/src/Console.php
@@ -0,0 +1,170 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\trace;
+
+use think\App;
+use think\Response;
+
+/**
+ * 浏览器调试输出
+ */
+class Console
+{
+ protected $config = [
+ '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 Response $response Response对象
+ * @param array $log 日志信息
+ * @return string|bool
+ */
+ public function output(App $app, Response $response, array $log = [])
+ {
+ $request = $app->request;
+ $contentType = $response->getHeader('Content-Type');
+ $accept = $request->header('accept', '');
+ if (strpos($accept, 'application/json') === 0 || $request->isAjax()) {
+ return false;
+ } elseif (!empty($contentType) && strpos($contentType, 'html') === false) {
+ 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);
+
+ if ($request->host()) {
+ $uri = $request->protocol() . ' ' . $request->method() . ' : ' . $request->url(true);
+ } else {
+ $uri = 'cmd:' . implode(' ', $_SERVER['argv']);
+ }
+
+ // 页面Trace信息
+ $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] ?? '';
+ }
+ }
+ }
+
+ //输出到控制台
+ $lines = '';
+ foreach ($trace as $type => $msg) {
+ $lines .= $this->console($type, empty($msg) ? [] : $msg);
+ }
+ $js = <<
+{$lines}
+
+JS;
+ return $js;
+ }
+
+ protected function console(string $type, $msg)
+ {
+ $type = strtolower($type);
+ $trace_tabs = array_values($this->config['tabs']);
+ $line = [];
+ $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type)
+ ? "console.group('{$type}');"
+ : "console.groupCollapsed('{$type}');";
+
+ foreach ((array) $msg as $key => $m) {
+ switch ($type) {
+ case '调试':
+ $var_type = gettype($m);
+ if (in_array($var_type, ['array', 'string'])) {
+ $line[] = "console.log(" . json_encode($m) . ");";
+ } else {
+ $line[] = "console.log(" . json_encode(var_export($m, true)) . ");";
+ }
+ break;
+ case '错误':
+ $msg = str_replace("\n", '\n', addslashes(is_scalar($m) ? $m : json_encode($m)));
+ $style = 'color:#F4006B;font-size:14px;';
+ $line[] = "console.error(\"%c{$msg}\", \"{$style}\");";
+ break;
+ case 'sql':
+ $msg = str_replace("\n", '\n', addslashes($m));
+ $style = "color:#009bb4;";
+ $line[] = "console.log(\"%c{$msg}\", \"{$style}\");";
+ break;
+ default:
+ $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m;
+ $msg = json_encode($m);
+ $line[] = "console.log({$msg});";
+ break;
+ }
+ }
+ $line[] = "console.groupEnd();";
+ return implode(PHP_EOL, $line);
+ }
+
+ /**
+ * 获取文件加载信息
+ * @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/Html.php b/vendor/topthink/think-trace/src/Html.php
new file mode 100644
index 0000000..aeb5e1b
--- /dev/null
+++ b/vendor/topthink/think-trace/src/Html.php
@@ -0,0 +1,125 @@
+
+// +----------------------------------------------------------------------
+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');
+ $accept = $request->header('accept', '');
+ if (strpos($accept, 'application/json') === 0 || $request->isAjax()) {
+ return false;
+ } elseif (!empty($contentType) && strpos($contentType, 'html') === false) {
+ 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/Service.php b/vendor/topthink/think-trace/src/Service.php
new file mode 100644
index 0000000..df78519
--- /dev/null
+++ b/vendor/topthink/think-trace/src/Service.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+namespace think\trace;
+
+use think\Service as BaseService;
+
+class Service extends BaseService
+{
+ public function register()
+ {
+ $this->app->middleware->add(TraceDebug::class);
+ }
+}
diff --git a/vendor/topthink/think-trace/src/TraceDebug.php b/vendor/topthink/think-trace/src/TraceDebug.php
new file mode 100644
index 0000000..1a9695b
--- /dev/null
+++ b/vendor/topthink/think-trace/src/TraceDebug.php
@@ -0,0 +1,109 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\trace;
+
+use Closure;
+use think\App;
+use think\Config;
+use think\event\LogWrite;
+use think\Request;
+use think\Response;
+use think\response\Redirect;
+
+/**
+ * 页面Trace中间件
+ */
+class TraceDebug
+{
+
+ /**
+ * Trace日志
+ * @var array
+ */
+ protected $log = [];
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [];
+
+ /** @var App */
+ protected $app;
+
+ public function __construct(App $app, Config $config)
+ {
+ $this->app = $app;
+ $this->config = $config->get('trace');
+ }
+
+ /**
+ * 页面Trace调试
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return void
+ */
+ public function handle($request, Closure $next)
+ {
+ $debug = $this->app->isDebug();
+
+ // 注册日志监听
+ if ($debug) {
+ $this->log = [];
+ $this->app->event->listen(LogWrite::class, function ($event) {
+ if (empty($this->config['channel']) || $this->config['channel'] == $event->channel) {
+ $this->log = array_merge_recursive($this->log, $event->log);
+ }
+ });
+ }
+
+ $response = $next($request);
+
+ // Trace调试注入
+ if ($debug) {
+ $data = $response->getContent();
+ $this->traceDebug($response, $data);
+ $response->content($data);
+ }
+
+ return $response;
+ }
+
+ public function traceDebug(Response $response, &$content)
+ {
+ $config = $this->config;
+ $type = $config['type'] ?? 'Html';
+
+ unset($config['type']);
+
+ $trace = App::factory($type, '\\think\\trace\\', $config);
+
+ if ($response instanceof Redirect) {
+ //TODO 记录
+ } else {
+ $log = $this->app->log->getLog($config['channel'] ?? '');
+ $log = array_merge_recursive($this->log, $log);
+ $output = $trace->output($this->app, $response, $log);
+ if (is_string($output)) {
+ // trace调试信息注入
+ $pos = strripos($content, '');
+ if (false !== $pos) {
+ $content = substr($content, 0, $pos) . $output . substr($content, $pos);
+ } else {
+ $content = $content . $output;
+ }
+ }
+ }
+ }
+}
diff --git a/vendor/topthink/think-trace/src/config.php b/vendor/topthink/think-trace/src/config.php
new file mode 100644
index 0000000..0fd4b4c
--- /dev/null
+++ b/vendor/topthink/think-trace/src/config.php
@@ -0,0 +1,10 @@
+ 'Html',
+ // 读取的日志通道名
+ 'channel' => '',
+];
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..962bbf8
--- /dev/null
+++ b/vendor/topthink/think-trace/src/tpl/page_trace.tpl
@@ -0,0 +1,71 @@
+
+
+
+ $value) {?>
+
+
+
+
+
+
+
+ $val) {
+ echo '' . (is_numeric($k) ? '' : $k.' : ') . htmlentities(print_r($val,true), ENT_COMPAT, 'utf-8') . ' ';
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/topthink/think-view/.gitignore b/vendor/topthink/think-view/.gitignore
new file mode 100644
index 0000000..1da8522
--- /dev/null
+++ b/vendor/topthink/think-view/.gitignore
@@ -0,0 +1 @@
+.idea
diff --git a/vendor/topthink/think-view/LICENSE b/vendor/topthink/think-view/LICENSE
new file mode 100644
index 0000000..c0ee812
--- /dev/null
+++ b/vendor/topthink/think-view/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-view/README.md b/vendor/topthink/think-view/README.md
new file mode 100644
index 0000000..3fdcd64
--- /dev/null
+++ b/vendor/topthink/think-view/README.md
@@ -0,0 +1,36 @@
+# think-view
+
+ThinkPHP6.0 Think-Template模板引擎驱动
+
+
+## 安装
+
+~~~php
+composer require topthink/think-view
+~~~
+
+## 用法示例
+
+本扩展不能单独使用,依赖ThinkPHP6.0+
+
+首先配置config目录下的template.php配置文件,然后可以按照下面的用法使用。
+
+~~~php
+
+use think\facade\View;
+
+// 模板变量赋值和渲染输出
+View::assign(['name' => 'think'])
+ // 输出过滤
+ ->filter(function($content){
+ return str_replace('search', 'replace', $content);
+ })
+ // 读取模板文件渲染输出
+ ->fetch('index');
+
+
+// 或者使用助手函数
+view('index', ['name' => 'think']);
+~~~
+
+具体的模板引擎配置请参考think-template库。
\ No newline at end of file
diff --git a/vendor/topthink/think-view/composer.json b/vendor/topthink/think-view/composer.json
new file mode 100644
index 0000000..33223b6
--- /dev/null
+++ b/vendor/topthink/think-view/composer.json
@@ -0,0 +1,20 @@
+{
+ "name": "topthink/think-view",
+ "description": "thinkphp template driver",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "topthink/think-template": "^2.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\view\\driver\\": "src"
+ }
+ }
+}
diff --git a/vendor/topthink/think-view/src/Think.php b/vendor/topthink/think-view/src/Think.php
new file mode 100644
index 0000000..c5ef6dc
--- /dev/null
+++ b/vendor/topthink/think-view/src/Think.php
@@ -0,0 +1,259 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\view\driver;
+
+use think\App;
+use think\helper\Str;
+use think\Template;
+use think\template\exception\TemplateNotFoundException;
+
+class Think
+{
+ // 模板引擎实例
+ private $template;
+ private $app;
+
+ // 模板引擎参数
+ protected $config = [
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+ 'auto_rule' => 1,
+ // 视图目录名
+ 'view_dir_name' => 'view',
+ // 模板起始路径
+ 'view_path' => '',
+ // 模板文件后缀
+ 'view_suffix' => 'html',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ // 是否开启模板编译缓存,设为false则每次都会重新编译
+ 'tpl_cache' => true,
+ ];
+
+ public function __construct(App $app, array $config = [])
+ {
+ $this->app = $app;
+
+ $this->config = array_merge($this->config, (array) $config);
+
+ if (empty($this->config['cache_path'])) {
+ $this->config['cache_path'] = $app->getRuntimePath() . 'temp' . DIRECTORY_SEPARATOR;
+ }
+
+ $this->template = new Template($this->config);
+ $this->template->setCache($app->cache);
+ $this->template->extend('$Think', function (array $vars) {
+ $type = strtoupper(trim(array_shift($vars)));
+ $param = implode('.', $vars);
+
+ switch ($type) {
+ case 'CONST':
+ $parseStr = strtoupper($param);
+ break;
+ case 'CONFIG':
+ $parseStr = 'config(\'' . $param . '\')';
+ break;
+ case 'LANG':
+ $parseStr = 'lang(\'' . $param . '\')';
+ break;
+ case 'NOW':
+ $parseStr = "date('Y-m-d g:i a',time())";
+ break;
+ case 'LDELIM':
+ $parseStr = '\'' . ltrim($this->getConfig('tpl_begin'), '\\') . '\'';
+ break;
+ case 'RDELIM':
+ $parseStr = '\'' . ltrim($this->getConfig('tpl_end'), '\\') . '\'';
+ break;
+ default:
+ $parseStr = defined($type) ? $type : '\'\'';
+ }
+
+ return $parseStr;
+ });
+
+ $this->template->extend('$Request', function (array $vars) {
+ // 获取Request请求对象参数
+ $method = array_shift($vars);
+ if (!empty($vars)) {
+ $params = implode('.', $vars);
+ if ('true' != $params) {
+ $params = '\'' . $params . '\'';
+ }
+ } else {
+ $params = '';
+ }
+
+ return 'app(\'request\')->' . $method . '(' . $params . ')';
+ });
+ }
+
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ return is_file($template);
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void
+ {
+ if (empty($this->config['view_path'])) {
+ $view = $this->config['view_dir_name'];
+
+ if (is_dir($this->app->getAppPath() . $view)) {
+ $path = $this->app->getAppPath() . $view . DIRECTORY_SEPARATOR;
+ } else {
+ $appName = $this->app->http->getName();
+ $path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . ($appName ? $appName . DIRECTORY_SEPARATOR : '');
+ }
+
+ $this->config['view_path'] = $path;
+ $this->template->view_path = $path;
+ }
+
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ // 模板不存在 抛出异常
+ if (!is_file($template)) {
+ throw new TemplateNotFoundException('template not exists:' . $template, $template);
+ }
+
+ $this->template->fetch($template, $data);
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $template 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $template, array $data = []): void
+ {
+ $this->template->display($template, $data);
+ }
+
+ /**
+ * 自动定位模板文件
+ * @access private
+ * @param string $template 模板文件规则
+ * @return string
+ */
+ private function parseTemplate(string $template): string
+ {
+ // 分析模板文件规则
+ $request = $this->app['request'];
+
+ // 获取视图根目录
+ if (strpos($template, '@')) {
+ // 跨模块调用
+ list($app, $template) = explode('@', $template);
+ }
+
+ if (isset($app)) {
+ $view = $this->config['view_dir_name'];
+ $viewPath = $this->app->getBasePath() . $app . DIRECTORY_SEPARATOR . $view . DIRECTORY_SEPARATOR;
+
+ if (is_dir($viewPath)) {
+ $path = $viewPath;
+ } else {
+ $path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR;
+ }
+
+ $this->template->view_path = $path;
+ } else {
+ $path = $this->config['view_path'];
+ }
+
+ $depr = $this->config['view_depr'];
+
+ if (0 !== strpos($template, '/')) {
+ $template = str_replace(['/', ':'], $depr, $template);
+ $controller = $request->controller();
+
+ if (strpos($controller, '.')) {
+ $pos = strrpos($controller, '.');
+ $controller = substr($controller, 0, $pos) . '.' . Str::snake(substr($controller, $pos + 1));
+ } else {
+ $controller = Str::snake($controller);
+ }
+
+ if ($controller) {
+ if ('' == $template) {
+ // 如果模板文件名为空 按照默认模板渲染规则定位
+ if (2 == $this->config['auto_rule']) {
+ $template = $request->action(true);
+ } elseif (3 == $this->config['auto_rule']) {
+ $template = $request->action();
+ } else {
+ $template = Str::snake($request->action());
+ }
+
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ } elseif (false === strpos($template, $depr)) {
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ }
+ }
+ } else {
+ $template = str_replace(['/', ':'], $depr, substr($template, 1));
+ }
+
+ return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ /**
+ * 配置模板引擎
+ * @access private
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void
+ {
+ $this->template->config($config);
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 获取模板引擎配置
+ * @access public
+ * @param string $name 参数名
+ * @return void
+ */
+ public function getConfig(string $name)
+ {
+ return $this->template->getConfig($name);
+ }
+
+ public function __call($method, $params)
+ {
+ return call_user_func_array([$this->template, $method], $params);
+ }
+}
diff --git a/view/article/index.html b/view/article/index.html
new file mode 100644
index 0000000..c5eb74f
--- /dev/null
+++ b/view/article/index.html
@@ -0,0 +1,3 @@
+{layout name="layout" /}
+
+新闻列表
\ No newline at end of file
diff --git a/view/error/400.html b/view/error/400.html
new file mode 100644
index 0000000..8f2ceed
--- /dev/null
+++ b/view/error/400.html
@@ -0,0 +1,46 @@
+
+{__NOLAYOUT__}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 页面自动 跳转 等待时间:
+
+
立即返回
+
+
+
+
diff --git a/view/error/jump.html b/view/error/jump.html
new file mode 100644
index 0000000..acc0123
--- /dev/null
+++ b/view/error/jump.html
@@ -0,0 +1,54 @@
+
+{__NOLAYOUT__}
+
+
+
+
+ 跳转提示
+
+
+
+
+
+
+
+
+
:)
+
+
+
+
+
+
+
+
+
+ 页面自动 跳转 等待时间:
+
+
立即返回
+
+
+
+
diff --git a/view/index/index.html b/view/index/index.html
new file mode 100644
index 0000000..435fca9
--- /dev/null
+++ b/view/index/index.html
@@ -0,0 +1 @@
+首页
\ No newline at end of file
diff --git a/view/layout.html b/view/layout.html
new file mode 100644
index 0000000..8ad99ba
--- /dev/null
+++ b/view/layout.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ {$seoTitle??''}
+
+
+
+
+
+
+
+
+
+
+
+ {:widget('menu/index', ['categoryId' => $categoryId])}
+ {include file="public/banner"}
+ {__CONTENT__}
+ {include file="public/footer"}
+
+
diff --git a/view/manager/Invitation_template_class/add.html b/view/manager/Invitation_template_class/add.html
new file mode 100644
index 0000000..7a123f8
--- /dev/null
+++ b/view/manager/Invitation_template_class/add.html
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/view/manager/Invitation_template_class/edit.html b/view/manager/Invitation_template_class/edit.html
new file mode 100644
index 0000000..56c3c85
--- /dev/null
+++ b/view/manager/Invitation_template_class/edit.html
@@ -0,0 +1,42 @@
+
+
+ {empty name='item'}
+
无效请求,该信息不存在
+ {else /}
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/Invitation_template_class/index.html b/view/manager/Invitation_template_class/index.html
new file mode 100644
index 0000000..8500c2e
--- /dev/null
+++ b/view/manager/Invitation_template_class/index.html
@@ -0,0 +1,87 @@
+{layout name="manager/layout" /}
+
+
+
+ {empty name="items"}
+ 无记录
+ {else /}
+
+
+
+
+
+
+
+
+ 栏目
+ 操作
+
+
+ {foreach $items as $item}
+
+
+ {$item.title}
+
+
+
+ 添加下级权限
+
+
+ 向上
+
+
+ 向下
+
+
+ 编辑
+
+
+ 删除
+
+
+
+ {if isset($item['children']) && !empty($item['children'])}
+
+
+
+
+
+ {/if}
+
+
+ {/foreach}
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/article/add.html b/view/manager/article/add.html
new file mode 100644
index 0000000..adf786a
--- /dev/null
+++ b/view/manager/article/add.html
@@ -0,0 +1,138 @@
+
\ No newline at end of file
diff --git a/view/manager/article/edit.html b/view/manager/article/edit.html
new file mode 100644
index 0000000..81f5438
--- /dev/null
+++ b/view/manager/article/edit.html
@@ -0,0 +1,143 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
\ No newline at end of file
diff --git a/view/manager/backup/index.html b/view/manager/backup/index.html
new file mode 100644
index 0000000..2a89a7c
--- /dev/null
+++ b/view/manager/backup/index.html
@@ -0,0 +1,41 @@
+{layout name="manager/layout" /}
+
diff --git a/view/manager/category/add.html b/view/manager/category/add.html
new file mode 100644
index 0000000..2298708
--- /dev/null
+++ b/view/manager/category/add.html
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/manager/category/edit.html b/view/manager/category/edit.html
new file mode 100644
index 0000000..0233fab
--- /dev/null
+++ b/view/manager/category/edit.html
@@ -0,0 +1,164 @@
+{php}
+$imgSize = '';
+if(!empty($item['width']) && is_int($item['width']) && $item['width'] > 0 && !empty($item['height']) && is_int($item['height']) && $item['height'] > 0){
+ $imgSize = $item['width'] . '像素 X ' . $item['height'] . '像素';
+}
+{/php}
+
+
+ {empty name='item'}
+
无效请求,该信息不存在
+ {else /}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/category/index.html b/view/manager/category/index.html
new file mode 100644
index 0000000..1e527b9
--- /dev/null
+++ b/view/manager/category/index.html
@@ -0,0 +1,202 @@
+{php}
+$sortUrl = url('manager.category/sort');
+$delUrl = url('manager.category/del');
+{/php}
+{layout name="manager/layout" /}
+
+
+ {if $power_add}
+
+ {/if}
+
+ {empty name="items"}
+ 无记录
+ {else /}
+
+ {/empty}
+
+
+
\ No newline at end of file
diff --git a/view/manager/content/article.html b/view/manager/content/article.html
new file mode 100644
index 0000000..db6bc40
--- /dev/null
+++ b/view/manager/content/article.html
@@ -0,0 +1,120 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/content/history.html b/view/manager/content/history.html
new file mode 100644
index 0000000..5ebeac4
--- /dev/null
+++ b/view/manager/content/history.html
@@ -0,0 +1,59 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+ {empty name="items"}
+ 无记录
+ {else /}
+
+
+
+
+
+
+
+
+ 历程标题
+ 状态
+ 操作
+
+
+ {foreach $items as $item}
+
+
+ {$item.title}
+ {:$item.visible == 0 ? '隐藏 ' : '正常 '}
+
+
+ 历程事例
+
+
+ 向上
+
+
+ 向下
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+ {/foreach}
+
+
+
{$items->render()|raw}
+ {/empty}
+
+
diff --git a/view/manager/content/page.html b/view/manager/content/page.html
new file mode 100644
index 0000000..7fa4a58
--- /dev/null
+++ b/view/manager/content/page.html
@@ -0,0 +1,142 @@
+{php}
+use app\model\Block;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+ {notempty name="blocks"}
+
+
+ {if $groupId == 1}
+
+ {/if}
+
+
+
+
+
+ {if $groupId == 1}
+ 键值
+ {/if}
+ 标题
+ 内容
+ 操作
+
+ {foreach $blocks as $block}
+
+ {if $groupId == 1}
+ {$block.keyword}
+ {/if}
+ {$block.title}
+
+ {php} if($block['type'] == Block::BLOCK):{/php}
+ {$block.value}
+ {php}
+ elseif($block['type'] == Block::IMG):
+ if($block['width'] && $block['height']){
+ $imgSize = $block['width'].'像素 X '.$block['height'].'像素';
+ }else{
+ $imgSize = '';
+ }
+ {/php}
+ {:widget('manager.upload/image',['src' => $block.value??'', 'imgSize' => $imgSize, 'append' => "_img_" . $block['id']])}
+ {php}elseif($block['type'] == Block::TEXT):{/php}
+
+
+
+ {php}
+ elseif($block['type'] == Block::GROUP):
+ if($block['width'] && $block['height']){
+ $imgSize = $block['width'].'像素 X '.$block['height'].'像素';
+ }else{
+ $imgSize = '';
+ }
+ {/php}
+
+ {:widget('manager.upload/multi',['imgs' => $block.value??'', 'num' => $block.num??10, 'imgSize' => $imgSize, 'append' => "_group_" . $block['id']])}
+ {php}
+ elseif($block['type'] == Block::VIDEO):
+ if($block['width'] && $block['height']){
+ $imgSize = $block['width'].'像素 X '.$block['height'].'像素';
+ }else{
+ $imgSize = '';
+ }
+ {/php}
+ {:widget('manager.upload/image',['src' => $block.img??'', 'imgSize' => $imgSize, 'append' => "_video_" . $block['id']])}
+ {:widget('manager.upload/video',['src' => $block.value??'', 'append' => "_video_" . $block['id']])}
+ {php}
+ elseif($block['type'] == Block::CODE):
+ {/php}
+
+ {$block.value??''}
+
+ {php}endif;{/php}
+
+
+
+
+
+ {/foreach}
+
+ {/notempty}
+
+ {if $groupId == 1}
+
+ {/if}
+ {if !empty($blocks)}
+
保存
+ {/if}
+
+
+
+
+
diff --git a/view/manager/desk/add.html b/view/manager/desk/add.html
new file mode 100644
index 0000000..30e9d69
--- /dev/null
+++ b/view/manager/desk/add.html
@@ -0,0 +1,46 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/desk/edit.html b/view/manager/desk/edit.html
new file mode 100644
index 0000000..475cd98
--- /dev/null
+++ b/view/manager/desk/edit.html
@@ -0,0 +1,48 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/desk/index.html b/view/manager/desk/index.html
new file mode 100644
index 0000000..eaa937b
--- /dev/null
+++ b/view/manager/desk/index.html
@@ -0,0 +1,72 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+ 当前大厅:---- {$hall['title']} ----
+
添加内容
+
+
+
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/error/jump.html b/view/manager/error/jump.html
new file mode 100644
index 0000000..33e16f5
--- /dev/null
+++ b/view/manager/error/jump.html
@@ -0,0 +1,53 @@
+{__NOLAYOUT__}
+
+
+
+
+ 跳转提示
+
+
+
+
+
+
+
+
+
:)
+
+
+
+
+
+
+
+
+
+ 页面自动 跳转 等待时间:
+
+
立即返回
+
+
+
+
diff --git a/view/manager/file/index.html b/view/manager/file/index.html
new file mode 100644
index 0000000..46affd8
--- /dev/null
+++ b/view/manager/file/index.html
@@ -0,0 +1,41 @@
+{php}
+use app\service\Image as FImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
{$items->render()|raw}
+
+
\ No newline at end of file
diff --git a/view/manager/file/unuse.html b/view/manager/file/unuse.html
new file mode 100644
index 0000000..4be13d5
--- /dev/null
+++ b/view/manager/file/unuse.html
@@ -0,0 +1,96 @@
+{layout name="manager/layout" /}
+
+
+
+ 磁盘上未使用的文件
+ 数据库中未使用的文件
+
+
+
diff --git a/view/manager/gift/add.html b/view/manager/gift/add.html
new file mode 100644
index 0000000..3dcb902
--- /dev/null
+++ b/view/manager/gift/add.html
@@ -0,0 +1,57 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/gift/edit.html b/view/manager/gift/edit.html
new file mode 100644
index 0000000..c091a8a
--- /dev/null
+++ b/view/manager/gift/edit.html
@@ -0,0 +1,55 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/gift/index.html b/view/manager/gift/index.html
new file mode 100644
index 0000000..7ab562e
--- /dev/null
+++ b/view/manager/gift/index.html
@@ -0,0 +1,89 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/group/add.html b/view/manager/group/add.html
new file mode 100644
index 0000000..8763ee3
--- /dev/null
+++ b/view/manager/group/add.html
@@ -0,0 +1,25 @@
+
diff --git a/view/manager/group/edit.html b/view/manager/group/edit.html
new file mode 100644
index 0000000..a0ee291
--- /dev/null
+++ b/view/manager/group/edit.html
@@ -0,0 +1,30 @@
+
+
+ {empty name='item'}
+
无效请求,该信息不存在
+ {else /}
+
+
+
+
+
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/group/index.html b/view/manager/group/index.html
new file mode 100644
index 0000000..fbb46d6
--- /dev/null
+++ b/view/manager/group/index.html
@@ -0,0 +1,41 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+
+
+
+ 名称
+ 状态
+ 操作
+
+ {foreach name="list" item="item"}
+
+ {$item.title}
+ {$item.status?'正常':'禁用'}
+
+
+
+
+ {/foreach}
+
+
+
\ No newline at end of file
diff --git a/view/manager/group/rule.html b/view/manager/group/rule.html
new file mode 100644
index 0000000..737a02e
--- /dev/null
+++ b/view/manager/group/rule.html
@@ -0,0 +1,50 @@
+
+
+
+
+ {foreach name="rules" item="item"}
+
+ {/foreach}
+
+
+
+
+ 保存
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/hall/add.html b/view/manager/hall/add.html
new file mode 100644
index 0000000..f65160c
--- /dev/null
+++ b/view/manager/hall/add.html
@@ -0,0 +1,61 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/hall/edit.html b/view/manager/hall/edit.html
new file mode 100644
index 0000000..f6e14f7
--- /dev/null
+++ b/view/manager/hall/edit.html
@@ -0,0 +1,59 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/hall/index.html b/view/manager/hall/index.html
new file mode 100644
index 0000000..7691ff5
--- /dev/null
+++ b/view/manager/hall/index.html
@@ -0,0 +1,98 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/hall_layout/add.html b/view/manager/hall_layout/add.html
new file mode 100644
index 0000000..3052bdb
--- /dev/null
+++ b/view/manager/hall_layout/add.html
@@ -0,0 +1,34 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/hall_layout/edit.html b/view/manager/hall_layout/edit.html
new file mode 100644
index 0000000..b562506
--- /dev/null
+++ b/view/manager/hall_layout/edit.html
@@ -0,0 +1,31 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/hall_layout/index.html b/view/manager/hall_layout/index.html
new file mode 100644
index 0000000..8ca9b67
--- /dev/null
+++ b/view/manager/hall_layout/index.html
@@ -0,0 +1,82 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/history/add.html b/view/manager/history/add.html
new file mode 100644
index 0000000..8aba755
--- /dev/null
+++ b/view/manager/history/add.html
@@ -0,0 +1,32 @@
+
+
+
+
+ 注意*号为必填选项!
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/history/add_info.html b/view/manager/history/add_info.html
new file mode 100644
index 0000000..3c08472
--- /dev/null
+++ b/view/manager/history/add_info.html
@@ -0,0 +1,38 @@
+
+
+
+
+ 注意*号为必填选项!
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/history/edit.html b/view/manager/history/edit.html
new file mode 100644
index 0000000..f789cae
--- /dev/null
+++ b/view/manager/history/edit.html
@@ -0,0 +1,32 @@
+
+
+
+
+ 注意*号为必填选项!
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/history/edit_info.html b/view/manager/history/edit_info.html
new file mode 100644
index 0000000..6e4cebf
--- /dev/null
+++ b/view/manager/history/edit_info.html
@@ -0,0 +1,38 @@
+
+
+
+
+ 注意*号为必填选项!
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/history/info.html b/view/manager/history/info.html
new file mode 100644
index 0000000..cc463d7
--- /dev/null
+++ b/view/manager/history/info.html
@@ -0,0 +1,78 @@
+{php}
+use app\service\Image as CImage;
+{/php}
+{layout name="manager/layout" /}
+
+{if isset($history) && count($history) > 0}
+
+
+
+
+
+
历程: {$history.title}
+
+ {empty name="items"}
+
该历程暂无相关事例信息!
+ {else /}
+
+ {/empty}
+
+
+{else /}
+
+{/if}
diff --git a/view/manager/index/index.html b/view/manager/index/index.html
new file mode 100644
index 0000000..77b6198
--- /dev/null
+++ b/view/manager/index/index.html
@@ -0,0 +1,137 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 服务器
+ {:php_uname('s')}{:php_uname('r')}
+
+
+ 服务器解译引擎
+
+
+
+ PHP
+ ({:php_sapi_name()})
+
+
+ MySQL
+ {$mysqlVersion}
+
+
+ ThinkPHP
+ {$thinkphpVersion}
+
+
+ JQuery
+ 3.4.1
+
+
+ layui
+ 2.5.6
+
+
+ wangEditor
+ 3
+
+
+ 官方网址
+ www.scdxtc.com
+
+
+ 联系人
+ 郑老板
+
+
+ 售后咨询
+ 18081208996
+
+
+ BUG反馈
+ 联系我们
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/layout.html b/view/manager/layout.html
new file mode 100644
index 0000000..7abf621
--- /dev/null
+++ b/view/manager/layout.html
@@ -0,0 +1,63 @@
+{php}
+$jsVersion = '0.0.1';
+$cssVersion = '0.0.1';
+{/php}
+
+
+
+
+
+ 后台管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/link/add.html b/view/manager/link/add.html
new file mode 100644
index 0000000..e78d4e7
--- /dev/null
+++ b/view/manager/link/add.html
@@ -0,0 +1,30 @@
+
\ No newline at end of file
diff --git a/view/manager/link/edit.html b/view/manager/link/edit.html
new file mode 100644
index 0000000..b3ef236
--- /dev/null
+++ b/view/manager/link/edit.html
@@ -0,0 +1,31 @@
+
diff --git a/view/manager/link/index.html b/view/manager/link/index.html
new file mode 100644
index 0000000..8a421ac
--- /dev/null
+++ b/view/manager/link/index.html
@@ -0,0 +1,58 @@
+{layout name="manager/layout" /}
+
\ No newline at end of file
diff --git a/view/manager/login/index.html b/view/manager/login/index.html
new file mode 100644
index 0000000..11e7533
--- /dev/null
+++ b/view/manager/login/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+ 管理后台登录页面
+
+
+
+
+
+
+
+
+
+
+
管理登录
+
账号
+
+
密码
+
+
+
忘记密码,请联系管理员!
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/member/add.html b/view/manager/member/add.html
new file mode 100644
index 0000000..25d1cc9
--- /dev/null
+++ b/view/manager/member/add.html
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/view/manager/member/edit.html b/view/manager/member/edit.html
new file mode 100644
index 0000000..4adeb45
--- /dev/null
+++ b/view/manager/member/edit.html
@@ -0,0 +1,35 @@
+
diff --git a/view/manager/member/index.html b/view/manager/member/index.html
new file mode 100644
index 0000000..c7d21dc
--- /dev/null
+++ b/view/manager/member/index.html
@@ -0,0 +1,45 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+
+
+
+
+ 用户名
+ 分组
+ 最近登录时日期
+ 操作
+
+ {foreach name="items" item="item"}
+
+ {$item.username}
+ {$item.title}
+
+ {:$item.login_time > 0 ? date('Y-m-d H:i:s', $item.login_time) : ''}
+
+
+
+ 栏目分配
+
+
+ 编辑
+
+
+ 删除
+
+
+
+ {/foreach}
+
+
{$items->render()|raw}
+
+
\ No newline at end of file
diff --git a/view/manager/member/menu_alloter.html b/view/manager/member/menu_alloter.html
new file mode 100644
index 0000000..57f5ee8
--- /dev/null
+++ b/view/manager/member/menu_alloter.html
@@ -0,0 +1,146 @@
+
+
+
+
+ {foreach name="cates" item="item"}
+
+ {/foreach}
+
+
+
+
+ 保存
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/model/add.html b/view/manager/model/add.html
new file mode 100644
index 0000000..5ab97e0
--- /dev/null
+++ b/view/manager/model/add.html
@@ -0,0 +1,30 @@
+
diff --git a/view/manager/model/edit.html b/view/manager/model/edit.html
new file mode 100644
index 0000000..b149d52
--- /dev/null
+++ b/view/manager/model/edit.html
@@ -0,0 +1,35 @@
+
+
+ {empty name='item'}
+
无效请求,该信息不存在
+ {else /}
+
+
+
+
+
+
+
+ {/empty}
+
+
diff --git a/view/manager/model/index.html b/view/manager/model/index.html
new file mode 100644
index 0000000..4655951
--- /dev/null
+++ b/view/manager/model/index.html
@@ -0,0 +1,48 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+
+
+
+
+
+ 名称
+ 后台管理
+ 前端模版
+ 操作
+
+ {foreach name="items" item="item"}
+
+ {$item.name}
+ {$item.manager}
+ {$item.template}
+
+
+
+
+ {/foreach}
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/block.html b/view/manager/page/block.html
new file mode 100644
index 0000000..2f75e25
--- /dev/null
+++ b/view/manager/page/block.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/code.html b/view/manager/page/code.html
new file mode 100644
index 0000000..bf4fad7
--- /dev/null
+++ b/view/manager/page/code.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/group.html b/view/manager/page/group.html
new file mode 100644
index 0000000..04cc089
--- /dev/null
+++ b/view/manager/page/group.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+ {if $groupId == 1}
+
+ {/if}
+
+ {if $groupId == 1}
+
+ {/if}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/img.html b/view/manager/page/img.html
new file mode 100644
index 0000000..ada8b7c
--- /dev/null
+++ b/view/manager/page/img.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+
+
+
+ {if $groupId == 1}
+
+ {/if}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/text.html b/view/manager/page/text.html
new file mode 100644
index 0000000..b4a3212
--- /dev/null
+++ b/view/manager/page/text.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/page/video.html b/view/manager/page/video.html
new file mode 100644
index 0000000..47255fd
--- /dev/null
+++ b/view/manager/page/video.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ {if $groupId == 1}
+
+
+
+ {else /}
+
+
+
+ {/if}
+
+
+
+ {if $groupId == 1}
+
+ {/if}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/rule/add.html b/view/manager/rule/add.html
new file mode 100644
index 0000000..fc12f5f
--- /dev/null
+++ b/view/manager/rule/add.html
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/view/manager/rule/edit.html b/view/manager/rule/edit.html
new file mode 100644
index 0000000..7da5d7f
--- /dev/null
+++ b/view/manager/rule/edit.html
@@ -0,0 +1,53 @@
+
+
+ {empty name='item'}
+
无效请求,该信息不存在
+ {else /}
+
+
+
+
+
+
+
+
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/rule/index.html b/view/manager/rule/index.html
new file mode 100644
index 0000000..566f079
--- /dev/null
+++ b/view/manager/rule/index.html
@@ -0,0 +1,90 @@
+{layout name="manager/layout" /}
+
+
+
+ {empty name="items"}
+ 无记录
+ {else /}
+
+
+
+
+
+
+
+
+ 栏目
+ 路径
+ 操作
+
+
+ {foreach $items as $item}
+
+
+ {$item.title}
+ {$item.name}
+
+
+ 添加下级权限
+
+
+ 向上
+
+
+ 向下
+
+
+ 编辑
+
+
+ 删除
+
+
+
+ {if isset($item['children']) && !empty($item['children'])}
+
+
+
+
+
+ {/if}
+
+
+ {/foreach}
+
+ {/empty}
+
+
\ No newline at end of file
diff --git a/view/manager/safe/index.html b/view/manager/safe/index.html
new file mode 100644
index 0000000..010d02e
--- /dev/null
+++ b/view/manager/safe/index.html
@@ -0,0 +1,51 @@
+{layout name="manager/layout" /}
+
\ No newline at end of file
diff --git a/view/manager/slide/add.html b/view/manager/slide/add.html
new file mode 100644
index 0000000..4a94d04
--- /dev/null
+++ b/view/manager/slide/add.html
@@ -0,0 +1,44 @@
+
\ No newline at end of file
diff --git a/view/manager/slide/edit.html b/view/manager/slide/edit.html
new file mode 100644
index 0000000..257acfc
--- /dev/null
+++ b/view/manager/slide/edit.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/slide/index.html b/view/manager/slide/index.html
new file mode 100644
index 0000000..a011d11
--- /dev/null
+++ b/view/manager/slide/index.html
@@ -0,0 +1,61 @@
+{layout name="manager/layout" /}
+
\ No newline at end of file
diff --git a/view/manager/store/add.html b/view/manager/store/add.html
new file mode 100644
index 0000000..4876c80
--- /dev/null
+++ b/view/manager/store/add.html
@@ -0,0 +1,54 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
\ No newline at end of file
diff --git a/view/manager/store/edit.html b/view/manager/store/edit.html
new file mode 100644
index 0000000..0e884e3
--- /dev/null
+++ b/view/manager/store/edit.html
@@ -0,0 +1,52 @@
+
+
+
+
注意*号为必填选项!
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/store/index.html b/view/manager/store/index.html
new file mode 100644
index 0000000..2fe8515
--- /dev/null
+++ b/view/manager/store/index.html
@@ -0,0 +1,79 @@
+
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
{$list->render()|raw}
+
+
diff --git a/view/manager/system/index.html b/view/manager/system/index.html
new file mode 100644
index 0000000..75fa9ba
--- /dev/null
+++ b/view/manager/system/index.html
@@ -0,0 +1,396 @@
+{layout name="manager/layout" /}
+
+
+
+
+
+
+
+ 公司信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 其他设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 页面相关
+
+
+
+
+
+
+
+
+
+ SEO信息
+
+
+
+
+
+
+
+
+
+
+ 图片上传
+
+
+
+
+ 视频上传
+
+
+ 文件上传
+
+
+ 图片最佳尺寸
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/manager/system/other.html b/view/manager/system/other.html
new file mode 100644
index 0000000..939329b
--- /dev/null
+++ b/view/manager/system/other.html
@@ -0,0 +1,12 @@
+{layout name="manager/layout" /}
+
diff --git a/view/manager/widget/crumbs.html b/view/manager/widget/crumbs.html
new file mode 100644
index 0000000..a3d6d0b
--- /dev/null
+++ b/view/manager/widget/crumbs.html
@@ -0,0 +1,12 @@
+{if isset($parent) && !empty($parent)}
+{$parent.title} -
+{/if}
+
+{if $isContent}
+ {foreach $cateCrumbs as $k => $cate}
+ {if $k != 0}-{/if}
+ {$cate}
+ {/foreach}
+{elseif !empty($rule) /}
+ {$rule.title}
+{/if}
\ No newline at end of file
diff --git a/view/manager/widget/files.html b/view/manager/widget/files.html
new file mode 100644
index 0000000..0cc1b95
--- /dev/null
+++ b/view/manager/widget/files.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/view/manager/widget/image.html b/view/manager/widget/image.html
new file mode 100644
index 0000000..69acd49
--- /dev/null
+++ b/view/manager/widget/image.html
@@ -0,0 +1,186 @@
+
+
+ {if file_exists('.'.$src)&&!empty($src)}
+
+ {/if}
+
+
+
+
+
+
+
+ {if !empty($imgSize)}
+
注:图片最佳尺寸为: {$imgSize} 数字0表示不限制
+ {else /}
+
请上传大小合适的图片
+ {/if}
+
+
+
+
+
\ No newline at end of file
diff --git a/view/manager/widget/left.html b/view/manager/widget/left.html
new file mode 100644
index 0000000..05262e7
--- /dev/null
+++ b/view/manager/widget/left.html
@@ -0,0 +1,99 @@
+
+
+ {foreach $rules as $rule}
+ {php}
+ if(!strpos($rule['name'],'/')){
+ $ruleName = $rule['name'].'/index';
+ $controller = strtolower($rule['name']);
+ }else{
+ $ruleName = $rule['name'];
+ $controller = substr($rule['name'],0,strpos($rule['name'],'/'));
+ }
+ $controller = str_replace('manager.', '', $controller);
+ {/php}
+ {if $controller == 'content'}
+ {if !empty($menus)}
+
+
+ {$rule['title']}
+
+
+
+ {foreach $menus as $menu}
+ {if isset($menu['children']) && count($menu['children'])}
+
+ {else /}
+
+ {/if}
+ {/foreach}
+
+
+ {/if}
+ {elseif $rule['parent_id'] == 0 /}
+
+ {if isset($rule['children']) && count($rule['children'])}
+
+ {$rule['title']}
+
+
+
+ {foreach $rule['children'] as $child}
+ {php}
+ if(!strpos($child['name'],'/')){
+ $childRuleName = $child['name'].'/index';
+ }else{
+ $childRuleName = $child['name'];
+ }
+ {/php}
+
+
+ {$child.title}
+
+
+ {/foreach}
+
+ {else /}
+
+ {$rule.title}
+
+ {/if}
+
+ {/if}
+ {/foreach}
+
\ No newline at end of file
diff --git a/view/manager/widget/mark.html b/view/manager/widget/mark.html
new file mode 100644
index 0000000..0f00d68
--- /dev/null
+++ b/view/manager/widget/mark.html
@@ -0,0 +1,28 @@
+
+
+ {if file_exists('.'.$src)&&!empty($src)}
+
+
+
+ {/if}
+
+
+ 上传图片
+
+
+
\ No newline at end of file
diff --git a/view/manager/widget/multi.html b/view/manager/widget/multi.html
new file mode 100644
index 0000000..51047aa
--- /dev/null
+++ b/view/manager/widget/multi.html
@@ -0,0 +1,230 @@
+{php}
+use app\service\Image as WImage;
+$numid=0;
+{/php}
+
+
+
+
+
+
+ {if !empty($imgSize)}
+
注:图片最佳尺寸为: {$imgSize} 数字0表示不限制
+ {else /}
+
请上传大小合适的图片
+ {/if}
+
+
+
+
diff --git a/view/manager/widget/video.html b/view/manager/widget/video.html
new file mode 100644
index 0000000..05715d5
--- /dev/null
+++ b/view/manager/widget/video.html
@@ -0,0 +1,186 @@
+
+
+ {if file_exists('.'.$src)&&!empty($src)}
+
+ 您的浏览器不支持 video 标签。
+
+
+ {/if}
+
+
+
+
+
+
+
+ {if !empty($videoSize)}
+
视频最佳尺寸为: {$videoSize} 数字0表示不限制
+ {else /}
+
请上传大小合适的视频
+ {/if}
+
+
+
+
+
+
diff --git a/view/page/index.html b/view/page/index.html
new file mode 100644
index 0000000..2e33190
--- /dev/null
+++ b/view/page/index.html
@@ -0,0 +1,3 @@
+{layout name="layout" /}
+
+单页
diff --git a/view/public/banner.html b/view/public/banner.html
new file mode 100644
index 0000000..d824842
--- /dev/null
+++ b/view/public/banner.html
@@ -0,0 +1,28 @@
+
+{php}
+$banner = \app\model\Slide::getList();
+{/php}
+
\ No newline at end of file
diff --git a/view/public/footer.html b/view/public/footer.html
new file mode 100644
index 0000000..12cd18a
--- /dev/null
+++ b/view/public/footer.html
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/view/public/menu.html b/view/public/menu.html
new file mode 100644
index 0000000..fbdcfc1
--- /dev/null
+++ b/view/public/menu.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+ {foreach $menus as $menu }
+
+ {/foreach}
+
+
+
统一服务热线
+
{$system['service_hotline']}
+
+
+
+
\ No newline at end of file
diff --git a/view/widget/common/service_flow.html b/view/widget/common/service_flow.html
new file mode 100644
index 0000000..12ac78f
--- /dev/null
+++ b/view/widget/common/service_flow.html
@@ -0,0 +1,57 @@
+{notempty name="items"}
+
+
+
+
+
+
{$category.description ?? ''}
+
+
+
+ {foreach $items as $ki => $item}
+ {php}
+ $indexOf = $ki+1;
+ $indexOf = $indexOf < 10 ? str_pad($indexOf, 2, '0', STR_PAD_LEFT) : $indexOf;
+ $descList = getTextNlToList($item['summary'] ?? '');
+ {/php}
+
+
{$indexOf}
{if $ki < (count($items) -1)}
{/if}
+
{$item.title}
+
+ {foreach $descList as $descText}
+
{$descText|raw}
+ {/foreach}
+
+
+ {/foreach}
+
+
+
+
+
+
+ {foreach $items as $ki => $item}
+ {php}
+ $indexOf = $ki+1;
+ $indexOf = $indexOf < 10 ? str_pad($indexOf, 2, '0', STR_PAD_LEFT) : $indexOf;
+ $descList = getTextNlToList($item['summary'] ?? '');
+ {/php}
+
+
+
+
{$item.title}
+
+ {foreach $descList as $descText}
+
{$descText|raw}
+ {/foreach}
+
+
+
+ {/foreach}
+
+
+
+
+
+
+{/notempty}
\ No newline at end of file