├── .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 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/asika32764/php-sitemap/ci.yml?style=for-the-badge) 5 | [![Packagist Version](https://img.shields.io/packagist/v/asika/sitemap?style=for-the-badge) 6 | ](https://packagist.org/packages/asika/sitemap) 7 | [![Packagist Downloads](https://img.shields.io/packagist/dt/asika/sitemap?style=for-the-badge)](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 | --------------------------------------------------------------------------------