diff --git a/.gitignore b/.gitignore
index 22a39e8..b62ed99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,7 @@
/.idea
/.vscode
-/public/uploads
*.log
*/.DS_Store
.env
/runtime
/composer.lock
-/vendor/.gitignore
-/vendor
diff --git a/app/admin/view/login/login.html b/app/admin/view/login/login.html
index b344ae6..644de84 100644
--- a/app/admin/view/login/login.html
+++ b/app/admin/view/login/login.html
@@ -59,9 +59,9 @@
+ *
+ * @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('Start 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);
+ });
+
+ return 0;
+ }
+}
diff --git a/vendor/symfony/var-dumper/Dumper/AbstractDumper.php b/vendor/symfony/var-dumper/Dumper/AbstractDumper.php
new file mode 100644
index 0000000..4ddaf5e
--- /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
+{
+ public const DUMP_LIGHT_ARRAY = 1;
+ public const DUMP_STRING_LENGTH = 2;
+ public const DUMP_COMMA_SEPARATOR = 4;
+ public 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 = $this->outputStream ?? $this->lineDumper;
+
+ if (\is_callable($output)) {
+ $this->outputStream = null;
+ $this->lineDumper = $output;
+ } else {
+ if (\is_string($output)) {
+ $output = fopen($output, 'w');
+ }
+ $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+');
+ }
+ 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..b3d9e25
--- /dev/null
+++ b/vendor/symfony/var-dumper/Dumper/CliDumper.php
@@ -0,0 +1,655 @@
+
+ *
+ * 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 (!str_contains($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)
+ {
+ if (null === $this->colors) {
+ $this->colors = $this->supportsColors();
+ }
+
+ $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 {
+ $prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '[';
+ }
+
+ 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).($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') || (int) getenv('KONSOLE_VERSION') > 201100);
+ }
+
+ 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']) && str_starts_with($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 .= $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 && str_ends_with($value, $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'], $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', 'w') : $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..38f8789
--- /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..38ef3b0
--- /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..3684a47
--- /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..2e2c818
--- /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']) && !str_starts_with($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 (str_starts_with($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..7638417
--- /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..b173bcc
--- /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..8409a0c
--- /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 = $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'], $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 !== ($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(string $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..94795bf
--- /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..122f0d3
--- /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..c1f0aac
--- /dev/null
+++ b/vendor/symfony/var-dumper/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014-2021 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..a0da8c9
--- /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..98c813a
--- /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..8f706d6
--- /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..a485d57
--- /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..63101e5
--- /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..55d9214
--- /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 (!str_contains($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(int $t, string $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..1c2c348
--- /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 (!str_contains($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;
+ }
+
+ [$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..3d3d18e
--- /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..febc1e0
--- /dev/null
+++ b/vendor/symfony/var-dumper/VarDumper.php
@@ -0,0 +1,66 @@
+
+ *
+ * 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;
+
+ // Prevent replacing the handler with expected format as soon as the env var was set:
+ if (isset($_SERVER['VAR_DUMPER_FORMAT'])) {
+ return $prevHandler;
+ }
+
+ 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..d4e64cc
--- /dev/null
+++ b/vendor/symfony/var-dumper/composer.json
@@ -0,0 +1,50 @@
+{
+ "name": "symfony/var-dumper",
+ "type": "library",
+ "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+ "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.16"
+ },
+ "require-dev": {
+ "ext-iconv": "*",
+ "symfony/console": "^3.4|^4.0|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "twig/twig": "^1.43|^2.13|^3.0.4"
+ },
+ "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"
+}
diff --git a/vendor/topthink/framework/.gitignore b/vendor/topthink/framework/.gitignore
new file mode 100644
index 0000000..b267fba
--- /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..abaa271
--- /dev/null
+++ b/vendor/topthink/framework/.travis.yml
@@ -0,0 +1,35 @@
+dist: xenial
+language: php
+
+matrix:
+ fast_finish: true
+ include:
+ - php: 7.2
+ - php: 7.3
+ - php: 8.0
+
+cache:
+ directories:
+ - $HOME/.composer/cache
+
+services:
+ - memcached
+ - redis-server
+ - mysql
+
+before_install:
+ - echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+ - echo 'xdebug.mode = coverage' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.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
diff --git a/vendor/topthink/framework/CONTRIBUTING.md b/vendor/topthink/framework/CONTRIBUTING.md
new file mode 100644
index 0000000..efa3ad9
--- /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..4e910bb
--- /dev/null
+++ b/vendor/topthink/framework/LICENSE.txt
@@ -0,0 +1,32 @@
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+版权所有Copyright © 2006-2019 by ThinkPHP (http://thinkphp.cn)
+All rights reserved。
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+Apache Licence是著名的非盈利开源组织Apache采用的协议。
+该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
+允许代码修改,再作为开源或商业软件发布。需要满足
+的条件:
+1. 需要给代码的用户一份Apache Licence ;
+2. 如果你修改了代码,需要在被修改的文件中说明;
+3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
+带有原来代码中的协议,商标,专利声明和其他原来作者规
+定需要包含的说明;
+4. 如果再发布的产品中包含一个Notice文件,则在Notice文
+件中需要带有本协议内容。你可以在Notice中增加自己的
+许可,但不可以表现为对Apache Licence构成更改。
+具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/topthink/framework/README.md b/vendor/topthink/framework/README.md
new file mode 100644
index 0000000..0ea03c9
--- /dev/null
+++ b/vendor/topthink/framework/README.md
@@ -0,0 +1,86 @@
+
+
+ThinkPHP 6.0
+===============
+
+[](https://travis-ci.org/top-think/framework)
+[](https://scrutinizer-ci.com/g/top-think/framework/?branch=6.0)
+[](https://scrutinizer-ci.com/g/top-think/framework/?branch=6.0)
+[](https://packagist.org/packages/topthink/framework)
+[](https://packagist.org/packages/topthink/framework)
+[](http://www.php.net/)
+[](https://packagist.org/packages/topthink/framework)
+
+ThinkPHP6.0底层架构采用PHP7.1改写和进一步优化。
+
+[官方应用服务市场](https://market.topthink.com) | [`ThinkAPI`——官方统一API服务](https://docs.topthink.com/think-api/)
+
+## 主要新特性
+
+* 采用`PHP7`强类型(严格模式)
+* 支持更多的`PSR`规范
+* 原生多应用支持
+* 系统服务注入支持
+* ORM作为独立组件使用
+* 增加Filesystem
+* 全新的事件系统
+* 模板引擎分离出核心
+* 内部功能中间件化
+* SESSION机制改进
+* 日志多通道支持
+* 规范扩展接口
+* 更强大的控制台
+* 对Swoole以及协程支持改进
+* 对IDE更加友好
+* 统一和精简大量用法
+
+
+> ThinkPHP6.0的运行环境要求PHP7.1+,兼容PHP8.0。
+
+## 安装
+
+~~~
+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-2021 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..4be2ae0
--- /dev/null
+++ b/vendor/topthink/framework/composer.json
@@ -0,0 +1,54 @@
+{
+ "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.2.5",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "league/flysystem": "^1.1.4",
+ "league/flysystem-cached-adapter": "^1.0",
+ "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..e20a133
--- /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..650edcb
--- /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..a546330
--- /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..056a341
--- /dev/null
+++ b/vendor/topthink/framework/src/think/App.php
@@ -0,0 +1,639 @@
+
+// +----------------------------------------------------------------------
+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.9';
+
+ /**
+ * 应用调试模式
+ * @var bool
+ */
+ protected $appDebug = false;
+
+ /**
+ * 环境变量标识
+ * @var string
+ */
+ protected $envName = '';
+
+ /**
+ * 应用开始时间
+ * @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
+ * @param string $name 环境标识
+ * @return $this
+ */
+ public function setEnvName(string $name)
+ {
+ $this->envName = $name;
+ return $this;
+ }
+
+ /**
+ * 获取框架版本
+ * @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
+ * @param string $envName 环境标识
+ * @return void
+ */
+ public function loadEnv(string $envName = ''): void
+ {
+ // 加载环境变量
+ $envFile = $envName ? $this->rootPath . '.env.' . $envName : $this->rootPath . '.env';
+
+ if (is_file($envFile)) {
+ $this->env->load($envFile);
+ }
+ }
+
+ /**
+ * 初始化应用
+ * @access public
+ * @return $this
+ */
+ public function initialize()
+ {
+ $this->initialized = true;
+
+ $this->beginTime = microtime(true);
+ $this->beginMem = memory_get_usage();
+
+ $this->loadEnv($this->envName);
+
+ $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(): bool
+ {
+ 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..f802b55
--- /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..9162e82
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Config.php
@@ -0,0 +1,197 @@
+
+// +----------------------------------------------------------------------
+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
+ {
+ if (false === strpos($name, '.') && !isset($this->config[strtolower($name)])) {
+ return false;
+ }
+
+ 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..389d104
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Console.php
@@ -0,0 +1,787 @@
+
+// +----------------------------------------------------------------------
+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;
+
+ $this->initialize();
+
+ $this->definition = $this->getDefaultInputDefinition();
+
+ //加载指令
+ $this->loadCommands();
+
+ $this->start();
+ }
+
+ /**
+ * 初始化
+ */
+ protected function initialize()
+ {
+ if (!$this->app->initialized()) {
+ $this->app->initialize();
+ }
+ $this->makeRequest();
+ }
+
+ /**
+ * 构造request
+ */
+ protected function makeRequest()
+ {
+ $uri = $this->app->config->get('app.url', 'http://localhost');
+
+ $components = parse_url($uri);
+
+ $server = $_SERVER;
+
+ if (isset($components['path'])) {
+ $server = array_merge($server, [
+ 'SCRIPT_FILENAME' => $components['path'],
+ 'SCRIPT_NAME' => $components['path'],
+ ]);
+ }
+
+ if (isset($components['host'])) {
+ $server['SERVER_NAME'] = $components['host'];
+ $server['HTTP_HOST'] = $components['host'];
+ }
+
+ if (isset($components['scheme'])) {
+ if ('https' === $components['scheme']) {
+ $server['HTTPS'] = 'on';
+ $server['SERVER_PORT'] = 443;
+ } else {
+ unset($server['HTTPS']);
+ $server['SERVER_PORT'] = 80;
+ }
+ }
+
+ if (isset($components['port'])) {
+ $server['SERVER_PORT'] = $components['port'];
+ $server['HTTP_HOST'] .= ':' . $components['port'];
+ }
+
+ $server['REQUEST_URI'] = $uri;
+
+ /** @var Request $request */
+ $request = $this->app->make('request');
+
+ $request->withServer($server);
+ }
+
+ /**
+ * 添加初始化器
+ * @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..74026bb
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Container.php
@@ -0,0 +1,554 @@
+
+// +----------------------------------------------------------------------
+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);
+ if ($abstract != $concrete) {
+ $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);
+ $object = $method->invokeArgs(null, $args);
+ $this->invokeAfter($class, $object);
+ return $object;
+ }
+ }
+
+ $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);
+ $reflectionType = $param->getType();
+
+ if ($reflectionType && $reflectionType->isBuiltin() === false) {
+ $args[] = $this->getObjectParam($reflectionType->getName(), $vars);
+ } elseif (1 == $type && !empty($vars)) {
+ $args[] = array_shift($vars);
+ } elseif (0 == $type && array_key_exists($name, $vars)) {
+ $args[] = $vars[$name];
+ } elseif (0 == $type && array_key_exists($lowerName, $vars)) {
+ $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..ebbfd64
--- /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..0048874
--- /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..4c26b33
--- /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..3c70aad
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Event.php
@@ -0,0 +1,272 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ReflectionClass;
+use ReflectionMethod;
+
+/**
+ * 事件管理类
+ * @package think
+ */
+class Event
+{
+ /**
+ * 监听者
+ * @var array
+ */
+ protected $listener = [];
+
+ /**
+ * 事件别名
+ * @var array
+ */
+ protected $bind = [
+ 'AppInit' => event\AppInit::class,
+ 'HttpRun' => event\HttpRun::class,
+ 'HttpEnd' => event\HttpEnd::class,
+ 'RouteLoaded' => event\RouteLoaded::class,
+ 'LogWrite' => event\LogWrite::class,
+ 'LogRecord' => event\LogRecord::class,
+ ];
+
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 批量注册事件监听
+ * @access public
+ * @param array $events 事件定义
+ * @return $this
+ */
+ public function listenEvents(array $events)
+ {
+ foreach ($events as $event => $listeners) {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 注册事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @param mixed $listener 监听操作(或者类名)
+ * @param bool $first 是否优先执行
+ * @return $this
+ */
+ public function listen(string $event, $listener, bool $first = false)
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ if ($first && isset($this->listener[$event])) {
+ array_unshift($this->listener[$event], $listener);
+ } else {
+ $this->listener[$event][] = $listener;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 是否存在事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @return bool
+ */
+ public function hasListener(string $event): bool
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ return isset($this->listener[$event]);
+ }
+
+ /**
+ * 移除事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @return void
+ */
+ public function remove(string $event): void
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ unset($this->listener[$event]);
+ }
+
+ /**
+ * 指定事件别名标识 便于调用
+ * @access public
+ * @param array $events 事件别名
+ * @return $this
+ */
+ public function bind(array $events)
+ {
+ $this->bind = array_merge($this->bind, $events);
+
+ return $this;
+ }
+
+ /**
+ * 注册事件订阅者
+ * @access public
+ * @param mixed $subscriber 订阅者
+ * @return $this
+ */
+ public function subscribe($subscriber)
+ {
+ $subscribers = (array) $subscriber;
+
+ foreach ($subscribers as $subscriber) {
+ if (is_string($subscriber)) {
+ $subscriber = $this->app->make($subscriber);
+ }
+
+ if (method_exists($subscriber, 'subscribe')) {
+ // 手动订阅
+ $subscriber->subscribe($this);
+ } else {
+ // 智能订阅
+ $this->observe($subscriber);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 自动注册事件观察者
+ * @access public
+ * @param string|object $observer 观察者
+ * @param null|string $prefix 事件名前缀
+ * @return $this
+ */
+ public function observe($observer, string $prefix = '')
+ {
+ if (is_string($observer)) {
+ $observer = $this->app->make($observer);
+ }
+
+ $reflect = new ReflectionClass($observer);
+ $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC);
+
+ if (empty($prefix) && $reflect->hasProperty('eventPrefix')) {
+ $reflectProperty = $reflect->getProperty('eventPrefix');
+ $reflectProperty->setAccessible(true);
+ $prefix = $reflectProperty->getValue($observer);
+ }
+
+ foreach ($methods as $method) {
+ $name = $method->getName();
+ if (0 === strpos($name, 'on')) {
+ $this->listen($prefix . substr($name, 2), [$observer, $name]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string|object $event 事件名称
+ * @param mixed $params 传入参数
+ * @param bool $once 只获取一个有效返回值
+ * @return mixed
+ */
+ public function trigger($event, $params = null, bool $once = false)
+ {
+ if (is_object($event)) {
+ $params = $event;
+ $event = get_class($event);
+ }
+
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ $result = [];
+ $listeners = $this->listener[$event] ?? [];
+
+ if (strpos($event, '.')) {
+ [$prefix, $event] = explode('.', $event, 2);
+ if (isset($this->listener[$prefix . '.*'])) {
+ $listeners = array_merge($listeners, $this->listener[$prefix . '.*']);
+ }
+ }
+
+ $listeners = array_unique($listeners, SORT_REGULAR);
+
+ foreach ($listeners as $key => $listener) {
+ $result[$key] = $this->dispatch($listener, $params);
+
+ if (false === $result[$key] || (!is_null($result[$key]) && $once)) {
+ break;
+ }
+ }
+
+ return $once ? end($result) : $result;
+ }
+
+ /**
+ * 触发事件(只获取一个有效返回值)
+ * @param $event
+ * @param null $params
+ * @return mixed
+ */
+ public function until($event, $params = null)
+ {
+ return $this->trigger($event, $params, true);
+ }
+
+ /**
+ * 执行事件调度
+ * @access protected
+ * @param mixed $event 事件方法
+ * @param mixed $params 参数
+ * @return mixed
+ */
+ protected function dispatch($event, $params = null)
+ {
+ if (!is_string($event)) {
+ $call = $event;
+ } elseif (strpos($event, '::')) {
+ $call = $event;
+ } else {
+ $obj = $this->app->make($event);
+ $call = [$obj, 'handle'];
+ }
+
+ return $this->app->invoke($call, [$params]);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Exception.php b/vendor/topthink/framework/src/think/Exception.php
new file mode 100644
index 0000000..5cf7954
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Exception.php
@@ -0,0 +1,60 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 异常基础类
+ * @package think
+ */
+class Exception extends \Exception
+{
+ /**
+ * 保存异常页面显示的额外Debug数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 设置异常额外的Debug数据
+ * 数据将会显示为下面的格式
+ *
+ * Exception Data
+ * --------------------------------------------------
+ * Label 1
+ * key1 value1
+ * key2 value2
+ * Label 2
+ * key1 value1
+ * key2 value2
+ *
+ * @access protected
+ * @param string $label 数据分类,用于异常页面显示
+ * @param array $data 需要显示的数据,必须为关联数组
+ */
+ final protected function setData(string $label, array $data)
+ {
+ $this->data[$label] = $data;
+ }
+
+ /**
+ * 获取异常额外Debug数据
+ * 主要用于输出到异常页面便于调试
+ * @access public
+ * @return array 由setData设置的Debug数据
+ */
+ final public function getData()
+ {
+ return $this->data;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Facade.php b/vendor/topthink/framework/src/think/Facade.php
new file mode 100644
index 0000000..9a0e333
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Facade.php
@@ -0,0 +1,98 @@
+
+// +----------------------------------------------------------------------
+namespace think;
+
+/**
+ * Facade管理类
+ */
+class Facade
+{
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance;
+
+ /**
+ * 创建Facade实例
+ * @static
+ * @access protected
+ * @param string $class 类名或标识
+ * @param array $args 变量
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return object
+ */
+ protected static function createFacade(string $class = '', array $args = [], bool $newInstance = false)
+ {
+ $class = $class ?: static::class;
+
+ $facadeClass = static::getFacadeClass();
+
+ if ($facadeClass) {
+ $class = $facadeClass;
+ }
+
+ if (static::$alwaysNewInstance) {
+ $newInstance = true;
+ }
+
+ return Container::getInstance()->make($class, $args, $newInstance);
+ }
+
+ /**
+ * 获取当前Facade对应类名
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {}
+
+ /**
+ * 带参数实例化当前Facade类
+ * @access public
+ * @return object
+ */
+ public static function instance(...$args)
+ {
+ if (__CLASS__ != static::class) {
+ return self::createFacade('', $args);
+ }
+ }
+
+ /**
+ * 调用类的实例
+ * @access public
+ * @param string $class 类名或者标识
+ * @param array|true $args 变量
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return object
+ */
+ public static function make(string $class, $args = [], $newInstance = false)
+ {
+ if (__CLASS__ != static::class) {
+ return self::__callStatic('make', func_get_args());
+ }
+
+ if (true === $args) {
+ // 总是创建新的实例化对象
+ $newInstance = true;
+ $args = [];
+ }
+
+ return self::createFacade($class, $args, $newInstance);
+ }
+
+ // 调用实际类的方法
+ public static function __callStatic($method, $params)
+ {
+ return call_user_func_array([static::createFacade(), $method], $params);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/File.php b/vendor/topthink/framework/src/think/File.php
new file mode 100644
index 0000000..f7c37bd
--- /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..0aee929
--- /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..4e49c88
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Http.php
@@ -0,0 +1,288 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use think\event\HttpEnd;
+use think\event\HttpRun;
+use think\event\RouteLoaded;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * Web应用管理类
+ * @package think
+ */
+class Http
+{
+
+ /**
+ * @var App
+ */
+ protected $app;
+
+ /**
+ * 应用名称
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 应用路径
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * 路由路径
+ * @var string
+ */
+ protected $routePath;
+
+ /**
+ * 是否绑定应用
+ * @var bool
+ */
+ protected $isBind = false;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+
+ $this->routePath = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 设置应用名称
+ * @access public
+ * @param string $name 应用名称
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * 获取应用名称
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 设置应用目录
+ * @access public
+ * @param string $path 应用目录
+ * @return $this
+ */
+ public function path(string $path)
+ {
+ if (substr($path, -1) != DIRECTORY_SEPARATOR) {
+ $path .= DIRECTORY_SEPARATOR;
+ }
+
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * 获取应用路径
+ * @access public
+ * @return string
+ */
+ public function getPath(): string
+ {
+ return $this->path ?: '';
+ }
+
+ /**
+ * 获取路由目录
+ * @access public
+ * @return string
+ */
+ public function getRoutePath(): string
+ {
+ return $this->routePath;
+ }
+
+ /**
+ * 设置路由目录
+ * @access public
+ * @param string $path 路由定义目录
+ */
+ public function setRoutePath(string $path): void
+ {
+ $this->routePath = $path;
+ }
+
+ /**
+ * 设置应用绑定
+ * @access public
+ * @param bool $bind 是否绑定
+ * @return $this
+ */
+ public function setBind(bool $bind = true)
+ {
+ $this->isBind = $bind;
+ return $this;
+ }
+
+ /**
+ * 是否绑定应用
+ * @access public
+ * @return bool
+ */
+ public function isBind(): bool
+ {
+ return $this->isBind;
+ }
+
+ /**
+ * 执行应用程序
+ * @access public
+ * @param Request|null $request
+ * @return Response
+ */
+ public function run(Request $request = null): Response
+ {
+ //初始化
+ $this->initialize();
+
+ //自动创建request对象
+ $request = $request ?? $this->app->make('request', [], true);
+ $this->app->instance('request', $request);
+
+ try {
+ $response = $this->runWithRequest($request);
+ } catch (Throwable $e) {
+ $this->reportException($e);
+
+ $response = $this->renderException($request, $e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 初始化
+ */
+ protected function initialize()
+ {
+ if (!$this->app->initialized()) {
+ $this->app->initialize();
+ }
+ }
+
+ /**
+ * 执行应用程序
+ * @param Request $request
+ * @return mixed
+ */
+ protected function runWithRequest(Request $request)
+ {
+ // 加载全局中间件
+ $this->loadMiddleware();
+
+ // 监听HttpRun
+ $this->app->event->trigger(HttpRun::class);
+
+ return $this->app->middleware->pipeline()
+ ->send($request)
+ ->then(function ($request) {
+ return $this->dispatchToRoute($request);
+ });
+ }
+
+ protected function dispatchToRoute($request)
+ {
+ $withRoute = $this->app->config->get('app.with_route', true) ? function () {
+ $this->loadRoutes();
+ } : null;
+
+ return $this->app->route->dispatch($request, $withRoute);
+ }
+
+ /**
+ * 加载全局中间件
+ */
+ protected function loadMiddleware(): void
+ {
+ if (is_file($this->app->getBasePath() . 'middleware.php')) {
+ $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php');
+ }
+ }
+
+ /**
+ * 加载路由
+ * @access protected
+ * @return void
+ */
+ protected function loadRoutes(): void
+ {
+ // 加载路由定义
+ $routePath = $this->getRoutePath();
+
+ if (is_dir($routePath)) {
+ $files = glob($routePath . '*.php');
+ foreach ($files as $file) {
+ include $file;
+ }
+ }
+
+ $this->app->event->trigger(RouteLoaded::class);
+ }
+
+ /**
+ * Report the exception to the exception handler.
+ *
+ * @param Throwable $e
+ * @return void
+ */
+ protected function reportException(Throwable $e)
+ {
+ $this->app->make(Handle::class)->report($e);
+ }
+
+ /**
+ * Render the exception to a response.
+ *
+ * @param Request $request
+ * @param Throwable $e
+ * @return Response
+ */
+ protected function renderException($request, Throwable $e)
+ {
+ return $this->app->make(Handle::class)->render($request, $e);
+ }
+
+ /**
+ * HttpEnd
+ * @param Response $response
+ * @return void
+ */
+ public function end(Response $response): void
+ {
+ $this->app->event->trigger(HttpEnd::class, $response);
+
+ //执行中间件
+ $this->app->middleware->end($response);
+
+ // 写入日志
+ $this->app->log->save();
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Lang.php b/vendor/topthink/framework/src/think/Lang.php
new file mode 100644
index 0000000..0b79b76
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Lang.php
@@ -0,0 +1,294 @@
+
+// +----------------------------------------------------------------------
+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;
+ case 'json':
+ $data = file_get_contents($file);
+
+ if (false !== $data) {
+ $data = json_decode($data, true);
+
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $result = $data;
+ }
+ }
+
+ break;
+ }
+
+ return isset($result) && is_array($result) ? $result : [];
+ }
+
+ /**
+ * 判断是否存在语言定义(不区分大小写)
+ * @access public
+ * @param string|null $name 语言变量
+ * @param string $range 语言作用域
+ * @return bool
+ */
+ public function has(string $name, string $range = ''): bool
+ {
+ $range = $range ?: $this->range;
+
+ if ($this->config['allow_group'] && strpos($name, '.')) {
+ [$name1, $name2] = explode('.', $name, 2);
+ return isset($this->lang[$range][strtolower($name1)][$name2]);
+ }
+
+ return isset($this->lang[$range][strtolower($name)]);
+ }
+
+ /**
+ * 获取语言定义(不区分大小写)
+ * @access public
+ * @param string|null $name 语言变量
+ * @param array $vars 变量替换
+ * @param string $range 语言作用域
+ * @return mixed
+ */
+ public function get(string $name = null, array $vars = [], string $range = '')
+ {
+ $range = $range ?: $this->range;
+
+ // 空参数返回所有定义
+ if (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..c31210c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Log.php
@@ -0,0 +1,342 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use think\event\LogWrite;
+use think\helper\Arr;
+use think\log\Channel;
+use think\log\ChannelSet;
+
+/**
+ * 日志管理类
+ * @package think
+ * @mixin Channel
+ */
+class Log extends Manager implements LoggerInterface
+{
+ const EMERGENCY = 'emergency';
+ const ALERT = 'alert';
+ const CRITICAL = 'critical';
+ const ERROR = 'error';
+ const WARNING = 'warning';
+ const NOTICE = 'notice';
+ const INFO = 'info';
+ const DEBUG = 'debug';
+ const SQL = 'sql';
+
+ protected $namespace = '\\think\\log\\driver\\';
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver()
+ {
+ return $this->getConfig('default');
+ }
+
+ /**
+ * 获取日志配置
+ * @access public
+ * @param null|string $name 名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(string $name = null, $default = null)
+ {
+ if (!is_null($name)) {
+ return $this->app->config->get('log.' . $name, $default);
+ }
+
+ return $this->app->config->get('log');
+ }
+
+ /**
+ * 获取渠道配置
+ * @param string $channel
+ * @param null $name
+ * @param null $default
+ * @return array
+ */
+ public function getChannelConfig($channel, $name = null, $default = null)
+ {
+ if ($config = $this->getConfig("channels.{$channel}")) {
+ return Arr::get($config, $name, $default);
+ }
+
+ throw new InvalidArgumentException("Channel [$channel] not found.");
+ }
+
+ /**
+ * driver()的别名
+ * @param string|array $name 渠道名
+ * @return Channel|ChannelSet
+ */
+ public function channel($name = null)
+ {
+ if (is_array($name)) {
+ return new ChannelSet($this, $name);
+ }
+
+ return $this->driver($name);
+ }
+
+ protected function resolveType(string $name)
+ {
+ return $this->getChannelConfig($name, 'type', 'file');
+ }
+
+ public function createDriver(string $name)
+ {
+ $driver = parent::createDriver($name);
+
+ $lazy = !$this->getChannelConfig($name, "realtime_write", false) && !$this->app->runningInConsole();
+ $allow = array_merge($this->getConfig("level", []), $this->getChannelConfig($name, "level", []));
+
+ return new Channel($name, $driver, $allow, $lazy, $this->app->event);
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ return $this->getChannelConfig($name);
+ }
+
+ /**
+ * 清空日志信息
+ * @access public
+ * @param string|array $channel 日志通道名
+ * @return $this
+ */
+ public function clear($channel = '*')
+ {
+ if ('*' == $channel) {
+ $channel = array_keys($this->drivers);
+ }
+
+ $this->channel($channel)->clear();
+
+ return $this;
+ }
+
+ /**
+ * 关闭本次请求日志写入
+ * @access public
+ * @param string|array $channel 日志通道名
+ * @return $this
+ */
+ public function close($channel = '*')
+ {
+ if ('*' == $channel) {
+ $channel = array_keys($this->drivers);
+ }
+
+ $this->channel($channel)->close();
+
+ return $this;
+ }
+
+ /**
+ * 获取日志信息
+ * @access public
+ * @param string $channel 日志通道名
+ * @return array
+ */
+ public function getLog(string $channel = null): array
+ {
+ return $this->channel($channel)->getLog();
+ }
+
+ /**
+ * 保存日志信息
+ * @access public
+ * @return bool
+ */
+ public function save(): bool
+ {
+ /** @var Channel $channel */
+ foreach ($this->drivers as $channel) {
+ $channel->save();
+ }
+
+ return true;
+ }
+
+ /**
+ * 记录日志信息
+ * @access public
+ * @param mixed $msg 日志信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @param bool $lazy
+ * @return $this
+ */
+ public function record($msg, string $type = 'info', array $context = [], bool $lazy = true)
+ {
+ $channel = $this->getConfig('type_channel.' . $type);
+
+ $this->channel($channel)->record($msg, $type, $context, $lazy);
+
+ return $this;
+ }
+
+ /**
+ * 实时写入日志信息
+ * @access public
+ * @param mixed $msg 调试信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @return $this
+ */
+ public function write($msg, string $type = 'info', array $context = [])
+ {
+ return $this->record($msg, $type, $context, false);
+ }
+
+ /**
+ * 注册日志写入事件监听
+ * @param $listener
+ * @return Event
+ */
+ public function listen($listener)
+ {
+ return $this->app->event->listen(LogWrite::class, $listener);
+ }
+
+ /**
+ * 记录日志信息
+ * @access public
+ * @param string $level 日志级别
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ $this->record($message, $level, $context);
+ }
+
+ /**
+ * 记录emergency信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function emergency($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录警报信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function alert($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录紧急情况
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function critical($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录错误信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function error($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录warning信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function warning($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录notice信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function notice($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录一般信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function info($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录调试信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function debug($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * 记录sql信息
+ * @access public
+ * @param mixed $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function sql($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ public function __call($method, $parameters)
+ {
+ $this->log($method, ...$parameters);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Manager.php b/vendor/topthink/framework/src/think/Manager.php
new file mode 100644
index 0000000..ca3f6a5
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Manager.php
@@ -0,0 +1,177 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use InvalidArgumentException;
+use think\helper\Str;
+
+abstract class Manager
+{
+ /** @var App */
+ protected $app;
+
+ /**
+ * 驱动
+ * @var array
+ */
+ protected $drivers = [];
+
+ /**
+ * 驱动的命名空间
+ * @var string
+ */
+ protected $namespace = null;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 获取驱动实例
+ * @param null|string $name
+ * @return mixed
+ */
+ protected function driver(string $name = null)
+ {
+ $name = $name ?: $this->getDefaultDriver();
+
+ if (is_null($name)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Unable to resolve NULL driver for [%s].',
+ static::class
+ ));
+ }
+
+ return $this->drivers[$name] = $this->getDriver($name);
+ }
+
+ /**
+ * 获取驱动实例
+ * @param string $name
+ * @return mixed
+ */
+ protected function getDriver(string $name)
+ {
+ return $this->drivers[$name] ?? $this->createDriver($name);
+ }
+
+ /**
+ * 获取驱动类型
+ * @param string $name
+ * @return mixed
+ */
+ protected function resolveType(string $name)
+ {
+ return $name;
+ }
+
+ /**
+ * 获取驱动配置
+ * @param string $name
+ * @return mixed
+ */
+ protected function resolveConfig(string $name)
+ {
+ return $name;
+ }
+
+ /**
+ * 获取驱动类
+ * @param string $type
+ * @return string
+ */
+ protected function resolveClass(string $type): string
+ {
+ if ($this->namespace || false !== strpos($type, '\\')) {
+ $class = false !== strpos($type, '\\') ? $type : $this->namespace . Str::studly($type);
+
+ if (class_exists($class)) {
+ return $class;
+ }
+ }
+
+ throw new InvalidArgumentException("Driver [$type] not supported.");
+ }
+
+ /**
+ * 获取驱动参数
+ * @param $name
+ * @return array
+ */
+ protected function resolveParams($name): array
+ {
+ $config = $this->resolveConfig($name);
+ return [$config];
+ }
+
+ /**
+ * 创建驱动
+ *
+ * @param string $name
+ * @return mixed
+ *
+ */
+ protected function createDriver(string $name)
+ {
+ $type = $this->resolveType($name);
+
+ $method = 'create' . Str::studly($type) . 'Driver';
+
+ $params = $this->resolveParams($name);
+
+ if (method_exists($this, $method)) {
+ return $this->$method(...$params);
+ }
+
+ $class = $this->resolveClass($type);
+
+ return $this->app->invokeClass($class, $params);
+ }
+
+ /**
+ * 移除一个驱动实例
+ *
+ * @param array|string|null $name
+ * @return $this
+ */
+ public function forgetDriver($name = null)
+ {
+ $name = $name ?? $this->getDefaultDriver();
+
+ foreach ((array) $name as $cacheName) {
+ if (isset($this->drivers[$cacheName])) {
+ unset($this->drivers[$cacheName]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ abstract public function getDefaultDriver();
+
+ /**
+ * 动态调用
+ * @param string $method
+ * @param array $parameters
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ return $this->driver()->$method(...$parameters);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/Middleware.php b/vendor/topthink/framework/src/think/Middleware.php
new file mode 100644
index 0000000..a3db0f2
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Middleware.php
@@ -0,0 +1,257 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use InvalidArgumentException;
+use LogicException;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * 中间件管理类
+ * @package think
+ */
+class Middleware
+{
+ /**
+ * 中间件执行队列
+ * @var array
+ */
+ protected $queue = [];
+
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 导入中间件
+ * @access public
+ * @param array $middlewares
+ * @param string $type 中间件类型
+ * @return void
+ */
+ public function import(array $middlewares = [], string $type = 'global'): void
+ {
+ foreach ($middlewares as $middleware) {
+ $this->add($middleware, $type);
+ }
+ }
+
+ /**
+ * 注册中间件
+ * @access public
+ * @param mixed $middleware
+ * @param string $type 中间件类型
+ * @return void
+ */
+ public function add($middleware, string $type = 'global'): void
+ {
+ $middleware = $this->buildMiddleware($middleware, $type);
+
+ if (!empty($middleware)) {
+ $this->queue[$type][] = $middleware;
+ $this->queue[$type] = array_unique($this->queue[$type], SORT_REGULAR);
+ }
+ }
+
+ /**
+ * 注册路由中间件
+ * @access public
+ * @param mixed $middleware
+ * @return void
+ */
+ public function route($middleware): void
+ {
+ $this->add($middleware, 'route');
+ }
+
+ /**
+ * 注册控制器中间件
+ * @access public
+ * @param mixed $middleware
+ * @return void
+ */
+ public function controller($middleware): void
+ {
+ $this->add($middleware, 'controller');
+ }
+
+ /**
+ * 注册中间件到开始位置
+ * @access public
+ * @param mixed $middleware
+ * @param string $type 中间件类型
+ */
+ public function unshift($middleware, string $type = 'global')
+ {
+ $middleware = $this->buildMiddleware($middleware, $type);
+
+ if (!empty($middleware)) {
+ if (!isset($this->queue[$type])) {
+ $this->queue[$type] = [];
+ }
+
+ array_unshift($this->queue[$type], $middleware);
+ }
+ }
+
+ /**
+ * 获取注册的中间件
+ * @access public
+ * @param string $type 中间件类型
+ * @return array
+ */
+ public function all(string $type = 'global'): array
+ {
+ return $this->queue[$type] ?? [];
+ }
+
+ /**
+ * 调度管道
+ * @access public
+ * @param string $type 中间件类型
+ * @return Pipeline
+ */
+ public function pipeline(string $type = 'global')
+ {
+ return (new Pipeline())
+ ->through(array_map(function ($middleware) {
+ return function ($request, $next) use ($middleware) {
+ [$call, $params] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $call = [$this->app->make($call[0]), $call[1]];
+ }
+ $response = call_user_func($call, $request, $next, ...$params);
+
+ if (!$response instanceof Response) {
+ throw new LogicException('The middleware must return Response instance');
+ }
+ return $response;
+ };
+ }, $this->sortMiddleware($this->queue[$type] ?? [])))
+ ->whenException([$this, 'handleException']);
+ }
+
+ /**
+ * 结束调度
+ * @param Response $response
+ */
+ public function end(Response $response)
+ {
+ foreach ($this->queue as $queue) {
+ foreach ($queue as $middleware) {
+ [$call] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $instance = $this->app->make($call[0]);
+ if (method_exists($instance, 'end')) {
+ $instance->end($response);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 异常处理
+ * @param Request $passable
+ * @param Throwable $e
+ * @return Response
+ */
+ public function handleException($passable, Throwable $e)
+ {
+ /** @var Handle $handler */
+ $handler = $this->app->make(Handle::class);
+
+ $handler->report($e);
+
+ return $handler->render($passable, $e);
+ }
+
+ /**
+ * 解析中间件
+ * @access protected
+ * @param mixed $middleware
+ * @param string $type 中间件类型
+ * @return array
+ */
+ protected function buildMiddleware($middleware, string $type): array
+ {
+ if (is_array($middleware)) {
+ [$middleware, $params] = $middleware;
+ }
+
+ if ($middleware instanceof Closure) {
+ return [$middleware, $params ?? []];
+ }
+
+ if (!is_string($middleware)) {
+ throw new InvalidArgumentException('The middleware is invalid');
+ }
+
+ //中间件别名检查
+ $alias = $this->app->config->get('middleware.alias', []);
+
+ if (isset($alias[$middleware])) {
+ $middleware = $alias[$middleware];
+ }
+
+ if (is_array($middleware)) {
+ $this->import($middleware, $type);
+ return [];
+ }
+
+ return [[$middleware, 'handle'], $params ?? []];
+ }
+
+ /**
+ * 中间件排序
+ * @param array $middlewares
+ * @return array
+ */
+ protected function sortMiddleware(array $middlewares)
+ {
+ $priority = $this->app->config->get('middleware.priority', []);
+ uasort($middlewares, function ($a, $b) use ($priority) {
+ $aPriority = $this->getMiddlewarePriority($priority, $a);
+ $bPriority = $this->getMiddlewarePriority($priority, $b);
+ return $bPriority - $aPriority;
+ });
+
+ return $middlewares;
+ }
+
+ /**
+ * 获取中间件优先级
+ * @param $priority
+ * @param $middleware
+ * @return int
+ */
+ protected function getMiddlewarePriority($priority, $middleware)
+ {
+ [$call] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $index = array_search($call[0], array_reverse($priority));
+ return false === $index ? -1 : $index;
+ }
+ return -1;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/Pipeline.php b/vendor/topthink/framework/src/think/Pipeline.php
new file mode 100644
index 0000000..77151f3
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Pipeline.php
@@ -0,0 +1,107 @@
+
+// +----------------------------------------------------------------------
+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..1c15f63
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Request.php
@@ -0,0 +1,2169 @@
+
+// +----------------------------------------------------------------------
+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) {
+ $sub = stristr($this->host(), $rootDomain, true);
+ $this->subDomain = $sub ? rtrim($sub, '.') : '';
+ } 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 string|array $name 变量名
+ * @param string|array $filter 过滤方法
+ * @return mixed
+ */
+ public function all($name = '', $filter = '')
+ {
+ $data = array_merge($this->param(), $this->file() ?: []);
+
+ if (is_array($name)) {
+ $data = $this->only($name, $data, $filter);
+ } elseif ($name) {
+ $data = $data[$name] ?? null;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 设置路由变量
+ * @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 protected
+ * @param mixed $data
+ * @param string $type
+ * @return mixed
+ */
+ protected 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 protected
+ * @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 = 0;
+ 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..a8a61ff
--- /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 bool
+ */
+ 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..a3acf85
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Route.php
@@ -0,0 +1,926 @@
+
+// +----------------------------------------------------------------------
+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');
+ }
+
+ $this->config = array_merge($this->config, $this->app->config->get('route'));
+ }
+
+ protected function init()
+ {
+ 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 array
+ */
+ 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 (Request $request) use ($status, $route) {
+ $search = $replace = [];
+ $matches = $request->rule()->getVars();
+
+ foreach ($matches as $key => $value) {
+ $search[] = '<' . $key . '>';
+ $replace[] = $value;
+
+ $search[] = ':' . $key;
+ $replace[] = $value;
+ }
+
+ $route = str_replace($search, $replace, $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..d9e8960
--- /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..6c84faf
--- /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..0788406
--- /dev/null
+++ b/vendor/topthink/framework/src/think/Validate.php
@@ -0,0 +1,1688 @@
+
+// +----------------------------------------------------------------------
+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}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}]+$/u',
+ 'chsAlpha' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}a-zA-Z]+$/u',
+ 'chsAlphaNum' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}a-zA-Z0-9]+$/u',
+ 'chsDash' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}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;
+ unset($this->append[$key]);
+ }
+ }
+
+ 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);
+ unset($this->append[$field]);
+ }
+
+ 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 ?? true;
+ }
+
+ /**
+ * 获取当前验证类型及规则
+ * @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] ?? 0;
+ $rule = $rule[0];
+ } else {
+ $param = 0;
+ }
+
+ 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|array
+ */
+ 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);
+ }
+
+ // rule若是数组则转为字符串
+ if (is_array($rule)) {
+ $rule = implode(',', $rule);
+ }
+
+ 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..2e71088
--- /dev/null
+++ b/vendor/topthink/framework/src/think/View.php
@@ -0,0 +1,191 @@
+
+// +----------------------------------------------------------------------
+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();
+ if (PHP_VERSION > 8.0) {
+ ob_implicit_flush(false);
+ } else {
+ 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..5813c7b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/Driver.php
@@ -0,0 +1,357 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache;
+
+use Closure;
+use DateInterval;
+use DateTime;
+use DateTimeInterface;
+use Exception;
+use Psr\SimpleCache\CacheInterface;
+use think\Container;
+use think\contract\CacheHandlerInterface;
+use think\exception\InvalidArgumentException;
+use throwable;
+
+/**
+ * 缓存基础类
+ */
+abstract class Driver implements CacheInterface, CacheHandlerInterface
+{
+ /**
+ * 驱动句柄
+ * @var object
+ */
+ protected $handler = null;
+
+ /**
+ * 缓存读取次数
+ * @var integer
+ */
+ protected $readTimes = 0;
+
+ /**
+ * 缓存写入次数
+ * @var integer
+ */
+ protected $writeTimes = 0;
+
+ /**
+ * 缓存参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 缓存标签
+ * @var array
+ */
+ protected $tag = [];
+
+ /**
+ * 获取有效期
+ * @access protected
+ * @param integer|DateTimeInterface|DateInterval $expire 有效期
+ * @return int
+ */
+ protected function getExpireTime($expire): int
+ {
+ if ($expire instanceof DateTimeInterface) {
+ $expire = $expire->getTimestamp() - time();
+ } elseif ($expire instanceof DateInterval) {
+ $expire = DateTime::createFromFormat('U', (string) time())
+ ->add($expire)
+ ->format('U') - time();
+ }
+
+ return (int) $expire;
+ }
+
+ /**
+ * 获取实际的缓存标识
+ * @access public
+ * @param string $name 缓存名
+ * @return string
+ */
+ public function getCacheKey(string $name): string
+ {
+ return $this->options['prefix'] . $name;
+ }
+
+ /**
+ * 读取缓存并删除
+ * @access public
+ * @param string $name 缓存变量名
+ * @return mixed
+ */
+ public function pull(string $name)
+ {
+ $result = $this->get($name, false);
+
+ if ($result) {
+ $this->delete($name);
+ return $result;
+ }
+ }
+
+ /**
+ * 追加(数组)缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @return void
+ */
+ public function push(string $name, $value): void
+ {
+ $item = $this->get($name, []);
+
+ if (!is_array($item)) {
+ throw new InvalidArgumentException('only array cache can be push');
+ }
+
+ $item[] = $value;
+
+ if (count($item) > 1000) {
+ array_shift($item);
+ }
+
+ $item = array_unique($item);
+
+ $this->set($name, $item);
+ }
+
+ /**
+ * 追加TagSet数据
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @return void
+ */
+ public function append(string $name, $value): void
+ {
+ $this->push($name, $value);
+ }
+
+ /**
+ * 如果不存在则写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int $expire 有效时间 0为永久
+ * @return mixed
+ */
+ public function remember(string $name, $value, $expire = null)
+ {
+ if ($this->has($name)) {
+ 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])) {
+ $this->tag[$key] = new TagSet($name, $this);
+ }
+
+ return $this->tag[$key];
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 标签标识
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ $name = $this->getTagKey($tag);
+ return $this->get($name, []);
+ }
+
+ /**
+ * 获取实际标签名
+ * @access public
+ * @param string $tag 标签名
+ * @return string
+ */
+ public function getTagKey(string $tag): string
+ {
+ return $this->options['tag_prefix'] . md5($tag);
+ }
+
+ /**
+ * 序列化数据
+ * @access protected
+ * @param mixed $data 缓存数据
+ * @return string
+ */
+ protected function serialize($data): string
+ {
+ if (is_numeric($data)) {
+ return (string) $data;
+ }
+
+ $serialize = $this->options['serialize'][0] ?? "serialize";
+
+ return $serialize($data);
+ }
+
+ /**
+ * 反序列化数据
+ * @access protected
+ * @param string $data 缓存数据
+ * @return mixed
+ */
+ protected function unserialize(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..5ba2076
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/TagSet.php
@@ -0,0 +1,132 @@
+
+// +----------------------------------------------------------------------
+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) {
+ $key = $this->handler->getTagKey($tag);
+ $this->handler->append($key, $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);
+
+ $key = $this->handler->getTagKey($tag);
+ $this->handler->delete($key);
+ }
+
+ 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..b36b069
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/File.php
@@ -0,0 +1,304 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use FilesystemIterator;
+use think\App;
+use think\cache\Driver;
+
+/**
+ * 文件缓存类
+ */
+class File extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'expire' => 0,
+ 'cache_subdir' => true,
+ 'prefix' => '',
+ 'path' => '',
+ 'hash_type' => 'md5',
+ 'data_compress' => false,
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ ];
+
+ /**
+ * 架构函数
+ * @param App $app
+ * @param array $options 参数
+ */
+ public function __construct(App $app, array $options = [])
+ {
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ if (empty($this->options['path'])) {
+ $this->options['path'] = $app->getRuntimePath() . 'cache';
+ }
+
+ if (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) {
+ $this->options['path'] .= DIRECTORY_SEPARATOR;
+ }
+ }
+
+ /**
+ * 取得变量的存储文件名
+ * @access public
+ * @param string $name 缓存变量名
+ * @return string
+ */
+ public function getCacheKey(string $name): string
+ {
+ $name = hash($this->options['hash_type'], $name);
+
+ if ($this->options['cache_subdir']) {
+ // 使用子目录
+ $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2);
+ }
+
+ if ($this->options['prefix']) {
+ $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name;
+ }
+
+ return $this->options['path'] . $name . '.php';
+ }
+
+ /**
+ * 获取缓存数据
+ * @param string $name 缓存标识名
+ * @return array|null
+ */
+ protected function getRaw(string $name)
+ {
+ $filename = $this->getCacheKey($name);
+
+ if (!is_file($filename)) {
+ return;
+ }
+
+ $content = @file_get_contents($filename);
+
+ if (false !== $content) {
+ $expire = (int) substr($content, 8, 12);
+ if (0 != $expire && time() - $expire > filemtime($filename)) {
+ //缓存过期删除缓存文件
+ $this->unlink($filename);
+ return;
+ }
+
+ $content = substr($content, 32);
+
+ if ($this->options['data_compress'] && function_exists('gzcompress')) {
+ //启用数据压缩
+ $content = gzuncompress($content);
+ }
+
+ return is_string($content) ? ['content' => $content, 'expire' => $expire] : null;
+ }
+ }
+
+ /**
+ * 判断缓存是否存在
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ return $this->getRaw($name) !== null;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null)
+ {
+ $this->readTimes++;
+
+ $raw = $this->getRaw($name);
+
+ return is_null($raw) ? $default : $this->unserialize($raw['content']);
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|\DateTime $expire 有效时间 0为永久
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ $this->writeTimes++;
+
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $expire = $this->getExpireTime($expire);
+ $filename = $this->getCacheKey($name);
+
+ $dir = dirname($filename);
+
+ if (!is_dir($dir)) {
+ try {
+ mkdir($dir, 0755, true);
+ } catch (\Exception $e) {
+ // 创建失败
+ }
+ }
+
+ $data = $this->serialize($value);
+
+ if ($this->options['data_compress'] && function_exists('gzcompress')) {
+ //数据压缩
+ $data = gzcompress($data, 3);
+ }
+
+ $data = "\n" . $data;
+ $result = file_put_contents($filename, $data);
+
+ if ($result) {
+ clearstatcache();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc(string $name, int $step = 1)
+ {
+ if ($raw = $this->getRaw($name)) {
+ $value = $this->unserialize($raw['content']) + $step;
+ $expire = $raw['expire'];
+ } else {
+ $value = $step;
+ $expire = 0;
+ }
+
+ return $this->set($name, $value, $expire) ? $value : false;
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec(string $name, int $step = 1)
+ {
+ return $this->inc($name, -$step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ $this->writeTimes++;
+
+ return $this->unlink($this->getCacheKey($name));
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->writeTimes++;
+
+ $dirname = $this->options['path'] . $this->options['prefix'];
+
+ $this->rmdir($dirname);
+
+ return true;
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys): void
+ {
+ foreach ($keys as $key) {
+ $this->unlink($key);
+ }
+ }
+
+ /**
+ * 判断文件是否存在后,删除
+ * @access private
+ * @param string $path
+ * @return bool
+ */
+ private function unlink(string $path): bool
+ {
+ try {
+ return is_file($path) && unlink($path);
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 删除文件夹
+ * @param $dirname
+ * @return bool
+ */
+ private function rmdir($dirname)
+ {
+ if (!is_dir($dirname)) {
+ return false;
+ }
+
+ $items = new FilesystemIterator($dirname);
+
+ foreach ($items as $item) {
+ if ($item->isDir() && !$item->isLink()) {
+ $this->rmdir($item->getPathname());
+ } else {
+ $this->unlink($item->getPathname());
+ }
+ }
+
+ @rmdir($dirname);
+
+ return true;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/cache/driver/Memcache.php b/vendor/topthink/framework/src/think/cache/driver/Memcache.php
new file mode 100644
index 0000000..2fbbb9c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/Memcache.php
@@ -0,0 +1,209 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Memcache缓存类
+ */
+class Memcache extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 11211,
+ 'expire' => 0,
+ 'timeout' => 0, // 超时时间(单位:毫秒)
+ 'persistent' => true,
+ 'prefix' => '',
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ * @throws \BadFunctionCallException
+ */
+ public function __construct(array $options = [])
+ {
+ if (!extension_loaded('memcache')) {
+ throw new \BadFunctionCallException('not support: memcache');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ $this->handler = new \Memcache;
+
+ // 支持集群
+ $hosts = (array) $this->options['host'];
+ $ports = (array) $this->options['port'];
+
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ foreach ($hosts as $i => $host) {
+ $port = $ports[$i] ?? $ports[0];
+ $this->options['timeout'] > 0 ?
+ $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1, (int) $this->options['timeout']) :
+ $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return false !== $this->handler->get($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null)
+ {
+ $this->readTimes++;
+
+ $result = $this->handler->get($this->getCacheKey($name));
+
+ return false !== $result ? $this->unserialize($result) : $default;
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|\DateTime $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ $this->writeTimes++;
+
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if ($this->handler->set($key, $value, 0, $expire)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc(string $name, int $step = 1)
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ if ($this->handler->get($key)) {
+ return $this->handler->increment($key, $step);
+ }
+
+ return $this->handler->set($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec(string $name, int $step = 1)
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+ $value = $this->handler->get($key) - $step;
+ $res = $this->handler->set($key, $value);
+
+ return !$res ? false : $value;
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param bool|false $ttl
+ * @return bool
+ */
+ public function delete($name, $ttl = false): bool
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ return false === $ttl ?
+ $this->handler->delete($key) :
+ $this->handler->delete($key, $ttl);
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->writeTimes++;
+
+ return $this->handler->flush();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys): void
+ {
+ foreach ($keys as $key) {
+ $this->handler->delete($key);
+ }
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/cache/driver/Memcached.php b/vendor/topthink/framework/src/think/cache/driver/Memcached.php
new file mode 100644
index 0000000..71edb05
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/Memcached.php
@@ -0,0 +1,221 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Memcached缓存类
+ */
+class Memcached extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 11211,
+ 'expire' => 0,
+ 'timeout' => 0, // 超时时间(单位:毫秒)
+ 'prefix' => '',
+ 'username' => '', //账号
+ 'password' => '', //密码
+ 'option' => [],
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ */
+ public function __construct(array $options = [])
+ {
+ if (!extension_loaded('memcached')) {
+ throw new \BadFunctionCallException('not support: memcached');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ $this->handler = new \Memcached;
+
+ if (!empty($this->options['option'])) {
+ $this->handler->setOptions($this->options['option']);
+ }
+
+ // 设置连接超时时间(单位:毫秒)
+ if ($this->options['timeout'] > 0) {
+ $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']);
+ }
+
+ // 支持集群
+ $hosts = (array) $this->options['host'];
+ $ports = (array) $this->options['port'];
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ $servers = [];
+ foreach ($hosts as $i => $host) {
+ $servers[] = [$host, $ports[$i] ?? $ports[0], 1];
+ }
+
+ $this->handler->addServers($servers);
+
+ if ('' != $this->options['username']) {
+ $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return $this->handler->get($key) ? true : false;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null)
+ {
+ $this->readTimes++;
+
+ $result = $this->handler->get($this->getCacheKey($name));
+
+ return false !== $result ? $this->unserialize($result) : $default;
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|\DateTime $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ $this->writeTimes++;
+
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if ($this->handler->set($key, $value, $expire)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc(string $name, int $step = 1)
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ if ($this->handler->get($key)) {
+ return $this->handler->increment($key, $step);
+ }
+
+ return $this->handler->set($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec(string $name, int $step = 1)
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+ $value = $this->handler->get($key) - $step;
+ $res = $this->handler->set($key, $value);
+
+ return !$res ? false : $value;
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param bool|false $ttl
+ * @return bool
+ */
+ public function delete($name, $ttl = false): bool
+ {
+ $this->writeTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ return false === $ttl ?
+ $this->handler->delete($key) :
+ $this->handler->delete($key, $ttl);
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->writeTimes++;
+
+ return $this->handler->flush();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag(array $keys): void
+ {
+ $this->handler->deleteMulti($keys);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/cache/driver/Redis.php b/vendor/topthink/framework/src/think/cache/driver/Redis.php
new file mode 100644
index 0000000..791b27b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/cache/driver/Redis.php
@@ -0,0 +1,249 @@
+
+// +----------------------------------------------------------------------
+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((int) $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++;
+ $key = $this->getCacheKey($name);
+ $value = $this->handler->get($key);
+
+ 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++;
+
+ $key = $this->getCacheKey($name);
+ $result = $this->handler->del($key);
+ 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);
+ }
+
+ /**
+ * 追加TagSet数据
+ * @access public
+ * @param string $name 缓存标识
+ * @param mixed $value 数据
+ * @return void
+ */
+ public function append(string $name, $value): void
+ {
+ $key = $this->getCacheKey($name);
+ $this->handler->sAdd($key, $value);
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 缓存标签
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ $name = $this->getTagKey($tag);
+ $key = $this->getCacheKey($name);
+ return $this->handler->sMembers($key);
+ }
+
+}
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..8b3e8b8
--- /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..bd3fb20
--- /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..9ae9077
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/Input.php
@@ -0,0 +1,465 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+class Input
+{
+
+ /**
+ * @var Definition
+ */
+ protected $definition;
+
+ /**
+ * @var Option[]
+ */
+ protected $options = [];
+
+ /**
+ * @var Argument[]
+ */
+ protected $arguments = [];
+
+ protected $interactive = true;
+
+ private $tokens;
+ private $parsed;
+
+ public function __construct($argv = null)
+ {
+ if (null === $argv) {
+ $argv = $_SERVER['argv'];
+ // 去除命令名
+ array_shift($argv);
+ }
+
+ $this->tokens = $argv;
+
+ $this->definition = new Definition();
+ }
+
+ protected function setTokens(array $tokens)
+ {
+ $this->tokens = $tokens;
+ }
+
+ /**
+ * 绑定实例
+ * @param Definition $definition A InputDefinition instance
+ */
+ public function bind(Definition $definition): void
+ {
+ $this->arguments = [];
+ $this->options = [];
+ $this->definition = $definition;
+
+ $this->parse();
+ }
+
+ /**
+ * 解析参数
+ */
+ protected function parse(): void
+ {
+ $parseOptions = true;
+ $this->parsed = $this->tokens;
+ while (null !== $token = array_shift($this->parsed)) {
+ if ($parseOptions && '' == $token) {
+ $this->parseArgument($token);
+ } elseif ($parseOptions && '--' == $token) {
+ $parseOptions = false;
+ } elseif ($parseOptions && 0 === strpos($token, '--')) {
+ $this->parseLongOption($token);
+ } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
+ $this->parseShortOption($token);
+ } else {
+ $this->parseArgument($token);
+ }
+ }
+ }
+
+ /**
+ * 解析短选项
+ * @param string $token 当前的指令.
+ */
+ private function parseShortOption(string $token): void
+ {
+ $name = substr($token, 1);
+
+ if (strlen($name) > 1) {
+ if ($this->definition->hasShortcut($name[0])
+ && $this->definition->getOptionForShortcut($name[0])->acceptValue()
+ ) {
+ $this->addShortOption($name[0], substr($name, 1));
+ } else {
+ $this->parseShortOptionSet($name);
+ }
+ } else {
+ $this->addShortOption($name, null);
+ }
+ }
+
+ /**
+ * 解析短选项
+ * @param string $name 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseShortOptionSet(string $name): void
+ {
+ $len = strlen($name);
+ for ($i = 0; $i < $len; ++$i) {
+ if (!$this->definition->hasShortcut($name[$i])) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i]));
+ }
+
+ $option = $this->definition->getOptionForShortcut($name[$i]);
+ if ($option->acceptValue()) {
+ $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
+
+ break;
+ } else {
+ $this->addLongOption($option->getName(), null);
+ }
+ }
+ }
+
+ /**
+ * 解析完整选项
+ * @param string $token 当前指令
+ */
+ private function parseLongOption(string $token): void
+ {
+ $name = substr($token, 2);
+
+ if (false !== $pos = strpos($name, '=')) {
+ $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1));
+ } else {
+ $this->addLongOption($name, null);
+ }
+ }
+
+ /**
+ * 解析参数
+ * @param string $token 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseArgument(string $token): void
+ {
+ $c = count($this->arguments);
+
+ if ($this->definition->hasArgument($c)) {
+ $arg = $this->definition->getArgument($c);
+
+ $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
+
+ } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
+ $arg = $this->definition->getArgument($c - 1);
+
+ $this->arguments[$arg->getName()][] = $token;
+ } else {
+ throw new \RuntimeException('Too many arguments.');
+ }
+ }
+
+ /**
+ * 添加一个短选项的值
+ * @param string $shortcut 短名称
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addShortOption(string $shortcut, $value): void
+ {
+ if (!$this->definition->hasShortcut($shortcut)) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
+ }
+
+ /**
+ * 添加一个完整选项的值
+ * @param string $name 选项名
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addLongOption(string $name, $value): void
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ $option = $this->definition->getOption($name);
+
+ if (false === $value) {
+ $value = null;
+ }
+
+ if (null !== $value && !$option->acceptValue()) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value));
+ }
+
+ if (null === $value && $option->acceptValue() && count($this->parsed)) {
+ $next = array_shift($this->parsed);
+ if (isset($next[0]) && '-' !== $next[0]) {
+ $value = $next;
+ } elseif (empty($next)) {
+ $value = '';
+ } else {
+ array_unshift($this->parsed, $next);
+ }
+ }
+
+ if (null === $value) {
+ if ($option->isValueRequired()) {
+ throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name));
+ }
+
+ if (!$option->isArray()) {
+ $value = $option->isValueOptional() ? $option->getDefault() : true;
+ }
+ }
+
+ if ($option->isArray()) {
+ $this->options[$name][] = $value;
+ } else {
+ $this->options[$name] = $value;
+ }
+ }
+
+ /**
+ * 获取第一个参数
+ * @return string|null
+ */
+ public function getFirstArgument()
+ {
+ foreach ($this->tokens as $token) {
+ if ($token && '-' === $token[0]) {
+ continue;
+ }
+
+ return $token;
+ }
+ return;
+ }
+
+ /**
+ * 检查原始参数是否包含某个值
+ * @param string|array $values 需要检查的值
+ * @return bool
+ */
+ public function hasParameterOption($values): bool
+ {
+ $values = (array) $values;
+
+ foreach ($this->tokens as $token) {
+ foreach ($values as $value) {
+ if ($token === $value || 0 === strpos($token, $value . '=')) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取原始选项的值
+ * @param string|array $values 需要检查的值
+ * @param mixed $default 默认值
+ * @return mixed The option value
+ */
+ public function getParameterOption($values, $default = false)
+ {
+ $values = (array) $values;
+ $tokens = $this->tokens;
+
+ while (0 < count($tokens)) {
+ $token = array_shift($tokens);
+
+ foreach ($values as $value) {
+ if ($token === $value || 0 === strpos($token, $value . '=')) {
+ if (false !== $pos = strpos($token, '=')) {
+ return substr($token, $pos + 1);
+ }
+
+ return array_shift($tokens);
+ }
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * 验证输入
+ * @throws \RuntimeException
+ */
+ public function validate()
+ {
+ if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) {
+ throw new \RuntimeException('Not enough arguments.');
+ }
+ }
+
+ /**
+ * 检查输入是否是交互的
+ * @return bool
+ */
+ public function isInteractive(): bool
+ {
+ return $this->interactive;
+ }
+
+ /**
+ * 设置输入的交互
+ * @param bool
+ */
+ public function setInteractive(bool $interactive): void
+ {
+ $this->interactive = $interactive;
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[]
+ */
+ public function getArguments(): array
+ {
+ return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
+ }
+
+ /**
+ * 根据名称获取参数
+ * @param string $name 参数名
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument(string $name)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ return $this->arguments[$name] ?? $this->definition->getArgument($name)
+ ->getDefault();
+ }
+
+ /**
+ * 设置参数的值
+ * @param string $name 参数名
+ * @param string $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setArgument(string $name, $value)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $this->arguments[$name] = $value;
+ }
+
+ /**
+ * 检查是否存在某个参数
+ * @param string|int $name 参数名或位置
+ * @return bool
+ */
+ public function hasArgument($name): bool
+ {
+ return $this->definition->hasArgument($name);
+ }
+
+ /**
+ * 获取所有的选项
+ * @return Option[]
+ */
+ public function getOptions(): array
+ {
+ return array_merge($this->definition->getOptionDefaults(), $this->options);
+ }
+
+ /**
+ * 获取选项值
+ * @param string $name 选项名称
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getOption(string $name)
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ return $this->options[$name] ?? $this->definition->getOption($name)->getDefault();
+ }
+
+ /**
+ * 设置选项值
+ * @param string $name 选项名
+ * @param string|bool $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setOption(string $name, $value): void
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ $this->options[$name] = $value;
+ }
+
+ /**
+ * 是否有某个选项
+ * @param string $name 选项名
+ * @return bool
+ */
+ public function hasOption(string $name): bool
+ {
+ return $this->definition->hasOption($name) && isset($this->options[$name]);
+ }
+
+ /**
+ * 转义指令
+ * @param string $token
+ * @return string
+ */
+ public function escapeToken(string $token): string
+ {
+ return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
+ }
+
+ /**
+ * 返回传递给命令的参数的字符串
+ * @return string
+ */
+ public function __toString()
+ {
+ $tokens = array_map(function ($token) {
+ if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
+ return $match[1] . $this->escapeToken($match[2]);
+ }
+
+ if ($token && '-' !== $token[0]) {
+ return $this->escapeToken($token);
+ }
+
+ return $token;
+ }, $this->tokens);
+
+ return implode(' ', $tokens);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/LICENSE b/vendor/topthink/framework/src/think/console/LICENSE
new file mode 100644
index 0000000..0abe056
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-2016 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/vendor/topthink/framework/src/think/console/Output.php b/vendor/topthink/framework/src/think/console/Output.php
new file mode 100644
index 0000000..294c4b8
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/Output.php
@@ -0,0 +1,231 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use Exception;
+use think\console\output\Ask;
+use think\console\output\Descriptor;
+use think\console\output\driver\Buffer;
+use think\console\output\driver\Console;
+use think\console\output\driver\Nothing;
+use think\console\output\Question;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+use Throwable;
+
+/**
+ * Class Output
+ * @package think\console
+ *
+ * @see \think\console\output\driver\Console::setDecorated
+ * @method void setDecorated($decorated)
+ *
+ * @see \think\console\output\driver\Buffer::fetch
+ * @method string fetch()
+ *
+ * @method void info($message)
+ * @method void error($message)
+ * @method void comment($message)
+ * @method void warning($message)
+ * @method void highlight($message)
+ * @method void question($message)
+ */
+class Output
+{
+ // 不显示信息(静默)
+ const VERBOSITY_QUIET = 0;
+ // 正常信息
+ const VERBOSITY_NORMAL = 1;
+ // 详细信息
+ const VERBOSITY_VERBOSE = 2;
+ // 非常详细的信息
+ const VERBOSITY_VERY_VERBOSE = 3;
+ // 调试信息
+ const VERBOSITY_DEBUG = 4;
+
+ const OUTPUT_NORMAL = 0;
+ const OUTPUT_RAW = 1;
+ const OUTPUT_PLAIN = 2;
+
+ // 输出信息级别
+ private $verbosity = self::VERBOSITY_NORMAL;
+
+ /** @var Buffer|Console|Nothing */
+ private $handle = null;
+
+ protected $styles = [
+ 'info',
+ 'error',
+ 'comment',
+ 'question',
+ 'highlight',
+ 'warning',
+ ];
+
+ public function __construct($driver = 'console')
+ {
+ $class = '\\think\\console\\output\\driver\\' . ucwords($driver);
+
+ $this->handle = new $class($this);
+ }
+
+ public function ask(Input $input, $question, $default = null, $validator = null)
+ {
+ $question = new Question($question, $default);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function askHidden(Input $input, $question, $validator = null)
+ {
+ $question = new Question($question);
+
+ $question->setHidden(true);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function confirm(Input $input, $question, $default = true)
+ {
+ return $this->askQuestion($input, new Confirmation($question, $default));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function choice(Input $input, $question, array $choices, $default = null)
+ {
+ if (null !== $default) {
+ $values = array_flip($choices);
+ $default = $values[$default];
+ }
+
+ return $this->askQuestion($input, new Choice($question, $choices, $default));
+ }
+
+ protected function askQuestion(Input $input, Question $question)
+ {
+ $ask = new Ask($input, $this, $question);
+ $answer = $ask->run();
+
+ if ($input->isInteractive()) {
+ $this->newLine();
+ }
+
+ return $answer;
+ }
+
+ protected function block(string $style, string $message): void
+ {
+ $this->writeln("<{$style}>{$message}$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..5a861d7
--- /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..9acc52f
--- /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..da70b35
--- /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..2e4f2ca
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Help.php
@@ -0,0 +1,70 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+
+class Help extends Command
+{
+
+ private $command;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->ignoreValidationErrors();
+
+ $this->setName('help')->setDefinition([
+ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
+ ])->setDescription('Displays help for a command')->setHelp(
+ <<%command.name% command displays help for a given command:
+
+ php %command.full_name% list
+
+To display the list of available commands, please use the list command.
+EOF
+ );
+ }
+
+ /**
+ * Sets the command.
+ * @param Command $command The command to set
+ */
+ public function setCommand(Command $command): void
+ {
+ $this->command = $command;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ if (null === $this->command) {
+ $this->command = $this->getConsole()->find($input->getArgument('command_name'));
+ }
+
+ $output->describe($this->command, [
+ 'raw_text' => $input->getOption('raw'),
+ ]);
+
+ $this->command = null;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/Lists.php b/vendor/topthink/framework/src/think/console/command/Lists.php
new file mode 100644
index 0000000..d20fc75
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/Lists.php
@@ -0,0 +1,74 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+
+class Lists extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(
+ <<%command.name% command lists all commands:
+
+ php %command.full_name%
+
+You can also display the commands for a specific namespace:
+
+ php %command.full_name% test
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ php %command.full_name% --raw
+EOF
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNativeDefinition(): InputDefinition
+ {
+ return $this->createDefinition();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ $output->describe($this->getConsole(), [
+ 'raw_text' => $input->getOption('raw'),
+ 'namespace' => $input->getArgument('namespace'),
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function createDefinition(): InputDefinition
+ {
+ return new InputDefinition([
+ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
+ ]);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/Make.php b/vendor/topthink/framework/src/think/console/command/Make.php
new file mode 100644
index 0000000..662b337
--- /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..ed579b8
--- /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..d507c1d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/RunServer.php
@@ -0,0 +1,72 @@
+
+// +----------------------------------------------------------------------
+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 ', $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..e90f433
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/ServiceDiscover.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+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);
+ // Compatibility with Composer 2.0
+ if (isset($packages['packages'])) {
+ $packages = $packages['packages'];
+ }
+
+ $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..3998765
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/VendorPublish.php
@@ -0,0 +1,69 @@
+
+// +----------------------------------------------------------------------
+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);
+ // Compatibility with Composer 2.0
+ if (isset($packages['packages'])) {
+ $packages = $packages['packages'];
+ }
+ 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..beb49d2
--- /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..9549a02
--- /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..4a8d226
--- /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..6b16689
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Event.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Event extends Make
+{
+ protected $type = "Event";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:event')
+ ->setDescription('Create a new event class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'event.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\event';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Listener.php b/vendor/topthink/framework/src/think/console/command/make/Listener.php
new file mode 100644
index 0000000..5c92673
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Listener.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Listener extends Make
+{
+ protected $type = "Listener";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:listener')
+ ->setDescription('Create a new listener class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'listener.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\listener';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Middleware.php b/vendor/topthink/framework/src/think/console/command/make/Middleware.php
new file mode 100644
index 0000000..3b68b4a
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Middleware.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Middleware extends Make
+{
+ protected $type = "Middleware";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:middleware')
+ ->setDescription('Create a new middleware class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'middleware.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\middleware';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Model.php b/vendor/topthink/framework/src/think/console/command/make/Model.php
new file mode 100644
index 0000000..cb7a23c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/make/Model.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Model extends Make
+{
+ protected $type = "Model";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:model')
+ ->setDescription('Create a new model class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'model.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\model';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/command/make/Service.php b/vendor/topthink/framework/src/think/console/command/make/Service.php
new file mode 100644
index 0000000..c4bbaa0
--- /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..a1dc2a8
--- /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..8d36431
--- /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..3ee2b1c
--- /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..5d3383d
--- /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..56f7f5a
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/command/optimize/Route.php
@@ -0,0 +1,66 @@
+
+// +----------------------------------------------------------------------
+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);
+ $rules = $this->app->route->getName();
+
+ return '
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use Exception;
+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')) {
+ try {
+ /** @var \think\Model $model */
+ $model = new $class;
+ $connection = $model->db()->getConnection();
+ if ($connection instanceof PDOConnection) {
+ $table = $model->getTable();
+ //预读字段信息
+ $connection->getSchemaInfo($table, true);
+ }
+ } catch (Exception $e) {
+
+ }
+ }
+ }
+
+ 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..86cca36
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/input/Argument.php
@@ -0,0 +1,138 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Argument
+{
+ // 必传参数
+ const REQUIRED = 1;
+
+ // 可选参数
+ const OPTIONAL = 2;
+
+ // 数组参数
+ const IS_ARRAY = 4;
+
+ /**
+ * 参数名
+ * @var string
+ */
+ private $name;
+
+ /**
+ * 参数类型
+ * @var int
+ */
+ private $mode;
+
+ /**
+ * 参数默认值
+ * @var mixed
+ */
+ private $default;
+
+ /**
+ * 参数描述
+ * @var string
+ */
+ 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..ccf02a0
--- /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..19c7e1e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/input/Option.php
@@ -0,0 +1,221 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+/**
+ * 命令行选项
+ * @package think\console\input
+ */
+class Option
+{
+ // 无需传值
+ const VALUE_NONE = 1;
+ // 必须传值
+ const VALUE_REQUIRED = 2;
+ // 可选传值
+ const VALUE_OPTIONAL = 4;
+ // 传数组值
+ const VALUE_IS_ARRAY = 8;
+
+ /**
+ * 选项名
+ * @var string
+ */
+ private $name;
+
+ /**
+ * 选项短名称
+ * @var string
+ */
+ private $shortcut;
+
+ /**
+ * 选项类型
+ * @var int
+ */
+ private $mode;
+
+ /**
+ * 选项默认值
+ * @var mixed
+ */
+ private $default;
+
+ /**
+ * 选项描述
+ * @var string
+ */
+ private $description;
+
+ /**
+ * 构造方法
+ * @param string $name 选项名
+ * @param string|array $shortcut 短名称,多个用|隔开或者使用数组
+ * @param int $mode 选项类型(可选类型为 self::VALUE_*)
+ * @param string $description 描述
+ * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null)
+ {
+ if (0 === strpos($name, '--')) {
+ $name = substr($name, 2);
+ }
+
+ if (empty($name)) {
+ throw new \InvalidArgumentException('An option name cannot be empty.');
+ }
+
+ if (empty($shortcut)) {
+ $shortcut = 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..56821c7
--- /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..e4a9e61
--- /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..1b97ca3
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/Formatter.php
@@ -0,0 +1,198 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\output;
+
+use think\console\output\formatter\Stack as StyleStack;
+use think\console\output\formatter\Style;
+
+class Formatter
+{
+
+ private $decorated = false;
+ private $styles = [];
+ private $styleStack;
+
+ /**
+ * 转义
+ * @param string $text
+ * @return string
+ */
+ public static function escape($text)
+ {
+ return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red'));
+ $this->setStyle('info', new Style('green'));
+ $this->setStyle('comment', new Style('yellow'));
+ $this->setStyle('question', new Style('black', 'cyan'));
+ $this->setStyle('highlight', new Style('red'));
+ $this->setStyle('warning', new Style('black', 'yellow'));
+
+ $this->styleStack = new StyleStack();
+ }
+
+ /**
+ * 设置外观标识
+ * @param bool $decorated 是否美化文字
+ */
+ public function setDecorated($decorated)
+ {
+ $this->decorated = (bool) $decorated;
+ }
+
+ /**
+ * 获取外观标识
+ * @return bool
+ */
+ public function isDecorated()
+ {
+ return $this->decorated;
+ }
+
+ /**
+ * 添加一个新样式
+ * @param string $name 样式名
+ * @param Style $style 样式实例
+ */
+ public function setStyle($name, Style $style)
+ {
+ $this->styles[strtolower($name)] = $style;
+ }
+
+ /**
+ * 是否有这个样式
+ * @param string $name
+ * @return bool
+ */
+ public function hasStyle($name)
+ {
+ return isset($this->styles[strtolower($name)]);
+ }
+
+ /**
+ * 获取样式
+ * @param string $name
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function getStyle($name)
+ {
+ if (!$this->hasStyle($name)) {
+ throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name));
+ }
+
+ return $this->styles[strtolower($name)];
+ }
+
+ /**
+ * 使用所给的样式格式化文字
+ * @param string $message 文字
+ * @return string
+ */
+ public function format($message)
+ {
+ $offset = 0;
+ $output = '';
+ $tagRegex = '[a-z][a-z0-9_=;-]*';
+ preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE);
+ foreach ($matches[0] as $i => $match) {
+ $pos = $match[1];
+ $text = $match[0];
+
+ if (0 != $pos && '\\' == $message[$pos - 1]) {
+ continue;
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset));
+ $offset = $pos + strlen($text);
+
+ if ($open = '/' != $text[1]) {
+ $tag = $matches[1][$i][0];
+ } else {
+ $tag = $matches[3][$i][0] ?? '';
+ }
+
+ if (!$open && !$tag) {
+ // >
+ $this->styleStack->pop();
+ } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) {
+ $output .= $this->applyCurrentStyle($text);
+ } elseif ($open) {
+ $this->styleStack->push($style);
+ } else {
+ $this->styleStack->pop($style);
+ }
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset));
+
+ return str_replace('\\<', '<', $output);
+ }
+
+ /**
+ * @return StyleStack
+ */
+ public function getStyleStack()
+ {
+ return $this->styleStack;
+ }
+
+ /**
+ * 根据字符串创建新的样式实例
+ * @param string $string
+ * @return Style|bool
+ */
+ private function createStyleFromString($string)
+ {
+ if (isset($this->styles[$string])) {
+ return $this->styles[$string];
+ }
+
+ if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) {
+ return false;
+ }
+
+ $style = new Style();
+ foreach ($matches as $match) {
+ array_shift($match);
+
+ if ('fg' == $match[0]) {
+ $style->setForeground($match[1]);
+ } elseif ('bg' == $match[0]) {
+ $style->setBackground($match[1]);
+ } else {
+ try {
+ $style->setOption($match[1]);
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+ }
+ }
+
+ return $style;
+ }
+
+ /**
+ * 从堆栈应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ private function applyCurrentStyle($text)
+ {
+ return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/Question.php b/vendor/topthink/framework/src/think/console/output/Question.php
new file mode 100644
index 0000000..03975f2
--- /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..ff9f464
--- /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..576f31a
--- /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..31bdf1f
--- /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..a7cc49e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/console/output/driver/Nothing.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Nothing
+{
+
+ public function __construct(Output $output)
+ {
+ // do nothing
+ }
+
+ public function write($messages, bool $newline = false, int $options = 0)
+ {
+ // do nothing
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ // do nothing
+ }
+}
diff --git a/vendor/topthink/framework/src/think/console/output/formatter/Stack.php b/vendor/topthink/framework/src/think/console/output/formatter/Stack.php
new file mode 100644
index 0000000..5366259
--- /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..2aae768
--- /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..1da1750
--- /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..bf71b5d
--- /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..da5e696
--- /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..896ac29
--- /dev/null
+++ b/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php
@@ -0,0 +1,28 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+/**
+ * 日志驱动接口
+ */
+interface LogHandlerInterface
+{
+ /**
+ * 日志写入接口
+ * @access public
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function save(array $log): bool;
+
+}
diff --git a/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php b/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php
new file mode 100644
index 0000000..1f6f994
--- /dev/null
+++ b/vendor/topthink/framework/src/think/contract/ModelRelationInterface.php
@@ -0,0 +1,99 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+use Closure;
+use think\Collection;
+use think\db\Query;
+use think\Model;
+
+/**
+ * 模型关联接口
+ */
+interface ModelRelationInterface
+{
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包查询条件
+ * @return Collection
+ */
+ public function getRelation(array $subRelation = [], Closure $closure = null): Collection;
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包条件
+ * @return void
+ */
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null): void;
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包条件
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null): void;
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 模型对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure, string $aggregate = 'count', string $field = '*', string &$name = null);
+
+ /**
+ * 创建关联统计子查询
+ * @access public
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return string
+ */
+ public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string;
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER'): Query;
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null, string $joinType = ''): Query;
+}
diff --git a/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php b/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php
new file mode 100644
index 0000000..0b2e414
--- /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..9be93d2
--- /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..dda820b
--- /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..c40da57
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/HttpEnd.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\event;
+
+/**
+ * HttpEnd事件类
+ */
+class HttpEnd
+{}
diff --git a/vendor/topthink/framework/src/think/event/HttpRun.php b/vendor/topthink/framework/src/think/event/HttpRun.php
new file mode 100644
index 0000000..ce67e93
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/HttpRun.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\event;
+
+/**
+ * HttpRun事件类
+ */
+class HttpRun
+{}
diff --git a/vendor/topthink/framework/src/think/event/LogRecord.php b/vendor/topthink/framework/src/think/event/LogRecord.php
new file mode 100644
index 0000000..237468d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/LogRecord.php
@@ -0,0 +1,29 @@
+
+// +----------------------------------------------------------------------
+namespace think\event;
+
+/**
+ * LogRecord事件类
+ */
+class LogRecord
+{
+ /** @var string */
+ public $type;
+
+ /** @var string */
+ public $message;
+
+ public function __construct($type, $message)
+ {
+ $this->type = $type;
+ $this->message = $message;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/event/LogWrite.php b/vendor/topthink/framework/src/think/event/LogWrite.php
new file mode 100644
index 0000000..a787301
--- /dev/null
+++ b/vendor/topthink/framework/src/think/event/LogWrite.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\event;
+
+/**
+ * LogWrite事件类
+ */
+class LogWrite
+{
+ /** @var string */
+ public $channel;
+
+ /** @var array */
+ public $log;
+
+ public function __construct($channel, $log)
+ {
+ $this->channel = $channel;
+ $this->log = $log;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/event/RouteLoaded.php b/vendor/topthink/framework/src/think/event/RouteLoaded.php
new file mode 100644
index 0000000..ace7992
--- /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..c4cda77
--- /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..d1a2378
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/ErrorException.php
@@ -0,0 +1,57 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use think\Exception;
+
+/**
+ * ThinkPHP错误异常
+ * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误
+ * 除开从 think\Exception 继承的功能
+ * 其他和PHP系统\ErrorException功能基本一样
+ */
+class ErrorException extends Exception
+{
+ /**
+ * 用于保存错误级别
+ * @var integer
+ */
+ protected $severity;
+
+ /**
+ * 错误异常构造函数
+ * @access public
+ * @param integer $severity 错误级别
+ * @param string $message 错误详细信息
+ * @param string $file 出错文件路径
+ * @param integer $line 出错行号
+ */
+ public function __construct(int $severity, string $message, string $file, int $line)
+ {
+ $this->severity = $severity;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->code = 0;
+ }
+
+ /**
+ * 获取错误级别
+ * @access public
+ * @return integer 错误级别
+ */
+ final public function getSeverity()
+ {
+ return $this->severity;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/exception/FileException.php b/vendor/topthink/framework/src/think/exception/FileException.php
new file mode 100644
index 0000000..228a189
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/FileException.php
@@ -0,0 +1,17 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+class FileException extends \RuntimeException
+{
+}
diff --git a/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php b/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php
new file mode 100644
index 0000000..ee2bcad
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/FuncNotFoundException.php
@@ -0,0 +1,30 @@
+message = $message;
+ $this->func = $func;
+
+ parent::__construct($message, 0, $previous);
+ }
+
+ /**
+ * 获取方法名
+ * @access public
+ * @return string
+ */
+ public function getFunc()
+ {
+ return $this->func;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/exception/Handle.php b/vendor/topthink/framework/src/think/exception/Handle.php
new file mode 100644
index 0000000..1f783bc
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/Handle.php
@@ -0,0 +1,332 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use Exception;
+use think\App;
+use think\console\Output;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+use think\Request;
+use think\Response;
+use Throwable;
+
+/**
+ * 系统异常处理类
+ */
+class Handle
+{
+ /** @var App */
+ protected $app;
+
+ protected $ignoreReport = [
+ HttpException::class,
+ HttpResponseException::class,
+ ModelNotFoundException::class,
+ DataNotFoundException::class,
+ ValidateException::class,
+ ];
+
+ protected $isJson = false;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * Report or log an exception.
+ *
+ * @access public
+ * @param Throwable $exception
+ * @return void
+ */
+ public function report(Throwable $exception): void
+ {
+ if (!$this->isIgnoreReport($exception)) {
+ // 收集异常数据
+ if ($this->app->isDebug()) {
+ $data = [
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'message' => $this->getMessage($exception),
+ 'code' => $this->getCode($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";
+ } else {
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}";
+ }
+
+ if ($this->app->config->get('log.record_trace')) {
+ $log .= PHP_EOL . $exception->getTraceAsString();
+ }
+
+ try {
+ $this->app->log->record($log, 'error');
+ } catch (Exception $e) {}
+ }
+ }
+
+ protected function isIgnoreReport(Throwable $exception): bool
+ {
+ foreach ($this->ignoreReport as $class) {
+ if ($exception instanceof $class) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Render an exception into an HTTP response.
+ *
+ * @access public
+ * @param Request $request
+ * @param Throwable $e
+ * @return Response
+ */
+ public function render($request, Throwable $e): Response
+ {
+ $this->isJson = $request->isJson();
+ if ($e instanceof HttpResponseException) {
+ return $e->getResponse();
+ } elseif ($e instanceof HttpException) {
+ return $this->renderHttpException($e);
+ } else {
+ return $this->convertExceptionToResponse($e);
+ }
+ }
+
+ /**
+ * @access public
+ * @param Output $output
+ * @param Throwable $e
+ */
+ public function renderForConsole(Output $output, Throwable $e): void
+ {
+ if ($this->app->isDebug()) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ }
+
+ $output->renderException($e);
+ }
+
+ /**
+ * @access protected
+ * @param HttpException $e
+ * @return Response
+ */
+ protected function renderHttpException(HttpException $e): Response
+ {
+ $status = $e->getStatusCode();
+ $template = $this->app->config->get('app.http_exception_template');
+
+ if (!$this->app->isDebug() && !empty($template[$status])) {
+ return Response::create($template[$status], 'view', $status)->assign(['e' => $e]);
+ } else {
+ return $this->convertExceptionToResponse($e);
+ }
+ }
+
+ /**
+ * 收集异常数据
+ * @param Throwable $exception
+ * @return array
+ */
+ protected function convertExceptionToArray(Throwable $exception): array
+ {
+ if ($this->app->isDebug()) {
+ // 调试模式,获取详细的错误信息
+ $traces = [];
+ $nextException = $exception;
+ do {
+ $traces[] = [
+ 'name' => get_class($nextException),
+ 'file' => $nextException->getFile(),
+ 'line' => $nextException->getLine(),
+ 'code' => $this->getCode($nextException),
+ 'message' => $this->getMessage($nextException),
+ 'trace' => $nextException->getTrace(),
+ 'source' => $this->getSourceCode($nextException),
+ ];
+ } while ($nextException = $nextException->getPrevious());
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ 'traces' => $traces,
+ 'datas' => $this->getExtendData($exception),
+ 'tables' => [
+ 'GET Data' => $this->app->request->get(),
+ 'POST Data' => $this->app->request->post(),
+ 'Files' => $this->app->request->file(),
+ 'Cookies' => $this->app->request->cookie(),
+ 'Session' => $this->app->exists('session') ? $this->app->session->all() : [],
+ 'Server/Request Data' => $this->app->request->server(),
+ ],
+ ];
+ } else {
+ // 部署模式仅显示 Code 和 Message
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ ];
+
+ if (!$this->app->config->get('app.show_error_msg')) {
+ // 不显示详细错误信息
+ $data['message'] = $this->app->config->get('app.error_message');
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @access protected
+ * @param Throwable $exception
+ * @return Response
+ */
+ protected function convertExceptionToResponse(Throwable $exception): Response
+ {
+ if (!$this->isJson) {
+ $response = Response::create($this->renderExceptionContent($exception));
+ } else {
+ $response = Response::create($this->convertExceptionToArray($exception), 'json');
+ }
+
+ if ($exception instanceof HttpException) {
+ $statusCode = $exception->getStatusCode();
+ $response->header($exception->getHeaders());
+ }
+
+ return $response->code($statusCode ?? 500);
+ }
+
+ protected function renderExceptionContent(Throwable $exception): string
+ {
+ ob_start();
+ $data = $this->convertExceptionToArray($exception);
+ extract($data);
+ include $this->app->config->get('app.exception_tmpl') ?: __DIR__ . '/../../tpl/think_exception.tpl';
+
+ return ob_get_clean();
+ }
+
+ /**
+ * 获取错误编码
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param Throwable $exception
+ * @return integer 错误编码
+ */
+ protected function getCode(Throwable $exception)
+ {
+ $code = $exception->getCode();
+
+ if (!$code && $exception instanceof ErrorException) {
+ $code = $exception->getSeverity();
+ }
+
+ return $code;
+ }
+
+ /**
+ * 获取错误信息
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param Throwable $exception
+ * @return string 错误信息
+ */
+ protected function getMessage(Throwable $exception): string
+ {
+ $message = $exception->getMessage();
+
+ if ($this->app->runningInConsole()) {
+ return $message;
+ }
+
+ $lang = $this->app->lang;
+
+ if (strpos($message, ':')) {
+ $name = strstr($message, ':', true);
+ $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message;
+ } elseif (strpos($message, ',')) {
+ $name = strstr($message, ',', true);
+ $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message;
+ } elseif ($lang->has($message)) {
+ $message = $lang->get($message);
+ }
+
+ return $message;
+ }
+
+ /**
+ * 获取出错文件内容
+ * 获取错误的前9行和后9行
+ * @access protected
+ * @param Throwable $exception
+ * @return array 错误文件内容
+ */
+ protected function getSourceCode(Throwable $exception): array
+ {
+ // 读取前9行和后9行
+ $line = $exception->getLine();
+ $first = ($line - 9 > 0) ? $line - 9 : 1;
+
+ try {
+ $contents = file($exception->getFile()) ?: [];
+ $source = [
+ 'first' => $first,
+ 'source' => array_slice($contents, $first - 1, 19),
+ ];
+ } catch (Exception $e) {
+ $source = [];
+ }
+
+ return $source;
+ }
+
+ /**
+ * 获取异常扩展信息
+ * 用于非调试模式html返回类型显示
+ * @access protected
+ * @param Throwable $exception
+ * @return array 异常类定义的扩展数据
+ */
+ protected function getExtendData(Throwable $exception): array
+ {
+ $data = [];
+
+ if ($exception instanceof \think\Exception) {
+ $data = $exception->getData();
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取常量列表
+ * @access protected
+ * @return array 常量列表
+ */
+ protected function getConst(): array
+ {
+ $const = get_defined_constants(true);
+
+ return $const['user'] ?? [];
+ }
+}
diff --git a/vendor/topthink/framework/src/think/exception/HttpException.php b/vendor/topthink/framework/src/think/exception/HttpException.php
new file mode 100644
index 0000000..45302e5
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/HttpException.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use Exception;
+
+/**
+ * HTTP异常
+ */
+class HttpException extends \RuntimeException
+{
+ private $statusCode;
+ private $headers;
+
+ public function __construct(int $statusCode, string $message = '', Exception $previous = null, array $headers = [], $code = 0)
+ {
+ $this->statusCode = $statusCode;
+ $this->headers = $headers;
+
+ parent::__construct($message, $code, $previous);
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/exception/HttpResponseException.php b/vendor/topthink/framework/src/think/exception/HttpResponseException.php
new file mode 100644
index 0000000..607813d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/HttpResponseException.php
@@ -0,0 +1,37 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use think\Response;
+
+/**
+ * HTTP响应异常
+ */
+class HttpResponseException extends \RuntimeException
+{
+ /**
+ * @var Response
+ */
+ protected $response;
+
+ public function __construct(Response $response)
+ {
+ $this->response = $response;
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php b/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php
new file mode 100644
index 0000000..8ccd6f6
--- /dev/null
+++ b/vendor/topthink/framework/src/think/exception/InvalidArgumentException.php
@@ -0,0 +1,22 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\exception;
+
+use Psr\Cache\InvalidArgumentException as Psr6CacheInvalidArgumentInterface;
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface;
+
+/**
+ * 非法数据异常
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInvalidArgumentInterface, SimpleCacheInvalidArgumentInterface
+{
+}
diff --git a/vendor/topthink/framework/src/think/exception/RouteNotFoundException.php b/vendor/topthink/framework/src/think/exception/RouteNotFoundException.php
new file mode 100644
index 0000000..7a2ee87
--- /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..89b4e4d
--- /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..e9f8105
--- /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..aac105d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Cache.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\cache\Driver;
+use think\cache\TagSet;
+use think\Facade;
+
+/**
+ * @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..4ce73dd
--- /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..30dd935
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Console.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace 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;
+use think\Facade;
+
+/**
+ * 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..960f4a3
--- /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..bed2538
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Env.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Env
+ * @package think\facade
+ * @mixin \think\Env
+ * @method static void load(string $file) 读取环境变量定义文件
+ * @method static mixed get(string $name = null, mixed $default = null) 获取环境变量值
+ * @method static void set(string|array $env, mixed $value = null) 设置环境变量值
+ * @method static bool has(string $name) 检测是否存在环境变量
+ * @method static void __set(string $name, mixed $value) 设置环境变量
+ * @method static mixed __get(string $name) 获取环境变量
+ * @method static bool __isset(string $name) 检测是否存在环境变量
+ * @method static void offsetSet($name, $value)
+ * @method static bool offsetExists($name)
+ * @method static mixed offsetUnset($name)
+ * @method static mixed offsetGet($name)
+ */
+class Env extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'env';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Event.php b/vendor/topthink/framework/src/think/facade/Event.php
new file mode 100644
index 0000000..c09d816
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Event.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Event
+ * @package think\facade
+ * @mixin \think\Event
+ * @method static \think\Event listenEvents(array $events) 批量注册事件监听
+ * @method static \think\Event listen(string $event, mixed $listener, bool $first = false) 注册事件监听
+ * @method static bool hasListener(string $event) 是否存在事件监听
+ * @method static void remove(string $event) 移除事件监听
+ * @method static \think\Event bind(array $events) 指定事件别名标识 便于调用
+ * @method static \think\Event subscribe(mixed $subscriber) 注册事件订阅者
+ * @method static \think\Event observe(string|object $observer, null|string $prefix = '') 自动注册事件观察者
+ * @method static mixed trigger(string|object $event, mixed $params = null, bool $once = false) 触发事件
+ * @method static mixed until($event, $params = null) 触发事件(只获取一个有效返回值)
+ */
+class Event extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'event';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Filesystem.php b/vendor/topthink/framework/src/think/facade/Filesystem.php
new file mode 100644
index 0000000..53706a8
--- /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..b460fe2
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Lang.php
@@ -0,0 +1,41 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Lang
+ * @package think\facade
+ * @mixin \think\Lang
+ * @method static void setLangSet(string $lang) 设置当前语言
+ * @method static string getLangSet() 获取当前语言
+ * @method static string defaultLangSet() 获取默认语言
+ * @method static array load(string|array $file, string $range = '') 加载语言定义(不区分大小写)
+ * @method static bool has(string|null $name, string $range = '') 判断是否存在语言定义(不区分大小写)
+ * @method static mixed get(string|null $name = null, array $vars = [], string $range = '') 获取语言定义(不区分大小写)
+ * @method static string detect(\think\Request $request) 自动侦测设置获取语言选择
+ * @method static void saveToCookie(\think\Cookie $cookie) 保存当前语言到Cookie
+ */
+class Lang extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'lang';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Log.php b/vendor/topthink/framework/src/think/facade/Log.php
new file mode 100644
index 0000000..7c43d37
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Log.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\log\Channel;
+use think\log\ChannelSet;
+
+/**
+ * @see \think\Log
+ * @package think\facade
+ * @mixin \think\Log
+ * @method static string|null getDefaultDriver() 默认驱动
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取日志配置
+ * @method static array getChannelConfig(string $channel, null $name = null, null $default = null) 获取渠道配置
+ * @method static Channel|ChannelSet channel(string|array $name = null) driver() 的别名
+ * @method static mixed createDriver(string $name)
+ * @method static \think\Log clear(string|array $channel = '*') 清空日志信息
+ * @method static \think\Log close(string|array $channel = '*') 关闭本次请求日志写入
+ * @method static array getLog(string $channel = null) 获取日志信息
+ * @method static bool save() 保存日志信息
+ * @method static \think\Log record(mixed $msg, string $type = 'info', array $context = [], bool $lazy = true) 记录日志信息
+ * @method static \think\Log write(mixed $msg, string $type = 'info', array $context = []) 实时写入日志信息
+ * @method static Event listen($listener) 注册日志写入事件监听
+ * @method static void log(string $level, mixed $message, array $context = []) 记录日志信息
+ * @method static void emergency(mixed $message, array $context = []) 记录emergency信息
+ * @method static void alert(mixed $message, array $context = []) 记录警报信息
+ * @method static void critical(mixed $message, array $context = []) 记录紧急情况
+ * @method static void error(mixed $message, array $context = []) 记录错误信息
+ * @method static void warning(mixed $message, array $context = []) 记录warning信息
+ * @method static void notice(mixed $message, array $context = []) 记录notice信息
+ * @method static void info(mixed $message, array $context = []) 记录一般信息
+ * @method static void debug(mixed $message, array $context = []) 记录调试信息
+ * @method static void sql(mixed $message, array $context = []) 记录sql信息
+ * @method static mixed __call($method, $parameters)
+ */
+class Log extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'log';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Middleware.php b/vendor/topthink/framework/src/think/facade/Middleware.php
new file mode 100644
index 0000000..4203f82
--- /dev/null
+++ b/vendor/topthink/framework/src/think/facade/Middleware.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Middleware
+ * @package think\facade
+ * @mixin \think\Middleware
+ * @method static void import(array $middlewares = [], string $type = 'global') 导入中间件
+ * @method static void add(mixed $middleware, string $type = 'global') 注册中间件
+ * @method static void route(mixed $middleware) 注册路由中间件
+ * @method static void controller(mixed $middleware) 注册控制器中间件
+ * @method static mixed unshift(mixed $middleware, string $type = 'global') 注册中间件到开始位置
+ * @method static array all(string $type = 'global') 获取注册的中间件
+ * @method static Pipeline pipeline(string $type = 'global') 调度管道
+ * @method static mixed end(\think\Response $response) 结束调度
+ * @method static \think\Response handleException(\think\Request $passable, \Throwable $e) 异常处理
+ */
+class Middleware extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'middleware';
+ }
+}
diff --git a/vendor/topthink/framework/src/think/facade/Request.php b/vendor/topthink/framework/src/think/facade/Request.php
new file mode 100644
index 0000000..6531f46
--- /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..5a5b955
--- /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..68bf993
--- /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..6db6d34
--- /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..acde3b5
--- /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..7dff766
--- /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..0a62399
--- /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..6712959
--- /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..c10ccc3
--- /dev/null
+++ b/vendor/topthink/framework/src/think/filesystem/driver/Local.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+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..bab6d39
--- /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..201d947
--- /dev/null
+++ b/vendor/topthink/framework/src/think/initializer/Error.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\initializer;
+
+use think\App;
+use think\console\Output as ConsoleOutput;
+use think\exception\ErrorException;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * 错误和异常处理
+ */
+class Error
+{
+ /** @var App */
+ protected $app;
+
+ /**
+ * 注册异常处理
+ * @access public
+ * @param App $app
+ * @return void
+ */
+ public function init(App $app)
+ {
+ $this->app = $app;
+ error_reporting(E_ALL);
+ set_error_handler([$this, 'appError']);
+ set_exception_handler([$this, 'appException']);
+ register_shutdown_function([$this, 'appShutdown']);
+ }
+
+ /**
+ * Exception Handler
+ * @access public
+ * @param \Throwable $e
+ */
+ public function appException(Throwable $e): void
+ {
+ $handler = $this->getExceptionHandler();
+
+ $handler->report($e);
+
+ if ($this->app->runningInConsole()) {
+ $handler->renderForConsole(new ConsoleOutput, $e);
+ } else {
+ $handler->render($this->app->request, $e)->send();
+ }
+ }
+
+ /**
+ * Error Handler
+ * @access public
+ * @param integer $errno 错误编号
+ * @param string $errstr 详细错误信息
+ * @param string $errfile 出错的文件
+ * @param integer $errline 出错行号
+ * @throws ErrorException
+ */
+ public function appError(int $errno, string $errstr, string $errfile = '', int $errline = 0): void
+ {
+ $exception = new ErrorException($errno, $errstr, $errfile, $errline);
+
+ if (error_reporting() & $errno) {
+ // 将错误信息托管至 think\exception\ErrorException
+ throw $exception;
+ }
+ }
+
+ /**
+ * Shutdown Handler
+ * @access public
+ */
+ public function appShutdown(): void
+ {
+ if (!is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
+ // 将错误信息托管至think\ErrorException
+ $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']);
+
+ $this->appException($exception);
+ }
+ }
+
+ /**
+ * 确定错误类型是否致命
+ *
+ * @access protected
+ * @param int $type
+ * @return bool
+ */
+ protected function isFatal(int $type): bool
+ {
+ return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
+ }
+
+ /**
+ * Get an instance of the exception handler.
+ *
+ * @access protected
+ * @return Handle
+ */
+ protected function getExceptionHandler()
+ {
+ return $this->app->make(Handle::class);
+ }
+}
diff --git a/vendor/topthink/framework/src/think/initializer/RegisterService.php b/vendor/topthink/framework/src/think/initializer/RegisterService.php
new file mode 100644
index 0000000..b682a0b
--- /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..1de96f1
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/Channel.php
@@ -0,0 +1,286 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\log;
+
+use Psr\Log\LoggerInterface;
+use think\contract\LogHandlerInterface;
+use think\Event;
+use think\event\LogRecord;
+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->event) {
+ $this->event->trigger(new LogRecord($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..6dcb0bd
--- /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..e5682fc
--- /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..2cfb943
--- /dev/null
+++ b/vendor/topthink/framework/src/think/log/driver/Socket.php
@@ -0,0 +1,311 @@
+
+// +----------------------------------------------------------------------
+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,
+ // curl opt
+ 'curl_opt' => [
+ CURLOPT_CONNECTTIMEOUT => 1,
+ CURLOPT_TIMEOUT => 10,
+ ],
+ ];
+
+ 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, $this->config['curl_opt'][CURLOPT_CONNECTTIMEOUT] ?? 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, $this->config['curl_opt'][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..b7ab842
--- /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..b114351
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/CheckRequestCache.php
@@ -0,0 +1,183 @@
+
+// +----------------------------------------------------------------------
+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) {
+ if (false === $this->config['request_cache_key']) {
+ // 关闭当前缓存
+ $cache = false;
+ }
+
+ $cache = $cache ?? $this->getRequestCache($request);
+
+ if ($cache) {
+ if (is_array($cache)) {
+ [$key, $expire, $tag] = array_pad($cache, 3, null);
+ } else {
+ $key = md5($request->url(true));
+ $expire = $cache;
+ $tag = null;
+ }
+
+ $key = $this->parseCacheKey($request, $key);
+
+ 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'];
+
+ foreach ($except as $rule) {
+ if (0 === stripos($request->url(), $rule)) {
+ return;
+ }
+ }
+
+ return [$key, $expire, $tag];
+ }
+
+ /**
+ * 读取当前地址的请求缓存信息
+ * @access protected
+ * @param Request $request
+ * @param mixed $key
+ * @return null|string
+ */
+ protected function parseCacheKey($request, $key)
+ {
+ if ($key instanceof \Closure) {
+ $key = call_user_func($key, $request);
+ }
+
+ if (false === $key) {
+ // 关闭当前缓存
+ 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, (string) $val, $key);
+ }
+ }
+ } elseif (strpos($key, ']')) {
+ if ('[' . $request->ext() . ']' == $key) {
+ // 缓存某个后缀的请求
+ $key = md5($request->url());
+ } else {
+ return;
+ }
+ }
+
+ if (isset($fun)) {
+ $key = $fun($key);
+ }
+
+ return $key;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php b/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php
new file mode 100644
index 0000000..efbb77b
--- /dev/null
+++ b/vendor/topthink/framework/src/think/middleware/FormTokenCheck.php
@@ -0,0 +1,45 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\exception\ValidateException;
+use think\Request;
+use think\Response;
+
+/**
+ * 表单令牌支持
+ */
+class FormTokenCheck
+{
+
+ /**
+ * 表单令牌检测
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param string $token 表单令牌Token名称
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next, string $token = null)
+ {
+ $check = $request->checkToken($token ?: '__token__');
+
+ if (false === $check) {
+ throw new ValidateException('invalid token');
+ }
+
+ return $next($request);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/middleware/LoadLangPack.php b/vendor/topthink/framework/src/think/middleware/LoadLangPack.php
new file mode 100644
index 0000000..478e29c
--- /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..3cb2fad
--- /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..1e45f2f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/File.php
@@ -0,0 +1,160 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Exception;
+use think\Response;
+
+/**
+ * File Response
+ */
+class File extends Response
+{
+ protected $expire = 360;
+ protected $name;
+ protected $mimeType;
+ protected $isContent = false;
+ protected $force = true;
+
+ public function __construct($data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function output($data)
+ {
+ if (!$this->isContent && !is_file($data)) {
+ throw new Exception('file not exists:' . $data);
+ }
+
+ while (ob_get_level() > 0) {
+ ob_end_clean();
+ }
+
+ if (!empty($this->name)) {
+ $name = $this->name;
+ } else {
+ $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : '';
+ }
+
+ if ($this->isContent) {
+ $mimeType = $this->mimeType;
+ $size = strlen($data);
+ } else {
+ $mimeType = $this->getMimeType($data);
+ $size = filesize($data);
+ }
+
+ $this->header['Pragma'] = 'public';
+ $this->header['Content-Type'] = $mimeType ?: 'application/octet-stream';
+ $this->header['Cache-control'] = 'max-age=' . $this->expire;
+ $this->header['Content-Disposition'] = ($this->force ? 'attachment; ' : '') . 'filename="' . $name . '"';
+ $this->header['Content-Length'] = $size;
+ $this->header['Content-Transfer-Encoding'] = 'binary';
+ $this->header['Expires'] = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT';
+
+ $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT');
+
+ return $this->isContent ? $data : file_get_contents($data);
+ }
+
+ /**
+ * 设置是否为内容 必须配合mimeType方法使用
+ * @access public
+ * @param bool $content
+ * @return $this
+ */
+ public function isContent(bool $content = true)
+ {
+ $this->isContent = $content;
+ return $this;
+ }
+
+ /**
+ * 设置有效期
+ * @access public
+ * @param integer $expire 有效期
+ * @return $this
+ */
+ public function expire(int $expire)
+ {
+ $this->expire = $expire;
+ return $this;
+ }
+
+ /**
+ * 设置文件类型
+ * @access public
+ * @param string $filename 文件名
+ * @return $this
+ */
+ public function mimeType(string $mimeType)
+ {
+ $this->mimeType = $mimeType;
+ return $this;
+ }
+
+ /**
+ * 设置文件强制下载
+ * @access public
+ * @param bool $force 强制浏览器下载
+ * @return $this
+ */
+ public function force(bool $force)
+ {
+ $this->force = $force;
+ return $this;
+ }
+
+ /**
+ * 获取文件类型信息
+ * @access public
+ * @param string $filename 文件名
+ * @return string
+ */
+ protected function getMimeType(string $filename): string
+ {
+ if (!empty($this->mimeType)) {
+ return $this->mimeType;
+ }
+
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+ return finfo_file($finfo, $filename);
+ }
+
+ /**
+ * 设置下载文件的显示名称
+ * @access public
+ * @param string $filename 文件名
+ * @param bool $extension 后缀自动识别
+ * @return $this
+ */
+ public function name(string $filename, bool $extension = true)
+ {
+ $this->name = $filename;
+
+ if ($extension && false === strpos($filename, '.')) {
+ $this->name .= '.' . pathinfo($this->data, PATHINFO_EXTENSION);
+ }
+
+ return $this;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/response/Html.php b/vendor/topthink/framework/src/think/response/Html.php
new file mode 100644
index 0000000..c158f78
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/Html.php
@@ -0,0 +1,34 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+
+/**
+ * Html Response
+ */
+class Html extends Response
+{
+ /**
+ * 输出type
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+}
diff --git a/vendor/topthink/framework/src/think/response/Json.php b/vendor/topthink/framework/src/think/response/Json.php
new file mode 100644
index 0000000..a84501f
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/Json.php
@@ -0,0 +1,62 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+
+/**
+ * Json Response
+ */
+class Json extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/json';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ * @throws \Exception
+ */
+ protected function output($data): string
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/response/Jsonp.php b/vendor/topthink/framework/src/think/response/Jsonp.php
new file mode 100644
index 0000000..81d3a06
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/Jsonp.php
@@ -0,0 +1,74 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Request;
+use think\Response;
+
+/**
+ * Jsonp Response
+ */
+class Jsonp extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'var_jsonp_handler' => 'callback',
+ 'default_jsonp_handler' => 'jsonpReturn',
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/javascript';
+
+ protected $request;
+
+ public function __construct(Cookie $cookie, Request $request, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+
+ $this->cookie = $cookie;
+ $this->request = $request;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ * @throws \Exception
+ */
+ protected function output($data): string
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取]
+ $varJsonpHandler = $this->request->param($this->options['var_jsonp_handler'], "");
+ $handler = !empty($varJsonpHandler) ? $varJsonpHandler : $this->options['default_jsonp_handler'];
+
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ $data = $handler . '(' . $data . ');';
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/response/Redirect.php b/vendor/topthink/framework/src/think/response/Redirect.php
new file mode 100644
index 0000000..1f38764
--- /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..2c116c7
--- /dev/null
+++ b/vendor/topthink/framework/src/think/response/View.php
@@ -0,0 +1,151 @@
+
+// +----------------------------------------------------------------------
+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
+ {
+ // 渲染模板输出
+ $this->view->filter($this->filter);
+ return $this->isContent ?
+ $this->view->display($data, $this->vars) :
+ $this->view->fetch($data, $this->vars);
+ }
+
+ /**
+ * 获取视图变量
+ * @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..bddbb48
--- /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..e77e299
--- /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..84f1d46
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Domain.php
@@ -0,0 +1,183 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\helper\Str;
+use think\Request;
+use think\Route;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+
+/**
+ * 域名路由
+ */
+class Domain extends RuleGroup
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param string $name 路由域名
+ * @param mixed $rule 域名路由
+ */
+ public function __construct(Route $router, string $name = null, $rule = null)
+ {
+ $this->router = $router;
+ $this->domain = $name;
+ $this->rule = $rule;
+ }
+
+ /**
+ * 检测域名路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check(Request $request, string $url, bool $completeMatch = false)
+ {
+ // 检测URL绑定
+ $result = $this->checkUrlBind($request, $url);
+
+ if (!empty($this->option['append'])) {
+ $request->setRoute($this->option['append']);
+ unset($this->option['append']);
+ }
+
+ if (false !== $result) {
+ return $result;
+ }
+
+ return parent::check($request, $url, $completeMatch);
+ }
+
+ /**
+ * 设置路由绑定
+ * @access public
+ * @param string $bind 绑定信息
+ * @return $this
+ */
+ public function bind(string $bind)
+ {
+ $this->router->bind($bind, $this->domain);
+
+ return $this;
+ }
+
+ /**
+ * 检测URL绑定
+ * @access private
+ * @param Request $request
+ * @param string $url URL地址
+ * @return Dispatch|false
+ */
+ private function checkUrlBind(Request $request, string $url)
+ {
+ $bind = $this->router->getDomainBind($this->domain);
+
+ if ($bind) {
+ $this->parseBindAppendParam($bind);
+
+ // 如果有URL绑定 则进行绑定检测
+ $type = substr($bind, 0, 1);
+ $bind = substr($bind, 1);
+
+ $bindTo = [
+ '\\' => 'bindToClass',
+ '@' => 'bindToController',
+ ':' => 'bindToNamespace',
+ ];
+
+ if (isset($bindTo[$type])) {
+ return $this->{$bindTo[$type]}($request, $url, $bind);
+ }
+ }
+
+ return false;
+ }
+
+ protected function parseBindAppendParam(string &$bind): void
+ {
+ if (false !== strpos($bind, '?')) {
+ [$bind, $query] = explode('?', $bind);
+ parse_str($query, $vars);
+ $this->append($vars);
+ }
+ }
+
+ /**
+ * 绑定到类
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $class 类名(带命名空间)
+ * @return CallbackDispatch
+ */
+ protected function bindToClass(Request $request, string $url, string $class): CallbackDispatch
+ {
+ $array = explode('|', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action');
+ $param = [];
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($array[1], $param);
+ }
+
+ return new CallbackDispatch($request, $this, [$class, $action], $param);
+ }
+
+ /**
+ * 绑定到命名空间
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $namespace 命名空间
+ * @return CallbackDispatch
+ */
+ protected function bindToNamespace(Request $request, string $url, string $namespace): CallbackDispatch
+ {
+ $array = explode('|', $url, 3);
+ $class = !empty($array[0]) ? $array[0] : $this->router->config('default_controller');
+ $method = !empty($array[1]) ? $array[1] : $this->router->config('default_action');
+ $param = [];
+
+ if (!empty($array[2])) {
+ $this->parseUrlParams($array[2], $param);
+ }
+
+ return new CallbackDispatch($request, $this, [$namespace . '\\' . Str::studly($class), $method], $param);
+ }
+
+ /**
+ * 绑定到控制器
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $controller 控制器名
+ * @return ControllerDispatch
+ */
+ protected function bindToController(Request $request, string $url, string $controller): ControllerDispatch
+ {
+ $array = explode('|', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action');
+ $param = [];
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($array[1], $param);
+ }
+
+ return new ControllerDispatch($request, $this, $controller . '/' . $action, $param);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/route/Resource.php b/vendor/topthink/framework/src/think/route/Resource.php
new file mode 100644
index 0000000..bb37cb6
--- /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', 'pattern'] 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..31b2e0e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Rule.php
@@ -0,0 +1,905 @@
+
+// +----------------------------------------------------------------------
+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 = '')
+ {
+ $pattern = $this->pattern;
+
+ if ($this->parent) {
+ $pattern = array_merge($this->parent->getPattern(), $pattern);
+ }
+
+ if ('' === $name) {
+ return $pattern;
+ }
+
+ return $pattern[$name] ?? null;
+ }
+
+ /**
+ * 获取路由参数定义
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getOption(string $name = '', $default = null)
+ {
+ $option = $this->option;
+
+ if ($this->parent) {
+ $parentOption = $this->parent->getOption();
+
+ // 合并分组参数
+ foreach ($this->mergeOptions as $item) {
+ if (isset($parentOption[$item]) && isset($option[$item])) {
+ $option[$item] = array_merge($parentOption[$item], $option[$item]);
+ }
+ }
+
+ $option = array_merge($parentOption, $option);
+ }
+
+ if ('' === $name) {
+ return $option;
+ }
+
+ return $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
+ * @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;
+ }
+
+ // 替换路由地址中的变量
+ $extraParams = true;
+ $search = $replace = [];
+ $depr = $this->router->config('pathinfo_depr');
+ foreach ($matches as $key => $value) {
+ $search[] = '<' . $key . '>';
+ $replace[] = $value;
+
+ $search[] = ':' . $key;
+ $replace[] = $value;
+
+ if (strpos($value, $depr)) {
+ $extraParams = false;
+ }
+ }
+
+ if (is_string($route)) {
+ $route = str_replace($search, $replace, $route);
+ }
+
+ // 解析额外参数
+ if ($extraParams) {
+ $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, '::') || 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) {
+ $value = $this->buildNameRegex($name, $pattern, $suffix);
+ if ($value) {
+ $origin[] = $name;
+ $replace[] = $value;
+ }
+ }
+
+ // 是否区分 / 地址访问
+ if ('/' != $rule) {
+ if (!empty($option['remove_slash'])) {
+ $rule = rtrim($rule, '/');
+ } elseif (substr($rule, -1) == '/') {
+ $rule = rtrim($rule, '/');
+ $hasSlash = true;
+ }
+ }
+
+ $regex = isset($replace) ? str_replace($origin, $replace, $rule) : $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 '';
+ }
+
+ 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..cd9ddbd
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/RuleGroup.php
@@ -0,0 +1,523 @@
+
+// +----------------------------------------------------------------------
+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);
+ $option = $this->getOption();
+
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ if (!empty($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 (!empty($option['dispatcher'])) {
+ $result = $this->parseRule($request, '', $option['dispatcher'], $url, $option);
+ } elseif ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) {
+ // 未匹配所有路由的路由规则处理
+ $result = $this->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->getOption());
+ } 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
+ {
+ if (is_string($rule) && is_subclass_of($rule, Dispatch::class)) {
+ $this->dispatcher($rule);
+ return;
+ }
+
+ $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);
+ }
+
+ /**
+ * 设置分组的Dispatch调度
+ * @access public
+ * @param string $dispatch 调度类
+ * @return $this
+ */
+ public function dispatcher(string $dispatch)
+ {
+ return $this->setOption('dispatcher', $dispatch);
+ }
+
+ /**
+ * 获取完整分组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..1f9aa52
--- /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->getOption();
+ $pattern = $this->getPattern();
+ $url = $this->urlSuffixCheck($request, $url, $option);
+
+ if (is_null($match)) {
+ $match = $this->match($url, $option, $pattern, $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 array $pattern 变量规则
+ * @param bool $completeMatch 是否完全匹配
+ * @return array|false
+ */
+ private function match(string $url, array $option, array $pattern, bool $completeMatch)
+ {
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ $depr = $this->router->config('pathinfo_depr');
+
+ // 检查完整规则定义
+ 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..0684367
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/RuleName.php
@@ -0,0 +1,211 @@
+
+// +----------------------------------------------------------------------
+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);
+ $item = $this->getRuleItemInfo($ruleItem);
+ if ($first && isset($this->item[$name])) {
+ array_unshift($this->item[$name], $item);
+ } else {
+ $this->item[$name][] = $item;
+ }
+ }
+
+ /**
+ * 注册路由分组标识
+ * @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['domain'];
+ $itemMethod = $item['method'];
+
+ if (($itemDomain == $domain || '-' == $itemDomain) && ('*' == $itemMethod || '*' == $method || $method == $itemMethod)) {
+ $result[] = $item;
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取路由信息
+ * @access protected
+ * @param RuleItem $item 路由规则
+ * @return array
+ */
+ protected function getRuleItemInfo(RuleItem $item): array
+ {
+ return [
+ 'rule' => $item->getRule(),
+ 'domain' => $item->getDomain(),
+ 'method' => $item->getMethod(),
+ 'suffix' => $item->getSuffix(),
+ ];
+ }
+}
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..8dd410c
--- /dev/null
+++ b/vendor/topthink/framework/src/think/route/Url.php
@@ -0,0 +1,517 @@
+
+// +----------------------------------------------------------------------
+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['rule'];
+ $pattern = $this->parseVar($url);
+ $domain = $item['domain'];
+ $suffix = $item['suffix'];
+
+ 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 ? (string) $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 [];
+ }
+
+ /**
+ * 生成URL地址
+ * @access public
+ * @return string
+ */
+ 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 ? '?' . $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..2044ef8
--- /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..611101b
--- /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..147f5cb
--- /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..b517c4e
--- /dev/null
+++ b/vendor/topthink/framework/src/think/service/ModelService.php
@@ -0,0 +1,53 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\service;
+
+use think\Model;
+use think\Service;
+
+/**
+ * 模型服务类
+ */
+class ModelService extends Service
+{
+ public function boot()
+ {
+ Model::setDb($this->app->db);
+ Model::setEvent($this->app->event);
+ Model::setInvoker([$this->app, 'invoke']);
+ Model::maker(function (Model $model) {
+ $config = $this->app->config;
+
+ $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
+
+ if (is_null($isAutoWriteTimestamp)) {
+ // 自动写入时间戳
+ $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp'));
+ }
+
+ $dateFormat = $model->getDateFormat();
+
+ if (is_null($dateFormat)) {
+ // 设置时间戳格式
+ $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s'));
+ }
+
+ $timeField = $config->get('database.datetime_field');
+ if (!empty($timeField)) {
+ [$createTime, $updateTime] = explode(',', $timeField);
+ $model->setTimeField($createTime, $updateTime);
+ }
+
+ });
+ }
+}
diff --git a/vendor/topthink/framework/src/think/service/PaginatorService.php b/vendor/topthink/framework/src/think/service/PaginatorService.php
new file mode 100644
index 0000000..a01977d
--- /dev/null
+++ b/vendor/topthink/framework/src/think/service/PaginatorService.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\service;
+
+use think\Paginator;
+use think\paginator\driver\Bootstrap;
+use think\Service;
+
+/**
+ * 分页服务类
+ */
+class PaginatorService extends Service
+{
+ public function register()
+ {
+ if (!$this->app->bound(Paginator::class)) {
+ $this->app->bind(Paginator::class, Bootstrap::class);
+ }
+ }
+
+ public function boot()
+ {
+ Paginator::maker(function (...$args) {
+ return $this->app->make(Paginator::class, $args, true);
+ });
+
+ Paginator::currentPathResolver(function () {
+ return $this->app->request->baseUrl();
+ });
+
+ Paginator::currentPageResolver(function ($varPage = 'page') {
+
+ $page = $this->app->request->param($varPage);
+
+ if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
+ return (int) $page;
+ }
+
+ return 1;
+ });
+ }
+}
diff --git a/vendor/topthink/framework/src/think/service/ValidateService.php b/vendor/topthink/framework/src/think/service/ValidateService.php
new file mode 100644
index 0000000..94d7638
--- /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..49e1ba9
--- /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..4fabc79
--- /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..788f323
--- /dev/null
+++ b/vendor/topthink/framework/src/think/session/driver/File.php
@@ -0,0 +1,249 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\session\driver;
+
+use Closure;
+use Exception;
+use FilesystemIterator;
+use Generator;
+use SplFileInfo;
+use think\App;
+use think\contract\SessionHandlerInterface;
+
+/**
+ * Session 文件驱动
+ */
+class File implements SessionHandlerInterface
+{
+ protected $config = [
+ 'path' => '',
+ 'expire' => 1440,
+ 'prefix' => '',
+ 'data_compress' => false,
+ 'gc_probability' => 1,
+ 'gc_divisor' => 100,
+ ];
+
+ public function __construct(App $app, array $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+
+ if (empty($this->config['path'])) {
+ $this->config['path'] = $app->getRuntimePath() . 'session' . DIRECTORY_SEPARATOR;
+ } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) {
+ $this->config['path'] .= DIRECTORY_SEPARATOR;
+ }
+
+ $this->init();
+ }
+
+ /**
+ * 打开Session
+ * @access protected
+ * @throws Exception
+ */
+ protected function init(): void
+ {
+ try {
+ !is_dir($this->config['path']) && mkdir($this->config['path'], 0755, true);
+ } catch (\Exception $e) {
+ // 写入失败
+ }
+
+ // 垃圾回收
+ if (random_int(1, $this->config['gc_divisor']) <= $this->config['gc_probability']) {
+ $this->gc();
+ }
+ }
+
+ /**
+ * Session 垃圾回收
+ * @access public
+ * @return void
+ */
+ public function gc(): void
+ {
+ $lifetime = $this->config['expire'];
+ $now = time();
+
+ $files = $this->findFiles($this->config['path'], function (SplFileInfo $item) use ($lifetime, $now) {
+ return $now - $lifetime > $item->getMTime();
+ });
+
+ foreach ($files as $file) {
+ $this->unlink($file->getPathname());
+ }
+ }
+
+ /**
+ * 查找文件
+ * @param string $root
+ * @param Closure $filter
+ * @return Generator
+ */
+ protected function findFiles(string $root, Closure $filter)
+ {
+ $items = new FilesystemIterator($root);
+
+ /** @var SplFileInfo $item */
+ foreach ($items as $item) {
+ if ($item->isDir() && !$item->isLink()) {
+ yield from $this->findFiles($item->getPathname(), $filter);
+ } else {
+ if ($filter($item)) {
+ yield $item;
+ }
+ }
+ }
+ }
+
+ /**
+ * 取得变量的存储文件名
+ * @access protected
+ * @param string $name 缓存变量名
+ * @param bool $auto 是否自动创建目录
+ * @return string
+ */
+ protected function getFileName(string $name, bool $auto = false): string
+ {
+ if ($this->config['prefix']) {
+ // 使用子目录
+ $name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;
+ } else {
+ $name = 'sess_' . $name;
+ }
+
+ $filename = $this->config['path'] . $name;
+ $dir = dirname($filename);
+
+ if ($auto && !is_dir($dir)) {
+ try {
+ mkdir($dir, 0755, true);
+ } catch (\Exception $e) {
+ // 创建失败
+ }
+ }
+
+ return $filename;
+ }
+
+ /**
+ * 读取Session
+ * @access public
+ * @param string $sessID
+ * @return string
+ */
+ public function read(string $sessID): string
+ {
+ $filename = $this->getFileName($sessID);
+
+ if (is_file($filename) && filemtime($filename) >= time() - $this->config['expire']) {
+ $content = $this->readFile($filename);
+
+ if ($this->config['data_compress'] && function_exists('gzcompress')) {
+ //启用数据压缩
+ $content = (string) gzuncompress($content);
+ }
+
+ return $content;
+ }
+
+ return '';
+ }
+
+ /**
+ * 写文件(加锁)
+ * @param $path
+ * @param $content
+ * @return bool
+ */
+ protected function writeFile($path, $content): bool
+ {
+ return (bool) file_put_contents($path, $content, LOCK_EX);
+ }
+
+ /**
+ * 读取文件内容(加锁)
+ * @param $path
+ * @return string
+ */
+ protected function readFile($path): string
+ {
+ $contents = '';
+
+ $handle = fopen($path, 'rb');
+
+ if ($handle) {
+ try {
+ if (flock($handle, LOCK_SH)) {
+ clearstatcache(true, $path);
+
+ $contents = fread($handle, filesize($path) ?: 1);
+
+ flock($handle, LOCK_UN);
+ }
+ } finally {
+ fclose($handle);
+ }
+ }
+
+ return $contents;
+ }
+
+ /**
+ * 写入Session
+ * @access public
+ * @param string $sessID
+ * @param string $sessData
+ * @return bool
+ */
+ public function write(string $sessID, string $sessData): bool
+ {
+ $filename = $this->getFileName($sessID, true);
+ $data = $sessData;
+
+ if ($this->config['data_compress'] && function_exists('gzcompress')) {
+ //数据压缩
+ $data = gzcompress($data, 3);
+ }
+
+ return $this->writeFile($filename, $data);
+ }
+
+ /**
+ * 删除Session
+ * @access public
+ * @param string $sessID
+ * @return bool
+ */
+ public function delete(string $sessID): bool
+ {
+ try {
+ return $this->unlink($this->getFileName($sessID));
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 判断文件是否存在后,删除
+ * @access private
+ * @param string $file
+ * @return bool
+ */
+ private function unlink(string $file): bool
+ {
+ return is_file($file) && unlink($file);
+ }
+
+}
diff --git a/vendor/topthink/framework/src/think/validate/ValidateRule.php b/vendor/topthink/framework/src/think/validate/ValidateRule.php
new file mode 100644
index 0000000..b741f53
--- /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..9e6e54a
--- /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..7766caf
--- /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..6b86015
--- /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..5b5a13c
--- /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..271a34f
--- /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..e27deb0
--- /dev/null
+++ b/vendor/topthink/framework/tests/ContainerTest.php
@@ -0,0 +1,314 @@
+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->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..3bd0c1e
--- /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..cf2e65f
--- /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..ded5a36
--- /dev/null
+++ b/vendor/topthink/framework/tests/EventTest.php
@@ -0,0 +1,134 @@
+app = m::mock(App::class)->makePartial();
+
+ Container::setInstance($this->app);
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+
+ $this->event = new Event($this->app);
+ }
+
+ public function testBasic()
+ {
+ $this->event->bind(['foo' => 'baz']);
+
+ $this->event->listen('foo', function ($bar) {
+ $this->assertEquals('bar', $bar);
+ });
+
+ $this->assertTrue($this->event->hasListener('foo'));
+
+ $this->event->trigger('baz', 'bar');
+
+ $this->event->remove('foo');
+
+ $this->assertFalse($this->event->hasListener('foo'));
+ }
+
+ public function testOnceEvent()
+ {
+ $this->event->listen('AppInit', function ($bar) {
+ $this->assertEquals('bar', $bar);
+ return 'foo';
+ });
+
+ $this->assertEquals('foo', $this->event->trigger('AppInit', 'bar', true));
+ $this->assertEquals(['foo'], $this->event->trigger('AppInit', 'bar'));
+ }
+
+ public function testClassListener()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('handle')->andReturnTrue();
+
+ $this->event->listen('some', "SomeListener");
+
+ $this->assertTrue($this->event->until('some'));
+ }
+
+ public function testSubscribe()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('subscribe')->andReturnUsing(function (Event $event) use ($listener) {
+
+ $listener->shouldReceive('onBar')->once()->andReturnFalse();
+
+ $event->listenEvents(['SomeListener::onBar' => [[$listener, 'onBar']]]);
+ });
+
+ $this->event->subscribe('SomeListener');
+
+ $this->assertTrue($this->event->hasListener('SomeListener::onBar'));
+
+ $this->event->trigger('SomeListener::onBar');
+ }
+
+ public function testAutoObserve()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('onBar')->once();
+
+ $this->app->shouldReceive('make')->with('SomeListener')->andReturn($listener);
+
+ $this->event->observe('SomeListener');
+
+ $this->event->trigger('bar');
+ }
+
+}
+
+class TestListener
+{
+ public function handle()
+ {
+
+ }
+
+ public function onBar()
+ {
+
+ }
+
+ public function onFoo()
+ {
+
+ }
+
+ public function subscribe()
+ {
+
+ }
+}
diff --git a/vendor/topthink/framework/tests/FilesystemTest.php b/vendor/topthink/framework/tests/FilesystemTest.php
new file mode 100644
index 0000000..df5ffe2
--- /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..c3e0abd
--- /dev/null
+++ b/vendor/topthink/framework/tests/HttpTest.php
@@ -0,0 +1,155 @@
+app = m::mock(App::class)->makePartial();
+
+ $this->http = m::mock(Http::class, [$this->app])->shouldAllowMockingProtectedMethods()->makePartial();
+ }
+
+ protected function prepareApp($request, $response)
+ {
+ $this->app->shouldReceive('instance')->once()->with('request', $request);
+ $this->app->shouldReceive('initialized')->once()->andReturnFalse();
+ $this->app->shouldReceive('initialize')->once();
+ $this->app->shouldReceive('get')->with('request')->andReturn($request);
+
+ $route = m::mock(Route::class);
+
+ $route->shouldReceive('dispatch')->withArgs(function ($req, $withRoute) use ($request) {
+ if ($withRoute) {
+ $withRoute();
+ }
+ return $req === $request;
+ })->andReturn($response);
+
+ $route->shouldReceive('config')->with('route_annotation')->andReturn(true);
+
+ $this->app->shouldReceive('get')->with('route')->andReturn($route);
+
+ $console = m::mock(Console::class);
+
+ $console->shouldReceive('call');
+
+ $this->app->shouldReceive('get')->with('console')->andReturn($console);
+ }
+
+ public function testRun()
+ {
+ $root = vfsStream::setup('rootDir', null, [
+ 'app' => [
+ 'controller' => [],
+ 'middleware.php' => ' [
+ 'route.php' => 'app->shouldReceive('getBasePath')->andReturn($root->getChild('app')->url() . DIRECTORY_SEPARATOR);
+ $this->app->shouldReceive('getRootPath')->andReturn($root->url() . DIRECTORY_SEPARATOR);
+
+ $request = m::mock(Request::class)->makePartial();
+ $response = m::mock(Response::class)->makePartial();
+
+ $this->prepareApp($request, $response);
+
+ $this->assertEquals($response, $this->http->run($request));
+ }
+
+ public function multiAppRunProvider()
+ {
+ $request1 = m::mock(Request::class)->makePartial();
+ $request1->shouldReceive('subDomain')->andReturn('www');
+ $request1->shouldReceive('host')->andReturn('www.domain.com');
+
+ $request2 = m::mock(Request::class)->makePartial();
+ $request2->shouldReceive('subDomain')->andReturn('app2');
+ $request2->shouldReceive('host')->andReturn('app2.domain.com');
+
+ $request3 = m::mock(Request::class)->makePartial();
+ $request3->shouldReceive('pathinfo')->andReturn('some1/a/b/c');
+
+ $request4 = m::mock(Request::class)->makePartial();
+ $request4->shouldReceive('pathinfo')->andReturn('app3/a/b/c');
+
+ $request5 = m::mock(Request::class)->makePartial();
+ $request5->shouldReceive('pathinfo')->andReturn('some2/a/b/c');
+
+ return [
+ [$request1, true, 'app1'],
+ [$request2, true, 'app2'],
+ [$request3, true, 'app3'],
+ [$request4, true, null],
+ [$request5, true, 'some2', 'path'],
+ [$request1, false, 'some3'],
+ ];
+ }
+
+ public function testRunWithException()
+ {
+ $request = m::mock(Request::class);
+ $response = m::mock(Response::class);
+
+ $this->app->shouldReceive('instance')->once()->with('request', $request);
+ $this->app->shouldReceive('initialize')->once();
+
+ $exception = new Exception();
+
+ $this->http->shouldReceive('runWithRequest')->once()->with($request)->andThrow($exception);
+
+ $handle = m::mock(Handle::class);
+
+ $handle->shouldReceive('report')->once()->with($exception);
+ $handle->shouldReceive('render')->once()->with($request, $exception)->andReturn($response);
+
+ $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
+
+ $this->assertEquals($response, $this->http->run($request));
+ }
+
+ public function testEnd()
+ {
+ $response = m::mock(Response::class);
+ $event = m::mock(Event::class);
+ $event->shouldReceive('trigger')->once()->with(HttpEnd::class, $response);
+ $this->app->shouldReceive('get')->once()->with('event')->andReturn($event);
+ $log = m::mock(Log::class);
+ $log->shouldReceive('save')->once();
+ $this->app->shouldReceive('get')->once()->with('log')->andReturn($log);
+
+ $this->http->end($response);
+ }
+
+}
diff --git a/vendor/topthink/framework/tests/InteractsWithApp.php b/vendor/topthink/framework/tests/InteractsWithApp.php
new file mode 100644
index 0000000..f4fcf73
--- /dev/null
+++ b/vendor/topthink/framework/tests/InteractsWithApp.php
@@ -0,0 +1,30 @@
+app = m::mock(App::class)->makePartial();
+ Container::setInstance($this->app);
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->app->shouldReceive('isDebug')->andReturnTrue();
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->config->shouldReceive('get')->with('app.show_error_msg')->andReturnTrue();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+ $this->app->shouldReceive('runningInConsole')->andReturn(false);
+ }
+}
diff --git a/vendor/topthink/framework/tests/LogTest.php b/vendor/topthink/framework/tests/LogTest.php
new file mode 100644
index 0000000..981110f
--- /dev/null
+++ b/vendor/topthink/framework/tests/LogTest.php
@@ -0,0 +1,130 @@
+prepareApp();
+
+ $this->log = new Log($this->app);
+ }
+
+ public function testGetConfig()
+ {
+ $config = [
+ 'default' => 'file',
+ ];
+
+ $this->config->shouldReceive('get')->with('log')->andReturn($config);
+
+ $this->assertEquals($config, $this->log->getConfig());
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->log->getChannelConfig('foo');
+ }
+
+ public function testChannel()
+ {
+ $this->assertInstanceOf(ChannelSet::class, $this->log->channel(['file', 'mail']));
+ }
+
+ public function testLogManagerInstances()
+ {
+ $this->config->shouldReceive('get')->with("log.channels.single", null)->andReturn(['type' => 'file']);
+
+ $channel1 = $this->log->channel('single');
+ $channel2 = $this->log->channel('single');
+
+ $this->assertSame($channel1, $channel2);
+ }
+
+ public function testFileLog()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("log.channels.file", null)->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->log->info('foo');
+
+ $this->assertEquals($this->log->getLog(), ['info' => ['foo']]);
+
+ $this->log->clear();
+
+ $this->assertEmpty($this->log->getLog());
+
+ $this->log->error('foo');
+ $this->assertArrayHasKey('error', $this->log->getLog());
+
+ $this->log->emergency('foo');
+ $this->assertArrayHasKey('emergency', $this->log->getLog());
+
+ $this->log->alert('foo');
+ $this->assertArrayHasKey('alert', $this->log->getLog());
+
+ $this->log->critical('foo');
+ $this->assertArrayHasKey('critical', $this->log->getLog());
+
+ $this->log->warning('foo');
+ $this->assertArrayHasKey('warning', $this->log->getLog());
+
+ $this->log->notice('foo');
+ $this->assertArrayHasKey('notice', $this->log->getLog());
+
+ $this->log->debug('foo');
+ $this->assertArrayHasKey('debug', $this->log->getLog());
+
+ $this->log->sql('foo');
+ $this->assertArrayHasKey('sql', $this->log->getLog());
+
+ $this->log->custom('foo');
+ $this->assertArrayHasKey('custom', $this->log->getLog());
+
+ $this->log->write('foo');
+ $this->assertTrue($root->hasChildren());
+ $this->assertEmpty($this->log->getLog());
+
+ $this->log->close();
+
+ $this->log->info('foo');
+
+ $this->assertEmpty($this->log->getLog());
+ }
+
+ public function testSave()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("log.channels.file", null)->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->log->info('foo');
+
+ $this->log->save();
+
+ $this->assertTrue($root->hasChildren());
+ }
+
+}
diff --git a/vendor/topthink/framework/tests/MiddlewareTest.php b/vendor/topthink/framework/tests/MiddlewareTest.php
new file mode 100644
index 0000000..aa53059
--- /dev/null
+++ b/vendor/topthink/framework/tests/MiddlewareTest.php
@@ -0,0 +1,108 @@
+prepareApp();
+
+ $this->middleware = new Middleware($this->app);
+ }
+
+ public function testSetMiddleware()
+ {
+ $this->middleware->add('BarMiddleware', 'bar');
+
+ $this->assertEquals(1, count($this->middleware->all('bar')));
+
+ $this->middleware->controller('BarMiddleware');
+ $this->assertEquals(1, count($this->middleware->all('controller')));
+
+ $this->middleware->import(['FooMiddleware']);
+ $this->assertEquals(1, count($this->middleware->all()));
+
+ $this->middleware->unshift(['BazMiddleware', 'baz']);
+ $this->assertEquals(2, count($this->middleware->all()));
+ $this->assertEquals([['BazMiddleware', 'handle'], 'baz'], $this->middleware->all()[0]);
+
+ $this->config->shouldReceive('get')->with('middleware.alias', [])->andReturn(['foo' => ['FooMiddleware', 'FarMiddleware']]);
+
+ $this->middleware->add('foo');
+ $this->assertEquals(3, count($this->middleware->all()));
+ $this->middleware->add(function () {
+ });
+ $this->middleware->add(function () {
+ });
+ $this->assertEquals(5, count($this->middleware->all()));
+ }
+
+ public function testPipelineAndEnd()
+ {
+ $bar = m::mock("overload:BarMiddleware");
+ $foo = m::mock("overload:FooMiddleware", Foo::class);
+
+ $request = m::mock(Request::class);
+ $response = m::mock(Response::class);
+
+ $e = new Exception();
+
+ $handle = m::mock(Handle::class);
+ $handle->shouldReceive('report')->with($e)->andReturnNull();
+ $handle->shouldReceive('render')->with($request, $e)->andReturn($response);
+
+ $foo->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) {
+ return $next($request);
+ });
+ $bar->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) use ($e) {
+ $next($request);
+ throw $e;
+ });
+
+ $foo->shouldReceive('end')->once()->with($response)->andReturnNull();
+
+ $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
+
+ $this->config->shouldReceive('get')->once()->with('middleware.priority', [])->andReturn(['FooMiddleware', 'BarMiddleware']);
+
+ $this->middleware->import([function ($request, $next) {
+ return $next($request);
+ }, 'BarMiddleware', 'FooMiddleware']);
+
+ $this->assertInstanceOf(Pipeline::class, $pipeline = $this->middleware->pipeline());
+
+ $pipeline->send($request)->then(function ($request) use ($e, $response) {
+ throw $e;
+ });
+
+ $this->middleware->end($response);
+ }
+}
+
+class Foo
+{
+ public function end(Response $response)
+ {
+ }
+}
diff --git a/vendor/topthink/framework/tests/RouteTest.php b/vendor/topthink/framework/tests/RouteTest.php
new file mode 100644
index 0000000..e992d0f
--- /dev/null
+++ b/vendor/topthink/framework/tests/RouteTest.php
@@ -0,0 +1,286 @@
+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');
+ $this->app->shouldReceive('make')->with(Request::class)->andReturn($request);
+ $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..b3b48a7
--- /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..e413510
--- /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..3459061
--- /dev/null
+++ b/vendor/topthink/framework/tests/bootstrap.php
@@ -0,0 +1,3 @@
+ composer require topthink/think-captcha
+
+
+
+## 使用
+
+### 在控制器中输出验证码
+
+在控制器的操作方法中使用
+
+~~~
+public function captcha($id = '')
+{
+ return captcha($id);
+}
+~~~
+然后注册对应的路由来输出验证码
+
+
+### 模板里输出验证码
+
+首先要在你应用的路由定义文件中,注册一个验证码路由规则。
+
+~~~
+\think\facade\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index");
+~~~
+
+然后就可以在模板文件中使用
+~~~
+{:captcha_img()}
+~~~
+或者
+~~~
+
+~~~
+> 上面两种的最终效果是一样的
+
+
+### 控制器里验证
+
+使用TP的内置验证功能即可
+~~~
+$this->validate($data,[
+ 'captcha|验证码'=>'require|captcha'
+]);
+~~~
+或者手动验证
+~~~
+if(!captcha_check($captcha)){
+ //验证失败
+};
+~~~
\ No newline at end of file
diff --git a/vendor/topthink/think-captcha/assets/bgs/1.jpg b/vendor/topthink/think-captcha/assets/bgs/1.jpg
new file mode 100644
index 0000000..d417136
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/1.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/2.jpg b/vendor/topthink/think-captcha/assets/bgs/2.jpg
new file mode 100644
index 0000000..56640bd
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/2.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/3.jpg b/vendor/topthink/think-captcha/assets/bgs/3.jpg
new file mode 100644
index 0000000..83e5bd9
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/3.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/4.jpg b/vendor/topthink/think-captcha/assets/bgs/4.jpg
new file mode 100644
index 0000000..97a3721
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/4.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/5.jpg b/vendor/topthink/think-captcha/assets/bgs/5.jpg
new file mode 100644
index 0000000..220a17a
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/5.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/6.jpg b/vendor/topthink/think-captcha/assets/bgs/6.jpg
new file mode 100644
index 0000000..be53ea0
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/6.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/7.jpg b/vendor/topthink/think-captcha/assets/bgs/7.jpg
new file mode 100644
index 0000000..fbf537f
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/7.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/bgs/8.jpg b/vendor/topthink/think-captcha/assets/bgs/8.jpg
new file mode 100644
index 0000000..e10cf28
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/8.jpg differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/1.ttf b/vendor/topthink/think-captcha/assets/ttfs/1.ttf
new file mode 100644
index 0000000..9eae6f2
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/1.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/2.ttf b/vendor/topthink/think-captcha/assets/ttfs/2.ttf
new file mode 100644
index 0000000..6386c6b
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/2.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/3.ttf b/vendor/topthink/think-captcha/assets/ttfs/3.ttf
new file mode 100644
index 0000000..678a491
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/3.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/4.ttf b/vendor/topthink/think-captcha/assets/ttfs/4.ttf
new file mode 100644
index 0000000..db43334
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/4.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/5.ttf b/vendor/topthink/think-captcha/assets/ttfs/5.ttf
new file mode 100644
index 0000000..8c082c8
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/5.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/ttfs/6.ttf b/vendor/topthink/think-captcha/assets/ttfs/6.ttf
new file mode 100644
index 0000000..45a038b
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/6.ttf differ
diff --git a/vendor/topthink/think-captcha/assets/zhttfs/1.ttf b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf
new file mode 100644
index 0000000..1c14f7f
Binary files /dev/null and b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf differ
diff --git a/vendor/topthink/think-captcha/composer.json b/vendor/topthink/think-captcha/composer.json
new file mode 100644
index 0000000..e598819
--- /dev/null
+++ b/vendor/topthink/think-captcha/composer.json
@@ -0,0 +1,32 @@
+{
+ "name": "topthink/think-captcha",
+ "description": "captcha package for thinkphp",
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "license": "Apache-2.0",
+ "require": {
+ "topthink/framework": "^6.0.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\captcha\\": "src/"
+ },
+ "files": [
+ "src/helper.php"
+ ]
+ },
+ "extra": {
+ "think": {
+ "services": [
+ "think\\captcha\\CaptchaService"
+ ],
+ "config":{
+ "captcha": "src/config.php"
+ }
+ }
+ }
+}
diff --git a/vendor/topthink/think-captcha/src/Captcha.php b/vendor/topthink/think-captcha/src/Captcha.php
new file mode 100644
index 0000000..0789087
--- /dev/null
+++ b/vendor/topthink/think-captcha/src/Captcha.php
@@ -0,0 +1,340 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\captcha;
+
+use Exception;
+use think\Config;
+use think\Response;
+use think\Session;
+
+class Captcha
+{
+ private $im = null; // 验证码图片实例
+ private $color = null; // 验证码字体颜色
+
+ /**
+ * @var Config|null
+ */
+ private $config = null;
+
+ /**
+ * @var Session|null
+ */
+ private $session = null;
+
+ // 验证码字符集合
+ protected $codeSet = '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY';
+ // 验证码过期时间(s)
+ protected $expire = 1800;
+ // 使用中文验证码
+ protected $useZh = false;
+ // 中文验证码字符串
+ protected $zhSet = '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借';
+ // 使用背景图片
+ protected $useImgBg = false;
+ // 验证码字体大小(px)
+ protected $fontSize = 25;
+ // 是否画混淆曲线
+ protected $useCurve = true;
+ // 是否添加杂点
+ protected $useNoise = true;
+ // 验证码图片高度
+ protected $imageH = 0;
+ // 验证码图片宽度
+ protected $imageW = 0;
+ // 验证码位数
+ protected $length = 5;
+ // 验证码字体,不设置随机获取
+ protected $fontttf = '';
+ // 背景颜色
+ protected $bg = [243, 251, 254];
+ //算术验证码
+ protected $math = false;
+
+ /**
+ * 架构方法 设置参数
+ * @access public
+ * @param Config $config
+ * @param Session $session
+ */
+ public function __construct(Config $config, Session $session)
+ {
+ $this->config = $config;
+ $this->session = $session;
+ }
+
+ /**
+ * 配置验证码
+ * @param string|null $config
+ */
+ protected function configure(string $config = null): void
+ {
+ if (is_null($config)) {
+ $config = $this->config->get('captcha', []);
+ } else {
+ $config = $this->config->get('captcha.' . $config, []);
+ }
+
+ foreach ($config as $key => $val) {
+ if (property_exists($this, $key)) {
+ $this->{$key} = $val;
+ }
+ }
+ }
+
+ /**
+ * 创建验证码
+ * @return array
+ * @throws Exception
+ */
+ protected function generate(): array
+ {
+ $bag = '';
+
+ if ($this->math) {
+ $this->useZh = false;
+ $this->length = 5;
+
+ $x = random_int(10, 30);
+ $y = random_int(1, 9);
+ $bag = "{$x} + {$y} = ";
+ $key = $x + $y;
+ $key .= '';
+ } else {
+ if ($this->useZh) {
+ $characters = preg_split('/(?zhSet);
+ } else {
+ $characters = str_split($this->codeSet);
+ }
+
+ for ($i = 0; $i < $this->length; $i++) {
+ $bag .= $characters[rand(0, count($characters) - 1)];
+ }
+
+ $key = mb_strtolower($bag, 'UTF-8');
+ }
+
+ $hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]);
+
+ $this->session->set('captcha', [
+ 'key' => $hash,
+ ]);
+
+ return [
+ 'value' => $bag,
+ 'key' => $hash,
+ ];
+ }
+
+ /**
+ * 验证验证码是否正确
+ * @access public
+ * @param string $code 用户验证码
+ * @return bool 用户验证码是否正确
+ */
+ public function check(string $code): bool
+ {
+ if (!$this->session->has('captcha')) {
+ return false;
+ }
+
+ $key = $this->session->get('captcha.key');
+
+ $code = mb_strtolower($code, 'UTF-8');
+
+ $res = password_verify($code, $key);
+
+ if ($res) {
+ $this->session->delete('captcha');
+ }
+
+ return $res;
+ }
+
+ /**
+ * 输出验证码并把验证码的值保存的session中
+ * @access public
+ * @param null|string $config
+ * @param bool $api
+ * @return Response
+ */
+ public function create(string $config = null, bool $api = false): Response
+ {
+ $this->configure($config);
+
+ $generator = $this->generate();
+
+ // 图片宽(px)
+ $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2;
+ // 图片高(px)
+ $this->imageH || $this->imageH = $this->fontSize * 2.5;
+ // 建立一幅 $this->imageW x $this->imageH 的图像
+ $this->im = imagecreate($this->imageW, $this->imageH);
+ // 设置背景
+ imagecolorallocate($this->im, $this->bg[0], $this->bg[1], $this->bg[2]);
+
+ // 验证码字体随机颜色
+ $this->color = imagecolorallocate($this->im, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150));
+
+ // 验证码使用随机字体
+ $ttfPath = __DIR__ . '/../assets/' . ($this->useZh ? 'zhttfs' : 'ttfs') . '/';
+
+ if (empty($this->fontttf)) {
+ $dir = dir($ttfPath);
+ $ttfs = [];
+ while (false !== ($file = $dir->read())) {
+ if ('.' != $file[0] && substr($file, -4) == '.ttf') {
+ $ttfs[] = $file;
+ }
+ }
+ $dir->close();
+ $this->fontttf = $ttfs[array_rand($ttfs)];
+ }
+
+ $fontttf = $ttfPath . $this->fontttf;
+
+ if ($this->useImgBg) {
+ $this->background();
+ }
+
+ if ($this->useNoise) {
+ // 绘杂点
+ $this->writeNoise();
+ }
+ if ($this->useCurve) {
+ // 绘干扰线
+ $this->writeCurve();
+ }
+
+ // 绘验证码
+ $text = $this->useZh ? preg_split('/(? $char) {
+
+ $x = $this->fontSize * ($index + 1) * mt_rand(1.2, 1.6) * ($this->math ? 1 : 1.5);
+ $y = $this->fontSize + mt_rand(10, 20);
+ $angle = $this->math ? 0 : mt_rand(-40, 40);
+
+ imagettftext($this->im, $this->fontSize, $angle, $x, $y, $this->color, $fontttf, $char);
+ }
+
+ ob_start();
+ // 输出图像
+ imagepng($this->im);
+ $content = ob_get_clean();
+ imagedestroy($this->im);
+
+ return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png');
+ }
+
+ /**
+ * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
+ *
+ * 高中的数学公式咋都忘了涅,写出来
+ * 正弦型函数解析式:y=Asin(ωx+φ)+b
+ * 各常数值对函数图像的影响:
+ * A:决定峰值(即纵向拉伸压缩的倍数)
+ * b:表示波形在Y轴的位置关系或纵向移动距离(上加下减)
+ * φ:决定波形与X轴位置关系或横向移动距离(左加右减)
+ * ω:决定周期(最小正周期T=2π/∣ω∣)
+ *
+ */
+ protected function writeCurve(): void
+ {
+ $px = $py = 0;
+
+ // 曲线前部分
+ $A = mt_rand(1, $this->imageH / 2); // 振幅
+ $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量
+ $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
+ $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
+ $w = (2 * M_PI) / $T;
+
+ $px1 = 0; // 曲线横坐标起始位置
+ $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置
+
+ for ($px = $px1; $px <= $px2; $px = $px + 1) {
+ if (0 != $w) {
+ $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
+ $i = (int) ($this->fontSize / 5);
+ while ($i > 0) {
+ imagesetpixel($this->im, $px + $i, $py + $i, $this->color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多
+ $i--;
+ }
+ }
+ }
+
+ // 曲线后部分
+ $A = mt_rand(1, $this->imageH / 2); // 振幅
+ $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
+ $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
+ $w = (2 * M_PI) / $T;
+ $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2;
+ $px1 = $px2;
+ $px2 = $this->imageW;
+
+ for ($px = $px1; $px <= $px2; $px = $px + 1) {
+ if (0 != $w) {
+ $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
+ $i = (int) ($this->fontSize / 5);
+ while ($i > 0) {
+ imagesetpixel($this->im, $px + $i, $py + $i, $this->color);
+ $i--;
+ }
+ }
+ }
+ }
+
+ /**
+ * 画杂点
+ * 往图片上写不同颜色的字母或数字
+ */
+ protected function writeNoise(): void
+ {
+ $codeSet = '2345678abcdefhijkmnpqrstuvwxyz';
+ for ($i = 0; $i < 10; $i++) {
+ //杂点颜色
+ $noiseColor = imagecolorallocate($this->im, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225));
+ for ($j = 0; $j < 5; $j++) {
+ // 绘杂点
+ imagestring($this->im, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor);
+ }
+ }
+ }
+
+ /**
+ * 绘制背景图片
+ * 注:如果验证码输出图片比较大,将占用比较多的系统资源
+ */
+ protected function background(): void
+ {
+ $path = __DIR__ . '/../assets/bgs/';
+ $dir = dir($path);
+
+ $bgs = [];
+ while (false !== ($file = $dir->read())) {
+ if ('.' != $file[0] && substr($file, -4) == '.jpg') {
+ $bgs[] = $path . $file;
+ }
+ }
+ $dir->close();
+
+ $gb = $bgs[array_rand($bgs)];
+
+ list($width, $height) = @getimagesize($gb);
+ // Resample
+ $bgImage = @imagecreatefromjpeg($gb);
+ @imagecopyresampled($this->im, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height);
+ @imagedestroy($bgImage);
+ }
+
+}
diff --git a/vendor/topthink/think-captcha/src/CaptchaController.php b/vendor/topthink/think-captcha/src/CaptchaController.php
new file mode 100644
index 0000000..2c3cf59
--- /dev/null
+++ b/vendor/topthink/think-captcha/src/CaptchaController.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\captcha;
+
+class CaptchaController
+{
+ public function index(Captcha $captcha, $config = null)
+ {
+ return $captcha->create($config);
+ }
+}
diff --git a/vendor/topthink/think-captcha/src/CaptchaService.php b/vendor/topthink/think-captcha/src/CaptchaService.php
new file mode 100644
index 0000000..1848858
--- /dev/null
+++ b/vendor/topthink/think-captcha/src/CaptchaService.php
@@ -0,0 +1,23 @@
+extend('captcha', function ($value) {
+ return captcha_check($value);
+ }, ':attribute错误!');
+ });
+
+ $this->registerRoutes(function (Route $route) {
+ $route->get('captcha/[:config]', "\\think\\captcha\\CaptchaController@index");
+ });
+ }
+}
diff --git a/vendor/topthink/think-captcha/src/config.php b/vendor/topthink/think-captcha/src/config.php
new file mode 100644
index 0000000..9bbf529
--- /dev/null
+++ b/vendor/topthink/think-captcha/src/config.php
@@ -0,0 +1,39 @@
+ 5,
+ // 验证码字符集合
+ 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
+ // 验证码过期时间
+ 'expire' => 1800,
+ // 是否使用中文验证码
+ 'useZh' => false,
+ // 是否使用算术验证码
+ 'math' => false,
+ // 是否使用背景图
+ 'useImgBg' => false,
+ //验证码字符大小
+ 'fontSize' => 25,
+ // 是否使用混淆曲线
+ 'useCurve' => true,
+ //是否添加杂点
+ 'useNoise' => true,
+ // 验证码字体 不设置则随机
+ 'fontttf' => '',
+ //背景颜色
+ 'bg' => [243, 251, 254],
+ // 验证码图片高度
+ 'imageH' => 0,
+ // 验证码图片宽度
+ 'imageW' => 0,
+
+ // 添加额外的验证码设置
+ // verify => [
+ // 'length'=>4,
+ // ...
+ //],
+];
diff --git a/vendor/topthink/think-captcha/src/facade/Captcha.php b/vendor/topthink/think-captcha/src/facade/Captcha.php
new file mode 100644
index 0000000..cd9f793
--- /dev/null
+++ b/vendor/topthink/think-captcha/src/facade/Captcha.php
@@ -0,0 +1,18 @@
+
+// +----------------------------------------------------------------------
+
+use think\captcha\facade\Captcha;
+use think\facade\Route;
+use think\Response;
+
+/**
+ * @param string $config
+ * @return \think\Response
+ */
+function captcha($config = null): Response
+{
+ return Captcha::create($config);
+}
+
+/**
+ * @param $config
+ * @return string
+ */
+function captcha_src($config = null): string
+{
+ return Route::buildUrl('/captcha' . ($config ? "/{$config}" : ''));
+}
+
+/**
+ * @param $id
+ * @return string
+ */
+function captcha_img($id = '', $domid = ''): string
+{
+ $src = captcha_src($id);
+
+ $domid = empty($domid) ? $domid : "id='" . $domid . "'";
+
+ return "
";
+}
+
+/**
+ * @param string $value
+ * @return bool
+ */
+function captcha_check($value)
+{
+ return Captcha::check($value);
+}
diff --git a/vendor/topthink/think-helper/.gitignore b/vendor/topthink/think-helper/.gitignore
new file mode 100644
index 0000000..d851bdb
--- /dev/null
+++ b/vendor/topthink/think-helper/.gitignore
@@ -0,0 +1,3 @@
+/vendor/
+/.idea/
+composer.lock
\ No newline at end of file
diff --git a/vendor/topthink/think-helper/LICENSE b/vendor/topthink/think-helper/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/vendor/topthink/think-helper/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/topthink/think-helper/README.md b/vendor/topthink/think-helper/README.md
new file mode 100644
index 0000000..7baf8f7
--- /dev/null
+++ b/vendor/topthink/think-helper/README.md
@@ -0,0 +1,33 @@
+# thinkphp6 常用的一些扩展类库
+
+基于PHP7.1+
+
+> 以下类库都在`\\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..b68c43b
--- /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..fa408c2
--- /dev/null
+++ b/vendor/topthink/think-helper/src/Collection.php
@@ -0,0 +1,655 @@
+
+// +----------------------------------------------------------------------
+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 $this
+ */
+ public function push($value, string $key = null)
+ {
+ if (is_null($key)) {
+ $this->items[] = $value;
+ } else {
+ $this->items[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 把一个数组分割为新的数组块.
+ *
+ * @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 $this
+ */
+ public function unshift($value, string $key = null)
+ {
+ if (is_null($key)) {
+ array_unshift($this->items, $value);
+ } else {
+ $this->items = [$key => $value] + $this->items;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 给每个元素执行个回调
+ *
+ * @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) ? intval($fieldB > $fieldA) : intval($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..7c6b992
--- /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..ed4d6a9
--- /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..7391fbd
--- /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-multi-app/LICENSE b/vendor/topthink/think-multi-app/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/vendor/topthink/think-multi-app/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/topthink/think-multi-app/README.md b/vendor/topthink/think-multi-app/README.md
new file mode 100644
index 0000000..a746fa7
--- /dev/null
+++ b/vendor/topthink/think-multi-app/README.md
@@ -0,0 +1,14 @@
+# think-multi-app
+
+用于ThinkPHP6+的多应用支持
+
+## 安装
+
+~~~
+composer require topthink/think-multi-app
+~~~
+
+## 使用
+
+用法参考ThinkPHP6完全开发手册[多应用模式](https://www.kancloud.cn/manual/thinkphp6_0/1297876)章节。
+
diff --git a/vendor/topthink/think-multi-app/composer.json b/vendor/topthink/think-multi-app/composer.json
new file mode 100644
index 0000000..92d620e
--- /dev/null
+++ b/vendor/topthink/think-multi-app/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "topthink/think-multi-app",
+ "description": "thinkphp6 multi app support",
+ "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\\app\\": "src"
+ }
+ },
+ "extra": {
+ "think":{
+ "services":[
+ "think\\app\\Service"
+ ]
+ }
+ },
+ "minimum-stability": "dev"
+}
diff --git a/vendor/topthink/think-multi-app/src/MultiApp.php b/vendor/topthink/think-multi-app/src/MultiApp.php
new file mode 100644
index 0000000..b0ac260
--- /dev/null
+++ b/vendor/topthink/think-multi-app/src/MultiApp.php
@@ -0,0 +1,245 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\app;
+
+use Closure;
+use think\App;
+use think\exception\HttpException;
+use think\Request;
+use think\Response;
+
+/**
+ * 多应用模式支持
+ */
+class MultiApp
+{
+
+ /** @var App */
+ protected $app;
+
+ /**
+ * 应用名称
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 应用名称
+ * @var string
+ */
+ protected $appName;
+
+ /**
+ * 应用路径
+ * @var string
+ */
+ protected $path;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->name = $this->app->http->getName();
+ $this->path = $this->app->http->getPath();
+ }
+
+ /**
+ * 多应用解析
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle($request, Closure $next)
+ {
+ if (!$this->parseMultiApp()) {
+ return $next($request);
+ }
+
+ return $this->app->middleware->pipeline('app')
+ ->send($request)
+ ->then(function ($request) use ($next) {
+ return $next($request);
+ });
+ }
+
+ /**
+ * 获取路由目录
+ * @access protected
+ * @return string
+ */
+ protected function getRoutePath(): string
+ {
+ return $this->app->getAppPath() . 'route' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 解析多应用
+ * @return bool
+ */
+ protected function parseMultiApp(): bool
+ {
+ $scriptName = $this->getScriptName();
+ $defaultApp = $this->app->config->get('app.default_app') ?: 'index';
+
+ if ($this->name || ($scriptName && !in_array($scriptName, ['index', 'router', 'think']))) {
+ $appName = $this->name ?: $scriptName;
+ $this->app->http->setBind();
+ } else {
+ // 自动多应用识别
+ $this->app->http->setBind(false);
+ $appName = null;
+ $this->appName = '';
+
+ $bind = $this->app->config->get('app.domain_bind', []);
+
+ if (!empty($bind)) {
+ // 获取当前子域名
+ $subDomain = $this->app->request->subDomain();
+ $domain = $this->app->request->host(true);
+
+ if (isset($bind[$domain])) {
+ $appName = $bind[$domain];
+ $this->app->http->setBind();
+ } elseif (isset($bind[$subDomain])) {
+ $appName = $bind[$subDomain];
+ $this->app->http->setBind();
+ } elseif (isset($bind['*'])) {
+ $appName = $bind['*'];
+ $this->app->http->setBind();
+ }
+ }
+
+ if (!$this->app->http->isBind()) {
+ $path = $this->app->request->pathinfo();
+ $map = $this->app->config->get('app.app_map', []);
+ $deny = $this->app->config->get('app.deny_app_list', []);
+ $name = current(explode('/', $path));
+
+ if (strpos($name, '.')) {
+ $name = strstr($name, '.', true);
+ }
+
+ if (isset($map[$name])) {
+ if ($map[$name] instanceof Closure) {
+ $result = call_user_func_array($map[$name], [$this->app]);
+ $appName = $result ?: $name;
+ } else {
+ $appName = $map[$name];
+ }
+ } elseif ($name && (false !== array_search($name, $map) || in_array($name, $deny))) {
+ throw new HttpException(404, 'app not exists:' . $name);
+ } elseif ($name && isset($map['*'])) {
+ $appName = $map['*'];
+ } else {
+ $appName = $name ?: $defaultApp;
+ $appPath = $this->path ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR;
+
+ if (!is_dir($appPath)) {
+ $express = $this->app->config->get('app.app_express', false);
+ if ($express) {
+ $this->setApp($defaultApp);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ if ($name) {
+ $this->app->request->setRoot('/' . $name);
+ $this->app->request->setPathinfo(strpos($path, '/') ? ltrim(strstr($path, '/'), '/') : '');
+ }
+ }
+ }
+
+ $this->setApp($appName ?: $defaultApp);
+ return true;
+ }
+
+ /**
+ * 获取当前运行入口名称
+ * @access protected
+ * @codeCoverageIgnore
+ * @return string
+ */
+ protected function getScriptName(): string
+ {
+ if (isset($_SERVER['SCRIPT_FILENAME'])) {
+ $file = $_SERVER['SCRIPT_FILENAME'];
+ } elseif (isset($_SERVER['argv'][0])) {
+ $file = realpath($_SERVER['argv'][0]);
+ }
+
+ return isset($file) ? pathinfo($file, PATHINFO_FILENAME) : '';
+ }
+
+ /**
+ * 设置应用
+ * @param string $appName
+ */
+ protected function setApp(string $appName): void
+ {
+ $this->appName = $appName;
+ $this->app->http->name($appName);
+
+ $appPath = $this->path ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR;
+
+ $this->app->setAppPath($appPath);
+ // 设置应用命名空间
+ $this->app->setNamespace($this->app->config->get('app.app_namespace') ?: 'app\\' . $appName);
+
+ if (is_dir($appPath)) {
+ $this->app->setRuntimePath($this->app->getRuntimePath() . $appName . DIRECTORY_SEPARATOR);
+ $this->app->http->setRoutePath($this->getRoutePath());
+
+ //加载应用
+ $this->loadApp($appName, $appPath);
+ }
+ }
+
+ /**
+ * 加载应用文件
+ * @param string $appName 应用名
+ * @return void
+ */
+ protected function loadApp(string $appName, string $appPath): void
+ {
+ if (is_file($appPath . 'common.php')) {
+ include_once $appPath . 'common.php';
+ }
+
+ $files = [];
+
+ $files = array_merge($files, glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $this->app->getConfigExt()));
+
+ foreach ($files as $file) {
+ $this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME));
+ }
+
+ if (is_file($appPath . 'event.php')) {
+ $this->app->loadEvent(include $appPath . 'event.php');
+ }
+
+ if (is_file($appPath . 'middleware.php')) {
+ $this->app->middleware->import(include $appPath . 'middleware.php', 'app');
+ }
+
+ if (is_file($appPath . 'provider.php')) {
+ $this->app->bind(include $appPath . 'provider.php');
+ }
+
+ // 加载应用默认语言包
+ $this->app->loadLangPack($this->app->lang->defaultLangSet());
+ }
+
+}
diff --git a/vendor/topthink/think-multi-app/src/Service.php b/vendor/topthink/think-multi-app/src/Service.php
new file mode 100644
index 0000000..cdc90b4
--- /dev/null
+++ b/vendor/topthink/think-multi-app/src/Service.php
@@ -0,0 +1,32 @@
+
+// +----------------------------------------------------------------------
+namespace think\app;
+
+use think\Service as BaseService;
+
+class Service extends BaseService
+{
+ public function boot()
+ {
+ $this->app->event->listen('HttpRun', function () {
+ $this->app->middleware->add(MultiApp::class);
+ });
+
+ $this->commands([
+ 'build' => command\Build::class,
+ 'clear' => command\Clear::class,
+ ]);
+
+ $this->app->bind([
+ 'think\route\Url' => Url::class,
+ ]);
+ }
+}
diff --git a/vendor/topthink/think-multi-app/src/Url.php b/vendor/topthink/think-multi-app/src/Url.php
new file mode 100644
index 0000000..7bd6057
--- /dev/null
+++ b/vendor/topthink/think-multi-app/src/Url.php
@@ -0,0 +1,232 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\app;
+
+use think\App;
+use think\Route;
+use think\route\Url as UrlBuild;
+
+/**
+ * 路由地址生成
+ */
+class Url extends UrlBuild
+{
+ /**
+ * 直接解析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 = $this->getAppName() . '/' . $request->controller() . '/' . $request->action();
+ } else {
+ // 解析到 应用/控制器/操作
+ $controller = $request->controller();
+ $app = $this->getAppName();
+ $path = explode('/', $url);
+ $action = array_pop($path);
+ $controller = empty($path) ? $controller : array_pop($path);
+ $app = empty($path) ? $app : array_pop($path);
+ $url = $controller . '/' . $action;
+ $bind = $this->app->config->get('app.domain_bind', []);
+
+ if ($key = array_search($this->app->http->getName(), $bind)) {
+ isset($bind[$_SERVER['SERVER_NAME']]) && $domain = $_SERVER['SERVER_NAME'];
+
+ $domain = is_bool($domain) ? $key : $domain;
+ } else {
+ $url = $app . '/' . $url;
+ }
+ }
+
+ return $url;
+ }
+
+ 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, '?')) {
+ // 解析参数
+ list($anchor, $info['query']) = explode('?', $anchor, 2);
+ }
+
+ if (false !== strpos($anchor, '@')) {
+ // 解析域名
+ list($anchor, $domain) = explode('@', $anchor, 2);
+ }
+ } elseif (strpos($url, '@') && false === strpos($url, '\\')) {
+ // 解析域名
+ list($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];
+ }
+
+ if (!$this->app->http->isBind()) {
+ $app = $this->getAppName();
+ $url = $app . '/' . $url;
+ }
+ } 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, '/') . '/' . ltrim($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, '/');
+ }
+
+ /**
+ * 获取URL的应用名
+ * @access protected
+ * @return string
+ */
+ protected function getAppName()
+ {
+ $app = $this->app->http->getName();
+ $map = $this->app->config->get('app.app_map', []);
+
+ if ($key = array_search($app, $map)) {
+ $app = $key;
+ }
+
+ return $app;
+ }
+}
diff --git a/vendor/topthink/think-multi-app/src/command/Build.php b/vendor/topthink/think-multi-app/src/command/Build.php
new file mode 100644
index 0000000..65b2f87
--- /dev/null
+++ b/vendor/topthink/think-multi-app/src/command/Build.php
@@ -0,0 +1,180 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\app\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+
+class Build extends Command
+{
+ /**
+ * 应用基础目录
+ * @var string
+ */
+ protected $basePath;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setName('build')
+ ->addArgument('app', Argument::OPTIONAL, 'app name .')
+ ->setDescription('Build App Dirs');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $this->basePath = $this->app->getBasePath();
+ $app = $input->getArgument('app') ?: '';
+
+ if (is_file($this->basePath . 'build.php')) {
+ $list = include $this->basePath . 'build.php';
+ } else {
+ $list = [
+ '__dir__' => ['controller', 'model', 'view'],
+ ];
+ }
+
+ $this->buildApp($app, $list);
+ $output->writeln("Successed");
+
+ }
+
+ /**
+ * 创建应用
+ * @access protected
+ * @param string $app 应用名
+ * @param array $list 目录结构
+ * @return void
+ */
+ protected function buildApp(string $app, array $list = []): void
+ {
+ if (!is_dir($this->basePath . $app)) {
+ // 创建应用目录
+ mkdir($this->basePath . $app);
+ }
+
+ $appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
+ $namespace = 'app' . ($app ? '\\' . $app : '');
+
+ // 创建配置文件和公共文件
+ $this->buildCommon($app);
+ // 创建模块的默认页面
+ $this->buildHello($app, $namespace);
+
+ foreach ($list as $path => $file) {
+ if ('__dir__' == $path) {
+ // 生成子目录
+ foreach ($file as $dir) {
+ $this->checkDirBuild($appPath . $dir);
+ }
+ } elseif ('__file__' == $path) {
+ // 生成(空白)文件
+ foreach ($file as $name) {
+ if (!is_file($appPath . $name)) {
+ file_put_contents($appPath . $name, 'php' == pathinfo($name, PATHINFO_EXTENSION) ? 'app->config->get('route.controller_suffix')) {
+ $filename = $appPath . $path . DIRECTORY_SEPARATOR . $val . 'Controller.php';
+ $class = $val . 'Controller';
+ }
+ $content = "checkDirBuild(dirname($filename));
+ $content = '';
+ break;
+ default:
+ // 其他文件
+ $content = "app->config->get('route.controller_suffix') ? 'Controller' : '';
+ $filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
+
+ if (!is_file($filename)) {
+ $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'controller.stub');
+ $content = str_replace(['{%name%}', '{%app%}', '{%layer%}', '{%suffix%}'], [$app, $namespace, 'controller', $suffix], $content);
+ $this->checkDirBuild(dirname($filename));
+
+ file_put_contents($filename, $content);
+ }
+ }
+
+ /**
+ * 创建应用的公共文件
+ * @access protected
+ * @param string $app 目录
+ * @return void
+ */
+ protected function buildCommon(string $app): void
+ {
+ $appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
+
+ if (!is_file($appPath . 'common.php')) {
+ file_put_contents($appPath . 'common.php', "
+// +----------------------------------------------------------------------
+namespace think\app\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+
+class Clear extends Command
+{
+ protected function configure()
+ {
+ // 指令配置
+ $this->setName('clear')
+ ->addArgument('app', Argument::OPTIONAL, 'app name .')
+ ->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')
+ ->setDescription('Clear runtime file');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $app = $input->getArgument('app') ?: '';
+ $runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
+
+ if ($input->getOption('cache')) {
+ $path = $runtimePath . 'cache';
+ } elseif ($input->getOption('log')) {
+ $path = $runtimePath . 'log';
+ } else {
+ $path = $runtimePath;
+ }
+
+ $rmdir = $input->getOption('dir') ? true : false;
+ $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir);
+
+ $output->writeln("Clear Successed");
+ }
+
+ protected function clear(string $path, bool $rmdir): void
+ {
+ $files = is_dir($path) ? scandir($path) : [];
+
+ foreach ($files as $file) {
+ if ('.' != $file && '..' != $file && is_dir($path . $file)) {
+ array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*'));
+ if ($rmdir) {
+ rmdir($path . $file);
+ }
+ } elseif ('.gitignore' != $file && is_file($path . $file)) {
+ unlink($path . $file);
+ }
+ }
+ }
+}
diff --git a/vendor/topthink/think-multi-app/src/command/stubs/controller.stub b/vendor/topthink/think-multi-app/src/command/stubs/controller.stub
new file mode 100644
index 0000000..263c4c6
--- /dev/null
+++ b/vendor/topthink/think-multi-app/src/command/stubs/controller.stub
@@ -0,0 +1,12 @@
+=7.1.0",
+ "ext-json": "*",
+ "ext-pdo": "*",
+ "psr/simple-cache": "^1.0",
+ "psr/log": "~1.0",
+ "topthink/think-helper":"^3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7|^8|^9.5"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ },
+ "files": [
+ "stubs/load_stubs.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "tests\\": "tests"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/vendor/topthink/think-orm/src/DbManager.php b/vendor/topthink/think-orm/src/DbManager.php
new file mode 100644
index 0000000..f223cd2
--- /dev/null
+++ b/vendor/topthink/think-orm/src/DbManager.php
@@ -0,0 +1,376 @@
+
+// +----------------------------------------------------------------------
+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()
+ {
+ 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
+ */
+ public function triggerSql(): void
+ {}
+
+ /**
+ * 初始化配置参数
+ * @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, [$params]);
+ }
+ }
+ }
+
+ 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..041ec81
--- /dev/null
+++ b/vendor/topthink/think-orm/src/Model.php
@@ -0,0 +1,1068 @@
+
+// +----------------------------------------------------------------------
+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;
+ $this->get = [];
+
+ if ($relation) {
+ $this->relation = [];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置数据是否存在
+ * @access public
+ * @param bool $exists
+ * @return $this
+ */
+ public function exists(bool $exists = true)
+ {
+ $this->exists = $exists;
+ return $this;
+ }
+
+ /**
+ * 判断数据是否存在数据库
+ * @access public
+ * @return bool
+ */
+ public function isExists(): bool
+ {
+ return $this->exists;
+ }
+
+ /**
+ * 判断模型是否为空
+ * @access public
+ * @return bool
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->data);
+ }
+
+ /**
+ * 延迟保存当前数据对象
+ * @access public
+ * @param array|bool $data 数据
+ * @return void
+ */
+ public function lazySave($data = []): void
+ {
+ if (false === $data) {
+ $this->lazySave = false;
+ } else {
+ if (is_array($data)) {
+ $this->setAttrs($data);
+ }
+
+ $this->lazySave = true;
+ }
+ }
+
+ /**
+ * 保存当前数据对象
+ * @access public
+ * @param array $data 数据
+ * @param string $sequence 自增序列名
+ * @return bool
+ */
+ public function save(array $data = [], string $sequence = null): bool
+ {
+ // 数据对象赋值
+ $this->setAttrs($data);
+
+ if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
+ return false;
+ }
+
+ $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
+
+ if (false === $result) {
+ return false;
+ }
+
+ // 写入回调
+ $this->trigger('AfterWrite');
+
+ // 重新记录原始数据
+ $this->origin = $this->data;
+ $this->get = [];
+ $this->lazySave = false;
+
+ return true;
+ }
+
+ /**
+ * 检查数据是否允许写入
+ * @access protected
+ * @return array
+ */
+ protected function checkAllowFields(): array
+ {
+ // 检测字段
+ if (empty($this->field)) {
+ if (!empty($this->schema)) {
+ $this->field = array_keys(array_merge($this->schema, $this->jsonType));
+ } else {
+ $query = $this->db();
+ $table = $this->table ? $this->table . $this->suffix : $query->getTable();
+
+ $this->field = $query->getConnection()->getTableFields($table);
+ }
+
+ return $this->field;
+ }
+
+ $field = $this->field;
+
+ if ($this->autoWriteTimestamp) {
+ array_push($field, $this->createTime, $this->updateTime);
+ }
+
+ if (!empty($this->disuse)) {
+ // 废弃字段
+ $field = array_diff($field, $this->disuse);
+ }
+
+ return $field;
+ }
+
+ /**
+ * 保存写入数据
+ * @access protected
+ * @return bool
+ */
+ protected function updateData(): bool
+ {
+ // 事件回调
+ if (false === $this->trigger('BeforeUpdate')) {
+ return false;
+ }
+
+ $this->checkData();
+
+ // 获取有更新的数据
+ $data = $this->getChangedData();
+
+ if (empty($data)) {
+ // 关联更新
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationUpdate();
+ }
+
+ return true;
+ }
+
+ if ($this->autoWriteTimestamp && $this->updateTime) {
+ // 自动写入更新时间
+ $data[$this->updateTime] = $this->autoWriteTimestamp();
+ $this->data[$this->updateTime] = $this->getTimestampValue($data[$this->updateTime]);
+ }
+
+ // 检查允许字段
+ $allowFields = $this->checkAllowFields();
+
+ foreach ($this->relationWrite as $name => $val) {
+ if (!is_array($val)) {
+ continue;
+ }
+
+ foreach ($val as $key) {
+ if (isset($data[$key])) {
+ unset($data[$key]);
+ }
+ }
+ }
+
+ // 模型更新
+ $db = $this->db();
+
+ $db->transaction(function () use ($data, $allowFields, $db) {
+ $this->key = null;
+ $where = $this->getWhere();
+
+ $result = $db->where($where)
+ ->strict(false)
+ ->cache(true)
+ ->setOption('key', $this->key)
+ ->field($allowFields)
+ ->update($data);
+
+ $this->checkResult($result);
+
+ // 关联更新
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationUpdate();
+ }
+ });
+
+ // 更新回调
+ $this->trigger('AfterUpdate');
+
+ return true;
+ }
+
+ /**
+ * 新增写入数据
+ * @access protected
+ * @param string $sequence 自增名
+ * @return bool
+ */
+ protected function insertData(string $sequence = null): bool
+ {
+ if (false === $this->trigger('BeforeInsert')) {
+ return false;
+ }
+
+ $this->checkData();
+ $data = $this->data;
+
+ // 时间戳自动写入
+ if ($this->autoWriteTimestamp) {
+ if ($this->createTime && !isset($data[$this->createTime])) {
+ $data[$this->createTime] = $this->autoWriteTimestamp();
+ $this->data[$this->createTime] = $this->getTimestampValue($data[$this->createTime]);
+ }
+
+ if ($this->updateTime && !isset($data[$this->updateTime])) {
+ $data[$this->updateTime] = $this->autoWriteTimestamp();
+ $this->data[$this->updateTime] = $this->getTimestampValue($data[$this->updateTime]);
+ }
+ }
+
+ // 检查允许字段
+ $allowFields = $this->checkAllowFields();
+
+ $db = $this->db();
+
+ $db->transaction(function () use ($data, $sequence, $allowFields, $db) {
+ $result = $db->strict(false)
+ ->field($allowFields)
+ ->replace($this->replace)
+ ->sequence($sequence)
+ ->insert($data, true);
+
+ // 获取自动增长主键
+ if ($result) {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) {
+ unset($this->get[$pk]);
+ $this->data[$pk] = $result;
+ }
+ }
+
+ // 关联写入
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationInsert();
+ }
+ });
+
+ // 标记数据已经存在
+ $this->exists = true;
+ $this->origin = $this->data;
+
+ // 新增回调
+ $this->trigger('AfterInsert');
+
+ return true;
+ }
+
+ /**
+ * 获取当前的更新条件
+ * @access public
+ * @return mixed
+ */
+ public function getWhere()
+ {
+ $pk = $this->getPk();
+
+ if (is_string($pk) && isset($this->origin[$pk])) {
+ $where = [[$pk, '=', $this->origin[$pk]]];
+ $this->key = $this->origin[$pk];
+ } elseif (is_array($pk)) {
+ foreach ($pk as $field) {
+ if (isset($this->origin[$field])) {
+ $where[] = [$field, '=', $this->origin[$field]];
+ }
+ }
+ }
+
+ if (empty($where)) {
+ $where = empty($this->updateWhere) ? null : $this->updateWhere;
+ }
+
+ return $where;
+ }
+
+ /**
+ * 保存多个数据到当前数据对象
+ * @access public
+ * @param iterable $dataSet 数据
+ * @param boolean $replace 是否自动识别更新和写入
+ * @return Collection
+ * @throws \Exception
+ */
+ public function saveAll(iterable $dataSet, bool $replace = true): Collection
+ {
+ $db = $this->db();
+
+ $result = $db->transaction(function () use ($replace, $dataSet) {
+
+ $pk = $this->getPk();
+
+ if (is_string($pk) && $replace) {
+ $auto = true;
+ }
+
+ $result = [];
+
+ $suffix = $this->getSuffix();
+
+ foreach ($dataSet as $key => $data) {
+ if ($this->exists || (!empty($auto) && isset($data[$pk]))) {
+ $result[$key] = static::update($data, [], [], $suffix);
+ } else {
+ $result[$key] = static::create($data, $this->field, $this->replace, $suffix);
+ }
+ }
+
+ return $result;
+ });
+
+ return $this->toCollection($result);
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ if (!$this->exists || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
+ return false;
+ }
+
+ // 读取更新条件
+ $where = $this->getWhere();
+
+ $db = $this->db();
+
+ $db->transaction(function () use ($where, $db) {
+ // 删除当前模型数据
+ $db->where($where)->delete();
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+ });
+
+ $this->trigger('AfterDelete');
+
+ $this->exists = false;
+ $this->lazySave = false;
+
+ return true;
+ }
+
+ /**
+ * 写入数据
+ * @access public
+ * @param array $data 数据数组
+ * @param array $allowField 允许字段
+ * @param bool $replace 使用Replace
+ * @param string $suffix 数据表后缀
+ * @return static
+ */
+ public static function create(array $data, array $allowField = [], bool $replace = false, string $suffix = ''): Model
+ {
+ $model = new static();
+
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ if (!empty($suffix)) {
+ $model->setSuffix($suffix);
+ }
+
+ $model->replace($replace)->save($data);
+
+ return $model;
+ }
+
+ /**
+ * 更新数据
+ * @access public
+ * @param array $data 数据数组
+ * @param mixed $where 更新条件
+ * @param array $allowField 允许字段
+ * @param string $suffix 数据表后缀
+ * @return static
+ */
+ public static function update(array $data, $where = [], array $allowField = [], string $suffix = '')
+ {
+ $model = new static();
+
+ if (!empty($allowField)) {
+ $model->allowField($allowField);
+ }
+
+ if (!empty($where)) {
+ $model->setUpdateWhere($where);
+ }
+
+ if (!empty($suffix)) {
+ $model->setSuffix($suffix);
+ }
+
+ $model->exists(true)->save($data);
+
+ return $model;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 主键列表 支持闭包查询条件
+ * @param bool $force 是否强制删除
+ * @return bool
+ */
+ public static function destroy($data, bool $force = false): bool
+ {
+ if (empty($data) && 0 !== $data) {
+ return false;
+ }
+
+ $model = new static();
+
+ $query = $model->db();
+
+ if (is_array($data) && key($data) !== 0) {
+ $query->where($data);
+ $data = null;
+ } elseif ($data instanceof \Closure) {
+ $data($query);
+ $data = null;
+ }
+
+ $resultSet = $query->select($data);
+
+ foreach ($resultSet as $result) {
+ $result->force($force)->delete();
+ }
+
+ return true;
+ }
+
+ /**
+ * 解序列化后处理
+ */
+ public function __wakeup()
+ {
+ $this->initialize();
+ }
+
+ /**
+ * 修改器 设置数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @param mixed $value 值
+ * @return void
+ */
+ public function __set(string $name, $value): void
+ {
+ $this->setAttr($name, $value);
+ }
+
+ /**
+ * 获取器 获取数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ return $this->getAttr($name);
+ }
+
+ /**
+ * 检测数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ return !is_null($this->getAttr($name));
+ }
+
+ /**
+ * 销毁数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @return void
+ */
+ public function __unset(string $name): void
+ {
+ unset($this->data[$name],
+ $this->get[$name],
+ $this->relation[$name]);
+ }
+
+ // ArrayAccess
+ 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..d8d43d7
--- /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]', (string) $page, $this->options['path']);
+ }
+
+ if (count($this->options['query']) > 0) {
+ $parameters = array_merge($this->options['query'], $parameters);
+ }
+
+ $url = $path;
+ if (!empty($parameters)) {
+ $url .= '?' . http_build_query($parameters, '', '&');
+ }
+
+ return $url . $this->buildFragment();
+ }
+
+ /**
+ * 自动获取当前页码
+ * @access public
+ * @param string $varPage
+ * @param int $default
+ * @return int
+ */
+ public static function getCurrentPage(string $varPage = 'page', int $default = 1): int
+ {
+ if (isset(static::$currentPageResolver)) {
+ return call_user_func(static::$currentPageResolver, $varPage);
+ }
+
+ return $default;
+ }
+
+ /**
+ * 设置获取当前页码闭包
+ * @param Closure $resolver
+ */
+ public static function currentPageResolver(Closure $resolver)
+ {
+ static::$currentPageResolver = $resolver;
+ }
+
+ /**
+ * 自动获取当前的path
+ * @access public
+ * @param string $default
+ * @return string
+ */
+ public static function getCurrentPath($default = '/'): string
+ {
+ if (isset(static::$currentPathResolver)) {
+ return call_user_func(static::$currentPathResolver);
+ }
+
+ return $default;
+ }
+
+ /**
+ * 设置获取当前路径闭包
+ * @param Closure $resolver
+ */
+ public static function currentPathResolver(Closure $resolver)
+ {
+ static::$currentPathResolver = $resolver;
+ }
+
+ /**
+ * 获取数据总条数
+ * @return int
+ */
+ public function total(): int
+ {
+ if ($this->simple) {
+ throw new DomainException('not support total');
+ }
+
+ return $this->total;
+ }
+
+ /**
+ * 获取每页数量
+ * @return int
+ */
+ public function listRows(): int
+ {
+ return $this->listRows;
+ }
+
+ /**
+ * 获取当前页页码
+ * @return int
+ */
+ public function currentPage(): int
+ {
+ return $this->currentPage;
+ }
+
+ /**
+ * 获取最后一页页码
+ * @return int
+ */
+ public function lastPage(): int
+ {
+ if ($this->simple) {
+ throw new DomainException('not support last');
+ }
+
+ return $this->lastPage;
+ }
+
+ /**
+ * 数据是否足够分页
+ * @access public
+ * @return bool
+ */
+ public function hasPages(): bool
+ {
+ return !(1 == $this->currentPage && !$this->hasMore);
+ }
+
+ /**
+ * 创建一组分页链接
+ *
+ * @access public
+ * @param int $start
+ * @param int $end
+ * @return array
+ */
+ public function getUrlRange(int $start, int $end): array
+ {
+ $urls = [];
+
+ for ($page = $start; $page <= $end; $page++) {
+ $urls[$page] = $this->url($page);
+ }
+
+ return $urls;
+ }
+
+ /**
+ * 设置URL锚点
+ *
+ * @access public
+ * @param string|null $fragment
+ * @return $this
+ */
+ public function fragment(string $fragment = null)
+ {
+ $this->options['fragment'] = $fragment;
+
+ return $this;
+ }
+
+ /**
+ * 添加URL参数
+ *
+ * @access public
+ * @param array $append
+ * @return $this
+ */
+ public function appends(array $append)
+ {
+ foreach ($append as $k => $v) {
+ if ($k !== $this->options['var_page']) {
+ $this->options['query'][$k] = $v;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 构造锚点字符串
+ *
+ * @access public
+ * @return string
+ */
+ protected function buildFragment(): string
+ {
+ return $this->options['fragment'] ? '#' . $this->options['fragment'] : '';
+ }
+
+ /**
+ * 渲染分页html
+ * @access public
+ * @return mixed
+ */
+ abstract public function render();
+
+ public function items()
+ {
+ return $this->items->all();
+ }
+
+ /**
+ * 获取数据集
+ *
+ * @return Collection|\think\model\Collection
+ */
+ public function getCollection()
+ {
+ return $this->items;
+ }
+
+ 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..9754a26
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/BaseQuery.php
@@ -0,0 +1,1278 @@
+
+// +----------------------------------------------------------------------
+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|array $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column($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 = $total > 0 ? $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|array|static[]
+ * @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|static
+ * @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);
+ }
+
+ 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..1a1921f
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Builder.php
@@ -0,0 +1,1305 @@
+
+// +----------------------------------------------------------------------
+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 (empty($options['field']) || '*' == $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) : '';
+
+ [$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));
+ if (count($value) === 0) {
+ return 'IN' == $exp ? '0 = 1' : '1 = 1';
+ }
+ $array = [];
+
+ foreach ($value as $v) {
+ $name = $query->bindValue($v, $bindType);
+ $array[] = ':' . $name;
+ }
+
+ if (count($array) == 1) {
+ return $key . ('IN' == $exp ? ' = ' : ' <> ') . $array[0];
+ } else {
+ $value = implode(',', $array);
+ }
+ }
+
+ 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)) {
+ [$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 $k => $item) {
+ $val[$k] = $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 (empty($options['field']) || '*' == $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..839f384
--- /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..aa86ba8
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/Connection.php
@@ -0,0 +1,347 @@
+
+// +----------------------------------------------------------------------
+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)) {
+ $listen[] = function ($sql, $time, $master) {
+ if (0 === strpos($sql, 'CONNECT:')) {
+ $this->db->log($sql);
+ return;
+ }
+
+ // 记录SQL
+ if (is_bool($master)) {
+ // 分布式记录当前操作的主从
+ $master = $master ? 'master|' : 'slave|';
+ } else {
+ $master = '';
+ }
+
+ $this->db->log($sql . ' [ ' . $master . 'RunTime:' . $time . 's ]');
+ };
+ }
+
+ $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..18fe131
--- /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|array $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, $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..16caed2
--- /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..5e8a09a
--- /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..60679af
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/PDOConnection.php
@@ -0,0 +1,1793 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db;
+
+use Closure;
+use PDO;
+use PDOStatement;
+use think\db\exception\BindParamException;
+use think\db\exception\DbEventException;
+use think\db\exception\DbException;
+use think\db\exception\PDOException;
+use think\Model;
+
+/**
+ * 数据库连接基础类
+ * @property PDO[] $links
+ * @property PDO $linkID
+ * @property PDO $linkRead
+ * @property PDO $linkWrite
+ */
+abstract class PDOConnection extends Connection
+{
+ const PARAM_FLOAT = 21;
+
+ /**
+ * 数据库连接参数配置
+ * @var array
+ */
+ protected $config = [
+ // 数据库类型
+ 'type' => '',
+ // 服务器地址
+ 'hostname' => '',
+ // 数据库名
+ 'database' => '',
+ // 用户名
+ 'username' => '',
+ // 密码
+ 'password' => '',
+ // 端口
+ 'hostport' => '',
+ // 连接dsn
+ 'dsn' => '',
+ // 数据库连接参数
+ 'params' => [],
+ // 数据库编码默认采用utf8
+ 'charset' => 'utf8',
+ // 数据库表前缀
+ 'prefix' => '',
+ // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+ 'deploy' => 0,
+ // 数据库读写是否分离 主从式有效
+ 'rw_separate' => false,
+ // 读写分离后 主服务器数量
+ 'master_num' => 1,
+ // 指定从服务器序号
+ 'slave_no' => '',
+ // 模型写入后自动读取主服务器
+ 'read_master' => false,
+ // 是否严格检查字段是否存在
+ 'fields_strict' => true,
+ // 开启字段缓存
+ 'fields_cache' => false,
+ // 监听SQL
+ 'trigger_sql' => true,
+ // Builder类
+ 'builder' => '',
+ // Query类
+ 'query' => '',
+ // 是否需要断线重连
+ 'break_reconnect' => false,
+ // 断线标识字符串
+ 'break_match_str' => [],
+ ];
+
+ /**
+ * PDO操作实例
+ * @var PDOStatement
+ */
+ protected $PDOStatement;
+
+ /**
+ * 当前SQL指令
+ * @var string
+ */
+ protected $queryStr = '';
+
+ /**
+ * 事务指令数
+ * @var int
+ */
+ protected $transTimes = 0;
+
+ /**
+ * 重连次数
+ * @var int
+ */
+ protected $reConnectTimes = 0;
+
+ /**
+ * 查询结果类型
+ * @var int
+ */
+ protected $fetchType = PDO::FETCH_ASSOC;
+
+ /**
+ * 字段属性大小写
+ * @var int
+ */
+ protected $attrCase = PDO::CASE_LOWER;
+
+ /**
+ * 数据表信息
+ * @var array
+ */
+ protected $info = [];
+
+ /**
+ * 查询开始时间
+ * @var float
+ */
+ protected $queryStartTime;
+
+ /**
+ * PDO连接参数
+ * @var array
+ */
+ protected $params = [
+ PDO::ATTR_CASE => PDO::CASE_NATURAL,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
+ PDO::ATTR_STRINGIFY_FETCHES => false,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ];
+
+ /**
+ * 参数绑定类型映射
+ * @var array
+ */
+ protected $bindType = [
+ 'string' => PDO::PARAM_STR,
+ 'str' => PDO::PARAM_STR,
+ 'integer' => PDO::PARAM_INT,
+ 'int' => PDO::PARAM_INT,
+ 'boolean' => PDO::PARAM_BOOL,
+ 'bool' => PDO::PARAM_BOOL,
+ 'float' => self::PARAM_FLOAT,
+ 'datetime' => PDO::PARAM_STR,
+ 'timestamp' => PDO::PARAM_STR,
+ ];
+
+ /**
+ * 服务器断线标识字符
+ * @var array
+ */
+ protected $breakMatchStr = [
+ 'server has gone away',
+ 'no connection to the server',
+ 'Lost connection',
+ 'is dead or not enabled',
+ 'Error while sending',
+ 'decryption failed or bad record mac',
+ 'server closed the connection unexpectedly',
+ 'SSL connection has been closed unexpectedly',
+ 'Error writing data to the connection',
+ 'Resource deadlock avoided',
+ 'failed with errno',
+ 'child connection forced to terminate due to client_idle_limit',
+ 'query_wait_timeout',
+ 'reset by peer',
+ 'Physical connection is not usable',
+ 'TCP Provider: Error code 0x68',
+ 'ORA-03114',
+ 'Packets out of order. Expected',
+ 'Adaptive Server connection failed',
+ 'Communication link failure',
+ 'connection is no longer usable',
+ 'Login timeout expired',
+ 'SQLSTATE[HY000] [2002] Connection refused',
+ 'running with the --read-only option so it cannot execute this statement',
+ 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
+ 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
+ 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
+ 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
+ 'SQLSTATE[HY000] [2002] Connection timed out',
+ 'SSL: Connection timed out',
+ 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.',
+ ];
+
+ /**
+ * 绑定参数
+ * @var array
+ */
+ protected $bind = [];
+
+ /**
+ * 获取当前连接器类对应的Query类
+ * @access public
+ * @return string
+ */
+ public function getQueryClass(): string
+ {
+ return $this->getConfig('query') ?: Query::class;
+ }
+
+ /**
+ * 获取当前连接器类对应的Builder类
+ * @access public
+ * @return string
+ */
+ public function getBuilderClass(): string
+ {
+ return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
+ }
+
+ /**
+ * 解析pdo连接的dsn信息
+ * @access protected
+ * @param array $config 连接信息
+ * @return string
+ */
+ abstract protected function parseDsn(array $config): string;
+
+ /**
+ * 取得数据表的字段信息
+ * @access public
+ * @param string $tableName 数据表名称
+ * @return array
+ */
+ abstract public function getFields(string $tableName): array;
+
+ /**
+ * 取得数据库的表信息
+ * @access public
+ * @param string $dbName 数据库名称
+ * @return array
+ */
+ abstract public function getTables(string $dbName = ''): array;
+
+ /**
+ * 对返数据表字段信息进行大小写转换出来
+ * @access public
+ * @param array $info 字段信息
+ * @return array
+ */
+ public function fieldCase(array $info): array
+ {
+ // 字段大小写转换
+ switch ($this->attrCase) {
+ case PDO::CASE_LOWER:
+ $info = array_change_key_case($info);
+ break;
+ case PDO::CASE_UPPER:
+ $info = array_change_key_case($info, CASE_UPPER);
+ break;
+ case PDO::CASE_NATURAL:
+ default:
+ // 不做转换
+ }
+
+ return $info;
+ }
+
+ /**
+ * 获取字段类型
+ * @access protected
+ * @param string $type 字段类型
+ * @return string
+ */
+ protected function getFieldType(string $type): string
+ {
+ if (0 === 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 Model|null $model 模型对象实例
+ * @param null $condition 查询条件
+ * @return \Generator
+ * @throws DbException
+ */
+ public function getCursor(BaseQuery $query, string $sql, array $bind = [], $model = null, $condition = null)
+ {
+ $this->queryPDOStatement($query, $sql, $bind);
+
+ // 返回结果集
+ while ($result = $this->PDOStatement->fetch($this->fetchType)) {
+ if ($model) {
+ yield $model->newInstance($result, $condition);
+ } else {
+ yield $result;
+ }
+ }
+ }
+
+ /**
+ * 执行查询 返回数据集
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 主库读取
+ * @return array
+ * @throws DbException
+ */
+ public function query(string $sql, array $bind = [], bool $master = false): array
+ {
+ return $this->pdoQuery($this->newQuery(), $sql, $bind, $master);
+ }
+
+ /**
+ * 执行语句
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @return int
+ * @throws DbException
+ */
+ public function execute(string $sql, array $bind = []): int
+ {
+ return $this->pdoExecute($this->newQuery(), $sql, $bind, true);
+ }
+
+ /**
+ * 执行查询 返回数据集
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param mixed $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 主库读取
+ * @return array
+ * @throws DbException
+ */
+ protected function pdoQuery(BaseQuery $query, $sql, array $bind = [], bool $master = null): array
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ if ($query->getOptions('cache')) {
+ // 检查查询缓存
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+
+ $data = $this->cache->get($key);
+
+ if (null !== $data) {
+ return $data;
+ }
+ }
+
+ if ($sql instanceof Closure) {
+ $sql = $sql($query);
+ $bind = $query->getBind();
+ }
+
+ if (!isset($master)) {
+ $master = $query->getOptions('master') ? true : false;
+ }
+
+ $procedure = $query->getOptions('procedure') ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+
+ $this->getPDOStatement($sql, $bind, $master, $procedure);
+
+ $resultSet = $this->getResult($procedure);
+
+ if (isset($cacheItem) && $resultSet) {
+ // 缓存数据集
+ $cacheItem->set($resultSet);
+ $this->cacheData($cacheItem);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return \PDOStatement
+ * @throws DbException
+ */
+ public function pdo(BaseQuery $query): PDOStatement
+ {
+ $bind = $query->getBind();
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ return $this->queryPDOStatement($query, $sql, $bind);
+ }
+
+ /**
+ * 执行查询但只返回PDOStatement对象
+ * @access public
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $master 是否在主服务器读操作
+ * @param bool $procedure 是否为存储过程调用
+ * @return PDOStatement
+ * @throws DbException
+ */
+ public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
+ {
+ try {
+ $this->initConnect($this->readMaster ?: $master);
+ // 记录SQL语句
+ $this->queryStr = $sql;
+ $this->bind = $bind;
+
+ $this->db->updateQueryTimes();
+ $this->queryStartTime = microtime(true);
+
+ // 预处理
+ $this->PDOStatement = $this->linkID->prepare($sql);
+
+ // 参数绑定
+ if ($procedure) {
+ $this->bindParam($bind);
+ } else {
+ $this->bindValue($bind);
+ }
+
+ // 执行查询
+ $this->PDOStatement->execute();
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+
+ $this->reConnectTimes = 0;
+
+ return $this->PDOStatement;
+ } catch (\Throwable | \Exception $e) {
+ if ($this->transTimes > 0) {
+ // 事务活动中时不应该进行重试,应直接中断执行,防止造成污染。
+ if ($this->isBreak($e)) {
+ // 尝试对事务计数进行重置
+ $this->transTimes = 0;
+ }
+ } else {
+ if ($this->reConnectTimes < 4 && $this->isBreak($e)) {
+ ++$this->reConnectTimes;
+ return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
+ }
+ }
+
+ if ($e instanceof \PDOException) {
+ throw new PDOException($e, $this->config, $this->getLastsql());
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * 执行语句
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param string $sql sql指令
+ * @param array $bind 参数绑定
+ * @param bool $origin 是否原生查询
+ * @return int
+ * @throws DbException
+ */
+ protected function pdoExecute(BaseQuery $query, string $sql, array $bind = [], bool $origin = false): int
+ {
+ if ($origin) {
+ $query->parseOptions();
+ }
+
+ $this->queryPDOStatement($query->master(true), $sql, $bind);
+
+ if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
+ $this->readMaster = true;
+ }
+
+ $this->numRows = $this->PDOStatement->rowCount();
+
+ if ($query->getOptions('cache')) {
+ // 清理缓存数据
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+ $tag = $cacheItem->getTag();
+
+ if (isset($key) && $this->cache->has($key)) {
+ $this->cache->delete($key);
+ } elseif (!empty($tag) && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($tag)->clear();
+ }
+ }
+
+ return $this->numRows;
+ }
+
+ /**
+ * @param BaseQuery $query
+ * @param string $sql
+ * @param array $bind
+ * @return PDOStatement
+ * @throws DbException
+ */
+ protected function queryPDOStatement(BaseQuery $query, string $sql, array $bind = []): PDOStatement
+ {
+ $options = $query->getOptions();
+ $master = !empty($options['master']) ? true : false;
+ $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+
+ return $this->getPDOStatement($sql, $bind, $master, $procedure);
+ }
+
+ /**
+ * 查找单条记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ * @throws DbException
+ */
+ public function find(BaseQuery $query): array
+ {
+ // 事件回调
+ try {
+ $this->db->trigger('before_find', $query);
+ } catch (DbEventException $e) {
+ return [];
+ }
+
+ // 执行查询
+ $resultSet = $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query, true);
+ });
+
+ return $resultSet[0] ?? [];
+ }
+
+ /**
+ * 使用游标查询记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return \Generator
+ */
+ public function cursor(BaseQuery $query)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ $condition = $options['where']['AND'] ?? null;
+
+ // 执行查询操作
+ return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
+ }
+
+ /**
+ * 查找记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ * @throws DbException
+ */
+ public function select(BaseQuery $query): array
+ {
+ try {
+ $this->db->trigger('before_select', $query);
+ } catch (DbEventException $e) {
+ return [];
+ }
+
+ // 执行查询操作
+ return $this->pdoQuery($query, function ($query) {
+ return $this->builder->select($query);
+ });
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
+ */
+ public function insert(BaseQuery $query, bool $getLastInsID = false)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ // 生成SQL语句
+ $sql = $this->builder->insert($query);
+
+ // 执行操作
+ $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $sequence = $options['sequence'] ?? null;
+ $lastInsId = $this->getLastInsID($query, $sequence);
+
+ $data = $options['data'];
+
+ if ($lastInsId) {
+ $pk = $query->getAutoInc();
+ if ($pk) {
+ $data[$pk] = $lastInsId;
+ }
+ }
+
+ $query->setOption('data', $data);
+
+ $this->db->trigger('after_insert', $query);
+
+ if ($getLastInsID && $lastInsId) {
+ return $lastInsId;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param mixed $dataSet 数据集
+ * @param integer $limit 每次写入数据限制
+ * @return integer
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function insertAll(BaseQuery $query, array $dataSet = [], int $limit = 0): int
+ {
+ if (!is_array(reset($dataSet))) {
+ return 0;
+ }
+
+ $options = $query->parseOptions();
+ $replace = !empty($options['replace']);
+
+ if (0 === $limit && count($dataSet) >= 5000) {
+ $limit = 1000;
+ }
+
+ if ($limit) {
+ // 分批写入 自动启动事务支持
+ $this->startTrans();
+
+ try {
+ $array = array_chunk($dataSet, $limit, true);
+ $count = 0;
+
+ foreach ($array as $item) {
+ $sql = $this->builder->insertAll($query, $item, $replace);
+ $count += $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ // 提交事务
+ $this->commit();
+ } catch (\Exception | \Throwable $e) {
+ $this->rollback();
+ throw $e;
+ }
+
+ return $count;
+ }
+
+ $sql = $this->builder->insertAll($query, $dataSet, $replace);
+
+ return $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ /**
+ * 通过Select方式插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param array $fields 要插入的数据表字段名
+ * @param string $table 要插入的数据表名
+ * @return integer
+ * @throws PDOException
+ */
+ public function selectInsert(BaseQuery $query, array $fields, string $table): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ $sql = $this->builder->selectInsert($query, $fields, $table);
+
+ return $this->pdoExecute($query, $sql, $query->getBind());
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return integer
+ * @throws PDOException
+ */
+ public function update(BaseQuery $query): int
+ {
+ $query->parseOptions();
+
+ // 生成UPDATE SQL语句
+ $sql = $this->builder->update($query);
+
+ // 执行操作
+ $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $this->db->trigger('after_update', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ * @throws PDOException
+ */
+ public function delete(BaseQuery $query): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ // 生成删除SQL语句
+ $sql = $this->builder->delete($query);
+
+ // 执行操作
+ $result = $this->pdoExecute($query, $sql, $query->getBind());
+
+ if ($result) {
+ $this->db->trigger('after_delete', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $field 字段名
+ * @param mixed $default 默认值
+ * @param bool $one 返回一个值
+ * @return mixed
+ */
+ public function value(BaseQuery $query, string $field, $default = null, bool $one = true)
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['field'])) {
+ $query->removeOption('field');
+ }
+
+ if (isset($options['group'])) {
+ $query->group('');
+ }
+
+ $query->setOption('field', (array) $field);
+
+ if (!empty($options['cache'])) {
+ $cacheItem = $this->parseCache($query, $options['cache'], 'value');
+ $key = $cacheItem->getKey();
+
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query, $one);
+
+ if (isset($options['field'])) {
+ $query->setOption('field', $options['field']);
+ } else {
+ $query->removeOption('field');
+ }
+
+ if (isset($options['group'])) {
+ $query->setOption('group', $options['group']);
+ }
+
+ // 执行查询操作
+ $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
+
+ $result = $pdo->fetchColumn();
+
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
+ }
+
+ return false !== $result ? $result : $default;
+ }
+
+ /**
+ * 得到某个字段的值
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $aggregate 聚合方法
+ * @param mixed $field 字段名
+ * @param bool $force 强制转为数字类型
+ * @return mixed
+ */
+ public function aggregate(BaseQuery $query, string $aggregate, $field, bool $force = false)
+ {
+ if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
+ [$distinct, $field] = explode(' ', $field);
+ }
+
+ $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS think_' . strtolower($aggregate);
+
+ $result = $this->value($query, $field, 0);
+
+ return $force ? (float) $result : $result;
+ }
+
+ /**
+ * 得到某个列的数组
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string|array $column 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, $column, string $key = ''): array
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['field'])) {
+ $query->removeOption('field');
+ }
+
+ if (empty($key) || trim($key) === '') {
+ $key = null;
+ }
+
+ if (\is_string($column)) {
+ $column = \trim($column);
+ if ('*' !== $column) {
+ $column = \array_map('\trim', \explode(',', $column));
+ }
+ } elseif (\is_array($column)) {
+ if (\in_array('*', $column)) {
+ $column = '*';
+ }
+ } else {
+ throw new DbException('not support type');
+ }
+
+ $field = $column;
+ if ('*' !== $column && $key && !\in_array($key, $column)) {
+ $field[] = $key;
+ }
+
+ $query->setOption('field', $field);
+
+ if (!empty($options['cache'])) {
+ // 判断查询缓存
+ $cacheItem = $this->parseCache($query, $options['cache'], 'column');
+ $name = $cacheItem->getKey();
+
+ if ($this->cache->has($name)) {
+ return $this->cache->get($name);
+ }
+ }
+
+ // 生成查询SQL
+ $sql = $this->builder->select($query);
+
+ if (isset($options['field'])) {
+ $query->setOption('field', $options['field']);
+ } else {
+ $query->removeOption('field');
+ }
+
+ // 执行查询操作
+ $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
+ $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
+
+ if (is_string($key) && strpos($key, '.')) {
+ [$alias, $key] = explode('.', $key);
+ }
+
+ if (empty($resultSet)) {
+ $result = [];
+ } elseif ('*' !== $column && \count($column) === 1) {
+ $column = \array_shift($column);
+ if (\strpos($column, ' ')) {
+ $column = \substr(\strrchr(\trim($column), ' '), 1);
+ }
+
+ if (\strpos($column, '.')) {
+ [$alias, $column] = \explode('.', $column);
+ }
+
+ $result = \array_column($resultSet, $column, $key);
+ } elseif ($key) {
+ $result = \array_column($resultSet, null, $key);
+ } else {
+ $result = $resultSet;
+ }
+
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 根据参数绑定组装最终的SQL语句 便于调试
+ * @access public
+ * @param string $sql 带参数绑定的sql语句
+ * @param array $bind 参数绑定列表
+ * @return string
+ */
+ public function getRealSql(string $sql, array $bind = []): string
+ {
+ foreach ($bind as $key => $val) {
+ $value = strval(is_array($val) ? $val[0] : $val);
+ $type = is_array($val) ? $val[1] : PDO::PARAM_STR;
+
+ if (self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) {
+ $value = '\'' . addslashes($value) . '\'';
+ } elseif (PDO::PARAM_INT == $type && '' === $value) {
+ $value = '0';
+ }
+
+ // 判断占位符
+ $sql = is_numeric($key) ?
+ substr_replace($sql, $value, strpos($sql, '?'), 1) :
+ substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
+ }
+
+ return rtrim($sql);
+ }
+
+ /**
+ * 参数绑定
+ * 支持 ['name'=>'value','id'=>123] 对应命名占位符
+ * 或者 ['value',123] 对应问号占位符
+ * @access public
+ * @param array $bind 要绑定的参数列表
+ * @return void
+ * @throws BindParamException
+ */
+ protected function bindValue(array $bind = []): void
+ {
+ foreach ($bind as $key => $val) {
+ // 占位符
+ $param = is_numeric($key) ? $key + 1 : ':' . $key;
+
+ if (is_array($val)) {
+ if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
+ $val[0] = 0;
+ } elseif (self::PARAM_FLOAT == $val[1]) {
+ $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0];
+ $val[1] = PDO::PARAM_STR;
+ }
+
+ $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
+ } else {
+ $result = $this->PDOStatement->bindValue($param, $val);
+ }
+
+ if (!$result) {
+ throw new BindParamException(
+ "Error occurred when binding parameters '{$param}'",
+ $this->config,
+ $this->getLastsql(),
+ $bind
+ );
+ }
+ }
+ }
+
+ /**
+ * 存储过程的输入输出参数绑定
+ * @access public
+ * @param array $bind 要绑定的参数列表
+ * @return void
+ * @throws BindParamException
+ */
+ protected function bindParam(array $bind): void
+ {
+ foreach ($bind as $key => $val) {
+ $param = is_numeric($key) ? $key + 1 : ':' . $key;
+
+ if (is_array($val)) {
+ array_unshift($val, $param);
+ $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
+ } else {
+ $result = $this->PDOStatement->bindValue($param, $val);
+ }
+
+ if (!$result) {
+ $param = array_shift($val);
+
+ throw new BindParamException(
+ "Error occurred when binding parameters '{$param}'",
+ $this->config,
+ $this->getLastsql(),
+ $bind
+ );
+ }
+ }
+ }
+
+ /**
+ * 获得数据集数组
+ * @access protected
+ * @param bool $procedure 是否存储过程
+ * @return array
+ */
+ protected function getResult(bool $procedure = false): array
+ {
+ if ($procedure) {
+ // 存储过程返回结果
+ return $this->procedure();
+ }
+
+ $result = $this->PDOStatement->fetchAll($this->fetchType);
+
+ $this->numRows = count($result);
+
+ return $result;
+ }
+
+ /**
+ * 获得存储过程数据集
+ * @access protected
+ * @return array
+ */
+ protected function procedure(): array
+ {
+ $item = [];
+
+ do {
+ $result = $this->getResult();
+ if (!empty($result)) {
+ $item[] = $result;
+ }
+ } while ($this->PDOStatement->nextRowset());
+
+ $this->numRows = count($item);
+
+ return $item;
+ }
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transaction(callable $callback)
+ {
+ $this->startTrans();
+
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = $callback($this);
+ }
+
+ $this->commit();
+ return $result;
+ } catch (\Exception | \Throwable $e) {
+ $this->rollback();
+ throw $e;
+ }
+ }
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ * @throws \PDOException
+ * @throws \Exception
+ */
+ public function startTrans(): void
+ {
+ try {
+ $this->initConnect(true);
+
+ ++$this->transTimes;
+
+ if (1 == $this->transTimes) {
+ $this->linkID->beginTransaction();
+ } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
+ $this->linkID->exec(
+ $this->parseSavepoint('trans' . $this->transTimes)
+ );
+ }
+ $this->reConnectTimes = 0;
+ } catch (\Throwable | \Exception $e) {
+ if (1 === $this->transTimes && $this->reConnectTimes < 4 && $this->isBreak($e)) {
+ --$this->transTimes;
+ ++$this->reConnectTimes;
+ $this->close()->startTrans();
+ } else {
+ if ($this->isBreak($e)) {
+ // 尝试对事务计数进行重置
+ $this->transTimes = 0;
+ }
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws \PDOException
+ */
+ public function commit(): void
+ {
+ $this->initConnect(true);
+
+ if (1 == $this->transTimes) {
+ $this->linkID->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->transTimes = 0;
+
+ $this->free();
+
+ return $this;
+ }
+
+ /**
+ * 是否断线
+ * @access protected
+ * @param \PDOException|\Exception $e 异常对象
+ * @return bool
+ */
+ protected function isBreak($e): bool
+ {
+ if (!$this->config['break_reconnect']) {
+ return false;
+ }
+
+ $error = $e->getMessage();
+
+ foreach ($this->breakMatchStr as $msg) {
+ if (false !== stripos($error, $msg)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取最近一次查询的sql语句
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string
+ {
+ return $this->getRealSql($this->queryStr, $this->bind);
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $sequence 自增序列名
+ * @return mixed
+ */
+ public function getLastInsID(BaseQuery $query, string $sequence = null)
+ {
+ try {
+ $insertId = $this->linkID->lastInsertId($sequence);
+ } catch (\Exception $e) {
+ $insertId = '';
+ }
+
+ return $this->autoInsIDType($query, $insertId);
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string $insertId 自增ID
+ * @return mixed
+ */
+ protected function autoInsIDType(BaseQuery $query, string $insertId)
+ {
+ $pk = $query->getAutoInc();
+
+ if ($pk) {
+ $type = $this->getFieldBindType($pk);
+
+ if (PDO::PARAM_INT == $type) {
+ $insertId = (int) $insertId;
+ } elseif (self::PARAM_FLOAT == $type) {
+ $insertId = (float) $insertId;
+ }
+ }
+
+ return $insertId;
+ }
+
+ /**
+ * 获取最近的错误信息
+ * @access public
+ * @return string
+ */
+ public function getError(): string
+ {
+ if ($this->PDOStatement) {
+ $error = $this->PDOStatement->errorInfo();
+ $error = $error[1] . ':' . $error[2];
+ } else {
+ $error = '';
+ }
+
+ if ('' != $this->queryStr) {
+ $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
+ }
+
+ return $error;
+ }
+
+ /**
+ * 初始化数据库连接
+ * @access protected
+ * @param boolean $master 是否主服务器
+ * @return void
+ */
+ protected function initConnect(bool $master = true): void
+ {
+ if (!empty($this->config['deploy'])) {
+ // 采用分布式数据库
+ if ($master || $this->transTimes) {
+ if (!$this->linkWrite) {
+ $this->linkWrite = $this->multiConnect(true);
+ }
+
+ $this->linkID = $this->linkWrite;
+ } else {
+ if (!$this->linkRead) {
+ $this->linkRead = $this->multiConnect(false);
+ }
+
+ $this->linkID = $this->linkRead;
+ }
+ } elseif (!$this->linkID) {
+ // 默认单数据库
+ $this->linkID = $this->connect();
+ }
+ }
+
+ /**
+ * 连接分布式服务器
+ * @access protected
+ * @param boolean $master 主服务器
+ * @return PDO
+ */
+ protected function multiConnect(bool $master = false): PDO
+ {
+ $config = [];
+
+ // 分布式数据库配置解析
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
+ }
+
+ // 主服务器序号
+ $m = floor(mt_rand(0, $this->config['master_num'] - 1));
+
+ if ($this->config['rw_separate']) {
+ // 主从式采用读写分离
+ if ($master) // 主服务器写入
+ {
+ $r = $m;
+ } elseif (is_numeric($this->config['slave_no'])) {
+ // 指定服务器读
+ $r = $this->config['slave_no'];
+ } else {
+ // 读操作连接从服务器 每次随机连接的数据库
+ $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
+ }
+ } else {
+ // 读写操作不区分服务器 每次随机连接的数据库
+ $r = floor(mt_rand(0, count($config['hostname']) - 1));
+ }
+ $dbMaster = false;
+
+ if ($m != $r) {
+ $dbMaster = [];
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
+ }
+ }
+
+ $dbConfig = [];
+
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+ $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
+ }
+
+ return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
+ }
+
+ /**
+ * 启动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..aa96123
--- /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..833fbf0
--- /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..0880460
--- /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..823156b
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Mongo.php
@@ -0,0 +1,675 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\db\builder;
+
+use MongoDB\BSON\Javascript;
+use MongoDB\BSON\ObjectID;
+use MongoDB\BSON\Regex;
+use MongoDB\Driver\BulkWrite;
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Exception\InvalidArgumentException;
+use MongoDB\Driver\Query as MongoQuery;
+use think\db\connector\Mongo as Connection;
+use think\db\exception\DbException as Exception;
+use think\db\Mongo as Query;
+
+class Mongo
+{
+ // connection对象实例
+ protected $connection;
+ // 最后插入ID
+ protected $insertId = [];
+ // 查询表达式
+ protected $exp = ['<>' => 'ne', '=' => 'eq', '>' => 'gt', '>=' => 'gte', '<' => 'lt', '<=' => 'lte', 'in' => 'in', 'not in' => 'nin', 'nin' => 'nin', 'mod' => 'mod', 'exists' => 'exists', 'null' => 'null', 'notnull' => 'not null', 'not null' => 'not null', 'regex' => 'regex', 'type' => 'type', 'all' => 'all', '> time' => '> time', '< time' => '< time', 'between' => 'between', 'not between' => 'not between', 'between time' => 'between time', 'not between time' => 'not between time', 'notbetween time' => 'not between time', 'like' => 'like', 'near' => 'near', 'size' => 'size'];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Connection $connection 数据库连接对象实例
+ */
+ public function __construct(Connection $connection)
+ {
+ $this->connection = $connection;
+ }
+
+ /**
+ * 获取当前的连接对象实例
+ * @access public
+ * @return Connection
+ */
+ public function getConnection(): Connection
+ {
+ return $this->connection;
+ }
+
+ /**
+ * key分析
+ * @access protected
+ * @param string $key
+ * @return string
+ */
+ protected function parseKey(Query $query, string $key): string
+ {
+ if (0 === strpos($key, '__TABLE__.')) {
+ [$collection, $key] = explode('.', $key, 2);
+ }
+
+ if ('id' == $key && $this->connection->getConfig('pk_convert_id')) {
+ $key = '_id';
+ }
+
+ return trim($key);
+ }
+
+ /**
+ * value分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $value
+ * @param string $field
+ * @return string
+ */
+ protected function parseValue(Query $query, $value, $field = '')
+ {
+ if ('_id' == $field && 'ObjectID' == $this->connection->getConfig('pk_type') && is_string($value)) {
+ try {
+ return new ObjectID($value);
+ } catch (InvalidArgumentException $e) {
+ return new ObjectID();
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * insert数据分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $data 数据
+ * @return array
+ */
+ protected function parseData(Query $query, array $data): array
+ {
+ if (empty($data)) {
+ return [];
+ }
+
+ $result = [];
+
+ foreach ($data as $key => $val) {
+ $item = $this->parseKey($query, $key);
+
+ if (is_object($val)) {
+ $result[$item] = $val;
+ } elseif (isset($val[0]) && 'exp' == $val[0]) {
+ $result[$item] = $val[1];
+ } elseif (is_null($val)) {
+ $result[$item] = 'NULL';
+ } else {
+ $result[$item] = $this->parseValue($query, $val, $key);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Set数据分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param array $data 数据
+ * @return array
+ */
+ protected function parseSet(Query $query, array $data): array
+ {
+ if (empty($data)) {
+ return [];
+ }
+
+ $result = [];
+
+ foreach ($data as $key => $val) {
+ $item = $this->parseKey($query, $key);
+
+ if (is_array($val) && isset($val[0]) && is_string($val[0]) && 0 === strpos($val[0], '$')) {
+ $result[$val[0]][$item] = $this->parseValue($query, $val[1], $key);
+ } else {
+ $result['$set'][$item] = $this->parseValue($query, $val, $key);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 生成查询过滤条件
+ * @access public
+ * @param Query $query 查询对象
+ * @param mixed $where
+ * @return array
+ */
+ public function parseWhere(Query $query, array $where): array
+ {
+ if (empty($where)) {
+ $where = [];
+ }
+
+ $filter = [];
+ foreach ($where as $logic => $val) {
+ $logic = '$' . strtolower($logic);
+ foreach ($val as $field => $value) {
+ if (is_array($value)) {
+ if (key($value) !== 0) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
+ $field = array_shift($value);
+ } elseif (!($value instanceof \Closure)) {
+ throw new Exception('where express error:' . var_export($value, true));
+ }
+
+ if ($value instanceof \Closure) {
+ // 使用闭包查询
+ $query = new Query($this->connection);
+ call_user_func_array($value, [ & $query]);
+ $filter[$logic][] = $this->parseWhere($query, $query->getOptions('where'));
+ } else {
+ if (strpos($field, '|')) {
+ // 不同字段使用相同查询条件(OR)
+ $array = explode('|', $field);
+ foreach ($array as $k) {
+ $filter['$or'][] = $this->parseWhereItem($query, $k, $value);
+ }
+ } elseif (strpos($field, '&')) {
+ // 不同字段使用相同查询条件(AND)
+ $array = explode('&', $field);
+ foreach ($array as $k) {
+ $filter['$and'][] = $this->parseWhereItem($query, $k, $value);
+ }
+ } else {
+ // 对字段使用表达式查询
+ $field = is_string($field) ? $field : '';
+ $filter[$logic][] = $this->parseWhereItem($query, $field, $value);
+ }
+ }
+ }
+ }
+
+ $options = $query->getOptions();
+ if (!empty($options['soft_delete'])) {
+ // 附加软删除条件
+ [$field, $condition] = $options['soft_delete'];
+ $filter['$and'][] = $this->parseWhereItem($query, $field, $condition);
+ }
+
+ return $filter;
+ }
+
+ // where子单元分析
+ protected function parseWhereItem(Query $query, $field, $val): array
+ {
+ $key = $field ? $this->parseKey($query, $field) : '';
+ // 查询规则和条件
+ if (!is_array($val)) {
+ $val = ['=', $val];
+ }
+ [$exp, $value] = $val;
+
+ // 对一个字段使用多个查询条件
+ if (is_array($exp)) {
+ $data = [];
+ foreach ($val as $value) {
+ $exp = $value[0];
+ $value = $value[1];
+ if (!in_array($exp, $this->exp)) {
+ $exp = strtolower($exp);
+ if (isset($this->exp[$exp])) {
+ $exp = $this->exp[$exp];
+ }
+ }
+ $k = '$' . $exp;
+ $data[$k] = $value;
+ }
+ $result[$key] = $data;
+ return $result;
+ } elseif (!in_array($exp, $this->exp)) {
+ $exp = strtolower($exp);
+ if (isset($this->exp[$exp])) {
+ $exp = $this->exp[$exp];
+ } else {
+ throw new Exception('where express error:' . $exp);
+ }
+ }
+
+ $result = [];
+ if ('=' == $exp) {
+ // 普通查询
+ $result[$key] = $this->parseValue($query, $value, $key);
+ } elseif (in_array($exp, ['neq', 'ne', 'gt', 'egt', 'gte', 'lt', 'lte', 'elt', 'mod'])) {
+ // 比较运算
+ $k = '$' . $exp;
+ $result[$key] = [$k => $this->parseValue($query, $value, $key)];
+ } elseif ('null' == $exp) {
+ // NULL 查询
+ $result[$key] = null;
+ } elseif ('not null' == $exp) {
+ $result[$key] = ['$ne' => null];
+ } elseif ('all' == $exp) {
+ // 满足所有指定条件
+ $result[$key] = ['$all', $this->parseValue($query, $value, $key)];
+ } elseif ('between' == $exp) {
+ // 区间查询
+ $value = is_array($value) ? $value : explode(',', $value);
+ $result[$key] = ['$gte' => $this->parseValue($query, $value[0], $key), '$lte' => $this->parseValue($query, $value[1], $key)];
+ } elseif ('not between' == $exp) {
+ // 范围查询
+ $value = is_array($value) ? $value : explode(',', $value);
+ $result[$key] = ['$lt' => $this->parseValue($query, $value[0], $key), '$gt' => $this->parseValue($query, $value[1], $key)];
+ } elseif ('exists' == $exp) {
+ // 字段是否存在
+ $result[$key] = ['$exists' => (bool) $value];
+ } elseif ('type' == $exp) {
+ // 类型查询
+ $result[$key] = ['$type' => intval($value)];
+ } elseif ('exp' == $exp) {
+ // 表达式查询
+ $result['$where'] = $value instanceof Javascript ? $value : new Javascript($value);
+ } elseif ('like' == $exp) {
+ // 模糊查询 采用正则方式
+ $result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i');
+ } elseif (in_array($exp, ['nin', 'in'])) {
+ // IN 查询
+ $value = is_array($value) ? $value : explode(',', $value);
+ foreach ($value as $k => $val) {
+ $value[$k] = $this->parseValue($query, $val, $key);
+ }
+ $result[$key] = ['$' . $exp => $value];
+ } elseif ('regex' == $exp) {
+ $result[$key] = $value instanceof Regex ? $value : new Regex($value, 'i');
+ } elseif ('< time' == $exp) {
+ $result[$key] = ['$lt' => $this->parseDateTime($query, $value, $field)];
+ } elseif ('> time' == $exp) {
+ $result[$key] = ['$gt' => $this->parseDateTime($query, $value, $field)];
+ } elseif ('between time' == $exp) {
+ // 区间查询
+ $value = is_array($value) ? $value : explode(',', $value);
+ $result[$key] = ['$gte' => $this->parseDateTime($query, $value[0], $field), '$lte' => $this->parseDateTime($query, $value[1], $field)];
+ } elseif ('not between time' == $exp) {
+ // 范围查询
+ $value = is_array($value) ? $value : explode(',', $value);
+ $result[$key] = ['$lt' => $this->parseDateTime($query, $value[0], $field), '$gt' => $this->parseDateTime($query, $value[1], $field)];
+ } elseif ('near' == $exp) {
+ // 经纬度查询
+ $result[$key] = ['$near' => $this->parseValue($query, $value, $key)];
+ } elseif ('size' == $exp) {
+ // 元素长度查询
+ $result[$key] = ['$size' => intval($value)];
+ } else {
+ // 普通查询
+ $result[$key] = $this->parseValue($query, $value, $key);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 日期时间条件解析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $value
+ * @param string $key
+ * @return string
+ */
+ protected function parseDateTime(Query $query, $value, $key)
+ {
+ // 获取时间字段类型
+ $type = $query->getFieldType($key);
+
+ if ($type) {
+ if (is_string($value)) {
+ $value = strtotime($value) ?: $value;
+ }
+
+ if (is_int($value)) {
+ if (preg_match('/(datetime|timestamp)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d H:i:s', $value);
+ } elseif (preg_match('/(date)/is', $type)) {
+ // 日期及时间戳类型
+ $value = date('Y-m-d', $value);
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取最后写入的ID 如果是insertAll方法的话 返回所有写入的ID
+ * @access public
+ * @return mixed
+ */
+ public function getLastInsID()
+ {
+ return $this->insertId;
+ }
+
+ /**
+ * 生成insert BulkWrite对象
+ * @access public
+ * @param Query $query 查询对象
+ * @return BulkWrite
+ */
+ public function insert(Query $query): BulkWrite
+ {
+ // 分析并处理数据
+ $options = $query->getOptions();
+
+ $data = $this->parseData($query, $options['data']);
+
+ $bulk = new BulkWrite;
+
+ if ($insertId = $bulk->insert($data)) {
+ $this->insertId = $insertId;
+ }
+
+ $this->log('insert', $data, $options);
+
+ return $bulk;
+ }
+
+ /**
+ * 生成insertall BulkWrite对象
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $dataSet 数据集
+ * @return BulkWrite
+ */
+ public function insertAll(Query $query, array $dataSet): BulkWrite
+ {
+ $bulk = new BulkWrite;
+ $options = $query->getOptions();
+
+ $this->insertId = [];
+ foreach ($dataSet as $data) {
+ // 分析并处理数据
+ $data = $this->parseData($query, $data);
+ if ($insertId = $bulk->insert($data)) {
+ $this->insertId[] = $insertId;
+ }
+ }
+
+ $this->log('insert', $dataSet, $options);
+
+ return $bulk;
+ }
+
+ /**
+ * 生成update BulkWrite对象
+ * @access public
+ * @param Query $query 查询对象
+ * @return BulkWrite
+ */
+ public function update(Query $query): BulkWrite
+ {
+ $options = $query->getOptions();
+
+ $data = $this->parseSet($query, $options['data']);
+ $where = $this->parseWhere($query, $options['where']);
+
+ if (1 == $options['limit']) {
+ $updateOptions = ['multi' => false];
+ } else {
+ $updateOptions = ['multi' => true];
+ }
+
+ $bulk = new BulkWrite;
+
+ $bulk->update($where, $data, $updateOptions);
+
+ $this->log('update', $data, $where);
+
+ return $bulk;
+ }
+
+ /**
+ * 生成delete BulkWrite对象
+ * @access public
+ * @param Query $query 查询对象
+ * @return BulkWrite
+ */
+ public function delete(Query $query): BulkWrite
+ {
+ $options = $query->getOptions();
+ $where = $this->parseWhere($query, $options['where']);
+
+ $bulk = new BulkWrite;
+
+ if (1 == $options['limit']) {
+ $deleteOptions = ['limit' => 1];
+ } else {
+ $deleteOptions = ['limit' => 0];
+ }
+
+ $bulk->delete($where, $deleteOptions);
+
+ $this->log('remove', $where, $deleteOptions);
+
+ return $bulk;
+ }
+
+ /**
+ * 生成Mongo查询对象
+ * @access public
+ * @param Query $query 查询对象
+ * @param bool $one 是否仅获取一个记录
+ * @return MongoQuery
+ */
+ public function select(Query $query, bool $one = false): MongoQuery
+ {
+ $options = $query->getOptions();
+
+ $where = $this->parseWhere($query, $options['where']);
+
+ if ($one) {
+ $options['limit'] = 1;
+ }
+
+ $query = new MongoQuery($where, $options);
+
+ $this->log('find', $where, $options);
+
+ return $query;
+ }
+
+ /**
+ * 生成Count命令
+ * @access public
+ * @param Query $query 查询对象
+ * @return Command
+ */
+ public function count(Query $query): Command
+ {
+ $options = $query->getOptions();
+
+ $cmd['count'] = $options['table'];
+ $cmd['query'] = (object) $this->parseWhere($query, $options['where']);
+
+ foreach (['hint', 'limit', 'maxTimeMS', 'skip'] as $option) {
+ if (isset($options[$option])) {
+ $cmd[$option] = $options[$option];
+ }
+ }
+
+ $command = new Command($cmd);
+ $this->log('cmd', 'count', $cmd);
+
+ return $command;
+ }
+
+ /**
+ * 聚合查询命令
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $extra 指令和字段
+ * @return Command
+ */
+ public function aggregate(Query $query, array $extra): Command
+ {
+ $options = $query->getOptions();
+ [$fun, $field] = $extra;
+
+ if ('id' == $field && $this->connection->getConfig('pk_convert_id')) {
+ $field = '_id';
+ }
+
+ $group = isset($options['group']) ? '$' . $options['group'] : null;
+
+ $pipeline = [
+ ['$match' => (object) $this->parseWhere($query, $options['where'])],
+ ['$group' => ['_id' => $group, 'aggregate' => ['$' . $fun => '$' . $field]]],
+ ];
+
+ $cmd = [
+ 'aggregate' => $options['table'],
+ 'allowDiskUse' => true,
+ 'pipeline' => $pipeline,
+ 'cursor' => new \stdClass,
+ ];
+
+ foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) {
+ if (isset($options[$option])) {
+ $cmd[$option] = $options[$option];
+ }
+ }
+
+ $command = new Command($cmd);
+
+ $this->log('aggregate', $cmd);
+
+ return $command;
+ }
+
+ /**
+ * 多聚合查询命令, 可以对多个字段进行 group by 操作
+ *
+ * @param Query $query 查询对象
+ * @param array $extra 指令和字段
+ * @return Command
+ */
+ public function multiAggregate(Query $query, $extra): Command
+ {
+ $options = $query->getOptions();
+
+ [$aggregate, $groupBy] = $extra;
+
+ $groups = ['_id' => []];
+
+ foreach ($groupBy as $field) {
+ $groups['_id'][$field] = '$' . $field;
+ }
+
+ foreach ($aggregate as $fun => $field) {
+ $groups[$field . '_' . $fun] = ['$' . $fun => '$' . $field];
+ }
+
+ $pipeline = [
+ ['$match' => (object) $this->parseWhere($query, $options['where'])],
+ ['$group' => $groups],
+ ];
+
+ $cmd = [
+ 'aggregate' => $options['table'],
+ 'allowDiskUse' => true,
+ 'pipeline' => $pipeline,
+ 'cursor' => new \stdClass,
+ ];
+
+ foreach (['explain', 'collation', 'bypassDocumentValidation', 'readConcern'] as $option) {
+ if (isset($options[$option])) {
+ $cmd[$option] = $options[$option];
+ }
+ }
+
+ $command = new Command($cmd);
+ $this->log('group', $cmd);
+
+ return $command;
+ }
+
+ /**
+ * 生成distinct命令
+ * @access public
+ * @param Query $query 查询对象
+ * @param string $field 字段名
+ * @return Command
+ */
+ public function distinct(Query $query, $field): Command
+ {
+ $options = $query->getOptions();
+
+ $cmd = [
+ 'distinct' => $options['table'],
+ 'key' => $field,
+ ];
+
+ if (!empty($options['where'])) {
+ $cmd['query'] = (object) $this->parseWhere($query, $options['where']);
+ }
+
+ if (isset($options['maxTimeMS'])) {
+ $cmd['maxTimeMS'] = $options['maxTimeMS'];
+ }
+
+ $command = new Command($cmd);
+
+ $this->log('cmd', 'distinct', $cmd);
+
+ return $command;
+ }
+
+ /**
+ * 查询所有的collection
+ * @access public
+ * @return Command
+ */
+ public function listcollections(): Command
+ {
+ $cmd = ['listCollections' => 1];
+ $command = new Command($cmd);
+
+ $this->log('cmd', 'listCollections', $cmd);
+
+ return $command;
+ }
+
+ /**
+ * 查询数据表的状态信息
+ * @access public
+ * @param Query $query 查询对象
+ * @return Command
+ */
+ public function collStats(Query $query): Command
+ {
+ $options = $query->getOptions();
+
+ $cmd = ['collStats' => $options['table']];
+ $command = new Command($cmd);
+
+ $this->log('cmd', 'collStats', $cmd);
+
+ return $command;
+ }
+
+ protected function log($type, $data, $options = [])
+ {
+ $this->connection->mongoLog($type, $data, $options);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/builder/Mysql.php b/vendor/topthink/think-orm/src/db/builder/Mysql.php
new file mode 100644
index 0000000..136b0de
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/builder/Mysql.php
@@ -0,0 +1,426 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\db\exception\DbException as Exception;
+use think\db\Query;
+use think\db\Raw;
+
+/**
+ * mysql数据库驱动
+ */
+class Mysql extends Builder
+{
+ /**
+ * 查询表达式解析
+ * @var array
+ */
+ protected $parser = [
+ 'parseCompare' => ['=', '<>', '>', '>=', '<', '<='],
+ 'parseLike' => ['LIKE', 'NOT LIKE'],
+ 'parseBetween' => ['NOT BETWEEN', 'BETWEEN'],
+ 'parseIn' => ['NOT IN', 'IN'],
+ 'parseExp' => ['EXP'],
+ 'parseRegexp' => ['REGEXP', 'NOT REGEXP'],
+ 'parseNull' => ['NOT NULL', 'NULL'],
+ 'parseBetweenTime' => ['BETWEEN TIME', 'NOT BETWEEN TIME'],
+ 'parseTime' => ['< TIME', '> TIME', '<= TIME', '>= TIME'],
+ 'parseExists' => ['NOT EXISTS', 'EXISTS'],
+ 'parseColumn' => ['COLUMN'],
+ 'parseFindInSet' => ['FIND IN SET'],
+ ];
+
+ /**
+ * SELECT SQL表达式
+ * @var string
+ */
+ protected $selectSql = 'SELECT%DISTINCT%%EXTRA% %FIELD% FROM %TABLE%%PARTITION%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * INSERT SQL表达式
+ * @var string
+ */
+ protected $insertSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% SET %SET% %DUPLICATE%%COMMENT%';
+
+ /**
+ * INSERT ALL SQL表达式
+ * @var string
+ */
+ protected $insertAllSql = '%INSERT%%EXTRA% INTO %TABLE%%PARTITION% (%FIELD%) VALUES %DATA% %DUPLICATE%%COMMENT%';
+
+ /**
+ * UPDATE SQL表达式
+ * @var string
+ */
+ protected $updateSql = 'UPDATE%EXTRA% %TABLE%%PARTITION% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * DELETE SQL表达式
+ * @var string
+ */
+ protected $deleteSql = 'DELETE%EXTRA% FROM %TABLE%%PARTITION%%USING%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+ /**
+ * 生成查询SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param bool $one 是否仅获取一个记录
+ * @return string
+ */
+ public function select(Query $query, bool $one = false): string
+ {
+ $options = $query->getOptions();
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%DISTINCT%', '%EXTRA%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseDistinct($query, $options['distinct']),
+ $this->parseExtra($query, $options['extra']),
+ $this->parseField($query, $options['field'] ?? '*'),
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseGroup($query, $options['group']),
+ $this->parseHaving($query, $options['having']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $one ? '1' : $options['limit']),
+ $this->parseUnion($query, $options['union']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ $this->parseForce($query, $options['force']),
+ ],
+ $this->selectSql);
+ }
+
+ /**
+ * 生成Insert SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function insert(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ // 分析并处理数据
+ $data = $this->parseData($query, $options['data']);
+ if (empty($data)) {
+ return '';
+ }
+
+ $set = [];
+ foreach ($data as $key => $val) {
+ $set[] = $key . ' = ' . $val;
+ }
+
+ return str_replace(
+ ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%SET%', '%DUPLICATE%', '%COMMENT%'],
+ [
+ !empty($options['replace']) ? 'REPLACE' : 'INSERT',
+ $this->parseExtra($query, $options['extra']),
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ implode(' , ', $set),
+ $this->parseDuplicate($query, $options['duplicate']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->insertSql);
+ }
+
+ /**
+ * 生成insertall SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @param array $dataSet 数据集
+ * @param bool $replace 是否replace
+ * @return string
+ */
+ public function insertAll(Query $query, array $dataSet, bool $replace = false): string
+ {
+ $options = $query->getOptions();
+
+ // 获取绑定信息
+ $bind = $query->getFieldsBindType();
+
+ // 获取合法的字段
+ if (empty($options['field']) || '*' == $options['field']) {
+ $allowFields = array_keys($bind);
+ } else {
+ $allowFields = $options['field'];
+ }
+
+ $fields = [];
+ $values = [];
+
+ foreach ($dataSet as $data) {
+ $data = $this->parseData($query, $data, $allowFields, $bind);
+
+ $values[] = '( ' . implode(',', array_values($data)) . ' )';
+
+ if (!isset($insertFields)) {
+ $insertFields = array_keys($data);
+ }
+ }
+
+ foreach ($insertFields as $field) {
+ $fields[] = $this->parseKey($query, $field);
+ }
+
+ return str_replace(
+ ['%INSERT%', '%EXTRA%', '%TABLE%', '%PARTITION%', '%FIELD%', '%DATA%', '%DUPLICATE%', '%COMMENT%'],
+ [
+ $replace ? 'REPLACE' : 'INSERT',
+ $this->parseExtra($query, $options['extra']),
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ implode(' , ', $fields),
+ implode(' , ', $values),
+ $this->parseDuplicate($query, $options['duplicate']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->insertAllSql);
+ }
+
+ /**
+ * 生成update SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function update(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ $data = $this->parseData($query, $options['data']);
+
+ if (empty($data)) {
+ return '';
+ }
+ $set = [];
+ foreach ($data as $key => $val) {
+ $set[] = $key . ' = ' . $val;
+ }
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%EXTRA%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseExtra($query, $options['extra']),
+ implode(' , ', $set),
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $options['limit']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->updateSql);
+ }
+
+ /**
+ * 生成delete SQL
+ * @access public
+ * @param Query $query 查询对象
+ * @return string
+ */
+ public function delete(Query $query): string
+ {
+ $options = $query->getOptions();
+
+ return str_replace(
+ ['%TABLE%', '%PARTITION%', '%EXTRA%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+ [
+ $this->parseTable($query, $options['table']),
+ $this->parsePartition($query, $options['partition']),
+ $this->parseExtra($query, $options['extra']),
+ !empty($options['using']) ? ' USING ' . $this->parseTable($query, $options['using']) . ' ' : '',
+ $this->parseJoin($query, $options['join']),
+ $this->parseWhere($query, $options['where']),
+ $this->parseOrder($query, $options['order']),
+ $this->parseLimit($query, $options['limit']),
+ $this->parseLock($query, $options['lock']),
+ $this->parseComment($query, $options['comment']),
+ ],
+ $this->deleteSql);
+ }
+
+ /**
+ * 正则查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @return string
+ */
+ protected function parseRegexp(Query $query, string $key, string $exp, $value, string $field): string
+ {
+ if ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ }
+
+ return $key . ' ' . $exp . ' ' . $value;
+ }
+
+ /**
+ * FIND_IN_SET 查询
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string $key
+ * @param string $exp
+ * @param mixed $value
+ * @param string $field
+ * @return string
+ */
+ protected function parseFindInSet(Query $query, string $key, string $exp, $value, string $field): string
+ {
+ if ($value instanceof Raw) {
+ $value = $this->parseRaw($query, $value);
+ }
+
+ return 'FIND_IN_SET(' . $value . ', ' . $key . ')';
+ }
+
+ /**
+ * 字段和表名处理
+ * @access public
+ * @param Query $query 查询对象
+ * @param mixed $key 字段名
+ * @param bool $strict 严格检测
+ * @return string
+ */
+ public function parseKey(Query $query, $key, bool $strict = false): string
+ {
+ if (is_int($key)) {
+ return (string) $key;
+ } elseif ($key instanceof Raw) {
+ return $this->parseRaw($query, $key);
+ }
+
+ $key = trim($key);
+
+ if (strpos($key, '->>') && false === strpos($key, '(')) {
+ // JSON字段支持
+ [$field, $name] = explode('->>', $key, 2);
+
+ return $this->parseKey($query, $field, true) . '->>\'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->>', '.', $name) . '\'';
+ } elseif (strpos($key, '->') && false === strpos($key, '(')) {
+ // JSON字段支持
+ [$field, $name] = explode('->', $key, 2);
+ return 'json_extract(' . $this->parseKey($query, $field, true) . ', \'$' . (strpos($name, '[') === 0 ? '' : '.') . str_replace('->', '.', $name) . '\')';
+ } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
+ [$table, $key] = explode('.', $key, 2);
+
+ $alias = $query->getOptions('alias');
+
+ if ('__TABLE__' == $table) {
+ $table = $query->getOptions('table');
+ $table = is_array($table) ? array_shift($table) : $table;
+ }
+
+ if (isset($alias[$table])) {
+ $table = $alias[$table];
+ }
+ }
+
+ if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) {
+ throw new Exception('not support data:' . $key);
+ }
+
+ if ('*' != $key && !preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
+ $key = '`' . $key . '`';
+ }
+
+ if (isset($table)) {
+ if (strpos($table, '.')) {
+ $table = str_replace('.', '`.`', $table);
+ }
+
+ $key = '`' . $table . '`.' . $key;
+ }
+
+ return $key;
+ }
+
+ /**
+ * 随机排序
+ * @access protected
+ * @param Query $query 查询对象
+ * @return string
+ */
+ protected function parseRand(Query $query): string
+ {
+ return 'rand()';
+ }
+
+ /**
+ * Partition 分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param string|array $partition 分区
+ * @return string
+ */
+ protected function parsePartition(Query $query, $partition): string
+ {
+ if ('' == $partition) {
+ return '';
+ }
+
+ if (is_string($partition)) {
+ $partition = explode(',', $partition);
+ }
+
+ return ' PARTITION (' . implode(' , ', $partition) . ') ';
+ }
+
+ /**
+ * ON DUPLICATE KEY UPDATE 分析
+ * @access protected
+ * @param Query $query 查询对象
+ * @param mixed $duplicate
+ * @return string
+ */
+ protected function parseDuplicate(Query $query, $duplicate): string
+ {
+ if ('' == $duplicate) {
+ return '';
+ }
+
+ if ($duplicate instanceof Raw) {
+ return ' ON DUPLICATE KEY UPDATE ' . $this->parseRaw($query, $duplicate) . ' ';
+ }
+
+ if (is_string($duplicate)) {
+ $duplicate = explode(',', $duplicate);
+ }
+
+ $updates = [];
+ foreach ($duplicate as $key => $val) {
+ if (is_numeric($key)) {
+ $val = $this->parseKey($query, $val);
+ $updates[] = $val . ' = VALUES(' . $val . ')';
+ } elseif ($val instanceof Raw) {
+ $updates[] = $this->parseKey($query, $key) . " = " . $this->parseRaw($query, $val);
+ } else {
+ $name = $query->bindValue($val, $query->getConnection()->getFieldBindType($key));
+ $updates[] = $this->parseKey($query, $key) . " = :" . $name;
+ }
+ }
+
+ return ' ON DUPLICATE KEY UPDATE ' . implode(' , ', $updates) . ' ';
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/builder/Oracle.php b/vendor/topthink/think-orm/src/db/builder/Oracle.php
new file mode 100644
index 0000000..77ad3c8
--- /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..4eace0a
--- /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..40cab7f
--- /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..779b5e3
--- /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..dabfb92
--- /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..c33d1ed
--- /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..ffb72de
--- /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..296e221
--- /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..de01093
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/concern/ResultOperation.php
@@ -0,0 +1,251 @@
+
+// +----------------------------------------------------------------------
+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|static
+ * @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|static
+ */
+ 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|Collection|static[]
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ public function selectOrFail($data = null)
+ {
+ return $this->failException(true)->select($data);
+ }
+
+ /**
+ * 查找单条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|Closure $data 数据
+ * @return array|Model|static
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ */
+ 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..9070bef
--- /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..1267e54
--- /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..f804ae2
--- /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..1311628
--- /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' && !is_null($condition)) {
+ $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..4b05b79
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/connector/Mongo.php
@@ -0,0 +1,1176 @@
+
+// +----------------------------------------------------------------------
+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;
+use function implode;
+use function is_array;
+
+/**
+ * Mongo数据库驱动
+ * @property Manager[] $links
+ * @property Manager $linkRead
+ * @property Manager $linkWrite
+ */
+class Mongo extends Connection
+{
+
+ // 查询数据类型
+ protected $dbName = '';
+ protected $typeMap = 'array';
+ protected $mongo; // MongoDb Object
+ protected $cursor; // MongoCursor Object
+ protected $session_uuid; // sessions会话列表当前会话数组key 随机生成
+ protected $sessions = []; // 会话列表
+
+ /** @var Builder */
+ protected $builder;
+
+ // 数据库连接参数配置
+ protected $config = [
+ // 数据库类型
+ 'type' => '',
+ // 服务器地址
+ 'hostname' => '',
+ // 数据库名
+ 'database' => '',
+ // 是否是复制集
+ 'is_replica_set' => false,
+ // 用户名
+ 'username' => '',
+ // 密码
+ 'password' => '',
+ // 端口
+ 'hostport' => '',
+ // 连接dsn
+ 'dsn' => '',
+ // 数据库连接参数
+ 'params' => [],
+ // 数据库编码默认采用utf8
+ 'charset' => 'utf8',
+ // 主键名
+ 'pk' => '_id',
+ // 主键类型
+ 'pk_type' => 'ObjectID',
+ // 数据库表前缀
+ 'prefix' => '',
+ // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+ 'deploy' => 0,
+ // 数据库读写是否分离 主从式有效
+ 'rw_separate' => false,
+ // 读写分离后 主服务器数量
+ 'master_num' => 1,
+ // 指定从服务器序号
+ 'slave_no' => '',
+ // 是否严格检查字段是否存在
+ 'fields_strict' => true,
+ // 开启字段缓存
+ 'fields_cache' => false,
+ // 监听SQL
+ 'trigger_sql' => true,
+ // 自动写入时间戳字段
+ 'auto_timestamp' => false,
+ // 时间字段取出后的默认时间格式
+ 'datetime_format' => 'Y-m-d H:i:s',
+ // 是否_id转换为id
+ 'pk_convert_id' => false,
+ // typeMap
+ 'type_map' => ['root' => 'array', 'document' => 'array'],
+ ];
+
+ /**
+ * 获取当前连接器类对应的Query类
+ * @access public
+ * @return string
+ */
+ public function getQueryClass(): string
+ {
+ return Query::class;
+ }
+
+ /**
+ * 获取当前的builder实例对象
+ * @access public
+ * @return Builder
+ */
+ public function getBuilder()
+ {
+ return $this->builder;
+ }
+
+ /**
+ * 获取当前连接器类对应的Builder类
+ * @access public
+ * @return string
+ */
+ public function getBuilderClass(): string
+ {
+ return Builder::class;
+ }
+
+ /**
+ * 连接数据库方法
+ * @access public
+ * @param array $config 连接参数
+ * @param integer $linkNum 连接序号
+ * @return Manager
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ */
+ public function connect(array $config = [], $linkNum = 0)
+ {
+ if (!isset($this->links[$linkNum])) {
+ if (empty($config)) {
+ $config = $this->config;
+ } else {
+ $config = array_merge($this->config, $config);
+ }
+
+ $this->dbName = $config['database'];
+ $this->typeMap = $config['type_map'];
+
+ if ($config['pk_convert_id'] && '_id' == $config['pk']) {
+ $this->config['pk'] = 'id';
+ }
+
+ if (empty($config['dsn'])) {
+ $config['dsn'] = 'mongodb://' . ($config['username'] ? "{$config['username']}" : '') . ($config['password'] ? ":{$config['password']}@" : '') . $config['hostname'] . ($config['hostport'] ? ":{$config['hostport']}" : '');
+ }
+
+ $startTime = microtime(true);
+
+ $this->links[$linkNum] = new Manager($config['dsn'], $config['params']);
+
+ if (!empty($config['trigger_sql'])) {
+ // 记录数据库连接信息
+ $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
+ }
+
+ }
+
+ return $this->links[$linkNum];
+ }
+
+ /**
+ * 获取Mongo Manager对象
+ * @access public
+ * @return Manager|null
+ */
+ public function getMongo()
+ {
+ return $this->mongo ?: null;
+ }
+
+ /**
+ * 设置/获取当前操作的database
+ * @access public
+ * @param string $db db
+ * @return string
+ */
+ public function db(string $db = null)
+ {
+ if (is_null($db)) {
+ return $this->dbName;
+ } else {
+ $this->dbName = $db;
+ }
+ }
+
+ /**
+ * 执行查询但只返回Cursor对象
+ * @access public
+ * @param Query $query 查询对象
+ * @return Cursor
+ */
+ public function cursor($query)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ // 生成MongoQuery对象
+ $mongoQuery = $this->builder->select($query);
+
+ $master = $query->getOptions('master') ? true : false;
+
+ // 执行查询操作
+ return $this->getCursor($query, $mongoQuery, $master);
+ }
+
+ /**
+ * 执行查询并返回Cursor对象
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param MongoQuery|Closure $mongoQuery Mongo查询对象
+ * @param bool $master 是否主库操作
+ * @return Cursor
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ public function getCursor(BaseQuery $query, $mongoQuery, bool $master = false): Cursor
+ {
+ $this->initConnect($master);
+ $this->db->updateQueryTimes();
+
+ $options = $query->getOptions();
+ $namespace = $options['table'];
+
+ if (false === strpos($namespace, '.')) {
+ $namespace = $this->dbName . '.' . $namespace;
+ }
+
+ if (!empty($this->queryStr)) {
+ // 记录执行指令
+ $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr;
+ }
+
+ if ($mongoQuery instanceof Closure) {
+ $mongoQuery = $mongoQuery($query);
+ }
+
+ $readPreference = $options['readPreference'] ?? null;
+ $this->queryStartTime = microtime(true);
+
+ if ($session = $this->getSession()) {
+ $this->cursor = $this->mongo->executeQuery($namespace, $query, [
+ 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference,
+ 'session' => $session,
+ ]);
+ } else {
+ $this->cursor = $this->mongo->executeQuery($namespace, $mongoQuery, $readPreference);
+ }
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+
+ return $this->cursor;
+ }
+
+ /**
+ * 执行查询 返回数据集
+ * @access public
+ * @param MongoQuery $query 查询对象
+ * @return mixed
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ public function query(MongoQuery $query)
+ {
+ return $this->mongoQuery($this->newQuery(), $query);
+ }
+
+ /**
+ * 执行语句
+ * @access public
+ * @param BulkWrite $bulk
+ * @return int
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ public function execute(BulkWrite $bulk)
+ {
+ return $this->mongoExecute($this->newQuery(), $bulk);
+ }
+
+ /**
+ * 执行查询
+ * @access protected
+ * @param BaseQuery $query 查询对象
+ * @param MongoQuery|Closure $mongoQuery Mongo查询对象
+ * @return array
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ protected function mongoQuery(BaseQuery $query, $mongoQuery): array
+ {
+ $options = $query->parseOptions();
+
+ if ($query->getOptions('cache')) {
+ // 检查查询缓存
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
+ }
+
+ if ($mongoQuery instanceof Closure) {
+ $mongoQuery = $mongoQuery($query);
+ }
+
+ $master = $query->getOptions('master') ? true : false;
+ $this->getCursor($query, $mongoQuery, $master);
+
+ $resultSet = $this->getResult($options['typeMap']);
+
+ if (isset($cacheItem) && $resultSet) {
+ // 缓存数据集
+ $cacheItem->set($resultSet);
+ $this->cacheData($cacheItem);
+ }
+
+ return $resultSet;
+ }
+
+ /**
+ * 执行写操作
+ * @access protected
+ * @param BaseQuery $query
+ * @param BulkWrite $bulk
+ *
+ * @return WriteResult
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ protected function mongoExecute(BaseQuery $query, BulkWrite $bulk)
+ {
+ $this->initConnect(true);
+ $this->db->updateQueryTimes();
+
+ $options = $query->getOptions();
+
+ $namespace = $options['table'];
+ if (false === strpos($namespace, '.')) {
+ $namespace = $this->dbName . '.' . $namespace;
+ }
+
+ if (!empty($this->queryStr)) {
+ // 记录执行指令
+ $this->queryStr = 'db' . strstr($namespace, '.') . '.' . $this->queryStr;
+ }
+
+ $writeConcern = $options['writeConcern'] ?? null;
+ $this->queryStartTime = microtime(true);
+
+ if ($session = $this->getSession()) {
+ $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, [
+ 'session' => $session,
+ 'writeConcern' => is_null($writeConcern) ? new WriteConcern(1) : $writeConcern,
+ ]);
+ } else {
+ $writeResult = $this->mongo->executeBulkWrite($namespace, $bulk, $writeConcern);
+ }
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger();
+ }
+
+ $this->numRows = $writeResult->getMatchedCount();
+
+ if ($query->getOptions('cache')) {
+ // 清理缓存数据
+ $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
+ $key = $cacheItem->getKey();
+ $tag = $cacheItem->getTag();
+
+ if (isset($key) && $this->cache->has($key)) {
+ $this->cache->delete($key);
+ } elseif (!empty($tag) && method_exists($this->cache, 'tag')) {
+ $this->cache->tag($tag)->clear();
+ }
+ }
+
+ return $writeResult;
+ }
+
+ /**
+ * 执行指令
+ * @access public
+ * @param Command $command 指令
+ * @param string $dbName 当前数据库名
+ * @param ReadPreference $readPreference readPreference
+ * @param string|array $typeMap 指定返回的typeMap
+ * @param bool $master 是否主库操作
+ * @return array
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ public function command(Command $command, string $dbName = '', ReadPreference $readPreference = null, $typeMap = null, bool $master = false): array
+ {
+ $this->initConnect($master);
+ $this->db->updateQueryTimes();
+
+ $this->queryStartTime = microtime(true);
+
+ $dbName = $dbName ?: $this->dbName;
+
+ if (!empty($this->queryStr)) {
+ $this->queryStr = 'db.' . $this->queryStr;
+ }
+
+ if ($session = $this->getSession()) {
+ $this->cursor = $this->mongo->executeCommand($dbName, $command, [
+ 'readPreference' => is_null($readPreference) ? new ReadPreference(ReadPreference::RP_PRIMARY) : $readPreference,
+ 'session' => $session,
+ ]);
+ } else {
+ $this->cursor = $this->mongo->executeCommand($dbName, $command, $readPreference);
+ }
+
+ // SQL监控
+ if (!empty($this->config['trigger_sql'])) {
+ $this->trigger('', $master);
+ }
+
+ return $this->getResult($typeMap);
+ }
+
+ /**
+ * 获得数据集
+ * @access protected
+ * @param string|array $typeMap 指定返回的typeMap
+ * @return mixed
+ */
+ protected function getResult($typeMap = null): array
+ {
+ // 设置结果数据类型
+ if (is_null($typeMap)) {
+ $typeMap = $this->typeMap;
+ }
+
+ $typeMap = is_string($typeMap) ? ['root' => $typeMap] : $typeMap;
+
+ $this->cursor->setTypeMap($typeMap);
+
+ // 获取数据集
+ $result = $this->cursor->toArray();
+
+ if ($this->getConfig('pk_convert_id')) {
+ // 转换ObjectID 字段
+ foreach ($result as &$data) {
+ $this->convertObjectID($data);
+ }
+ }
+
+ $this->numRows = count($result);
+
+ return $result;
+ }
+
+ /**
+ * ObjectID处理
+ * @access protected
+ * @param array $data 数据
+ * @return void
+ */
+ protected function convertObjectID(array &$data): void
+ {
+ if (isset($data['_id']) && is_object($data['_id'])) {
+ $data['id'] = $data['_id']->__toString();
+ unset($data['_id']);
+ }
+ }
+
+ /**
+ * 数据库日志记录(仅供参考)
+ * @access public
+ * @param string $type 类型
+ * @param mixed $data 数据
+ * @param array $options 参数
+ * @return void
+ */
+ public function mongoLog(string $type, $data, array $options = [])
+ {
+ if (!$this->config['trigger_sql']) {
+ return;
+ }
+
+ if (is_array($data)) {
+ array_walk_recursive($data, function (&$value) {
+ if ($value instanceof ObjectID) {
+ $value = $value->__toString();
+ }
+ });
+ }
+
+ switch (strtolower($type)) {
+ case 'aggregate':
+ $this->queryStr = 'runCommand(' . ($data ? json_encode($data) : '') . ');';
+ break;
+ case 'find':
+ $this->queryStr = $type . '(' . ($data ? json_encode($data) : '') . ')';
+
+ if (isset($options['sort'])) {
+ $this->queryStr .= '.sort(' . json_encode($options['sort']) . ')';
+ }
+
+ if (isset($options['skip'])) {
+ $this->queryStr .= '.skip(' . $options['skip'] . ')';
+ }
+
+ if (isset($options['limit'])) {
+ $this->queryStr .= '.limit(' . $options['limit'] . ')';
+ }
+
+ $this->queryStr .= ';';
+ break;
+ case 'insert':
+ case 'remove':
+ $this->queryStr = $type . '(' . ($data ? json_encode($data) : '') . ');';
+ break;
+ case 'update':
+ $this->queryStr = $type . '(' . json_encode($options) . ',' . json_encode($data) . ');';
+ break;
+ case 'cmd':
+ $this->queryStr = $data . '(' . json_encode($options) . ');';
+ break;
+ }
+
+ $this->options = $options;
+ }
+
+ /**
+ * 获取最近执行的指令
+ * @access public
+ * @return string
+ */
+ public function getLastSql(): string
+ {
+ return $this->queryStr;
+ }
+
+ /**
+ * 关闭数据库
+ * @access public
+ */
+ public function close()
+ {
+ $this->mongo = null;
+ $this->cursor = null;
+ $this->linkRead = null;
+ $this->linkWrite = null;
+ $this->links = [];
+ }
+
+ /**
+ * 初始化数据库连接
+ * @access protected
+ * @param boolean $master 是否主服务器
+ * @return void
+ */
+ protected function initConnect(bool $master = true): void
+ {
+ if (!empty($this->config['deploy'])) {
+ // 采用分布式数据库
+ if ($master) {
+ if (!$this->linkWrite) {
+ $this->linkWrite = $this->multiConnect(true);
+ }
+
+ $this->mongo = $this->linkWrite;
+ } else {
+ if (!$this->linkRead) {
+ $this->linkRead = $this->multiConnect(false);
+ }
+
+ $this->mongo = $this->linkRead;
+ }
+ } elseif (!$this->mongo) {
+ // 默认单数据库
+ $this->mongo = $this->connect();
+ }
+ }
+
+ /**
+ * 连接分布式服务器
+ * @access protected
+ * @param boolean $master 主服务器
+ * @return Manager
+ */
+ protected function multiConnect(bool $master = false): Manager
+ {
+ $config = [];
+ // 分布式数据库配置解析
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) {
+ $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
+ }
+
+ // 主服务器序号
+ $m = floor(mt_rand(0, $this->config['master_num'] - 1));
+
+ if ($this->config['rw_separate']) {
+ // 主从式采用读写分离
+ if ($master) // 主服务器写入
+ {
+ if ($this->config['is_replica_set']) {
+ return $this->replicaSetConnect();
+ } else {
+ $r = $m;
+ }
+ } elseif (is_numeric($this->config['slave_no'])) {
+ // 指定服务器读
+ $r = $this->config['slave_no'];
+ } else {
+ // 读操作连接从服务器 每次随机连接的数据库
+ $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
+ }
+ } else {
+ // 读写操作不区分服务器 每次随机连接的数据库
+ $r = floor(mt_rand(0, count($config['hostname']) - 1));
+ }
+
+ $dbConfig = [];
+
+ foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn'] as $name) {
+ $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
+ }
+
+ return $this->connect($dbConfig, $r);
+ }
+
+ /**
+ * 创建基于复制集的连接
+ * @return Manager
+ */
+ public function replicaSetConnect(): Manager
+ {
+ $this->dbName = $this->config['database'];
+ $this->typeMap = $this->config['type_map'];
+
+ $startTime = microtime(true);
+
+ $this->config['params']['replicaSet'] = $this->config['database'];
+
+ $manager = new Manager($this->buildUrl(), $this->config['params']);
+
+ // 记录数据库连接信息
+ if (!empty($config['trigger_sql'])) {
+ $this->trigger('CONNECT:ReplicaSet[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $this->config['dsn']);
+ }
+
+ return $manager;
+ }
+
+ /**
+ * 根据配置信息 生成适用于连接复制集的 URL
+ * @return string
+ */
+ private function buildUrl(): string
+ {
+ $url = 'mongodb://' . ($this->config['username'] ? "{$this->config['username']}" : '') . ($this->config['password'] ? ":{$this->config['password']}@" : '');
+
+ $hostList = is_string($this->config['hostname']) ? explode(',', $this->config['hostname']) : $this->config['hostname'];
+ $portList = is_string($this->config['hostport']) ? explode(',', $this->config['hostport']) : $this->config['hostport'];
+
+ for ($i = 0; $i < count($hostList); $i++) {
+ $url = $url . $hostList[$i] . ':' . $portList[0] . ',';
+ }
+
+ return rtrim($url, ",") . '/';
+ }
+
+ /**
+ * 插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param boolean $getLastInsID 返回自增主键
+ * @return mixed
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ public function insert(BaseQuery $query, bool $getLastInsID = false)
+ {
+ // 分析查询表达式
+ $options = $query->parseOptions();
+
+ if (empty($options['data'])) {
+ throw new Exception('miss data to insert');
+ }
+
+ // 生成bulk对象
+ $bulk = $this->builder->insert($query);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
+ $result = $writeResult->getInsertedCount();
+
+ if ($result) {
+ $data = $options['data'];
+ $lastInsId = $this->getLastInsID($query);
+
+ if ($lastInsId) {
+ $pk = $query->getPk();
+ $data[$pk] = $lastInsId;
+ }
+
+ $query->setOption('data', $data);
+
+ $this->db->trigger('after_insert', $query);
+
+ if ($getLastInsID) {
+ return $lastInsId;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取最近插入的ID
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return mixed
+ */
+ public function getLastInsID(BaseQuery $query)
+ {
+ $id = $this->builder->getLastInsID();
+
+ if (is_array($id)) {
+ array_walk($id, function (&$item, $key) {
+ if ($item instanceof ObjectID) {
+ $item = $item->__toString();
+ }
+ });
+ } elseif ($id instanceof ObjectID) {
+ $id = $id->__toString();
+ }
+
+ return $id;
+ }
+
+ /**
+ * 批量插入记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param array $dataSet 数据集
+ * @return integer
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ public function insertAll(BaseQuery $query, array $dataSet = []): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ if (!is_array(reset($dataSet))) {
+ return 0;
+ }
+
+ // 生成bulkWrite对象
+ $bulk = $this->builder->insertAll($query, $dataSet);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
+
+ return $writeResult->getInsertedCount();
+ }
+
+ /**
+ * 更新记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ * @throws Exception
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ public function update(BaseQuery $query): int
+ {
+ $query->parseOptions();
+
+ // 生成bulkWrite对象
+ $bulk = $this->builder->update($query);
+
+ $writeResult = $this->mongoExecute($query, $bulk);
+
+ $result = $writeResult->getModifiedCount();
+
+ if ($result) {
+ $this->db->trigger('after_update', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return int
+ * @throws Exception
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ * @throws BulkWriteException
+ */
+ public function delete(BaseQuery $query): int
+ {
+ // 分析查询表达式
+ $query->parseOptions();
+
+ // 生成bulkWrite对象
+ $bulk = $this->builder->delete($query);
+
+ // 执行操作
+ $writeResult = $this->mongoExecute($query, $bulk);
+
+ $result = $writeResult->getDeletedCount();
+
+ if ($result) {
+ $this->db->trigger('after_delete', $query);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 查找记录
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @return array
+ * @throws ModelNotFoundException
+ * @throws DataNotFoundException
+ * @throws AuthenticationException
+ * @throws InvalidArgumentException
+ * @throws ConnectionException
+ * @throws RuntimeException
+ */
+ public function select(BaseQuery $query): array
+ {
+ $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 BaseQuery $query
+ * @param string|array $field 字段名 多个字段用逗号分隔
+ * @param string $key 索引
+ * @return array
+ */
+ public function column(BaseQuery $query, $field, string $key = ''): array
+ {
+ $options = $query->parseOptions();
+
+ if (isset($options['projection'])) {
+ $query->removeOption('projection');
+ }
+
+ if (is_array($field)) {
+ $field = implode(',', $field);
+ }
+ if ($key && '*' != $field) {
+ $projection = $key . ',' . $field;
+ } else {
+ $projection = $field;
+ }
+
+ $query->field($projection);
+
+ if (!empty($options['cache'])) {
+ // 判断查询缓存
+ $cacheItem = $this->parseCache($query, $options['cache']);
+ $key = $cacheItem->getKey();
+
+ if ($this->cache->has($key)) {
+ return $this->cache->get($key);
+ }
+ }
+
+ $mongoQuery = $this->builder->select($query);
+
+ if (isset($options['projection'])) {
+ $query->setOption('projection', $options['projection']);
+ } else {
+ $query->removeOption('projection');
+ }
+
+ // 执行查询操作
+ $resultSet = $this->mongoQuery($query, $mongoQuery);
+
+ if (('*' == $field || strpos($field, ',')) && $key) {
+ $result = array_column($resultSet, null, $key);
+ } elseif (!empty($resultSet)) {
+ $result = array_column($resultSet, $field, $key);
+ } else {
+ $result = [];
+ }
+
+ if (isset($cacheItem)) {
+ // 缓存数据
+ $cacheItem->set($result);
+ $this->cacheData($cacheItem);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 执行command
+ * @access public
+ * @param BaseQuery $query 查询对象
+ * @param string|array|object $command 指令
+ * @param mixed $extra 额外参数
+ * @param string $db 数据库名
+ * @return array
+ */
+ public function cmd(BaseQuery $query, $command, $extra = null, string $db = ''): array
+ {
+ if (is_array($command) || is_object($command)) {
+
+ $this->mongoLog('cmd', 'cmd', $command);
+
+ // 直接创建Command对象
+ $command = new Command($command);
+ } else {
+ // 调用Builder封装的Command对象
+ $command = $this->builder->$command($query, $extra);
+ }
+
+ return $this->command($command, $db);
+ }
+
+ /**
+ * 获取数据库字段
+ * @access public
+ * @param mixed $tableName 数据表名
+ * @return array
+ */
+ public function getTableFields($tableName): array
+ {
+ return [];
+ }
+
+ /**
+ * 执行数据库事务
+ * @access public
+ * @param callable $callback 数据操作方法回调
+ * @return mixed
+ * @throws PDOException
+ * @throws \Exception
+ * @throws \Throwable
+ */
+ public function transaction(callable $callback)
+ {
+ $this->startTrans();
+ try {
+ $result = null;
+ if (is_callable($callback)) {
+ $result = call_user_func_array($callback, [$this]);
+ }
+ $this->commit();
+ return $result;
+ } catch (\Exception $e) {
+ $this->rollback();
+ throw $e;
+ } catch (\Throwable $e) {
+ $this->rollback();
+ throw $e;
+ }
+ }
+
+ /**
+ * 启动事务
+ * @access public
+ * @return void
+ * @throws \PDOException
+ * @throws \Exception
+ */
+ public function startTrans()
+ {
+ $this->initConnect(true);
+ $this->session_uuid = uniqid();
+ $this->sessions[$this->session_uuid] = $this->getMongo()->startSession();
+
+ $this->sessions[$this->session_uuid]->startTransaction([]);
+ }
+
+ /**
+ * 用于非自动提交状态下面的查询提交
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function commit()
+ {
+ if ($session = $this->getSession()) {
+ $session->commitTransaction();
+ $this->setLastSession();
+ }
+ }
+
+ /**
+ * 事务回滚
+ * @access public
+ * @return void
+ * @throws PDOException
+ */
+ public function rollback()
+ {
+ if ($session = $this->getSession()) {
+ $session->abortTransaction();
+ $this->setLastSession();
+ }
+ }
+
+ /**
+ * 结束当前会话,设置上一个会话为当前会话
+ * @author klinson
+ */
+ protected function setLastSession()
+ {
+ if ($session = $this->getSession()) {
+ $session->endSession();
+ unset($this->sessions[$this->session_uuid]);
+ if (empty($this->sessions)) {
+ $this->session_uuid = null;
+ } else {
+ end($this->sessions);
+ $this->session_uuid = key($this->sessions);
+ }
+ }
+ }
+
+ /**
+ * 获取当前会话
+ * @return \MongoDB\Driver\Session|null
+ * @author klinson
+ */
+ public function getSession()
+ {
+ return ($this->session_uuid && isset($this->sessions[$this->session_uuid]))
+ ? $this->sessions[$this->session_uuid]
+ : null;
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/connector/Mysql.php b/vendor/topthink/think-orm/src/db/connector/Mysql.php
new file mode 100644
index 0000000..483b447
--- /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..236d8bf
--- /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..fec8f84
--- /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..c664f20
--- /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..10d944f
--- /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);
+ strpos($tableName,'.') && $tableName = substr($tableName,strpos($tableName,'.') + 1);
+ $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..e1a09a3
--- /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..08bb388
--- /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..d10dd43
--- /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/DbEventException.php b/vendor/topthink/think-orm/src/db/exception/DbEventException.php
new file mode 100644
index 0000000..394a1e8
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/DbEventException.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\exception;
+
+/**
+ * Db事件异常
+ */
+class DbEventException extends DbException
+{
+}
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..f68b21c
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/DbException.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+use think\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);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php b/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php
new file mode 100644
index 0000000..047e45e
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/InvalidArgumentException.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\db\exception;
+
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface;
+
+/**
+ * 非法数据异常
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentInterface
+{
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/ModelEventException.php b/vendor/topthink/think-orm/src/db/exception/ModelEventException.php
new file mode 100644
index 0000000..767bc1a
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/ModelEventException.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\db\exception;
+
+/**
+ * 模型事件异常
+ */
+class ModelEventException extends DbException
+{
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php b/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php
new file mode 100644
index 0000000..84a1525
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/ModelNotFoundException.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+class ModelNotFoundException extends DbException
+{
+ protected $model;
+
+ /**
+ * 构造方法
+ * @access public
+ * @param string $message
+ * @param string $model
+ * @param array $config
+ */
+ public function __construct(string $message, string $model = '', array $config = [])
+ {
+ $this->message = $message;
+ $this->model = $model;
+
+ $this->setData('Database Config', $config);
+ }
+
+ /**
+ * 获取模型类名
+ * @access public
+ * @return string
+ */
+ public function getModel()
+ {
+ return $this->model;
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/db/exception/PDOException.php b/vendor/topthink/think-orm/src/db/exception/PDOException.php
new file mode 100644
index 0000000..efe78b9
--- /dev/null
+++ b/vendor/topthink/think-orm/src/db/exception/PDOException.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\db\exception;
+
+/**
+ * PDO异常处理类
+ * 重新封装了系统的\PDOException类
+ */
+class PDOException extends DbException
+{
+ /**
+ * PDOException constructor.
+ * @access public
+ * @param \PDOException $exception
+ * @param array $config
+ * @param string $sql
+ * @param int $code
+ */
+ public function __construct(\PDOException $exception, array $config = [], string $sql = '', int $code = 10501)
+ {
+ $error = $exception->errorInfo;
+ $message = $exception->getMessage();
+
+ if (!empty($error)) {
+ $this->setData('PDO Error Info', [
+ 'SQLSTATE' => $error[0],
+ 'Driver Error Code' => isset($error[1]) ? $error[1] : 0,
+ 'Driver Error Message' => isset($error[2]) ? $error[2] : '',
+ ]);
+ }
+
+ parent::__construct($message, $config, $sql, $code);
+ }
+}
diff --git a/vendor/topthink/think-orm/src/facade/Db.php b/vendor/topthink/think-orm/src/facade/Db.php
new file mode 100644
index 0000000..b0296c6
--- /dev/null
+++ b/vendor/topthink/think-orm/src/facade/Db.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @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..f017e32
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/Collection.php
@@ -0,0 +1,265 @@
+
+// +----------------------------------------------------------------------
+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 string $scene 场景名称
+ * @return $this
+ */
+ public function scene(string $scene)
+ {
+ $this->each(function (Model $model) use ($scene) {
+ $model->scene($scene);
+ });
+
+ 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..893c01b
--- /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..1779896
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/Relation.php
@@ -0,0 +1,300 @@
+
+// +----------------------------------------------------------------------
+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;
+
+ /**
+ * 排除关联数据字段
+ * @var array
+ */
+ protected $withoutField;
+
+ /**
+ * 获取关联的所属模型
+ * @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 string
+ */
+ public function getForeignKey()
+ {
+ return $this->foreignKey;
+ }
+
+ /**
+ * 获取关联表主键
+ * @access public
+ * @return string
+ */
+ public function getLocalKey()
+ {
+ return $this->localKey;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @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 public
+ * @param array|string $field 关联字段限制
+ * @return $this
+ */
+ public function withoutField($field)
+ {
+ if (is_string($field)) {
+ $field = array_map('trim', explode(',', $field));
+ }
+
+ $this->withoutField = $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..34cb59a
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/Attribute.php
@@ -0,0 +1,657 @@
+
+// +----------------------------------------------------------------------
+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 $get = [];
+
+ /**
+ * 动态获取器
+ * @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
+ {
+ if ($this->convertNameToCamel || !$this->strict) {
+ return Str::snake($name);
+ }
+
+ return $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;
+ }
+
+ $fieldName = $this->getRealFieldName($name);
+
+ return array_key_exists($fieldName, $this->origin) ? $this->origin[$fieldName] : 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 (array_key_exists($field, $data)) {
+ 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;
+ unset($this->get[$name]);
+ }
+
+ /**
+ * 通过修改器 批量设置数据对象值
+ * @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);
+
+ // 检测修改器
+ $method = 'set' . Str::studly($name) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ $array = $this->data;
+
+ $value = $this->$method($value, array_merge($this->data, $data));
+
+ if (is_null($value) && $array !== $this->data) {
+ return;
+ }
+ } elseif (isset($this->type[$name])) {
+ // 类型转换
+ $value = $this->writeTransform($value, $this->type[$name]);
+ }
+
+ // 设置数据对象属性
+ $this->data[$name] = $value;
+ unset($this->get[$name]);
+ }
+
+ /**
+ * 数据写入 类型转换
+ * @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);
+
+ if (array_key_exists($fieldName, $this->get)) {
+ return $this->get[$fieldName];
+ }
+
+ $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];
+ if ($closure instanceof \Closure) {
+ $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;
+ }
+
+ $this->get[$fieldName] = $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..35d96d0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/Conversion.php
@@ -0,0 +1,358 @@
+
+// +----------------------------------------------------------------------
+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 array
+ */
+ protected $scene = [];
+
+ /**
+ * 数据输出字段映射
+ * @var array
+ */
+ protected $mapping = [];
+
+ /**
+ * 数据集对象名
+ * @var string
+ */
+ protected $resultSetType;
+
+ /**
+ * 数据命名是否自动转为驼峰
+ * @var bool
+ */
+ protected $convertNameToCamel;
+
+ /**
+ * 转换数据为驼峰命名(用于输出)
+ * @access public
+ * @param bool $toCamel 是否自动驼峰命名
+ * @return $this
+ */
+ public function convertNameToCamel(bool $toCamel = true)
+ {
+ $this->convertNameToCamel = $toCamel;
+ return $this;
+ }
+
+ /**
+ * 设置需要附加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ $this->append = $append;
+
+ return $this;
+ }
+
+ /**
+ * 设置输出层场景
+ * @access public
+ * @param string $scene 场景名称
+ * @return $this
+ */
+ public function scene(string $scene)
+ {
+ if (isset($this->scene[$scene])) {
+ $data = $this->scene[$scene];
+ foreach (['append', 'hidden', 'visible'] as $name) {
+ if (isset($data[$name])) {
+ $this->$name($data[$name]);
+ }
+ }
+ }
+
+ 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
+ * @param array $map
+ * @return $this
+ */
+ public function mapping(array $map)
+ {
+ $this->mapping = $map;
+
+ 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);
+ }
+
+ if (isset($this->mapping[$key])) {
+ // 检查字段映射
+ $mapName = $this->mapping[$key];
+ $item[$mapName] = $item[$key];
+ unset($item[$key]);
+ }
+ }
+
+ // 追加属性(必须定义获取器)
+ foreach ($this->append as $key => $name) {
+ $this->appendAttrToArray($item, $key, $name);
+ }
+
+ if ($this->convertNameToCamel) {
+ foreach ($item as $key => $val) {
+ $name = Str::camel($key);
+ if ($name !== $key) {
+ $item[$name] = $val;
+ unset($item[$key]);
+ }
+ }
+ }
+
+ 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->getBindAttrValue($name, $value, $item);
+ }
+ }
+
+ protected function getBindAttrValue(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..d2388ab
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/ModelEvent.php
@@ -0,0 +1,88 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\exception\ModelEventException;
+use think\helper\Str;
+
+/**
+ * 模型事件处理
+ */
+trait ModelEvent
+{
+
+ /**
+ * Event对象
+ * @var object
+ */
+ protected static $event;
+
+ /**
+ * 是否需要事件响应
+ * @var bool
+ */
+ protected $withEvent = true;
+
+ /**
+ * 设置Event对象
+ * @access public
+ * @param object $event Event对象
+ * @return void
+ */
+ public static function setEvent($event)
+ {
+ self::$event = $event;
+ }
+
+ /**
+ * 当前操作的事件响应
+ * @access protected
+ * @param bool $event 是否需要事件响应
+ * @return $this
+ */
+ public function withEvent(bool $event)
+ {
+ $this->withEvent = $event;
+ return $this;
+ }
+
+ /**
+ * 触发事件
+ * @access protected
+ * @param string $event 事件名
+ * @return bool
+ */
+ protected function trigger(string $event): bool
+ {
+ if (!$this->withEvent) {
+ return true;
+ }
+
+ $call = 'on' . Str::studly($event);
+
+ try {
+ if (method_exists(static::class, $call)) {
+ $result = call_user_func([static::class, $call], $this);
+ } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
+ $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
+ $result = empty($result) ? true : end($result);
+ } else {
+ $result = true;
+ }
+
+ return false === $result ? false : true;
+ } catch (ModelEventException $e) {
+ return false;
+ }
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/OptimLock.php b/vendor/topthink/think-orm/src/model/concern/OptimLock.php
new file mode 100644
index 0000000..5e61318
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/OptimLock.php
@@ -0,0 +1,85 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\exception\DbException as Exception;
+
+/**
+ * 乐观锁
+ */
+trait OptimLock
+{
+ protected function getOptimLockField()
+ {
+ return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'lock_version';
+ }
+
+ /**
+ * 数据检查
+ * @access protected
+ * @return void
+ */
+ protected function checkData(): void
+ {
+ $this->isExists() ? $this->updateLockVersion() : $this->recordLockVersion();
+ }
+
+ /**
+ * 记录乐观锁
+ * @access protected
+ * @return void
+ */
+ protected function recordLockVersion(): void
+ {
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock) {
+ $this->set($optimLock, 0);
+ }
+ }
+
+ /**
+ * 更新乐观锁
+ * @access protected
+ * @return void
+ */
+ protected function updateLockVersion(): void
+ {
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
+ // 更新乐观锁
+ $this->set($optimLock, $lockVer + 1);
+ }
+ }
+
+ public function getWhere()
+ {
+ $where = parent::getWhere();
+ $optimLock = $this->getOptimLockField();
+
+ if ($optimLock && $lockVer = $this->getOrigin($optimLock)) {
+ $where[] = [$optimLock, '=', $lockVer];
+ }
+
+ return $where;
+ }
+
+ protected function checkResult($result): void
+ {
+ if (!$result) {
+ throw new Exception('record has update');
+ }
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/model/concern/RelationShip.php b/vendor/topthink/think-orm/src/model/concern/RelationShip.php
new file mode 100644
index 0000000..33c1b89
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/RelationShip.php
@@ -0,0 +1,842 @@
+
+// +----------------------------------------------------------------------
+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((array) $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
+ * @param bool $force 强制删除
+ * @return void
+ */
+ protected function autoRelationDelete($force = false): void
+ {
+ foreach ($this->relationWrite as $key => $name) {
+ $name = is_numeric($key) ? $name : $key;
+ $result = $this->getRelation($name, true);
+
+ if ($result instanceof Model) {
+ $result->force($force)->delete();
+ } elseif ($result instanceof Collection) {
+ foreach ($result as $model) {
+ $model->force($force)->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..8d76bb0
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/SoftDelete.php
@@ -0,0 +1,257 @@
+
+// +----------------------------------------------------------------------
+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();
+ $force = $this->isForce();
+
+ if ($name && !$force) {
+ // 软删除
+ $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($force);
+ }
+
+ $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
+ {
+ // 传入空值(包括空字符串和空数组)的时候不会做任何的数据删除操作,但传入0则是有效的
+ if(empty($data) && $data !== 0){
+ return false;
+ }
+ // 仅当强制删除时包含软删除数据
+ $model = (new static());
+ if($force){
+ $model->withTrashedData(true);
+ }
+ $query = $model->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..2492e06
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/TimeStamp.php
@@ -0,0 +1,223 @@
+
+// +----------------------------------------------------------------------
+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
+ * @param string $createTime
+ * @param string $updateTime
+ * @return $this
+ */
+ public function setTimeField(string $createTime, string $updateTime)
+ {
+ $this->createTime = $createTime;
+ $this->updateTime = $updateTime;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动写入时间字段
+ * @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/concern/Virtual.php b/vendor/topthink/think-orm/src/model/concern/Virtual.php
new file mode 100644
index 0000000..66cdfb7
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/concern/Virtual.php
@@ -0,0 +1,90 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\model\concern;
+
+use think\db\BaseQuery as Query;
+use think\db\exception\DbException as Exception;
+
+/**
+ * 虚拟模型
+ */
+trait Virtual
+{
+ /**
+ * 获取当前模型的数据库查询对象
+ * @access public
+ * @param array $scope 设置不使用的全局查询范围
+ * @return Query
+ */
+ public function db($scope = []): Query
+ {
+ throw new Exception('virtual model not support db query');
+ }
+
+ /**
+ * 获取字段类型信息
+ * @access public
+ * @param string $field 字段名
+ * @return string|null
+ */
+ public function getFieldType(string $field)
+ {}
+
+ /**
+ * 保存当前数据对象
+ * @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;
+ }
+
+ // 写入回调
+ $this->trigger('AfterWrite');
+
+ $this->exists(true);
+
+ return true;
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ if (!$this->isExists() || $this->isEmpty() || false === $this->trigger('BeforeDelete')) {
+ return false;
+ }
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+
+ $this->trigger('AfterDelete');
+
+ $this->exists(false);
+
+ return true;
+ }
+
+}
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..789c944
--- /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..5f177c1
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php
@@ -0,0 +1,689 @@
+
+// +----------------------------------------------------------------------
+
+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
+ {
+ // 关联查询封装
+ if (empty($this->baseQuery)) {
+ $tableName = $this->query->getTable();
+ $table = $this->pivot->db()->getTable();
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $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;
+
+ // 关联查询
+ if (null === $this->parent->getKey()) {
+ $condition = ['pivot.' . $localKey, 'exp', new Raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk())];
+ } else {
+ $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..49f4c93
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasMany.php
@@ -0,0 +1,371 @@
+
+// +----------------------------------------------------------------------
+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));
+ }
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $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..4de5060
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/HasManyThrough.php
@@ -0,0 +1,387 @@
+
+// +----------------------------------------------------------------------
+
+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();
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $fields = $this->getQueryFields($alias);
+
+ $this->query
+ ->field($fields)
+ ->alias($alias)
+ ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
+ ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
+ ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
+
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/model/relation/HasOne.php b/vendor/topthink/think-orm/src/model/relation/HasOne.php
new file mode 100644
index 0000000..7fcd20a
--- /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 ? $query->alias($model) : $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..8ec42df
--- /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..82910cb
--- /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..bc89c0b
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphOne.php
@@ -0,0 +1,347 @@
+
+// +----------------------------------------------------------------------
+
+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;
+
+ /**
+ * 绑定的关联属性
+ * @var array
+ */
+ protected $bindAttr = [];
+
+ /**
+ * 构造函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $type 多态类型
+ */
+ public function __construct(Model $parent, string $model, string $morphKey, string $morphType, string $type)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->type = $type;
+ $this->morphKey = $morphKey;
+ $this->morphType = $morphType;
+ $this->query = (new $model)->db();
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation(array $subRelation = [], Closure $closure = null)
+ {
+ if ($closure) {
+ $closure($this->getClosureType($closure));
+ }
+
+ $this->baseQuery();
+
+ $relationModel = $this->query->relation($subRelation)->find();
+
+ if ($relationModel) {
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($this->parent, $relationModel);
+ }
+
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ 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);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], Closure $closure = null, array $cache = []): void
+ {
+ $pk = $result->getPk();
+
+ if (isset($result->$pk)) {
+ $pk = $result->$pk;
+ $data = $this->eagerlyMorphToOne([
+ [$this->morphKey, '=', $pk],
+ [$this->morphType, '=', $this->type],
+ ], $subRelation, $closure, $cache);
+
+ if (isset($data[$pk])) {
+ $relationModel = $data[$pk];
+ $relationModel->setParent(clone $result);
+ $relationModel->exists(true);
+ } else {
+ $relationModel = null;
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($result, $relationModel);
+ } else {
+ // 设置关联属性
+ $result->setRelation($relation, $relationModel);
+ }
+ }
+ }
+
+ /**
+ * 多态一对一 关联模型预查询
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包
+ * @param array $cache 关联缓存
+ * @return array
+ */
+ protected function eagerlyMorphToOne(array $where, array $subRelation = [], $closure = null, array $cache = []): array
+ {
+ // 预载入关联查询 支持嵌套预载入
+ if ($closure) {
+ $this->baseQuery = true;
+ $closure($this->getClosureType($closure));
+ }
+
+ $list = $this->query
+ ->where($where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
+ $morphKey = $this->morphKey;
+
+ // 组装模型数据
+ $data = [];
+
+ foreach ($list as $set) {
+ $data[$set->$morphKey] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象
+ * @param boolean $replace 是否自动识别更新和写入
+ * @return Model|false
+ */
+ public function save($data, bool $replace = true)
+ {
+ $model = $this->make();
+ return $model->replace($replace)->save($data) ? $model : false;
+ }
+
+ /**
+ * 创建关联对象实例
+ * @param array|Model $data
+ * @return Model
+ */
+ public function make($data = []): Model
+ {
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
+
+ // 保存关联表数据
+ $pk = $this->parent->getPk();
+
+ $data[$this->morphKey] = $this->parent->$pk;
+ $data[$this->morphType] = $this->type;
+
+ return new $this->model($data);
+ }
+
+ /**
+ * 执行基础查询(进执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery(): void
+ {
+ if (empty($this->baseQuery) && $this->parent->getData()) {
+ $pk = $this->parent->getPk();
+
+ $this->query->where([
+ [$this->morphKey, '=', $this->parent->$pk],
+ [$this->morphType, '=', $this->type],
+ ]);
+ $this->baseQuery = true;
+ }
+ }
+
+ /**
+ * 绑定关联表的属性到父模型属性
+ * @access public
+ * @param array $attr 要绑定的属性列表
+ * @return $this
+ */
+ public function bind(array $attr)
+ {
+ $this->bindAttr = $attr;
+
+ return $this;
+ }
+
+ /**
+ * 获取绑定属性
+ * @access public
+ * @return array
+ */
+ public function getBindAttr(): array
+ {
+ return $this->bindAttr;
+ }
+
+ /**
+ * 绑定关联属性到父模型
+ * @access protected
+ * @param Model $result 父模型对象
+ * @param Model $model 关联模型对象
+ * @return void
+ * @throws Exception
+ */
+ protected function bindAttr(Model $result, Model $model = null): void
+ {
+ foreach ($this->bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ $value = $result->getOrigin($key);
+
+ if (!is_null($value)) {
+ throw new Exception('bind attr has exists:' . $key);
+ }
+
+ $result->setAttr($key, $model ? $model->$attr : null);
+ }
+ }
+}
diff --git a/vendor/topthink/think-orm/src/model/relation/MorphTo.php b/vendor/topthink/think-orm/src/model/relation/MorphTo.php
new file mode 100644
index 0000000..986380e
--- /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..32f853f
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/MorphToMany.php
@@ -0,0 +1,463 @@
+
+// +----------------------------------------------------------------------
+
+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();
+
+ if ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $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..d47d7f3
--- /dev/null
+++ b/vendor/topthink/think-orm/src/model/relation/OneToOne.php
@@ -0,0 +1,330 @@
+
+// +----------------------------------------------------------------------
+
+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);
+ } elseif ($this->withoutField) {
+ $this->query->withoutField($this->withoutField);
+ }
+
+ $list = $this->query
+ ->where($where)
+ ->with($subRelation)
+ ->cache($cache[0] ?? false, $cache[1] ?? null, $cache[2] ?? null)
+ ->select();
+
+ // 组装模型数据
+ $data = [];
+
+ foreach ($list as $set) {
+ if (!isset($data[$set->$key])) {
+ $data[$set->$key] = $set;
+ }
+ }
+
+ return $data;
+ }
+
+}
diff --git a/vendor/topthink/think-orm/src/paginator/driver/Bootstrap.php b/vendor/topthink/think-orm/src/paginator/driver/Bootstrap.php
new file mode 100644
index 0000000..6d55c39
--- /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-orm/stubs/Exception.php b/vendor/topthink/think-orm/stubs/Exception.php
new file mode 100644
index 0000000..0fdba9c
--- /dev/null
+++ b/vendor/topthink/think-orm/stubs/Exception.php
@@ -0,0 +1,59 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace think;
+
+/**
+ * 异常基础类
+ * @package think
+ */
+class Exception extends \Exception
+{
+ /**
+ * 保存异常页面显示的额外Debug数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 设置异常额外的Debug数据
+ * 数据将会显示为下面的格式
+ *
+ * Exception Data
+ * --------------------------------------------------
+ * Label 1
+ * key1 value1
+ * key2 value2
+ * Label 2
+ * key1 value1
+ * key2 value2
+ *
+ * @access protected
+ * @param string $label 数据分类,用于异常页面显示
+ * @param array $data 需要显示的数据,必须为关联数组
+ */
+ final protected function setData(string $label, array $data)
+ {
+ $this->data[$label] = $data;
+ }
+
+ /**
+ * 获取异常额外Debug数据
+ * 主要用于输出到异常页面便于调试
+ * @access public
+ * @return array 由setData设置的Debug数据
+ */
+ final public function getData()
+ {
+ return $this->data;
+ }
+}
diff --git a/vendor/topthink/think-orm/stubs/Facade.php b/vendor/topthink/think-orm/stubs/Facade.php
new file mode 100644
index 0000000..d801d8b
--- /dev/null
+++ b/vendor/topthink/think-orm/stubs/Facade.php
@@ -0,0 +1,65 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+class Facade
+{
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance;
+
+ protected static $instance;
+
+ /**
+ * 获取当前Facade对应类名
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {}
+
+ /**
+ * 创建Facade实例
+ * @static
+ * @access protected
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return object
+ */
+ protected static function createFacade(bool $newInstance = false)
+ {
+ $class = static::getFacadeClass() ?: 'think\DbManager';
+
+ if (static::$alwaysNewInstance) {
+ $newInstance = true;
+ }
+
+ if ($newInstance) {
+ return new $class();
+ }
+
+ if (!self::$instance) {
+ self::$instance = new $class();
+ }
+
+ return self::$instance;
+
+ }
+
+ // 调用实际类的方法
+ public static function __callStatic($method, $params)
+ {
+ return call_user_func_array([static::createFacade(), $method], $params);
+ }
+}
diff --git a/vendor/topthink/think-orm/stubs/load_stubs.php b/vendor/topthink/think-orm/stubs/load_stubs.php
new file mode 100644
index 0000000..5854cda
--- /dev/null
+++ b/vendor/topthink/think-orm/stubs/load_stubs.php
@@ -0,0 +1,9 @@
+ './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..f4e1205
--- /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..0feca8e
--- /dev/null
+++ b/vendor/topthink/think-template/src/Template.php
@@ -0,0 +1,1330 @@
+
+// +----------------------------------------------------------------------
+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();
+ if (PHP_VERSION > 8.0) {
+ ob_implicit_flush(false);
+ } else {
+ 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;
+ }
+
+ // 读取第一行
+ $line = fgets($handle);
+
+ if (false === $line) {
+ return false;
+ }
+
+ preg_match('/\/\*(.+?)\*\//', $line, $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..665a180
--- /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..f6c8fbb
--- /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..510d10a
--- /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..dd88b32
--- /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..bccafc1
--- /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-view/.gitignore b/vendor/topthink/think-view/.gitignore
new file mode 100644
index 0000000..485dee6
--- /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..8dada3e
--- /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..4e52def
--- /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..f4e6431
--- /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..562b54a
--- /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/vendor/xaboy/form-builder/LICENSE b/vendor/xaboy/form-builder/LICENSE
new file mode 100644
index 0000000..bd7924d
--- /dev/null
+++ b/vendor/xaboy/form-builder/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 xaboy
+
+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/xaboy/form-builder/README.md b/vendor/xaboy/form-builder/README.md
new file mode 100644
index 0000000..b6ffa96
--- /dev/null
+++ b/vendor/xaboy/form-builder/README.md
@@ -0,0 +1,105 @@
+
+
+
+
+
+form-builder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+PHP表单生成器,快速生成现代化的form表单。包含复选框、单选框、输入框、下拉选择框等元素以及省市区三级联动、时间选择、日期选择、颜色选择、树型、文件/图片上传等功能。
+
+
+
+
+
+## 文档
+
+[文档](http://php.form-create.com)
+
+## 环境需求
+
+> - PHP >= 5.4
+
+## 支持 UI
+
+> - IView
+> - ElementUI
+
+## 功能介绍
+
+> - 内置17种常用的表单组件
+> - 支持表单验证
+> - 支持生成任何 Vue 组件
+> - 支持栅格布局
+> - 支持注解
+> - 可以配合 [form-create](https://github.com/xaboy/form-create) 生成更复杂的表单
+
+## 内置组件
+
+> - hidden
+> - input
+> - inputNumber
+> - checkbox
+> - radio
+> - switch
+> - select
+> - autoComplete
+> - cascader
+> - colorPicker
+> - datePicker
+> - timePicker
+> - rate
+> - slider
+> - upload
+> - tree
+> - frame
+
+## 安装
+
+使用 [composer](http://getcomposer.org/):
+
+```shell
+$ composer require xaboy/form-builder:~2.0
+```
+
+## DEMO
+下载项目
+
+```shell
+git clone https://github.com/xaboy/form-builder.git
+```
+开启服务
+
+```shell
+cd form-builder
+php -S 127.0.0.1:8112
+```
+查看 Demo
+
+- elementUI : [127.0.0.1:8112/demo/elm.php](127.0.0.1:8112/demo/elm.php)
+- iview : [127.0.0.1:8112/demo/iview.php](127.0.0.1:8112/demo/iview.php)
+
+## 演示项目
+[开源的高品质微信商城](http://github.crmeb.net/u/xaboy)
+
+演示地址: [http://demo25.crmeb.net](http://demo25.crmeb.net) 账号:demo 密码:crmeb.com
+
+## 使用建议
+1. 建议将静态资源加载方式从 CDN 加载修改为自己本地资源或自己信任的 CDN
+2. 建议根据自己的业务逻辑重写默认的表单生成页 默认表单生成页
+
+
+## 组件生成效果
+
diff --git a/vendor/xaboy/form-builder/composer.json b/vendor/xaboy/form-builder/composer.json
new file mode 100644
index 0000000..89d33cc
--- /dev/null
+++ b/vendor/xaboy/form-builder/composer.json
@@ -0,0 +1,36 @@
+{
+ "name": "xaboy/form-builder",
+ "description": "PHP表单生成器,快速生成现代化的form表单。包含复选框、单选框、输入框、下拉选择框等元素以及,省市区三级联动,时间选择,日期选择,颜色选择,文件/图片上传等功能。",
+ "license": "MIT",
+ "keywords": [
+ "form",
+ "form-create",
+ "form-generator",
+ "dynamic-form",
+ "element-ui",
+ "iview"
+ ],
+ "homepage": "https://github.com/xaboy/form-builder",
+ "authors": [
+ {
+ "name": "xaboy",
+ "email": "xaboy2005@qq.com"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "FormBuilder\\": "./src/"
+ }
+ },
+ "support": {
+ "issues": "https://github.com/xaboy/form-builder/issues",
+ "source": "https://github.com/xaboy/form-builder"
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "ext-json": "*",
+ "symfony/http-foundation": ">=2.6",
+ "doctrine/annotations": "^1.2.7"
+ },
+ "minimum-stability": "stable"
+}
diff --git a/vendor/xaboy/form-builder/demo/demo.php b/vendor/xaboy/form-builder/demo/demo.php
new file mode 100644
index 0000000..5f631ec
--- /dev/null
+++ b/vendor/xaboy/form-builder/demo/demo.php
@@ -0,0 +1,34 @@
+required();
+$textarea = Elm::textarea('goods_info', '商品简介');
+$switch = Elm::switches('is_open', '是否开启')->activeText('开启')->inactiveText('关闭');
+
+//创建表单
+$form = (new IviewForm($action))->setMethod($method);
+
+//添加组件
+$form->setRule([$input, $textarea]);
+$form->append($switch);
+
+$form->formData([
+ 'goods_name' => 'goods_name123',
+ 'asdf' => 'asdfafd',
+ 'is_open' => '0'
+])->setValue('goods_info', "asdf\r\nadfa");
+
+//生成表单页面
+$formHtml = $form->view();
+
+echo $formHtml;
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/demo/elm.php b/vendor/xaboy/form-builder/demo/elm.php
new file mode 100644
index 0000000..f7e1321
--- /dev/null
+++ b/vendor/xaboy/form-builder/demo/elm.php
@@ -0,0 +1,162 @@
+ '开启时间',
+ 'star' => '点赞'
+ ];
+
+ protected $scene = 'get';
+
+ protected function getScene()
+ {
+// $this->except = ['goods_name'];
+ }
+
+ /**
+ * @Col(6)
+ * @return \FormBuilder\UI\Elm\Components\Input
+ */
+ public function goods_name_field()
+ {
+ return Elm::input('goods_name', '商品名称')->required();
+ }
+
+ /**
+ * @Required()
+ * @Group()
+ * @Col(8)
+ * @Range({10,1000},message = "最少输入10个字")
+ * @return \FormBuilder\UI\Elm\Components\Input
+ */
+ public function goods_info_field()
+ {
+ return Elm::textarea('goods_info', '商品简介');
+ }
+
+ /**
+ * @Group()
+ * @Col(8)
+ * @Emit({"change","click"})
+ * @return \FormBuilder\UI\Elm\Components\Switches
+ */
+ public function is_open_field()
+ {
+ return Elm::switches('is_open', '是否开启');
+ }
+
+ /**
+ * @Group(2)
+ * @Col(12)
+ * @return \FormBuilder\UI\Elm\Components\Frame
+ */
+ public function frame_field()
+ {
+ return Elm::frameFile('as', 'asd', 'afsdfasdf');
+ }
+
+ /**
+ * @Group(2)
+ * @Col(12)
+ * @return \FormBuilder\UI\Elm\Components\Upload
+ */
+ public function test_field()
+ {
+ return Elm::uploadFiles('aaa', 'aaa', 'bbb', [1])->required();
+ }
+
+ /**
+ * @return \FormBuilder\UI\Elm\Components\Hidden
+ */
+ public function id_field()
+ {
+ return Elm::hidden('1', '1');
+ }
+
+ /**
+ * @Required("请输入 testRow")
+ * @return array
+ */
+ public function row_field()
+ {
+// return [
+// 'type' => 'row',
+// 'children' =>
+// [
+ return [
+ 'type' => 'input',
+ 'field' => 'row',
+ 'title' => 'test Row',
+ 'value' => '123',
+ 'col' => [
+ 'span' => 12
+ ]
+ ];
+// ,
+// Elm::input('row2', 'row2', 'asdf')->col(12)
+// ],
+// 'native' => true
+// ];
+ }
+
+ /**
+ * 通过依赖注入方式生成组件
+ *
+ * @param DatePicker $date
+ * @return DatePicker
+ */
+ public function start_time_field(DatePicker $date)
+ {
+ return $date->required()->info('asdfasdfasdfsf');
+ }
+
+ public function starField(Rate $rate)
+ {
+ return $rate;
+ }
+
+ protected function getFormConfig()
+ {
+ $config = Elm::config();
+ $config->createResetBtn()->show(true);
+
+ return $config;
+ }
+
+ protected function getFormData()
+ {
+ return [
+ 'goods_name' => 'goods_name123',
+ 'asdf' => 'asdfafd',
+ 'is_open' => '0',
+ 'goods_info' => "asdf\r\nadfa",
+ 'star' => 0,
+ 'row' => 'adsfasdfasd'
+ ];
+ }
+}
+
+$formHtml = (new GoodsForm())->view();
+//$formHtml = (new GoodsForm())->form()->view();
+
+echo $formHtml;
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/demo/iview.php b/vendor/xaboy/form-builder/demo/iview.php
new file mode 100644
index 0000000..4b57ea1
--- /dev/null
+++ b/vendor/xaboy/form-builder/demo/iview.php
@@ -0,0 +1,140 @@
+ '开启时间',
+ 'star' => '点赞'
+ ];
+
+ protected $scene = 'get';
+
+ protected function getScene()
+ {
+// $this->except = ['goods_name'];
+ }
+
+ /**
+ * @Col(12)
+ * @return \FormBuilder\UI\Iview\Components\Input
+ */
+ public function goods_name_field()
+ {
+ return Iview::input('goods_name', '商品名称')->required();
+ }
+
+ /**
+ * @Group(className="test")
+ * @Col(12)
+ * @return \FormBuilder\UI\Iview\Components\Input
+ */
+ public function goods_info_field()
+ {
+ return Iview::textarea('goods_info', '商品简介');
+ }
+
+ /**
+ * @Group()
+ * @Col(12)
+ * @return \FormBuilder\UI\Iview\Components\Switches
+ */
+ public function is_open_field()
+ {
+ return Iview::switches('is_open', '是否开启');
+ }
+
+ public function id_field()
+ {
+ return Iview::hidden('1', '1');
+ }
+
+ public function frame_field()
+ {
+ return Iview::frame('as', 'asd', 'afsdfasdf');
+ }
+
+ public function test_field()
+ {
+ return Iview::dateTime('aaa', 'aaa')->required();
+ }
+
+ public function row_field()
+ {
+ return [
+ 'type' => 'row',
+ 'children' => [
+ [
+ 'type' => 'input',
+ 'field' => 'row',
+ 'title' => 'test Row',
+ 'value' => '123',
+ 'col' => [
+ 'span' => 12
+ ]
+ ],
+ Iview::input('row2', 'row2', 'asdf')->col(12)
+ ],
+ 'native' => true
+ ];
+ }
+
+ /**
+ * 通过依赖注入方式生成组件
+ *
+ * @param DatePicker $date
+ * @return DatePicker
+ */
+ public function start_time_field(DatePicker $date)
+ {
+ return $date->required()->info('asdfasdfasdfsf');
+ }
+
+ public function starField(Rate $rate)
+ {
+ return $rate;
+ }
+
+ protected function getFormConfig()
+ {
+ $config = Iview::config();
+ $config->createResetBtn()->show(true);
+
+ return $config;
+ }
+
+ protected function getFormData()
+ {
+ return [
+ 'goods_name' => 'goods_name123',
+ 'asdf' => 'asdfafd',
+ 'is_open' => '0',
+ 'goods_info' => "asdf\r\nadfa",
+ 'start_time' => '1999-11-11',
+ 'star' => 0,
+ 'row' => 'adsfasdfasd'
+ ];
+ }
+}
+
+$formHtml = (new GoodsForm())->view();
+//$formHtml = (new GoodsForm())->form()->view();
+
+echo $formHtml;
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/images/components.png b/vendor/xaboy/form-builder/images/components.png
new file mode 100644
index 0000000..2429f4e
Binary files /dev/null and b/vendor/xaboy/form-builder/images/components.png differ
diff --git a/vendor/xaboy/form-builder/images/demo02.jpg b/vendor/xaboy/form-builder/images/demo02.jpg
new file mode 100644
index 0000000..21fdcf7
Binary files /dev/null and b/vendor/xaboy/form-builder/images/demo02.jpg differ
diff --git a/vendor/xaboy/form-builder/images/demo03.jpg b/vendor/xaboy/form-builder/images/demo03.jpg
new file mode 100644
index 0000000..655bb6e
Binary files /dev/null and b/vendor/xaboy/form-builder/images/demo03.jpg differ
diff --git a/vendor/xaboy/form-builder/images/demo04.jpg b/vendor/xaboy/form-builder/images/demo04.jpg
new file mode 100644
index 0000000..fef1f46
Binary files /dev/null and b/vendor/xaboy/form-builder/images/demo04.jpg differ
diff --git a/vendor/xaboy/form-builder/src/Annotation/AnnotationReader.php b/vendor/xaboy/form-builder/src/Annotation/AnnotationReader.php
new file mode 100644
index 0000000..f62681b
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/AnnotationReader.php
@@ -0,0 +1,122 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+
+use Doctrine\Common\Annotations\AnnotationRegistry;
+use Doctrine\Common\Annotations\AnnotationReader as Render;
+use FormBuilder\Contract\AnnotationInterface;
+use FormBuilder\FormHandle;
+use FormBuilder\Util;
+
+class AnnotationReader
+{
+ protected static $isInit = false;
+
+ /**
+ * @var Render
+ */
+ protected $annotationReader;
+
+ /**
+ * @var FormHandle
+ */
+ protected $handle;
+
+ public function __construct(FormHandle $handle)
+ {
+ if (!self::$isInit) {
+ AnnotationRegistry::registerLoader('class_exists');
+ self::$isInit = true;
+ }
+
+ $this->annotationReader = new Render();
+ $this->handle = $handle;
+ }
+
+ public function getRender()
+ {
+ return $this->annotationReader;
+ }
+
+ /**
+ * @return array
+ * @throws \ReflectionException
+ */
+ public function render()
+ {
+ $reflectionClass = new \ReflectionClass($this->handle);
+ $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
+ $rule = [];
+ $except = $this->handle->getExcept();
+ foreach ($methods as $method) {
+ $field = preg_replace('/^(.+)(Field|_field)$/', '$1', $method->name);
+ $value = null;
+ if ($field != $method->name && !in_array($field, $except)) {
+ $params = $method->getParameters();
+ if (isset($params[0]) && ($dep = $params[0]->getClass())) {
+ if (in_array('FormBuilder\\Contract\\FormComponentInterface', $dep->getInterfaceNames())) {
+ $componentClass = $dep->getName();
+ $value = $method->invokeArgs($this->handle, [new $componentClass($field, $this->handle->getFieldTitle($field))]);
+ }
+ }
+ if (is_null($value)) $value = $method->invoke($this->handle);
+ if (!is_null($value) && (($isArray = is_array($value)) || Util::isComponent($value))) {
+ $rule[] = compact('value', 'method', 'isArray');
+ }
+ }
+ }
+ return $this->parse($rule);
+ }
+
+ /**
+ * @param $rules
+ * @return array
+ */
+ protected function parse($rules)
+ {
+ $formRule = [];
+ $groupList = [];
+ foreach ($rules as $rule) {
+ $annotations = $this->annotationReader->getMethodAnnotations($rule['method']);
+ $value = $rule['value'];
+ $group = null;
+ foreach ($annotations as $annotation) {
+ if (!$annotation instanceof AnnotationInterface) continue;
+ if ($annotation instanceof Group) {
+ $group = $annotation;
+ } else {
+ $value = $rule['isArray'] ? $annotation->parseRule($value) : $annotation->parseComponent($value);
+ }
+ }
+
+ if (!is_null($group)) {
+ if (!isset($groupList[$group->id])) {
+ $groupList[$group->id] = $group;
+ $formRule[] = $group;
+ }
+ $groupList[$group->id]->appendChildren($value);
+ } else {
+ $formRule[] = $value;
+ }
+ }
+
+ foreach ($formRule as $k => $v) {
+ if ($v instanceof Group) {
+ $formRule[$k] = $v->parse($this->handle->ui());
+ }
+ }
+
+ return $formRule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/ClassName.php b/vendor/xaboy/form-builder/src/Annotation/ClassName.php
new file mode 100644
index 0000000..573852e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/ClassName.php
@@ -0,0 +1,39 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ */
+final class ClassName implements AnnotationInterface
+{
+
+ /**
+ * @var string
+ */
+ public $className = '';
+
+ public function parseRule($rule)
+ {
+ $rule['className'] = $this->className;
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->className($this->className);
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Col.php b/vendor/xaboy/form-builder/src/Annotation/Col.php
new file mode 100644
index 0000000..22926fb
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Col.php
@@ -0,0 +1,45 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ */
+final class Col implements AnnotationInterface
+{
+ public $props = 24;
+
+ protected function getCol()
+ {
+ if (is_integer($this->props))
+ return ['span' => $this->props];
+ else
+ return $this->props;
+ }
+
+ public function parseRule($rule)
+ {
+ $rule['col'] = $this->getCol();
+
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->col($this->getCol());
+
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Emit.php b/vendor/xaboy/form-builder/src/Annotation/Emit.php
new file mode 100644
index 0000000..1a26cd9
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Emit.php
@@ -0,0 +1,47 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ */
+final class Emit implements AnnotationInterface
+{
+ /**
+ * @var array
+ */
+ public $emit = [];
+
+ /**
+ * @var string
+ */
+ public $prefix = '';
+
+
+ public function parseRule($rule)
+ {
+ $rule['emit'] = $this->emit;
+ if ($this->prefix)
+ $rule['emitPrefix'] = $this->prefix;
+
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->emit($this->emit)->emitPrefix($this->prefix);
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Group.php b/vendor/xaboy/form-builder/src/Annotation/Group.php
new file mode 100644
index 0000000..bc88c1a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Group.php
@@ -0,0 +1,86 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ */
+final class Group implements AnnotationInterface
+{
+ /**
+ * @var int
+ */
+ public $id = 1;
+
+ /**
+ * @var string
+ */
+ public $tag;
+
+ /**
+ * @var string
+ */
+ public $className = '';
+
+ public $span = 24;
+
+ protected $children = [];
+
+ public function parseRule($rule)
+ {
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ return $component;
+ }
+
+ public function appendChildren($rule)
+ {
+ $this->children[] = $rule;
+ }
+
+ /**
+ * @param string $UI
+ * @return array
+ */
+ public function parse($UI)
+ {
+ $col = 'i-col';
+ $row = 'row';
+ if ($UI == 'elm') {
+ $row = 'el-row';
+ $col = 'el-col';
+ }
+
+ return [
+ 'type' => $col,
+ 'props' => [
+ 'span' => $this->span
+ ],
+ 'children' => [
+ [
+ 'type' => is_null($this->tag) ? $row : $this->tag,
+ 'children' => $this->children,
+ 'attrs' => [
+ 'class' => $this->className
+ ]
+ ]
+ ],
+ 'native' => true
+ ];
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Info.php b/vendor/xaboy/form-builder/src/Annotation/Info.php
new file mode 100644
index 0000000..7f7ab8f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Info.php
@@ -0,0 +1,40 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ */
+final class Info implements AnnotationInterface
+{
+ /**
+ * @var string
+ */
+ public $info = '';
+
+ public function parseRule($rule)
+ {
+ $rule['info'] = $this->info;
+
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->info($this->info);
+
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Enum.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Enum.php
new file mode 100644
index 0000000..0609f57
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Enum.php
@@ -0,0 +1,40 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Enum extends ValidateAnnotation
+{
+ /**
+ * @Required
+ * @var array
+ */
+ public $value = [];
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['enum' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->enum($this->value)->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Len.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Len.php
new file mode 100644
index 0000000..6a627c7
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Len.php
@@ -0,0 +1,39 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Len extends ValidateAnnotation
+{
+ /**
+ * @Required
+ */
+ public $value;
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['len' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->len($this->value)->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Max.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Max.php
new file mode 100644
index 0000000..aead994
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Max.php
@@ -0,0 +1,39 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Max extends ValidateAnnotation
+{
+ /**
+ * @Required
+ */
+ public $value;
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['max' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->max($this->value)->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Min.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Min.php
new file mode 100644
index 0000000..5518d62
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Min.php
@@ -0,0 +1,39 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Min extends ValidateAnnotation
+{
+ /**
+ * @Required
+ */
+ public $value;
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['min' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->min($this->value)->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Pattern.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Pattern.php
new file mode 100644
index 0000000..f5a1dc2
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Pattern.php
@@ -0,0 +1,40 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Pattern extends ValidateAnnotation
+{
+ /**
+ * @Required
+ * @var string
+ */
+ public $value = '';
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['pattern' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->pattern($this->value)->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Range.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Range.php
new file mode 100644
index 0000000..beba0e4
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Range.php
@@ -0,0 +1,40 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+/**
+ * @Annotation
+ */
+final class Range extends ValidateAnnotation
+{
+ /**
+ * @Required
+ * @var array
+ */
+ public $value = [];
+
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['validate'][] = ['range' => $this->value, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->appendValidate($component->createValidate()->range($this->value[0], $this->value[1])->message($this->message));
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/Required.php b/vendor/xaboy/form-builder/src/Annotation/Validate/Required.php
new file mode 100644
index 0000000..e809e7e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/Required.php
@@ -0,0 +1,38 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+use FormBuilder\Contract\AnnotationInterface;
+
+/**
+ * @Annotation
+ * Class RuleAnnotation
+ * @package FormBuilder
+ */
+final class Required extends ValidateAnnotation
+{
+
+ public function parseRule($rule)
+ {
+ $rule = $this->tidyValidate($rule);
+ $rule['required'] = true;
+ $rule['validate'][] = ['required' => true, 'type' => $this->type, 'trigger' => $this->trigger, 'message' => $this->message];
+ return $rule;
+ }
+
+ public function parseComponent($component)
+ {
+ $component->required($this->message);
+ return $component;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Annotation/Validate/ValidateAnnotation.php b/vendor/xaboy/form-builder/src/Annotation/Validate/ValidateAnnotation.php
new file mode 100644
index 0000000..56e44bf
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Annotation/Validate/ValidateAnnotation.php
@@ -0,0 +1,44 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Annotation\Validate;
+
+
+use FormBuilder\Contract\AnnotationInterface;
+
+abstract class ValidateAnnotation implements AnnotationInterface
+{
+
+ /**
+ * @var string
+ */
+ public $message;
+
+ /**
+ * @var string
+ */
+ public $type = 'string';
+
+ /**
+ * @var string
+ */
+ public $trigger = 'change';
+
+
+ public function tidyValidate($rule)
+ {
+ if (!isset($rule['validate']) || !is_array($rule['validate'])) {
+ $rule['validate'] = [];
+ }
+ return $rule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/AnnotationInterface.php b/vendor/xaboy/form-builder/src/Contract/AnnotationInterface.php
new file mode 100644
index 0000000..d98be87
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/AnnotationInterface.php
@@ -0,0 +1,29 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface AnnotationInterface
+{
+ /**
+ * @param array $rule
+ * @return array
+ */
+ public function parseRule($rule);
+
+ /**
+ * @param CustomComponentInterface $component
+ * @return CustomComponentInterface
+ */
+ public function parseComponent($component);
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/BootstrapInterface.php b/vendor/xaboy/form-builder/src/Contract/BootstrapInterface.php
new file mode 100644
index 0000000..b198747
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/BootstrapInterface.php
@@ -0,0 +1,27 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+use FormBuilder\Form;
+
+interface BootstrapInterface
+{
+ /**
+ * 初始化
+ *
+ * @param Form $form
+ * @return void
+ */
+ public function init(Form $form);
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/ColComponentInterface.php b/vendor/xaboy/form-builder/src/Contract/ColComponentInterface.php
new file mode 100644
index 0000000..210ccf0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/ColComponentInterface.php
@@ -0,0 +1,19 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface ColComponentInterface
+{
+ public function getCol();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/ConfigInterface.php b/vendor/xaboy/form-builder/src/Contract/ConfigInterface.php
new file mode 100644
index 0000000..d2e5eae
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/ConfigInterface.php
@@ -0,0 +1,44 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface ConfigInterface
+{
+ public function info($type);
+
+ public function formStyle($formStyle);
+
+ public function row($row);
+
+ public function submitBtn($submitBtn);
+
+ public function resetBtn($resetBtn);
+
+ public function injectEvent($bool);
+
+ /**
+ * @param string $componentName
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalConfig($componentName, array $config);
+
+ /**
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalCommonConfig(array $config);
+
+ public function getConfig();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/CustomComponentInterface.php b/vendor/xaboy/form-builder/src/Contract/CustomComponentInterface.php
new file mode 100644
index 0000000..34e124b
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/CustomComponentInterface.php
@@ -0,0 +1,25 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface CustomComponentInterface
+{
+
+ /**
+ * 获取组件的生成规则
+ *
+ * @return array
+ */
+ public function build();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/FormComponentInterface.php b/vendor/xaboy/form-builder/src/Contract/FormComponentInterface.php
new file mode 100644
index 0000000..38a8a4b
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/FormComponentInterface.php
@@ -0,0 +1,20 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface FormComponentInterface extends CustomComponentInterface
+{
+ public function __construct($field, $title);
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/FormHandleInterface.php b/vendor/xaboy/form-builder/src/Contract/FormHandleInterface.php
new file mode 100644
index 0000000..e56fc19
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/FormHandleInterface.php
@@ -0,0 +1,11 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface OptionComponentInterface
+{
+ public function getOption();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/StyleInterface.php b/vendor/xaboy/form-builder/src/Contract/StyleInterface.php
new file mode 100644
index 0000000..203bda1
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/StyleInterface.php
@@ -0,0 +1,19 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface StyleInterface
+{
+ public function getStyle();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Contract/ValidateInterface.php b/vendor/xaboy/form-builder/src/Contract/ValidateInterface.php
new file mode 100644
index 0000000..9f08c49
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Contract/ValidateInterface.php
@@ -0,0 +1,25 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Contract;
+
+
+interface ValidateInterface
+{
+
+ /**
+ * 获取验证规则
+ *
+ * @return array
+ */
+ public function getValidate();
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Driver/CustomComponent.php b/vendor/xaboy/form-builder/src/Driver/CustomComponent.php
new file mode 100644
index 0000000..8812287
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Driver/CustomComponent.php
@@ -0,0 +1,121 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Driver;
+
+
+use FormBuilder\Contract\CustomComponentInterface;
+use FormBuilder\Rule\BaseRule;
+use FormBuilder\Rule\CallPropsRule;
+use FormBuilder\Rule\ChildrenRule;
+use FormBuilder\Rule\ControlRule;
+use FormBuilder\Rule\EmitRule;
+use FormBuilder\Rule\PropsRule;
+use FormBuilder\Rule\ValidateRule;
+
+/**
+ * 自定义组件
+ * Class CustomComponent
+ */
+class CustomComponent implements CustomComponentInterface, \JsonSerializable, \ArrayAccess
+{
+ use BaseRule;
+ use ChildrenRule;
+ use EmitRule;
+ use PropsRule;
+ use ValidateRule;
+ use CallPropsRule;
+ use ControlRule;
+
+ protected static $propsRule = [];
+
+ protected $defaultProps = [];
+
+ protected $appendRule = [];
+
+ /**
+ * CustomComponent constructor.
+ * @param null|string $type
+ */
+ public function __construct($type = null)
+ {
+ $this->setRuleType(is_null($type) ? $this->getComponentName() : $type)->props($this->defaultProps);
+ }
+
+ public function __toString()
+ {
+ return $this->toJson();
+ }
+
+ public function __invoke()
+ {
+ return $this->build();
+ }
+
+ public function toJson()
+ {
+ return json_encode($this->build());
+ }
+
+ protected function getComponentName()
+ {
+ return lcfirst(basename(str_replace('\\', '/', get_class($this))));
+ }
+
+ public function appendRule($name, $value)
+ {
+ $this->appendRule[$name] = $name == 'props' ? (object)$value : $value;
+ return $this;
+ }
+
+ public function getRule()
+ {
+ return array_merge(
+ $this->parseBaseRule(),
+ $this->parseEmitRule(),
+ $this->parsePropsRule(),
+ $this->parseValidateRule(),
+ $this->parseChildrenRule(),
+ $this->parseControlRule()
+ );
+ }
+
+ public function build()
+ {
+ return $this->appendRule + $this->getRule();
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->build();
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->props[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->props[$offset];
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ $this->props[$offset] = $value;
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->props[$offset]);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Driver/FormComponent.php b/vendor/xaboy/form-builder/src/Driver/FormComponent.php
new file mode 100644
index 0000000..7f40cb0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Driver/FormComponent.php
@@ -0,0 +1,69 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Driver;
+
+
+use FormBuilder\Contract\FormComponentInterface;
+use FormBuilder\Contract\ValidateInterface;
+
+abstract class FormComponent extends CustomComponent implements FormComponentInterface
+{
+ protected $defaultValue = '';
+
+ protected $selectComponent = false;
+
+ /**
+ * FormComponentDriver constructor.
+ *
+ * @param string $field 字段名
+ * @param string $title 字段昵称
+ * @param mixed $value 字段值
+ */
+ public function __construct($field, $title, $value = null)
+ {
+ parent::__construct();
+ $this->field($field)->title($title)->value(is_null($value) ? $this->defaultValue : $value);
+ if (isset(static::$propsRule['placeholder']))
+ $this->placeholder($this->getPlaceHolder());
+ $this->init();
+ }
+
+ protected function init()
+ {
+ }
+
+ /**
+ * @return ValidateInterface
+ */
+ abstract public function createValidate();
+
+
+ /**
+ * @param null|string $message
+ * @return $this
+ */
+ public function required($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ $this->appendValidate($this->createValidate()->message($message)->required());
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getPlaceHolder()
+ {
+ return ($this->selectComponent ? '请选择' : '请输入') . $this->title;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Driver/FormOptionsComponent.php b/vendor/xaboy/form-builder/src/Driver/FormOptionsComponent.php
new file mode 100644
index 0000000..94bcd91
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Driver/FormOptionsComponent.php
@@ -0,0 +1,29 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Driver;
+
+
+use FormBuilder\Contract\FormOptionsComponentInterface;
+use FormBuilder\Rule\OptionsRule;
+
+abstract class FormOptionsComponent extends FormComponent implements FormOptionsComponentInterface
+{
+ use OptionsRule;
+
+ public function getRule()
+ {
+ $rule = parent::getRule();
+
+ return array_merge($rule, $this->parseOptionsRule());
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Exception/FormBuilderException.php b/vendor/xaboy/form-builder/src/Exception/FormBuilderException.php
new file mode 100644
index 0000000..0b924ed
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Exception/FormBuilderException.php
@@ -0,0 +1,18 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Exception;
+
+class FormBuilderException extends \Exception
+{
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Factory/Base.php b/vendor/xaboy/form-builder/src/Factory/Base.php
new file mode 100644
index 0000000..5604f90
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Factory/Base.php
@@ -0,0 +1,30 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Factory;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+abstract class Base
+{
+ /**
+ * 创建自定义组件
+ *
+ * @param string $type
+ * @return CustomComponent
+ */
+ public static function createComponent($type)
+ {
+ return new CustomComponent($type);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Factory/Elm.php b/vendor/xaboy/form-builder/src/Factory/Elm.php
new file mode 100644
index 0000000..848c59a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Factory/Elm.php
@@ -0,0 +1,154 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Factory;
+
+use FormBuilder\Exception\FormBuilderException;
+use FormBuilder\Form;
+use FormBuilder\UI\Elm\Components\Button;
+use FormBuilder\UI\Elm\Components\Option;
+use FormBuilder\UI\Elm\Components\Popover;
+use FormBuilder\UI\Elm\Components\Tooltip;
+use FormBuilder\UI\Elm\Config;
+use FormBuilder\UI\Elm\Traits\CascaderFactoryTrait;
+use FormBuilder\UI\Elm\Traits\CheckBoxFactoryTrait;
+use FormBuilder\UI\Elm\Traits\ColorPickerFactoryTrait;
+use FormBuilder\UI\Elm\Traits\DatePickerFactoryTrait;
+use FormBuilder\UI\Elm\Traits\FrameFactoryTrait;
+use FormBuilder\UI\Elm\Traits\FormStyleFactoryTrait;
+use FormBuilder\UI\Elm\Traits\GroupFactoryTrait;
+use FormBuilder\UI\Elm\Traits\HiddenFactoryTrait;
+use FormBuilder\UI\Elm\Traits\InputFactoryTrait;
+use FormBuilder\UI\Elm\Traits\InputNumberFactoryTrait;
+use FormBuilder\UI\Elm\Traits\RadioFactoryTrait;
+use FormBuilder\UI\Elm\Traits\RateFactoryTrait;
+use FormBuilder\UI\Elm\Traits\SelectFactoryTrait;
+use FormBuilder\UI\Elm\Traits\SliderFactoryTrait;
+use FormBuilder\UI\Elm\Traits\SwitchesFactoryTrait;
+use FormBuilder\UI\Elm\Traits\TimePickerFactoryTrait;
+use FormBuilder\UI\Elm\Traits\TreeFactoryTrait;
+use FormBuilder\UI\Elm\Traits\UploadFactoryTrait;
+use FormBuilder\UI\Elm\Traits\ValidateFactoryTrait;
+
+abstract class Elm extends Base
+{
+ use CascaderFactoryTrait;
+ use CheckBoxFactoryTrait;
+ use ColorPickerFactoryTrait;
+ use DatePickerFactoryTrait;
+ use FrameFactoryTrait;
+ use HiddenFactoryTrait;
+ use InputNumberFactoryTrait;
+ use InputFactoryTrait;
+ use RadioFactoryTrait;
+ use RateFactoryTrait;
+ use SliderFactoryTrait;
+ use SelectFactoryTrait;
+ use FormStyleFactoryTrait;
+ use SwitchesFactoryTrait;
+ use TimePickerFactoryTrait;
+ use TreeFactoryTrait;
+ use UploadFactoryTrait;
+ use ValidateFactoryTrait;
+ use GroupFactoryTrait;
+
+ /**
+ * 获取选择类组件 option 类
+ *
+ * @param string|number $value
+ * @param string $label
+ * @param bool $disabled
+ * @return Option
+ */
+ public static function option($value, $label = '', $disabled = false)
+ {
+ return new Option($value, $label, $disabled);
+ }
+
+ /**
+ * @param array $rule
+ * @return \FormBuilder\UI\Elm\Style\FormStyle
+ * @author xaboy
+ * @day 2020/5/22
+ */
+ public static function formStyle(array $rule = [])
+ {
+ return self::style($rule);
+ }
+
+ /**
+ * 全局配置
+ *
+ * @param array $config
+ * @return Config
+ */
+ public static function config(array $config = [])
+ {
+ return new Config($config);
+ }
+
+ /**
+ * 组件提示消息配置 Popover
+ *
+ * @return Popover
+ */
+ public static function popover()
+ {
+ return new Popover();
+ }
+
+ /**
+ * 组件提示消息配置 Tooltip
+ *
+ * @return Tooltip
+ */
+ public static function tooltip()
+ {
+ return new Tooltip();
+ }
+
+ /**
+ * 按钮组件
+ *
+ * @return Button
+ */
+ public static function button()
+ {
+ return new Button();
+ }
+
+
+ /**
+ * 创建表单
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function createForm($action = '', $rule = [], $config = [])
+ {
+ return Form::elm($action, $rule, $config);
+ }
+
+ /**
+ * 组件分组
+ *
+ * @param array $children
+ * @return \FormBuilder\Driver\CustomComponent
+ */
+ public static function item($children = [])
+ {
+ return self::createComponent('el-row')->children($children);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Factory/Iview.php b/vendor/xaboy/form-builder/src/Factory/Iview.php
new file mode 100644
index 0000000..44114bb
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Factory/Iview.php
@@ -0,0 +1,169 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Factory;
+
+use FormBuilder\Exception\FormBuilderException;
+use FormBuilder\Form;
+use FormBuilder\UI\Iview\Components\Button;
+use FormBuilder\UI\Iview\Components\Option;
+use FormBuilder\UI\Iview\Components\Poptip;
+use FormBuilder\UI\Iview\Components\Tooltip;
+use FormBuilder\UI\Iview\Config;
+use FormBuilder\UI\Iview\Traits\CascaderFactoryTrait;
+use FormBuilder\UI\Iview\Traits\CheckBoxFactoryTrait;
+use FormBuilder\UI\Iview\Traits\ColorPickerFactoryTrait;
+use FormBuilder\UI\Iview\Traits\DatePickerFactoryTrait;
+use FormBuilder\UI\Iview\Traits\FrameFactoryTrait;
+use FormBuilder\UI\Iview\Traits\FormStyleFactoryTrait;
+use FormBuilder\UI\Iview\Traits\GroupFactoryTrait;
+use FormBuilder\UI\Iview\Traits\HiddenFactoryTrait;
+use FormBuilder\UI\Iview\Traits\InputFactoryTrait;
+use FormBuilder\UI\Iview\Traits\InputNumberFactoryTrait;
+use FormBuilder\UI\Iview\Traits\RadioFactoryTrait;
+use FormBuilder\UI\Iview\Traits\RateFactoryTrait;
+use FormBuilder\UI\Iview\Traits\SelectFactoryTrait;
+use FormBuilder\UI\Iview\Traits\SliderFactoryTrait;
+use FormBuilder\UI\Iview\Traits\SwitchesFactoryTrait;
+use FormBuilder\UI\Iview\Traits\TimePickerFactoryTrait;
+use FormBuilder\UI\Iview\Traits\TreeFactoryTrait;
+use FormBuilder\UI\Iview\Traits\UploadFactoryTrait;
+use FormBuilder\UI\Iview\Traits\ValidateFactoryTrait;
+
+abstract class Iview extends Base
+{
+ use CascaderFactoryTrait;
+ use CheckBoxFactoryTrait;
+ use ColorPickerFactoryTrait;
+ use DatePickerFactoryTrait;
+ use FrameFactoryTrait;
+ use HiddenFactoryTrait;
+ use InputNumberFactoryTrait;
+ use InputFactoryTrait;
+ use RadioFactoryTrait;
+ use RateFactoryTrait;
+ use SliderFactoryTrait;
+ use SelectFactoryTrait;
+ use FormStyleFactoryTrait;
+ use SwitchesFactoryTrait;
+ use TimePickerFactoryTrait;
+ use TreeFactoryTrait;
+ use UploadFactoryTrait;
+ use ValidateFactoryTrait;
+ use GroupFactoryTrait;
+
+ /**
+ * 获取选择类组件 option 类
+ *
+ * @param string|number $value
+ * @param string $label
+ * @param bool $disabled
+ * @return Option
+ */
+ public static function option($value, $label = '', $disabled = false)
+ {
+ return new Option($value, $label, $disabled);
+ }
+
+ /**
+ * @param array $rule
+ * @return \FormBuilder\UI\Iview\Style\FormStyle
+ * @author xaboy
+ * @day 2020/5/22
+ */
+ public static function formStyle(array $rule = [])
+ {
+ return self::style($rule);
+ }
+
+ /**
+ * 全局配置
+ *
+ * @param array $config
+ * @return Config
+ */
+ public static function config(array $config = [])
+ {
+ return new Config($config);
+ }
+
+ /**
+ * 组件提示消息配置 Poptip
+ *
+ * @return Poptip
+ */
+ public static function poptip()
+ {
+ return new Poptip();
+ }
+
+ /**
+ * 组件提示消息配置 Tooltip
+ *
+ * @return Tooltip
+ */
+ public static function tooltip()
+ {
+ return new Tooltip();
+ }
+
+ /**
+ * 按钮组件
+ *
+ * @return Button
+ */
+ public static function button()
+ {
+ return new Button();
+ }
+
+
+ /**
+ * 创建表单
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function createForm($action = '', $rule = [], $config = [])
+ {
+ return Form::iview($action, $rule, $config);
+ }
+
+
+ /**
+ * 创建表单 v4版本
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function createFormV4($action = '', $rule = [], $config = [])
+ {
+ return Form::iview4($action, $rule, $config);
+ }
+
+ /**
+ * 组件分组
+ *
+ * @param array $children
+ * @return \FormBuilder\Driver\CustomComponent
+ */
+ public static function item($children = [])
+ {
+ return self::createComponent('row')->children($children);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Form.php b/vendor/xaboy/form-builder/src/Form.php
new file mode 100644
index 0000000..d4e7c79
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Form.php
@@ -0,0 +1,535 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder;
+
+
+use FormBuilder\Contract\BootstrapInterface;
+use FormBuilder\Contract\ConfigInterface;
+use FormBuilder\Exception\FormBuilderException;
+use FormBuilder\UI\Iview\Bootstrap as IViewBootstrap;
+use FormBuilder\UI\Elm\Bootstrap as ElmBootstrap;
+
+class Form
+{
+ protected $headers = [];
+
+ protected $formContentType = 'application/x-www-form-urlencoded';
+
+ /**
+ * @var BootstrapInterface
+ */
+ protected $ui;
+
+ /**
+ * @var array|ConfigInterface
+ */
+ protected $config;
+
+ protected $action;
+
+ protected $method = 'POST';
+
+ protected $title = 'FormBuilder';
+
+ protected $rule;
+
+ protected $formData = [];
+
+ protected $dependScript = [
+ '',
+ '',
+ '',
+ ''
+ ];
+
+ protected $template;
+
+ /**
+ * Form constructor.
+ * @param BootstrapInterface $ui
+ * @param string $action
+ * @param array $rule
+ * @param array $config
+ * @throws FormBuilderException
+ */
+ protected function __construct(BootstrapInterface $ui, $action = '', $rule = [], $config = [])
+ {
+ $this->action = $action;
+ $this->rule = $rule;
+ $this->config = $config;
+ $this->ui = $ui;
+ $ui->init($this);
+ $this->checkFieldUnique();
+ $this->template = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Template' . DIRECTORY_SEPARATOR . 'form.php';
+ }
+
+ /**
+ * @return string
+ */
+ public function getFormContentType()
+ {
+ return $this->formContentType;
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ * @return $this
+ */
+ public function setHeader($name, $value)
+ {
+ $this->headers[$name] = (string)$value;
+
+ return $this;
+ }
+
+ /**
+ * @param array $headers
+ * @return $this
+ */
+ public function headers(array $headers)
+ {
+ $this->headers = $headers;
+
+ return $this;
+ }
+
+ /**
+ * @param array $formData
+ * @return $this
+ */
+ public function formData(array $formData)
+ {
+ $this->formData = $formData;
+ return $this;
+ }
+
+ /**
+ * @param $field
+ * @param $value
+ * @return $this
+ */
+ public function setValue($field, $value)
+ {
+ $this->formData[$field] = $value;
+ return $this;
+ }
+
+ /**
+ * @return false|string
+ */
+ public function parseHeaders()
+ {
+ return json_encode((object)$this->headers);
+ }
+
+ /**
+ * @param $formContentType
+ * @return $this
+ */
+ public function setFormContentType($formContentType)
+ {
+ $this->formContentType = (string)$formContentType;
+
+ return $this;
+ }
+
+ public function setDependScript(array $dependScript)
+ {
+ $this->dependScript = $dependScript;
+
+ return $this;
+ }
+
+ /**
+ * @param string $title
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = (string)$title;
+
+ return $this;
+ }
+
+ /**
+ * @param string $method
+ * @return $this
+ */
+ public function setMethod($method)
+ {
+ $this->method = (string)$method;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * @param array $rule
+ * @return $this
+ * @throws FormBuilderException
+ */
+ public function setRule(array $rule)
+ {
+ $this->rule = $rule;
+ $this->checkFieldUnique();
+ return $this;
+ }
+
+ /**
+ * @param array|ConfigInterface $config
+ * @return $this
+ */
+ public function setConfig($config)
+ {
+ if (is_array($config) || $config instanceof ConfigInterface)
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * 追加组件
+ *
+ * @param $component
+ * @return $this
+ * @throws FormBuilderException
+ */
+ public function append($component)
+ {
+ $this->rule[] = $component;
+ $this->checkFieldUnique();
+ return $this;
+ }
+
+ /**
+ * 开头插入组件
+ *
+ * @param $component
+ * @return $this
+ * @throws FormBuilderException
+ */
+ public function prepend($component)
+ {
+ array_unshift($this->rule, $component);
+ $this->checkFieldUnique();
+ return $this;
+ }
+
+ /**
+ * @param $action
+ * @return $this
+ */
+ public function setAction($action)
+ {
+ $this->action = (string)$action;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ /**
+ * 提交按钮显示状态
+ *
+ * @param $isShow
+ * @return $this
+ */
+ public function showSubmitBtn($isShow)
+ {
+ if ($this->config instanceof ConfigInterface)
+ $this->config->submitBtn(!!$isShow);
+ else
+ $this->config['submitBtn'] = !!$isShow;
+ return $this;
+ }
+
+ /**
+ * 重置按钮显示状态
+ *
+ * @param $isShow
+ * @return $this
+ */
+ public function showResetBtn($isShow)
+ {
+ if ($this->config instanceof ConfigInterface)
+ $this->config->resetBtn(!!$isShow);
+ else
+ $this->config['resetBtn'] = !!$isShow;
+ return $this;
+ }
+
+ /**
+ * 设置组件全局配置
+ * @param string $componentName
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalConfig($componentName, array $config)
+ {
+ if ($this->config instanceof ConfigInterface)
+ $this->config->componentGlobalConfig($componentName, $config);
+ else {
+ if (!isset($this->config['global'])) $this->config['global'] = [];
+ $this->config['global'][$componentName] = $config;
+ }
+ return $this;
+ }
+
+ protected function parseFormComponent($rule)
+ {
+ if (Util::isComponent($rule)) {
+ $rule = $rule->build();
+ } else {
+ if (isset($rule['children']) && is_array($rule['children'])) {
+ foreach ($rule['children'] as $i => $child) {
+ $rule['children'][$i] = $this->parseFormComponent($child);
+ }
+ }
+ if (isset($rule['control'])) {
+ foreach ($rule['control'] as $i => $child) {
+ foreach ($child['rule'] as $k => $rule) {
+ $child['rule'][$k] = Util::isComponent($rule) ? $rule->build() : $rule;
+ }
+ $rule['control'][$i] = $child;
+ }
+ }
+ }
+ return $rule;
+ }
+
+ public function getDependScript()
+ {
+ return $this->dependScript;
+ }
+
+ /**
+ * @param array $formData
+ * @param array $rule
+ * @return array
+ */
+ protected function deepSetFormData($formData, $rule)
+ {
+ if (!count($formData)) return $rule;
+ foreach ($rule as $k => $item) {
+ if (is_array($item)) {
+ if (isset($item['field']) && isset($formData[$item['field']])) {
+ $item['value'] = $formData[$item['field']];
+ }
+ if (isset($item['children']) && is_array($item['children']) && count($item['children'])) {
+ $item['children'] = $this->deepSetFormData($formData, $item['children']);
+ }
+ if (isset($item['control']) && count($item['control'])) {
+ foreach ($item['control'] as $_k => $_rule) {
+ $item['control'][$_k]['rule'] = $this->deepSetFormData($formData, $_rule['rule']);
+ }
+ }
+ }
+ $rule[$k] = $item;
+ }
+
+ return $rule;
+ }
+
+ /**
+ * 获取表单生成规则
+ *
+ * @return array
+ */
+ public function formRule()
+ {
+ $rules = [];
+ foreach ($this->rule as $rule) {
+ $rules[] = $this->parseFormComponent($rule);
+ }
+ return $this->deepSetFormData($this->formData, $rules);
+ }
+
+ /**
+ * @return false|string
+ */
+ public function parseFormRule()
+ {
+ return json_encode($this->formRule());
+ }
+
+ /**
+ * @return false|string
+ */
+ public function parseFormConfig()
+ {
+ return json_encode((object)$this->formConfig());
+ }
+
+ /**
+ * @return string
+ */
+ public function parseDependScript()
+ {
+ return implode("\r\n", $this->dependScript);
+ }
+
+ /**
+ * 获取表单配置
+ *
+ * @return array
+ */
+ public function formConfig()
+ {
+ $config = $this->config;
+ if ($config instanceof ConfigInterface) return $config->getConfig();
+ foreach ($config as $k => $v) {
+ $config[$k] = $this->parseFormComponent($v);
+ }
+ return $config;
+ }
+
+
+ /**
+ * 获取表单创建的 js 代码
+ *
+ * @return false|string
+ */
+ public function formScript()
+ {
+ return $this->template(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Template' . DIRECTORY_SEPARATOR . 'createScript.min.php');
+ }
+
+ /**
+ * 获取表单视图
+ *
+ * @return string
+ */
+ public function view()
+ {
+ return $this->template($this->template);
+ }
+
+ /**
+ * 自定义表单页面
+ *
+ * @param $templateDir
+ * @return false|string
+ */
+ public function template($templateDir)
+ {
+ ob_start();
+ $form = $this;
+ require $templateDir;
+ $html = ob_get_clean();
+ return $html;
+ }
+
+ /**
+ * 设置模板
+ *
+ * @param string $templateDir
+ * @return $this
+ */
+ public function setTemplate($templateDir)
+ {
+ $this->template = $templateDir;
+ return $this;
+ }
+
+ /**
+ * 检查field 是否重复
+ *
+ * @param null $rules
+ * @param array $fields
+ * @return array
+ * @throws FormBuilderException
+ */
+ protected function checkFieldUnique($rules = null, $fields = [])
+ {
+ if (is_null($rules)) $rules = $this->rule;
+
+ foreach ($rules as $rule) {
+ $rule = $this->parseFormComponent($rule);
+ $field = isset($rule['field']) ? $rule['field'] : null;
+
+ if (isset($rule['children']) && count($rule['children']))
+ $fields = $this->checkFieldUnique($rule['children'], $fields);
+
+ if (is_null($field) || $field === '')
+ continue;
+ else if (isset($fields[$field]))
+ throw new FormBuilderException('组件的 field 不能重复');
+ else
+ $fields[$field] = true;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Iview 版表单生成器
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array|ConfigInterface $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function iview($action = '', $rule = [], $config = [])
+ {
+ return new self(new IViewBootstrap(), $action, $rule, $config);
+ }
+
+ /**
+ * Iview v4 版表单生成器
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array|ConfigInterface $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function iview4($action = '', $rule = [], $config = [])
+ {
+ return new self(new IViewBootstrap(4), $action, $rule, $config);
+ }
+
+ /**
+ * element-ui 版表单生成器
+ *
+ * @param string $action
+ * @param array $rule
+ * @param array|ConfigInterface $config
+ * @return Form
+ * @throws FormBuilderException
+ */
+ public static function elm($action = '', $rule = [], $config = [])
+ {
+ return new self(new ElmBootstrap(), $action, $rule, $config);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Form/ElmForm.php b/vendor/xaboy/form-builder/src/Form/ElmForm.php
new file mode 100644
index 0000000..455bddd
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Form/ElmForm.php
@@ -0,0 +1,24 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Form;
+
+
+use FormBuilder\UI\Elm\Bootstrap;
+
+class ElmForm extends \FormBuilder\Form
+{
+ public function __construct($action = '', $rule = [], $config = [])
+ {
+ parent::__construct(new Bootstrap(), $action, $rule, $config);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Form/IviewForm.php b/vendor/xaboy/form-builder/src/Form/IviewForm.php
new file mode 100644
index 0000000..04ae124
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Form/IviewForm.php
@@ -0,0 +1,24 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Form;
+
+
+use FormBuilder\UI\Iview\Bootstrap;
+
+class IviewForm extends \FormBuilder\Form
+{
+ public function __construct($action = '', $rule = [], $config = [])
+ {
+ parent::__construct(new Bootstrap(), $action, $rule, $config);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/FormHandle.php b/vendor/xaboy/form-builder/src/FormHandle.php
new file mode 100644
index 0000000..8346202
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/FormHandle.php
@@ -0,0 +1,153 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder;
+
+
+use FormBuilder\Annotation\AnnotationReader;
+use FormBuilder\Contract\ConfigInterface;
+use FormBuilder\Contract\FormHandleInterface;
+
+/**
+ * 表单生成类
+ *
+ * Class FormHandle
+ * @package FormBuilder
+ */
+abstract class FormHandle implements FormHandleInterface
+{
+ protected $action = '';
+
+ protected $method = 'POST';
+
+ protected $title;
+
+ protected $formContentType;
+
+ protected $headers = [];
+
+ protected $fieldTitles = [];
+
+ protected $except = [];
+
+ protected $scene;
+
+ /**
+ * 表单 UI
+ *
+ * @return mixed
+ */
+ abstract public function ui();
+
+ final public function getExcept()
+ {
+ return $this->except;
+ }
+
+ /**
+ * 获取表单数据
+ * @return array
+ */
+ protected function getFormData()
+ {
+ return [];
+ }
+
+ public function scene($scene = null)
+ {
+ if (!is_null($scene)) $this->scene = $scene;
+ return $this->scene;
+ }
+
+ /**
+ * 获取表单配置
+ *
+ * @return mixed|array|ConfigInterface
+ */
+ protected function getFormConfig()
+ {
+ return;
+ }
+
+ public function getFieldTitle($field)
+ {
+ return isset($this->fieldTitles[$field]) ? $this->fieldTitles[$field] : null;
+ }
+
+ /**
+ * 获取表单组件
+ *
+ * @return array
+ * @throws \ReflectionException
+ */
+ protected function getFormRule()
+ {
+ $render = new AnnotationReader($this);
+ return $render->render();
+ }
+
+ /**
+ * 创建表单
+ *
+ * @return Form
+ * @throws \ReflectionException
+ */
+ protected function createForm()
+ {
+ $ui = lcfirst($this->ui());
+ return call_user_func_array(['FormBuilder\\Form', $ui], $this->getParams());
+ }
+
+ /**
+ * @return array
+ * @throws \ReflectionException
+ */
+ protected function getParams()
+ {
+ $params = [$this->action, $this->getFormRule()];
+ $config = $this->getFormConfig();
+ if (is_array($config) || $config instanceof ConfigInterface)
+ $params[] = $config;
+
+ return $params;
+ }
+
+ /**
+ * 获取表单
+ *
+ * @return Form
+ * @throws \ReflectionException
+ */
+ public function form()
+ {
+ if ($this->scene && method_exists($this, $this->scene . 'Scene'))
+ $this->{$this->scene . 'Scene'}();
+
+ $form = $this->createForm()->setMethod($this->method);
+ if (!is_null($this->title)) $form->setTitle($this->title)->headers($this->headers);
+ $formData = $this->getFormData();
+ if (is_array($formData)) $form->formData($formData);
+ if ($this->formContentType) $form->setFormContentType($this->formContentType);
+ $config = $this->getFormConfig();
+ if ($config) $form->setConfig($config);
+ return $form;
+ }
+
+ /**
+ * @return string
+ * @throws \ReflectionException
+ */
+ public function view()
+ {
+ return $this->form()->view();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Handle/ElmFormHandle.php b/vendor/xaboy/form-builder/src/Handle/ElmFormHandle.php
new file mode 100644
index 0000000..b7aebf7
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Handle/ElmFormHandle.php
@@ -0,0 +1,30 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Handle;
+
+
+use FormBuilder\FormHandle;
+
+/**
+ * ElementUI 表单生成类
+ * Class ElmFormHandle
+ * @package FormBuilder\Factory
+ */
+abstract class ElmFormHandle extends FormHandle
+{
+
+ public function ui()
+ {
+ return 'elm';
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Handle/IviewFormHandle.php b/vendor/xaboy/form-builder/src/Handle/IviewFormHandle.php
new file mode 100644
index 0000000..5d43d50
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Handle/IviewFormHandle.php
@@ -0,0 +1,33 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Handle;
+
+
+use FormBuilder\FormHandle;
+
+/**
+ * Iview 表单生成类
+ *
+ * Class IviewFormHandle
+ * @package FormBuilder\Factory
+ */
+abstract class IviewFormHandle extends FormHandle
+{
+
+ protected $version = 3;
+
+ public function ui()
+ {
+ return $this->version == 4 ? 'iview4' : 'iview';
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Response.php b/vendor/xaboy/form-builder/src/Response.php
new file mode 100644
index 0000000..b82acb0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Response.php
@@ -0,0 +1,107 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder;
+
+use \Symfony\Component\HttpFoundation\Response as HttpResponse;
+
+abstract class Response
+{
+ /**
+ * @param int $code
+ * @param string $msg
+ * @param null|array $data
+ * @return HttpResponse
+ */
+ protected static function createResponse($code, $msg = 'ok', $data = null)
+ {
+ $res = compact('code', 'msg');
+ if (!is_null($data)) $res['data'] = $data;
+
+ return new HttpResponse(json_encode($res));
+ }
+
+ /**
+ * 请求成功
+ *
+ * @param string $msg
+ * @param null|array $data
+ * @return HttpResponse
+ */
+ public static function succ($msg = 'ok', $data = null)
+ {
+ return self::createResponse(200, $msg, $data);
+ }
+
+ /**
+ * 请求失败
+ *
+ * @param string $msg
+ * @param null|array $data
+ * @return HttpResponse
+ */
+ public static function fail($msg = 'fail', $data = null)
+ {
+ return self::createResponse(400, $msg, $data);
+ }
+
+ /**
+ * 请求成功
+ *
+ * @param string $msg
+ * @param null|array $data
+ * @return HttpResponse
+ */
+ public static function success($msg = 'ok', $data = null)
+ {
+ return self::succ($msg, $data);
+ }
+
+ /**
+ * 图片/文件上传成功
+ *
+ * @param string $filePath
+ * @param string $msg
+ * @param array $data
+ * @return HttpResponse
+ */
+ public static function uploadSucc($filePath, $msg = '上传成功', array $data = [])
+ {
+ $data['filePath'] = $filePath;
+ return self::succ($msg, $data);
+ }
+
+ /**
+ * 图片/文件上传失败
+ *
+ * @param string $msg
+ * @param null|array $data
+ * @return HttpResponse
+ */
+ public static function uploadFail($msg = '上传失败', $data = null)
+ {
+ return self::fail($msg, $data);
+ }
+
+ /**
+ * 图片/文件上传成功
+ *
+ * @param string $filePath
+ * @param string $msg
+ * @param array $data
+ * @return HttpResponse
+ */
+ public static function uploadSuccess($filePath, $msg = '上传成功', array $data = [])
+ {
+ return self::uploadSucc($filePath, $msg, $data);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/BaseRule.php b/vendor/xaboy/form-builder/src/Rule/BaseRule.php
new file mode 100644
index 0000000..1a00ba1
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/BaseRule.php
@@ -0,0 +1,341 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Contract\ColComponentInterface;
+
+trait BaseRule
+{
+
+ /**
+ * 组件类型
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * 组件字段名
+ *
+ * @var string
+ */
+ protected $field;
+
+ /**
+ * 字段昵称
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * 组件名称
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 组件的提示信息
+ *
+ * @var string
+ */
+ protected $info;
+
+ /**
+ * 组件 class
+ *
+ * @var string
+ */
+ protected $className;
+
+ /**
+ * 是否原样生成组件,不嵌套的FormItem中
+ *
+ * @var bool
+ */
+ protected $native;
+
+ /**
+ * 事件注入时的自定义数据
+ *
+ * @var mixed
+ */
+ protected $inject;
+
+ /**
+ * 组件布局规则
+ *
+ * @var ColComponentInterface|array
+ */
+ protected $col;
+
+ /**
+ * 组件的值
+ *
+ * @var mixed
+ */
+ protected $value = '';
+
+ /**
+ * 组件显示状态
+ *
+ * @var bool
+ */
+ protected $hidden;
+
+ /**
+ * 组件显示状态
+ *
+ * @var bool
+ */
+ protected $visibility;
+
+ /**
+ * @var array
+ */
+ protected $effect;
+
+ /**
+ * 组件显示状态
+ *
+ * @param bool $hidden
+ * @return $this
+ */
+ public function hiddenStatus($hidden = true)
+ {
+ $this->hidden = !!$hidden;
+ return $this;
+ }
+
+ /**
+ * 组件显示状态
+ *
+ * @param bool $visibility
+ * @return $this
+ */
+ public function visibilityStatus($visibility = true)
+ {
+ $this->visibility = !!$visibility;
+ return $this;
+ }
+
+ /**
+ * @param string $type
+ * @return $this
+ */
+ protected function setRuleType($type)
+ {
+ $this->type = (string)$type;
+ return $this;
+ }
+
+ /**
+ * @param string $field
+ * @return $this
+ */
+ public function field($field)
+ {
+ $this->field = (string)$field;
+ return $this;
+ }
+
+ /**
+ * @param string $title
+ * @return $this
+ */
+ public function title($title)
+ {
+ $this->title = (string)$title;
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ * @return $this
+ */
+ public function name($name)
+ {
+ $this->name = (string)$name;
+ return $this;
+ }
+
+ /**
+ * @param string $className
+ * @return $this
+ */
+ public function className($className)
+ {
+ $this->className = (string)$className;
+ return $this;
+ }
+
+ /**
+ * @param bool $native
+ * @return $this
+ */
+ public function native($native)
+ {
+ $this->native = !!$native;
+ return $this;
+ }
+
+ /**
+ * @param mixed $inject
+ * @return $this
+ */
+ public function inject($inject)
+ {
+ $this->inject = $inject;
+ return $this;
+ }
+
+ /**
+ * @param string $info
+ * @return $this
+ */
+ public function info($info)
+ {
+ $this->info = $info;
+ return $this;
+ }
+
+ /**
+ * @param array|int|ColComponentInterface $col
+ * @return $this
+ */
+ public function col($col)
+ {
+ if (is_integer($col)) $col = ['span' => $col];
+ $this->col = $col;
+ return $this;
+ }
+
+ public function effect(array $effect)
+ {
+ $this->effect = $effect;
+ return $this;
+ }
+
+ /**
+ * @param mixed $value
+ * @return $this
+ */
+ public function value($value)
+ {
+ if (is_null($value)) $value = '';
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getHiddenStatus()
+ {
+ return $this->hidden;
+ }
+
+ public function getVisibilityStatus()
+ {
+ return $this->visibility;
+ }
+
+ public function getField()
+ {
+ return $this->field;
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getClassName()
+ {
+ return $this->className;
+ }
+
+ public function getInfo()
+ {
+ return $this->info;
+ }
+
+ public function getNative()
+ {
+ return $this->native;
+ }
+
+ public function getInject()
+ {
+ return $this->inject;
+ }
+
+ public function getCol()
+ {
+ return $this->col;
+ }
+
+ public function getEffect()
+ {
+ return $this->effect;
+ }
+
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ protected function parseCol($col)
+ {
+ return $col instanceof ColComponentInterface ? $col->getCol() : $col;
+ }
+
+ protected function parseBaseRule()
+ {
+ $rule = [
+ 'type' => $this->type
+ ];
+
+ if (!is_null($this->field))
+ $rule['field'] = $this->field;
+ if (!is_null($this->value))
+ $rule['value'] = $this->value;
+ if (!is_null($this->title))
+ $rule['title'] = $this->title;
+ if (!is_null($this->className))
+ $rule['className'] = $this->className;
+ if (!is_null($this->name))
+ $rule['name'] = $this->name;
+ if (!is_null($this->native))
+ $rule['native'] = $this->native;
+ if (!is_null($this->info))
+ $rule['info'] = $this->info;
+ if (!is_null($this->effect))
+ $rule['effect'] = $this->effect;
+ if (!is_null($this->inject))
+ $rule['inject'] = $this->inject;
+ if (!is_null($this->hidden))
+ $rule['hidden'] = $this->hidden;
+ if (!is_null($this->visibility))
+ $rule['visibility'] = $this->visibility;
+ if (!is_null($this->col))
+ $rule['col'] = $this->parseCol($this->col);
+
+ return $rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/CallPropsRule.php b/vendor/xaboy/form-builder/src/Rule/CallPropsRule.php
new file mode 100644
index 0000000..44ad4c4
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/CallPropsRule.php
@@ -0,0 +1,45 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Exception\FormBuilderException;
+
+trait CallPropsRule
+{
+ /**
+ * 设置组件属性
+ *
+ * @param $name
+ * @param $arguments
+ * @return $this
+ * @throws FormBuilderException
+ */
+ public function __call($name, $arguments)
+ {
+ if (isset(static::$propsRule[$name])) {
+ if (!isset($arguments[0])) return isset($this->props[$name]) ? $this->props[$name] : null;
+ $value = $arguments[0];
+ if (is_array(static::$propsRule[$name])) {
+ settype($value, static::$propsRule[$name][0]);
+ $name = static::$propsRule[$name][1];
+ } else if (static::$propsRule[$name]) {
+ settype($value, static::$propsRule[$name]);
+ }
+ $this->props[$name] = $value;
+ return $this;
+ } else {
+ throw new FormBuilderException($name . '方法不存在');
+ }
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/ChildrenRule.php b/vendor/xaboy/form-builder/src/Rule/ChildrenRule.php
new file mode 100644
index 0000000..a9a951f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/ChildrenRule.php
@@ -0,0 +1,97 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Util;
+
+trait ChildrenRule
+{
+ /**
+ * 组件的插槽名称,如果组件是其它组件的子组件,需为插槽指定名称
+ *
+ * @var
+ */
+ protected $slot;
+
+ /**
+ * 设置父级组件的插槽,默认为default.可配合 slot 配置项使用
+ *
+ * @var
+ */
+ protected $children = [];
+
+ /**
+ * @param $slot
+ * @return $this
+ */
+ public function slot($slot)
+ {
+ $this->slot = (string)$slot;
+ return $this;
+ }
+
+ /**
+ * @param array $children
+ * @return $this
+ */
+ public function children(array $children)
+ {
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * @param string|array|self $child
+ * @return $this
+ */
+ public function appendChild($child)
+ {
+ $this->children[] = $child;
+ return $this;
+ }
+
+ /**
+ * @param array $children
+ * @return $this
+ */
+ public function appendChildren($children)
+ {
+ $this->children = array_merge($this->children, $children);
+ return $this;
+ }
+
+ public function getSlot()
+ {
+ return $this->slot;
+ }
+
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * @return array
+ */
+ public function parseChildrenRule()
+ {
+ if (!count($this->children)) return [];
+ $children = [];
+ foreach ($this->children as $child) {
+ $children[] = Util::isComponent($child) ? $child->build() : $child;
+ }
+
+ return compact('children');
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/ControlRule.php b/vendor/xaboy/form-builder/src/Rule/ControlRule.php
new file mode 100644
index 0000000..2ce6778
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/ControlRule.php
@@ -0,0 +1,80 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Util;
+
+trait ControlRule
+{
+ /**
+ * 根据组件的值显示不同的组件
+ *
+ * @var
+ */
+ protected $control = [];
+
+
+ /**
+ * @param array $control
+ * @return $this
+ */
+ public function control(array $control)
+ {
+ $this->control = $control;
+ return $this;
+ }
+
+ /**
+ * @param string|int|float $value
+ * @param array $rule
+ * @return $this
+ */
+ public function appendControl($value, array $rule)
+ {
+ $this->control[] = compact('value', 'rule');
+ return $this;
+ }
+
+ /**
+ * @param array $controls
+ * @return $this
+ */
+ public function appendControls(array $controls)
+ {
+ $this->control = array_merge($this->control, $controls);
+ return $this;
+ }
+
+ public function getControl()
+ {
+ return $this->control;
+ }
+
+ /**
+ * @return array
+ */
+ public function parseControlRule()
+ {
+ if (!count($this->control)) return [];
+ $control = [];
+ foreach ($this->control as $child) {
+ foreach ($child['rule'] as $k => $rule) {
+ $child['rule'][$k] = Util::isComponent($rule) ? $rule->build() : $rule;
+ }
+ $control[] = $child;
+ }
+
+ return compact('control');
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/EmitRule.php b/vendor/xaboy/form-builder/src/Rule/EmitRule.php
new file mode 100644
index 0000000..1562725
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/EmitRule.php
@@ -0,0 +1,68 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+trait EmitRule
+{
+ /**
+ * 组件模式下配置使用emit方式触发的事件名
+ * @var array
+ */
+ protected $emit = [];
+
+ /**
+ * 自定义组件emit事件的前缀
+ * @var
+ */
+ protected $emitPrefix;
+
+ public function emit(array $emits)
+ {
+ $this->emit = array_merge($this->emit, array_map('strval', $emits));
+ return $this;
+ }
+
+ public function appendEmit($emit)
+ {
+ $this->emit[] = (string)$emit;
+ return $this;
+ }
+
+ public function emitPrefix($prefix)
+ {
+ $this->emitPrefix = (string)$prefix;
+ return $prefix;
+ }
+
+ public function getEmit()
+ {
+ return $this->emit;
+ }
+
+ public function getEmitPrefix()
+ {
+ return $this->emitPrefix;
+ }
+
+ public function parseEmitRule()
+ {
+ $rule = [];
+ if (count($this->emit))
+ $rule['emit'] = $this->emit;
+ if (!is_null($this->emitPrefix))
+ $rule['emitPrefix'] = $this->emitPrefix;
+
+ return $rule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/OptionsRule.php b/vendor/xaboy/form-builder/src/Rule/OptionsRule.php
new file mode 100644
index 0000000..d35732e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/OptionsRule.php
@@ -0,0 +1,109 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Contract\OptionComponentInterface;
+
+trait OptionsRule
+{
+ /**
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 添加选项
+ *
+ * @param string $value
+ * @param string $label
+ * @param bool $disabled
+ * @return $this
+ */
+ public function appendOption($value, $label, $disabled = false)
+ {
+ $label = (string)$label;
+ $this->options[] = compact('value', 'label', 'disabled');
+ return $this;
+ }
+
+ /**
+ * 批量添加选项
+ *
+ * @param array $options
+ * @return $this
+ */
+ public function appendOptions(array $options)
+ {
+ $this->options = array_merge($this->options, $options);
+ return $this;
+ }
+
+ /**
+ * 批量设置的选项
+ *
+ * @param array $options
+ * @return $this
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * 批量设置选项 支持匿名函数
+ *
+ * @param array|callable $options
+ * @return $this
+ */
+ public function options($options)
+ {
+ return $this->setOptions(is_callable($options) ? $options($this) : $options);
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * @param $option
+ * @return array
+ */
+ protected function parseOption($option)
+ {
+ return $option instanceof OptionComponentInterface ? $option->getOption() : $option;
+ }
+
+ /**
+ * @return array
+ */
+ protected function parseOptions()
+ {
+ $options = [];
+ foreach ($this->options as $option) {
+ $options[] = $this->parseOption($option);
+ }
+
+ return $options;
+ }
+
+ public function parseOptionsRule()
+ {
+ return ['options' => $this->parseOptions()];
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/PropsRule.php b/vendor/xaboy/form-builder/src/Rule/PropsRule.php
new file mode 100644
index 0000000..51ce097
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/PropsRule.php
@@ -0,0 +1,132 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+trait PropsRule
+{
+ /**
+ * 组件的props
+ *
+ * @var array
+ */
+ protected $props = [];
+
+ /**
+ * 组件普通的 HTML 特性
+ *
+ * @var array
+ */
+ protected $attrs = [];
+
+ /**
+ * 组件的样式
+ * @var string|array
+ */
+ protected $style;
+
+ /**
+ * 组件的 DOM 属性
+ *
+ * @var array
+ */
+ protected $domProps = [];
+
+ /**
+ * 设置组件的 style 属性
+ * @param string|array $style
+ * @return $this
+ */
+ public function style($style)
+ {
+ $this->style = $style;
+ return $this;
+ }
+
+ /**
+ * @return array|string
+ */
+ public function getStyle()
+ {
+ return $this->style;
+ }
+
+ public function prop($name, $value)
+ {
+ $this->props[$name] = $value;
+ return $this;
+ }
+
+ public function props(array $props)
+ {
+ $this->props = array_merge($this->props, $props);
+ return $this;
+ }
+
+ public function attr($name, $value)
+ {
+ $this->attrs[$name] = $value;
+ return $this;
+ }
+
+ public function attrs(array $attrs)
+ {
+ $this->attrs = array_merge($this->attrs, $attrs);
+ return $this;
+ }
+
+ public function domProp($name, $value)
+ {
+ $this->domProps[$name] = $value;
+ return $this;
+ }
+
+ public function domProps(array $domProps)
+ {
+ $this->domProps = array_merge($this->domProps, $domProps);
+ return $this;
+ }
+
+ public function getProps()
+ {
+ return $this->props;
+ }
+
+ public function getProp($name)
+ {
+ return isset($this->props[$name]) ? $this->props[$name] : null;
+ }
+
+ public function getAttrs()
+ {
+ return $this->attrs;
+ }
+
+ public function getDomProps()
+ {
+ return $this->domProps;
+ }
+
+ public function parsePropsRule()
+ {
+ $rule = ['props' => (object)$this->props];
+ if (count($this->attrs))
+ $rule['attrs'] = $this->attrs;
+ if (count($this->domProps))
+ $rule['domProps'] = $this->domProps;
+ if (!is_null($this->style))
+ $rule['style'] = $this->style;
+
+ return $rule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Rule/ValidateRule.php b/vendor/xaboy/form-builder/src/Rule/ValidateRule.php
new file mode 100644
index 0000000..d544899
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Rule/ValidateRule.php
@@ -0,0 +1,70 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\Rule;
+
+
+use FormBuilder\Contract\ValidateInterface;
+
+trait ValidateRule
+{
+ /**
+ * 组件验证规则
+ *
+ * @var array
+ */
+ protected $validate = [];
+
+ public function validate(array $validates)
+ {
+ $this->validate = $validates;
+ return $this;
+ }
+
+ /**
+ * @param array|ValidateInterface $validate
+ * @return $this
+ */
+ public function appendValidate($validate)
+ {
+ $this->validate[] = $validate;
+ return $this;
+ }
+
+ public function appendValidates(array $validates)
+ {
+ $this->validate = array_merge($this->validate, $validates);
+ return $this;
+ }
+
+ protected function getValidate()
+ {
+ return $this->validate;
+ }
+
+ protected function parseValidate()
+ {
+ $validate = [];
+ foreach ($this->validate as $value) {
+ $validate[] = $value instanceof ValidateInterface ? $value->getValidate() : $value;
+ }
+
+ return $validate;
+ }
+
+ public function parseValidateRule()
+ {
+ if (!count($this->validate))
+ return [];
+ return ['validate' => $this->parseValidate()];
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Template/createScript.min.php b/vendor/xaboy/form-builder/src/Template/createScript.min.php
new file mode 100644
index 0000000..aab39b0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Template/createScript.min.php
@@ -0,0 +1 @@
+(function(){var e=function(l,k){return Object.prototype.toString.call(l)===("[object "+k+"]")};var g=function(k){if(!k){return[]}k.forEach(function(n){var m=n.type?(""+n.type).toLocaleLowerCase():"",l=e(k.children,"Array")?k.children:[];if((m==="cascader"||m==="tree")&&n.props){if(n.props.data&&e(n.props.data,"String")&&n.props.data.indexOf("js.")===0){n.props.data=window[n.props.data.substr(3)]}else{if(n.props.options&&e(n.props.options,"String")&&n.props.options.indexOf("js.")===0){n.props.options=window[n.props.options.substr(3)]}}}else{if(m==="group"){if(n.props.rules){g(n.props.rules)}if(n.props.rule){g([n.props.rule])}}else{if(n.control && n.control.length){n.control.forEach(function(o){g(o.rule)})}}}if(l.length){g(l)}});return k};var i=function(k,l,m,n){$.ajax({url:k,type:(""+l).toLocaleUpperCase(),dataType:"json",headers:c,contentType:j,data:m,success:function(o){n(1,o)},error:function(){n(0,{})}})};var h=g(=$form->parseFormRule()?>),c==$form->parseHeaders()?>,d==$form->parseFormConfig()?>,j="=$form->getFormContentType()?>",f="=$form->getAction()?>",a="=$form->getMethod()?>",b=new Vue();if(!b.$Message&&b.$message){Vue.prototype.$Message=b.$message}return function(l){if(!l){l={}}if(l.el){d.el=l.el}d.onSubmit=function(o,n){n.submitBtnProps({loading:true,disabled:true});i(f,a,o,function(p,q){if(l.callback){return l.callback(p,q,n)}n.submitBtnProps({loading:false,disabled:false});if(p&&q.code===200){b.$Message.success(q.msg||"表单提交成功")}else{b.$Message.error(q.msg||"表单提交失败")}})};if(!d.global){d.global={}}if(!d.global.upload){d.global.upload={}}if(!d.global.upload.props){d.global.upload.props={}}var k=d.global.upload.props;k.onExceededSize=function(n){b.$Message.error(n.name+"超出指定大小限制")};k.onFormatError=function(n){b.$Message.error(n.name+"格式验证失败")};k.onError=function(n,o){b.$Message.error(o.name+"上传失败,("+n+")")};k.onSuccess=function(o,n){if(o.code===200){n.url=o.data.filePath}else{b.$Message.error(o.msg)}};var m=function(){return formCreate.create(h,d)};return l.filter?l.filter(m):m()}})();
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Template/createScript.php b/vendor/xaboy/form-builder/src/Template/createScript.php
new file mode 100644
index 0000000..8a67b76
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Template/createScript.php
@@ -0,0 +1,94 @@
+(function () {
+
+ var isType = function (data, type) {
+ return Object.prototype.toString.call(data) === ('[object ' + type + ']');
+ };
+
+ var parseRule = function (rule) {
+ if (!rule) return [];
+ rule.forEach(function (c) {
+ var type = c.type ? ('' + c.type).toLocaleLowerCase() : '',
+ children = isType(rule.children, 'Array') ? rule.children : [];
+ if ((type === 'cascader' || type === 'tree') && c.props) {
+ if (c.props.data && isType(c.props.data, 'String') && c.props.data.indexOf('js.') === 0) {
+ c.props.data = window[c.props.data.substr(3)];
+ } else if (c.props.options && isType(c.props.options, 'String') && c.props.options.indexOf('js.') === 0) {
+ c.props.options = window[c.props.options.substr(3)];
+ }
+ } else if (type === 'group') {
+ if (c.props.rules) parseRule(c.props.rules);
+ if (c.props.rule) parseRule([c.props.rule]);
+ } else if (c.control && c.control.length) {
+ c.control.forEach(function(r) {
+ parseRule(r.rule);
+ });
+ }
+ if (children.length) parseRule(children);
+ });
+
+ return rule;
+ };
+
+ var ajax = function (url, type, formData, callback){
+ $.ajax({
+ url: url,
+ type: ('' + type).toLocaleUpperCase(),
+ dataType: 'json',
+ headers: headers,
+ contentType: contentType,
+ data: formData,
+ success: function (res) {
+ callback(1, res);
+ },
+ error: function () {
+ callback(0, {});
+ }
+ });
+
+ }
+
+ var rule = parseRule(=$form->parseFormRule()?>), headers = =$form->parseHeaders()?>, config = =$form->parseFormConfig()?>, contentType = "=$form->getFormContentType()?>", action = "=$form->getAction()?>", method = "=$form->getMethod()?>", vm = new Vue();
+ if (!vm.$Message && vm.$message) {
+ Vue.prototype.$Message = vm.$message
+ }
+ return function (option) {
+ if (!option) option = {};
+ if (option.el) config.el = option.el;
+
+ config.onSubmit = function (formData, $f) {
+ $f.submitBtnProps({loading: true, disabled: true});
+ ajax(action, method, formData, function (status, res) {
+ if (option.callback) return option.callback(status, res, $f);
+
+ $f.submitBtnProps({loading: false, disabled: false});
+ if (status && res.code === 200) {
+ vm.$Message.success(res.msg || '表单提交成功');
+ } else {
+ vm.$Message.error(res.msg || '表单提交失败');
+ }
+ });
+ };
+ if(!config.global) config.global = {};
+ if(!config.global.upload) config.global.upload = {};
+ if(!config.global.upload.props) config.global.upload.props = {};
+ var uploadProps = config.global.upload.props;
+ uploadProps.onExceededSize = function (file) {
+ vm.$Message.error(file.name + '超出指定大小限制');
+ };
+ uploadProps.onFormatError = function (file) {
+ vm.$Message.error(file.name + '格式验证失败');
+ };
+ uploadProps.onError = function (error, file) {
+ vm.$Message.error(file.name + '上传失败,(' + error + ')');
+ };
+ uploadProps.onSuccess = function (res, file) {
+ if (res.code === 200) {
+ file.url = res.data.filePath;
+ } else {
+ vm.$Message.error(res.msg);
+ }
+ };
+ var _create = function(){return formCreate.create(rule, config);}
+ return option.filter ? option.filter(_create) : _create() ;
+ };
+})();
diff --git a/vendor/xaboy/form-builder/src/Template/form.php b/vendor/xaboy/form-builder/src/Template/form.php
new file mode 100644
index 0000000..97d16d4
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Template/form.php
@@ -0,0 +1,16 @@
+
+
+
+
+ = $form->getTitle() ?>
+ = $form->parseDependScript() ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Bootstrap.php b/vendor/xaboy/form-builder/src/UI/Elm/Bootstrap.php
new file mode 100644
index 0000000..01113e3
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Bootstrap.php
@@ -0,0 +1,33 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm;
+
+use FormBuilder\Contract\BootstrapInterface;
+use FormBuilder\Form;
+
+class Bootstrap implements BootstrapInterface
+{
+
+ public function init(Form $form)
+ {
+ $dependScript = $form->getDependScript();
+
+ array_splice($dependScript, 2, 0, [
+ '',
+ '',
+ '',
+ ]);
+
+ $form->setDependScript($dependScript);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Button.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Button.php
new file mode 100644
index 0000000..cc4f50c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Button.php
@@ -0,0 +1,51 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Button
+ *
+ * @method $this size(string $size) 尺寸, 可选值: medium / small / mini
+ * @method $this type(string $type) 类型, 可选值: primary / success / warning / danger / info / text
+ * @method $this plain(bool $plain) 是否朴素按钮, 默认值: false
+ * @method $this round(bool $round) 是否圆角按钮, 默认值: false
+ * @method $this circle(bool $circle) 是否圆形按钮, 默认值: false
+ * @method $this loading(bool $loading) 是否加载中状态, 默认值: false
+ * @method $this disabled(bool $disabled) 是否禁用状态, 默认值: false
+ * @method $this icon(string $icon) 图标类名
+ * @method $this autofocus(bool $autofocus) 是否默认聚焦, 默认值: false
+ * @method $this nativeType(string $nativeType) 原生 type 属性, 可选值: button / submit / reset, 默认值: button
+ *
+ * @method $this show(bool $show) 是否显示, 默认显示
+ * @method $this innerText(string $innerText) 按钮文字提示
+ */
+class Button extends CustomComponent
+{
+ protected static $propsRule = [
+ 'size' => 'string',
+ 'type' => 'string',
+ 'plain' => 'bool',
+ 'round' => 'bool',
+ 'circle' => 'bool',
+ 'loading' => 'bool',
+ 'disabled' => 'bool',
+ 'icon' => 'string',
+ 'autofocus' => 'bool',
+ 'nativeType' => 'string',
+ 'innerText' => 'string',
+ 'show' => 'bool'
+ ];
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Cascader.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Cascader.php
new file mode 100644
index 0000000..53f9eac
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Cascader.php
@@ -0,0 +1,126 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 多级联动组件
+ * Class Cascader
+ *
+ * @method $this type(string $type) 数据类型, 支持 city_area(省市区三级联动), city (省市二级联动), other (自定义)
+ * @method $this props(array $props) 配置选项
+ * @method $this size(string $size) 尺寸, 可选值: medium / small / mini
+ * @method $this placeholder(string $placeholder) 输入框占位文本, 默认值: 请选择
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this clearable(bool $clearable) 是否支持清空选项, 默认值: false
+ * @method $this showAllLevels(bool $showAllLevels) 输入框中是否显示选中值的完整路径, 默认值: true
+ * @method $this collapseTags(bool $collapseTags) 多选模式下是否折叠Tag, 默认值: false
+ * @method $this separator(string $separator) 选项分隔符, 默认值: 斜杠' / '
+ * @method $this filterable(bool $filterable) 是否可搜索选项
+ * @method $this debounce(float $debounce) 搜索关键词输入的去抖延迟,毫秒, 默认值: 300
+ * @method $this popperClass(string $popperClass) 自定义浮层类名
+ */
+class Cascader extends FormComponent
+{
+ /**
+ * 省市区三级联动数据
+ */
+ const TYPE_CITY_AREA = 'city_area';
+
+ /**
+ * 省市二级联动数据
+ */
+ const TYPE_CITY = 'city';
+
+ /**
+ * 自定义数据
+ */
+ const TYPE_OTHER = 'other';
+
+
+ protected $defaultValue = [];
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_OTHER,
+ 'options' => []
+ ];
+
+ /**
+ * @var array
+ */
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'props' => 'array',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'disabled' => 'bool',
+ 'clearable' => 'bool',
+ 'showAllLevels' => 'bool',
+ 'collapseTags' => 'bool',
+ 'separator' => 'string',
+ 'filterable' => 'bool',
+ 'debounce' => 'float',
+ 'popperClass' => 'string',
+ ];
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function value($value)
+ {
+ $this->value = (array)$value;
+ return $this;
+ }
+
+ /**
+ * 可选项的数据源
+ * 例如:{
+ * "value":"北京市", "label":"北京市", "children":[{
+ * "value":"东城区", "label":"东城区"
+ * }]
+ * }
+ *
+ * @param array|callable $data
+ * @return $this
+ */
+ public function options($data)
+ {
+ $is_callable = is_callable($data);
+ if (!is_array($data) && !$is_callable) return $this;
+
+ $this->props['options'] = $is_callable ? $data($this) : $data;
+ return $this;
+ }
+
+ /**
+ * @param $var
+ * @return $this
+ */
+ public function jsOptions($var)
+ {
+ $this->props['options'] = 'js.' . (string)$var;
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Elm::validateArr();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Checkbox.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Checkbox.php
new file mode 100644
index 0000000..ed090b4
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Checkbox.php
@@ -0,0 +1,79 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 复选框组件
+ * Class Checkbox
+ *
+ * @method $this size(string $size) 多选框组尺寸,仅对按钮形式的 Checkbox 或带有边框的 Checkbox 有效, 可选值: medium / small / mini
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this min(float $min) 可被勾选的 checkbox 的最小数量
+ * @method $this max(float $max) 可被勾选的 checkbox 的最大数量
+ * @method $this textColor(string $textColor) 按钮形式的 Checkbox 激活时的文本颜色, 默认值: #ffffff
+ * @method $this fill(string $fill) 按钮形式的 Checkbox 激活时的填充色和边框色, 默认值: #409EFF
+ * @method $this checked(bool $checked) 当前是否勾选(只有在checkbox-button时有效), 默认值: false
+ */
+class Checkbox extends FormOptionsComponent
+{
+ const TYPE_BUTTON = 'button';
+
+ const TYPE_GROUP = 'group';
+
+ protected $defaultValue = [];
+
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'size' => 'string',
+ 'disabled' => 'bool',
+ 'min' => 'float',
+ 'max' => 'float',
+ 'textColor' => 'string',
+ 'fill' => 'string',
+ 'checked' => 'bool',
+ ];
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function value($value)
+ {
+ $this->value = (array)$value;
+ return $this;
+ }
+
+ /**
+ * @param bool $bool
+ * @return $this
+ */
+ public function button($bool)
+ {
+ if ($bool)
+ $this->props['type'] = 'button';
+ else
+ unset($this->props['type']);
+
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Elm::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/ColorPicker.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/ColorPicker.php
new file mode 100644
index 0000000..57cf8f1
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/ColorPicker.php
@@ -0,0 +1,47 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 颜色选择器组件
+ * Class ColorPicker
+ *
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this size(string $size) 尺寸, 默认值: medium / small / mini
+ * @method $this showAlpha(bool $showAlpha) 是否支持透明度选择, 默认值: false
+ * @method $this colorFormat(string $colorFormat) 的格式, 可选值: hsl / hsv / hex / rgb, 默认值: hex(show-alpha 为 false)/ rgb(show-alpha 为 true)
+ * @method $this popperClass(string $popperClass) ColorPicker 下拉框的类名
+ * @method $this predefine(array $predefine) 预定义颜色
+ */
+class ColorPicker extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'disabled' => 'bool',
+ 'size' => 'string',
+ 'showAlpha' => 'bool',
+ 'colorFormat' => 'string',
+ 'popperClass' => 'string',
+ 'predefine' => 'array',
+ ];
+
+ public function createValidate()
+ {
+ return Elm::validateStr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/DatePicker.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/DatePicker.php
new file mode 100644
index 0000000..caf9394
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/DatePicker.php
@@ -0,0 +1,153 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 日期选择器组件
+ * Class DatePicker
+ *
+ * @method $this readonly(bool $readonly) 完全只读, 默认值: false
+ * @method $this disabled(bool $disabled) 禁用, 默认值: false
+ * @method $this editable(bool $editable) 文本框可输入, 默认值: true
+ * @method $this clearable(bool $clearable) 是否显示清除按钮, 默认值: true
+ * @method $this size(string $size) 输入框尺寸, 可选值: large, small, mini
+ * @method $this placeholder(string $placeholder) 非范围选择时的占位内容
+ * @method $this startPlaceholder(string $startPlaceholder) 范围选择时开始日期的占位内容
+ * @method $this endPlaceholder(string $endPlaceholder) 范围选择时结束日期的占位内容
+ * @method $this type(string $type) 显示类型, 可选值: year/month/date/dates/ week/datetime/datetimerange/ daterange/monthrange, 默认值: date
+ * @method $this format(string $format) 显示在输入框中的格式, 可选值: 见日期格式, 默认值: yyyy-MM-dd
+ * @method $this align(string $align) 对齐方式, 可选值: left, center, right, 默认值: left
+ * @method $this popperClass(string $popperClass) DatePicker 下拉框的类名
+ * @method $this pickerOptions(array $pickerOptions) 当前时间日期选择器特有的选项参考下表, 默认值: {
+ * }
+ * @method $this rangeSeparator(string $rangeSeparator) 选择范围时的分隔符, 默认值: '-'
+ * @method $this defaultValue(string $defaultValue) 可选,选择器打开时默认显示的时间, 可选值: 可被new Date()解析
+ * @method $this defaultTime(array $defaultTime) 范围选择时选中日期所使用的当日内具体时刻, 可选值: 数组,长度为 2,每项值为字符串,形如12:00:00,第一项指定开始日期的时刻,第二项指定结束日期的时刻,不指定会使用时刻 00:00:00
+ * @method $this valueFormat(string $valueFormat) 可选,绑定值的格式。不指定则绑定值为 Date 对象, 可选值: 见日期格式
+ * @method $this name(string $name) 原生属性
+ * @method $this unlinkPanels(bool $unlinkPanels) 在范围选择器里取消两个日期面板之间的联动, 默认值: false
+ * @method $this prefixIcon(string $prefixIcon) 自定义头部图标的类名, 默认值: el-icon-date
+ * @method $this clearIcon(string $clearIcon) 自定义清空图标的类名, 默认值: el-icon-circle-close
+ * @method $this validateEvent(bool $validateEvent) 输入时是否触发表单的校验, 默认值: true
+ * @method $this timeArrowControl(bool $timeArrowControl) 是否使用箭头进行时间选择, 默认值: false
+ *
+ *
+ */
+class DatePicker extends FormComponent
+{
+ /**
+ * 日期选择
+ */
+ const TYPE_DATE = 'date';
+
+ const TYPE_DATES = 'dates';
+
+ const TYPE_WEEK = 'week';
+
+ /**
+ * 日期区间选择
+ */
+ const TYPE_DATE_RANGE = 'daterange';
+
+ const TYPE_MONTH_RANGE = 'monthrange';
+
+ /**
+ * 日期+时间选择
+ */
+ const TYPE_DATE_TIME = 'datetime';
+ /**
+ * 日期+时间区间选择
+ */
+ const TYPE_DATE_TIME_RANGE = 'datetimerange';
+ /**
+ * 年份选择
+ */
+ const TYPE_YEAR = 'year';
+ /**
+ * 月份选择
+ */
+ const TYPE_MONTH = 'month';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_DATE,
+ 'editable' => false
+ ];
+
+ protected static $propsRule = [
+ 'readonly' => 'bool',
+ 'disabled' => 'bool',
+ 'editable' => 'bool',
+ 'clearable' => 'bool',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'startPlaceholder' => 'string',
+ 'endPlaceholder' => 'string',
+ 'timeArrowControl' => 'bool',
+ 'type' => 'string',
+ 'format' => 'string',
+ 'align' => 'string',
+ 'popperClass' => 'string',
+ 'pickerOptions' => 'array',
+ 'rangeSeparator' => 'string',
+ 'defaultValue' => 'string',
+ 'defaultTime' => 'array',
+ 'valueFormat' => 'string',
+ 'name' => 'string',
+ 'unlinkPanels' => 'bool',
+ 'prefixIcon' => 'string',
+ 'clearIcon' => 'string',
+ 'validateEvent' => 'bool',
+ ];
+
+ protected function isRange()
+ {
+ return in_array(strtolower($this->props['type']), ['datetimerange', 'daterange', 'monthrange']);
+ }
+
+ protected function isMultiple()
+ {
+ return isset($this->props['type']) && strtolower($this->props['type']) == 'dates';
+ }
+
+ public function createValidate()
+ {
+ if ($this->isRange() || $this->isMultiple())
+ return Elm::validateArr();
+ else
+ return Elm::validateDate();
+ }
+
+ public function required($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ $validate = $this->createValidate();
+
+ if ($this->isRange()) {
+ $dateRequired = Elm::validateStr()->message($message)->required();
+ $validate->fields([
+ '0' => $dateRequired,
+ '1' => $dateRequired
+ ]);
+ }
+
+ $this->appendValidate($validate->message($message)->required());
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Frame.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Frame.php
new file mode 100644
index 0000000..d4f1141
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Frame.php
@@ -0,0 +1,84 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 框架组件
+ * Class Frame
+ *
+ * @method $this type(string $type) frame类型, 有input, file, image, 默认为input
+ * @method $this src(string $src) iframe地址
+ * @method $this maxLength(int $length) value的最大数量, 默认无限制
+ * @method $this icon(string $icon) 打开弹出框的按钮图标
+ * @method $this height(string $height) 弹出框高度
+ * @method $this width(string $width) 弹出框宽度
+ * @method $this spin(bool $bool) 是否显示加载动画, 默认为 true
+ * @method $this frameTitle(string $title) 弹出框标题
+ * @method $this modal(array $modalProps) 弹出框props
+ * @method $this handleIcon(bool $bool) 操作按钮的图标, 设置为false将不显示, 设置为true为默认的预览图标, 类型为file时默认为false, image类型默认为true
+ * @method $this allowRemove(bool $bool) 是否可删除, 设置为false是不显示删除按钮
+ * @method $this disabled(bool $bool) 是否禁用
+ *
+ *
+ */
+class Frame extends FormComponent
+{
+ /**
+ * 图片类型
+ */
+ const TYPE_IMAGE = 'image';
+ /**
+ * 文件类型
+ */
+ const TYPE_FILE = 'file';
+ /**
+ * input 类型
+ */
+ const TYPE_INPUT = 'input';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_INPUT,
+ 'maxLength' => 0
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'src' => 'string',
+ 'maxLength' => 'int',
+ 'icon' => 'string',
+ 'height' => 'string',
+ 'width' => 'string',
+ 'spin' => 'bool',
+ 'modal' => 'array',
+ 'frameTitle' => ['string', 'title'],
+ 'handleIcon' => 'bool',
+ 'allowRemove' => 'bool',
+ ];
+
+ public function createValidate()
+ {
+ return Elm::validateArr();
+ }
+
+ protected function init()
+ {
+ $this->frameTitle($this->getPlaceHolder());
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Group.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Group.php
new file mode 100644
index 0000000..fcfdca0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Group.php
@@ -0,0 +1,77 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Contract\ValidateInterface;
+use FormBuilder\Driver\CustomComponent;
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+use FormBuilder\Util;
+
+/**
+ * 数组组件
+ *
+ * Class Group
+ * @method $this min(int $min) 最少几项
+ * @method $this max(int $max) 最多几项
+ * @method $this disabled(bool $bool) 是否禁用
+ */
+class Group extends FormComponent
+{
+ protected $defaultValue = [];
+
+ protected static $propsRule = [
+ 'min' => 'string',
+ 'max' => 'string',
+ 'disabled' => 'int',
+ ];
+
+ /**
+ * @param array|CustomComponent $rule
+ * @return $this
+ */
+ public function rule($rule)
+ {
+ $this->props['rule'] = $this->tidyRule([$rule])[0];
+ return $this;
+ }
+
+ /**
+ * @param array $rules
+ * @return array
+ */
+ protected function tidyRule(array $rules)
+ {
+ foreach ($rules as $k => $rule) {
+ if (Util::isComponent($rule)) {
+ $rules[$k] = $rule->build();
+ }
+ }
+ return $rules;
+ }
+
+ public function rules(array $rules)
+ {
+ $this->props['rules'] = $this->tidyRule($rules);
+ return $this;
+ }
+
+ /**
+ * @return ValidateInterface
+ */
+ public function createValidate()
+ {
+ return Elm::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Hidden.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Hidden.php
new file mode 100644
index 0000000..7a70ff3
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Hidden.php
@@ -0,0 +1,57 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Exception\FormBuilderException;
+
+/**
+ * hidden组件
+ * Class Hidden
+ *
+ */
+class Hidden extends FormComponent
+{
+ /**
+ * Hidden constructor.
+ *
+ * @param string $field
+ * @param string $value
+ */
+ public function __construct($field, $value)
+ {
+ parent::__construct($field, '', $value);
+ }
+
+ /**
+ * @return array
+ */
+ public function getRule()
+ {
+ return [
+ 'type' => $this->type,
+ 'field' => $this->field,
+ 'value' => $this->value
+ ];
+ }
+
+ /**
+ * @return void
+ * @throws FormBuilderException
+ */
+ public function createValidate()
+ {
+ throw new FormBuilderException('hidden 组件不支持 createValidate 方法');
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Input.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Input.php
new file mode 100644
index 0000000..ea5bc5c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Input.php
@@ -0,0 +1,111 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * Input组件,支持类型text、password、textarea、url、email、date
+ * Class Input
+ *
+ * @method $this type(string $type) 类型, 可选值: text,textarea 和其他 原生 input 的 type 值, 默认值: text
+ * @method $this maxlength(float $maxlength) 原生属性,最大输入长度
+ * @method $this minlength(float $minlength) 原生属性,最小输入长度
+ * @method $this showWordLimit(bool $showWordLimit) 是否显示输入字数统计,只在 type = "text" 或 type = "textarea" 时有效, 默认值: false
+ * @method $this placeholder(string $placeholder) 输入框占位文本
+ * @method $this clearable(bool $clearable) 是否可清空, 默认值: false
+ * @method $this showPassword(bool $showPassword) 是否显示切换密码图标, 默认值: false
+ * @method $this disabled(bool $disabled) 禁用, 默认值: false
+ * @method $this size(string $size) 输入框尺寸,只在 type!="textarea" 时有效, 可选值: medium / small / mini
+ * @method $this prefixIcon(string $prefixIcon) 输入框头部图标
+ * @method $this suffixIcon(string $suffixIcon) 输入框尾部图标
+ * @method $this rows(float $rows) 输入框行数,只对 type = "textarea" 有效, 默认值: 2
+ * @method $this autocomplete(string $autocomplete) 原生属性,自动补全, 可选值: on, off, 默认值: off
+ * @method $this name(string $name) 原生属性
+ * @method $this readonly(bool $readonly) 原生属性,是否只读, 默认值: false
+ * @method $this max(string $max) 原生属性,设置最大值
+ * @method $this min(string $min) 原生属性,设置最小值
+ * @method $this step(string $step) 原生属性,设置输入字段的合法数字间隔
+ * @method $this resize(string $resize) 控制是否能被用户缩放, 可选值: none, both, horizontal, vertical
+ * @method $this autofocus(bool $autofocus) 原生属性,自动获取焦点, 可选值: true, false, 默认值: false
+ * @method $this form(string $form) 原生属性
+ * @method $this label(string $label) 输入框关联的label文字
+ * @method $this tabindex(string $tabindex) 输入框的tabindex
+ * @method $this validateEvent(bool $validateEvent) 输入时是否触发表单的校验, 默认值: true
+ */
+class Input extends FormComponent
+{
+ const TYPE_TEXT = 'text';
+
+ const TYPE_PASSWORD = 'password';
+
+ const TYPE_TEXTAREA = 'textarea';
+
+ const TYPE_URL = 'url';
+
+ const TYPE_EMAIL = 'email';
+
+ const TYPE_DATE = 'date';
+
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_TEXT
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'maxlength' => 'float',
+ 'minlength' => 'float',
+ 'showWordLimit' => 'bool',
+ 'placeholder' => 'string',
+ 'clearable' => 'bool',
+ 'showPassword' => 'bool',
+ 'disabled' => 'bool',
+ 'size' => 'string',
+ 'prefixIcon' => 'string',
+ 'suffixIcon' => 'string',
+ 'rows' => 'float',
+ 'autocomplete' => 'string',
+ 'name' => 'string',
+ 'readonly' => 'bool',
+ 'max' => 'string',
+ 'min' => 'string',
+ 'step' => 'string',
+ 'resize' => 'string',
+ 'autofocus' => 'bool',
+ 'form' => 'string',
+ 'label' => 'string',
+ 'tabindex' => 'string',
+ 'validateEvent' => 'bool',
+ ];
+
+ /**
+ * 自适应内容高度,仅在 textarea 类型下有效
+ *
+ * @param Bool|Number $minRows
+ * @param null|Number $maxRows
+ * @return $this
+ */
+ public function autoSize($minRows = false, $maxRows = null)
+ {
+ $this->props['autosize'] = $maxRows === null ? boolval($minRows) : compact('minRows', 'maxRows');
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Elm::validateStr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/InputNumber.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/InputNumber.php
new file mode 100644
index 0000000..fd1f817
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/InputNumber.php
@@ -0,0 +1,57 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 数字输入框组件
+ * Class InputNumber
+ *
+ * @method $this min(float $min) 设置计数器允许的最小值, 默认值: -Infinity
+ * @method $this max(float $max) 设置计数器允许的最大值, 默认值: Infinity
+ * @method $this step(float $step) 计数器步长, 默认值: 1
+ * @method $this stepStrictly(float $stepStrictly) 是否只能输入 step 的倍数, 默认值: false
+ * @method $this precision(float $precision) 数值精度
+ * @method $this size(string $size) 计数器尺寸, 可选值: large, small
+ * @method $this disabled(bool $disabled) 是否禁用计数器, 默认值: false
+ * @method $this controls(bool $controls) 是否使用控制按钮, 默认值: true
+ * @method $this controlsPosition(string $controlsPosition) 控制按钮位置, 可选值: right
+ * @method $this name(string $name) 原生属性
+ * @method $this label(string $label) 输入框关联的label文字
+ * @method $this placeholder(string $placeholder) 输入框默认 placeholder
+ */
+class InputNumber extends FormComponent
+{
+ protected static $propsRule = [
+ 'min' => 'float',
+ 'max' => 'float',
+ 'step' => 'float',
+ 'stepStrictly' => 'float',
+ 'precision' => 'float',
+ 'size' => 'string',
+ 'disabled' => 'bool',
+ 'controls' => 'bool',
+ 'controlsPosition' => 'string',
+ 'name' => 'string',
+ 'label' => 'string',
+ 'placeholder' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ return Elm::validateNum();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Option.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Option.php
new file mode 100644
index 0000000..342ed21
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Option.php
@@ -0,0 +1,56 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Contract\OptionComponentInterface;
+
+class Option implements OptionComponentInterface
+{
+ /**
+ * @var array
+ */
+ protected $rule;
+
+ /**
+ * Option constructor.
+ *
+ * @param string|number $value
+ * @param string $label
+ * @param bool $disabled
+ */
+ public function __construct($value, $label = '', $disabled = null)
+ {
+ $this->rule = compact('label', 'value');
+ if (!is_null($disabled))
+ $this->disabled($disabled);
+ }
+
+ /**
+ * @param bool $disabled
+ * @return $this
+ */
+ public function disabled($disabled = true)
+ {
+ $this->rule['disabled'] = !!$disabled;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOption()
+ {
+ return $this->rule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Popover.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Popover.php
new file mode 100644
index 0000000..89ccea9
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Popover.php
@@ -0,0 +1,55 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Popover
+ *
+ * @method $this trigger(string $trigger) 触发方式, 可选值: click/focus/hover/manual, 默认值: click
+ * @method $this popoverTitle(string $title) 标题
+ * @method $this content(string $content) 显示的内容,也可以通过 slot 传入 DOM
+ * @method $this width(string $width) 宽度, 默认值: 最小宽度 150px
+ * @method $this placement(string $placement) 出现位置, 可选值: top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end, 默认值: bottom
+ * @method $this disabled(boolean $disabled) Popover 是否可用, 默认值: false
+ * @method $this offset(number $offset) 出现位置的偏移量
+ * @method $this transition(string $transition) 定义渐变动画, 默认值: fade-in-linear
+ * @method $this visibleArrow(boolean $visibleArrow) 是否显示 Tooltip 箭头,更多参数可见Vue-popper, 默认值: true
+ * @method $this popperOptions(object $popperOptions) popper.js 的参数, 可选值: 参考 popper.js 文档, 默认值: {boundariesElement: 'body', gpuAcceleration: false}
+ * @method $this popperClass(string $popperClass) 为 popper 添加类名
+ * @method $this openDelay(number $openDelay) 触发方式为 hover 时的显示延迟,单位为毫秒
+ * @method $this closeDelay(float $closeDelay) 触发方式为 hover 时的隐藏延迟,单位为毫秒, 默认值: 200
+ * @method $this tabindex(float $tabindex) Popover 组件的 tabindex
+ */
+class Popover extends CustomComponent
+{
+ protected static $propsRule = [
+ 'trigger' => 'string',
+ 'title' => 'string',
+ 'content' => 'string',
+ 'width' => 'string',
+ 'placement' => 'string',
+ 'disabled' => 'boolean',
+ 'popoverTitle' => ['string', 'title'],
+ 'offset' => 'number',
+ 'transition' => 'string',
+ 'visibleArrow' => 'boolean',
+ 'popperOptions' => 'object',
+ 'popperClass' => 'string',
+ 'openDelay' => 'number',
+ 'closeDelay' => 'float',
+ 'tabindex' => 'float',
+ ];
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Radio.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Radio.php
new file mode 100644
index 0000000..ee36519
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Radio.php
@@ -0,0 +1,69 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 单选框组件
+ * Class Radio
+ *
+ * @method $this size(string $size) 单选框组尺寸,仅对按钮形式的 Radio 或带有边框的 Radio 有效, 可选值: medium / small / mini
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this textColor(string $textColor) 按钮形式的 Radio 激活时的文本颜色, 默认值: #ffffff
+ * @method $this fill(string $fill) 按钮形式的 Radio 激活时的填充色和边框色, 默认值: #409EFF
+ */
+class Radio extends FormOptionsComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'size' => 'string',
+ 'disabled' => 'bool',
+ 'textColor' => 'string',
+ 'fill' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ return Elm::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Elm::validateNum();
+ }
+
+ public function requiredNum($message = '')
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ return $this->appendValidate($this->createValidateNum()->message($message)->required());
+ }
+
+ /**
+ * 按钮样式
+ *
+ * @param bool $button
+ * @return $this
+ */
+ public function button($button = true)
+ {
+ if ($button)
+ $this->props['type'] = 'button';
+ else
+ unset($this->props['type']);
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Rate.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Rate.php
new file mode 100644
index 0000000..7a46075
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Rate.php
@@ -0,0 +1,69 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 评分组件
+ * Class Rate
+ *
+ * @method $this max(float $max) 最大分值, 默认值: 5
+ * @method $this disabled(bool $disabled) 是否为只读, 默认值: false
+ * @method $this allowHalf(bool $allowHalf) 是否允许半选, 默认值: false
+ * @method $this lowThreshold(float $lowThreshold) 低分和中等分数的界限值,值本身被划分在低分中, 默认值: 2
+ * @method $this highThreshold(float $highThreshold) 高分和中等分数的界限值,值本身被划分在高分中, 默认值: 4
+ * @method $this colors(array $colors) icon 的颜色。若传入数组,共有 3 个元素,为 3 个分段所对应的颜色;若传入对象,可自定义分段,键名为分段的界限值,键值为对应的颜色, 默认值: ['#F7BA2A', '#F7BA2A', '#F7BA2A']
+ * @method $this voidColor(string $voidColor) 未选中 icon 的颜色, 默认值: #C6D1DE
+ * @method $this disabledVoidColor(string $disabledVoidColor) 只读时未选中 icon 的颜色, 默认值: #EFF2F7
+ * @method $this iconClasses(array $iconClasses) icon 的类名。若传入数组,共有 3 个元素,为 3 个分段所对应的类名;若传入对象,可自定义分段,键名为分段的界限值,键值为对应的类名, 默认值: ['el-icon-star-on', 'el-icon-star-on', 'el-icon-star-on']
+ * @method $this voidIconClass(string $voidIconClass) 未选中 icon 的类名, 默认值: el-icon-star-off
+ * @method $this disabledVoidIconClass(string $disabledVoidIconClass) 只读时未选中 icon 的类名, 默认值: el-icon-star-on
+ * @method $this showText(bool $showText) 是否显示辅助文字,若为真,则会从 texts 数组中选取当前分数对应的文字内容, 默认值: false
+ * @method $this showScore(bool $showScore) 是否显示当前分数,show-score 和 show-text 不能同时为真, 默认值: false
+ * @method $this textColor(string $textColor) 辅助文字的颜色, 默认值: #1F2D3D
+ * @method $this texts(array $texts) 辅助文字数组, 默认值: ['极差', '失望', '一般', '满意', '惊喜']
+ * @method $this scoreTemplate(string $scoreTemplate) 分数显示模板, 默认值: {value}
+ *
+ */
+class Rate extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'max' => 'float',
+ 'disabled' => 'bool',
+ 'allowHalf' => 'bool',
+ 'lowThreshold' => 'float',
+ 'highThreshold' => 'float',
+ 'colors' => 'array',
+ 'voidColor' => 'string',
+ 'disabledVoidColor' => 'string',
+ 'iconClasses' => 'array',
+ 'voidIconClass' => 'string',
+ 'disabledVoidIconClass' => 'string',
+ 'showText' => 'bool',
+ 'showScore' => 'bool',
+ 'textColor' => 'string',
+ 'texts' => 'array',
+ 'scoreTemplate' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ return Elm::validateNum();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Select.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Select.php
new file mode 100644
index 0000000..8fdb437
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Select.php
@@ -0,0 +1,99 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 选择器组件
+ * Class Select
+ *
+ * @method $this multiple(bool $multiple) 是否多选, 默认值: false
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this valueKey(string $valueKey) 作为 value 唯一标识的键名,绑定值为对象类型时必填, 默认值: value
+ * @method $this size(string $size) 输入框尺寸, 可选值: medium/small/mini
+ * @method $this clearable(bool $clearable) 是否可以清空选项, 默认值: false
+ * @method $this collapseTags(bool $collapseTags) 多选时是否将选中值按文字的形式展示, 默认值: false
+ * @method $this multipleLimit(float $multipleLimit) 多选时用户最多可以选择的项目数,为 0 则不限制
+ * @method $this name(string $name) select input 的 name 属性
+ * @method $this autocomplete(string $autocomplete) select input 的 autocomplete 属性, 默认值: off
+ * @method $this placeholder(string $placeholder) 占位符, 默认值: 请选择
+ * @method $this filterable(bool $filterable) 是否可搜索, 默认值: false
+ * @method $this allowCreate(bool $allowCreate) 是否允许用户创建新条目,需配合 filterable 使用, 默认值: false
+ * @method $this remote(bool $remote) 是否为远程搜索, 默认值: false
+ * @method $this loading(bool $loading) 是否正在从远程获取数据, 默认值: false
+ * @method $this loadingText(string $loadingText) 远程加载时显示的文字, 默认值: 加载中
+ * @method $this noMatchText(string $noMatchText) 搜索条件无匹配时显示的文字,也可以使用slot = "empty"设置, 默认值: 无匹配数据
+ * @method $this noDataText(string $noDataText) 选项为空时显示的文字,也可以使用slot = "empty"设置, 默认值: 无数据
+ * @method $this popperClass(string $popperClass) Select 下拉框的类名
+ * @method $this reserveKeyword(bool $reserveKeyword) 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词, 默认值: false
+ * @method $this defaultFirstOption(bool $defaultFirstOption) 在输入框按下回车,选择第一个匹配项。需配合 filterable 或 remote 使用, 默认值: false
+ * @method $this popperAppendToBody(bool $popperAppendToBody) 是否将弹出框插入至 body 元素。在弹出框的定位出现问题时,可将该属性设置为 false, 默认值: true
+ * @method $this automaticDropdown(bool $automaticDropdown) 对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单, 默认值: false
+ *
+ */
+class Select extends FormOptionsComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'multiple' => false
+ ];
+
+ protected static $propsRule = [
+ 'multiple' => 'bool',
+ 'disabled' => 'bool',
+ 'valueKey' => 'string',
+ 'size' => 'string',
+ 'clearable' => 'bool',
+ 'collapseTags' => 'bool',
+ 'multipleLimit' => 'float',
+ 'name' => 'string',
+ 'autocomplete' => 'string',
+ 'placeholder' => 'string',
+ 'filterable' => 'bool',
+ 'allowCreate' => 'bool',
+ 'remote' => 'bool',
+ 'loading' => 'bool',
+ 'loadingText' => 'string',
+ 'noMatchText' => 'string',
+ 'noDataText' => 'string',
+ 'popperClass' => 'string',
+ 'reserveKeyword' => 'bool',
+ 'defaultFirstOption' => 'bool',
+ 'popperAppendToBody' => 'bool',
+ 'automaticDropdown' => 'bool',
+ ];
+
+ public function createValidate()
+ {
+ if ($this->props['multiple'] == true)
+ return Elm::validateArr();
+ else
+ return Elm::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Elm::validateNum();
+ }
+
+ public function requiredNum($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ return $this->appendValidate($this->createValidateNum()->message($message)->required());
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Slider.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Slider.php
new file mode 100644
index 0000000..1181188
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Slider.php
@@ -0,0 +1,75 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 滑块组件
+ * Class Slider
+ *
+ * @method $this min(float $min) 最小值
+ * @method $this max(float $max) 最大值, 默认值: 100
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this step(float $step) 步长, 默认值: 1
+ * @method $this showInput(bool $showInput) 是否显示输入框,仅在非范围选择时有效, 默认值: false
+ * @method $this showInputControls(bool $showInputControls) 在显示输入框的情况下,是否显示输入框的控制按钮, 默认值: true
+ * @method $this inputSize(string $inputSize) 输入框的尺寸, 可选值: large / medium / small / mini, 默认值: small
+ * @method $this showStops(bool $showStops) 是否显示间断点, 默认值: false
+ * @method $this showTooltip(bool $showTooltip) 是否显示 tooltip, 默认值: true
+ * @method $this range(bool $range) 是否为范围选择, 默认值: false
+ * @method $this vertical(bool $vertical) 是否竖向模式, 默认值: false
+ * @method $this height(string $height) Slider 高度,竖向模式时必填
+ * @method $this label(string $label) 屏幕阅读器标签
+ * @method $this debounce(float $debounce) 输入时的去抖延迟,毫秒,仅在show-input等于true时有效, 默认值: 300
+ * @method $this tooltipClass(string $tooltipClass) tooltip 的自定义类名
+ * @method $this marks(array $marks) 标记, key 的类型必须为 number 且取值在闭区间 [min, max] 内,每个标记可以单独设置样式
+ *
+ */
+class Slider extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'range' => false
+ ];
+
+ protected static $propsRule = [
+ 'min' => 'float',
+ 'max' => 'float',
+ 'disabled' => 'bool',
+ 'step' => 'float',
+ 'showInput' => 'bool',
+ 'showInputControls' => 'bool',
+ 'inputSize' => 'string',
+ 'showStops' => 'bool',
+ 'showTooltip' => 'bool',
+ 'range' => 'bool',
+ 'vertical' => 'bool',
+ 'height' => 'string',
+ 'label' => 'string',
+ 'debounce' => 'float',
+ 'tooltipClass' => 'string',
+ 'marks' => 'array',
+ ];
+
+ public function createValidate()
+ {
+ if ($this->props['range'] == true)
+ return Elm::validateArr();
+ else
+ return Elm::validateNum();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Switches.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Switches.php
new file mode 100644
index 0000000..5827b8f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Switches.php
@@ -0,0 +1,75 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 开关组件
+ * Class Switches
+ *
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this width(float $width) switch 的宽度(像素), 默认值: 40
+ * @method $this activeIconClass(string $activeIconClass) switch 打开时所显示图标的类名,设置此项会忽略 active-text
+ * @method $this inactiveIconClass(string $inactiveIconClass) switch 关闭时所显示图标的类名,设置此项会忽略 inactive-text
+ * @method $this activeText(string $activeText) switch 打开时的文字描述
+ * @method $this inactiveText(string $inactiveText) switch 关闭时的文字描述
+ * @method $this activeValue(mixed $activeValue) switch 打开时的值, 默认值: true
+ * @method $this inactiveValue(mixed $inactiveValue) switch 关闭时的值, 默认值: false
+ * @method $this activeColor(string $activeColor) switch 打开时的背景色, 默认值: #409EFF
+ * @method $this inactiveColor(string $inactiveColor) switch 关闭时的背景色, 默认值: #C0CCDA
+ * @method $this name(string $name) switch 对应的 name 属性
+ * @method $this validateEvent(bool $validateEvent) 改变 switch 状态时是否触发表单的校验, 默认值: true
+ */
+class Switches extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'activeValue' => '1',
+ 'inactiveValue' => '0',
+ ];
+
+ protected static $propsRule = [
+ 'disabled' => 'bool',
+ 'width' => 'float',
+ 'activeIconClass' => 'string',
+ 'inactiveIconClass' => 'string',
+ 'activeText' => 'string',
+ 'inactiveText' => 'string',
+ 'activeValue' => '',
+ 'inactiveValue' => '',
+ 'activeColor' => 'string',
+ 'inactiveColor' => 'string',
+ 'name' => 'string',
+ 'validateEvent' => 'bool',
+ ];
+
+ public function getComponentName()
+ {
+ return 'switch';
+ }
+
+ public function createValidate()
+ {
+ return Elm::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Elm::validateNum();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/TimePicker.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/TimePicker.php
new file mode 100644
index 0000000..1acc74e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/TimePicker.php
@@ -0,0 +1,111 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 时间选择器组件
+ * Class TimePicker
+ *
+ * @method $this readonly(bool $readonly) 完全只读, 默认值: false
+ * @method $this disabled(bool $disabled) 禁用, 默认值: false
+ * @method $this editable(bool $editable) 文本框可输入, 默认值: true
+ * @method $this clearable(bool $clearable) 是否显示清除按钮, 默认值: true
+ * @method $this size(string $size) 输入框尺寸, 可选值: medium / small / mini
+ * @method $this placeholder(string $placeholder) 非范围选择时的占位内容
+ * @method $this startPlaceholder(string $startPlaceholder) 范围选择时开始日期的占位内容
+ * @method $this endPlaceholder(string $endPlaceholder) 范围选择时开始日期的占位内容
+ * @method $this isRange(bool $isRange) 是否为时间范围选择,仅对有效, 默认值: false
+ * @method $this arrowControl(bool $arrowControl) 是否使用箭头进行时间选择,仅对有效, 默认值: false
+ * @method $this align(string $align) 对齐方式, 可选值: left / center / right, 默认值: left
+ * @method $this popperClass(string $popperClass) TimePicker 下拉框的类名
+ * @method $this pickerOptions(array $pickerOptions) 当前时间日期选择器特有的选项参考下表, 默认值: {}
+ * @method $this rangeSeparator(string $rangeSeparator) 选择范围时的分隔符, 默认值: '-'
+ * @method $this valueFormat(string $valueFormat) 可选,仅TimePicker时可用,绑定值的格式。不指定则绑定值为 Date 对象, 可选值: 见日期格式
+ * @method $this prefixIcon(string $prefixIcon) 自定义头部图标的类名, 默认值: el-icon-time
+ * @method $this clearIcon(string $clearIcon) 自定义清空图标的类名, 默认值: el-icon-circle-close
+ *
+ */
+class TimePicker extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'isRange' => false,
+ 'editable' => false,
+ ];
+
+ protected static $propsRule = [
+ 'readonly' => 'bool',
+ 'disabled' => 'bool',
+ 'isRange' => 'bool',
+ 'editable' => 'bool',
+ 'clearable' => 'bool',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'startPlaceholder' => 'string',
+ 'endPlaceholder' => 'string',
+ 'arrowControl' => 'bool',
+ 'align' => 'string',
+ 'popperClass' => 'string',
+ 'pickerOptions' => 'array',
+ 'rangeSeparator' => 'string',
+ 'valueFormat' => 'string',
+ 'prefixIcon' => 'string',
+ 'clearIcon' => 'string',
+ ];
+
+ /**
+ * 下拉列表的时间间隔,数组的三项分别对应小时、分钟、秒。
+ * 例如设置为 [1, 15] 时,分钟会显示:00、15、30、45。
+ *
+ * @param $h
+ * @param int $i
+ * @param int $s
+ * @return $this
+ */
+ public function steps($h, $i = 0, $s = 0)
+ {
+ $this->props['steps'] = [$h, $i, $s];
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ if ($this->props['isRange'])
+ return Elm::validateArr();
+ else
+ return Elm::validateStr();
+ }
+
+ public function required($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ $validate = $this->createValidate();
+ if ($this->props['isRange']) {
+ $required = ['required' => true, 'message' => $message];
+ $validate->fields([
+ '0' => $required,
+ '1' => $required
+ ]);
+ return $this;
+ }
+
+ $this->appendValidate($validate->message($message)->required());
+ return $this;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Tooltip.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Tooltip.php
new file mode 100644
index 0000000..866c21c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Tooltip.php
@@ -0,0 +1,54 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Tooltip
+ *
+ * @method $this effect(string $effect) 默认提供的主题, 可选值: dark/light, 默认值: dark
+ * @method $this placement(string $placement) Tooltip 的出现位置, 可选值: top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end, 默认值: bottom
+ * @method $this disabled(bool $disabled) Tooltip 是否可用, 默认值: false
+ * @method $this offset(float $offset) 出现位置的偏移量
+ * @method $this transition(string $transition) 定义渐变动画, 默认值: el-fade-in-linear
+ * @method $this visibleArrow(bool $visibleArrow) 是否显示 Tooltip 箭头,更多参数可见Vue-popper, 默认值: true
+ * @method $this popperOptions(array $popperOptions) popper.js 的参数, 可选值: 参考 popper.js 文档, 默认值: {boundariesElement: 'body', gpuAcceleration: false}
+ * @method $this openDelay(float $openDelay) 延迟出现,单位毫秒
+ * @method $this manual(bool $manual) 手动控制模式,设置为 true 后,mouseenter 和 mouseleave 事件将不会生效, 默认值: false
+ * @method $this popperClass(string $popperClass) 为 Tooltip 的 popper 添加类名
+ * @method $this enterable(bool $enterable) 鼠标是否可进入到 tooltip 中, 默认值: true
+ * @method $this hideAfter(float $hideAfter) Tooltip 出现后自动隐藏延时,单位毫秒,为 0 则不会自动隐藏
+ * @method $this tabindex(float $tabindex) Tooltip 组件的 tabindex
+ */
+class Tooltip extends CustomComponent
+{
+ protected static $propsRule = [
+ 'effect' => 'string',
+ 'content' => 'string',
+ 'placement' => 'string',
+ 'value / vModel' => 'bool',
+ 'disabled' => 'bool',
+ 'offset' => 'float',
+ 'transition' => 'string',
+ 'visibleArrow' => 'bool',
+ 'popperOptions' => 'array',
+ 'openDelay' => 'float',
+ 'manual' => 'bool',
+ 'popperClass' => 'string',
+ 'enterable' => 'bool',
+ 'hideAfter' => 'float',
+ 'tabindex' => 'float',
+ ];
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Tree.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Tree.php
new file mode 100644
index 0000000..8ddbc5a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Tree.php
@@ -0,0 +1,108 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 树型组件
+ * Class Tree
+ *
+ * @method $this type(string $type) 类型,可选值为 checked、selected
+ * @method $this emptyText(string $emptyText) 内容为空的时候展示的文本
+ * @method $this nodeKey(string $nodeKey) 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
+ * @method $this props(array $props) 配置选项,具体看下表
+ * @method $this renderAfterExpand(bool $renderAfterExpand) 是否在第一次展开某个树节点后才渲染其子节点, 默认值: true
+ * @method $this highlightCurrent(bool $highlightCurrent) 是否高亮当前选中节点,默认值是 false。, 默认值: false
+ * @method $this defaultExpandAll(bool $defaultExpandAll) 是否默认展开所有节点, 默认值: false
+ * @method $this expandOnClickNode(bool $expandOnClickNode) 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。, 默认值: true
+ * @method $this checkOnClickNode(bool $checkOnClickNode) 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。, 默认值: false
+ * @method $this autoExpandParent(bool $autoExpandParent) 展开子节点的时候是否自动展开父节点, 默认值: true
+ * @method $this showCheckbox(bool $showCheckbox) 节点是否可被选择, 默认值: false
+ * @method $this checkStrictly(bool $checkStrictly) 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false, 默认值: false
+ * @method $this accordion(bool $accordion) 是否每次只打开一个同级树节点展开, 默认值: false
+ * @method $this indent(float $indent) 相邻级节点间的水平缩进,单位为像素, 默认值: 16
+ * @method $this iconClass(string $iconClass) 自定义树节点的图标
+ * @method $this draggable(bool $draggable) 是否开启拖拽节点功能, 默认值: false
+ */
+class Tree extends FormComponent
+{
+ /**
+ * 选中
+ */
+ const TYPE_SELECTED = 'selected';
+ /**
+ * 选择
+ */
+ const TYPE_CHECKED = 'checked';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_CHECKED,
+ 'showCheckbox' => true,
+ 'data' => []
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'emptyText' => 'string',
+ 'nodeKey' => 'string',
+ 'props' => 'array',
+ 'renderAfterExpand' => 'bool',
+ 'highlightCurrent' => 'bool',
+ 'defaultExpandAll' => 'bool',
+ 'expandOnClickNode' => 'bool',
+ 'checkOnClickNode' => 'bool',
+ 'autoExpandParent' => 'bool',
+ 'showCheckbox' => 'bool',
+ 'checkStrictly' => 'bool',
+ 'accordion' => 'bool',
+ 'indent' => 'float',
+ 'iconClass' => 'string',
+ 'draggable' => 'bool',
+ ];
+
+ /**
+ * @param array $treeData
+ * @return $this
+ */
+ public function data(array $treeData)
+ {
+ $this->props['data'] = [];
+ foreach ($treeData as $child) {
+ $this->props['data'][] = $child instanceof TreeData
+ ? $child->getOption()
+ : $child;
+ }
+ return $this;
+ }
+
+ /**
+ * @param string $var
+ * @return $this
+ */
+ public function jsData($var)
+ {
+ $this->props['data'] = 'js.' . (string)$var;
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Elm::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/TreeData.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/TreeData.php
new file mode 100644
index 0000000..0301897
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/TreeData.php
@@ -0,0 +1,106 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Contract\OptionComponentInterface;
+use FormBuilder\Rule\CallPropsRule;
+
+/**
+ * Class TreeData
+ *
+ * @method $this id(string $id) Id, 必须唯一
+ * @method $this title(string $title) 标题
+ * @method $this expand(bool $bool) 是否展开直子节点, 默认为false
+ * @method $this disabled(bool $bool) 禁掉响应, 默认为false
+ * @method $this disableCheckbox(bool $bool) 禁掉 checkbox
+ * @method $this selected(bool $bool) 是否选中子节点
+ * @method $this checked(bool $bool) 是否勾选(如果勾选,子节点也会全部勾选)
+ */
+class TreeData implements OptionComponentInterface
+{
+ use CallPropsRule;
+
+ /**
+ * @var array
+ */
+ protected $children = [];
+
+ /**
+ * @var array
+ */
+ protected $props = [];
+
+ /**
+ * @var array
+ */
+ protected static $propsRule = [
+ 'id' => 'string',
+ 'title' => 'string',
+ 'expand' => 'bool',
+ 'disabled' => 'bool',
+ 'disableCheckbox' => 'bool',
+ 'selected' => 'bool',
+ 'checked' => 'bool',
+ ];
+
+ /**
+ * TreeData constructor.
+ *
+ * @param $id
+ * @param $title
+ * @param array $children
+ */
+ public function __construct($id, $title, array $children = [])
+ {
+ $this->props['id'] = $id;
+ $this->props['title'] = $title;
+ $this->children = $children;
+ }
+
+ /**
+ * @param array $children
+ * @return $this
+ */
+ public function children(array $children)
+ {
+ $this->children = array_merge($this->children, $children);
+ return $this;
+ }
+
+ /**
+ * @param TreeData $child
+ * @return $this
+ */
+ public function child(TreeData $child)
+ {
+ $this->children[] = $child;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOption()
+ {
+ $children = [];
+ foreach ($this->children as $child) {
+ $children[] = $child instanceof TreeData
+ ? $child->getOption()
+ : $child;
+ }
+ $this->props['children'] = $children;
+ return $this->props;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Components/Upload.php b/vendor/xaboy/form-builder/src/UI/Elm/Components/Upload.php
new file mode 100644
index 0000000..42d94cb
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Components/Upload.php
@@ -0,0 +1,106 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Elm;
+
+/**
+ * 上传组件
+ * Class Upload
+ *
+ * @method $this uploadType(string $uploadType) 上传文件类型,可选值为 image(图片上传),file(文件上传)
+ * @method $this action(string $action) 必选参数,上传的地址
+ * @method $this multiple(bool $multiple) 是否支持多选文件
+ * @method $this uploadName(string $name) 上传的文件字段名, 默认值: file
+ * @method $this withCredentials(bool $withCredentials) 支持发送 cookie 凭证信息, 默认值: false
+ * @method $this accept(string $accept) 接受上传的文件类型(thumbnail-mode 模式下此参数无效)
+ * @method $this listType(string $listType) 文件列表的类型, 可选值: text/picture/picture-card, 默认值: text
+ * @method $this autoUpload(bool $autoUpload) 是否在选取文件后立即进行上传, 默认值: true
+ * @method $this disabled(bool $disabled) 是否禁用, 默认值: false
+ * @method $this limit(float $limit) 最大允许上传个数
+ *
+ */
+class Upload extends FormComponent
+{
+ /**
+ * file类型
+ */
+ const TYPE_FILE = 'file';
+
+ /**
+ * image类型
+ */
+ const TYPE_IMAGE = 'image';
+
+
+ protected $defaultProps = [
+ 'limit' => 0,
+ 'uploadType' => self::TYPE_FILE,
+ 'headers' => [],
+ 'data' => []
+ ];
+
+ protected static $propsRule = [
+ 'uploadType' => 'string',
+ 'action' => 'string',
+ 'multiple' => 'bool',
+ 'uploadName' => ['string', 'name'],
+ 'withCredentials' => 'bool',
+ 'accept' => 'string',
+ 'listType' => 'string',
+ 'autoUpload' => 'bool',
+ 'disabled' => 'bool',
+ 'limit' => 'float',
+ ];
+
+ protected function init()
+ {
+ $this->name($this->field);
+ }
+
+ /**
+ * 设置上传的请求头部
+ *
+ * @param array $headers
+ * @return $this
+ */
+ public function headers(array $headers)
+ {
+ $this->props['headers'] = array_merge($this->props['headers'], $headers);
+ return $this;
+ }
+
+ /**
+ * 上传时附带的额外参数
+ *
+ * @param array $data
+ * @return $this
+ */
+ public function data(array $data)
+ {
+ $this->props['data'] = array_merge($this->props['data'], $data);
+ return $this;
+ }
+
+ protected function getPlaceHolder()
+ {
+ return '请上传' . $this->title;
+ }
+
+ public function createValidate()
+ {
+ return $this->props['limit'] == 1 ? Elm::validateStr() : Elm::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Config.php b/vendor/xaboy/form-builder/src/UI/Elm/Config.php
new file mode 100644
index 0000000..a32d187
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Config.php
@@ -0,0 +1,218 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm;
+
+
+use FormBuilder\Contract\ConfigInterface;
+use FormBuilder\UI\Elm\Components\Button;
+use FormBuilder\UI\Elm\Components\Popover;
+use FormBuilder\UI\Elm\Components\Tooltip;
+use FormBuilder\UI\Elm\Style\FormStyle;
+use FormBuilder\UI\Elm\Style\Row;
+
+class Config implements ConfigInterface
+{
+ const INFO_TYPE_POPOVER = 'popover';
+
+ const INFO_TYPE_TOOLTIP = 'tooltip';
+
+ protected $config;
+
+ public function __construct(array $config = [])
+ {
+ $this->config = $config;
+ }
+
+ public function info($type)
+ {
+ if (is_array($type) || $type instanceof Popover || $type instanceof Tooltip)
+ $this->config['info'] = $type;
+
+ return $this;
+ }
+
+ /**
+ * @param string $type tooltip,poptip
+ * @return Popover|Tooltip
+ */
+ public function createInfo($type = self::INFO_TYPE_POPOVER)
+ {
+ if (strtolower($type) === self::INFO_TYPE_TOOLTIP)
+ $info = new Tooltip();
+ else
+ $info = new Popover();
+ $this->info($info);
+ return $info;
+ }
+
+ /**
+ * 表单整体显示规则配置
+ *
+ * @param FormStyle|array $formStyle
+ * @return $this
+ */
+ public function formStyle($formStyle)
+ {
+ $this->config['form'] = $formStyle;
+ return $this;
+ }
+
+ public function createFormStyle(array $rule = [])
+ {
+ $formStyle = new FormStyle($rule);
+ $this->formStyle($formStyle);
+ return $formStyle;
+ }
+
+ /**
+ * 表单组件布局配置
+ *
+ * @param Row|array $row
+ * @return $this
+ */
+ public function row($row)
+ {
+ $this->config['row'] = $row;
+ return $this;
+ }
+
+ public function createRow(array $rule = [])
+ {
+ $row = new Row($rule);
+ $this->row($row);
+ return $row;
+ }
+
+ /**
+ * 提交按钮样式和布局配置
+ *
+ * @param Button|array|bool $btn
+ * @return $this
+ */
+ public function submitBtn($btn)
+ {
+ $this->config['submitBtn'] = $btn;
+ return $this;
+ }
+
+ public function createSubmitBtn()
+ {
+ $submitBtn = new Button();
+ $this->submitBtn($submitBtn);
+ return $submitBtn;
+ }
+
+ /**
+ * 开启事件注入
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function injectEvent($bool)
+ {
+ $this->config['injectEvent'] = !!$bool;
+ return $this;
+ }
+
+ /**
+ * 重置按钮样式和布局配置
+ *
+ * @param Button|array|bool $btn
+ * @return $this
+ */
+ public function resetBtn($btn)
+ {
+ $this->config['resetBtn'] = $btn;
+ return $this;
+ }
+
+ /**
+ * @return Button
+ */
+ public function createResetBtn()
+ {
+ $resetBtn = new Button();
+ $this->resetBtn($resetBtn);
+ return $resetBtn;
+ }
+
+ /**
+ * @param Button $btn
+ * @return mixed
+ */
+ protected function parseButton(Button $btn)
+ {
+ $rule = $btn->build();
+ if (isset($rule['col']))
+ $rule['props']->col = $rule['col'];
+ return $rule['props'];
+ }
+
+ /**
+ * @param $info
+ * @return mixed
+ */
+ protected function parseInfo($info)
+ {
+ if ($info instanceof Popover || $info instanceof Tooltip) {
+ $rule = $info->build();
+ $info = $rule['props'];
+ $info->type = $rule['type'];
+ }
+
+ return $info;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ $config = $this->config;
+ if (isset($config['form']) && ($form = $config['form']) instanceof FormStyle)
+ $config['form'] = $form->getStyle();
+ if (isset($config['row']) && ($row = $config['row']) instanceof Row)
+ $config['row'] = $row->getStyle();
+ if (isset($config['submitBtn']) && ($submitBtn = $config['submitBtn']) instanceof Button)
+ $config['submitBtn'] = $this->parseButton($submitBtn);
+ if (isset($config['resetBtn']) && ($resetBtn = $config['resetBtn']) instanceof Button)
+ $config['resetBtn'] = $this->parseButton($resetBtn);
+ if (isset($config['info']))
+ $config['info'] = $this->parseInfo($config['info']);
+
+ return $config;
+ }
+
+
+ /**
+ * @param string $componentName
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalConfig($componentName, array $config)
+ {
+ if (!isset($this->config['global'])) $this->config['global'] = [];
+ $this->config['global'][$componentName] = $config;
+ return $this;
+ }
+
+
+ /**
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalCommonConfig(array $config)
+ {
+ return $this->componentGlobalConfig('*', $config);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Style/Col.php b/vendor/xaboy/form-builder/src/UI/Elm/Style/Col.php
new file mode 100644
index 0000000..3373733
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Style/Col.php
@@ -0,0 +1,166 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Style;
+
+
+use FormBuilder\Contract\ColComponentInterface;
+
+/**
+ * col栅格规则
+ *
+ * Class Col
+ * @package FormBuilder\Style
+ */
+class Col implements ColComponentInterface
+{
+ protected $rule;
+
+ public function __construct(array $rule = [])
+ {
+ $this->rule = $rule;
+ }
+
+ /**
+ * 栅格的占位格数,可选值为0~24的整数,为 0 时,相当于display:none
+ *
+ * @param int|string $span
+ * @return $this
+ */
+ public function span($span)
+ {
+ $this->rule['span'] = $span;
+ return $this;
+ }
+
+ /**
+ * 设置表单域 label 的宽度
+ *
+ * @param int|string $labelWidth
+ * @return $this
+ */
+ public function labelWidth($labelWidth)
+ {
+ $this->rule['labelWidth'] = $labelWidth;
+ return $this;
+ }
+
+ /**
+ * 栅格左侧的间隔格数,间隔内不可以有栅格
+ *
+ * @param $offset
+ * @return $this
+ */
+ public function offset($offset)
+ {
+ $this->rule['offset'] = $offset;
+ return $this;
+ }
+
+ /**
+ * 栅格向右移动格数
+ *
+ * @param $push
+ * @return $this
+ */
+ public function push($push)
+ {
+ $this->rule['push'] = $push;
+ return $this;
+ }
+
+ /**
+ * 栅格向左移动格数
+ *
+ * @param $pull
+ * @return $this
+ */
+ public function pull($pull)
+ {
+ $this->rule['pull'] = $pull;
+ return $this;
+ }
+
+ /**
+ * 自定义元素标签
+ *
+ * @param string $tag
+ * @return $this
+ */
+ public function tag($tag)
+ {
+ $this->rule['tag'] = (string)$tag;
+ return $this;
+ }
+
+ /**
+ * <768px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $xs
+ * @return $this
+ */
+ public function xs($xs)
+ {
+ $this->rule['xs'] = $this->buildGrid($xs);
+ return $this;
+ }
+
+ /**
+ * ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $sm
+ * @return $this
+ */
+ public function sm($sm)
+ {
+ $this->rule['sm'] = $this->buildGrid($sm);
+ return $this;
+ }
+
+ /**
+ * ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $md
+ * @return $this
+ */
+ public function md($md)
+ {
+ $this->rule['md'] = $this->buildGrid($md);
+ return $this;
+ }
+
+ /**
+ * ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $lg
+ * @return $this
+ */
+ public function lg($lg)
+ {
+ $this->rule['lg'] = $this->buildGrid($lg);
+ return $this;
+ }
+
+ protected function buildGrid($grid)
+ {
+ return $grid instanceof self ? $grid->getCol() : (int)$grid;
+ }
+
+ /**
+ * @return object
+ */
+ public function getCol()
+ {
+ return (object)$this->rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Style/FormStyle.php b/vendor/xaboy/form-builder/src/UI/Elm/Style/FormStyle.php
new file mode 100644
index 0000000..a047b0a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Style/FormStyle.php
@@ -0,0 +1,86 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Style;
+
+
+use FormBuilder\Contract\StyleInterface;
+use FormBuilder\Rule\CallPropsRule;
+
+/**
+ * form表单样式
+ * Class FormStyle
+ *
+ * @method $this inline(bool $inline) 行内表单模式, 默认值: false
+ * @method $this labelPosition(string $labelPosition) 表单域标签的位置,如果值为 left 或者 right 时,则需要设置 label-width, 可选值: right/left/top, 默认值: right
+ * @method $this labelWidth(string $labelWidth) 表单域标签的宽度,例如 '50px'。作为 Form 直接子元素的 form-item 会继承该值。支持 auto。
+ * @method $this labelSuffix(string $labelSuffix) 表单域标签的后缀
+ * @method $this hideRequiredAsterisk(bool $hideRequiredAsterisk) 是否显示必填字段的标签旁边的红色星号, 默认值: false
+ * @method $this showMessage(bool $showMessage) 是否显示校验错误信息, 默认值: true
+ * @method $this inlineMessage(bool $inlineMessage) 是否以行内形式展示校验信息, 默认值: false
+ * @method $this statusIcon(bool $statusIcon) 是否在输入框中显示校验结果反馈图标, 默认值: false
+ * @method $this validateOnRuleChange(bool $validateOnRuleChange) 是否在 rules 属性改变后立即触发一次验证, 默认值: true
+ * @method $this size(string $size) 用于控制该表单内组件的尺寸, 可选值: medium / small / mini
+ * @method $this disabled(bool $disabled) 是否禁用该表单内的所有组件。若设置为 true,则表单内组件上的 disabled 属性不再生效, 默认值: false
+ */
+class FormStyle implements StyleInterface
+{
+ use CallPropsRule;
+
+ /**
+ * @var array
+ */
+ protected $props;
+
+ protected static $propsRule = [
+ 'inline' => 'bool',
+ 'labelPosition' => 'string',
+ 'labelWidth' => 'string',
+ 'labelSuffix' => 'string',
+ 'hideRequiredAsterisk' => 'bool',
+ 'showMessage' => 'bool',
+ 'inlineMessage' => 'bool',
+ 'statusIcon' => 'bool',
+ 'validateOnRuleChange' => 'bool',
+ 'size' => 'string',
+ 'disabled' => 'bool',
+ ];
+
+ /**
+ * FormStyle constructor.
+ * @param array $rule
+ */
+ public function __construct(array $rule = [])
+ {
+ $this->props = $rule;
+ }
+
+ /**
+ * 设置表单 class
+ * @param $class
+ * @return $this
+ */
+ public function className($class)
+ {
+ $this->props['className'] = $class;
+ return $this;
+ }
+
+ /**
+ * @return object
+ */
+ public function getStyle()
+ {
+ return (object)$this->props;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Style/Row.php b/vendor/xaboy/form-builder/src/UI/Elm/Style/Row.php
new file mode 100644
index 0000000..d33358d
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Style/Row.php
@@ -0,0 +1,90 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Style;
+
+
+use FormBuilder\Contract\StyleInterface;
+
+/**
+ * row栅格规则
+ *
+ * Class Row
+ */
+class Row implements StyleInterface
+{
+ protected $rule;
+
+ public function __construct(array $rule = [])
+ {
+ $this->rule = $rule;
+ }
+
+ /**
+ * 栅格间距,单位 px,左右平分
+ *
+ * @param int $gutter
+ */
+ public function gutter($gutter)
+ {
+ $this->rule['gutter'] = (float)$gutter;
+ }
+
+ /**
+ * 布局模式,可选值为flex或不选,在现代浏览器下有效
+ *
+ * @param string $type
+ */
+ public function type($type)
+ {
+ $this->rule['type'] = $type;
+ }
+
+ /**
+ * flex 布局下的垂直对齐方式,可选值为top、middle、bottom
+ *
+ * @param string $align
+ */
+ public function align($align)
+ {
+ $this->rule['align'] = $align;
+ }
+
+ /**
+ * flex 布局下的水平排列方式,可选值为start、end、center、space-around、space-between
+ *
+ * @param string $justify
+ */
+ public function justify($justify)
+ {
+ $this->rule['justify'] = $justify;
+ }
+
+ /**
+ * 自定义元素标签
+ *
+ * @param string $tag
+ */
+ public function tag($tag)
+ {
+ $this->rule['tag'] = $tag;
+ }
+
+ /**
+ * @return object
+ */
+ public function getStyle()
+ {
+ return (object)$this->rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/CascaderFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/CascaderFactoryTrait.php
new file mode 100644
index 0000000..f0346cb
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/CascaderFactoryTrait.php
@@ -0,0 +1,80 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Cascader;
+
+trait CascaderFactoryTrait
+{
+ /**
+ * 多级联动组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @param string $type
+ * @return Cascader
+ */
+ public static function cascader($field, $title, array $value = [], $type = Cascader::TYPE_OTHER)
+ {
+ $cascader = new Cascader($field, $title, $value);
+ $cascader->type($type);
+ return $cascader;
+ }
+
+
+ /**
+ * 省市二级联动
+ *
+ * @param string $field
+ * @param string $title
+ * @param array|string $province
+ * @param string $city
+ * @return Cascader
+ */
+ public static function city($field, $title, $province = [], $city = '')
+ {
+ if (is_array($province))
+ $value = $province;
+ else
+ $value = [(string)$province, (string)$city];
+
+ $cascader = self::cascader($field, $title, $value, Cascader::TYPE_CITY);
+ $cascader->jsOptions('province_city');
+ return $cascader;
+ }
+
+
+ /**
+ * 省市区三级联动
+ *
+ * @param string $field
+ * @param string $title
+ * @param array|string $province
+ * @param string $city
+ * @param string $area
+ * @return Cascader
+ */
+ public static function cityArea($field, $title, $province = [], $city = '', $area = '')
+ {
+ if (is_array($province))
+ $value = $province;
+ else
+ $value = [(string)$province, (string)$city, (string)$area];
+
+ $cascader = self::cascader($field, $title, $value, Cascader::TYPE_CITY_AREA);
+ $cascader->jsOptions('province_city_area');
+ return $cascader;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/CheckBoxFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/CheckBoxFactoryTrait.php
new file mode 100644
index 0000000..092e77e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/CheckBoxFactoryTrait.php
@@ -0,0 +1,31 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+use FormBuilder\UI\Elm\Components\Checkbox;
+
+trait CheckBoxFactoryTrait
+{
+ /**
+ * 多选框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Checkbox
+ */
+ public static function checkbox($field, $title, array $value = [])
+ {
+ return new Checkbox($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/ColorPickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/ColorPickerFactoryTrait.php
new file mode 100644
index 0000000..36dbc9d
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/ColorPickerFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\ColorPicker;
+
+trait ColorPickerFactoryTrait
+{
+ /**
+ * 颜色选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return ColorPicker
+ */
+ public static function color($field, $title, $value = '')
+ {
+ return new ColorPicker($field, $title, (string)$value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/DatePickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/DatePickerFactoryTrait.php
new file mode 100644
index 0000000..431c791
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/DatePickerFactoryTrait.php
@@ -0,0 +1,141 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\DatePicker;
+
+trait DatePickerFactoryTrait
+{
+ /**
+ * 日期组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param string $type
+ * @return DatePicker
+ */
+ public static function datePicker($field, $title, $value = '', $type = DatePicker::TYPE_DATE)
+ {
+ $datePicker = new DatePicker($field, $title, $value);
+ return $datePicker->type($type);
+ }
+
+ /**
+ * 单选日期
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function date($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_DATE);
+ }
+
+ /**
+ * 多选日期
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return DatePicker
+ */
+ public static function dates($field, $title, array $value)
+ {
+ return self::datePicker($field, $title, $value, DatePicker::TYPE_DATES);
+ }
+
+ /**
+ * 日期区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $startDate
+ * @param string $endDate
+ * @return DatePicker
+ */
+ public static function dateRange($field, $title, $startDate = '', $endDate = '')
+ {
+ return self::datePicker($field, $title, [(string)$startDate, (string)$endDate], DatePicker::TYPE_DATE_RANGE);
+ }
+
+ /**
+ * 单选日期时间
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function dateTime($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_DATE_TIME);
+ }
+
+ /**
+ * 日期时间区间选择
+ *
+ * @param $field
+ * @param $title
+ * @param string $startDate
+ * @param string $endDate
+ * @return DatePicker
+ */
+ public static function dateTimeRange($field, $title, $startDate = '', $endDate = '')
+ {
+ return self::datePicker($field, $title, [(string)$startDate, (string)$endDate], DatePicker::TYPE_DATE_TIME_RANGE);
+ }
+
+ /**
+ * 选择年
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function year($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_YEAR);
+ }
+
+ /**
+ * 选择月
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function month($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_MONTH);
+ }
+
+ /**
+ * 月区间选择
+ *
+ * @param $field
+ * @param $title
+ * @param string $startDate
+ * @param string $endDate
+ * @return DatePicker
+ */
+ public static function monthRange($field, $title, $startDate = '', $endDate = '')
+ {
+ return self::datePicker($field, $title, [(string)$startDate, (string)$endDate], DatePicker::TYPE_MONTH_RANGE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/FormStyleFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/FormStyleFactoryTrait.php
new file mode 100644
index 0000000..31aee75
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/FormStyleFactoryTrait.php
@@ -0,0 +1,55 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Style\Col;
+use FormBuilder\UI\Elm\Style\FormStyle;
+use FormBuilder\UI\Elm\Style\Row;
+
+trait FormStyleFactoryTrait
+{
+
+ /**
+ * 组件布局规则类
+ *
+ * @param array $rule
+ * @return Col
+ */
+ public static function col(array $rule = [])
+ {
+ return new Col($rule);
+ }
+
+ /**
+ * 表格布局规则类
+ *
+ * @param $rule
+ * @return Row
+ */
+ public static function row(array $rule = [])
+ {
+ return new Row($rule);
+ }
+
+ /**
+ * 表格样式类
+ *
+ * @param array $rule
+ * @return FormStyle
+ */
+ public static function style(array $rule = [])
+ {
+ return new FormStyle($rule);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/FrameFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/FrameFactoryTrait.php
new file mode 100644
index 0000000..573aa01
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/FrameFactoryTrait.php
@@ -0,0 +1,126 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Frame;
+
+trait FrameFactoryTrait
+{
+ /**
+ * 框架组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string|array $value
+ * @param string $type
+ * @return Frame
+ */
+ public static function frame($field, $title, $src, $value = [], $type = Frame::TYPE_INPUT)
+ {
+ $length = is_array($value) ? 0 : 1;
+ $frame = new Frame($field, $title, $value);
+ return $frame->maxLength($length)->src($src)->type($type);
+ }
+
+ /**
+ * 使用input类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameInputs($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value);
+ }
+
+ /**
+ * 使用文件类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameFiles($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_FILE);
+ }
+
+ /**
+ * 使用图片类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameImages($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_IMAGE);
+ }
+
+ /**
+ * 使用input类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameInput($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value);
+ }
+
+ /**
+ * 使用文件类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameFile($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_FILE);
+ }
+
+ /**
+ * 使用图片类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameImage($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_IMAGE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/GroupFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/GroupFactoryTrait.php
new file mode 100644
index 0000000..cd17eae
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/GroupFactoryTrait.php
@@ -0,0 +1,33 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Group;
+
+trait GroupFactoryTrait
+{
+
+ /**
+ * 数组组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Group
+ */
+ public static function group($field, $title, $value = [])
+ {
+ return new Group($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/HiddenFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/HiddenFactoryTrait.php
new file mode 100644
index 0000000..33fa3c8
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/HiddenFactoryTrait.php
@@ -0,0 +1,31 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Hidden;
+
+trait HiddenFactoryTrait
+{
+ /**
+ * 隐藏组件
+ *
+ * @param string $field
+ * @param mixed $value
+ * @return Hidden
+ */
+ public static function hidden($field, $value)
+ {
+ return new Hidden($field, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputFactoryTrait.php
new file mode 100644
index 0000000..2304c61
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputFactoryTrait.php
@@ -0,0 +1,112 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Input;
+
+trait InputFactoryTrait
+{
+ /**
+ * input输入框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param string $type
+ * @return Input
+ */
+ public static function input($field, $title, $value = '', $type = Input::TYPE_TEXT)
+ {
+ $input = new Input($field, $title, (string)$value);
+ return $input->type($type);
+ }
+
+ /**
+ * text 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function text($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value);
+ }
+
+ /**
+ * password 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function password($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_PASSWORD);
+ }
+
+ /**
+ * textarea 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function textarea($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_TEXTAREA);
+ }
+
+ /**
+ * url 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function url($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_URL);
+ }
+
+ /**
+ * email 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function email($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_EMAIL);
+ }
+
+ /**
+ * date 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function idate($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_DATE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputNumberFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputNumberFactoryTrait.php
new file mode 100644
index 0000000..0918984
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/InputNumberFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\InputNumber;
+
+trait InputNumberFactoryTrait
+{
+ /**
+ * 数字输入框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param null|number $value
+ * @return InputNumber
+ */
+ public static function number($field, $title, $value = null)
+ {
+ return new InputNumber($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/RadioFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/RadioFactoryTrait.php
new file mode 100644
index 0000000..6ce1591
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/RadioFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Radio;
+
+trait RadioFactoryTrait
+{
+ /**
+ * 单选框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Radio
+ */
+ public static function radio($field, $title, $value = '')
+ {
+ return new Radio($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/RateFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/RateFactoryTrait.php
new file mode 100644
index 0000000..c2e08b5
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/RateFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Rate;
+
+trait RateFactoryTrait
+{
+ /**
+ * 评分选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param int|float $value
+ * @return Rate
+ */
+ public static function rate($field, $title, $value = 0)
+ {
+ return new Rate($field, $title, (float)$value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/SelectFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SelectFactoryTrait.php
new file mode 100644
index 0000000..27e8628
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SelectFactoryTrait.php
@@ -0,0 +1,48 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Select;
+
+trait SelectFactoryTrait
+{
+ /**
+ * 下拉选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Select
+ */
+ public static function select($field, $title, $value = '')
+ {
+ $multiple = is_array($value) ? true : false;
+ $select = new Select($field, $title, $value);
+ $select->multiple($multiple);
+ return $select;
+ }
+
+ /**
+ * 多选
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Select
+ */
+ public static function selectMultiple($field, $title, array $value = [])
+ {
+ return self::select($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/SliderFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SliderFactoryTrait.php
new file mode 100644
index 0000000..6f66599
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SliderFactoryTrait.php
@@ -0,0 +1,50 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Slider;
+
+trait SliderFactoryTrait
+{
+ /**
+ * 滑块组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param int|array $value
+ * @return Slider
+ */
+ public static function slider($field, $title, $value = 0)
+ {
+ $slider = new Slider($field, $title, $value);
+ if (is_array($value)) $slider->range(true);
+ return $slider;
+ }
+
+ /**
+ * 区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param int $start
+ * @param int $end
+ * @return Slider
+ */
+ public static function sliderRange($field, $title, $start = 0, $end = 0)
+ {
+ $slider = self::slider($field, $title, [(int)$start, (int)$end]);
+ $slider->range(true);
+ return $slider;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/SwitchesFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SwitchesFactoryTrait.php
new file mode 100644
index 0000000..7343083
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/SwitchesFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Switches;
+
+trait SwitchesFactoryTrait
+{
+ /**
+ * 开关组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Switches
+ */
+ public static function switches($field, $title, $value = '0')
+ {
+ return new Switches($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/TimePickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/TimePickerFactoryTrait.php
new file mode 100644
index 0000000..921871f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/TimePickerFactoryTrait.php
@@ -0,0 +1,61 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\TimePicker;
+
+trait TimePickerFactoryTrait
+{
+ /**
+ * 时间选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param bool $isRange
+ * @return TimePicker
+ */
+ public static function timePicker($field, $title, $value = '', $isRange = false)
+ {
+ $timePicker = new TimePicker($field, $title, $value);
+ return $timePicker->isRange(!!$isRange);
+ }
+
+ /**
+ * 时间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return TimePicker
+ */
+ public static function time($field, $title, $value = '')
+ {
+ return self::timePicker($field, $title, (string)$value);
+ }
+
+ /**
+ * 时间区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $startTime
+ * @param string $endTime
+ * @return TimePicker
+ */
+ public static function timeRange($field, $title, $startTime = '', $endTime = '')
+ {
+ return self::timePicker($field, $title, [(string)$startTime, (string)$endTime], true);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/TreeFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/TreeFactoryTrait.php
new file mode 100644
index 0000000..b979d00
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/TreeFactoryTrait.php
@@ -0,0 +1,74 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+
+use FormBuilder\UI\Elm\Components\Tree;
+use FormBuilder\UI\Elm\Components\TreeData;
+
+trait TreeFactoryTrait
+{
+ /**
+ * 树形组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @param string $type
+ * @return Tree
+ */
+ public static function tree($field, $title, $value = [], $type = Tree::TYPE_CHECKED)
+ {
+ $tree = new Tree($field, $title, $value);
+ return $tree->type($type);
+ }
+
+ /**
+ * 获取选中的值
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Tree
+ */
+ public static function treeSelected($field, $title, $value = [])
+ {
+ return self::tree($field, $title, $value, Tree::TYPE_SELECTED);
+ }
+
+ /**
+ * 获取勾选的值
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Tree
+ */
+ public static function treeChecked($field, $title, $value = [])
+ {
+ return self::tree($field, $title, $value)->showCheckbox(true);
+ }
+
+ /**
+ * 树形组件数据 date 类
+ *
+ * @param mixed $id
+ * @param string $title
+ * @param array $children
+ * @return TreeData
+ */
+ public static function treeData($id, $title, array $children = [])
+ {
+ return new TreeData($id, $title, $children);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/UploadFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/UploadFactoryTrait.php
new file mode 100644
index 0000000..b032c8f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/UploadFactoryTrait.php
@@ -0,0 +1,93 @@
+uploadType($type)->action($action);
+ }
+
+ /**
+ * 图片上传
+ * value 为 Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param array $value
+ * @return Upload
+ */
+ public static function uploadImages($field, $title, $action, array $value = [])
+ {
+ $upload = self::upload($field, $title, $action, $value, Upload::TYPE_IMAGE);
+ return $upload->accept('image/*');
+ }
+
+ /**
+ * 文件上传
+ * value 为 Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param array $value
+ * @return Upload
+ */
+ public static function uploadFiles($field, $title, $action, array $value = [])
+ {
+ return self::upload($field, $title, $action, $value, Upload::TYPE_FILE);
+ }
+
+ /**
+ * 单图片上传
+ * value 为 string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param string $value
+ * @return Upload
+ */
+ public static function uploadImage($field, $title, $action, $value = '')
+ {
+ $upload = self::upload($field, $title, $action, (string)$value, Upload::TYPE_IMAGE);
+ return $upload->accept('image/*')->limit(1);
+ }
+
+ /**
+ * 单文件上传
+ * value 为 string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param string $value
+ * @return Upload
+ */
+ public static function uploadFile($field, $title, $action, $value = '')
+ {
+ $upload = self::upload($field, $title, $action, (string)$value, Upload::TYPE_FILE);
+ return $upload->limit(1);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Traits/ValidateFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Elm/Traits/ValidateFactoryTrait.php
new file mode 100644
index 0000000..35eb73a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Traits/ValidateFactoryTrait.php
@@ -0,0 +1,73 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm\Traits;
+
+use FormBuilder\UI\Elm\Validate;
+
+trait ValidateFactoryTrait
+{
+ public static function validateStr($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_STRING, $trigger);
+ }
+
+ public static function validateArr($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_ARRAY, $trigger);
+ }
+
+ public static function validateNum($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_NUMBER, $trigger);
+ }
+
+ public static function validateDate($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_DATE, $trigger);
+ }
+
+ public static function validateInt($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_INTEGER, $trigger);
+ }
+
+ public static function validateFloat($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_FLOAT, $trigger);
+ }
+
+ public static function validateObject($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_OBJECT, $trigger);
+ }
+
+ public static function validateEmail($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_EMAIL, $trigger);
+ }
+
+ public static function validateEnum($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_ENUM, $trigger);
+ }
+
+ public static function validateUrl($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_URL, $trigger);
+ }
+
+ public static function validateHex($trigger = Validate::TRIGGER_CHANGE)
+ {
+ return new Validate(Validate::TYPE_HEX, $trigger);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Elm/Validate.php b/vendor/xaboy/form-builder/src/UI/Elm/Validate.php
new file mode 100644
index 0000000..0f369cb
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Elm/Validate.php
@@ -0,0 +1,20 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Elm;
+
+
+use FormBuilder\UI\Iview\Validate as IViewValidate;
+
+class Validate extends IViewValidate
+{
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Bootstrap.php b/vendor/xaboy/form-builder/src/UI/Iview/Bootstrap.php
new file mode 100644
index 0000000..6c16926
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Bootstrap.php
@@ -0,0 +1,53 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview;
+
+use FormBuilder\Contract\BootstrapInterface;
+use FormBuilder\Form;
+
+class Bootstrap implements BootstrapInterface
+{
+
+ protected $version;
+
+ /**
+ * Bootstrap constructor.
+ * @param int $version
+ */
+ public function __construct($version = 3)
+ {
+ $this->version = $version;
+ }
+
+ public function init(Form $form)
+ {
+ $dependScript = $form->getDependScript();
+
+ if ($this->version != 4) {
+ array_splice($dependScript, 2, 0, [
+ '',
+ '',
+ '',
+ ]);
+ } else {
+ array_splice($dependScript, 2, 0, [
+ '',
+ '',
+ '',
+ ]);
+ }
+
+
+ $form->setDependScript($dependScript);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Button.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Button.php
new file mode 100644
index 0000000..c7e924c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Button.php
@@ -0,0 +1,58 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Button
+ * @method $this type(string $type) 按钮类型,可选值为primary、ghost、dashed、text、info、success、warning、error或者不设置
+ * @method $this size(string $size) 按钮大小,可选值为large、small、default或者不设置
+ * @method $this long(bool $long) 开启后,按钮的长度为 100%
+ * @method $this htmlType(string $type) 设置button原生的type,可选值为button、submit、reset
+ * @method $this disabled(bool $disabled) 设置按钮为禁用状态
+ * @method $this icon(string $icon) 设置按钮的图标类型
+ * @method $this innerText(string $innerText) 按钮文字提示
+ * @method $this loading(bool $loading) 设置按钮为加载中状态
+ * @method $this show(bool $show) 是否显示, 默认显示
+ */
+class Button extends CustomComponent
+{
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'size' => 'string',
+ 'long' => 'bool',
+ 'htmlType' => 'string',
+ 'disabled' => 'bool',
+ 'icon' => 'string',
+ 'innerText' => 'string',
+ 'loading' => 'bool',
+ 'show' => 'bool'
+ ];
+
+ /**
+ * 按钮形状,可选值为circle或者不设置
+ *
+ * @param bool $isCircle
+ * @return $this
+ */
+ public function shape($isCircle = true)
+ {
+ if ($isCircle)
+ $this->props['shape'] = 'circle';
+ else
+ unset($this->props['shape']);
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Cascader.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Cascader.php
new file mode 100644
index 0000000..eb791bc
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Cascader.php
@@ -0,0 +1,122 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 多级联动组件
+ * Class Cascader
+ *
+ * @method $this type(string $type) 数据类型, 支持 city_area(省市区三级联动), city (省市二级联动), other (自定义)
+ * @method $this disabled(bool $bool) 是否禁用选择器
+ * @method $this clearable(bool $bool) 是否支持清除
+ * @method $this placeholder(string $placeholder)
+ * @method $this trigger(string $trigger) 次级菜单展开方式,可选值为 click 或 hover
+ * @method $this changeOnSelect(bool $bool) 当此项为 true 时,点选每级菜单选项值都会发生变化, 默认为 false
+ * @method $this size(string $size) 输入框大小,可选值为large和small或者不填
+ * @method $this filterable(bool $bool) 是否支持搜索
+ * @method $this notFoundText(string $text) 当搜索列表为空时显示的内容
+ * @method $this transfer(bool $bool) /是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果
+ */
+class Cascader extends FormComponent
+{
+ /**
+ * 省市区三级联动数据
+ */
+ const TYPE_CITY_AREA = 'city_area';
+
+ /**
+ * 省市二级联动数据
+ */
+ const TYPE_CITY = 'city';
+
+ /**
+ * 自定义数据
+ */
+ const TYPE_OTHER = 'other';
+
+
+ protected $defaultValue = [];
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_OTHER,
+ 'data' => []
+ ];
+
+ /**
+ * @var array
+ */
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'disabled' => 'bool',
+ 'clearable' => 'bool',
+ 'changeOnSelect' => 'bool',
+ 'filterable' => 'bool',
+ 'transfer' => 'bool',
+ 'placeholder' => 'string',
+ 'trigger' => 'string',
+ 'size' => 'string',
+ 'notFoundText' => 'string',
+ ];
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function value($value)
+ {
+ $this->value = (array)$value;
+ return $this;
+ }
+
+ /**
+ * 可选项的数据源
+ * 例如:{
+ * "value":"北京市", "label":"北京市", "children":[{
+ * "value":"东城区", "label":"东城区"
+ * }]
+ * }
+ *
+ * @param array|callable $data
+ * @return $this
+ */
+ public function data($data)
+ {
+ $is_callable = is_callable($data);
+ if (!is_array($data) && !$is_callable) return $this;
+
+ $this->props['data'] = $is_callable ? $data($this) : $data;
+ return $this;
+ }
+
+ /**
+ * @param $var
+ * @return $this
+ */
+ public function jsData($var)
+ {
+ $this->props['data'] = 'js.' . (string)$var;
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Iview::validateArr();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Checkbox.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Checkbox.php
new file mode 100644
index 0000000..e92b667
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Checkbox.php
@@ -0,0 +1,49 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 复选框组件
+ * Class Checkbox
+ *
+ * @method $this size(string $size) 多选框组的尺寸,可选值为 large、small、default 或者不设置
+ */
+class Checkbox extends FormOptionsComponent
+{
+ protected $defaultValue = [];
+
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'size' => 'string'
+ ];
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function value($value)
+ {
+ $this->value = (array)$value;
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Iview::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/ColorPicker.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/ColorPicker.php
new file mode 100644
index 0000000..a625710
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/ColorPicker.php
@@ -0,0 +1,49 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 颜色选择器组件
+ * Class ColorPicker
+ *
+ * @method $this disabled(bool $bool) 是否禁用
+ * @method $this alpha(bool $bool) 是否支持透明度选择, 默认为false
+ * @method $this colors(array $colors) colors
+ * @method $this hue(bool $bool) 是否支持色彩选择, 默认为true
+ * @method $this recommend(bool $bool) 是否显示推荐的颜色预设, 默认为false
+ * @method $this size(string $size) 尺寸,可选值为large、small、default或者不设置
+ * @method $this format(string $format) 颜色的格式,可选值为 hsl、hsv、hex、rgb.开启 alpha 时为 rgb,其它为 hex
+ */
+class ColorPicker extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'disabled' => 'bool',
+ 'alpha' => 'bool',
+ 'hue' => 'bool',
+ 'recommend' => 'bool',
+ 'size' => 'string',
+ 'colors' => 'array',
+ 'format' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ return Iview::validateStr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/DatePicker.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/DatePicker.php
new file mode 100644
index 0000000..b81d5d6
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/DatePicker.php
@@ -0,0 +1,125 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 日期选择器组件
+ * Class DatePicker
+ *
+ * @method $this type(string $type) 显示类型,可选值为 date、daterange、datetime、datetimerange、year、month
+ * @method $this format(string $format) 展示的日期格式, 默认为yyyy-MM-dd HH:mm:ss
+ * @method $this placement(string $placement) 日期选择器出现的位置,可选值为top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end, 默认为bottom-start
+ * @method $this placeholder(string $placeholder) 占位文本
+ * @method $this confirm(bool $bool) 是否显示底部控制栏,开启后,选择完日期,选择器不会主动关闭,需用户确认后才可关闭, 默认为false
+ * @method $this size(string $size) 尺寸,可选值为large、small、default或者不设置
+ * @method $this disabled(bool $bool) 是否禁用选择器
+ * @method $this clearable(bool $bool) 是否显示清除按钮
+ * @method $this readonly(bool $bool) 完全只读,开启后不会弹出选择器,只在没有设置 open 属性下生效
+ * @method $this editable(bool $bool) 文本框是否可以输入, 默认为false
+ * @method $this multiple(bool $bool) 开启后, 可以选择多个日期, 仅在 date 下可用, 默认为false
+ * @method $this transfer(bool $bool) 是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果, 默认为false
+ * @method $this splitPanels(bool $bool) 开启后,左右面板不联动,仅在 daterange 和 datetimerange 下可用。
+ * @method $this showWeekNumbers(bool $bool) 开启后,可以显示星期数。
+ *
+ */
+class DatePicker extends FormComponent
+{
+ /**
+ * 日期选择
+ */
+ const TYPE_DATE = 'date';
+ /**
+ * 日期区间选择
+ */
+ const TYPE_DATE_RANGE = 'daterange';
+ /**
+ * 日期+时间选择
+ */
+ const TYPE_DATE_TIME = 'datetime';
+ /**
+ * 日期+时间区间选择
+ */
+ const TYPE_DATE_TIME_RANGE = 'datetimerange';
+ /**
+ * 年份选择
+ */
+ const TYPE_YEAR = 'year';
+ /**
+ * 月份选择
+ */
+ const TYPE_MONTH = 'month';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_DATE,
+ 'editable' => false
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'format' => 'string',
+ 'placement' => 'string',
+ 'placeholder' => 'string',
+ 'size' => 'string',
+ 'confirm' => 'bool',
+ 'disabled' => 'bool',
+ 'clearable' => 'bool',
+ 'readonly' => 'bool',
+ 'editable' => 'bool',
+ 'multiple' => 'bool',
+ 'transfer' => 'bool',
+ 'splitPanels' => 'bool',
+ 'showWeekNumbers' => 'bool'
+ ];
+
+ protected function isRange()
+ {
+ return in_array(strtolower($this->props['type']), ['datetimerange', 'daterange']);
+ }
+
+ protected function isMultiple()
+ {
+ return isset($this->props['multiple']) && $this->props['multiple'];
+ }
+
+ public function createValidate()
+ {
+ if ($this->isRange() || $this->isMultiple())
+ return Iview::validateArr();
+ else
+ return Iview::validateDate();
+ }
+
+ public function required($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ $validate = $this->createValidate();
+
+ if ($this->isRange()) {
+ $dateRequired = Iview::validateDate()->message($message)->required();
+ $validate->fields([
+ '0' => $dateRequired,
+ '1' => $dateRequired
+ ]);
+ }
+
+ $this->appendValidate($validate->message($message)->required());
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Frame.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Frame.php
new file mode 100644
index 0000000..e5824d4
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Frame.php
@@ -0,0 +1,83 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 框架组件
+ * Class Frame
+ *
+ * @method $this type(string $type) frame类型, 有input, file, image, 默认为input
+ * @method $this src(string $src) iframe地址
+ * @method $this maxLength(int $length) value的最大数量, 默认无限制
+ * @method $this icon(string $icon) 打开弹出框的按钮图标
+ * @method $this height(string $height) 弹出框高度
+ * @method $this width(string $width) 弹出框宽度
+ * @method $this spin(bool $bool) 是否显示加载动画, 默认为 true
+ * @method $this frameTitle(string $title) 弹出框标题
+ * @method $this modal(array $modalProps) 弹出框props
+ * @method $this handleIcon(bool $bool) 操作按钮的图标, 设置为false将不显示, 设置为true为默认的预览图标, 类型为file时默认为false, image类型默认为true
+ * @method $this allowRemove(bool $bool) 是否可删除, 设置为false是不显示删除按钮
+ * @method $this disabled(bool $bool) 是否禁用
+ *
+ */
+class Frame extends FormComponent
+{
+ /**
+ * 图片类型
+ */
+ const TYPE_IMAGE = 'image';
+ /**
+ * 文件类型
+ */
+ const TYPE_FILE = 'file';
+ /**
+ * input 类型
+ */
+ const TYPE_INPUT = 'input';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_INPUT,
+ 'maxLength' => 0
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'src' => 'string',
+ 'maxLength' => 'int',
+ 'icon' => 'string',
+ 'height' => 'string',
+ 'width' => 'string',
+ 'spin' => 'bool',
+ 'modal' => 'array',
+ 'frameTitle' => ['string', 'title'],
+ 'handleIcon' => 'bool',
+ 'allowRemove' => 'bool',
+ ];
+
+ public function createValidate()
+ {
+ return Iview::validateArr();
+ }
+
+ protected function init()
+ {
+ $this->frameTitle($this->getPlaceHolder());
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Group.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Group.php
new file mode 100644
index 0000000..397f802
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Group.php
@@ -0,0 +1,77 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Contract\ValidateInterface;
+use FormBuilder\Driver\CustomComponent;
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+use FormBuilder\Util;
+
+/**
+ * 数组组件
+ *
+ * Class Group
+ * @method $this min(int $min) 最少几项
+ * @method $this max(int $max) 最多几项
+ * @method $this disabled(bool $bool) 是否禁用
+ */
+class Group extends FormComponent
+{
+ protected $defaultValue = [];
+
+ protected static $propsRule = [
+ 'min' => 'string',
+ 'max' => 'string',
+ 'disabled' => 'int',
+ ];
+
+ /**
+ * @param array|CustomComponent $rule
+ * @return $this
+ */
+ public function rule($rule)
+ {
+ $this->props['rule'] = $this->tidyRule([$rule])[0];
+ return $this;
+ }
+
+ /**
+ * @param array $rules
+ * @return array
+ */
+ protected function tidyRule(array $rules)
+ {
+ foreach ($rules as $k => $rule) {
+ if (Util::isComponent($rule)) {
+ $rules[$k] = $rule->build();
+ }
+ }
+ return $rules;
+ }
+
+ public function rules(array $rules)
+ {
+ $this->props['rules'] = $this->tidyRule($rules);
+ return $this;
+ }
+
+ /**
+ * @return ValidateInterface
+ */
+ public function createValidate()
+ {
+ return Iview::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Hidden.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Hidden.php
new file mode 100644
index 0000000..b2edaaf
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Hidden.php
@@ -0,0 +1,57 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Exception\FormBuilderException;
+
+/**
+ * hidden组件
+ * Class Hidden
+ *
+ */
+class Hidden extends FormComponent
+{
+ /**
+ * Hidden constructor.
+ *
+ * @param string $field
+ * @param string $value
+ */
+ public function __construct($field, $value)
+ {
+ parent::__construct($field, '', $value);
+ }
+
+ /**
+ * @return array
+ */
+ public function getRule()
+ {
+ return [
+ 'type' => $this->type,
+ 'field' => $this->field,
+ 'value' => $this->value
+ ];
+ }
+
+ /**
+ * @return void
+ * @throws FormBuilderException
+ */
+ public function createValidate()
+ {
+ throw new FormBuilderException('hidden 组件不支持 createValidate 方法');
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Input.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Input.php
new file mode 100644
index 0000000..3bd6bd1
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Input.php
@@ -0,0 +1,91 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * Input组件,支持类型text、password、textarea、url、email、date
+ * Class Input
+ *
+ * @method $this type(string $type) 输入框类型,可选值为 text、password、textarea、url、email、date;
+ * @method $this size(string $size) 输入框尺寸,可选值为large、small、default或者不设置;
+ * @method $this placeholder(string $placeholder) 占位文本
+ * @method $this clearable(bool $bool) 是否显示清空按钮, 默认为false
+ * @method $this disabled(bool $bool) 设置输入框为禁用状态, 默认为false
+ * @method $this readonly(bool $bool) 设置输入框为只读, 默认为false
+ * @method $this maxlength(int $length) 最大输入长度
+ * @method $this icon(string $icon) 输入框尾部图标,仅在 text 类型下有效
+ * @method $this rows(int $rows) 文本域默认行数,仅在 textarea 类型下有效, 默认为2
+ * @method $this number(bool $bool) 将用户的输入转换为 Number 类型, 默认为false
+ * @method $this autofocus(bool $bool) 自动获取焦点, 默认为false
+ * @method $this autocomplete(bool $bool) 原生的自动完成功能, 默认为false
+ * @method $this spellcheck(bool $bool) 原生的 spellcheck 属性, 默认为false
+ * @method $this wrap(string $warp) 原生的 wrap 属性,可选值为 hard 和 soft, 默认为soft
+ */
+class Input extends FormComponent
+{
+ const TYPE_TEXT = 'text';
+
+ const TYPE_PASSWORD = 'password';
+
+ const TYPE_TEXTAREA = 'textarea';
+
+ const TYPE_URL = 'url';
+
+ const TYPE_EMAIL = 'email';
+
+ const TYPE_DATE = 'date';
+
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_TEXT
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'clearable' => 'bool',
+ 'disabled' => 'bool',
+ 'readonly' => 'bool',
+ 'maxlength' => 'int',
+ 'icon' => 'string',
+ 'rows' => 'int',
+ 'number' => 'bool',
+ 'autofocus' => 'bool',
+ 'autocomplete' => 'bool',
+ 'spellcheck' => 'bool',
+ 'wrap' => 'string',
+ ];
+
+ /**
+ * 自适应内容高度,仅在 textarea 类型下有效
+ *
+ * @param Bool|Number $minRows
+ * @param null|Number $maxRows
+ * @return $this
+ */
+ public function autoSize($minRows = false, $maxRows = null)
+ {
+ $this->props['autosize'] = $maxRows === null ? boolval($minRows) : compact('minRows', 'maxRows');
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ return Iview::validateStr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/InputNumber.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/InputNumber.php
new file mode 100644
index 0000000..5ad52d3
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/InputNumber.php
@@ -0,0 +1,51 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 数字输入框组件
+ * Class InputNumber
+ *
+ * @method $this max(float $max) 最大值
+ * @method $this min(float $min) 最小值
+ * @method $this step(float $step) 每次改变的步伐,可以是小数
+ * @method $this size(string $size) 输入框尺寸,可选值为large、small、default或者不填
+ * @method $this disabled(bool $bool) 设置禁用状态,默认为false
+ * @method $this placeholder(string $placeholder) 占位文本
+ * @method $this readonly(bool $bool) 是否设置为只读,默认为false
+ * @method $this editable(bool $bool) 是否可编辑,默认为true
+ * @method $this precision(int $precision) 数值精度
+ */
+class InputNumber extends FormComponent
+{
+ protected static $propsRule = [
+ 'max' => 'float',
+ 'min' => 'float',
+ 'step' => 'float',
+ 'disabled' => 'bool',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'readonly' => 'bool',
+ 'editable' => 'bool',
+ 'precision' => 'int',
+ ];
+
+ public function createValidate()
+ {
+ return Iview::validateNum();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Option.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Option.php
new file mode 100644
index 0000000..73e1df5
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Option.php
@@ -0,0 +1,56 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Contract\OptionComponentInterface;
+
+class Option implements OptionComponentInterface
+{
+ /**
+ * @var array
+ */
+ protected $rule;
+
+ /**
+ * Option constructor.
+ *
+ * @param string|number $value
+ * @param string $label
+ * @param bool $disabled
+ */
+ public function __construct($value, $label = '', $disabled = null)
+ {
+ $this->rule = compact('label', 'value');
+ if (!is_null($disabled))
+ $this->disabled($disabled);
+ }
+
+ /**
+ * @param bool $disabled
+ * @return $this
+ */
+ public function disabled($disabled = true)
+ {
+ $this->rule['disabled'] = !!$disabled;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOption()
+ {
+ return $this->rule;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Poptip.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Poptip.php
new file mode 100644
index 0000000..1d93ac0
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Poptip.php
@@ -0,0 +1,57 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Poptip
+ * @method $this trigger(string $trigger) 触发方式,可选值为hover(悬停)click(点击)focus(聚焦), 在 confirm 模式下,只有 click 有效
+ * @method $this popperTitle(string $title) 显示的标题
+ * @method $this placement(string $placement) 提示框出现的位置,可选值为top, top-start, top-end, bottom, bottom-start, bottom-endleft, left-start, left-end, right, right-start, right-end
+ * @method $this width(string $width) 宽度,最小宽度为 150px,在 confirm 模式下,默认最大宽度为 300px
+ * @method $this confirm(bool $confirm) 是否开启对话框模式
+ * @method $this disabled(bool $disabled) 是否禁用
+ * @method $this okText(string $okText) 确定按钮的文字,只在 confirm 模式下有效
+ * @method $this cancelText(string $cancelText) 取消按钮的文字,只在 confirm 模式下有效
+ * @method $this transfer(bool $transfer) 是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果
+ * @method $this popperClass(string $popperClass) 给 Poptip 设置 class-name,在使用 transfer 时会很有用
+ * @method $this wordWrap(bool $wordWrap) 开启后,超出指定宽度文本将自动换行,并两端对齐
+ * @method $this padding(string $padding) 自定义间距值
+ * @method $this offset(float $offset) 出现位置的偏移量
+ * @method $this options(array $options) 自定义 popper.js 的配置项
+ */
+class Poptip extends CustomComponent
+{
+ protected $defaultProps = [
+ 'transfer' => true
+ ];
+
+ protected static $propsRule = [
+ 'trigger' => 'string',
+ 'placement' => 'string',
+ 'width' => 'string',
+ 'confirm' => 'bool',
+ 'disabled ' => 'bool',
+ 'okText ' => 'string',
+ 'cancelText' => 'string',
+ 'transfer' => 'bool',
+ 'popperClass' => 'string',
+ 'popperTitle' => ['string', 'title'],
+ 'wordWrap' => 'bool',
+ 'padding' => 'string',
+ 'offset' => 'float',
+ 'options' => 'array',
+ ];
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Radio.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Radio.php
new file mode 100644
index 0000000..1c33bf9
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Radio.php
@@ -0,0 +1,65 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 单选框组件
+ * Class Radio
+ *
+ * @method $this size(string $size) 单选框的尺寸,可选值为 large、small、default 或者不设置
+ * @method $this vertical(bool $bool) 是否垂直排列,按钮样式下无效
+ */
+class Radio extends FormOptionsComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'size' => 'string',
+ 'vertical' => 'bool'
+ ];
+
+ public function createValidate()
+ {
+ return Iview::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Iview::validateNum();
+ }
+
+ public function requiredNum($message = '')
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ return $this->appendValidate($this->createValidateNum()->message($message)->required());
+ }
+
+ /**
+ * 按钮样式
+ *
+ * @param bool $button
+ * @return $this
+ */
+ public function button($button = true)
+ {
+ if ($button)
+ $this->props['type'] = 'button';
+ else
+ unset($this->props['type']);
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Rate.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Rate.php
new file mode 100644
index 0000000..2059ca9
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Rate.php
@@ -0,0 +1,47 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 评分组件
+ * Class Rate
+ *
+ * @method $this count(int $star) star 总数, 默认为 5
+ * @method $this allowHalf(bool $bool) 是否允许半选, 默认为 false
+ * @method $this disabled(bool $bool) 是否只读,无法进行交互, 默认为
+ * @method $this showText(bool $bool) 是否显示提示文字, 默认为 false
+ * @method $this clearable(bool $bool) 是否可以取消选择, 默认为 false
+ *
+ */
+class Rate extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected static $propsRule = [
+ 'count' => 'float',
+ 'allowHalf' => 'bool',
+ 'disabled' => 'bool',
+ 'showText' => 'bool',
+ 'clearable' => 'bool',
+ ];
+
+ public function createValidate()
+ {
+ return Iview::validateNum();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Select.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Select.php
new file mode 100644
index 0000000..9a71ff3
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Select.php
@@ -0,0 +1,73 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormOptionsComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 选择器组件
+ * Class Select
+ *
+ * @method $this multiple(bool $bool) 是否支持多选, 默认为false
+ * @method $this disabled(bool $bool) 是否禁用, 默认为false
+ * @method $this clearable(bool $bool) 是否可以清空选项,只在单选时有效, 默认为false
+ * @method $this filterable(bool $bool) 是否支持搜索, 默认为false
+ * @method $this size(string $size) 选择框大小,可选值为large、small、default或者不填
+ * @method $this placeholder(string $placeholder) 选择框默认文字
+ * @method $this transfer(string $transfer) 是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果, 默认为false
+ * @method $this placement(string $placement) 弹窗的展开方向,可选值为 bottom 和 top, 默认为bottom
+ * @method $this notFoundText(string $text) 当下拉列表为空时显示的内容, 默认为 无匹配数据
+ *
+ */
+class Select extends FormOptionsComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'multiple' => false
+ ];
+
+ protected static $propsRule = [
+ 'multiple' => 'bool',
+ 'disabled' => 'bool',
+ 'clearable' => 'bool',
+ 'filterable' => 'bool',
+ 'size' => 'string',
+ 'placeholder' => 'string',
+ 'transfer' => 'string',
+ 'placement' => 'string',
+ 'notFoundText' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ if ($this->props['multiple'] == true)
+ return Iview::validateArr();
+ else
+ return Iview::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Iview::validateNum();
+ }
+
+ public function requiredNum($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ return $this->appendValidate($this->createValidateNum()->message($message)->required());
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Slider.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Slider.php
new file mode 100644
index 0000000..545a19f
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Slider.php
@@ -0,0 +1,61 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 滑块组件
+ * Class Slider
+ *
+ * @method $this min(float $min) 最小值, 默认 0
+ * @method $this max(float $max) 最大值, 默认 100
+ * @method $this step(float $step) 步长,取值建议能被(max - min)整除, 默认 1
+ * @method $this disabled(bool $bool) 是否禁用滑块, 默认 false
+ * @method $this range(bool $bool) 是否开启双滑块模式, 默认
+ * @method $this showInput(bool $bool) 是否显示数字输入框,仅在单滑块模式下有效, 默认 false
+ * @method $this showStops(bool $bool) 是否显示间断点,建议在 step 不密集时使用, 默认 false
+ * @method $this showTip(string $tip) 提示的显示控制,可选值为 hover(悬停,默认)、always(总是可见)、never(不可见)
+ * @method $this inputSize(string $size) 数字输入框的尺寸,可选值为large、small、default或者不填,仅在开启 show-input 时有效
+ *
+ */
+class Slider extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'range' => false
+ ];
+
+ protected static $propsRule = [
+ 'min' => 'float',
+ 'max' => 'float',
+ 'step' => 'float',
+ 'disabled' => 'bool',
+ 'range' => 'bool',
+ 'showInput' => 'bool',
+ 'showStops' => 'bool',
+ 'showTip' => 'string',
+ 'inputSize' => 'string',
+ ];
+
+ public function createValidate()
+ {
+ if ($this->props['range'] == true)
+ return Iview::validateArr();
+ else
+ return Iview::validateNum();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Switches.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Switches.php
new file mode 100644
index 0000000..4deb1e8
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Switches.php
@@ -0,0 +1,93 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 开关组件
+ * Class Switches
+ *
+ * @method $this size(string $size) 开关的尺寸,可选值为large、small、default或者不写。建议开关如果使用了2个汉字的文字,使用 large。
+ * @method $this disabled(bool $bool) 禁用开关, 默认为false
+ * @method $this trueValue(string $value) 选中时的值,默认为1
+ * @method $this falseValue(string $value) 没有选中时的值,默认为0
+ */
+class Switches extends FormComponent
+{
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'trueValue' => '1',
+ 'falseValue' => '0',
+ 'slot' => []
+ ];
+
+ protected static $propsRule = [
+ 'size' => 'string',
+ 'disabled' => 'bool',
+ 'trueValue' => '',
+ 'falseValue' => ''
+ ];
+
+ public function getComponentName()
+ {
+ return 'switch';
+ }
+
+ /**
+ * 自定义显示打开时的内容
+ *
+ * @param string $open
+ * @return $this
+ */
+ public function openStr($open)
+ {
+ $this->props['slot']['open'] = (string)$open;
+ return $this;
+ }
+
+ /**
+ * 自定义显示关闭时的内容
+ *
+ * @param string $close
+ * @return $this
+ */
+ public function closeStr($close)
+ {
+ $this->props['slot']['close'] = (string)$close;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getRule()
+ {
+ if (isset($this->props['slot']) && !count($this->props['slot'])) unset($this->props['slot']);
+ return parent::getRule();
+ }
+
+ public function createValidate()
+ {
+ return Iview::validateStr();
+ }
+
+ public function createValidateNum()
+ {
+ return Iview::validateNum();
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/TimePicker.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/TimePicker.php
new file mode 100644
index 0000000..fa3c0da
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/TimePicker.php
@@ -0,0 +1,115 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 时间选择器组件
+ * Class TimePicker
+ *
+ * @method $this type(string $type) 显示类型,可选值为 time、timerange
+ * @method $this format(string $format) 展示的时间格式, 默认为HH:mm:ss
+ * @method $this placement(string $placement) 时间选择器出现的位置,可选值为top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end, 默认为bottom-start
+ * @method $this placeholder(string $placeholder) 占位文本
+ * @method $this confirm(bool $bool) 是否显示底部控制栏, 默认为false
+ * @method $this size(string $size) 尺寸,可选值为large、small、default或者不设置
+ * @method $this disabled(bool $bool) 是否禁用选择器
+ * @method $this clearable(bool $bool) 是否显示清除按钮
+ * @method $this readonly(bool $bool) 完全只读,开启后不会弹出选择器,只在没有设置 open 属性下生效
+ * @method $this editable(bool $bool) 文本框是否可以输入, 默认为false
+ * @method $this transfer(bool $bool) 是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果, 默认为false
+ *
+ */
+class TimePicker extends FormComponent
+{
+ /**
+ * 时间选择
+ */
+ const TYPE_TIME = 'time';
+ /**
+ * 时间区间选择
+ */
+ const TYPE_TIME_RANGE = 'timerange';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_TIME,
+ 'editable' => false,
+ 'confirm' => true
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'format' => 'string',
+ 'placement' => 'string',
+ 'placeholder' => 'string',
+ 'size' => 'string',
+ 'confirm' => 'bool',
+ 'disabled' => 'bool',
+ 'clearable' => 'bool',
+ 'readonly' => 'bool',
+ 'editable' => 'bool',
+ 'transfer' => 'bool',
+ ];
+
+ /**
+ * 下拉列表的时间间隔,数组的三项分别对应小时、分钟、秒。
+ * 例如设置为 [1, 15] 时,分钟会显示:00、15、30、45。
+ *
+ * @param $h
+ * @param int $i
+ * @param int $s
+ * @return $this
+ */
+ public function steps($h, $i = 0, $s = 0)
+ {
+ $this->props['steps'] = [$h, $i, $s];
+ return $this;
+ }
+
+ protected function isRange()
+ {
+ return strtolower($this->props['type']) === 'timerange';
+ }
+
+ public function createValidate()
+ {
+ if ($this->isRange())
+ return Iview::validateArr();
+ else
+ return Iview::validateStr();
+ }
+
+ public function required($message = null)
+ {
+ if (is_null($message)) $message = $this->getPlaceHolder();
+ $validate = $this->createValidate();
+ if ($this->isRange()) {
+ $required = ['required' => true, 'message' => $message];
+ $validate->fields([
+ '0' => $required,
+ '1' => $required
+ ]);
+ return $this;
+ }
+
+ $this->appendValidate($validate->message($message)->required());
+ return $this;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Tooltip.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Tooltip.php
new file mode 100644
index 0000000..362b775
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Tooltip.php
@@ -0,0 +1,47 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\CustomComponent;
+
+/**
+ * Class Tooltip
+ * @method $this placement(string $placement) 提示框出现的位置,可选值为top, top-start, top-end, bottom, bottom-start, bottom-endleft, left-start, left-end, right, right-start, right-end
+ * @method $this disabled(bool $disabled) 是否禁用
+ * @method $this delay(float $delay) 延迟显示,单位毫秒
+ * @method $this always(bool $always) 是否总是可见
+ * @method $this theme(string $theme) 主题,可选值为 dark 或 light
+ * @method $this maxWidth(string $maxWidth) 最大宽度,超出最大值后,文本将自动换行,并两端对齐
+ * @method $this transfer(bool $transfer) 是否将弹层放置于 body 内,在 Tabs、带有 fixed 的 Table 列内使用时,建议添加此属性,它将不受父级样式影响,从而达到更好的效果
+ * @method $this offset(float $offset) 出现位置的偏移量
+ * @method $this options(array $options) 自定义 popper.js 的配置项
+ */
+class Tooltip extends CustomComponent
+{
+ protected $defaultProps = [
+ 'transfer' => true
+ ];
+
+ protected static $propsRule = [
+ 'placement' => 'string',
+ 'disabled ' => 'bool',
+ 'delay ' => 'float',
+ 'theme' => 'string',
+ 'maxWidth' => 'string',
+ 'wordWrap' => 'bool',
+ 'transfer' => 'bool',
+ 'offset' => 'float',
+ 'options' => 'array',
+ ];
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Tree.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Tree.php
new file mode 100644
index 0000000..6a94f13
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Tree.php
@@ -0,0 +1,87 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 树型组件
+ * Class Tree
+ *
+ * @method $this type(string $type) 类型,可选值为 checked、selected
+ * @method $this multiple(bool $bool) 是否支持多选, 当`type=selected`并且`multiple=false`, 默认为false, 值为string或Number类型,其他情况为Array类型
+ * @method $this showCheckbox(bool $bool) 是否显示多选框, 默认为false
+ * @method $this emptyText(string $emptyText) 没有数据时的提示, 默认为'暂无数据'
+ */
+class Tree extends FormComponent
+{
+ /**
+ * 选中
+ */
+ const TYPE_SELECTED = 'selected';
+ /**
+ * 选择
+ */
+ const TYPE_CHECKED = 'checked';
+
+
+ protected $selectComponent = true;
+
+ protected $defaultProps = [
+ 'type' => self::TYPE_CHECKED,
+ 'data' => [],
+ 'multiple' => true
+ ];
+
+ protected static $propsRule = [
+ 'type' => 'string',
+ 'multiple' => 'bool',
+ 'showCheckbox' => 'bool',
+ 'emptyText' => 'string',
+ ];
+
+ /**
+ * @param array $treeData
+ * @return $this
+ */
+ public function data(array $treeData)
+ {
+ $this->props['data'] = [];
+ foreach ($treeData as $child) {
+ $this->props['data'][] = $child instanceof TreeData
+ ? $child->getOption()
+ : $child;
+ }
+ return $this;
+ }
+
+ /**
+ * @param string $var
+ * @return $this
+ */
+ public function jsData($var)
+ {
+ $this->props['data'] = 'js.' . (string)$var;
+ return $this;
+ }
+
+ public function createValidate()
+ {
+ if ($this->props['multiple'])
+ return Iview::validateArr();
+ else
+ return Iview::validateStr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/TreeData.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/TreeData.php
new file mode 100644
index 0000000..355e46e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/TreeData.php
@@ -0,0 +1,106 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Contract\OptionComponentInterface;
+use FormBuilder\Rule\CallPropsRule;
+
+/**
+ * Class TreeData
+ *
+ * @method $this id(string $id) Id, 必须唯一
+ * @method $this title(string $title) 标题
+ * @method $this expand(bool $bool) 是否展开直子节点, 默认为false
+ * @method $this disabled(bool $bool) 禁掉响应, 默认为false
+ * @method $this disableCheckbox(bool $bool) 禁掉 checkbox
+ * @method $this selected(bool $bool) 是否选中子节点
+ * @method $this checked(bool $bool) 是否勾选(如果勾选,子节点也会全部勾选)
+ */
+class TreeData implements OptionComponentInterface
+{
+ use CallPropsRule;
+
+ /**
+ * @var array
+ */
+ protected $children = [];
+
+ /**
+ * @var array
+ */
+ protected $props = [];
+
+ /**
+ * @var array
+ */
+ protected static $propsRule = [
+ 'id' => 'string',
+ 'title' => 'string',
+ 'expand' => 'bool',
+ 'disabled' => 'bool',
+ 'disableCheckbox' => 'bool',
+ 'selected' => 'bool',
+ 'checked' => 'bool',
+ ];
+
+ /**
+ * TreeData constructor.
+ *
+ * @param $id
+ * @param $title
+ * @param array $children
+ */
+ public function __construct($id, $title, array $children = [])
+ {
+ $this->props['id'] = $id;
+ $this->props['title'] = $title;
+ $this->children = $children;
+ }
+
+ /**
+ * @param array $children
+ * @return $this
+ */
+ public function children(array $children)
+ {
+ $this->children = array_merge($this->children, $children);
+ return $this;
+ }
+
+ /**
+ * @param TreeData $child
+ * @return $this
+ */
+ public function child(TreeData $child)
+ {
+ $this->children[] = $child;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOption()
+ {
+ $children = [];
+ foreach ($this->children as $child) {
+ $children[] = $child instanceof TreeData
+ ? $child->getOption()
+ : $child;
+ }
+ $this->props['children'] = $children;
+ return $this->props;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Components/Upload.php b/vendor/xaboy/form-builder/src/UI/Iview/Components/Upload.php
new file mode 100644
index 0000000..fcb1a58
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Components/Upload.php
@@ -0,0 +1,119 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Components;
+
+
+use FormBuilder\Driver\FormComponent;
+use FormBuilder\Factory\Iview;
+
+/**
+ * 上传组件
+ * Class Upload
+ *
+ * @method $this uploadType(string $uploadType) 上传文件类型,可选值为 image(图片上传),file(文件上传)
+ * @method $this action(string $action) 上传的地址
+ * @method $this multiple(bool $bool) 是否支持多选文件
+ * @method $this uploadName(string $name) 上传的文件字段名
+ * @method $this accept(string $accept) 接受上传的文件类型
+ * @method $this maxSize(int $size) 文件大小限制,单位 kb
+ * @method $this withCredentials(bool $bool) 支持发送 cookie 凭证信息, 默认为false
+ * @method $this maxLength(Int $length) 最大上传文件数, 0为无限
+ *
+ */
+class Upload extends FormComponent
+{
+ /**
+ * file类型
+ */
+ const TYPE_FILE = 'file';
+
+ /**
+ * image类型
+ */
+ const TYPE_IMAGE = 'image';
+
+
+ protected $defaultProps = [
+ 'maxLength' => 0,
+ 'type' => 'select',
+ 'uploadType' => self::TYPE_FILE,
+ 'headers' => [],
+ 'data' => [],
+ 'format' => [],
+ 'show-upload-list' => false
+ ];
+
+ protected static $propsRule = [
+ 'uploadType' => 'string',
+ 'action' => 'string',
+ 'multiple' => 'bool',
+ 'uploadName' => ['string', 'name'],
+ 'accept' => 'string',
+ 'maxSize' => 'int',
+ 'withCredentials' => 'bool',
+ 'maxLength' => 'int'
+ ];
+
+ protected function init()
+ {
+ $this->name($this->field);
+ }
+
+ /**
+ * 设置上传的请求头部
+ *
+ * @param array $headers
+ * @return $this
+ */
+ public function headers(array $headers)
+ {
+ $this->props['headers'] = array_merge($this->props['headers'], $headers);
+ return $this;
+ }
+
+ /**
+ * 支持的文件类型,与 accept 不同的是,
+ * format 是识别文件的后缀名,accept 为 input 标签原生的 accept 属性,
+ * 会在选择文件时过滤,可以两者结合使用
+ *
+ * @param array $format
+ * @return $this
+ */
+ public function format(array $format)
+ {
+ $this->props['format'] = array_merge($this->props['format'], $format);
+ return $this;
+ }
+
+ /**
+ * 上传时附带的额外参数
+ *
+ * @param array $data
+ * @return $this
+ */
+ public function data(array $data)
+ {
+ $this->props['data'] = array_merge($this->props['data'], $data);
+ return $this;
+ }
+
+ protected function getPlaceHolder()
+ {
+ return '请上传' . $this->field;
+ }
+
+ public function createValidate()
+ {
+ return $this->props['maxLength'] == 1 ? Iview::validateStr() : Iview::validateArr();
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Config.php b/vendor/xaboy/form-builder/src/UI/Iview/Config.php
new file mode 100644
index 0000000..9015418
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Config.php
@@ -0,0 +1,218 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview;
+
+
+use FormBuilder\Contract\ConfigInterface;
+use FormBuilder\UI\Iview\Components\Button;
+use FormBuilder\UI\Iview\Components\Poptip;
+use FormBuilder\UI\Iview\Components\Tooltip;
+use FormBuilder\UI\Iview\Style\FormStyle;
+use FormBuilder\UI\Iview\Style\Row;
+
+class Config implements ConfigInterface
+{
+ const INFO_TYPE_POPTIP = 'poptip';
+
+ const INFO_TYPE_TOOLTIP = 'tooltip';
+
+ protected $config;
+
+ public function __construct(array $config = [])
+ {
+ $this->config = $config;
+ }
+
+ public function info($type)
+ {
+ if (is_array($type) || $type instanceof Poptip || $type instanceof Tooltip)
+ $this->config['info'] = $type;
+
+ return $this;
+ }
+
+ /**
+ * @param string $type tooltip,poptip
+ * @return Poptip|Tooltip
+ */
+ public function createInfo($type = self::INFO_TYPE_POPTIP)
+ {
+ if (strtolower($type) === self::INFO_TYPE_TOOLTIP)
+ $info = new Tooltip();
+ else
+ $info = new Poptip();
+ $this->info($info);
+ return $info;
+ }
+
+ /**
+ * 表单整体显示规则配置
+ *
+ * @param FormStyle|array $formStyle
+ * @return $this
+ */
+ public function formStyle($formStyle)
+ {
+ $this->config['form'] = $formStyle;
+ return $this;
+ }
+
+ public function createFormStyle(array $rule = [])
+ {
+ $formStyle = new FormStyle($rule);
+ $this->formStyle($formStyle);
+ return $formStyle;
+ }
+
+ /**
+ * 表单组件布局配置
+ *
+ * @param Row|array $row
+ * @return $this
+ */
+ public function row($row)
+ {
+ $this->config['row'] = $row;
+ return $this;
+ }
+
+ public function createRow(array $rule = [])
+ {
+ $row = new Row($rule);
+ $this->row($row);
+ return $row;
+ }
+
+ /**
+ * 提交按钮样式和布局配置
+ *
+ * @param Button|array|bool $btn
+ * @return $this
+ */
+ public function submitBtn($btn)
+ {
+ $this->config['submitBtn'] = $btn;
+ return $this;
+ }
+
+ public function createSubmitBtn()
+ {
+ $submitBtn = new Button();
+ $this->submitBtn($submitBtn);
+ return $submitBtn;
+ }
+
+ /**
+ * 开启事件注入
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function injectEvent($bool)
+ {
+ $this->config['injectEvent'] = !!$bool;
+ return $this;
+ }
+
+ /**
+ * 重置按钮样式和布局配置
+ *
+ * @param Button|array|bool $btn
+ * @return $this
+ */
+ public function resetBtn($btn)
+ {
+ $this->config['resetBtn'] = $btn;
+ return $this;
+ }
+
+ /**
+ * @return Button
+ */
+ public function createResetBtn()
+ {
+ $resetBtn = new Button();
+ $this->resetBtn($resetBtn);
+ return $resetBtn;
+ }
+
+ /**
+ * @param Button $btn
+ * @return mixed
+ */
+ protected function parseButton(Button $btn)
+ {
+ $rule = $btn->build();
+ if (isset($rule['col']))
+ $rule['props']->col = $rule['col'];
+ return $rule['props'];
+ }
+
+ /**
+ * @param $info
+ * @return mixed
+ */
+ protected function parseInfo($info)
+ {
+ if ($info instanceof Poptip || $info instanceof Tooltip) {
+ $rule = $info->build();
+ $info = $rule['props'];
+ $info->type = $rule['type'];
+ }
+
+ return $info;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ $config = $this->config;
+ if (isset($config['form']) && ($form = $config['form']) instanceof FormStyle)
+ $config['form'] = $form->getStyle();
+ if (isset($config['row']) && ($row = $config['row']) instanceof Row)
+ $config['row'] = $row->getStyle();
+ if (isset($config['submitBtn']) && ($submitBtn = $config['submitBtn']) instanceof Button)
+ $config['submitBtn'] = $this->parseButton($submitBtn);
+ if (isset($config['resetBtn']) && ($resetBtn = $config['resetBtn']) instanceof Button)
+ $config['resetBtn'] = $this->parseButton($resetBtn);
+ if (isset($config['info']))
+ $config['info'] = $this->parseInfo($config['info']);
+
+ return $config;
+ }
+
+
+ /**
+ * @param string $componentName
+ * @param array $config
+ * @return mixed
+ */
+ public function componentGlobalConfig($componentName, array $config)
+ {
+ if (!isset($this->config['global'])) $this->config['global'] = [];
+ $this->config['global'][$componentName] = $config;
+ return $this;
+ }
+
+
+ /**
+ * @param array $config
+ * @return $this
+ */
+ public function componentGlobalCommonConfig(array $config)
+ {
+ return $this->componentGlobalConfig('*', $config);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Style/Col.php b/vendor/xaboy/form-builder/src/UI/Iview/Style/Col.php
new file mode 100644
index 0000000..37db671
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Style/Col.php
@@ -0,0 +1,178 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Style;
+
+
+use FormBuilder\Contract\ColComponentInterface;
+
+/**
+ * col栅格规则
+ *
+ * Class Col
+ * @package FormBuilder\Style
+ */
+class Col implements ColComponentInterface
+{
+ protected $rule;
+
+ public function __construct(array $rule = [])
+ {
+ $this->rule = $rule;
+ }
+
+ /**
+ * 栅格的占位格数,可选值为0~24的整数,为 0 时,相当于display:none
+ *
+ * @param int|string $span
+ * @return $this
+ */
+ public function span($span)
+ {
+ $this->rule['span'] = $span;
+ return $this;
+ }
+
+ /**
+ * 栅格的顺序,在flex布局模式下有效
+ *
+ * @param int|string $order
+ * @return $this
+ */
+ public function order($order)
+ {
+ $this->rule['order'] = $order;
+ return $this;
+ }
+
+ /**
+ * 设置表单域 label 的宽度
+ *
+ * @param int|string $labelWidth
+ * @return $this
+ */
+ public function labelWidth($labelWidth)
+ {
+ $this->rule['labelWidth'] = $labelWidth;
+ return $this;
+ }
+
+ /**
+ * 栅格左侧的间隔格数,间隔内不可以有栅格
+ *
+ * @param int $offset
+ * @return $this
+ */
+ public function offset($offset)
+ {
+ $this->rule['offset'] = $offset;
+ return $this;
+ }
+
+ /**
+ * 栅格向右移动格数
+ *
+ * @param int $push
+ * @return $this
+ */
+ public function push($push)
+ {
+ $this->rule['push'] = $push;
+ return $this;
+ }
+
+ /**
+ * 栅格向左移动格数
+ *
+ * @param $pull
+ * @return $this
+ */
+ public function pull($pull)
+ {
+ $this->rule['pull'] = $pull;
+ return $this;
+ }
+
+ /**
+ * 组件的class
+ *
+ * @param $class
+ * @return $this
+ */
+ public function className($class)
+ {
+ $this->rule['className'] = (string)$class;
+ return $this;
+ }
+
+ /**
+ * <768px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $xs
+ * @return $this
+ */
+ public function xs($xs)
+ {
+ $this->rule['xs'] = $this->buildGrid($xs);
+ return $this;
+ }
+
+ /**
+ * ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $sm
+ * @return $this
+ */
+ public function sm($sm)
+ {
+ $this->rule['sm'] = $this->buildGrid($sm);
+ return $this;
+ }
+
+ /**
+ * ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $md
+ * @return $this
+ */
+ public function md($md)
+ {
+ $this->rule['md'] = $this->buildGrid($md);
+ return $this;
+ }
+
+ /**
+ * ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象
+ *
+ * @param int|self $lg
+ * @return $this
+ */
+ public function lg($lg)
+ {
+ $this->rule['lg'] = $this->buildGrid($lg);
+ return $this;
+ }
+
+ protected function buildGrid($grid)
+ {
+ return $grid instanceof self ? $grid->getCol() : (int)$grid;
+ }
+
+ /**
+ * @return object
+ */
+ public function getCol()
+ {
+ return (object)$this->rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Style/FormStyle.php b/vendor/xaboy/form-builder/src/UI/Iview/Style/FormStyle.php
new file mode 100644
index 0000000..e75be1e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Style/FormStyle.php
@@ -0,0 +1,117 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Style;
+
+
+use FormBuilder\Contract\StyleInterface;
+
+/**
+ * form表单样式
+ * Class FormStyle
+ */
+class FormStyle implements StyleInterface
+{
+ /**
+ * @var array
+ */
+ protected $rule;
+
+ /**
+ * FormStyle constructor.
+ * @param array $rule
+ */
+ public function __construct(array $rule = [])
+ {
+ $this->rule = $rule;
+ }
+
+ /**
+ * 是否开启行内表单模式
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function inline($bool)
+ {
+ $this->rule['inline'] = !!$bool;
+ return $this;
+ }
+
+ /**
+ * 设置表单 class
+ * @param $class
+ * @return $this
+ */
+ public function className($class)
+ {
+ $this->rule['className'] = $class;
+ return $this;
+ }
+
+ /**
+ * 表单域标签的位置,可选值为 left、right、top
+ *
+ * @param string $position
+ * @return $this
+ */
+ public function labelPosition($position)
+ {
+ $this->rule['labelPosition'] = $position;
+ return $this;
+ }
+
+ /**
+ * 表单域标签的宽度,所有的 FormItem 都会继承 Form 组件的 label-width 的值
+ *
+ * @param $labelWidth
+ * @return $this
+ */
+ public function labelWidth($labelWidth)
+ {
+ $this->rule['labelWidth'] = $labelWidth;
+ return $this;
+ }
+
+ /**
+ * 是否显示校验错误信息
+ *
+ * @param $showMessage
+ * @return $this
+ */
+ public function showMessage($showMessage)
+ {
+ $this->rule['showMessage'] = !!$showMessage;
+ return $this;
+ }
+
+ /**
+ * 原生的 autocomplete 属性,可选值为 true = off 或 false = on
+ *
+ * @param bool $bool
+ * @return $this
+ */
+ public function autocomplete($bool = false)
+ {
+ $this->rule['autocomplete'] = $bool === false ? 'on' : 'off';
+ return $this;
+ }
+
+ /**
+ * @return object
+ */
+ public function getStyle()
+ {
+ return (object)$this->rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Style/Row.php b/vendor/xaboy/form-builder/src/UI/Iview/Style/Row.php
new file mode 100644
index 0000000..ce3d939
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Style/Row.php
@@ -0,0 +1,90 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Style;
+
+
+use FormBuilder\Contract\StyleInterface;
+
+/**
+ * row栅格规则
+ *
+ * Class Row
+ */
+class Row implements StyleInterface
+{
+ protected $rule;
+
+ public function __construct(array $rule = [])
+ {
+ $this->rule = $rule;
+ }
+
+ /**
+ * 栅格间距,单位 px,左右平分
+ *
+ * @param int $gutter
+ */
+ public function gutter($gutter)
+ {
+ $this->rule['gutter'] = (float)$gutter;
+ }
+
+ /**
+ * 布局模式,可选值为flex或不选,在现代浏览器下有效
+ *
+ * @param string $type
+ */
+ public function type($type)
+ {
+ $this->rule['type'] = $type;
+ }
+
+ /**
+ * flex 布局下的垂直对齐方式,可选值为top、middle、bottom
+ *
+ * @param string $align
+ */
+ public function align($align)
+ {
+ $this->rule['align'] = $align;
+ }
+
+ /**
+ * flex 布局下的水平排列方式,可选值为start、end、center、space-around、space-between
+ *
+ * @param string $justify
+ */
+ public function justify($justify)
+ {
+ $this->rule['justify'] = $justify;
+ }
+
+ /**
+ * 组件的class
+ *
+ * @param string $className
+ */
+ public function className($className)
+ {
+ $this->rule['className'] = $className;
+ }
+
+ /**
+ * @return object
+ */
+ public function getStyle()
+ {
+ return (object)$this->rule;
+ }
+
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/CascaderFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/CascaderFactoryTrait.php
new file mode 100644
index 0000000..43ea767
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/CascaderFactoryTrait.php
@@ -0,0 +1,80 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Cascader;
+
+trait CascaderFactoryTrait
+{
+ /**
+ * 多级联动组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @param string $type
+ * @return Cascader
+ */
+ public static function cascader($field, $title, array $value = [], $type = Cascader::TYPE_OTHER)
+ {
+ $cascader = new Cascader($field, $title, $value);
+ $cascader->type($type);
+ return $cascader;
+ }
+
+
+ /**
+ * 省市二级联动
+ *
+ * @param string $field
+ * @param string $title
+ * @param array|string $province
+ * @param string $city
+ * @return Cascader
+ */
+ public static function city($field, $title, $province = [], $city = '')
+ {
+ if (is_array($province))
+ $value = $province;
+ else
+ $value = [(string)$province, (string)$city];
+
+ $cascader = self::cascader($field, $title, $value, Cascader::TYPE_CITY);
+ $cascader->jsData('province_city');
+ return $cascader;
+ }
+
+
+ /**
+ * 省市区三级联动
+ *
+ * @param string $field
+ * @param string $title
+ * @param array|string $province
+ * @param string $city
+ * @param string $area
+ * @return Cascader
+ */
+ public static function cityArea($field, $title, $province = [], $city = '', $area = '')
+ {
+ if (is_array($province))
+ $value = $province;
+ else
+ $value = [(string)$province, (string)$city, (string)$area];
+
+ $cascader = self::cascader($field, $title, $value, Cascader::TYPE_CITY_AREA);
+ $cascader->jsData('province_city_area');
+ return $cascader;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/CheckBoxFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/CheckBoxFactoryTrait.php
new file mode 100644
index 0000000..9f8cd8a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/CheckBoxFactoryTrait.php
@@ -0,0 +1,31 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+use FormBuilder\UI\Iview\Components\Checkbox;
+
+trait CheckBoxFactoryTrait
+{
+ /**
+ * 多选框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Checkbox
+ */
+ public static function checkbox($field, $title, array $value = [])
+ {
+ return new Checkbox($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/ColorPickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/ColorPickerFactoryTrait.php
new file mode 100644
index 0000000..b2e8e81
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/ColorPickerFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\ColorPicker;
+
+trait ColorPickerFactoryTrait
+{
+ /**
+ * 颜色选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return ColorPicker
+ */
+ public static function color($field, $title, $value = '')
+ {
+ return new ColorPicker($field, $title, (string)$value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/DatePickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/DatePickerFactoryTrait.php
new file mode 100644
index 0000000..77aa1dd
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/DatePickerFactoryTrait.php
@@ -0,0 +1,128 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\DatePicker;
+
+trait DatePickerFactoryTrait
+{
+ /**
+ * 日期组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param string $type
+ * @return DatePicker
+ */
+ public static function datePicker($field, $title, $value = '', $type = DatePicker::TYPE_DATE)
+ {
+ $datePicker = new DatePicker($field, $title, $value);
+ return $datePicker->type($type);
+ }
+
+ /**
+ * 单选日期
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function date($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_DATE);
+ }
+
+ /**
+ * 多选日期
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return DatePicker
+ */
+ public static function dateMultiple($field, $title, array $value)
+ {
+ $date = self::datePicker($field, $title, $value, DatePicker::TYPE_DATE);
+ return $date->multiple(true);
+ }
+
+ /**
+ * 日期区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $startDate
+ * @param string $endDate
+ * @return DatePicker
+ */
+ public static function dateRange($field, $title, $startDate = '', $endDate = '')
+ {
+ return self::datePicker($field, $title, [(string)$startDate, (string)$endDate], DatePicker::TYPE_DATE_RANGE);
+ }
+
+ /**
+ * 单选日期时间
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function dateTime($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_DATE_TIME);
+ }
+
+ /**
+ * 日期时间区间选择
+ *
+ * @param $field
+ * @param $title
+ * @param string $startDate
+ * @param string $endDate
+ * @return DatePicker
+ */
+ public static function dateTimeRange($field, $title, $startDate = '', $endDate = '')
+ {
+ return self::datePicker($field, $title, [(string)$startDate, (string)$endDate], DatePicker::TYPE_DATE_TIME_RANGE);
+ }
+
+ /**
+ * 选择年
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function year($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_YEAR);
+ }
+
+ /**
+ * 选择月
+ *
+ * @param $field
+ * @param $title
+ * @param string $value
+ * @return DatePicker
+ */
+ public static function month($field, $title, $value = '')
+ {
+ return self::datePicker($field, $title, (string)$value, DatePicker::TYPE_MONTH);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/FormStyleFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/FormStyleFactoryTrait.php
new file mode 100644
index 0000000..3a9aaf9
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/FormStyleFactoryTrait.php
@@ -0,0 +1,55 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Style\Col;
+use FormBuilder\UI\Iview\Style\FormStyle;
+use FormBuilder\UI\Iview\Style\Row;
+
+trait FormStyleFactoryTrait
+{
+
+ /**
+ * 组件布局规则类
+ *
+ * @param array $rule
+ * @return Col
+ */
+ public static function col(array $rule = [])
+ {
+ return new Col($rule);
+ }
+
+ /**
+ * 表格布局规则类
+ *
+ * @param $rule
+ * @return Row
+ */
+ public static function row(array $rule = [])
+ {
+ return new Row($rule);
+ }
+
+ /**
+ * 表格样式类
+ *
+ * @param array $rule
+ * @return FormStyle
+ */
+ public static function style(array $rule = [])
+ {
+ return new FormStyle($rule);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/FrameFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/FrameFactoryTrait.php
new file mode 100644
index 0000000..94f12ee
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/FrameFactoryTrait.php
@@ -0,0 +1,126 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Frame;
+
+trait FrameFactoryTrait
+{
+ /**
+ * 框架组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string|array $value
+ * @param string $type
+ * @return Frame
+ */
+ public static function frame($field, $title, $src, $value = [], $type = Frame::TYPE_INPUT)
+ {
+ $length = is_array($value) ? 0 : 1;
+ $frame = new Frame($field, $title, $value);
+ return $frame->maxLength($length)->src($src)->type($type);
+ }
+
+ /**
+ * 使用input类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameInputs($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value);
+ }
+
+ /**
+ * 使用文件类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameFiles($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_FILE);
+ }
+
+ /**
+ * 使用文件类型显示,多选
+ * value为Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param array $value
+ * @return Frame
+ */
+ public static function frameImages($field, $title, $src, array $value = [])
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_IMAGE);
+ }
+
+ /**
+ * 使用input类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameInput($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value);
+ }
+
+ /**
+ * 使用文件类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameFile($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_FILE);
+ }
+
+ /**
+ * 使用文件类型显示,单选
+ * value为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $src
+ * @param string $value
+ * @return Frame
+ */
+ public static function frameImage($field, $title, $src, $value = '')
+ {
+ return self::frame($field, $title, $src, $value, Frame::TYPE_IMAGE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/GroupFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/GroupFactoryTrait.php
new file mode 100644
index 0000000..6c7240d
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/GroupFactoryTrait.php
@@ -0,0 +1,33 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Group;
+
+trait GroupFactoryTrait
+{
+
+ /**
+ * 数组组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Group
+ */
+ public static function group($field, $title, $value = [])
+ {
+ return new Group($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/HiddenFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/HiddenFactoryTrait.php
new file mode 100644
index 0000000..434c3f3
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/HiddenFactoryTrait.php
@@ -0,0 +1,31 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Hidden;
+
+trait HiddenFactoryTrait
+{
+ /**
+ * 隐藏组件
+ *
+ * @param string $field
+ * @param mixed $value
+ * @return Hidden
+ */
+ public static function hidden($field, $value)
+ {
+ return new Hidden($field, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputFactoryTrait.php
new file mode 100644
index 0000000..02d6a32
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputFactoryTrait.php
@@ -0,0 +1,112 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Input;
+
+trait InputFactoryTrait
+{
+ /**
+ * input输入框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param string $type
+ * @return Input
+ */
+ public static function input($field, $title, $value = '', $type = Input::TYPE_TEXT)
+ {
+ $input = new Input($field, $title, (string)$value);
+ return $input->type($type);
+ }
+
+ /**
+ * text 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function text($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value);
+ }
+
+ /**
+ * password 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function password($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_PASSWORD);
+ }
+
+ /**
+ * textarea 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function textarea($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_TEXTAREA);
+ }
+
+ /**
+ * url 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function url($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_URL);
+ }
+
+ /**
+ * email 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function email($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_EMAIL);
+ }
+
+ /**
+ * date 类型输入框
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return Input
+ */
+ public static function idate($field, $title, $value = '')
+ {
+ return self::input($field, $title, $value, Input::TYPE_DATE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputNumberFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputNumberFactoryTrait.php
new file mode 100644
index 0000000..f65a63c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/InputNumberFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\InputNumber;
+
+trait InputNumberFactoryTrait
+{
+ /**
+ * 数字输入框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param null|number $value
+ * @return InputNumber
+ */
+ public static function number($field, $title, $value = null)
+ {
+ return new InputNumber($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/RadioFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/RadioFactoryTrait.php
new file mode 100644
index 0000000..40f9407
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/RadioFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Radio;
+
+trait RadioFactoryTrait
+{
+ /**
+ * 单选框组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string|number $value
+ * @return Radio
+ */
+ public static function radio($field, $title, $value = '')
+ {
+ return new Radio($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/RateFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/RateFactoryTrait.php
new file mode 100644
index 0000000..6b94035
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/RateFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Rate;
+
+trait RateFactoryTrait
+{
+ /**
+ * 评分选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param int|float $value
+ * @return Rate
+ */
+ public static function rate($field, $title, $value = 0)
+ {
+ return new Rate($field, $title, (float)$value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/SelectFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SelectFactoryTrait.php
new file mode 100644
index 0000000..cb57a8e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SelectFactoryTrait.php
@@ -0,0 +1,48 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Select;
+
+trait SelectFactoryTrait
+{
+ /**
+ * 下拉选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Select
+ */
+ public static function select($field, $title, $value = '')
+ {
+ $multiple = is_array($value) ? true : false;
+ $select = new Select($field, $title, $value);
+ $select->multiple($multiple);
+ return $select;
+ }
+
+ /**
+ * 多选
+ *
+ * @param string $field
+ * @param string $title
+ * @param array $value
+ * @return Select
+ */
+ public static function selectMultiple($field, $title, array $value = [])
+ {
+ return self::select($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/SliderFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SliderFactoryTrait.php
new file mode 100644
index 0000000..b3a0b70
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SliderFactoryTrait.php
@@ -0,0 +1,50 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Slider;
+
+trait SliderFactoryTrait
+{
+ /**
+ * 滑块组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param int|array $value
+ * @return Slider
+ */
+ public static function slider($field, $title, $value = 0)
+ {
+ $slider = new Slider($field, $title, $value);
+ if (is_array($value)) $slider->range(true);
+ return $slider;
+ }
+
+ /**
+ * 区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param int $start
+ * @param int $end
+ * @return Slider
+ */
+ public static function sliderRange($field, $title, $start = 0, $end = 0)
+ {
+ $slider = self::slider($field, $title, [(int)$start, (int)$end]);
+ $slider->range(true);
+ return $slider;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/SwitchesFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SwitchesFactoryTrait.php
new file mode 100644
index 0000000..bcfcde8
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/SwitchesFactoryTrait.php
@@ -0,0 +1,32 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Switches;
+
+trait SwitchesFactoryTrait
+{
+ /**
+ * 开关组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Switches
+ */
+ public static function switches($field, $title, $value = '0')
+ {
+ return new Switches($field, $title, $value);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/TimePickerFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/TimePickerFactoryTrait.php
new file mode 100644
index 0000000..6d6327a
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/TimePickerFactoryTrait.php
@@ -0,0 +1,61 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\TimePicker;
+
+trait TimePickerFactoryTrait
+{
+ /**
+ * 时间选择组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @param string|array $type
+ * @return TimePicker
+ */
+ public static function timePicker($field, $title, $value = '', $type = TimePicker::TYPE_TIME)
+ {
+ $timePicker = new TimePicker($field, $title, $value);
+ return $timePicker->type($type);
+ }
+
+ /**
+ * 时间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $value
+ * @return TimePicker
+ */
+ public static function time($field, $title, $value = '')
+ {
+ return self::timePicker($field, $title, (string)$value);
+ }
+
+ /**
+ * 时间区间选择
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $startTime
+ * @param string $endTime
+ * @return TimePicker
+ */
+ public static function timeRange($field, $title, $startTime = '', $endTime = '')
+ {
+ return self::timePicker($field, $title, [(string)$startTime, (string)$endTime], TimePicker::TYPE_TIME_RANGE);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/TreeFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/TreeFactoryTrait.php
new file mode 100644
index 0000000..751066c
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/TreeFactoryTrait.php
@@ -0,0 +1,74 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+
+use FormBuilder\UI\Iview\Components\Tree;
+use FormBuilder\UI\Iview\Components\TreeData;
+
+trait TreeFactoryTrait
+{
+ /**
+ * 树形组件
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @param string $type
+ * @return Tree
+ */
+ public static function tree($field, $title, $value = [], $type = Tree::TYPE_CHECKED)
+ {
+ $tree = new Tree($field, $title, $value);
+ return $tree->type($type);
+ }
+
+ /**
+ * 获取选中的值
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Tree
+ */
+ public static function treeSelected($field, $title, $value = [])
+ {
+ return self::tree($field, $title, $value, Tree::TYPE_SELECTED);
+ }
+
+ /**
+ * 获取勾选的值
+ *
+ * @param string $field
+ * @param string $title
+ * @param mixed $value
+ * @return Tree
+ */
+ public static function treeChecked($field, $title, $value = [])
+ {
+ return self::tree($field, $title, $value)->showCheckbox(true);
+ }
+
+ /**
+ * 树形组件数据 date 类
+ *
+ * @param mixed $id
+ * @param string $title
+ * @param array $children
+ * @return TreeData
+ */
+ public static function treeData($id, $title, array $children = [])
+ {
+ return new TreeData($id, $title, $children);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/UploadFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/UploadFactoryTrait.php
new file mode 100644
index 0000000..5f80e3e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/UploadFactoryTrait.php
@@ -0,0 +1,110 @@
+uploadType($type)->action($action);
+ }
+
+ /**
+ * 图片上传
+ * value 为 Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param array $value
+ * @return Upload
+ */
+ public static function uploadImages($field, $title, $action, array $value = [])
+ {
+ $upload = self::upload($field, $title, $action, $value, Upload::TYPE_IMAGE);
+ return $upload->format(['jpg', 'jpeg', 'png', 'gif'])->accept('image/*');
+ }
+
+ /**
+ * 文件上传
+ * value 为 Array类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param array $value
+ * @return Upload
+ */
+ public static function uploadFiles($field, $title, $action, array $value = [])
+ {
+ return self::upload($field, $title, $action, $value, Upload::TYPE_FILE);
+ }
+
+ /**
+ * 单图片上传
+ * value 为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param string $value
+ * @return Upload
+ * @deprecated
+ */
+ public static function uploadImageOne($field, $title, $action, $value = '')
+ {
+ $upload = self::upload($field, $title, $action, (string)$value, Upload::TYPE_IMAGE);
+ return $upload->format(['jpg', 'jpeg', 'png', 'gif'])->accept('image/*')->maxLength(1);
+ }
+
+ /**
+ * 单图片上传
+ * value 为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param string $value
+ * @return Upload
+ */
+ public static function uploadImage($field, $title, $action, $value = '')
+ {
+ $upload = self::upload($field, $title, $action, (string)$value, Upload::TYPE_IMAGE);
+ return $upload->format(['jpg', 'jpeg', 'png', 'gif'])->accept('image/*')->maxLength(1);
+ }
+
+ /**
+ * 单文件上传
+ * value 为string类型
+ *
+ * @param string $field
+ * @param string $title
+ * @param string $action
+ * @param string $value
+ * @return Upload
+ */
+ public static function uploadFile($field, $title, $action, $value = '')
+ {
+ $upload = self::upload($field, $title, $action, (string)$value, Upload::TYPE_FILE);
+ return $upload->maxLength(1);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Traits/ValidateFactoryTrait.php b/vendor/xaboy/form-builder/src/UI/Iview/Traits/ValidateFactoryTrait.php
new file mode 100644
index 0000000..0dee74e
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Traits/ValidateFactoryTrait.php
@@ -0,0 +1,73 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview\Traits;
+
+use FormBuilder\UI\Iview\Validate as IViewValidate;
+
+trait ValidateFactoryTrait
+{
+ public static function validateStr($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_STRING, $trigger);
+ }
+
+ public static function validateArr($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_ARRAY, $trigger);
+ }
+
+ public static function validateNum($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_NUMBER, $trigger);
+ }
+
+ public static function validateDate($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_DATE, $trigger);
+ }
+
+ public static function validateInt($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_INTEGER, $trigger);
+ }
+
+ public static function validateFloat($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_FLOAT, $trigger);
+ }
+
+ public static function validateObject($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_OBJECT, $trigger);
+ }
+
+ public static function validateEmail($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_EMAIL, $trigger);
+ }
+
+ public static function validateEnum($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_ENUM, $trigger);
+ }
+
+ public static function validateUrl($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_URL, $trigger);
+ }
+
+ public static function validateHex($trigger = IViewValidate::TRIGGER_CHANGE)
+ {
+ return new IViewValidate(IViewValidate::TYPE_HEX, $trigger);
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/UI/Iview/Validate.php b/vendor/xaboy/form-builder/src/UI/Iview/Validate.php
new file mode 100644
index 0000000..d67fe36
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/UI/Iview/Validate.php
@@ -0,0 +1,225 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder\UI\Iview;
+
+
+use FormBuilder\Contract\ValidateInterface;
+
+class Validate implements ValidateInterface
+{
+ const TYPE_STRING = 'string';
+
+ const TYPE_ARRAY = 'array';
+
+ const TYPE_NUMBER = 'number';
+
+ const TYPE_INTEGER = 'integer';
+
+ const TYPE_FLOAT = 'float';
+
+ const TYPE_OBJECT = 'object';
+
+ const TYPE_ENUM = 'enum';
+
+ const TYPE_URL = 'url';
+
+ const TYPE_HEX = 'hex';
+
+ const TYPE_EMAIL = 'email';
+
+ const TYPE_DATE = 'date';
+
+ const TRIGGER_CHANGE = 'change';
+
+ const TRIGGER_BLUR = 'blur';
+
+ const TRIGGER_SUBMIT = 'submit';
+
+ protected $validate = [
+ 'fields' => []
+ ];
+
+ protected $type;
+
+ protected $trigger;
+
+ /**
+ * Validate constructor.
+ * @param string $type
+ * @param string $trigger
+ */
+ public function __construct($type, $trigger = self::TRIGGER_CHANGE)
+ {
+ $this->type($type);
+ $this->trigger($trigger);
+ }
+
+ /**
+ * @param string $trigger
+ * @return $this
+ */
+ public function trigger($trigger)
+ {
+ $this->trigger = (string)$trigger;
+ return $this;
+ }
+
+ /**
+ * @param $type
+ * @return $this
+ */
+ public function type($type)
+ {
+
+ $this->type = (string)$type;
+ return $this;
+ }
+
+
+ public function set($validate)
+ {
+ array_merge($this->validate, $validate);
+ if (!is_array($validate['fields'])) $validate['fields'] = [];
+
+ return $this;
+ }
+
+ public function fields(array $fields)
+ {
+ $this->validate['fields'] = array_merge($this->validate['fields'], $fields);
+ return $this;
+ }
+
+ public function field($field, $validate)
+ {
+ $this->validate['fields'][$field] = $validate;
+ return $this;
+ }
+
+ /**
+ * 必填
+ *
+ * @return $this
+ */
+ public function required()
+ {
+ $this->validate['required'] = true;
+ return $this;
+ }
+
+ /**
+ * 长度或值必须在这个范围内
+ *
+ * @param int|float $min
+ * @param int|float $max
+ * @return $this
+ */
+ public function range($min, $max)
+ {
+ $this->validate['min'] = (float)$min;
+ $this->validate['max'] = (float)$max;
+ return $this;
+ }
+
+ /**
+ * 长度或值必须大于这个值
+ *
+ * @param int|float $min
+ * @return $this
+ */
+ public function min($min)
+ {
+ $this->validate['min'] = (float)$min;
+ return $this;
+ }
+
+ /**
+ * 长度或值必须小于这个值
+ *
+ * @param int|float $max
+ * @return $this
+ */
+ public function max($max)
+ {
+ $this->validate['max'] = (float)$max;
+ return $this;
+ }
+
+ /**
+ * 长度或值必须等于这个值
+ *
+ * @param int $length
+ * @return $this
+ */
+ public function length($length)
+ {
+ $this->validate['len'] = (int)$length;
+ return $this;
+ }
+
+ /**
+ * 值必须在 list 中
+ *
+ * @param array $list
+ * @return $this
+ */
+ public function enum(array $list)
+ {
+ $this->validate['enum'] = $list;
+ return $this;
+ }
+
+ /**
+ * 错误信息
+ * @param $message
+ * @return $this
+ */
+ public function message($message)
+ {
+ $this->validate['message'] = (string)$message;
+ return $this;
+ }
+
+ /**
+ * 正则
+ *
+ * @param $pattern
+ * @return $this
+ */
+ public function pattern($pattern)
+ {
+ $this->validate['pattern'] = (string)$pattern;
+ return $this;
+ }
+
+ public function getValidate()
+ {
+ $validate = $this->validate;
+ $validate['type'] = $this->type;
+ $validate['trigger'] = $this->trigger;
+ $fields = $validate['fields'];
+
+ if (!($fieldCount = count($fields)) && count($validate) === 1)
+ return [];
+
+ if ($fieldCount) {
+ foreach ($fields as $k => $field) {
+ $fields[$k] = $field instanceof self ? $field->getValidate() : $field;
+ }
+ $validate['fields'] = (object)$fields;
+ } else {
+ unset($validate['fields']);
+ }
+ return $validate;
+ }
+}
\ No newline at end of file
diff --git a/vendor/xaboy/form-builder/src/Util.php b/vendor/xaboy/form-builder/src/Util.php
new file mode 100644
index 0000000..ff5cb01
--- /dev/null
+++ b/vendor/xaboy/form-builder/src/Util.php
@@ -0,0 +1,25 @@
+
+ * @version 2.0
+ * @license MIT
+ * @link https://github.com/xaboy/form-builder
+ * @document http://php.form-create.com
+ */
+
+namespace FormBuilder;
+
+
+use FormBuilder\Contract\CustomComponentInterface;
+use FormBuilder\Contract\FormComponentInterface;
+
+class Util
+{
+ public static function isComponent($component)
+ {
+ return $component instanceof CustomComponentInterface || $component instanceof FormComponentInterface;
+ }
+}
\ No newline at end of file