mirror of
https://gitee.com/zoujingli/ThinkAdmin.git
synced 2026-06-07 20:48:09 +08:00
将 v8 重构分支中残留的 ThinkAdminDeveloper 文本统一调整为 ThinkAdmin,避免迁移到主仓库后继续暴露旧开发仓库名称。 主要内容: - 更新 README 标题与项目描述。 - 统一 PHP 文件头注释中的项目标识。 - 同步调整测试、配置、插件与文档中的旧仓库名称文本。 - 保持旧包删除说明与架构边界测试语义不变,只清理品牌名称残留。
282 lines
9.9 KiB
PHP
282 lines
9.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* +----------------------------------------------------------------------
|
|
* | ThinkAdmin Plugin
|
|
* +----------------------------------------------------------------------
|
|
* | 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;
|
|
|
|
/**
|
|
* @internal
|
|
* @coversNothing
|
|
*/
|
|
class ComposerDependencyBoundaryTest extends TestCase
|
|
{
|
|
private string $projectRoot;
|
|
|
|
/**
|
|
* @var array<string, array{name:string, path:string, require:array<string, string>}>
|
|
*/
|
|
private array $packages = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->projectRoot = TEST_PROJECT_ROOT;
|
|
$this->packages = $this->loadPackages();
|
|
}
|
|
|
|
public function testLocalPluginDependencyGraphIsAcyclic(): void
|
|
{
|
|
$graph = $this->localGraph();
|
|
$state = [];
|
|
$stack = [];
|
|
$cycle = [];
|
|
|
|
$visit = function (string $package) use (&$visit, &$graph, &$state, &$stack, &$cycle): void {
|
|
if ($cycle !== []) {
|
|
return;
|
|
}
|
|
$state[$package] = 1;
|
|
$stack[] = $package;
|
|
|
|
foreach ($graph[$package] ?? [] as $dependency) {
|
|
$depState = $state[$dependency] ?? 0;
|
|
if ($depState === 0) {
|
|
$visit($dependency);
|
|
if ($cycle !== []) {
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
if ($depState === 1) {
|
|
$offset = array_search($dependency, $stack, true);
|
|
$cycle = array_slice($stack, $offset === false ? 0 : $offset);
|
|
$cycle[] = $dependency;
|
|
return;
|
|
}
|
|
}
|
|
|
|
array_pop($stack);
|
|
$state[$package] = 2;
|
|
};
|
|
|
|
foreach (array_keys($graph) as $package) {
|
|
if (($state[$package] ?? 0) === 0) {
|
|
$visit($package);
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $cycle, 'Local composer dependency cycle detected: ' . implode(' -> ', $cycle));
|
|
}
|
|
|
|
public function testBasePackagesStayAtTheBottomOfDependencyGraph(): void
|
|
{
|
|
$this->assertSame([], $this->localRequires('zoujingli/think-library'));
|
|
$this->assertSame([
|
|
'zoujingli/think-library',
|
|
'zoujingli/think-plugs-static',
|
|
'zoujingli/think-plugs-worker',
|
|
], $this->localRequires('zoujingli/think-plugs-system'));
|
|
$this->assertSame(['zoujingli/think-library'], $this->localRequires('zoujingli/think-plugs-worker'));
|
|
}
|
|
|
|
public function testSystemDoesNotReintroduceLegacyStoragePackage(): void
|
|
{
|
|
$system = $this->localRequires('zoujingli/think-plugs-system');
|
|
|
|
$this->assertNotContains('zoujingli/think-plugs-storage', $system);
|
|
}
|
|
|
|
public function testHelperPackageStaysMergedIntoSystem(): void
|
|
{
|
|
$this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-helper'));
|
|
$this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/Service.php'));
|
|
$this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/project/PublishCommand.php'));
|
|
|
|
$legacyHelperNamespace = 'plugin' . '\\helper\\';
|
|
$systemComposer = $this->jsonFile($this->path('plugin/think-plugs-system/composer.json'));
|
|
$systemPsr4 = is_array($systemComposer['autoload']['psr-4'] ?? null) ? $systemComposer['autoload']['psr-4'] : [];
|
|
$systemServices = is_array($systemComposer['extra']['think']['services'] ?? null) ? $systemComposer['extra']['think']['services'] : [];
|
|
|
|
$this->assertArrayNotHasKey($legacyHelperNamespace, $systemPsr4);
|
|
$this->assertContains('plugin\system\helper\Service', $systemServices);
|
|
$this->assertNotContains($legacyHelperNamespace . 'Service', $systemServices);
|
|
|
|
$violations = [];
|
|
foreach ($this->composerFiles() as $file) {
|
|
$json = $this->jsonFile($file);
|
|
|
|
if (str_ends_with($file, '/composer.lock')) {
|
|
$packages = array_merge(
|
|
is_array($json['packages'] ?? null) ? $json['packages'] : [],
|
|
is_array($json['packages-dev'] ?? null) ? $json['packages-dev'] : []
|
|
);
|
|
|
|
foreach ($packages as $package) {
|
|
if (!is_array($package)) {
|
|
continue;
|
|
}
|
|
if (($package['name'] ?? null) === 'zoujingli/think-plugs-helper') {
|
|
$violations[] = [$file, 'package'];
|
|
}
|
|
foreach (['require', 'require-dev'] as $section) {
|
|
if (isset($package[$section]['zoujingli/think-plugs-helper'])) {
|
|
$violations[] = [$file, $section, $package['name'] ?? 'unknown'];
|
|
}
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
foreach (['require', 'require-dev'] as $section) {
|
|
if (isset($json[$section]['zoujingli/think-plugs-helper'])) {
|
|
$violations[] = [$file, $section];
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $violations, 'Legacy helper package dependencies found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
public function testLegacyViewPackagesStayRemoved(): void
|
|
{
|
|
$legacyPackages = [
|
|
'zoujingli/think-plugs-view' => 'plugin/think-plugs-view',
|
|
'zoujingli/think-plugs-system-view' => 'plugin/think-plugs-system-view',
|
|
'zoujingli/think-plugs-wechat-client-view' => 'plugin/think-plugs-wechat-client-view',
|
|
'zoujingli/think-plugs-wechat-service-view' => 'plugin/think-plugs-wechat-service-view',
|
|
];
|
|
|
|
foreach ($legacyPackages as $package => $directory) {
|
|
$this->assertDirectoryDoesNotExist($this->path($directory), $package . ' directory should stay removed');
|
|
}
|
|
|
|
$violations = [];
|
|
foreach ($this->composerFiles() as $file) {
|
|
$content = file_get_contents($file) ?: '';
|
|
foreach (array_keys($legacyPackages) as $package) {
|
|
if (strpos($content, $package) !== false) {
|
|
$violations[] = [$package, $file];
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $violations, 'Legacy view package references found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
public function testEveryLocalDependencyPointsToAnExistingWorkspacePackage(): void
|
|
{
|
|
$known = array_keys($this->packages);
|
|
$missing = [];
|
|
|
|
foreach ($this->packages as $name => $package) {
|
|
foreach ($this->localRequires($name) as $dependency) {
|
|
if (!in_array($dependency, $known, true)) {
|
|
$missing[] = [$name, $dependency];
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $missing, 'Unknown local package dependencies found: ' . json_encode($missing, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{name:string, path:string, require:array<string, string>}>
|
|
*/
|
|
private function loadPackages(): array
|
|
{
|
|
$items = [];
|
|
foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) {
|
|
$json = json_decode(file_get_contents($file) ?: '', true);
|
|
if (!is_array($json) || empty($json['name'])) {
|
|
continue;
|
|
}
|
|
$items[strval($json['name'])] = [
|
|
'name' => strval($json['name']),
|
|
'path' => $file,
|
|
'require' => is_array($json['require'] ?? null) ? $json['require'] : [],
|
|
];
|
|
}
|
|
|
|
ksort($items);
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, list<string>>
|
|
*/
|
|
private function localGraph(): array
|
|
{
|
|
$graph = [];
|
|
foreach (array_keys($this->packages) as $name) {
|
|
$graph[$name] = $this->localRequires($name);
|
|
}
|
|
return $graph;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function localRequires(string $package): array
|
|
{
|
|
$requires = array_keys($this->packages[$package]['require'] ?? []);
|
|
$locals = array_values(array_filter($requires, fn (string $name): bool => isset($this->packages[$name])));
|
|
sort($locals);
|
|
return $locals;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function composerFiles(): array
|
|
{
|
|
$files = [];
|
|
|
|
foreach (['composer.json', 'composer.lock'] as $name) {
|
|
$file = $this->path($name);
|
|
if (is_file($file)) {
|
|
$files[] = str_replace('\\', '/', $file);
|
|
}
|
|
}
|
|
|
|
foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) {
|
|
$files[] = str_replace('\\', '/', $file);
|
|
}
|
|
|
|
sort($files);
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function jsonFile(string $file): array
|
|
{
|
|
$json = json_decode(file_get_contents($file) ?: '', true);
|
|
return is_array($json) ? $json : [];
|
|
}
|
|
|
|
private function path(string $relative): string
|
|
{
|
|
return $this->projectRoot . '/' . ltrim($relative, '/');
|
|
}
|
|
}
|