1

I'm trying to create a Job for my sitemap generator in Laravel. It should create a job for adding each 100 rows form my Cars model but I'm stuck at one error. As soon as I use sitemap:make command and my Job handler is trying to call a addUrlTag() methid I'm getting this error

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):

I guess that there is something wrong in my Job class logic, but I can't figure out what exactly. I appreciate any help! Thanks!

Here are my classes:

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();
    }
}

SitemapSerivce.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\n", 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);
        }
    }
}

I've tried to remove the addUrlTag() method from the handler and it works exactly how I need it, but as soon as I add the logic it can't find $xml variable.

I've also tried to add the __construct() method to make $xml variable accessible, but in this case even if it runs the Jobs without any errors it does not add any of the cars urls to the xml and the file looks empty:

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('/'),
        ];
    
        // ...
}
ADyson
  • 57,178
  • 14
  • 51
  • 63
  • Since `$this->xml` is only populated by calling the `make()` function, you must ensure to call the `make()` function before calling `addURLTag()`. In the `public function handle(SitemapService $sitemapService)` function, you do not do that. Either that or you need to provide anorther way for callers to initialise that variable without involving `make()`. It depends what the logic should be, as to how you would resolve it. But the simple fact of the error is correct - you can't expect to use a variable which hasn't been populated yet. – ADyson Sep 01 '23 at 10:16
  • `it does not add any of the cars urls to the xml and the file looks empty`... these are two separate tasks. As far as I can see, `public function addUrlTag($url, $lastmod): void { $sitemapTag = $this->xml->addChild('url'); $sitemapTag->addChild('loc', $url); $sitemapTag->addChild('lastmod', $lastmod); }` this just populates stuff into `$this->xml`. So if you debug it, I suspect you'll find it _does_ populate `$this->xml` properly. But there's nothing in that function which then writes the data out to a file. – ADyson Sep 01 '23 at 10:23
  • Thank you for the quick response. I thought that if I'm calling a `make()` method inside my `sitemap:make` command it's counted as involved, but I guess I just didn't understand it correctly and when I call it inside command it's a different object even if this method will call the handler of a job which also uses a different object of a `SitemapService`. –  YahorLukyanchyk Sep 01 '23 at 10:36
  • Yes, as far as I know, each handler function will receive a new instance of the SitemapService class. – ADyson Sep 01 '23 at 10:42

0 Answers0