This commit is contained in:
gaofeng
2026-05-13 10:44:29 +08:00
commit 0b165153c6
3674 changed files with 316663 additions and 0 deletions

35
vendor/topthink/think-dumper/README.md vendored Normal file
View File

@@ -0,0 +1,35 @@
ThinkPHP 调试输出扩展
--------------------
支持本地和远程调试输出
![](https://github.com/user-attachments/assets/a9979d5a-2ac9-41b4-ad35-92e52858526a)
## 安装
```
composer require topthink/think-dumper --dev
```
环境要求 `PHP8.0+`
## 调试
打开浏览器访问 https://developer.topthink.com/thinkphp/dumper
点击左侧菜单 生成令牌并复制令牌内容
修改`.env` 环境变量文件,增加
```
DUMPER_TOKEN = 令牌值
```
然后 在代码中使用助手函数`d`输出变量调试
```
dump($var...)
```
调试内容会在
https://developer.topthink.com/thinkphp/dumper 中显示
如果没有设置令牌或远程打印页面没有打开,则`dump`函数只会在本地输出调试内容
> 请放心,变量内容不会经过服务器,只会保存在本地浏览器,并且可以随时清除。

View File

@@ -0,0 +1,32 @@
{
"name": "topthink/think-dumper",
"description": "Dumper extend for thinkphp",
"license": "Apache-2.0",
"authors": [
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"autoload": {
"psr-4": {
"think\\dumper\\": "src"
},
"files": [
"src/helper.php"
]
},
"autoload-dev": {
"psr-4": {
"think\\tests\\dumper\\": "tests/"
}
},
"require": {
"php": ">=8.0.2",
"topthink/framework": "^6.0|^8.0",
"symfony/var-dumper": ">=6.0"
},
"require-dev": {
"phpunit/phpunit": "^11.4"
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutTestsThatDoNotTestAnything="false"
bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
verbose="true"
>
<testsuites>
<testsuite name="ThinkPHP Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,82 @@
<?php
namespace think\dumper;
use Symfony\Component\VarDumper\Cloner\Data;
class Connection
{
private string $host;
private string $token;
private array $contextProviders;
private $handle;
public function __construct(array $contextProviders = [])
{
$this->host = env('DUMPER_HOST', 'https://developer.topthink.com');
$this->token = env('DUMPER_TOKEN');
$this->contextProviders = $contextProviders;
}
public function write(Data $data): bool
{
if (!$this->handle = $this->handle ?: $this->createHandle()) {
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(fn() => true);
try {
return $this->sendPayload($encodedPayload);
} finally {
restore_error_handler();
}
}
private function sendPayload($payload)
{
curl_setopt($this->handle, CURLOPT_POSTFIELDS, $payload);
$res = curl_exec($this->handle);
if ($res === false) {
curl_close($this->handle);
$this->handle = null;
return false;
}
$code = curl_getinfo($this->handle, CURLINFO_HTTP_CODE);
return $code == 204;
}
private function createHandle()
{
set_error_handler(fn() => true);
try {
$handle = curl_init("{$this->host}/api/thinkphp/dumper/server");
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handle, CURLOPT_POST, true);
curl_setopt($handle, CURLOPT_HTTPHEADER, [
"Accept: application/json",
"Authorization: Bearer {$this->token}",
]);
curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($handle, CURLOPT_TIMEOUT, 3);
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 1);
return $handle;
} finally {
restore_error_handler();
}
}
public function __destruct()
{
if ($this->handle) {
curl_close($this->handle);
}
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace think\dumper;
use Exception;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
use Symfony\Component\VarDumper\Dumper\ContextualizedDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use think\App;
use think\Env;
use think\Request;
class Dumper
{
public function __construct(protected App $app, protected Env $env)
{
}
public function dump($var, ?string $label = null)
{
$format = $this->app->runningInConsole() ? 'cli' : 'html';
$handler = $this->createHandler($format);
return $handler($var, $label);
}
private function createHandler($format)
{
$cloner = new VarCloner();
$cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
switch ($format) {
case 'html' :
$dumper = new HtmlDumper();
break;
case 'cli':
$dumper = new CliDumper();
break;
default:
throw new Exception('Invalid dump format.');
}
if ($this->env->has('DUMPER_TOKEN')) {
$dumper = new ServerDumper($dumper, $this->getDefaultContextProviders($format));
} else {
$dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]);
}
return function ($var, ?string $label = null) use ($cloner, $dumper) {
$var = $cloner->cloneVar($var);
if (null !== $label) {
$var = $var->withContext(['label' => $label]);
}
$dumper->dump($var);
};
}
private function getDefaultContextProviders($format): array
{
$contextProviders = [];
switch ($format) {
case 'html' :
$contextProviders['request'] = new class($this->app->request) implements ContextProviderInterface {
public function __construct(protected Request $request)
{
}
public function getContext(): ?array
{
$request = $this->request;
return [
'uri' => $request->url(),
'method' => $request->method(),
'identifier' => spl_object_hash($request),
];
}
};
break;
case 'cli':
$contextProviders['cli'] = new class implements ContextProviderInterface {
public function getContext(): ?array
{
return [
'command_line' => $commandLine = implode(' ', $_SERVER['argv'] ?? []),
'identifier' => hash('crc32b', $commandLine . $_SERVER['REQUEST_TIME_FLOAT']),
];
}
};
break;
}
$contextProviders['source'] = new SourceContextProvider();
return $contextProviders;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace think\dumper;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\DataDumperInterface;
class ServerDumper implements DataDumperInterface
{
protected Connection $connection;
protected ?DataDumperInterface $wrappedDumper;
public function __construct(?DataDumperInterface $wrappedDumper = null, array $contextProviders = [])
{
$this->connection = new Connection($contextProviders);
$this->wrappedDumper = $wrappedDumper;
}
/**
* @return string|null
*/
public function dump(Data $data)
{
if (!$this->connection->write($data) && $this->wrappedDumper) {
return $this->wrappedDumper->dump($data);
}
return null;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace think\dumper;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
use Symfony\Component\VarDumper\VarDumper;
final class SourceContextProvider implements ContextProviderInterface
{
public function __construct(
private ?string $charset = null,
private ?string $projectDir = null,
private int $limit = 9,
)
{
}
public function getContext(): ?array
{
$trace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit);
$trace = array_reverse($trace);
$i = 0;
$file = '-';
$line = 0;
do {
$file = $trace[$i]['file'] ?? $file;
$line = $trace[$i]['line'] ?? $line;
$function = $trace[$i]['function'] ?? null;
$class = $trace[$i]['class'] ?? null;
if ($function == 'dump' && ($class == null || $class == Dumper::class || $class == VarDumper::class)) {
break;
}
} while (++$i < $this->limit);
$name = '-' === $file || 'Standard input code' === $file ? 'Standard input code' : false;
if (false === $name) {
$name = str_replace('\\', '/', $file);
$name = substr($name, strrpos($name, '/') + 1);
}
$context = ['name' => $name, 'file' => $file, 'line' => $line];
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);
}
}
return $context;
}
}

View File

@@ -0,0 +1,8 @@
<?php
use Symfony\Component\VarDumper\VarDumper;
use think\dumper\Dumper;
VarDumper::setHandler(function ($var, ?string $label = null) {
app(Dumper::class)->dump($var, $label);
});

View File

@@ -0,0 +1,14 @@
<?php
namespace think\tests\dumper;
use PHPUnit\Framework\TestCase;
class DumperTest extends TestCase
{
public function testDump()
{
dump(['aa' => 'bbb', 'cc' => 'dd']);
dump($this);
}
}

View File

@@ -0,0 +1,13 @@
<?php
$app = new \think\App();
$app->config->set([
'default' => 'file',
'stores' => [
'file' => [
'type' => 'File',
],
],
], 'cache');
$app->initialize();