├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── VERSION
├── bin
├── Console.php
└── release.php
├── composer.json
├── phpunit.ci.xml
├── phpunit.dist.xml
├── src
├── AbstractSitemap.php
├── ChangeFreq.php
├── NewsSitemap.php
├── Sitemap.php
└── SitemapIndex.php
└── test
├── SitemapIndexTest.php
└── SitemapTest.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: UnitTest
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | strategy:
8 | matrix:
9 | php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | # PHP
14 | - name: Setup PHP
15 | uses: shivammathur/setup-php@v2
16 | with:
17 | php-version: ${{ matrix.php-versions }}
18 | # extensions: mbstring
19 | - name: Get composer cache directory
20 | id: composercache
21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
22 | - name: Cache composer dependencies
23 | uses: actions/cache@v2
24 | with:
25 | path: ${{ steps.composercache.outputs.dir }}
26 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
27 | restore-keys: ${{ runner.os }}-composer-
28 | - name: Install dependencies
29 | run: composer update --prefer-dist --prefer-stable --no-progress --no-suggest
30 |
31 | - name: Run test suite
32 | run: php vendor/bin/phpunit --configuration phpunit.ci.xml
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # System
2 | .idea
3 | !.gitignore
4 |
5 | # Composer
6 | /vendor/*
7 | composer.lock
8 |
9 | # Test
10 | phpunit.xml
11 | .phpunit.result.cache
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Simon Asika
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Sitemap
2 |
3 |
4 | 
5 | [
6 | ](https://packagist.org/packages/asika/sitemap)
7 | [](https://packagist.org/packages/asika/sitemap)
8 |
9 | PHP Simple Sitemap Generator. Follows the [W3C Sitemap Protocol](http://www.sitemaps.org/protocol.html)
10 |
11 |
12 | * [PHP Sitemap](#php-sitemap)
13 | * [Installation via Composer](#installation-via-composer)
14 | * [Getting Started](#getting-started)
15 | * [Render it to XML:](#render-it-to-xml)
16 | * [Arguments](#arguments)
17 | * [loc](#loc)
18 | * [changefreq](#changefreq)
19 | * [priority](#priority)
20 | * [lastmod](#lastmod)
21 | * [Google News Sitemap](#google-news-sitemap)
22 | * [Using Sitemap index files (to group multiple sitemap files)](#using-sitemap-index-files-to-group-multiple-sitemap-files)
23 | * [More](#more)
24 |
25 |
26 | ## Installation via Composer
27 |
28 | ```shell
29 | composer require
30 | ```
31 |
32 | ## Getting Started
33 |
34 | Create a sitemap object:
35 |
36 | ```php
37 | use Asika\Sitemap\Sitemap;
38 |
39 | $sitemap = new Sitemap();
40 | ```
41 |
42 | Add items to sitemap:
43 |
44 | ```php
45 | $sitemap->addItem($url);
46 | $sitemap->addItem($url);
47 | $sitemap->addItem($url);
48 | ```
49 |
50 | You can add some optional params.
51 |
52 | ```php
53 | use Asika\Sitemap\ChangeFreq;
54 |
55 | $sitemap->addItem($url, '1.0', ChangeFreq::DAILY, '2015-06-07 10:51:20');
56 | $sitemap->addItem($url, '0.7', ChangeFreq::WEEKLY, new \DateTime('2015-06-03 11:24:20'));
57 | ```
58 |
59 | The arguments are `loc`, `priority`, `changefreq` and `lastmod`. See this table:
60 |
61 | | Params | Required | Description |
62 | |--------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
63 | | `loc` | required | URL of the page. This URL must begin with the protocol (such as http) and end with a trailing slash, if your web server requires it. This value must be less than 2,048 characters. |
64 | | `priority` | optional | The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0. This value does not affect how your pages are compared to pages on other sites—it only lets the search engines know which pages you deem most important for the crawlers. |
65 | | `changefreq` | optional | How frequently the page is likely to change. This value provides general information to search engines and may not correlate exactly to how often they crawl the page. |
66 | | `lastmod` | optional | The date of last modification of the file. This date should be in [W3C Datetime format](http://www.w3.org/TR/NOTE-datetime). This format allows you to omit the time portion, if desired, and use YYYY-MM-DD. |
67 |
68 | See: http://www.sitemaps.org/protocol.html#xmlTagDefinitions
69 |
70 | ### Render it to XML:
71 |
72 | ```php
73 | echo $sitemap->render();
74 |
75 | // OR
76 |
77 | (string) $sitemap;
78 | ```
79 |
80 | This is an example to send it as real sitemap for Google or other search engine:
81 |
82 | ```php
83 | header('Content-Type: application/xml');
84 |
85 | echo $sitemap;
86 |
87 | exit();
88 | ```
89 |
90 | Use `output()` to instantly print header and XML body:
91 |
92 | ```php
93 | $sitemap->output();
94 |
95 | exit();
96 | ```
97 |
98 | Handle Psr7 Response
99 |
100 | ```php
101 | $response = new Response();
102 |
103 | $response = $sitemap->handleResponse($response);
104 |
105 | return $response;
106 | ```
107 |
108 | The XML output in browser:
109 |
110 | ```xml
111 |
112 |
113 |
114 | http://sitemap.io
115 |
116 |
117 | http://sitemap.io/foo/bar/?flower=sakura&fly=bird
118 | daily
119 | 1.0
120 | 2015-06-07T10:51:20+02:00
121 |
122 |
123 | ```
124 |
125 | ## Arguments
126 |
127 | ### loc
128 |
129 | The URL will be auto escaped. For example, the `&`, `>` will convert to `&`, `>`.
130 |
131 | If you want to escape it yourself, set auto escape off:
132 |
133 | ```php
134 | $sitemap->setAutoEscape(false);
135 | ```
136 |
137 | See: http://www.sitemaps.org/protocol.html#escaping
138 |
139 | ### changefreq
140 |
141 | Valid values are:
142 |
143 | ```php
144 | ChangeFreq::ALWAYS;
145 | ChangeFreq::HOURLY;
146 | ChangeFreq::DAILY;
147 | ChangeFreq::WEEKLY;
148 | ChangeFreq::MONTHLY;
149 | ChangeFreq::YEARLY;
150 | ChangeFreq::NEVER;
151 | ```
152 |
153 | The value `always` should be used to describe documents that change each time they are accessed.
154 |
155 | The value `never` should be used to describe archived URLs.
156 |
157 | Please note that the value of this tag is considered a hint and not a command. Even though search engine crawlers may consider this information when making decisions,
158 | they may crawl pages marked `hourly` less frequently than that, and they may crawl pages marked `yearly` more frequently than that.
159 |
160 | Crawlers may periodically crawl pages marked `never` so that they can handle unexpected changes to those pages.
161 |
162 | ### priority
163 |
164 | The default priority of a page is `0.5`.
165 | Please note that the priority you assign to a page is not likely to influence the position of your URLs in a search engine's result pages. Search engines may use this information when selecting between URLs on the same site, so you can use this tag to increase the likelihood that your most important pages are present in a search index.
166 | Also, please note that assigning a high priority to all of the URLs on your site is not likely to help you. Since the priority is relative, it is only used to select between URLs on your site.
167 |
168 | ### lastmod
169 |
170 | Your date format will auto convert to [W3c Datetime format](http://www.w3.org/TR/NOTE-datetime). for example, if you send
171 | a string look like: `2015-06-07 10:51:20`, Sitemap object will auto convert it to `2015-06-07T10:51:20+02:00`.
172 |
173 | You can set the format you want:
174 |
175 | ```php
176 | $sitemap->setDateFormat(\DateTimeInterface::ISO8601);
177 |
178 | // OR
179 |
180 | $sitemap->setDateFormat('Y-m-d');
181 | ```
182 |
183 | ## Google News Sitemap
184 |
185 | Please see [Google News Sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap?visit_id=637247859078479568-4208069007&rd=3) document.
186 |
187 | ```php
188 | $sitemap = new \Asika\Sitemap\NewsSitemap();
189 |
190 | $sitemap->addItem(
191 | $url,
192 | $newsTitle,
193 | 'Publication Name',
194 | 'en-us',
195 | $publishedDate
196 | );
197 | ```
198 |
199 | The format:
200 |
201 | ```xml
202 |
203 |
205 |
206 | http://www.example.org/business/article55.html
207 |
208 |
209 | The Example Times
210 | en
211 |
212 | 2008-12-23
213 | Companies A, B in Merger Talks
214 |
215 |
216 |
217 | ```
218 |
219 | ## Using Sitemap index files (to group multiple sitemap files)
220 |
221 | ```php
222 | use Asika\Sitemap\SitemapIndex;
223 |
224 | $index = new SitemapIndex();
225 |
226 | $index->addItem('http://domain.com/sitemap1.xml', $lastmod1);
227 | $index->addItem('http://domain.com/sitemap2.xml', $lastmod2);
228 |
229 | echo $index->render();
230 | ```
231 |
232 | Output:
233 |
234 | ```xml
235 |
236 |
237 |
238 | http://domain.com/sitemap1.xml
239 | 2015-06-07T10:51:20+02:00
240 |
241 |
242 | http://domain.com/sitemap2.xml
243 | 2015-06-07T10:51:20+02:00
244 |
245 |
246 | ```
247 |
248 | See: http://www.sitemaps.org/protocol.html#index
249 |
250 | ## More
251 |
252 | - [Extending the Sitemaps protocol](http://www.sitemaps.org/protocol.html#extending)
253 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 2.1.1
2 |
--------------------------------------------------------------------------------
/bin/Console.php:
--------------------------------------------------------------------------------
1 | array('n', 'no', 'false', 0, '0', true),
60 | 1 => array('y', 'yes', 'true', 1, '1', false, null)
61 | );
62 |
63 | /**
64 | * CliInput constructor.
65 | *
66 | * @param array $argv
67 | */
68 | public function __construct($argv = null)
69 | {
70 | $this->parseArgv($argv ?: $_SERVER['argv']);
71 |
72 | $this->init();
73 | }
74 |
75 | /**
76 | * init
77 | *
78 | * @return void
79 | */
80 | protected function init()
81 | {
82 | // Override if necessary
83 | }
84 |
85 | /**
86 | * execute
87 | *
88 | * @param \Closure|null $callback
89 | *
90 | * @return int
91 | */
92 | public function execute(\Closure $callback = null)
93 | {
94 | try {
95 | if ($this->getOption($this->helpOptions)) {
96 | $this->out($this->getHelp());
97 |
98 | return 0;
99 | }
100 |
101 | if ($callback) {
102 | if (PHP_VERSION_ID >= 50400) {
103 | $callback = $callback->bindTo($this);
104 | }
105 |
106 | $result = call_user_func($callback, $this);
107 | } else {
108 | $result = $this->doExecute();
109 | }
110 | } catch (\Exception $e) {
111 | $result = $this->handleException($e);
112 | } catch (\Throwable $e) {
113 | $result = $this->handleException($e);
114 | }
115 |
116 | if ($result === true) {
117 | $result = 0;
118 | } elseif ($result === false) {
119 | $result = 255;
120 | } else {
121 | $result = (bool) $result;
122 | }
123 |
124 | return (int) $result;
125 | }
126 |
127 | /**
128 | * doExecute
129 | *
130 | * @return mixed
131 | */
132 | protected function doExecute()
133 | {
134 | // Please override this method.
135 | return 0;
136 | }
137 |
138 | /**
139 | * delegate
140 | *
141 | * @param string $method
142 | *
143 | * @return mixed
144 | */
145 | protected function delegate($method)
146 | {
147 | $args = func_get_args();
148 | array_shift($args);
149 |
150 | if (!is_callable(array($this, $method))) {
151 | throw new \LogicException(sprintf('Method: %s not found', $method));
152 | }
153 |
154 | return call_user_func_array(array($this, $method), $args);
155 | }
156 |
157 | /**
158 | * getHelp
159 | *
160 | * @return string
161 | */
162 | protected function getHelp()
163 | {
164 | return trim($this->help);
165 | }
166 |
167 | /**
168 | * handleException
169 | *
170 | * @param \Exception|\Throwable $e
171 | *
172 | * @return int
173 | */
174 | protected function handleException($e)
175 | {
176 | $v = $this->getOption('v');
177 |
178 | if ($e instanceof CommandArgsException) {
179 | $this->err('[Warning] ' . $e->getMessage())
180 | ->err()
181 | ->err($this->getHelp());
182 | } else {
183 | $this->err('[Error] ' . $e->getMessage());
184 | }
185 |
186 | if ($v) {
187 | $this->err('[Backtrace]:')
188 | ->err($e->getTraceAsString());
189 | }
190 |
191 | $code = $e->getCode();
192 |
193 | return $code === 0 ? 255 : $code;
194 | }
195 |
196 | /**
197 | * getArgument
198 | *
199 | * @param int $offset
200 | * @param mixed $default
201 | *
202 | * @return mixed|null
203 | */
204 | public function getArgument($offset, $default = null)
205 | {
206 | if (!isset($this->args[$offset])) {
207 | return $default;
208 | }
209 |
210 | return $this->args[$offset];
211 | }
212 |
213 | /**
214 | * setArgument
215 | *
216 | * @param int $offset
217 | * @param mixed $value
218 | *
219 | * @return static
220 | */
221 | public function setArgument($offset, $value)
222 | {
223 | $this->args[$offset] = $value;
224 |
225 | return $this;
226 | }
227 |
228 | /**
229 | * getOption
230 | *
231 | * @param string|array $name
232 | * @param mixed $default
233 | *
234 | * @return mixed|null
235 | */
236 | public function getOption($name, $default = null)
237 | {
238 | $name = (array) $name;
239 |
240 | foreach ($name as $n) {
241 | if (isset($this->options[$n])) {
242 | return $this->options[$n];
243 | }
244 | }
245 |
246 | return $default;
247 | }
248 |
249 | /**
250 | * setOption
251 | *
252 | * @param string|array $name
253 | * @param mixed $value
254 | *
255 | * @return static
256 | */
257 | public function setOption($name, $value)
258 | {
259 | $name = (array) $name;
260 |
261 | foreach ($name as $n) {
262 | $this->options[$n] = $value;
263 | }
264 |
265 | return $this;
266 | }
267 |
268 | /**
269 | * out
270 | *
271 | * @param string $text
272 | * @param boolean $nl
273 | *
274 | * @return static
275 | */
276 | public function out($text = null, $nl = true)
277 | {
278 | fwrite(STDOUT, $text . ($nl ? "\n" : ''));
279 |
280 | return $this;
281 | }
282 |
283 | /**
284 | * err
285 | *
286 | * @param string $text
287 | * @param boolean $nl
288 | *
289 | * @return static
290 | */
291 | public function err($text = null, $nl = true)
292 | {
293 | fwrite(STDERR, $text . ($nl ? "\n" : ''));
294 |
295 | return $this;
296 | }
297 |
298 | /**
299 | * in
300 | *
301 | * @param string $ask
302 | * @param mixed $default
303 | *
304 | * @return string
305 | */
306 | public function in($ask = '', $default = null, $bool = false)
307 | {
308 | $this->out($ask, false);
309 |
310 | $in = rtrim(fread(STDIN, 8192), "\n\r");
311 |
312 | if ($bool) {
313 | $in = $in === '' ? $default : $in;
314 |
315 | return (bool) $this->mapBoolean($in);
316 | }
317 |
318 | return $in === '' ? (string) $default : $in;
319 | }
320 |
321 | /**
322 | * mapBoolean
323 | *
324 | * @param string $in
325 | *
326 | * @return bool
327 | */
328 | public function mapBoolean($in)
329 | {
330 | $in = strtolower((string) $in);
331 |
332 | if (in_array($in, $this->booleanMapping[0], true)) {
333 | return false;
334 | }
335 |
336 | if (in_array($in, $this->booleanMapping[1], true)) {
337 | return true;
338 | }
339 |
340 | return null;
341 | }
342 |
343 | /**
344 | * exec
345 | *
346 | * @param string $command
347 | *
348 | * @return static
349 | */
350 | protected function exec($command)
351 | {
352 | $this->out('>> ' . $command);
353 |
354 | system($command);
355 |
356 | return $this;
357 | }
358 |
359 | /**
360 | * parseArgv
361 | *
362 | * @param array $argv
363 | *
364 | * @return void
365 | */
366 | protected function parseArgv($argv)
367 | {
368 | $this->executable = array_shift($argv);
369 | $key = null;
370 |
371 | $out = array();
372 |
373 | for ($i = 0, $j = count($argv); $i < $j; $i++) {
374 | $arg = $argv[$i];
375 |
376 | // --foo --bar=baz
377 | if (0 === strpos($arg, '--')) {
378 | $eqPos = strpos($arg, '=');
379 |
380 | // --foo
381 | if ($eqPos === false) {
382 | $key = substr($arg, 2);
383 |
384 | // --foo value
385 | if ($i + 1 < $j && $argv[$i + 1][0] !== '-') {
386 | $value = $argv[$i + 1];
387 | $i++;
388 | } else {
389 | $value = isset($out[$key]) ? $out[$key] : true;
390 | }
391 |
392 | $out[$key] = $value;
393 | } else {
394 | // --bar=baz
395 | $key = substr($arg, 2, $eqPos - 2);
396 | $value = substr($arg, $eqPos + 1);
397 | $out[$key] = $value;
398 | }
399 | } elseif (0 === strpos($arg, '-')) {
400 | // -k=value -abc
401 |
402 | // -k=value
403 | if (isset($arg[2]) && $arg[2] === '=') {
404 | $key = $arg[1];
405 | $value = substr($arg, 3);
406 | $out[$key] = $value;
407 | } else {
408 | // -abc
409 | $chars = str_split(substr($arg, 1));
410 |
411 | foreach ($chars as $char) {
412 | $key = $char;
413 | $out[$key] = isset($out[$key]) ? $out[$key] + 1 : 1;
414 | }
415 |
416 | // -a a-value
417 | if (($i + 1 < $j) && ($argv[$i + 1][0] !== '-') && (count($chars) === 1)) {
418 | $out[$key] = $argv[$i + 1];
419 | $i++;
420 | }
421 | }
422 | } else {
423 | // Plain-arg
424 | $this->args[] = $arg;
425 | }
426 | }
427 |
428 | $this->options = $out;
429 | }
430 | }
431 |
432 | class CommandArgsException extends \RuntimeException
433 | {
434 | }
435 |
--------------------------------------------------------------------------------
/bin/release.php:
--------------------------------------------------------------------------------
1 |
24 |
25 | [Options]
26 | h | help Show help information
27 | v Show more debug information.
28 | --dry-run Dry run without git push or commit.
29 | HELP;
30 |
31 | /**
32 | * doExecute
33 | *
34 | * @return bool|mixed
35 | *
36 | * @since __DEPLOY_VERSION__
37 | */
38 | protected function doExecute()
39 | {
40 | $currentVersion = trim(file_get_contents(__DIR__ . '/../VERSION'));
41 | $targetVersion = $this->getArgument(0);
42 |
43 | if (!$targetVersion) {
44 | $targetVersion = static::versionPlus($currentVersion, 1);
45 | }
46 |
47 | $this->out('Release version: ' . $targetVersion);
48 |
49 | static::writeVersion($targetVersion);
50 | $this->replaceDocblockTags($targetVersion);
51 |
52 | $this->exec(sprintf('git commit -am "Release version: %s"', $targetVersion));
53 | $this->exec(sprintf('git tag %s', $targetVersion));
54 |
55 | $this->exec('git push');
56 | $this->exec('git push --tags');
57 |
58 | return true;
59 | }
60 |
61 | /**
62 | * writeVersion
63 | *
64 | * @param string $version
65 | *
66 | * @return bool|int
67 | *
68 | * @since __DEPLOY_VERSION__
69 | */
70 | protected static function writeVersion(string $version)
71 | {
72 | return file_put_contents(static::versionFile(), $version . "\n");
73 | }
74 |
75 | /**
76 | * versionFile
77 | *
78 | * @return string
79 | *
80 | * @since __DEPLOY_VERSION__
81 | */
82 | protected static function versionFile(): string
83 | {
84 | return __DIR__ . '/../VERSION';
85 | }
86 |
87 | /**
88 | * versionPlus
89 | *
90 | * @param string $version
91 | * @param int $offset
92 | * @param string $suffix
93 | *
94 | * @return string
95 | *
96 | * @since __DEPLOY_VERSION__
97 | */
98 | protected static function versionPlus(string $version, int $offset, string $suffix = ''): string
99 | {
100 | [$version] = explode('-', $version, 2);
101 |
102 | $numbers = explode('.', $version);
103 |
104 | if (!isset($numbers[2])) {
105 | $numbers[2] = 0;
106 | }
107 |
108 | $numbers[2] += $offset;
109 |
110 | if ($numbers[2] === 0) {
111 | unset($numbers[2]);
112 | }
113 |
114 | $version = implode('.', $numbers);
115 |
116 | if ($suffix) {
117 | $version .= '-' . $suffix;
118 | }
119 |
120 | return $version;
121 | }
122 |
123 | /**
124 | * replaceDocblockTags
125 | *
126 | * @param string $version
127 | *
128 | * @return void
129 | */
130 | protected function replaceDocblockTags(string $version): void
131 | {
132 | $this->out('Replacing Docblock...');
133 |
134 | $files = new RecursiveIteratorIterator(
135 | new \RecursiveDirectoryIterator(
136 | __DIR__ . '/../src',
137 | \FilesystemIterator::SKIP_DOTS
138 | )
139 | );
140 |
141 | /** @var \SplFileInfo $file */
142 | foreach ($files as $file) {
143 | if ($file->isDir() || $file->getExtension() !== 'php') {
144 | continue;
145 | }
146 |
147 | $content = file_get_contents($file->getPathname());
148 |
149 | $content = str_replace(
150 | ['{DEPLOY_VERSION}', '__DEPLOY_VERSION__', '__LICENSE__', '${ORGANIZATION}', '{ORGANIZATION}'],
151 | [$version, $version, 'MIT License', 'LYRASOFT', 'LYRASOFT'],
152 | $content
153 | );
154 |
155 | file_put_contents($file->getPathname(), $content);
156 | }
157 |
158 | $this->exec('git checkout master');
159 | $this->exec(sprintf('git commit -am "Prepare for %s release."', $version));
160 | $this->exec('git push origin master');
161 | }
162 |
163 | /**
164 | * exec
165 | *
166 | * @param string $command
167 | *
168 | * @return static
169 | */
170 | protected function exec($command)
171 | {
172 | $this->out('>> ' . $command);
173 |
174 | if (!$this->getOption('dry-run')) {
175 | system($command);
176 | }
177 |
178 | return $this;
179 | }
180 | }
181 |
182 | exit((new Build())->execute());
183 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "asika/sitemap",
3 | "type": "library",
4 | "description": "PHP Simple Sitemap Generator",
5 | "keywords": ["sitemap", "seo", "google"],
6 | "homepage": "https://github.com/asika32764/php-sitemap",
7 | "license": "MIT",
8 | "scripts": {
9 | "test": "phpunit"
10 | },
11 | "require": {
12 | "php": ">=8.1",
13 | "ext-simplexml": "*"
14 | },
15 | "require-dev": {
16 | "windwalker/test": "^4.0",
17 | "phpunit/phpunit": "^10.3||^11.0",
18 | "windwalker/dom": "^4.0",
19 | "psr/http-message": "^2.0"
20 | },
21 | "autoload": {
22 | "psr-4": {
23 | "Asika\\Sitemap\\": "src/"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/phpunit.ci.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | test
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/phpunit.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | test
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/AbstractSitemap.php:
--------------------------------------------------------------------------------
1 | xmlns = $xmlns ?: $this->xmlns;
86 | $this->encoding = $encoding;
87 | $this->xmlVersion = $xmlVersion;
88 |
89 | $this->xml = $this->getSimpleXmlElement();
90 | }
91 |
92 | /**
93 | * @return SimpleXMLElement
94 | */
95 | public function getSimpleXmlElement(): SimpleXMLElement
96 | {
97 | return $this->xml ??= simplexml_load_string(
98 | sprintf(
99 | '<%s xmlns="%s" />',
100 | $this->xmlVersion,
101 | $this->encoding,
102 | $this->root,
103 | $this->xmlns
104 | )
105 | );
106 | }
107 |
108 | /**
109 | * toString
110 | *
111 | * @return string
112 | */
113 | public function render(): string
114 | {
115 | return $this->xml->asXML();
116 | }
117 |
118 | /**
119 | * @return string
120 | */
121 | public function __toString()
122 | {
123 | return $this->render();
124 | }
125 |
126 | public function handleResponse(
127 | ResponseInterface $response,
128 | ?StreamInterface $body = null
129 | ): ResponseInterface {
130 | $body ??= $response->getBody();
131 | $body->rewind();
132 | $body->write($this->render());
133 |
134 | return $response->withHeader('content-type', $this->contentType)
135 | ->withBody($body);
136 | }
137 |
138 | public function output(): void
139 | {
140 | header('Content-Type: ' . $this->contentType);
141 |
142 | echo $this->render();
143 | }
144 |
145 | /**
146 | * Method to get property AutoEscape
147 | *
148 | * @return bool
149 | */
150 | public function getAutoEscape(): bool
151 | {
152 | return $this->autoEscape;
153 | }
154 |
155 | /**
156 | * Method to set property autoEscape
157 | *
158 | * @param bool $autoEscape
159 | *
160 | * @return static Return self to support chaining.
161 | */
162 | public function setAutoEscape(bool $autoEscape): static
163 | {
164 | $this->autoEscape = $autoEscape;
165 |
166 | return $this;
167 | }
168 |
169 | /**
170 | * Method to get property DateFormat
171 | *
172 | * @return string
173 | */
174 | public function getDateFormat(): string
175 | {
176 | return $this->dateFormat;
177 | }
178 |
179 | /**
180 | * Method to set property dateFormat
181 | *
182 | * @param string $dateFormat
183 | *
184 | * @return static Return self to support chaining.
185 | */
186 | public function setDateFormat(string $dateFormat): static
187 | {
188 | $this->dateFormat = $dateFormat;
189 |
190 | return $this;
191 | }
192 |
193 | public function getContentType(): string
194 | {
195 | return $this->contentType;
196 | }
197 |
198 | /**
199 | * @param string $contentType
200 | *
201 | * @return static Return self to support chaining.
202 | */
203 | public function setContentType(string $contentType): static
204 | {
205 | $this->contentType = $contentType;
206 |
207 | return $this;
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/ChangeFreq.php:
--------------------------------------------------------------------------------
1 | xml['xmlns:news'] = $this->newsXmlns;
31 | $this->xml->registerXPathNamespace('news', $this->newsXmlns);
32 | }
33 |
34 | /**
35 | * @param string|\Stringable $loc
36 | * @param string $title
37 | * @param string $publicationName
38 | * @param string $language
39 | * @param DateTimeInterface|string $publicationDate
40 | *
41 | * @return static
42 | * @throws Exception
43 | */
44 | public function addItem(
45 | string|\Stringable $loc,
46 | string $title,
47 | string $publicationName,
48 | string $language,
49 | DateTimeInterface|string $publicationDate,
50 | ): static {
51 | $loc = (string) $loc;
52 |
53 | if ($this->autoEscape) {
54 | $loc = htmlspecialchars($loc);
55 | }
56 |
57 | $url = $this->xml->addChild('url');
58 |
59 | if ($url === null) {
60 | throw new UnexpectedValueException('Add URL to XML failed.');
61 | }
62 |
63 | $url->addChild('loc', $loc);
64 | $news = $url->addChild('xmlns:news:news');
65 |
66 | if ($news === null) {
67 | throw new UnexpectedValueException('Add URL to XML failed.');
68 | }
69 |
70 | $publication = $news->addChild('xmlns:news:publication');
71 |
72 | if ($publication === null) {
73 | throw new UnexpectedValueException('Add URL to XML failed.');
74 | }
75 |
76 | $publication->addChild('xmlns:news:name', $publicationName);
77 | $publication->addChild('xmlns:news:language', $language);
78 |
79 | if (!($publicationDate instanceof DateTimeInterface)) {
80 | $publicationDate = new DateTimeImmutable($publicationDate);
81 | }
82 |
83 | $news->addChild('xmlns:news:publication_date', $publicationDate->format($this->dateFormat));
84 | $news->addChild('xmlns:news:title', $title);
85 |
86 | return $this;
87 | }
88 |
89 | public function getNewsXmlns(): string
90 | {
91 | return $this->newsXmlns;
92 | }
93 |
94 | public function setNewsXmlns(string $newsXmlns): static
95 | {
96 | $this->newsXmlns = $newsXmlns;
97 |
98 | return $this;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Sitemap.php:
--------------------------------------------------------------------------------
1 | autoEscape) {
42 | $loc = htmlspecialchars($loc);
43 | }
44 |
45 | $url = $this->xml->addChild('url');
46 |
47 | if ($url === null) {
48 | throw new UnexpectedValueException('Add URL to XML failed.');
49 | }
50 |
51 | $url->addChild('loc', $loc);
52 |
53 | if ($changeFreq) {
54 | if ($changeFreq instanceof ChangeFreq) {
55 | $changeFreq = $changeFreq->value;
56 | }
57 |
58 | $url->addChild('changefreq', (string) $changeFreq);
59 | }
60 |
61 | if ($priority) {
62 | $url->addChild('priority', (string) $priority);
63 | }
64 |
65 | if ($lastmod) {
66 | if (!($lastmod instanceof DateTimeInterface)) {
67 | $lastmod = new DateTimeImmutable($lastmod);
68 | }
69 |
70 | $url->addChild('lastmod', $lastmod->format($this->dateFormat));
71 | }
72 |
73 | return $this;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/SitemapIndex.php:
--------------------------------------------------------------------------------
1 | autoEscape) {
36 | $loc = htmlspecialchars($loc);
37 | }
38 |
39 | $sitemap = $this->xml->addChild('sitemap');
40 |
41 | if ($sitemap === null) {
42 | throw new UnexpectedValueException('Add Sitemap to XML failed.');
43 | }
44 |
45 | $sitemap->addChild('loc', $loc);
46 |
47 | if ($lastmod) {
48 | if (!($lastmod instanceof DateTimeInterface)) {
49 | $lastmod = new DateTimeImmutable($lastmod);
50 | }
51 |
52 | $sitemap->addChild('lastmod', $lastmod->format($this->dateFormat));
53 | }
54 |
55 | return $this;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/SitemapIndexTest.php:
--------------------------------------------------------------------------------
1 | instance = new SitemapIndex();
40 | }
41 |
42 | /**
43 | * testAddItem
44 | *
45 | * @return void
46 | */
47 | public function testAddItem(): void
48 | {
49 | $this->instance->addItem('http://windwalker.io/sitemap.xml');
50 |
51 | $xml = <<
53 |
54 |
55 | http://windwalker.io/sitemap.xml
56 |
57 |
58 | XML;
59 |
60 | self::assertDomStringEqualsDomString($xml, $this->instance->render());
61 |
62 | $this->instance->addItem('http://windwalker.io/sitemap2.xml', '2015-06-07 10:51:20');
63 |
64 | $xml = <<
66 |
67 |
68 | http://windwalker.io/sitemap.xml
69 |
70 |
71 | http://windwalker.io/sitemap2.xml
72 | 2015-06-07T10:51:20+00:00
73 |
74 |
75 | XML;
76 |
77 | self::assertDomStringEqualsDomString($xml, $this->instance->render());
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/test/SitemapTest.php:
--------------------------------------------------------------------------------
1 | instance = new Sitemap();
41 | }
42 |
43 | /**
44 | * testAddItem
45 | *
46 | * @return void
47 | */
48 | public function testAddItem(): void
49 | {
50 | $this->instance->addItem('http://windwalker.io');
51 |
52 | $xml = <<
54 |
55 |
56 | http://windwalker.io
57 |
58 |
59 | XML;
60 |
61 | self::assertDomStringEqualsDomString($xml, $this->instance->render());
62 |
63 | $this->instance->addItem('http://windwalker.io/foo/bar/?flower=sakura&fly=bird', '1.0', ChangeFreq::DAILY, '2015-06-07 10:51:20');
64 |
65 | $xml = <<
67 |
68 |
69 | http://windwalker.io
70 |
71 |
72 | http://windwalker.io/foo/bar/?flower=sakura&fly=bird
73 | daily
74 | 1.0
75 | 2015-06-07T10:51:20+00:00
76 |
77 |
78 | XML;
79 |
80 | self::assertDomStringEqualsDomString($xml, $this->instance->render());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------