• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / cache-warmup / 12166579903

04 Dec 2024 07:08PM UTC coverage: 86.147% (-4.2%) from 90.313%
12166579903

Pull #421

github

web-flow
Merge 428b1186a into 48195b2a3
Pull Request #421: [TASK] Use simple XML parsing to reduce high memory load

78 of 159 new or added lines in 9 files covered. (49.06%)

1 existing line in 1 file now uncovered.

1480 of 1718 relevant lines covered (86.15%)

8.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.51
/src/Xml/XmlParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "eliashaeussler/cache-warmup".
7
 *
8
 * Copyright (C) 2020-2024 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace EliasHaeussler\CacheWarmup\Xml;
25

26
use EliasHaeussler\CacheWarmup\Exception;
27
use EliasHaeussler\CacheWarmup\Helper;
28
use EliasHaeussler\CacheWarmup\Result;
29
use EliasHaeussler\CacheWarmup\Sitemap;
30
use GuzzleHttp\Client;
31
use GuzzleHttp\ClientInterface;
32
use GuzzleHttp\Exception\GuzzleException;
33
use GuzzleHttp\Psr7;
34
use GuzzleHttp\RequestOptions;
35
use Netlogix\XmlProcessor;
36
use Psr\Http\Message;
37

38
use function array_map;
39
use function fclose;
40
use function fopen;
41
use function fread;
42
use function is_file;
43
use function is_readable;
44
use function is_resource;
45
use function restore_error_handler;
46
use function set_error_handler;
47
use function sha1;
48
use function sprintf;
49
use function sys_get_temp_dir;
50

51
/**
52
 * XmlParser.
53
 *
54
 * @author Elias Häußler <elias@haeussler.dev>
55
 * @license GPL-3.0-or-later
56
 */
57
final class XmlParser
58
{
59
    private readonly Node\SitemapNodeProcessor $sitemapProcessor;
60
    private readonly Node\SitemapNodeProcessor $urlProcessor;
61
    private readonly XmlProcessor\XmlProcessor $xmlProcessor;
62
    private readonly Node\SitemapNodeConverter $sitemapConverter;
63

64
    public function __construct(
9✔
65
        private readonly ClientInterface $client = new Client(),
66
    ) {
67
        $this->sitemapProcessor = new Node\SitemapNodeProcessor(
9✔
68
            Node\SitemapNodePath::Sitemap,
9✔
69
            [
9✔
70
                Node\SitemapNode::LastModificationDate,
9✔
71
                Node\SitemapNode::Location,
9✔
72
            ],
9✔
73
        );
9✔
74
        $this->urlProcessor = new Node\SitemapNodeProcessor(
9✔
75
            Node\SitemapNodePath::Url,
9✔
76
            [
9✔
77
                Node\SitemapNode::ChangeFrequency,
9✔
78
                Node\SitemapNode::LastModificationDate,
9✔
79
                Node\SitemapNode::Location,
9✔
80
                Node\SitemapNode::Priority,
9✔
81
            ],
9✔
82
        );
9✔
83
        $this->xmlProcessor = new XmlProcessor\XmlProcessor([
9✔
84
            $this->sitemapProcessor,
9✔
85
            $this->urlProcessor,
9✔
86
        ]);
9✔
87
        $this->sitemapConverter = new Node\SitemapNodeConverter();
9✔
88
    }
89

90
    /**
91
     * @throws Exception\FileIsMissing
92
     * @throws Exception\FileIsNotReadable
93
     * @throws Exception\SitemapCannotBeRead
94
     * @throws Exception\SitemapIsMalformed
95
     * @throws GuzzleException
96
     */
97
    public function parse(Sitemap\Sitemap $sitemap): Result\ParserResult
9✔
98
    {
99
        $filename = $this->fetchSitemapFile($sitemap);
9✔
100

101
        set_error_handler(
8✔
102
            static fn () => throw new Exception\SitemapCannotBeRead($sitemap),
8✔
103
        );
8✔
104

105
        $this->sitemapProcessor->reset();
8✔
106
        $this->urlProcessor->reset();
8✔
107

108
        try {
109
            $this->xmlProcessor->processFile($filename);
8✔
110
        } catch (Exception\XmlNodeIsEmpty $exception) {
2✔
111
            throw new Exception\SitemapIsMalformed($sitemap, $exception);
1✔
112
        } finally {
113
            restore_error_handler();
8✔
114
        }
115

116
        $sitemaps = $this->sitemapProcessor->getProcessedNodes();
6✔
117
        $urls = $this->urlProcessor->getProcessedNodes();
6✔
118

119
        return new Result\ParserResult(
6✔
120
            array_map(fn (array $node) => $this->sitemapConverter->convertSitemap($node, $sitemap), $sitemaps),
6✔
121
            array_map(fn (array $node) => $this->sitemapConverter->convertUrl($node, $sitemap), $urls),
6✔
122
        );
6✔
123
    }
124

125
    /**
126
     * @throws Exception\FileIsMissing
127
     * @throws Exception\FileIsNotReadable
128
     * @throws GuzzleException
129
     */
130
    private function fetchSitemapFile(Sitemap\Sitemap $sitemap): string
9✔
131
    {
132
        $uri = $sitemap->getUri();
9✔
133

134
        // Fetch XML source
135
        if ($sitemap->isLocalFile()) {
9✔
136
            $filename = $sitemap->getLocalFilePath();
2✔
137
        } else {
138
            $filename = $this->downloadSitemap($uri);
7✔
139
        }
140

141
        // Check if file exists
142
        if (!is_file($filename) || !is_readable($filename)) {
9✔
143
            throw new Exception\FileIsMissing($filename);
1✔
144
        }
145

146
        $file = fopen($filename, 'rb');
8✔
147

148
        if (!is_resource($file)) {
8✔
NEW
UNCOV
149
            throw new Exception\FileIsNotReadable($filename);
×
150
        }
151

152
        // Use built-in gzip decoding if necessary
153
        if (0 === mb_strpos((string) fread($file, 10), "\x1f\x8b\x08")) {
8✔
154
            $filename = 'compress.zlib://'.$filename;
1✔
155
        }
156

157
        fclose($file);
8✔
158

159
        return $filename;
8✔
160
    }
161

162
    /**
163
     * @throws GuzzleException
164
     */
165
    private function downloadSitemap(Message\UriInterface $uri): string
7✔
166
    {
167
        $filename = $this->createTemporaryFilename((string) $uri);
7✔
168

169
        $this->client->send(
7✔
170
            new Psr7\Request('GET', $uri),
7✔
171
            [
7✔
172
                RequestOptions::SINK => $filename,
7✔
173
            ],
7✔
174
        );
7✔
175

176
        return $filename;
7✔
177
    }
178

179
    private function createTemporaryFilename(string $identifier): string
7✔
180
    {
181
        return Helper\FilesystemHelper::joinPathSegments(
7✔
182
            sys_get_temp_dir(),
7✔
183
            sprintf('sitemap_%s.xml', sha1($identifier)),
7✔
184
        );
7✔
185
    }
186
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc