diff --git a/.gitignore b/.gitignore index 92bf9b0..224ad2d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ /windows.php /bootstrap.php /composer.lock +.env_copy +/public/excel \ No newline at end of file diff --git a/app/command/InquiryAssignInquiryCommand.php b/app/command/InquiryAssignInquiryCommand.php new file mode 100644 index 0000000..adb8309 --- /dev/null +++ b/app/command/InquiryAssignInquiryCommand.php @@ -0,0 +1,161 @@ + $v > 0 + )); + + if (empty($userIds)) { + throw new BusinessException('SALES_USER_IDS 业务员列表不能为空,请先在命令中配置.'); + } + + // 取出“待分配”的询盘(你可以根据自己系统调整条件) + // 当前策略:status = 0 且 recipient = 0 的视为未分配 + $inquiries = Db::table('inquiry_log') + ->where('enter_user_id', 186) + ->where('channel', 10) + ->where('status', 0) + ->where('recipient', 0) + ->where('id', '>', 34426) + ->orderBy('id') + ->limit(static::BATCH_LIMIT) + ->get(); + + if ($inquiries->isEmpty()) { + $output->writeln('没有需要分配的询盘(status=0 且 recipient=0)'); + return self::SUCCESS; + } + + $now = time(); + $count = 0; + + foreach ($inquiries as $row) { + $inquiryId = (int)$row->id; + sleep(1); + // 轮询取出一个业务员 ID + $recipientUserId = $this->nextRecipientId($userIds, static::ROUND_ROBIN_GROUP_KEY); + + // 对每一条记录单独事务,避免单条异常拖垮全部 + Db::transaction(function () use ($inquiryId, $recipientUserId, $now, $output, &$count) { + // 1)更新 inquiry_log + $affected = Db::table('inquiry_log') + ->where('id', $inquiryId) + ->update([ + 'recipient' => $recipientUserId, + 'status' => 2, + 'r_time' => $now, + 't_time' => $now, + 'updated_at' => $now, + ]); + + if ($affected === 0) { + // 被其他进程抢先更新也有可能,这里只打印提示不中断整个批次 + $output->writeln("询盘 {$inquiryId} 未更新(可能已被处理)"); + return; + } + + // 2)构造队列 payload(后续 CRM 消费时至少知道 ID) + $res = [ + 'id' => $inquiryId, + 'recipient' => $recipientUserId, + 'dispatched_at' => $now, + 'source' => 'fb_leads_inquiry', + ]; + + // 3)推送到 CRM 队列 + RedisQueueRedis::send('inquiry-to-crm-new', $res); + + $count++; + $output->writeln("已分配询盘 {$inquiryId} 给业务员 {$recipientUserId},并推送到队列"); + }); + } + + $output->writeln("本次共分配成功 {$count} 条询盘"); + + return self::SUCCESS; + } + + /** + * 轮询获取“下一位业务员 ID” + * + * @param int[] $userIds + * @param string $groupKey + * @return int + * @throws BusinessException + */ + protected function nextRecipientId(array $userIds, string $groupKey = 'default'): int + { + $userIds = array_values(array_filter(array_map('intval', $userIds), fn($v) => $v > 0)); + if (empty($userIds)) { + throw new BusinessException('轮询分配失败:业务员列表为空'); + } + + $count = count($userIds); + $key = 'inquiry:assign:round_robin:' . $groupKey; + + // Redis INCR 是原子操作,适合并发下使用 + $val = Redis::incr($key); // 第一次会从 0 变成 1 + + // 换算为从 0 开始的下标 + $index = ($val - 1) % $count; + + return $userIds[$index]; + } +} diff --git a/app/command/InquiryFbLeadCommand.php b/app/command/InquiryFbLeadCommand.php new file mode 100644 index 0000000..aec2199 --- /dev/null +++ b/app/command/InquiryFbLeadCommand.php @@ -0,0 +1,164 @@ +where('sync_status', 0) + ->get(); + + if ($leads->isEmpty()) { + $output->writeln('没有需要同步的 fb_leads(sync_status = 0)'); + return self::SUCCESS; + } + + $now = time(); + + Db::transaction(function () use ($leads, $now, $output) { + $insert = []; + $ids = []; + + foreach ($leads as $lead) { + $leadArr = (array)$lead; + $insert[] = $this->mapLeadToInquiry($leadArr, $now); + // 假设 fb_leads 的主键字段名为 id + $ids[] = $leadArr['id']; + } + + if ($insert) { + // 1) 写入 inquiry_log + Db::table('inquiry_log')->insert($insert); + + // 2) 回写 fb_leads 的同步状态 + Db::table('fb_leads') + ->whereIn('id', $ids) + ->update([ + 'sync_status' => 1, + 'updated_at' => $now, + ]); + + $output->writeln('已导入 ' . count($insert) . ' 条数据到 inquiry_log,并更新 fb_leads.sync_status = 1'); + } + }); + + return self::SUCCESS; + } + + /** + * 将一条 fb_leads 记录映射为 inquiry_log 记录 + */ + protected function mapLeadToInquiry(array $lead, int $now): array + { + // 联系方式 + $email = (string)($lead['email'] ?? ''); + $tel = (string)($lead['phone'] ?? ''); + $wa = $tel; // 先用电话号码当 WhatsApp + + // 根据电话或 WhatsApp 区号识别国家 + $countryId = PhoneCountryHelper::detectCountryId($tel ?: $wa); + + // 所有默认值按你的要求设置 + $data = [ + 'order_no' => $this->orderNum(), + 'enter_user_id' => static::DEFAULT_ENTER_USER_ID, + 'recipient' => 0, + 'quality' => 2, + 'channel' => static::DEFAULT_CHANNEL, + + 'email' => $email, + 'tel' => $tel, + 'whatsapp' => $wa, + 'country' => $countryId ?: 0, + 'contacts' => (string)($lead['full_name'] ?? ''), + + 'content' => $this->buildContentHtml($lead), + + 'status' => 0, + 'r_time' => 0, + 't_time' => 0, + 'sync_crm' => 0, + + 'created_at' => $now, + 'updated_at' => $now, + ]; + + return $data; + } + + /** + * 生成订单号 + */ + protected function orderNum(): string + { + return 'SN' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); + } + + /** + * 根据 fb_leads 的字段拼出富文本内容 + */ + protected function buildContentHtml(array $lead): string + { + $e = function ($v) { + return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + }; + + $lines = []; + + if (!empty($lead['car_model'])) { + $lines[] = '

意向车型:' . $e($lead['car_model']) . '

'; + } + if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) { + $qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? ''; + $lines[] = '

意向数量:' . $e($qty) . '

'; + } + if (!empty($lead['favorite_color'])) { + $lines[] = '

意向颜色:' . $e($lead['favorite_color']) . '

'; + } + if (!empty($lead['usage_type'])) { + $lines[] = '

用途:' . $e($lead['usage_type']) . '

'; + } + if (!empty($lead['seat_preference'])) { + $lines[] = '

座位数偏好:' . $e($lead['seat_preference']) . '

'; + } + + $lines[] = '
'; + $lines[] = '

广告与表单信息:

'; + $lines[] = '

平台:' . $e($lead['platform'] ?? '') . '

'; + $lines[] = '

Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')

'; + $lines[] = '

Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')

'; + $lines[] = '

Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')

'; + $lines[] = '

Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')

'; + + $lines[] = '
'; + $lines[] = '

Meta 信息:

'; + $lines[] = '

Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '

'; + $lines[] = '

FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '

'; + + if (!empty($lead['inbox_url'])) { + $url = $e($lead['inbox_url']); + $lines[] = '

Inbox 链接:' . $url . '

'; + } + + return implode("\n", $lines); + } +} diff --git a/app/command/InquiryImportFbCommand.php b/app/command/InquiryImportFbCommand.php new file mode 100644 index 0000000..3c9a3c0 --- /dev/null +++ b/app/command/InquiryImportFbCommand.php @@ -0,0 +1,256 @@ +addOption( + 'file', + null, + InputOption::VALUE_OPTIONAL, + '要导入的单个 Excel 文件名(相对 public/excel 的路径,或绝对路径)' + ) + ->addOption( + 'dir', + null, + InputOption::VALUE_OPTIONAL, + 'Excel 目录(相对 public/ 的路径)', + 'excel' // 默认就是 public/excel + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $fileOption = $input->getOption('file'); + $dirOption = $input->getOption('dir') ?? 'excel'; + + // 基准目录:public/{$dirOption} + $baseDir = rtrim(public_path($dirOption), DIRECTORY_SEPARATOR); + + if ($fileOption) { + // 如果传的是绝对路径,直接用;否则认为是相对 baseDir 的文件名 + if ($this->isAbsolutePath($fileOption)) { + $filePath = $fileOption; + } else { + $filePath = $baseDir . DIRECTORY_SEPARATOR . $fileOption; + } + + $output->writeln("开始导入单个文件: {$filePath}"); + + $this->importOneFile($filePath, $output); + } else { + // 不指定 file 时,默认导入目录下所有 .xlsx + if (!is_dir($baseDir)) { + throw new BusinessException("目录不存在:{$baseDir}"); + } + + $pattern = $baseDir . DIRECTORY_SEPARATOR . '*.xlsx'; + $files = glob($pattern); + + if (empty($files)) { + $output->writeln("目录 {$baseDir} 下没有找到 .xlsx 文件"); + return self::SUCCESS; + } + + $output->writeln("开始批量导入目录: {$baseDir}"); + foreach ($files as $filePath) { + $output->writeln(" -> 文件:{$filePath}"); + try { + $this->importOneFile($filePath, $output); + $output->writeln(" 导入成功"); + } catch (\Throwable $e) { + // 某个文件失败,不影响后面的文件 + $output->writeln(" 导入失败:{$e->getMessage()}"); + } + } + } + + return self::SUCCESS; + } + + /** + * 导入单个 Excel 文件 + */ + protected function importOneFile(string $filePath, OutputInterface $output): void + { + if (!is_file($filePath)) { + throw new BusinessException("文件不存在:{$filePath}"); + } + + $spreadsheet = IOFactory::load($filePath); + $sheet = $spreadsheet->getActiveSheet(); + $rows = $sheet->toArray(null, true, true, true); + + if (empty($rows)) { + throw new BusinessException('Excel 内容为空:' . $filePath); + } + + // 取第一行表头 + $headerRow = array_shift($rows); + + // 表头映射器 + $mapper = new FbLeadHeaderMapper(); + + // 如果有未知表头,这里会直接抛异常中断当前文件 + $colToField = $mapper->buildColumnFieldMap($headerRow); + + $insertData = []; + $now = time(); + + foreach ($rows as $row) { + $item = []; + + foreach ($colToField as $colIndex => $field) { + $raw = $row[$colIndex] ?? null; + + switch ($field) { + // 1) is_organic: Excel 里是 true/false,这里转成 0/1 + case 'is_organic': + $item['is_organic'] = $this->normalizeIsOrganic($raw); + break; + + // 2) fb_created_time: 统一转成 Y-m-d H:i:s + case 'fb_created_time': + $item['fb_created_time'] = $this->normalizeFbCreatedTime($raw); + break; + + default: + if (is_string($raw)) { + $raw = trim($raw); + } + $item[$field] = $raw; + break; + } + } + + // 补充公共字段 + $item['source_file'] = basename($filePath); + $item['created_at'] = $now; + $item['updated_at'] = $now; + + // 整行都空就跳过 + $allNull = true; + foreach ($item as $v) { + if ($v !== null && $v !== '') { + $allNull = false; + break; + } + } + if ($allNull) { + continue; + } + + $insertData[] = $item; + } + + if ($insertData) { + Db::table('fb_leads')->insert($insertData); + } else { + $output->writeln(" 文件 {$filePath} 没有有效数据行"); + } + } + + /** + * 将 Excel 里的 is_organic 值转换为 0/1 + */ + protected function normalizeIsOrganic($value): int + { + if ($value === null) { + return 0; + } + + $v = strtolower(trim((string)$value)); + + // 认为这些是 “true” + $trueValues = ['1', 'true', 'yes', 'y']; + + return in_array($v, $trueValues, true) ? 1 : 0; + } + + /** + * 统一处理 fb_created_time + * Excel 目前是类似 '12/8/25',这里尽量转成 'Y-m-d H:i:s' + * 解析失败时返回 null + */ + protected function normalizeFbCreatedTime($value): ?string + { + if ($value === null || $value === '') { + return null; + } + + if ($value instanceof \DateTimeInterface) { + return $value->format('Y-m-d H:i:s'); + } + + $v = trim((string)$value); + + $formats = [ + 'Y-m-d H:i:s', + 'Y-m-d', + 'n/j/y', // 12/8/25 + 'n/j/Y', + 'd/m/Y', + 'd.m.Y', + ]; + + foreach ($formats as $fmt) { + $dt = \DateTime::createFromFormat($fmt, $v); + if ($dt !== false) { + return $dt->format('Y-m-d H:i:s'); + } + } + + // 纯数字当时间戳试一下 + if (is_numeric($v) && strlen($v) >= 9 && strlen($v) <= 11) { + $ts = (int)$v; + if ($ts > 0) { + return date('Y-m-d H:i:s', $ts); + } + } + + return null; + } + + /** + * 判断是否为绝对路径(简单兼容 Windows / Linux) + */ + protected function isAbsolutePath(string $path): bool + { + // Windows: X:\ 或 \\server\share + if (preg_match('/^[a-zA-Z]:\\\\/', $path) || str_starts_with($path, '\\\\')) { + return true; + } + + // *nix: / 开头 + return str_starts_with($path, '/'); + } +} + +/* + +php webman tb-db +# 等价于 php webman tb-db --dir=excel +# 会遍历 public/excel/*.xlsx 逐个导入 + +# 相对路径(相对 public/excel) +php webman tb-db --file="BYD tang_Leads_2025-12-04_2025-12-09.xlsx" + +# 或给绝对路径 +php webman tb-db --file="/www/wwwroot/xxx/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx" + +*/ \ No newline at end of file diff --git a/app/command/VehMoneyCommand.php b/app/command/VehMoneyCommand.php new file mode 100644 index 0000000..0485e09 --- /dev/null +++ b/app/command/VehMoneyCommand.php @@ -0,0 +1,29 @@ +service = new UsedcarCrawlResultsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/UsedcarCrawlResultsImagesController.php b/app/common/controller/UsedcarCrawlResultsImagesController.php new file mode 100644 index 0000000..5560fda --- /dev/null +++ b/app/common/controller/UsedcarCrawlResultsImagesController.php @@ -0,0 +1,33 @@ +service = new UsedcarCrawlResultsImagesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/UsedcarCrawlResultsImagesLogic.php b/app/common/logic/UsedcarCrawlResultsImagesLogic.php new file mode 100644 index 0000000..428bc16 --- /dev/null +++ b/app/common/logic/UsedcarCrawlResultsImagesLogic.php @@ -0,0 +1,17 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/UsedcarCrawlResultsService.php b/app/common/service/UsedcarCrawlResultsService.php new file mode 100644 index 0000000..5aa1cdd --- /dev/null +++ b/app/common/service/UsedcarCrawlResultsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/utils/crawl/service/UsedCarCrawl.php b/app/common/utils/crawl/service/UsedCarCrawl.php new file mode 100644 index 0000000..7e99090 --- /dev/null +++ b/app/common/utils/crawl/service/UsedCarCrawl.php @@ -0,0 +1,857 @@ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/', + ]; + + // Redis队列键名 + private const REDIS_QUEUE_KEY = 'usedcar:urls'; + + // 能源类型常量 + public const ENERGY_TYPE_RENEWABLE = 'RE'; + public const ENERGY_TYPE_CONVENTIONAL = 'CE'; + public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN'; + + /** + * 插入队列url + * + * @param string $url URL地址 + * @param string $scene 场景:usedcar(二手信息) config(配置信息) + * @param int $id 记录ID + * @return int + */ + public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int + { + $data = [ + 'scene' => $scene, + 'url' => $url, + 'id' => $id, + ]; + + return Redis::lpush( + self::REDIS_QUEUE_KEY, + json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } + + /** + * 获取队列url + * + * @return array|null + * @throws BusinessException + */ + public function popUrl(): ?array + { + $data = Redis::rPop(self::REDIS_QUEUE_KEY); + if (!$data) { + return null; + } + + $task = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($task)) { + throw new BusinessException('任务数据解析失败:' . json_last_error_msg()); + } + + // 基本字段校验 + if (empty($task['url']) || empty($task['scene'])) { + throw new BusinessException('队列任务数据缺少必要字段:url 或 scene'); + } + + return $task; + } + + /** + * 执行抓取任务 + * + * @return bool + * @throws BusinessException + */ + public function execute(): bool + { + try { + $task = $this->popUrl(); + + if (!$task) { + // 队列为空,任务结束 + return false; + } + + $url = (string)$task['url']; + $scene = (string)$task['scene']; + $id = (int)($task['id'] ?? 0); + + $html = $this->fetchHtml($url); + if ($html === '') { + throw new BusinessException("页面内容为空: {$url}"); + } + + switch ($scene) { + case 'usedcar': + $info = $this->parseUsedCarDetail($html, $url); + $insertId = $this->saveUsedCarBasicInfo($info); + + // 下载图片 + 上传 + 入库(图片失败不影响主记录) + $imagesArray = json_decode($info['images'] ?? '[]', true); + if (json_last_error() !== JSON_ERROR_NONE) { + $imagesArray = []; + } + $saveDir = public_path('uploads/usedcar/' . $insertId); + $this->uploadAndRemove($imagesArray, $saveDir, $info['used_car_id'], $insertId); + + // 继续派发配置页任务 + if (!empty($info['car_id_url'])) { + $this->pushUrl($info['car_id_url'], 'config', $insertId); + } + break; + + case 'config': + if ($id <= 0) { + throw new BusinessException("配置任务缺少主记录ID"); + } + + $data = $this->parseConfigDetail($html); + $this->saveUsedCarConfigInfo($data, $id); + break; + + default: + throw new BusinessException("未知的任务场景: " . $scene); + } + + return true; + } catch (GuzzleException $e) { + throw new BusinessException("网络请求失败: " . $e->getMessage()); + } catch (BusinessException $e) { + // 业务异常原样抛出,方便上层知道是逻辑问题 + throw $e; + } catch (Throwable $th) { + // 兜底异常,避免 worker 直接挂掉 + throw new BusinessException( + "执行任务出错: " . $th->getMessage() + . ' | 行号: ' . $th->getLine() + . ' | 文件: ' . $th->getFile() + ); + } + } + + /** + * 获取网页HTML内容 + * + * @param string $url + * @return string + * @throws GuzzleException + */ + public function fetchHtml(string $url): string + { + $client = new Client(); + $response = $client->request('GET', $url, [ + 'headers' => self::DEFAULT_HEADERS, + 'timeout' => 10, + ]); + + return (string)$response->getBody(); + } + + /** + * 解析二手车详情页信息 + * + * @param string $html + * @param string $url + * @return array + * @throws BusinessException + */ + public function parseUsedCarDetail(string $html = '', string $url = ''): array + { + try { + $ql = QueryList::html($html); + } catch (Throwable $e) { + throw new BusinessException('解析 HTML 失败(详情页): ' . $e->getMessage()); + } + + $title = trim($ql->find('h1')->text()); + $price = trim($ql->find('.head-info_price-wrap__Y4bxi > p')->text()); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + if ($jsonText === '') { + throw new BusinessException('详情页未找到 __NEXT_DATA__ JSON'); + } + + $jsonArray = $this->decodeJson($jsonText, '详情页 __NEXT_DATA__'); + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID / 年份 + $carInfo = $jsonArray['props']['pageProps']['skuDetail']['car_info'] ?? []; + $carId = $carInfo['car_id'] ?? 0; + $year = (int)($carInfo['year'] ?? 0); + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型(两个字段择一) + $overview = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview'] ?? []; + $fuelForm = $overview['power']['fuel_form'] ?? ''; + $newFuelForm = $overview['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = $importantText ? explode(' | ', $importantText) : []; + $usageMileage = ''; + if (isset($importantTextArray[1])) { + $usageMileage = str_replace('万公里', '', $importantTextArray[1]); + } + + // 车辆名称 品牌、车系、车型 + $brandName = $carInfo['brand_name'] ?? ''; + $seriesName = $carInfo['series_name'] ?? ''; + $carName = $carInfo['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '', + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + $displacement = $this->getParamValue($otherParams, '排量'); + $gearbox = $this->getParamValue($otherParams, '变速箱'); + + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox, + ]; + + // 提取新车指导价 / 比新车省金额 + $newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u'); + $priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u'); + + // 提取车况介绍 + $conditionDesc = ''; + if ($pageText && preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + + return [ + 'used_car_id' => $this->extractIdFromUrl($url), + 'car_id' => $carId, + 'url' => $url, + 'title' => $title, + 'price' => str_replace('万', '', $this->decodePrice($price)), + 'new_price' => str_replace('万', '', $newPrice), + 'price_difference' => str_replace('万', '', $priceDifference), + 'condition_desc' => $conditionDesc, + 'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'car_id_url' => $carIdUrl, + 'used_detail' => $jsonText, + 'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'fuel_form' => $resultFuelForm, + 'classify_energy' => $this->detectEnergyType($resultFuelForm), + 'usage_mileage' => $usageMileage, + 'brand_name' => $brandName, + 'series_name' => $seriesName, + 'car_name' => $carName, + 'year' => $year, + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ]; + } + + /** + * 解析配置详情页信息 + * + * @param string $html + * @return array + * @throws BusinessException + */ + public function parseConfigDetail(string $html = ''): array + { + try { + $ql = QueryList::html($html); + } catch (Throwable $e) { + throw new BusinessException('解析 HTML 失败(配置页): ' . $e->getMessage()); + } + + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + if ($jsonText === '') { + throw new BusinessException('配置页未找到 __NEXT_DATA__ JSON'); + } + + $raw = $this->decodeJson($jsonText, '配置页 __NEXT_DATA__'); + + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMap($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + return [ + 'format_car_info' => [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), // 电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), // 电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage'), // 续航里程 + ], + 'groups' => $groups, + ]; + } + + /** + * 从车辆配置中获取能源类型 + * + * @param array $data JSON数据 + * @return string|null 能源类型 + */ + public function getFuelFormByConfig(array $data): ?string + { + if (empty($data[0]['items']) || !is_array($data[0]['items'])) { + return null; + } + + foreach ($data[0]['items'] as $item) { + if (isset($item['key']) && $item['key'] === 'fuel_form') { + return $item['value'] ?? null; + } + } + + return null; + } + + /** + * 从数据中根据字段key获取值 + * + * @param mixed $data 数据 + * @param string $field 字段名 + * @return string|null + */ + public function getFieldValue($data, string $field): ?string + { + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + } + + if (!is_array($data)) { + return null; + } + + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (($item['key'] ?? null) === $field) { + return $item['value'] ?? null; + } + + if (!empty($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as $child) { + if (($child['key'] ?? null) === $field) { + return $child['value'] ?? null; + } + } + } + } + } + } + + return null; + } + + /** + * 从参数列表中获取指定名称的值 + */ + public function getParamValue(array $params, string $name): ?string + { + foreach ($params as $item) { + if (($item['name'] ?? null) === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 判断车辆能源类型 + */ + public function detectEnergyType(string $text): string + { + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + + // 明确定义的新能源关键字 + $newEnergyKeywords = [ + '纯电', '纯电动', '电动车', 'BEV', + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + '增程式', '增程', 'REEV', + '氢能', '氢燃料', '燃料电池', 'FCEV', + '太阳能', + ]; + + // 传统能源关键字 + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', + '双擎', + 'HEV', + ]; + + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_RENEWABLE; + } + } + + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + return self::ENERGY_TYPE_UNKNOWN; + } + + /** + * 保存二手车基本信息到数据库 + * + * @param array $data 数据 + * @return int 插入记录ID + * @throws BusinessException + */ + public function saveUsedCarBasicInfo(array $data): int + { + try { + $time = time(); + $data['created_at'] = $data['created_at'] ?? $time; + $data['updated_at'] = $time; + + return (int)Db::table('usedcar_crawl_results')->insertGetId($data); + } catch (Throwable $e) { + throw new BusinessException('保存二手车基础信息失败:' . $e->getMessage()); + } + } + + /** + * 更新配置信息到数据库 + * + * @param array $data 配置数据 + * @param int $id 记录ID + * @return bool|int + * @throws BusinessException + */ + public function saveUsedCarConfigInfo(array $data, int $id) + { + try { + $record = Db::table('usedcar_crawl_results')->find($id); + + if (!$record) { + // 找不到记录就算了,不抛致命错误,视业务情况也可以抛异常 + return false; + } + + $groups = $data['groups'] ?? []; + $oldFormat = []; + + if (!empty($record->format_car_info)) { + $decoded = json_decode($record->format_car_info, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $oldFormat = $decoded; + } + } + + $formatCarInfo = array_merge( + $oldFormat, + $data['format_car_info'] ?? [] + ); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'updated_at' => time(), + ]); + } catch (Throwable $e) { + throw new BusinessException('保存车辆配置信息失败:' . $e->getMessage()); + } + } + + /** + * 构建值映射表 + */ + public function buildValuesMap(array $carInfo): array + { + $valuesMap = []; + + // 顶层字段 + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // info中的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 构建车辆规格分组 + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // 分类字段:type = 0 / 2 + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // 普通字段:挂到最近的分类下 + if ($currentKey === null) { + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + return array_values($groups); + } + + /** + * 解码懂车帝价格 + */ + protected function decodePrice(string $str): string + { + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 从URL中提取ID + */ + protected function extractIdFromUrl(string $url) + { + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return (int)$matches[1]; + } + return false; + } + + /** + * 提取符合正则表达式的值(只保留数字和小数点) + */ + protected function extractPatternValue(string $text, string $pattern): string + { + $value = ''; + if ($text && preg_match($pattern, $text, $m)) { + $value = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + return $value; + } + + /** + * 下载单张图片 + */ + public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + if (!is_dir($saveDir)) { + if (!@mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + // echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); + $ext = pathinfo($path ?? '', PATHINFO_EXTENSION); + + if ($ext === '') { + $ext = 'jpg'; + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + $defaultHeaders = [ + 'User-Agent: ' . self::DEFAULT_HEADERS['User-Agent'], + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: ' . self::DEFAULT_HEADERS['Referer'], + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0 || $code !== 200 || $data === false || $data === '') { + return null; + } + + $bytes = @file_put_contents($savePath, $data); + if ($bytes === false) { + return null; + } + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片URL数组 + * @param string $saveDir 保存目录 + * @return array [原始URL => 保存路径或null] + */ + public function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim((string)$url); + if ($url === '') { + continue; + } + + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + try { + $path = $this->downloadImage($url, $saveDir); + } catch (Throwable $e) { + $path = null; + } + + $result[$url] = $path; + } + + return $result; + } + + /** + * 下载 -> 上传 -> 删除本地 + * + * @param array $images 原始相册URL列表 + * @param string $path 本地保存目录 + * @param int $usedCarId 懂车帝 used_car_id + * @param int $id 本地 crawl 记录id + * @return bool + */ + public function uploadAndRemove(array $images, string $path, $usedCarId, $id): bool + { + if (empty($images)) { + return false; + } + + // 下载图片 + $filePatch = $this->downloadImageAlbum($images, $path); + if (empty($filePatch)) { + return false; + } + + $uploadService = new UploadService(); + + foreach ($filePatch as $url => $filePath) { + if ($filePath === null) { + continue; + } + + try { + // 上传云端 + $param = [ + 'path' => $filePath, + 'scene' => 'usedcar_crawl', + 'category' => 'usedcar', + ]; + + $result = $uploadService->uploadLocalFile($param); + if (empty($result['file_name'])) { + // 上传失败则跳过,但不抛异常影响主流程 + continue; + } + + // 保存图片信息 + UsedcarCrawlResultsImages::create([ + 'uc_id' => $id, + 'used_car_id' => $usedCarId, + 'original_name'=> $result['original_name'] ?? '', + 'file_path' => $result['file_name'], + ]); + } catch (Throwable $e) { + // 单张图失败不影响其他 + // 可以按需写日志 + } finally { + // 删除本地文件 + if (is_file($filePath)) { + @unlink($filePath); + } + } + } + + // 删除目录 + rmdir($path); + + return true; + } + + /** + * 安全解 JSON + * + * @param string $json + * @param string $context + * @return array + * @throws BusinessException + */ + protected function decodeJson(string $json, string $context = 'JSON'): array + { + $data = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { + throw new BusinessException($context . ' 解析失败:' . json_last_error_msg()); + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/utils/crawl/service/UsedCarCrawl_bak.php b/app/common/utils/crawl/service/UsedCarCrawl_bak.php new file mode 100644 index 0000000..11ea8c8 --- /dev/null +++ b/app/common/utils/crawl/service/UsedCarCrawl_bak.php @@ -0,0 +1,681 @@ + $scene, + 'url' => $url, + 'id' => $id + ], JSON_UNESCAPED_UNICODE)); + } + + /** + * 获取队列url + * + * $scene 二手信息 usedcar 配置信息 config + */ + public function rUrl() + { + return json_decode(Redis::rPop('usedcar:urls'), true); + } + + /** + * 执行抓取 + */ + public function doData() + { + try { + $raw = $this->rUrl(); + + if (!$raw) { + return false; + } + + $html = $this->getHtml($raw['url']); + + switch ($raw['scene']) { + case 'usedcar': + $info = $this->getUsedDetail($html, $raw['url']); + $id = $this->saveUsedcarBasicToDb($info); + $this->iUrl($info['car_id_url'], 'config', $id); + break; + + case 'config': + $data = $this->getConfigDetail($html); + $this->saveUsedcarConfigToDb($data, $raw['id']); + break; + } + + return true; + } catch (\Throwable $th) { + throw new BusinessException($th->getMessage().'|'.$th->getLine().'|'.$th->getFile()); + } + } + + /** + * 获取html + */ + public function getHtml($url) + { + $client = new Client(); + $res = $client->request('GET', $url, [ + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + . 'AppleWebKit/537.36 (KHTML, like Gecko) ' + . 'Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/' + ] + ]); + return (string)$res->getBody(); + } + + /** + * 获取二手信息 + */ + public function getUsedDetail(string $html = '', string $url = '') + { + $ql = QueryList::html($html); + + $title = $ql->find('h1')->text(); + $price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text(); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + // 新车指导价 + $newPrice = ''; + if ($pageText) { + if (preg_match('/新车指导价[::]\s*([^\r\n万]*万)/u', $pageText, $m)) { + $newPrice = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + + // 比新车省 + $priceDifference = ''; + if ($pageText) { + if (preg_match('/比新车省[::]\s*([^\r\n万]*万)/u', $pageText, $m)) { + $priceDifference = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + + // 车况介绍 + $conditionDesc = ''; + if ($pageText) { + if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + } + + $jsonArray = json_decode($jsonText, true); + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID + $carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0; + + // 车辆年份 + $year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0; + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型 + $fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? ''; + $newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = explode(' | ', $importantText); + $usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : ''); + + // 车辆名称 品牌、车系、车型 + $brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? ''; + $seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? ''; + $carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '' + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + // 排量 + $displacement = $this->getOtherParamValue($otherParams, '排量'); + // 变速箱 + $gearbox = $this->getOtherParamValue($otherParams, '变速箱'); + // 格式化车辆信息 + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox + ]; + + // 整理车辆信息 + $info['used_car_id'] = $this->getLastIdFromUrl($url); + $info['car_id'] = $carId; + $info['url'] = $url; + $info['title'] = $title; + $info['price'] = str_replace('万', '', $this->decodeDongchediPrice($price)); + $info['new_price'] = str_replace('万', '', $newPrice); + $info['price_difference'] = str_replace('万', '', $priceDifference); + $info['condition_desc'] = $conditionDesc; + $info['images'] = json_encode($images); + $info['car_id_url'] = $carIdUrl; + $info['used_detail'] = $jsonText; + $info['format_shop_info'] = json_encode($formatShopInfo); + $info['fuel_form'] = $resultFuelForm; + $info['classify_energy'] = $this->detectEnergyType($resultFuelForm); + $info['usage_mileage'] = $usageMileage; + $info['brand_name'] = $brandName; + $info['series_name'] = $seriesName; + $info['car_name'] = $carName; + $info['year'] = $year; + $info['format_car_info'] = json_encode($formatCarInfo); + + return $info; + } + + /** + * 获取配置信息 + */ + public function getConfigDetail(string $html = '') + { + $ql = QueryList::html($html); + + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + $raw = json_decode($jsonText, true); + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMapFromCarInfo($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + $data['format_car_info'] = [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程 + ]; + $data['groups'] = $groups; + return $data; + } + + /** + * 从车辆配置 JSON 数据中获取能源类型 + * + * @param array $data JSON 数据 + * @return string|null 能源类型,如 '油电混合'、'纯电动'、 + */ + public function getFuelFormByConfigInfo(array $data) + { + $fuelForm = null; + foreach ($data[0]['items'] as $item) { + if ($item['key'] === 'fuel_form') { + $fuelForm = $item['value']; + break; + } + } + return $fuelForm; + } + + /** + * 从车辆配置 JSON 数据中根据字段 key 获取对应的 value + * + * @param string|array $data JSON 字符串 或 已解析的数组 + * @param string $field 要查找的字段 key(如 'fuel_form') + * @return string|null 找到则返回 value,否则返回 null + */ + public function getFieldValue($data, string $field) + { + // 如果传入的是 JSON 字符串,先解码 + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; // JSON 格式错误 + } + } + + // 遍历第一层(假设结构是 [ { "items": [...] } ]) + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (isset($item['key']) && $item['key'] === $field) { + return $item['value'] ?? null; + } + } + } + } + + return null; // 未找到 + } + + /** + * 从 other_params 数组中根据 name 获取对应的 value + * + * @param array $otherParams 格式为 [ ['name' => 'xxx', 'value' => 'yyy'], ... ] + * @param string $name 要查找的字段名称(如 '上牌地') + * @return string|null 找到则返回 value,否则返回 null + */ + public function getOtherParamValue(array $otherParams, string $name): ?string + { + foreach ($otherParams as $item) { + if (isset($item['name']) && $item['name'] === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 根据给定字符串判断车辆能源类型 + * + * 返回: + * - RE 新能源(纯电、插电混动、增程、氢燃料等) + * - CE 传统能源(汽油、柴油、天然气、非插电混动等) + * - UNKNOWN 无法判断 + * + * @param string $text 例如:'2022款 某某牌 2.0L 油电混合 双擎' + * @return string + */ + public function detectEnergyType(string $text): string + { + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + + // 明确定义的新能源关键字(纯电 / 插电 / 增程 / 氢 / 太阳能 等) + $newEnergyKeywords = [ + // 纯电 + '纯电', '纯电动', '电动车', 'BEV', + // 插电式混合 + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + // 增程式 + '增程式', '增程', 'REEV', + // 氢能 / 燃料电池 + '氢能', '氢燃料', '燃料电池', 'FCEV', + // 其它 + '太阳能', + ]; + + // 传统能源关键字(包括燃油/燃气) + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(按你的要求,统一归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', // 不带“插电”的混动 + '双擎', // 丰田常见表达 + 'HEV', + ]; + + // 辅助:不区分大小写的包含判断,优先 mb_stripos + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + // 1. 先判断“明确新能源”关键词(插电、增程、氢、纯电等) + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return 'RE'; + } + } + + // 2. 再判断“普通油电混动”——按照你的业务,一律归为传统能源 + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return 'CE'; + } + } + + // 3. 再判断传统能源(纯燃油/气) + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return 'CE'; + } + } + + // 4. 都没匹配上 + return 'UNKNOWN'; + } + + /** + * 二手信息入库 + */ + public function saveUsedcarBasicToDb(array $data = []) + { + $data['created_at'] = time(); + $data['updated_at'] = time(); + return Db::table('usedcar_crawl_results')->insertGetId($data); + } + + /** + * 更新信息入库 + */ + public function saveUsedcarConfigToDb(array $data = [], int $id = 0) + { + $info = Db::table('usedcar_crawl_results')->find($id); + + if (!$info) { + return false; + } + + $groups = $data['groups']; + $formatCarInfo = array_merge($data['format_car_info'], json_decode($info->format_car_info, true)); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE), + 'updated_at' => time() + ]); + } + + /** + * 把 car_info 扁平成一个 [key => ['value' => xxx]] 的 map + * + * @param array $carInfo rawData['car_info'][0] + * @return array + */ + public function buildValuesMapFromCarInfo(array $carInfo): array + { + $valuesMap = []; + + // 1) car_info 顶层的一些字段(official_price、series_name 等) + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // 2) info 里的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 根据 type 重新分组: + * - type=0,2 为分组(左侧分类) + * - 其它 type 都视为该分组下的字段 + * + * @param array $properties rawData['properties'] + * @param array $valuesMap 来自 buildValuesMapFromCarInfo() + * @return array + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; // 当前分组 key + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // ---------- 1) 分类字段:type = 0 / 2 ---------- + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // ---------- 2) 普通字段:挂到最近的分类下 ---------- + if ($currentKey === null) { + // 如果一开始就没有遇到分类,给一个默认分组 + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + // 取 value + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + // ---------- 3) sub_list 子项(有就当成 children) ---------- + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + // 你可以按需返回关联数组或索引数组,这里转成索引数组方便前端遍历 + return array_values($groups); + } + + /** + * 转义价格 + */ + protected function decodeDongchediPrice(string $str): string + { + // 映射表:PUA 字符 => 数字 / 万 + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 获取URL最后ID + */ + protected function getLastIdFromUrl($url) + { + // 方法1:使用正则表达式提取最后的数字ID + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return $matches[1]; + } + return false; + } + + /** + * 下载单张图片 + * + * @param string $url 图片完整 URL + * @param string $saveDir 保存目录(如 /www/usedcar_images/2025-11-20) + * @param string|null $filename 指定文件名(可选,不含路径),为空则自动生成 + * @param array $headers 额外 HTTP 头(可选) + * + * @return string|null 成功返回保存路径,失败返回 null + */ + protected function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + // 1. 目录检查 & 创建 + if (!is_dir($saveDir)) { + if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + // 2. 自动生成文件名(基于 URL md5 + 原扩展名) + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); // /xxx/xxx.jpg + $ext = pathinfo($path, PATHINFO_EXTENSION); // jpg 或 webp 之类 + + if ($ext === '') { + $ext = 'jpg'; // 没有扩展名时兜底 + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + // 3. 组装 HTTP 头 + $defaultHeaders = [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + . 'AppleWebKit/537.36 (KHTML, like Gecko) ' + . 'Chrome/120.0.0.0 Safari/537.36', + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: https://www.dongchedi.com/', // 很多图床需要 Referer + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + // 4. 使用 cURL 下载 + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $errMsg = curl_error($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0) { + echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n"; + return null; + } + + if ($code !== 200 || $data === false || $data === '') { + echo "[Image] 下载失败(HTTP {$code}):{$url}\n"; + return null; + } + + // 5. 写入本地文件 + $bytes = file_put_contents($savePath, $data); + if ($bytes === false) { + echo "[Image] 保存失败:{$savePath}\n"; + return null; + } + + echo "[Image] 下载成功:{$url} => {$savePath}\n"; + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片 URL 数组 + * @param string $saveDir 保存目录 + * + * @return array [原始 URL => 保存路径或 null] + */ + protected function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim($url); + if ($url === '') { + continue; + } + + // 如果存在 // 开头的相对协议,补全 + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + $path = $this->downloadImage($url, $saveDir); + $result[$url] = $path; + } + + return $result; + } +} diff --git a/app/common/utils/crawl/service/UsedCarCrawl_bak2.php b/app/common/utils/crawl/service/UsedCarCrawl_bak2.php new file mode 100644 index 0000000..2612baa --- /dev/null +++ b/app/common/utils/crawl/service/UsedCarCrawl_bak2.php @@ -0,0 +1,798 @@ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/' + ]; + + // Redis队列键名 + private const REDIS_QUEUE_KEY = 'usedcar:urls'; + + // 能源类型常量 + public const ENERGY_TYPE_RENEWABLE = 'RE'; + public const ENERGY_TYPE_CONVENTIONAL = 'CE'; + public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN'; + + /** + * 插入队列url + * + * @param string $url URL地址 + * @param string $scene 场景:usedcar(二手信息) config(配置信息) + * @param int $id 记录ID + * @return int + */ + public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int + { + $data = [ + 'scene' => $scene, + 'url' => $url, + 'id' => $id + ]; + + return Redis::lpush(self::REDIS_QUEUE_KEY, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + + /** + * 获取队列url + * + * @return array|null + */ + public function popUrl(): ?array + { + $data = Redis::rPop(self::REDIS_QUEUE_KEY); + if (!$data) { + return null; + } + + return json_decode($data, true); + } + + /** + * 执行抓取任务 + * + * @return bool + * @throws BusinessException + */ + public function execute(): bool + { + try { + $task = $this->popUrl(); + + if (!$task) { + return false; + } + + $html = $this->fetchHtml($task['url']); + + switch ($task['scene']) { + case 'usedcar': + $info = $this->parseUsedCarDetail($html, $task['url']); + $id = $this->saveUsedCarBasicInfo($info); + $this->uploadAndRemove(json_decode($info['images'], true), public_path('uploads/usedcar/' . $id), $info['used_car_id'], $id); + $this->pushUrl($info['car_id_url'], 'config', $id); + break; + + case 'config': + $data = $this->parseConfigDetail($html); + $this->saveUsedCarConfigInfo($data, $task['id']); + break; + + default: + throw new BusinessException("未知的任务场景: " . $task['scene']); + } + + return true; + } catch (GuzzleException $e) { + throw new BusinessException("网络请求失败: " . $e->getMessage()); + } catch (Throwable $th) { + throw new BusinessException("执行任务出错: " . $th->getMessage() . ' | 行号: ' . $th->getLine() . ' | 文件: ' . $th->getFile()); + } + } + + /** + * 获取网页HTML内容 + * + * @param string $url + * @return string + * @throws GuzzleException + */ + public function fetchHtml(string $url): string + { + $client = new Client(); + $response = $client->request('GET', $url, [ + 'headers' => self::DEFAULT_HEADERS + ]); + return (string)$response->getBody(); + } + + /** + * 解析二手车详情页信息 + * + * @param string $html + * @param string $url + * @return array + */ + public function parseUsedCarDetail(string $html = '', string $url = ''): array + { + $ql = QueryList::html($html); + + $title = $ql->find('h1')->text(); + $price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text(); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + // 解析JSON数据 + $jsonArray = json_decode($jsonText, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BusinessException("JSON解析失败: " . json_last_error_msg()); + } + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID + $carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0; + + // 车辆年份 + $year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0; + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型 + $fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? ''; + $newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = explode(' | ', $importantText); + $usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : ''); + + // 车辆名称 品牌、车系、车型 + $brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? ''; + $seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? ''; + $carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '' + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + // 排量 + $displacement = $this->getParamValue($otherParams, '排量'); + // 变速箱 + $gearbox = $this->getParamValue($otherParams, '变速箱'); + // 格式化车辆信息 + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox + ]; + + // 提取新车指导价 + $newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u'); + + // 提取比新车省金额 + $priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u'); + + // 提取车况介绍 + $conditionDesc = ''; + if ($pageText) { + if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + } + + // 整理车辆信息 + return [ + 'used_car_id' => $this->extractIdFromUrl($url), + 'car_id' => $carId, + 'url' => $url, + 'title' => $title, + 'price' => str_replace('万', '', $this->decodePrice($price)), + 'new_price' => str_replace('万', '', $newPrice), + 'price_difference' => str_replace('万', '', $priceDifference), + 'condition_desc' => $conditionDesc, + 'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'car_id_url' => $carIdUrl, + 'used_detail' => $jsonText, + 'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'fuel_form' => $resultFuelForm, + 'classify_energy' => $this->detectEnergyType($resultFuelForm), + 'usage_mileage' => $usageMileage, + 'brand_name' => $brandName, + 'series_name' => $seriesName, + 'car_name' => $carName, + 'year' => $year, + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ]; + } + + /** + * 解析配置详情页信息 + * + * @param string $html + * @return array + */ + public function parseConfigDetail(string $html = ''): array + { + $ql = QueryList::html($html); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + $raw = json_decode($jsonText, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BusinessException("配置页面JSON解析失败: " . json_last_error_msg()); + } + + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMap($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + return [ + 'format_car_info' => [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程 + ], + 'groups' => $groups + ]; + } + + /** + * 从车辆配置中获取能源类型 + * + * @param array $data JSON数据 + * @return string|null 能源类型 + */ + public function getFuelFormByConfig(array $data): ?string + { + if (empty($data[0]['items'])) { + return null; + } + + foreach ($data[0]['items'] as $item) { + if (isset($item['key']) && $item['key'] === 'fuel_form') { + return $item['value'] ?? null; + } + } + + return null; + } + + /** + * 从数据中根据字段key获取值 + * + * @param mixed $data 数据 + * @param string $field 字段名 + * @return string|null + */ + public function getFieldValue($data, string $field): ?string + { + // 如果传入的是 JSON 字符串,先解码 + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + } + + // 如果不是数组,直接返回null + if (!is_array($data)) { + return null; + } + + // 遍历寻找对应字段 + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (isset($item['key']) && $item['key'] === $field) { + return $item['value'] ?? null; + } + + // 检查子元素 + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as $child) { + if (isset($child['key']) && $child['key'] === $field) { + return $child['value'] ?? null; + } + } + } + } + } + } + + return null; + } + + /** + * 从参数列表中获取指定名称的值 + * + * @param array $params 参数列表 + * @param string $name 名称 + * @return string|null + */ + public function getParamValue(array $params, string $name): ?string + { + foreach ($params as $item) { + if (isset($item['name']) && $item['name'] === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 判断车辆能源类型 + * + * @param string $text 描述文本 + * @return string 能源类型标识 + */ + public function detectEnergyType(string $text): string + { + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + + // 明确定义的新能源关键字 + $newEnergyKeywords = [ + // 纯电 + '纯电', '纯电动', '电动车', 'BEV', + // 插电式混合 + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + // 增程式 + '增程式', '增程', 'REEV', + // 氢能 / 燃料电池 + '氢能', '氢燃料', '燃料电池', 'FCEV', + // 其它 + '太阳能', + ]; + + // 传统能源关键字 + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', // 不带"插电"的混动 + '双擎', // 丰田常见表达 + 'HEV', + ]; + + // 辅助函数:不区分大小写的包含判断 + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + // 1. 先判断"明确新能源"关键词 + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_RENEWABLE; + } + } + + // 2. 再判断"普通油电混动" + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + // 3. 再判断传统能源 + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + // 4. 都没匹配上 + return self::ENERGY_TYPE_UNKNOWN; + } + + /** + * 保存二手车基本信息到数据库 + * + * @param array $data 数据 + * @return int 插入记录ID + */ + public function saveUsedCarBasicInfo(array $data): int + { + $data['created_at'] = time(); + $data['updated_at'] = time(); + return Db::table('usedcar_crawl_results')->insertGetId($data); + } + + /** + * 更新配置信息到数据库 + * + * @param array $data 配置数据 + * @param int $id 记录ID + * @return bool|int + */ + public function saveUsedCarConfigInfo(array $data, int $id) + { + $record = Db::table('usedcar_crawl_results')->find($id); + + if (!$record) { + return false; + } + + $groups = $data['groups']; + $formatCarInfo = array_merge( + $data['format_car_info'], + json_decode($record->format_car_info, true) + ); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'updated_at' => time() + ]); + } + + /** + * 构建值映射表 + * + * @param array $carInfo 车辆信息 + * @return array + */ + public function buildValuesMap(array $carInfo): array + { + $valuesMap = []; + + // 处理顶层字段 + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // 处理info中的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 构建车辆规格分组 + * + * @param array $properties 属性列表 + * @param array $valuesMap 值映射表 + * @return array + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // 分类字段:type = 0 / 2 + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // 普通字段:挂到最近的分类下 + if ($currentKey === null) { + // 如果一开始就没有遇到分类,给一个默认分组 + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + // 取 value + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + // 处理子项 + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + // 转成索引数组方便前端遍历 + return array_values($groups); + } + + /** + * 解码懂车帝价格 + * + * @param string $str 加密字符串 + * @return string 解码后的字符串 + */ + protected function decodePrice(string $str): string + { + // 映射表:PUA 字符 => 数字 / 万 + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 从URL中提取ID + * + * @param string $url URL + * @return int|false + */ + protected function extractIdFromUrl(string $url) + { + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return (int)$matches[1]; + } + return false; + } + + /** + * 提取符合正则表达式的值 + * + * @param string $text 文本 + * @param string $pattern 正则表达式 + * @return string + */ + protected function extractPatternValue(string $text, string $pattern): string + { + $value = ''; + if ($text) { + if (preg_match($pattern, $text, $m)) { + $value = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + return $value; + } + + /** + * 下载单张图片 + * + * @param string $url 图片完整URL + * @param string $saveDir 保存目录 + * @param string|null $filename 指定文件名 + * @param array $headers 额外HTTP头 + * @return string|null 成功返回保存路径,失败返回null + */ + public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + // 目录检查 & 创建 + if (!is_dir($saveDir)) { + if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + // 自动生成文件名 + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); + $ext = pathinfo($path, PATHINFO_EXTENSION); + + if ($ext === '') { + $ext = 'jpg'; + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + // 组装HTTP头 + $defaultHeaders = [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: https://www.dongchedi.com/', + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + // 使用cURL下载 + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $errMsg = curl_error($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0) { + // echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n"; + return null; + } + + if ($code !== 200 || $data === false || $data === '') { + // echo "[Image] 下载失败(HTTP {$code}):{$url}\n"; + return null; + } + + // 写入本地文件 + $bytes = file_put_contents($savePath, $data); + if ($bytes === false) { + // echo "[Image] 保存失败:{$savePath}\n"; + return null; + } + + // echo "[Image] 下载成功:{$url} => {$savePath}\n"; + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片URL数组 + * @param string $saveDir 保存目录 + * @return array [原始URL => 保存路径或null] + */ + public function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim($url); + if ($url === '') { + continue; + } + + // 补全协议 + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + $path = $this->downloadImage($url, $saveDir); + $result[$url] = $path; + } + + return $result; + } + + + public function uploadAndRemove($images, $path, $usedCarId, $id) + { + // 下载图片 + $filePatch = $this->downloadImageAlbum($images, $path); + + // 检查图片 + if (empty($filePatch)) { + return false; + } + + // 上传图片 + foreach ($filePatch as $url => $path) { + if ($path === null) { + continue; + } + + // 上传云端 + $param = [ + 'path' => $path, + 'scene' => 'usedcar_crawl', + 'category' => 'usedcar' + ]; + $up = new \app\common\service\UploadService(); + $result = $up->uploadLocalFile($param); + + // 保存图片信息 + UsedcarCrawlResultsImages::create([ + 'uc_id' => $id, + 'used_car_id' => $usedCarId, + 'original_name' => $result['original_name'], + 'file_path' => $result['file_name'], + ]); + + // 删除本地文件 + @unlink($path); + } + } +} \ No newline at end of file diff --git a/app/common/validate/UsedcarCrawlResultsImagesValidate.php b/app/common/validate/UsedcarCrawlResultsImagesValidate.php new file mode 100644 index 0000000..e6cbbd5 --- /dev/null +++ b/app/common/validate/UsedcarCrawlResultsImagesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/UsedcarCrawlResultsValidate.php b/app/common/validate/UsedcarCrawlResultsValidate.php new file mode 100644 index 0000000..f0766bd --- /dev/null +++ b/app/common/validate/UsedcarCrawlResultsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index c3c75ae..4b4fb43 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -74,25 +74,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'model' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'service' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'logic' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'validate' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], - 'table' => 'finance_recon_dubai_files' + 'table' => 'usedcar_crawl_results_images' ]; $result = (new Generate($options))->doGenerate(); @@ -159,11 +159,16 @@ class IndexController public function test(Request $request) { - $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');; - - $service = new \app\common\service\FinanceRecoDubaiFilesService(); - $result = $service->import($tmp, true); + $service = new \app\common\utils\crawl\service\UsedCarCrawl(); - return json(['code' => 0, 'msg' => 'ok', 'data' => $result]); + $result = $service->pushUrl('https://www.dongchedi.com/usedcar/21631642'); + + // $result = $service->doData(); + + // $result = $service->decodeDongchediPrice('.'); + + // $result = $service->detectEnergyType('油电混合'); + + return json($result); } } \ No newline at end of file diff --git a/app/inquiry/utils/FbLeadHeaderMapper.php b/app/inquiry/utils/FbLeadHeaderMapper.php new file mode 100644 index 0000000..ed048ed --- /dev/null +++ b/app/inquiry/utils/FbLeadHeaderMapper.php @@ -0,0 +1,154 @@ + DB 字段名 + * 注意:key 用的是“原始表头字符串”(包括下划线) + */ + protected array $headerMap = [ + // ===== 基础公共字段 ===== + 'id' => 'fb_lead_id', + 'created_time' => 'fb_created_time', + 'ad_id' => 'ad_id', + 'ad_name' => 'ad_name', + 'adset_id' => 'adset_id', + 'adset_name' => 'adset_name', + 'campaign_id' => 'campaign_id', + 'campaign_name' => 'campaign_name', + 'form_id' => 'form_id', + 'form_name' => 'form_name', + 'is_organic' => 'is_organic', + 'platform' => 'platform', + + 'full_name' => 'full_name', + 'phone' => 'phone', + 'phone_number' => 'phone', // 兼容写法 + 'email' => 'email', + 'lead_status' => 'lead_status', + 'inbox_url' => 'inbox_url', + + // ===== 车型相关:全部归到 car_model ===== + 'which_model_of_this_car_would_you_like_to_purchase?_' => 'car_model', + 'which_model_of_this_car_would_you_like_to_purchase?' => 'car_model', + 'which_car_model_would_you_like_to_purchase?' => 'car_model', + 'which_model_do_you_want_to_purchase?' => 'car_model', + 'which_model_would_you_like_to_purchase?' => 'car_model', + 'which_version_do_you_want_to_buy?' => 'car_model', // 这里也归到车型/版本字段 + + // ===== 数量相关:全部归到 car_quantity ===== + 'how_many_cars_would_you_like_to_buy?_' => 'car_quantity', + 'how_many_cars_would_you_like_to_buy?' => 'car_quantity', + '_how_many_cars_would_you_like_to_buy?' => 'car_quantity', + 'how_many_cars_do_you_buy?' => 'car_quantity', + 'how_many_cars_do_you_want_to_purchase?' => 'car_quantity', + 'how_many_cars_do_you_want_to_buy?' => 'car_quantity', + 'what_quantity_do_you_want_to_buy?' => 'car_quantity', + + // ===== 颜色相关:归到 favorite_color ===== + 'what_color_do_you_like?' => 'favorite_color', + 'which_color_would_you_like_to_purchase?' => 'favorite_color', + + // ===== 用途相关:归到 usage_type ===== + 'are_you_using_it_for_business_or_personal_use?' => 'usage_type', + 'business_use_or_personal_use?' => 'usage_type', + 'personal_use_or_business_use?' => 'usage_type', + + // ===== 座位数相关 ===== + 'do_you_want_to_buy_six_seats_or_seven_seats?' => 'seat_preference', + ]; + + /** + * 返回所有支持的 Excel 表头 + */ + public function getSupportedHeaders(): array + { + return array_keys($this->headerMap); + } + + /** + * 单个表头映射:原始表头 -> DB 字段名 + */ + public function mapHeader(string $rawHeader): ?string + { + $key = $this->normalizeHeader($rawHeader); + + return $this->headerMap[$key] ?? null; + } + + /** + * 校验一整行表头: + * - 有任何不认识的表头,就抛 BusinessException + */ + public function validateHeaders(array $rawHeaders): void + { + $unsupported = []; + + foreach ($rawHeaders as $header) { + $header = (string)$header; + $key = $this->normalizeHeader($header); + + // 允许空表头(多余空列) + if ($key === '') { + continue; + } + + if (!isset($this->headerMap[$key])) { + $unsupported[] = $header; + } + } + + if (!empty($unsupported)) { + $msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported) + . '。请检查这些字段是否需要加入映射配置。'; + throw new BusinessException($msg); + } + } + + /** + * 根据第一行表头,构建 “列索引 => DB 字段” 映射 + */ + public function buildColumnFieldMap(array $rawHeaders): array + { + // 先做一遍校验(发现问题直接抛异常) + $this->validateHeaders($rawHeaders); + + $colToField = []; + + foreach ($rawHeaders as $index => $header) { + $key = $this->normalizeHeader((string)$header); + if ($key === '') { + continue; + } + + $field = $this->headerMap[$key] ?? null; + if ($field === null) { + continue; // 理论上不会到这儿,双保险 + } + + $colToField[$index] = $field; + } + + return $colToField; + } + + /** + * 表头标准化: + * 目前只去 BOM + trim,保持和 headerMap 中 key 完全一致 + * 如果后面再出现“有空格/大小写”的版本,我们可以在这里做 strtolower / 替换空格 为 _ + */ + protected function normalizeHeader(string $header): string + { + // 去掉 UTF-8 BOM + $header = preg_replace('/^\xEF\xBB\xBF/', '', $header); + + // 去掉前后空白 + $header = trim($header); + + return $header; + } +} diff --git a/app/inquiry/utils/PhoneCountryHelper.php b/app/inquiry/utils/PhoneCountryHelper.php new file mode 100644 index 0000000..e6b0a48 --- /dev/null +++ b/app/inquiry/utils/PhoneCountryHelper.php @@ -0,0 +1,58 @@ + + + if (str_starts_with($phone, '00')) { + $phone = '+' . substr($phone, 2); + } elseif ($phone[0] !== '+') { + // 没有 +,默认补一个(大多数线索都是国际号) + $phone = '+' . $phone; + } + + $digits = ltrim($phone, '+'); + if ($digits === '') { + return null; + } + + // 3. 国家区号最长 3 位,从长到短匹配 + for ($len = 3; $len >= 1; $len--) { + $code = substr($digits, 0, $len); + if ($code === '') { + continue; + } + + $country = Db::table('country') + ->where('phone_code', $code) + ->first(); + + if ($country) { + return (int)$country->id; + } + } + + // 没找到 + return null; + } +} diff --git a/app/inquiry/utils/RoundRobinAssigner.php b/app/inquiry/utils/RoundRobinAssigner.php new file mode 100644 index 0000000..dc0ac77 --- /dev/null +++ b/app/inquiry/utils/RoundRobinAssigner.php @@ -0,0 +1,46 @@ + $v > 0)); + if (empty($userIds)) { + throw new BusinessException('轮询分配业务员失败:业务员列表不能为空'); + } + + $count = count($userIds); + + // Redis key:同一个 groupKey 共用一条轮询链 + $key = self::REDIS_KEY_PREFIX . $groupKey; + + // 使用 INCR 做自增计数,天然是原子操作,适合并发环境 + // 第一次调用 INCR 返回 1,我们用 (value - 1) 来做索引,从 0 开始 + $val = Redis::incr($key); // 如果不存在,Redis 会先置为 0 再加 1 + + $index = ($val - 1) % $count; + + return $userIds[$index]; + } +} diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php index 8b99d61..f3439ef 100644 --- a/app/process/CheckWeb.php +++ b/app/process/CheckWeb.php @@ -22,6 +22,12 @@ class CheckWeb // }); // } + // 每秒执行:队列二手车数据处理 + new Crontab('* * * * * *', function(){ + $service = new \app\common\utils\crawl\service\UsedCarCrawl(); + $service->execute(); + }); + new Crontab('*/50 * * * * *', function(){ $userIds = User::where('status', 1)->pluck('id')->toArray(); foreach ($userIds as $userId) { diff --git a/app/process/Task.php b/app/process/Task.php index 9675b28..47fe78f 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -15,7 +15,7 @@ class Task { // 每半小时执行一次,通知待跟进客户 new Crontab('0 */30 * * * *', function(){ - echo date('Y-m-d H:i:s') . "\n"; + // echo date('Y-m-d H:i:s') . "\n"; Log::info('send follow start',[time()]); $where = [ diff --git a/composer.json b/composer.json index a696240..d502105 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ "webman/gateway-worker": "^1.0", "mpdf/mpdf": "^8.2", "jaeger/querylist": "^4.4", - "zoujingli/ip2region": "^3.0" + "zoujingli/ip2region": "^3.0", + "webman/log": "^2.1" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index ba35bd5..ecd2fc5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac2fa5a625069729975d072614509e1f", + "content-hash": "bf987c3cd5510fc0637d6b64d460dde1", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -18,13 +18,7 @@ "type": "zip", "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "php": ">=5.3" @@ -59,25 +53,25 @@ }, { "name": "brick/math", - "version": "0.13.1", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -107,7 +101,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -115,7 +109,7 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -267,33 +261,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -338,7 +331,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -354,7 +347,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "firebase/php-jwt", @@ -421,31 +414,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -476,7 +469,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -488,7 +481,7 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", @@ -554,22 +547,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -660,7 +653,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -676,20 +669,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -697,7 +690,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -743,7 +736,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -759,20 +752,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -788,7 +781,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -859,7 +852,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -875,20 +868,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", "shasum": "" }, "require": { @@ -897,7 +890,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", "uri-template/tests": "1.0.0" }, "type": "library", @@ -945,7 +938,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" }, "funding": [ { @@ -961,20 +954,20 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { "name": "illuminate/bus", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "60da78ea881c539ce56c5b66321be73755c5918c" + "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/60da78ea881c539ce56c5b66321be73755c5918c", - "reference": "60da78ea881c539ce56c5b66321be73755c5918c", + "url": "https://api.github.com/repos/illuminate/bus/zipball/fdb568a20ad96461caa83b98ffc36d9e95894491", + "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491", "shasum": "" }, "require": { @@ -1014,27 +1007,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-11-30T17:14:13+00:00" }, { "name": "illuminate/collections", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "e5711846b7c68128bc8de72c13e017606043c996" + "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/e5711846b7c68128bc8de72c13e017606043c996", - "reference": "e5711846b7c68128bc8de72c13e017606043c996", + "url": "https://api.github.com/repos/illuminate/collections/zipball/16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d", + "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d", "shasum": "" }, "require": { "illuminate/conditionable": "^12.0", "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", - "php": "^8.2" + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "illuminate/http": "Required to convert collections to API resources (^12.0).", @@ -1071,11 +1066,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-28T13:08:33+00:00" + "time": "2025-12-06T18:08:25+00:00" }, { "name": "illuminate/conditionable", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -1121,26 +1116,37 @@ }, { "name": "illuminate/container", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9" + "reference": "326667a4c813e3ad5a645969a7e3f5c10d159de2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/ff9dde2c8dce16ea9ecf0418095749311240aff9", - "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9", + "url": "https://api.github.com/repos/illuminate/container/zipball/326667a4c813e3ad5a645969a7e3f5c10d159de2", + "reference": "326667a4c813e3ad5a645969a7e3f5c10d159de2", "shasum": "" }, "require": { "illuminate/contracts": "^12.0", + "illuminate/reflection": "^12.0", "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1" + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "provide": { "psr/container-implementation": "1.1|2.0" }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, "type": "library", "extra": { "branch-alias": { @@ -1168,20 +1174,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T14:04:48+00:00" + "time": "2025-12-08T22:34:25+00:00" }, { "name": "illuminate/contracts", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "152313571553ef1be907a3c18b8ef2d635cb4339" + "reference": "19e8938edb73047017cfbd443b96844b86da4a59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/152313571553ef1be907a3c18b8ef2d635cb4339", - "reference": "152313571553ef1be907a3c18b8ef2d635cb4339", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/19e8938edb73047017cfbd443b96844b86da4a59", + "reference": "19e8938edb73047017cfbd443b96844b86da4a59", "shasum": "" }, "require": { @@ -1216,24 +1222,24 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T18:23:03+00:00" + "time": "2025-11-26T21:36:01+00:00" }, { "name": "illuminate/database", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "da274285e4c09f6794945354b5b11157f4d9cab1" + "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/da274285e4c09f6794945354b5b11157f4d9cab1", - "reference": "da274285e4c09f6794945354b5b11157f4d9cab1", + "url": "https://api.github.com/repos/illuminate/database/zipball/b44f021fee7b48289312e22fd6e4d580374a86ac", + "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "ext-pdo": "*", "illuminate/collections": "^12.0", "illuminate/container": "^12.0", @@ -1241,7 +1247,8 @@ "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", "laravel/serializable-closure": "^1.3|^2.0", - "php": "^8.2" + "php": "^8.2", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", @@ -1286,20 +1293,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T14:07:55+00:00" + "time": "2025-12-06T18:08:25+00:00" }, { "name": "illuminate/events", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5" + "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/bf1f121ea51e077e893d32e2848e102513d4b1b5", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5", + "url": "https://api.github.com/repos/illuminate/events/zipball/5fbf9a127cb649699071c2fe98ac1d39e4991da3", + "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3", "shasum": "" }, "require": { @@ -1341,20 +1348,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-12-01T15:02:02+00:00" }, { "name": "illuminate/filesystem", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab" + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/80fe8605cfa360fdbc85f67c19801a9657615aab", - "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b1fbb20010e868f838feac86aeac8ba439fca10d", + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d", "shasum": "" }, "require": { @@ -1408,20 +1415,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-29T15:59:33+00:00" }, { "name": "illuminate/http", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/http.git", - "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184" + "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/http/zipball/bbafb2b2e629530e516d52a1fa46dfeb09b5e184", - "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184", + "url": "https://api.github.com/repos/illuminate/http/zipball/b7997f2faffb3c31f2a0fa079428ff6bfcce8907", + "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907", "shasum": "" }, "require": { @@ -1437,7 +1444,8 @@ "symfony/http-foundation": "^7.2.0", "symfony/http-kernel": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31" + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." @@ -1469,11 +1477,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-06T18:14:49+00:00" + "time": "2025-12-09T15:25:50+00:00" }, { "name": "illuminate/macroable", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -1519,16 +1527,16 @@ }, { "name": "illuminate/pagination", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", - "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67" + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pagination/zipball/7f68669ad7d8a706883b8409f6b7254fdc437f67", - "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", "shasum": "" }, "require": { @@ -1565,27 +1573,31 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-28T21:37:18+00:00" + "time": "2025-11-16T14:36:17+00:00" }, { "name": "illuminate/pipeline", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4" + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pipeline/zipball/a1039dfe54854470cdda37782bab0901aa588dd4", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", "shasum": "" }, "require": { "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", "php": "^8.2" }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, "type": "library", "extra": { "branch-alias": { @@ -1613,20 +1625,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-08-20T13:36:50+00:00" }, { "name": "illuminate/redis", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/redis.git", - "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae" + "reference": "8dc6682638100db9feceaac3db206de752dd0f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/redis/zipball/b225dc28092b00ed0555df66ce3a3a448b4dd8ae", - "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae", + "url": "https://api.github.com/repos/illuminate/redis/zipball/8dc6682638100db9feceaac3db206de752dd0f9a", + "reference": "8dc6682638100db9feceaac3db206de752dd0f9a", "shasum": "" }, "require": { @@ -1667,20 +1679,71 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-11-20T20:50:40+00:00" }, { - "name": "illuminate/session", - "version": "v12.18.0", + "name": "illuminate/reflection", + "version": "v12.42.0", "source": { "type": "git", - "url": "https://github.com/illuminate/session.git", - "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8" + "url": "https://github.com/illuminate/reflection.git", + "reference": "7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/session/zipball/9d27155b34bca502fe3e1adc16035b46d5ef3ed8", - "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75", + "reference": "7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Reflection package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-12-09T15:11:22+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.42.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/c376e4a0dda79a88462e8a7d16cc924aa3eb6e15", + "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15", "shasum": "" }, "require": { @@ -1724,20 +1787,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-26T17:11:19+00:00" }, { "name": "illuminate/support", - "version": "v12.18.0", + "version": "v12.42.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01" + "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/3c3894ba09a86663b747efaa6822639f1c32ea01", - "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01", + "url": "https://api.github.com/repos/illuminate/support/zipball/d35411be5657e0b5560a5885f3c9140e4cbe0be5", + "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5", "shasum": "" }, "require": { @@ -1749,8 +1812,11 @@ "illuminate/conditionable": "^12.0", "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", + "illuminate/reflection": "^12.0", "nesbot/carbon": "^3.8.4", "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", "voku/portable-ascii": "^2.0.2" }, "conflict": { @@ -1801,7 +1867,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-06T18:15:26+00:00" + "time": "2025-12-09T15:26:52+00:00" }, { "name": "jaeger/g-http", @@ -1960,16 +2026,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -1978,7 +2044,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -2017,27 +2083,21 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "league/flysystem", - "version": "3.29.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0", - "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "shasum": "" }, "require": { "league/flysystem-local": "^3.0.0", @@ -2060,13 +2120,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -2104,29 +2164,23 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2024-09-29T11:59:11+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", + "shasum": "" }, "require": { "ext-fileinfo": "*", @@ -2159,29 +2213,23 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" }, "require": { "ext-fileinfo": "*", @@ -2211,7 +2259,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -2223,26 +2271,26 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "maennchen/zipstream-php", - "version": "3.1.2", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-zlib": "*", - "php-64bit": "^8.2" + "php-64bit": "^8.3" }, "require-dev": { "brianium/paratest": "^7.7", @@ -2251,7 +2299,7 @@ "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^11.0", + "phpunit/phpunit": "^12.0", "vimeo/psalm": "^6.0" }, "suggest": { @@ -2293,7 +2341,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" }, "funding": [ { @@ -2301,7 +2349,7 @@ "type": "github" } ], - "time": "2025-01-27T12:07:53+00:00" + "time": "2025-07-17T11:15:13+00:00" }, { "name": "markbaker/complex", @@ -2514,16 +2562,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.6", + "version": "v8.2.7", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { @@ -2591,7 +2639,7 @@ "type": "custom" } ], - "time": "2025-08-18T08:51:51+00:00" + "time": "2025-12-01T10:18:02+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -2747,16 +2795,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -2764,9 +2812,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2774,13 +2822,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", "phpmd/phpmd": "^2.15.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpunit/phpunit": "^10.5.46", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" }, "bin": [ "bin/carbon" @@ -2848,7 +2896,7 @@ "type": "tidelift" } ], - "time": "2025-06-12T10:24:28+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nikic/fast-route", @@ -2952,16 +3000,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.10.0", + "version": "v6.12.0", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144" + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", "shasum": "" }, "require": { @@ -3021,7 +3069,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" }, "funding": [ { @@ -3029,7 +3077,7 @@ "type": "github" } ], - "time": "2025-04-24T15:19:31+00:00" + "time": "2025-10-15T16:49:08+00:00" }, { "name": "phpoffice/phpspreadsheet", @@ -3139,16 +3187,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -3156,7 +3204,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -3198,7 +3246,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -3210,7 +3258,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "psr/cache", @@ -3801,13 +3849,7 @@ "type": "zip", "url": "https://api.github.com/repos/shopwwi/filesystem-oss/zipball/85cb385d1a5d11ca3c22bc485a7b18c588fe7645", "reference": "85cb385d1a5d11ca3c22bc485a7b18c588fe7645", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "aliyuncs/oss-sdk-php": "^2.4", @@ -3860,13 +3902,7 @@ "type": "zip", "url": "https://api.github.com/repos/shopwwi/webman-filesystem/zipball/26ac82515d3787943573c50d1a6f0ca339f0d3d4", "reference": "26ac82515d3787943573c50d1a6f0ca339f0d3d4", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "league/flysystem": "^2.3|^3.0", @@ -3909,16 +3945,16 @@ }, { "name": "symfony/cache", - "version": "v7.3.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "c4b217b578c11ec764867aa0c73e602c602965de" + "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/c4b217b578c11ec764867aa0c73e602c602965de", - "reference": "c4b217b578c11ec764867aa0c73e602c602965de", + "url": "https://api.github.com/repos/symfony/cache/zipball/21e0755783bbbab58f2bb6a7a57896d21d27a366", + "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366", "shasum": "" }, "require": { @@ -3926,12 +3962,14 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", "symfony/dependency-injection": "<6.4", "symfony/http-kernel": "<6.4", "symfony/var-dumper": "<6.4" @@ -3946,13 +3984,13 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3987,7 +4025,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.3.0" + "source": "https://github.com/symfony/cache/tree/v7.4.1" }, "funding": [ { @@ -3998,12 +4036,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-06T19:00:13+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "symfony/cache-contracts", @@ -4083,16 +4125,16 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -4137,7 +4179,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -4148,25 +4190,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/console", - "version": "v7.3.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -4174,7 +4220,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4188,16 +4234,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4231,7 +4277,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.0" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -4242,12 +4288,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T10:34:04+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4318,32 +4368,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4375,7 +4426,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -4386,25 +4437,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -4421,13 +4476,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4455,7 +4511,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -4466,12 +4522,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-22T09:11:45+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4551,16 +4611,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -4569,7 +4629,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4597,7 +4657,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -4608,32 +4668,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4661,7 +4725,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -4672,32 +4736,35 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4236baf01609667d53b20371486228231eb135fd" + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", - "reference": "4236baf01609667d53b20371486228231eb135fd", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4706,13 +4773,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4740,7 +4807,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" }, "funding": [ { @@ -4751,34 +4818,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-12T14:48:23+00:00" + "time": "2025-12-07T11:13:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.0", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4788,6 +4859,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -4805,27 +4877,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -4854,7 +4926,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" }, "funding": [ { @@ -4865,29 +4937,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:47:32+00:00" + "time": "2025-12-08T07:43:37+00:00" }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -4902,11 +4979,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -4938,7 +5015,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -4949,16 +5026,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -5017,7 +5098,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -5028,6 +5109,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5037,16 +5122,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -5095,7 +5180,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -5106,16 +5191,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -5178,7 +5267,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -5189,6 +5278,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5198,7 +5291,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5259,7 +5352,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -5270,6 +5363,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5279,7 +5376,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5340,7 +5437,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -5351,6 +5448,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5360,7 +5461,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -5420,7 +5521,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -5431,6 +5532,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5440,16 +5545,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -5496,7 +5601,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -5507,25 +5612,189 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.6.0", + "name": "symfony/polyfill-php84", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -5579,7 +5848,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5590,31 +5859,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -5622,12 +5896,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5666,7 +5939,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -5677,38 +5950,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -5727,17 +5998,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5768,7 +6039,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.0" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -5779,25 +6050,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -5846,7 +6121,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -5857,25 +6132,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -5887,11 +6166,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -5930,7 +6208,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -5941,25 +6219,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T18:39:23+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c9a1168891b5aaadfd6332ef44393330b3498c4c", - "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -5967,9 +6249,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6007,7 +6289,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -6018,12 +6300,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-15T09:04:05+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "topthink/think-container", @@ -6358,22 +6644,22 @@ }, { "name": "webman/console", - "version": "v2.1.8", + "version": "v2.1.10", "source": { "type": "git", "url": "https://github.com/webman-php/console.git", - "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10" + "reference": "6b24bb77d04588c7c3b235087073e69ddfdaa773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/console/zipball/2d4ce527810f91e1b4f583ec90eae24a0334ab10", - "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10", + "url": "https://api.github.com/repos/webman-php/console/zipball/6b24bb77d04588c7c3b235087073e69ddfdaa773", + "reference": "6b24bb77d04588c7c3b235087073e69ddfdaa773", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", "php": ">=8.1", - "symfony/console": ">=6.0" + "symfony/console": "^6.0 || ^7.0" }, "require-dev": { "workerman/webman": "^2.1" @@ -6408,20 +6694,20 @@ "source": "https://github.com/webman-php/console", "wiki": "http://www.workerman.net/doc/webman" }, - "time": "2025-06-02T00:42:16+00:00" + "time": "2025-12-08T08:23:12+00:00" }, { "name": "webman/database", - "version": "v2.1.6", + "version": "v2.1.7", "source": { "type": "git", "url": "https://github.com/webman-php/database.git", - "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8" + "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/database/zipball/943c5a0f92d8959d9160c2ee3925112b15c308c8", - "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8", + "url": "https://api.github.com/repos/webman-php/database/zipball/81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6", + "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6", "shasum": "" }, "require": { @@ -6444,9 +6730,9 @@ "description": "Webman database", "support": { "issues": "https://github.com/webman-php/database/issues", - "source": "https://github.com/webman-php/database/tree/v2.1.6" + "source": "https://github.com/webman-php/database/tree/v2.1.7" }, - "time": "2025-04-04T13:23:25+00:00" + "time": "2025-11-27T08:09:25+00:00" }, { "name": "webman/gateway-worker", @@ -6481,6 +6767,40 @@ }, "time": "2024-04-30T01:05:42+00:00" }, + { + "name": "webman/log", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/log.git", + "reference": "14e3f3e64e87783952417a68cf5205c1293aba69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/log/zipball/14e3f3e64e87783952417a68cf5205c1293aba69", + "reference": "14e3f3e64e87783952417a68cf5205c1293aba69", + "shasum": "" + }, + "require": { + "workerman/webman-framework": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman plugin webman/log", + "support": { + "issues": "https://github.com/webman-php/log/issues", + "source": "https://github.com/webman-php/log/tree/v2.1.3" + }, + "time": "2025-02-26T11:43:36+00:00" + }, { "name": "webman/redis", "version": "v2.1.3", @@ -6519,16 +6839,16 @@ }, { "name": "webman/redis-queue", - "version": "v2.1.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/webman-php/redis-queue.git", - "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c" + "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/12e1c9915ec64c91c0c9c255606bd0c7de27334c", - "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/ff4791e21f3c324a47e21da7b6f2dae5a7311dcb", + "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb", "shasum": "" }, "require": { @@ -6547,22 +6867,22 @@ "description": "Redis message queue plugin for webman.", "support": { "issues": "https://github.com/webman-php/redis-queue/issues", - "source": "https://github.com/webman-php/redis-queue/tree/v2.1.0" + "source": "https://github.com/webman-php/redis-queue/tree/v2.1.1" }, - "time": "2025-01-24T12:56:01+00:00" + "time": "2025-11-14T07:12:52+00:00" }, { "name": "workerman/coroutine", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/workerman-php/coroutine.git", - "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa" + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/df8fc428967d512a74a8a7d80355c1d40228c9fa", - "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", "shasum": "" }, "require": { @@ -6586,9 +6906,9 @@ "description": "Workerman coroutine", "support": { "issues": "https://github.com/workerman-php/coroutine/issues", - "source": "https://github.com/workerman-php/coroutine/tree/v1.1.3" + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" }, - "time": "2025-02-17T03:34:21+00:00" + "time": "2025-10-11T15:09:08+00:00" }, { "name": "workerman/crontab", @@ -6696,23 +7016,17 @@ }, { "name": "workerman/redis", - "version": "v2.0.4", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/walkor/redis.git", - "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e" + "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/redis/zipball/82b398c5bdd5d683a3458aaf645b159b7b39b84e", - "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/walkor/redis/zipball/49627c1809eff1ef7175eb8ee7549234a1d67ec5", + "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5", + "shasum": "" }, "require": { "php": ">=7", @@ -6731,9 +7045,9 @@ "homepage": "http://www.workerman.net", "support": { "issues": "https://github.com/walkor/redis/issues", - "source": "https://github.com/walkor/redis/tree/v2.0.4" + "source": "https://github.com/walkor/redis/tree/v2.0.5" }, - "time": "2025-04-05T07:45:12+00:00" + "time": "2025-04-07T01:58:58+00:00" }, { "name": "workerman/redis-queue", @@ -6747,13 +7061,7 @@ "type": "zip", "url": "https://api.github.com/repos/walkor/redis-queue/zipball/75dbf7ed2ea228c45dc0df82c0fea35879b715d0", "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "php": ">=7.0", @@ -6780,16 +7088,16 @@ }, { "name": "workerman/webman-framework", - "version": "v2.1.2", + "version": "v2.1.4", "source": { "type": "git", "url": "https://github.com/walkor/webman-framework.git", - "reference": "f803bd867f07bb0929faef060b59a19a44186bfc" + "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f803bd867f07bb0929faef060b59a19a44186bfc", - "reference": "f803bd867f07bb0929faef060b59a19a44186bfc", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f19d1f9e47cc50210555f0b63db5ae1dd584f793", + "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793", "shasum": "" }, "require": { @@ -6797,7 +7105,7 @@ "nikic/fast-route": "^1.3", "php": ">=8.1", "psr/container": ">=1.0", - "psr/log": "^3.0", + "psr/log": "^2.0 || ^3.0", "workerman/workerman": "^5.1 || dev-master" }, "suggest": { @@ -6842,20 +7150,20 @@ "source": "https://github.com/walkor/webman-framework", "wiki": "https://doc.workerman.net/" }, - "time": "2025-03-10T11:52:22+00:00" + "time": "2025-11-10T06:59:23+00:00" }, { "name": "workerman/workerman", - "version": "v5.1.3", + "version": "v5.1.6", "source": { "type": "git", "url": "https://github.com/walkor/workerman.git", - "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5" + "reference": "9b83bdf9fd0eaff9419a240b47da870447583112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/workerman/zipball/371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", - "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", + "url": "https://api.github.com/repos/walkor/workerman/zipball/9b83bdf9fd0eaff9419a240b47da870447583112", + "reference": "9b83bdf9fd0eaff9419a240b47da870447583112", "shasum": "" }, "require": { @@ -6868,9 +7176,9 @@ }, "require-dev": { "guzzlehttp/guzzle": "^7.0", - "mockery/mockery": "^1.6", + "mockery/mockery": "2.0.x-dev", "pestphp/pest": "2.x-dev", - "phpstan/phpstan": "1.11.x-dev" + "phpstan/phpstan": "2.1.x-dev" }, "suggest": { "ext-event": "For better performance. " @@ -6918,28 +7226,31 @@ "type": "patreon" } ], - "time": "2025-06-12T13:34:04+00:00" + "time": "2025-11-12T01:36:20+00:00" }, { "name": "zoujingli/ip2region", - "version": "v3.0.0", + "version": "v3.0.8", "source": { "type": "git", "url": "https://github.com/zoujingli/ip2region.git", - "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6" + "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", - "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", + "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53", + "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53", "shasum": "" }, "require": { - "php": ">=5.4" + "php": ">=7.1.0" }, "suggest": { "ext-opcache": "启用 OPcache 以获得更好的性能" }, + "bin": [ + "bin/ip2down" + ], "type": "library", "extra": { "branch-alias": { @@ -6948,10 +7259,13 @@ }, "autoload": { "files": [ - "ip2region.php" + "src/common.php" ], + "psr-4": { + "ip2region\\": "src/ip2region/" + }, "classmap": [ - "src/" + "src/Ip2Region.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6965,30 +7279,28 @@ "homepage": "https://thinkadmin.top" } ], - "description": "Ip2Region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4/IPv6,分片文件管理,智能缓存,零依赖", + "description": "ip2region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4 和 IPv6,多种缓存策略,零依赖,开箱即用", "homepage": "https://github.com/zoujingli/Ip2Region", "keywords": [ "IP", - "IP定位", "Ip2Region", - "geoip", + "composer", "geolocation", "ipv4", "ipv6", "location", + "php", "企业级", - "分片数据库", - "压缩", - "地理位置", - "地理位置查询", - "缓存", + "智能加载", + "缓存策略", + "零依赖", "高性能" ], "support": { "issues": "https://github.com/zoujingli/ip2region/issues", - "source": "https://github.com/zoujingli/ip2region/tree/v3.0.0" + "source": "https://github.com/zoujingli/ip2region/tree/v3.0.8" }, - "time": "2025-09-15T12:27:22+00:00" + "time": "2025-11-20T11:02:33+00:00" } ], "packages-dev": [], diff --git a/config/plugin/webman/log/app.php b/config/plugin/webman/log/app.php new file mode 100644 index 0000000..6217553 --- /dev/null +++ b/config/plugin/webman/log/app.php @@ -0,0 +1,19 @@ + true, + 'exception' => [ + // 是否记录异常到日志 + 'enable' => true, + // 不会记录到日志的异常类 + 'dontReport' => [ + support\exception\BusinessException::class + ] + ], + 'dontReport' => [ + 'app' => [], + 'controller' => [], + 'action' => [], + 'path' => [] + ], + 'channel' => 'default' // 日志通道(在config/log.php里配置,默认是default) +]; diff --git a/config/plugin/webman/log/middleware.php b/config/plugin/webman/log/middleware.php new file mode 100644 index 0000000..7c7b0a9 --- /dev/null +++ b/config/plugin/webman/log/middleware.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Log\Middleware; + +return [ + '@' => [ + Middleware::class + ] +];