You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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('/'),
        ];
    
        // ...
}

问题分析与解决

核心问题

  1. 初始错误原因:Job执行时,容器注入的SitemapService是全新实例,没有执行make()方法初始化$xml属性,导致调用addUrlTag()时触发"未初始化"错误。
  2. 构造函数初始化后无效原因:Job中的SitemapService实例是独立的,其$xml对象和主进程中的实例互不关联,Job添加的URL不会同步到主进程的XML对象中,最终主进程调用divide()时只处理了初始的首页URL,生成空文件。

解决方案

方案1:让Job直接生成并写入分片XML

放弃共享XML对象的思路,让每个Job直接生成对应分片的XML文件,最后由主进程生成索引文件:

  1. 修改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());
}
  1. 修改SitemapService的make()方法:
    • 先处理首页URL并写入基础XML文件;
    • 等待所有Job执行完成;
    • 合并所有临时XML文件的URL标签,再调用divide()生成最终分片和索引。

方案2:使用缓存存储URL列表

让Job将生成的URL存入缓存,主进程等待所有Job完成后再从缓存读取所有URL生成XML:

  1. 修改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);
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 18:15:55