mirror of
https://gitee.com/zoujingli/ThinkAdmin.git
synced 2026-06-08 04:48:10 +08:00
将 v6 中直接放在本地 app 的后台与微信能力迁移为 v8 插件组件,并把运行时基础能力沉淀到独立插件包。 主要内容: - 新增 think-library、system、worker、static、install 等基础插件包。 - 新增 account、payment、wechat-client、wechat-service、wemall、wuma 等业务插件包。 - 移除 v6 的 app/admin 与 app/wechat 本地应用实现,改由插件分发接管。 - 将 Helper 能力彻底并入 System,统一为 plugin\system\helper\* 命名空间。 - 同步插件迁移发布清单与根 route 占位,保证安装发布流程可复现。
294 lines
9.4 KiB
PHP
294 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* +----------------------------------------------------------------------
|
|
* | ThinkAdmin Plugin for ThinkAdminDeveloper
|
|
* +----------------------------------------------------------------------
|
|
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
|
|
* +----------------------------------------------------------------------
|
|
* | Official Website: https://thinkadmin.top
|
|
* +----------------------------------------------------------------------
|
|
* | Licensed: https://mit-license.org
|
|
* | Disclaimer: https://thinkadmin.top/disclaimer
|
|
* | Vip Rights: https://thinkadmin.top/vip-introduce
|
|
* +----------------------------------------------------------------------
|
|
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
|
|
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
|
|
* +----------------------------------------------------------------------
|
|
*/
|
|
|
|
namespace think\admin\tests;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use think\admin\middleware\MultAccess;
|
|
use think\admin\runtime\RequestContext;
|
|
use think\admin\service\AppService;
|
|
use think\admin\service\RuntimeService;
|
|
use think\App;
|
|
use think\Request;
|
|
use think\Response;
|
|
|
|
/**
|
|
* @internal
|
|
* @coversNothing
|
|
*/
|
|
class MultAccessDispatchTest extends TestCase
|
|
{
|
|
private string $projectRoot;
|
|
|
|
/** @var list<string> */
|
|
private array $createdFiles = [];
|
|
|
|
/** @var list<string> */
|
|
private array $createdDirectories = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->projectRoot = TEST_PROJECT_ROOT;
|
|
$this->resetRuntimeContext();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupPaths();
|
|
$this->resetRuntimeContext();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testLocalAppsAreDiscoveredWithoutTreatingSharedDirectoriesAsApps(): void
|
|
{
|
|
$this->createLocalApp('demoapp');
|
|
$this->bootApplication();
|
|
|
|
$locals = AppService::local(true);
|
|
$this->assertArrayHasKey('index', $locals);
|
|
$this->assertArrayHasKey('demoapp', $locals);
|
|
$this->assertArrayNotHasKey('controller', $locals);
|
|
$this->assertArrayNotHasKey('model', $locals);
|
|
|
|
$payload = $this->dispatchPath($this->bootApplication(), 'demoapp/dashboard/index');
|
|
$this->assertSame('demoapp', $payload['app']);
|
|
$this->assertSame('', $payload['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $payload['entry']);
|
|
$this->assertSame('/demoapp', $payload['root']);
|
|
$this->assertSame('dashboard/index', $payload['pathinfo']);
|
|
}
|
|
|
|
public function testExplicitPluginPrefixStillWinsOverGlobalRoutes(): void
|
|
{
|
|
$this->createLocalApp('demoapp');
|
|
$this->createRouteFile('multaccess_explicit_plugin.php', <<<'PHP'
|
|
<?php
|
|
|
|
use think\facade\Route;
|
|
|
|
Route::bindApp('system/login', 'dashboard/index', 'demoapp');
|
|
PHP);
|
|
|
|
$payload = $this->dispatchPath($this->bootApplication(), 'system/login');
|
|
$this->assertSame('system', $payload['app']);
|
|
$this->assertSame('system', $payload['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $payload['entry']);
|
|
$this->assertSame('/system', $payload['root']);
|
|
$this->assertSame('login', $payload['pathinfo']);
|
|
}
|
|
|
|
public function testGlobalRoutesCanBindLocalAndPluginTargets(): void
|
|
{
|
|
$this->createLocalApp('demoapp');
|
|
$this->createRouteFile('multaccess_targets.php', <<<'PHP'
|
|
<?php
|
|
|
|
use think\admin\runtime\RequestContext;
|
|
use think\facade\Route;
|
|
|
|
Route::bindApp('shortcut', 'dashboard/index', 'demoapp');
|
|
Route::bindPlugin('open-upload', 'api.upload/file', 'system', RequestContext::ENTRY_API);
|
|
PHP);
|
|
|
|
$local = $this->dispatchPath($this->bootApplication(), 'shortcut');
|
|
$this->assertSame('demoapp', $local['app']);
|
|
$this->assertSame('', $local['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $local['entry']);
|
|
$this->assertSame('shortcut', $local['pathinfo']);
|
|
|
|
$plugin = $this->dispatchPath($this->bootApplication(), 'open-upload');
|
|
$this->assertSame('system', $plugin['app']);
|
|
$this->assertSame('system', $plugin['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_API, $plugin['entry']);
|
|
$this->assertSame('open-upload', $plugin['pathinfo']);
|
|
}
|
|
|
|
public function testGlobalRouteGroupsCanDeclareDispatchTarget(): void
|
|
{
|
|
$this->createLocalApp('demoapp');
|
|
$this->createRouteFile('multaccess_groups.php', <<<'PHP'
|
|
<?php
|
|
|
|
use think\admin\runtime\RequestContext;
|
|
use think\facade\Route;
|
|
|
|
Route::appGroup('demoapp', function () {
|
|
Route::get('group-shortcut', 'dashboard/index');
|
|
});
|
|
|
|
Route::pluginGroup('system', function () {
|
|
Route::get('group-open-upload', 'api.upload/file');
|
|
}, RequestContext::ENTRY_API);
|
|
PHP);
|
|
|
|
$local = $this->dispatchPath($this->bootApplication(), 'group-shortcut');
|
|
$this->assertSame('demoapp', $local['app']);
|
|
$this->assertSame('', $local['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $local['entry']);
|
|
|
|
$plugin = $this->dispatchPath($this->bootApplication(), 'group-open-upload');
|
|
$this->assertSame('system', $plugin['app']);
|
|
$this->assertSame('system', $plugin['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_API, $plugin['entry']);
|
|
}
|
|
|
|
public function testLegacyModuleStyleGlobalRoutesCanStillInferDispatchTarget(): void
|
|
{
|
|
$this->createLocalApp('demoapp');
|
|
$this->createRouteFile('multaccess_legacy_targets.php', <<<'PHP'
|
|
<?php
|
|
|
|
use think\facade\Route;
|
|
|
|
Route::rule('legacy-demo', 'demoapp/dashboard/index');
|
|
Route::rule('legacy-system', 'system/login/index');
|
|
PHP);
|
|
|
|
$local = $this->dispatchPath($this->bootApplication(), 'legacy-demo');
|
|
$this->assertSame('demoapp', $local['app']);
|
|
$this->assertSame('', $local['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $local['entry']);
|
|
|
|
$plugin = $this->dispatchPath($this->bootApplication(), 'legacy-system');
|
|
$this->assertSame('system', $plugin['app']);
|
|
$this->assertSame('system', $plugin['plugin']);
|
|
$this->assertSame(RequestContext::ENTRY_WEB, $plugin['entry']);
|
|
}
|
|
|
|
private function bootApplication(): App
|
|
{
|
|
$this->resetRuntimeContext();
|
|
|
|
$app = new App($this->projectRoot);
|
|
RuntimeService::init($app);
|
|
$app->initialize();
|
|
$app->config->set(['with_route' => true], 'app');
|
|
$app->config->set(['default_app' => 'index'], 'route');
|
|
|
|
return $app;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function dispatchPath(App $app, string $pathinfo): array
|
|
{
|
|
$request = $app->make('request', [], true);
|
|
$request->setRoot('');
|
|
$request->setPathinfo($pathinfo);
|
|
$app->instance('request', $request);
|
|
|
|
$response = (new MultAccess($app))->handle($request, static function (Request $request) use ($app): Response {
|
|
return Response::create([
|
|
'app' => $app->http->getName(),
|
|
'plugin' => RequestContext::instance()->pluginCode(),
|
|
'entry' => RequestContext::instance()->entryType(),
|
|
'root' => $request->root(),
|
|
'pathinfo' => $request->pathinfo(),
|
|
], 'json');
|
|
});
|
|
|
|
return (array)json_decode((string)$response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
private function createLocalApp(string $code): void
|
|
{
|
|
$base = $this->projectRoot . '/app/' . $code;
|
|
$controllerPath = $base . '/controller';
|
|
if (!is_dir($controllerPath)) {
|
|
mkdir($controllerPath, 0777, true);
|
|
}
|
|
|
|
$this->createdDirectories[] = $base;
|
|
|
|
$file = $controllerPath . '/Index.php';
|
|
file_put_contents($file, str_replace('__CODE__', $code, <<<'PHP'
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\__CODE__\controller;
|
|
|
|
class Index
|
|
{
|
|
}
|
|
PHP));
|
|
$this->createdFiles[] = $file;
|
|
}
|
|
|
|
private function createRouteFile(string $name, string $content): void
|
|
{
|
|
$directory = $this->projectRoot . '/route';
|
|
if (!is_dir($directory)) {
|
|
mkdir($directory, 0777, true);
|
|
$this->createdDirectories[] = $directory;
|
|
}
|
|
$file = $this->projectRoot . '/route/' . $name;
|
|
file_put_contents($file, $content);
|
|
$this->createdFiles[] = $file;
|
|
}
|
|
|
|
private function cleanupPaths(): void
|
|
{
|
|
foreach (array_reverse($this->createdFiles) as $file) {
|
|
if (is_file($file)) {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
|
|
foreach (array_reverse(array_unique($this->createdDirectories)) as $directory) {
|
|
$this->removeDirectory($directory);
|
|
}
|
|
}
|
|
|
|
private function removeDirectory(string $path): void
|
|
{
|
|
if ($path === '' || !is_dir($path)) {
|
|
return;
|
|
}
|
|
|
|
foreach (scandir($path) ?: [] as $item) {
|
|
if ($item === '.' || $item === '..') {
|
|
continue;
|
|
}
|
|
|
|
$target = $path . DIRECTORY_SEPARATOR . $item;
|
|
if (is_dir($target)) {
|
|
$this->removeDirectory($target);
|
|
} elseif (is_file($target)) {
|
|
@unlink($target);
|
|
}
|
|
}
|
|
|
|
@rmdir($path);
|
|
}
|
|
|
|
private function resetRuntimeContext(): void
|
|
{
|
|
AppService::clear();
|
|
RequestContext::clear();
|
|
if (function_exists('sysvar')) {
|
|
sysvar('', '');
|
|
}
|
|
function_exists('test_reset_model_makers') && test_reset_model_makers();
|
|
}
|
|
}
|