Showing Posts From

PHP

ThinkPHP 5.1 老项目 + Composer 2:think-installer 装不上

维护一个老 ThinkPHP 5.1 项目,composer install 直接崩: Problem 1 - topthink/think-installer is locked to version v2.0.0 and an update of this package was not requested. - topthink/think-installer v2.0.0 requires composer-plugin-api ^1.0 -> found composer-plugin-api[2.9.0] but it does not match the constraint. Problem 2 - topthink/framework v5.1.39 requires topthink/think-installer 2.* ...顺带还有: The "https://packagist.phpcomposer.com/packages.json" file could not be downloaded (404)两个问题:Composer 版本不兼容 + 镜像源死了。 症状 1:think-installer 只支持 Composer 1 topthink/think-installer v2.0.0 明确要 composer-plugin-api ^1.0,也就是 Composer 1.x 的插件接口。你机器上是 Composer 2.x,插件接口版本变成 2.x,直接不兼容。 TP5.1 上线那会儿(2018)Composer 2 还没出,这问题在当时不存在,遗留下来才成了坑。 解法:降回 Composer 1 推荐、最省事、也最不折腾业务代码。 方法 A:Composer 自己降版本 composer self-update --1Composer 会在原地切到最新的 1.x(一般是 1.10.27)。想指定: composer self-update 1.10.27方法 B:装一个独立的 Composer 1 不想动全局 Composer 的话,单独下一份: curl -sS https://getcomposer.org/installer | php -- --1 mv composer.phar /usr/local/bin/composer1 composer1 install老项目用 composer1,新项目继续用 composer。 症状 2:packagist.phpcomposer.com 已凉 老教程里推荐的 packagist.phpcomposer.com 这个镜像已经关服多年,仍然响应 404。检查 composer.json: cat composer.json | grep -A 3 repositories看到: "repositories": { "packagist": { "type": "composer", "url": "https://packagist.phpcomposer.com" } }改成官方源或者阿里镜像: # 官方 composer config -g repo.packagist composer https://repo.packagist.org# 阿里云 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/# 清华(有时更快) composer config -g repo.packagist composer https://mirrors.tuna.tsinghua.edu.cn/composer/-g 是全局,改单个项目就去掉。 也可以直接改 composer.json 里的 repositories 字段,同样效果。 完整恢复流程 # 1. Composer 降 1 composer self-update --1# 2. 换镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/# 3. 清缓存 composer clear-cache# 4. 重装依赖 rm -rf vendor composer.lock composer install90% 的老 TP5.1 项目这一套下来就恢复了。 什么时候该升级项目 如果这不是紧急救火,考虑:升到 ThinkPHP 6 或 8,从 5.1 迁移文档官方有 或者只把 topthink/think-installer 换成 topthink/framework 的现代版本,去掉这个已废弃的插件但生产环境急救时先别动这些——先降 Composer 让站起来,稳定了再排期改造。 一句话总结 think-installer 只兼容 Composer 1,最直接的解法是 composer self-update --1。顺手把死了的 phpcomposer.com 镜像换成阿里或官方源。

ThinkPHP 5.1 / PhalApi 老项目 Composer 兼容性修复:Composer 2 报错与 composer.lock 镜像污染

部署老 PHP 项目时,composer install 报错往往是 Composer 版本与框架不兼容,或者 lock 文件里记录了失效的镜像地址。 基本部署脚本 #!/bin/bashcd /www/wwwroot/project || exit 1 git pull origin main不依赖 cd 的写法: git -C /www/wwwroot/project pull origin mainComposer 2 与 ThinkPHP 5.1 不兼容 典型错误: topthink/think-installer v2.0.0 requires composer-plugin-api ^1.0 found composer-plugin-api[2.9.0]原因:think-installer v2.0.0 只支持 Composer 1.x,而 Composer 2.x 内置的 composer-plugin-api 是 2.9.0。 确认版本: composer -V降级到 Composer 1(推荐) composer self-update --1 # 或指定版本 composer self-update 1.10.27然后: composer install如果项目目录里有 composer.phar(旧版本): php composer.phar install注意:Packagist 已于 2025-09-01 停止支持 Composer 1 对于无 composer.lock 的老项目,Composer 1 的 composer update 无法重新解析依赖,因为 Packagist 已不提供 Composer 1 格式的包数据。 如果 lock 文件还在,composer install 仍然可以按 lock 文件安装。 dev-master 是什么 "phalapi/task": "dev-master"dev-master 表示直接跟踪 Git 仓库的 master 分支最新提交,不安装正式 release 版本。需要在 composer.json 里声明: "minimum-stability": "dev"才能安装。风险:每次 composer update 拉的内容不固定,接口可能变动。GitHub 默认分支改名后,新项目改用 dev-main。 composer.lock 里的阿里云镜像污染 如果 lock 文件在其他人的阿里云镜像环境下生成,里面会固化 dist URL: "dist": { "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%" }执行 composer install 时 Composer 严格按照 lock 安装,会尝试从这个地址下载,触发: Authentication required (mirrors.aliyun.com): Username:检测 grep -n "mirrors.aliyun.com" composer.lock修复方案 方案 1:--prefer-source(推荐快速处理) php composer.phar install --prefer-source优先 git clone 源码,绕过 dist zip 下载,大多数情况能直接解决。 方案 2:重新生成 lock rm composer.lock rm -rf vendor php composer.phar clear-cache php composer.phar update重新生成后验证: grep "mirrors.aliyun.com" composer.lock应该没有任何结果。 方案 3:从仓库恢复 lock 如果 lock 是仓库管理的: git checkout composer.lock然后再用 --prefer-source 安装。 PhalApi 2.x 依赖安装成功的标志 Generating autoload files这行出现且后面没有 RuntimeException / Installation failed,说明安装成功。然后验证: php -r "require 'vendor/autoload.php'; echo 'OK';"PHP 版本建议 ThinkPHP 5.1 / ThinkCMF 5.1 / PhalApi 2.x 老项目:PHP 7.2 ~ 7.4:最稳定 PHP 8.0+:可能遇到 each()、create_function() 等已删除函数报错

用 Composer 从零搭建 PHP 框架:Router、IoC 容器、Middleware 到 ServiceProvider

自己搭一个最小 PHP 框架是理解 Laravel 架构的最好方式,比直接读 Laravel 源码有效得多。 项目结构 my-framework/ ├── app/ │ └── Controller/ │ └── HomeController.php ├── public/ │ └── index.php ├── src/ │ ├── Router.php │ ├── Container.php │ └── Middleware.php ├── vendor/ └── composer.json第一步:Composer 初始化 mkdir my-framework && cd my-framework composer initcomposer.json: { "name": "me/framework", "autoload": { "psr-4": { "App\\": "app/", "Framework\\": "src/" } } }生成自动加载文件: composer dump-autoload第二步:最小 Router src/Router.php: <?php namespace Framework;class Router { private array $routes = []; public function get(string $uri, $action): void { $this->routes['GET'][$uri] = $action; } public function post(string $uri, $action): void { $this->routes['POST'][$uri] = $action; } public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if (isset($this->routes[$method][$uri])) { call_user_func($this->routes[$method][$uri]); return; } http_response_code(404); echo "404 Not Found"; } }入口文件 public/index.php: <?php require '../vendor/autoload.php';use Framework\Router; use App\Controller\HomeController;$router = new Router(); $router->get('/', [HomeController::class, 'index']); $router->dispatch();启动: php -S localhost:8000 -t public第三步:IoC 容器(依赖注入) Laravel 的核心是 Container,不是 Router。 src/Container.php: <?php namespace Framework;use ReflectionClass;class Container { private array $bindings = []; public function bind(string $abstract, callable $factory): void { $this->bindings[$abstract] = $factory; } public function make(string $class): object { if (isset($this->bindings[$class])) { return ($this->bindings[$class])($this); } // 用反射自动解析构造函数依赖 $ref = new ReflectionClass($class); $constructor = $ref->getConstructor(); if (!$constructor) { return new $class(); } $params = array_map( fn($p) => $this->make($p->getType()->getName()), $constructor->getParameters() ); return $ref->newInstanceArgs($params); } }这样 $container->make(HomeController::class) 会自动实例化其所有依赖: class HomeController { public function __construct( private UserService $userService ) {} }第四步:Middleware 请求流程:Request → Middleware → Controller → Response <?php namespace Framework;class Middleware { private array $stack = []; public function add(callable $middleware): void { $this->stack[] = $middleware; } public function run(callable $handler): void { $runner = array_reduce( array_reverse($this->stack), fn($next, $mw) => fn() => $mw($next), $handler ); $runner(); } }示例认证中间件: $middleware->add(function (callable $next) { if (!isset($_SESSION['user'])) { http_response_code(401); echo '请先登录'; return; } $next(); });第五步:ServiceProvider <?php namespace Framework;abstract class ServiceProvider { public function __construct( protected Container $app ) {} abstract public function register(): void; }示例: class DatabaseServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('db', fn($app) => new Database( config('database.host'), config('database.name') )); } }注册: (new DatabaseServiceProvider($container))->register();学习路线Router + 自动加载 → 理解 HTTP 分发 Container + ReflectionClass → 理解依赖注入 Middleware → 理解 Laravel Pipeline ServiceProvider → 理解 Laravel 服务注册 自己实现后再看 Laravel 源码,对照理解完成前 3 步后基本就能看懂 Laravel 入口流程了。

用 Composer 从零搭一个 PHP mini 框架:路由 + PSR-4 + 容器

想理解 Laravel、Symfony 是怎么跑起来的,别一头扎进它们的源码——十几万行随便一个 ServiceProvider 就把人绕晕。自己用 Composer 手写一个 100 行的 mini 框架,把路由、自动加载、IoC 弄明白,再去看大框架就轻松了。 目录结构 my-framework/ ├── app/ │ └── Controller/ │ └── HomeController.php ├── public/ │ └── index.php ├── src/ │ ├── Router.php │ ├── Container.php │ └── Response.php ├── vendor/ # composer install 后生成 └── composer.jsonapp/ 是业务代码、src/ 是框架核心、public/ 是入口。 composer.json 与 PSR-4 mkdir my-framework && cd my-framework composer init改 composer.json: { "name": "you/mini-framework", "type": "project", "require": { "php": ">=8.1" }, "autoload": { "psr-4": { "App\\": "app/", "Framework\\": "src/" } } }生成自动加载器: composer dump-autoloadPSR-4 的规则很简单:类的命名空间路径 = 文件相对路径。App\Controller\HomeController 就在 app/Controller/HomeController.php。 Router:50 行的路由 src/Router.php: <?php namespace Framework;class Router { private array $routes = []; public function get(string $path, callable|array $handler): void { $this->routes['GET'][$path] = $handler; } public function post(string $path, callable|array $handler): void { $this->routes['POST'][$path] = $handler; } public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $handler = $this->routes[$method][$path] ?? null; if (!$handler) { http_response_code(404); echo "404 Not Found"; return; } // 支持 [Controller::class, 'method'] if (is_array($handler)) { [$class, $method] = $handler; $instance = new $class(); echo $instance->$method(); return; } // 支持匿名函数 echo $handler(); } }入口 public/index.php: <?php require __DIR__ . '/../vendor/autoload.php';use Framework\Router; use App\Controller\HomeController;$router = new Router();$router->get('/', fn() => "Hello Framework"); $router->get('/home', [HomeController::class, 'index']);$router->dispatch();Controller: <?php namespace App\Controller;class HomeController { public function index(): string { return "home page"; } }跑起来: php -S localhost:8000 -t public访问 http://localhost:8000/ 和 /home 都能看到内容。 到这里,Laravel 最核心的三件事你都实现了:Composer PSR-4 加载、路由、控制器分发。 第二步:加个 IoC 容器 Laravel 的灵魂其实不是路由,是容器(Container / Service Container)。它做两件事:帮你 new 对象,自动注入构造参数 让你把接口绑定到具体实现,实现依赖倒置最简版本: <?php namespace Framework;use ReflectionClass;class Container { private array $bindings = []; public function bind(string $abstract, callable|string $concrete): void { $this->bindings[$abstract] = $concrete; } public function make(string $class): object { // 1. 有绑定就走绑定 if (isset($this->bindings[$class])) { $c = $this->bindings[$class]; return is_callable($c) ? $c($this) : $this->make($c); } // 2. 反射构造函数依赖,递归 make $ref = new ReflectionClass($class); $ctor = $ref->getConstructor(); if (!$ctor) return new $class(); $args = []; foreach ($ctor->getParameters() as $p) { $type = $p->getType()?->getName(); $args[] = $type ? $this->make($type) : null; } return $ref->newInstanceArgs($args); } }用法: class Logger { public function log(string $msg): void { file_put_contents('log', $msg); } }class UserService { public function __construct(public Logger $logger) {} }$c = new Container(); $user = $c->make(UserService::class); // 自动注入 Logger $user->logger->log("hello");这 40 行代码就是 Laravel Container 的核心思路。真正的 Laravel 加了:方法注入、上下文绑定、单例、tag、event、缓存反射等等,本质还是这个。 后续可以往上加 按需实现,每加一个都是一次深度理解:Middleware — 前后置拦截 Request/Response 对象 View 层(模板引擎或 Blade 简版) ORM(先用 PDO 封个 Query Builder) Event / Listener CLI 命令(symfony/console 或自己写) Config 加载 环境变量(vlucas/phpdotenv)一步步加,你会发现 Laravel 每个模块都能对应上。 一句话总结 Composer PSR-4 + Router + Container,三件套加起来 100 行代码,就是一个能跑的 mini 框架。学 Laravel 前先自己写一遍,再去看源码会很轻松。

Laravel + ClickHouse 统计数据推送飞书卡片

用 Laravel 定时任务对接 ClickHouse 日志,每小时/每天推送应用流量报告到飞书群。 核心架构 ClickHouse (request_logs) → Laravel Artisan Command → 飞书 Webhook → 交互式卡片定时任务两种模式:push:feishu-stats — 每小时统计近 1h / 3h 流量 push:feishu-stats daily — 每天 0 点统计近 24h / 3d / 7dArtisan Command <?phpnamespace App\Console\Commands;use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Carbon\Carbon; use App\Services\ClickHouseService;class PushFeishuStats extends Command { protected $signature = 'push:feishu-stats {type=hourly}'; protected $description = '推送应用访问统计数据到飞书群'; protected ClickHouseService $clickHouseService; public function __construct(ClickHouseService $clickHouseService) { parent::__construct(); $this->clickHouseService = $clickHouseService; } public function handle() { $type = $this->argument('type'); try { $type === 'daily' ? $this->handleDailyPush() : $this->handleHourlyPush(); return Command::SUCCESS; } catch (\Throwable $e) { $this->error($e->getMessage()); return Command::FAILURE; } } protected function handleHourlyPush() { $now = Carbon::now(); $oneHourAgo = $now->copy()->subHour()->toDateTimeString(); $threeHoursAgo = $now->copy()->subHours(3)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneHourAgo}') AS count_1h, COUNT(1) AS count_3h FROM request_logs WHERE timestamp >= '{$threeHoursAgo}' GROUP BY appid HAVING count_3h > 10 ORDER BY count_3h DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📊 每小时应用流量监控', $rows, 'hourly'); } protected function handleDailyPush() { $now = Carbon::now(); $oneDayAgo = $now->copy()->subDay()->toDateTimeString(); $threeDaysAgo = $now->copy()->subDays(3)->toDateTimeString(); $sevenDaysAgo = $now->copy()->subDays(7)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneDayAgo}') AS count_24h, COUNTIf(timestamp >= '{$threeDaysAgo}') AS count_3d, COUNT(1) AS count_7d FROM request_logs WHERE timestamp >= '{$sevenDaysAgo}' GROUP BY appid HAVING count_7d > 50 ORDER BY count_7d DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📅 每日应用流量大盘', $rows, 'daily'); } protected function query(string $sql): array { return $this->clickHouseService->getClient()->select($sql)->rows(); } protected function sendToFeishu(string $title, array $rows, string $type = 'hourly') { $webhookUrl = env('FEISHU_WEBHOOK'); if (!$webhookUrl) { $this->error('未配置 FEISHU_WEBHOOK'); return; } $elements = [ ['tag' => 'markdown', 'content' => '**🕒 生成时间:** ' . now()->format('Y-m-d H:i:s')], ['tag' => 'markdown', 'content' => '**📈 应用数量:** ' . count($rows)], ['tag' => 'hr'], ]; foreach ($rows as $index => $row) { $rank = $index + 1; $appid = $row['appid'] ?: 'unknown'; if ($type === 'hourly') { $count1h = (int) $row['count_1h']; $count3h = (int) $row['count_3h']; $avg = max(1, $count3h / 3); $rate = round(($count1h / $avg) * 100); $trend = '🟢'; $status = '正常'; if ($rate >= 200) { $trend = '🚨'; $status = '流量暴涨'; } elseif ($rate >= 150) { $trend = '🟡'; $status = '流量升高'; } elseif ($rate <= 50) { $trend = '🔵'; $status = '流量下降'; } $content = "### {$trend} TOP {$rank} · {$appid}\n\n> {$status}\n\n" . "- 1小时请求量:**" . number_format($count1h) . "**\n" . "- 3小时请求量:**" . number_format($count3h) . "**\n" . "- 当前热度:**{$rate}%**"; } else { $count24h = (int) $row['count_24h']; $count3d = (int) $row['count_3d']; $count7d = (int) $row['count_7d']; $avg = max(1, $count7d / 7); $rate = round(($count24h / $avg) * 100); $trend = $rate >= 180 ? '🚨' : ($rate >= 130 ? '🟡' : '🟢'); $content = "### {$trend} TOP {$rank} · {$appid}\n\n" . "- 24小时请求量:**" . number_format($count24h) . "**\n" . "- 近3天:**" . number_format($count3d) . "**\n" . "- 近7天:**" . number_format($count7d) . "**\n" . "- 当前热度:**{$rate}%**"; } $elements[] = ['tag' => 'markdown', 'content' => $content]; $elements[] = ['tag' => 'hr']; } $elements[] = ['tag' => 'markdown', 'content' => '⚡ Powered By Laravel + ClickHouse']; $payload = [ 'msg_type' => 'interactive', 'card' => [ 'config' => ['wide_screen_mode' => true], 'header' => [ 'title' => ['tag' => 'plain_text', 'content' => $title], 'template' => 'turquoise', ], 'elements' => $elements, ], ]; $response = Http::timeout(10)->post($webhookUrl, $payload); $response->successful() ? $this->info('飞书推送成功') : $this->error('飞书推送失败: ' . $response->body()); } }注册定时任务 // routes/console.php Schedule::command('push:feishu-stats')->hourly(); Schedule::command('push:feishu-stats daily')->dailyAt('00:00');.env 配置 FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxClickHouse 性能建议 当前用 JSONExtractString(request_params, 'appid') 实时解析 JSON,数据量大时 CPU 开销高。建议在 request_logs 表冗余一个 appid String 字段,插入时直接写入,便于索引和 GROUP BY 加速。 飞书卡片效果 📊 每小时应用流量监控 🕒 生成时间:2026-05-28 10:00:00 📈 应用数量:15🚨 TOP 1 · app_main > 流量暴涨 • 1小时请求量:23,412 • 3小时请求量:41,221 • 当前热度:171%🟢 TOP 2 · app_test • 1小时请求量:1,242 • 当前热度:119%比 Markdown 表格可读性高很多,支持趋势颜色标识。