Laravel站点地图生成Job报错:Typed property未初始化及URL添加失败
站点地图生成Job的问题排查与解决
问题背景
在Laravel中开发站点地图生成功能,计划每处理100条Car模型数据就创建一个Job来添加URL标签,但执行sitemap:make命令时出现错误。
错误信息
Error: Typed property App\Services\Sitemap\SitemapService::$xml must not be accessed before initialization in /var/www/html/app/Services/Sitemap/SitemapService.php:41 Stack trace: #0 /var/www/html/app/Jobs/MakeSitemapJob.php(41):
尝试过的操作
- 移除处理器中的
addUrlTag()逻辑时,Job能正常运行; - 给SitemapService添加构造函数初始化
$xml后,Job无报错,但Car的URL并未写入XML文件,生成的文件为空。
相关代码
MakeSitemapCommand.php
<?php namespace App\Console\Commands; use App\Jobs\MakeSitemapJob; use App\Services\Sitemap\SitemapService; use Illuminate\Console\Command; class MakeSitemapCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'sitemap:make'; /** * The console command description. * * @var string */ protected $description = 'This command creates sitemap'; /** * Execute the console command. */ public function handle(SitemapService $sitemapService): void { $sitemapService->make(); } }
SitemapService.php
namespace App\Services\Sitemap; use App\Jobs\MakeSitemapJob; use App\Models\Car; use SimpleXMLElement; class SitemapService { private SimpleXMLElement $xml; public function make(): SimpleXMLElement { $this->clear(); $this->xml = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </urlset>'); $urls = [ url('/'), ]; foreach ($urls as $url) { $this->addUrlTag($url, date('Y-m-d')); } Car::chunk(100, function ($cars) { MakeSitemapJob::dispatch($cars); }); $this->divide($this->xml, env('SITEMAP_URL_LIMIT', 50000)); return $this->xml; } public function addUrlTag($url, $lastmod): void { $sitemapTag = $this->xml->addChild('url'); $sitemapTag->addChild('loc', $url); $sitemapTag->addChild('lastmod', $lastmod); } public function divide($sitemap, $urlLimit): void { $urlTags = $sitemap->url; $numberOfFilesToCreate = ceil(count($urlTags) / $urlLimit); $finalSitemap = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </sitemapindex>'); for ($i = 1; $i <= $numberOfFilesToCreate; $i++) { $sitemapTag = $finalSitemap->addChild('sitemap'); $sitemapTag->addChild('loc', url('/') . "/sitemaps/sitemap-$i.xml"); $sitemapTag->addChild('lastmod', date('Y-m-d')); $start = ($i - 1) * $urlLimit; $end = $i * $urlLimit - 1; $fileContent = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </urlset>' ); for ($j = $start; $j <= $end && $j < count($urlTags); $j++) { $sourceUrlTag = $urlTags[$j]; $newUrlTag = $fileContent->addChild('url'); foreach ($sourceUrlTag->children() as $child) { $tagName = $child->getName(); $tagValue = (string)$child; $newUrlTag->addChild($tagName, $tagValue); } } file_put_contents(public_path() . "/sitemaps/sitemap-$i.xml", $fileContent->asXML()); } file_put_contents(public_path() . '/sitemap.xml', $finalSitemap->asXML()); file_put_contents(public_path() . '/robots.txt', "Sitemap: http://vincheck.site/sitemap.xml ", FILE_APPEND); } public function clear(): void { $this->removeMainFile(); $this->removeFromFolder(); $this->removeFromRobots(); } public function removeFromRobots(): void { $robotsFilePath = public_path() . '/robots.txt'; $pattern = '/^Sitemap:\s*.*\.xml$/im'; $updatedRobotsContent = preg_replace($pattern, '', file_get_contents($robotsFilePath)); $updatedRobotsContent = preg_replace("/^\h*\v+/m", "", $updatedRobotsContent); $updatedRobotsContent = preg_replace("/\n{2,}/", "\n", $updatedRobotsContent); file_put_contents($robotsFilePath, $updatedRobotsContent); } public function removeFromFolder(): void { $baseDirectory = public_path() . '/sitemaps'; $filePattern = '/^sitemap\s*.*\.xml$/'; $files = scandir($baseDirectory); foreach ($files as $file) { if (preg_match($filePattern, $file)) { $filePath = $baseDirectory . '/' . $file; unlink($filePath); } } } public function removeMainFile(): void { $baseDirectory = public_path() . '/sitemap.xml'; if (file_exists($baseDirectory)) { unlink($baseDirectory); } } }
MakeSitemapJob.php
<?php namespace App\Jobs; use App\Models\Car; use App\Services\Sitemap\SitemapService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class MakeSitemapJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $cars; /** * The number of times the job may be attempted. * * @var int */ public $tries = 3; /** * Create a new job instance. */ public function __construct($cars) { $this->cars = $cars; } /** * Execute the job. */ public function handle(SitemapService $sitemapService): void { foreach ($this->cars as $car) { $sitemapService->addUrlTag("http://vincheck.site/cars/$car->vin", $car->updated_at); } } }
修改后的SitemapService构造函数
class SitemapService { private SimpleXMLElement $xml; public function __construct() { $this->xml = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </urlset>' ); } public function make(): SimpleXMLElement { $this->clear(); $urls = [ url('/'), ]; // ... }
问题分析与解决
核心问题
- 初始错误原因:Job执行时,容器注入的
SitemapService是全新实例,没有执行make()方法初始化$xml属性,导致调用addUrlTag()时触发"未初始化"错误。 - 构造函数初始化后无效原因:Job中的
SitemapService实例是独立的,其$xml对象和主进程中的实例互不关联,Job添加的URL不会同步到主进程的XML对象中,最终主进程调用divide()时只处理了初始的首页URL,生成空文件。
解决方案
方案1:让Job直接生成并写入分片XML
放弃共享XML对象的思路,让每个Job直接生成对应分片的XML文件,最后由主进程生成索引文件:
- 修改
MakeSitemapJob:
public function handle(): void { $xml = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </urlset>' ); foreach ($this->cars as $car) { $sitemapTag = $xml->addChild('url'); $sitemapTag->addChild('loc', "http://vincheck.site/cars/{$car->vin}"); $sitemapTag->addChild('lastmod', $car->updated_at->toDateString()); } // 使用唯一文件名避免冲突 $tempFile = public_path("/sitemaps/temp_" . uniqid() . ".xml"); file_put_contents($tempFile, $xml->asXML()); }
- 修改
SitemapService的make()方法:- 先处理首页URL并写入基础XML文件;
- 等待所有Job执行完成;
- 合并所有临时XML文件的URL标签,再调用
divide()生成最终分片和索引。
方案2:使用缓存存储URL列表
让Job将生成的URL存入缓存,主进程等待所有Job完成后再从缓存读取所有URL生成XML:
- 修改
MakeSitemapJob:
use Illuminate\Support\Facades\Cache; public function handle(): void { $urls = []; foreach ($this->cars as $car) { $urls[] = [ 'loc' => "http://vincheck.site/cars/{$car->vin}", 'lastmod' => $car->updated_at->toDateString() ]; } // 将URL列表追加到缓存集合 Cache::push('sitemap_urls', $urls); }
- 修改
SitemapService的make()方法:
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Bus; public function make(): void { $this->clear(); Cache::forget('sitemap_urls'); // 处理首页URL $allUrls = [ ['loc' => url('/'), 'lastmod' => date('Y-m-d')] ]; // 分发所有Job并等待完成 $jobs = []; Car::chunk(100, function ($cars) use (&$jobs) { $jobs[] = new MakeSitemapJob($cars); }); Bus::batch($jobs)->dispatch()->wait(); // 从缓存读取所有URL $cachedUrls = Cache::pull('sitemap_urls') ?? []; foreach ($cachedUrls as $batch) { $allUrls = array_merge($allUrls, $batch); } // 生成XML并分片 $xml = new SimpleXMLElement( '<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </urlset>' ); foreach ($allUrls as $urlData) { $sitemapTag = $xml->addChild('url'); $sitemapTag->addChild('loc', $urlData['loc']); $sitemapTag->addChild('lastmod', $urlData['lastmod']); } $this->divide($xml, env('SITEMAP_URL_LIMIT', 50000)); }
方案3:同步处理(适合数据量不大的情况)
如果Car数据量不是特别大,直接放弃异步Job,在主进程中同步处理:
// 修改SitemapService的make()方法,移除Job分发代码 Car::chunk(100, function ($cars) { foreach ($cars as $car) { $this->addUrlTag("http://vincheck.site/cars/{$car->vin}", $car->updated_at->toDateString()); } });
总结
异步Job的核心问题是无法共享内存中的对象,因此需要通过文件、缓存等持久化方式传递数据,或者直接同步处理。根据数据量大小选择合适的方案,数据量大优先选方案1或2,数据量小选方案3更简单。
内容的提问来源于stack exchange,提问作者YahorLukyanchyk
相关产品推荐
相关产品推荐

