diff --git a/.gitignore b/.gitignore index 5657f6e..22d0d82 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -vendor \ No newline at end of file +vendor diff --git a/.travis.yml b/.travis.yml index 43ed454..86c0ccc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,7 @@ language: php php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - 7.1 - nightly -matrix: - include: - - php: hhvm - dist: trusty - - php: 5.3 - dist: precise + before_script: - composer install diff --git a/README.md b/README.md index c64916b..a2fff7c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Features -------- - Create sitemap files: either regular or gzipped. -- Create multi-language sitemap files. +- Sitemap extensions support. Included extensions are multi-language sitemaps, video sitemaps, image siteamaps. - Create sitemap index files. - Automatically creates new file if either URL limit or file size limit is reached. - Fast and memory efficient. @@ -30,17 +30,29 @@ How to use it ------------- ```php -use samdark\sitemap\Sitemap; -use samdark\sitemap\Index; +use SamDark\Sitemap\Sitemap; +use SamDark\Sitemap\Index; // create sitemap $sitemap = new Sitemap(__DIR__ . '/sitemap.xml'); // add some URLs -$sitemap->addItem('http://example.com/mylink1'); -$sitemap->addItem('http://example.com/mylink2', time()); -$sitemap->addItem('http://example.com/mylink3', time(), Sitemap::HOURLY); -$sitemap->addItem('http://example.com/mylink4', time(), Sitemap::DAILY, 0.3); +$sitemap->addUrl(new Url('http://example.com/mylink1')); +$sitemap->addUrl( + (new Url('http://example.com/mylink2')) + ->setLastModified(new \DateTime()) +); +$sitemap->addUrl( + (new Url('http://example.com/mylink3')) + ->setLastModified(new \DateTime()) + ->setChangeFrequency(Frequency::HOURLY) +); +$sitemap->addUrl( + (new Url('http://example.com/mylink4')) + ->setChangeFrequency(Frequency::DAILY) + ->setLastModified(new \DateTime()) + ->setPriority(0.3) +); // write it $sitemap->write(); @@ -52,9 +64,9 @@ $sitemapFileUrls = $sitemap->getSitemapUrls('http://example.com/'); $staticSitemap = new Sitemap(__DIR__ . '/sitemap_static.xml'); // add some URLs -$staticSitemap->addItem('http://example.com/about'); -$staticSitemap->addItem('http://example.com/tos'); -$staticSitemap->addItem('http://example.com/jobs'); +$staticSitemap->addUrl(new Url('http://example.com/about')); +$staticSitemap->addUrl(new Url('http://example.com/tos')); +$staticSitemap->addUrl(new Url('http://example.com/jobs')); // write it $staticSitemap->write(); @@ -83,36 +95,31 @@ Multi-language sitemap ---------------------- ```php -use samdark\sitemap\Sitemap; +use SamDark\Sitemap\Sitemap; -// create sitemap -// be sure to pass `true` as second parameter to specify XHTML namespace -$sitemap = new Sitemap(__DIR__ . '/sitemap_multi_language.xml', true); - -// Set URL limit to fit in default limit of 50000 (default limit / number of languages) -$sitemap->setMaxUrls(25000); +// create sitemap declaring you need alternate links support +$sitemap = new Sitemap(__DIR__ . '/sitemap_multi_language.xml', [AlternateLink::class]); // add some URLs -$sitemap->addItem('http://example.com/mylink1'); - -$sitemap->addItem([ - 'ru' => 'http://example.com/ru/mylink2', - 'en' => 'http://example.com/en/mylink2', -], time()); -$sitemap->addItem([ - 'ru' => 'http://example.com/ru/mylink3', - 'en' => 'http://example.com/en/mylink3', -], time(), Sitemap::HOURLY); - -$sitemap->addItem([ - 'ru' => 'http://example.com/ru/mylink4', - 'en' => 'http://example.com/en/mylink4', -], time(), Sitemap::DAILY, 0.3); +$sitemap->addUrl( + (new Url('http://example.com/en/mylink2')) + ->setLastModified(new \DateTime()) + ->setChangeFrequency(Frequency::HOURLY) + ->add(new AlternateLink('en', 'http://example.com/en/mylink1')) + ->add(new AlternateLink('ru', 'http://example.com/ru/mylink1')) +); + +$sitemap->addUrl( + (new Url('http://example.com/en/mylink2')) + ->setLastModified(new \DateTime()) + ->setChangeFrequency(Frequency::HOURLY) + ->add(new AlternateLink('en', 'http://example.com/en/mylink2')) + ->add(new AlternateLink('ru', 'http://example.com/ru/mylink2')) +); // write it $sitemap->write(); - ``` Options diff --git a/Sitemap.php b/Sitemap.php deleted file mode 100644 index dc20fd7..0000000 --- a/Sitemap.php +++ /dev/null @@ -1,494 +0,0 @@ - - */ -class Sitemap -{ - const ALWAYS = 'always'; - const HOURLY = 'hourly'; - const DAILY = 'daily'; - const WEEKLY = 'weekly'; - const MONTHLY = 'monthly'; - const YEARLY = 'yearly'; - const NEVER = 'never'; - - /** - * @var integer Maximum allowed number of URLs in a single file. - */ - private $maxUrls = 50000; - - /** - * @var integer number of URLs added - */ - private $urlsCount = 0; - - /** - * @var integer Maximum allowed number of bytes in a single file. - */ - private $maxBytes = 10485760; - - /** - * @var integer number of bytes already written to the current file, before compression - */ - private $byteCount = 0; - - /** - * @var string path to the file to be written - */ - private $filePath; - - /** - * @var integer number of files written - */ - private $fileCount = 0; - - /** - * @var array path of files written - */ - private $writtenFilePaths = array(); - - /** - * @var integer number of URLs to be kept in memory before writing it to file - */ - private $bufferSize = 10; - - /** - * @var bool if XML should be indented - */ - private $useIndent = true; - - /** - * @var bool if should XHTML namespace be specified - * Useful for multi-language sitemap to point crawler to alternate language page via xhtml:link tag. - * @see https://support.google.com/webmasters/answer/2620865?hl=en - */ - private $useXhtml = false; - - /** - * @var array valid values for frequency parameter - */ - private $validFrequencies = array( - self::ALWAYS, - self::HOURLY, - self::DAILY, - self::WEEKLY, - self::MONTHLY, - self::YEARLY, - self::NEVER - ); - - /** - * @var bool whether to gzip the resulting files or not - */ - private $useGzip = false; - - /** - * @var WriterInterface that does the actual writing - */ - private $writerBackend; - - /** - * @var XMLWriter - */ - private $writer; - - /** - * @param string $filePath path of the file to write to - * @param bool $useXhtml is XHTML namespace should be specified - * - * @throws \InvalidArgumentException - */ - public function __construct($filePath, $useXhtml = false) - { - $dir = dirname($filePath); - if (!is_dir($dir)) { - throw new \InvalidArgumentException( - "Please specify valid file path. Directory not exists. You have specified: {$dir}." - ); - } - - $this->filePath = $filePath; - $this->useXhtml = $useXhtml; - } - - /** - * Get array of generated files - * @return array - */ - public function getWrittenFilePath() - { - return $this->writtenFilePaths; - } - - /** - * Creates new file - * @throws \RuntimeException if file is not writeable - */ - private function createNewFile() - { - $this->fileCount++; - $filePath = $this->getCurrentFilePath(); - $this->writtenFilePaths[] = $filePath; - - if (file_exists($filePath)) { - $filePath = realpath($filePath); - if (is_writable($filePath)) { - unlink($filePath); - } else { - throw new \RuntimeException("File \"$filePath\" is not writable."); - } - } - - if ($this->useGzip) { - if (function_exists('deflate_init') && function_exists('deflate_add')) { - $this->writerBackend = new DeflateWriter($filePath); - } else { - $this->writerBackend = new TempFileGZIPWriter($filePath); - } - } else { - $this->writerBackend = new PlainFileWriter($filePath); - } - - $this->writer = new XMLWriter(); - $this->writer->openMemory(); - $this->writer->startDocument('1.0', 'UTF-8'); - $this->writer->setIndent($this->useIndent); - $this->writer->startElement('urlset'); - $this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); - if ($this->useXhtml) { - $this->writer->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); - } - - /* - * XMLWriter does not give us much options, so we must make sure, that - * the header was written correctly and we can simply reuse any - * elements that did not fit into the previous file. (See self::flush) - */ - $this->writer->text(PHP_EOL); - $this->flush(true); - } - - /** - * Writes closing tags to current file - */ - private function finishFile() - { - if ($this->writer !== null) { - $this->writer->endElement(); - $this->writer->endDocument(); - - /* To prevent infinite recursion through flush */ - $this->urlsCount = 0; - - $this->flush(0); - $this->writerBackend->finish(); - $this->writerBackend = null; - - $this->byteCount = 0; - } - } - - /** - * Finishes writing - */ - public function write() - { - $this->finishFile(); - } - - /** - * Flushes buffer into file - * - * @param int $footSize Size of the remaining closing tags - * @throws \OverflowException - */ - private function flush($footSize = 10) - { - $data = $this->writer->flush(true); - $dataSize = mb_strlen($data, '8bit'); - - /* - * Limit the file size of each single site map - * - * We use a heuristic of 10 Bytes for the remainder of the file, - * i.e. plus a new line. - */ - if ($this->byteCount + $dataSize + $footSize > $this->maxBytes) { - if ($this->urlsCount <= 1) { - throw new \OverflowException('The buffer size is too big for the defined file size limit'); - } - $this->finishFile(); - $this->createNewFile(); - } - - $this->writerBackend->append($data); - $this->byteCount += $dataSize; - } - - /** - * Takes a string and validates, if the string - * is a valid url - * - * @param string $location - * @throws \InvalidArgumentException - */ - protected function validateLocation($location) { - if (false === filter_var($location, FILTER_VALIDATE_URL)) { - throw new \InvalidArgumentException( - "The location must be a valid URL. You have specified: {$location}." - ); - } - } - - /** - * Adds a new item to sitemap - * - * @param string|array $location location item URL - * @param integer $lastModified last modification timestamp - * @param string $changeFrequency change frequency. Use one of self:: constants here - * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5 - * - * @throws \InvalidArgumentException - */ - public function addItem($location, $lastModified = null, $changeFrequency = null, $priority = null) - { - if ($this->urlsCount >= $this->maxUrls) { - $this->finishFile(); - } - - if ($this->writerBackend === null) { - $this->createNewFile(); - } - - if (is_array($location)) { - $this->addMultiLanguageItem($location, $lastModified, $changeFrequency, $priority); - } else { - $this->addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority); - } - - $this->urlsCount++; - - if ($this->urlsCount % $this->bufferSize === 0) { - $this->flush(); - } - } - - - /** - * Adds a new single item to sitemap - * - * @param string $location location item URL - * @param integer $lastModified last modification timestamp - * @param float $changeFrequency change frequency. Use one of self:: constants here - * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5 - * - * @throws \InvalidArgumentException - * - * @see addItem - */ - private function addSingleLanguageItem($location, $lastModified, $changeFrequency, $priority) - { - $this->validateLocation($location); - - - $this->writer->startElement('url'); - - $this->writer->writeElement('loc', $location); - - if ($lastModified !== null) { - $this->writer->writeElement('lastmod', date('c', $lastModified)); - } - - if ($changeFrequency !== null) { - if (!in_array($changeFrequency, $this->validFrequencies, true)) { - throw new \InvalidArgumentException( - 'Please specify valid changeFrequency. Valid values are: ' - . implode(', ', $this->validFrequencies) - . "You have specified: {$changeFrequency}." - ); - } - - $this->writer->writeElement('changefreq', $changeFrequency); - } - - if ($priority !== null) { - if (!is_numeric($priority) || $priority < 0 || $priority > 1) { - throw new \InvalidArgumentException( - "Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}." - ); - } - $this->writer->writeElement('priority', number_format($priority, 1, '.', ',')); - } - - $this->writer->endElement(); - } - - /** - * Adds a multi-language item, based on multiple locations with alternate hrefs to sitemap - * - * @param array $locations array of language => link pairs - * @param integer $lastModified last modification timestamp - * @param float $changeFrequency change frequency. Use one of self:: constants here - * @param string $priority item's priority (0.0-1.0). Default null is equal to 0.5 - * - * @throws \InvalidArgumentException - * - * @see addItem - */ - private function addMultiLanguageItem($locations, $lastModified, $changeFrequency, $priority) - { - foreach ($locations as $language => $url) { - $this->validateLocation($url); - - $this->writer->startElement('url'); - - $this->writer->writeElement('loc', $url); - - if ($lastModified !== null) { - $this->writer->writeElement('lastmod', date('c', $lastModified)); - } - - if ($changeFrequency !== null) { - if (!in_array($changeFrequency, $this->validFrequencies, true)) { - throw new \InvalidArgumentException( - 'Please specify valid changeFrequency. Valid values are: ' - . implode(', ', $this->validFrequencies) - . "You have specified: {$changeFrequency}." - ); - } - - $this->writer->writeElement('changefreq', $changeFrequency); - } - - if ($priority !== null) { - if (!is_numeric($priority) || $priority < 0 || $priority > 1) { - throw new \InvalidArgumentException( - "Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}." - ); - } - $this->writer->writeElement('priority', number_format($priority, 1, '.', ',')); - } - - foreach ($locations as $hreflang => $href) { - - $this->writer->startElement('xhtml:link'); - $this->writer->startAttribute('rel'); - $this->writer->text('alternate'); - $this->writer->endAttribute(); - - $this->writer->startAttribute('hreflang'); - $this->writer->text($hreflang); - $this->writer->endAttribute(); - - $this->writer->startAttribute('href'); - $this->writer->text($href); - $this->writer->endAttribute(); - $this->writer->endElement(); - } - - $this->writer->endElement(); - } - } - - - /** - * @return string path of currently opened file - */ - private function getCurrentFilePath() - { - if ($this->fileCount < 2) { - return $this->filePath; - } - - $parts = pathinfo($this->filePath); - if ($parts['extension'] === 'gz') { - $filenameParts = pathinfo($parts['filename']); - if (!empty($filenameParts['extension'])) { - $parts['filename'] = $filenameParts['filename']; - $parts['extension'] = $filenameParts['extension'] . '.gz'; - } - } - return $parts['dirname'] . DIRECTORY_SEPARATOR . $parts['filename'] . '_' . $this->fileCount . '.' . $parts['extension']; - } - - /** - * Returns an array of URLs written - * - * @param string $baseUrl base URL of all the sitemaps written - * @return array URLs of sitemaps written - */ - public function getSitemapUrls($baseUrl) - { - $urls = array(); - foreach ($this->writtenFilePaths as $file) { - $urls[] = $baseUrl . pathinfo($file, PATHINFO_BASENAME); - } - return $urls; - } - - /** - * Sets maximum number of URLs to write in a single file. - * Default is 50000. - * @param integer $number - */ - public function setMaxUrls($number) - { - $this->maxUrls = (int)$number; - } - - /** - * Sets maximum number of bytes to write in a single file. - * Default is 10485760 or 10 MiB. - * @param integer $number - */ - public function setMaxBytes($number) - { - $this->maxBytes = (int)$number; - } - - /** - * Sets number of URLs to be kept in memory before writing it to file. - * Default is 10. - * - * @param integer $number - */ - public function setBufferSize($number) - { - $this->bufferSize = (int)$number; - } - - - /** - * Sets if XML should be indented. - * Default is true. - * - * @param bool $value - */ - public function setUseIndent($value) - { - $this->useIndent = (bool)$value; - } - - /** - * Sets whether the resulting files will be gzipped or not. - * @param bool $value - * @throws \RuntimeException when trying to enable gzip while zlib is not available or when trying to change - * setting when some items are already written - */ - public function setUseGzip($value) - { - if ($value && !extension_loaded('zlib')) { - throw new \RuntimeException('Zlib extension must be enabled to gzip the sitemap.'); - } - if ($this->writerBackend !== null && $value != $this->useGzip) { - throw new \RuntimeException('Cannot change the gzip value once items have been added to the sitemap.'); - } - $this->useGzip = $value; - } -} diff --git a/composer.json b/composer.json index fdf7d7c..9264b5b 100644 --- a/composer.json +++ b/composer.json @@ -1,4 +1,3 @@ - { "name": "samdark/sitemap", "description": "Sitemap and sitemap index builder", @@ -19,15 +18,29 @@ "source": "https://github.com/samdark/sitemap" }, "require": { - "php": ">=5.3.0", + "php": ">=7.1.0", "ext-xmlwriter": "*" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "~7.1.5", + "vimeo/psalm": "*" }, "autoload": { "psr-4": { - "samdark\\sitemap\\": "" + "SamDark\\Sitemap\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "SamDark\\Sitemap\\tests\\": "tests" + } + }, + "scripts": { + "test" : "@php vendor/bin/phpunit tests", + "psalm" : "@php vendor/bin/psalm", + "build" : [ + "@psalm", + "@test" + ] } } diff --git a/src/Extension/AlternateLink.php b/src/Extension/AlternateLink.php new file mode 100644 index 0000000..04a972a --- /dev/null +++ b/src/Extension/AlternateLink.php @@ -0,0 +1,89 @@ +language = $language; + $this->location = $location; + } + + /** + * Get language of the page + * @return string + */ + public function getLanguage(): string + { + return $this->language; + } + + /** + * Get URL of the page + * @return string + */ + public function getLocation(): string + { + return $this->location; + } + + /** + * @inheritdoc + */ + public static function getLimit(): ?int + { + return null; + } + + /** + * @inheritdoc + */ + public static function writeXmlNamepsace(\XMLWriter $writer): void + { + $writer->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + } + + /** + * @inheritdoc + */ + public function write(\XMLWriter $writer): void + { + $writer->startElement('xhtml:link'); + $writer->startAttribute('rel'); + $writer->text('alternate'); + $writer->endAttribute(); + + $writer->startAttribute('hreflang'); + $writer->text($this->language); + $writer->endAttribute(); + + $writer->startAttribute('href'); + $writer->text($this->location); + $writer->endAttribute(); + $writer->endElement(); + } +} diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php new file mode 100644 index 0000000..ae03a7a --- /dev/null +++ b/src/Extension/ExtensionInterface.php @@ -0,0 +1,27 @@ +location = $location; + } + + /** + * @return string + */ + public function getCaption(): string + { + return $this->caption; + } + + /** + * @param string $caption + */ + public function setCaption(string $caption): void + { + $this->caption = $caption; + } + + /** + * @return string + */ + public function getGeoLocation(): string + { + return $this->geoLocation; + } + + /** + * @param string $geoLocation + */ + public function setGeoLocation(string $geoLocation): void + { + $this->geoLocation = $geoLocation; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle($title): void + { + $this->title = $title; + } + + /** + * @return string + */ + public function getLicense(): string + { + return $this->license; + } + + /** + * @param string $license + */ + public function setLicense(string $license): void + { + $this->license = $license; + } + + /** + * @return string + */ + public function getLocation(): string + { + return $this->location; + } + + /** + * @inheritdoc + */ + public static function getLimit(): ?int + { + return self::LIMIT; + } + + /** + * @inheritdoc + */ + public function write(\XMLWriter $writer): void + { + $writer->startElement('image:image'); + + if (!empty($this->location)) { + $writer->writeElement('image:loc', $this->location); + } + if (!empty($this->caption)) { + $writer->writeElement('image:caption', $this->caption); + } + if (!empty($this->geoLocation)) { + $writer->writeElement('image:geo_location', $this->geoLocation); + } + if (!empty($this->title)) { + $writer->writeElement('image:title', $this->title); + } + if (!empty($this->license)) { + $writer->writeElement('image:license', $this->license); + } + + $writer->endElement(); + } + + /** + * Writes XML namespace attribute + * @param \XMLWriter $writer + */ + public static function writeXmlNamepsace(\XMLWriter $writer): void + { + $writer->writeAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1'); + } +} \ No newline at end of file diff --git a/src/Extension/Video/AllowCountryRestriction.php b/src/Extension/Video/AllowCountryRestriction.php new file mode 100644 index 0000000..16766ea --- /dev/null +++ b/src/Extension/Video/AllowCountryRestriction.php @@ -0,0 +1,14 @@ +validateCountry($country); + } + + $this->countries = $countries; + } + + private function validateCountry($name) + { + // TODO: ISO 3166 + } + + abstract public function areAllowed(): bool; +} diff --git a/src/Extension/Video/DenyCountryRestriction.php b/src/Extension/Video/DenyCountryRestriction.php new file mode 100644 index 0000000..b9bd593 --- /dev/null +++ b/src/Extension/Video/DenyCountryRestriction.php @@ -0,0 +1,12 @@ + tag can be listed for each video. The optional attribute title indicates the title of the gallery. + * + */ +class GalleryLocation +{ + private $link; + private $title; +} \ No newline at end of file diff --git a/src/Extension/Video/PlatformRestriction.php b/src/Extension/Video/PlatformRestriction.php new file mode 100644 index 0000000..17ef3e0 --- /dev/null +++ b/src/Extension/Video/PlatformRestriction.php @@ -0,0 +1,13 @@ +currency = $currency; + $this->value = $value; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType(string $type): void + { + $this->type = $type; + } + + /** + * @return string + */ + public function getResolution(): string + { + return $this->resolution; + } + + /** + * @param string $resolution + */ + public function setResolution(string $resolution): void + { + $this->resolution = $resolution; + } + + /** + * @return string + */ + public function getCurrency(): string + { + return $this->currency; + } + + /** + * @return float + */ + public function getValue(): float + { + return $this->value; + } + + + + +} \ No newline at end of file diff --git a/src/Extension/Video/Uploader.php b/src/Extension/Video/Uploader.php new file mode 100644 index 0000000..a2f71a2 --- /dev/null +++ b/src/Extension/Video/Uploader.php @@ -0,0 +1,57 @@ +name = $name; + } + + /** + * @return string + */ + public function getInfoUrl(): string + { + return $this->infoUrl; + } + + /** + * @param string $infoUrl + * @return self + */ + public function setInfoUrl(string $infoUrl): self + { + $this->infoUrl = $infoUrl; + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + + + +} \ No newline at end of file diff --git a/src/Extension/Video/Video.php b/src/Extension/Video/Video.php new file mode 100644 index 0000000..0260406 --- /dev/null +++ b/src/Extension/Video/Video.php @@ -0,0 +1,414 @@ +thumbnailLocation = $thumbnailLocation; + $this->title = $title; + $this->description = $description; + } + + /** + * @inheritdoc + */ + public static function getLimit(): ?int + { + return 1; + } + + /** + * @inheritdoc + */ + public function write(\XMLWriter $writer): void + { + // TODO: Implement write() method. + } + + /** + * @inheritdoc + */ + public static function writeXmlNamepsace(\XMLWriter $writer): void + { + $writer->writeAttribute('xmlns:video', 'http://www.google.com/schemas/sitemap-video/1.1'); + } + + /** + * @return string + */ + public function getContentLocations() + { + return $this->contentLocations; + } + + /** + * @param string $contentLocation + * @return self + */ + public function addContentLocation($contentLocation) + { + $this->contentLocations[] = $contentLocation; + + return $this; + } + + /** + * @return array + */ + public function getPlayerLocations() + { + return $this->playerLocations; + } + + /** + * @param string $playerLocation + * @return self + */ + public function addPlayerLocation($playerLocation): self + { + $this->playerLocations[] = $playerLocation; + return $this; + } + + /** + * @return int Duration of the video in seconds + */ + public function getDuration(): int + { + return $this->duration; + } + + /** + * @param int $duration Duration of the video in seconds + * @return self + */ + public function setDuration(int $duration): self + { + $this->duration = $duration; + return $this; + } + + /** + * @return mixed + */ + public function getExpirationDate() + { + return $this->expirationDate; + } + + /** + * @param mixed $expirationDate + * @return self + */ + public function setExpirationDate($expirationDate): self + { + $this->expirationDate = $expirationDate; + return $this; + } + + /** + * @return mixed + */ + public function getRating() + { + return $this->rating; + } + + /** + * @param mixed $rating + * @return self + */ + public function setRating($rating): self + { + $this->rating = $rating; + return $this; + } + + /** + * @return mixed + */ + public function getViewCount() + { + return $this->viewCount; + } + + /** + * @param mixed $viewCount + * @return self + */ + public function setViewCount($viewCount): self + { + $this->viewCount = $viewCount; + return $this; + } + + /** + * @return mixed + */ + public function getPublictionDate() + { + return $this->publictionDate; + } + + /** + * @param mixed $publictionDate + * @return self + */ + public function setPublictionDate($publictionDate): self + { + $this->publictionDate = $publictionDate; + return $this; + } + + /** + * @return mixed + */ + public function getFamilyFriendly() + { + return $this->familyFriendly; + } + + /** + * @param mixed $familyFriendly + * @return self + */ + public function setFamilyFriendly($familyFriendly): self + { + $this->familyFriendly = $familyFriendly; + return $this; + } + + /** + * @return CountryRestriction + */ + public function getRestriction(): CountryRestriction + { + return $this->restriction; + } + + /** + * @param CountryRestriction $restriction + * @return self + */ + public function setRestriction(CountryRestriction $restriction): self + { + $this->restriction = $restriction; + return $this; + } + + /** + * @return GalleryLocation + */ + public function getGalleryLocation(): GalleryLocation + { + return $this->galleryLocation; + } + + /** + * @param GalleryLocation $galleryLocation + * @return self + */ + public function setGalleryLocation(GalleryLocation $galleryLocation): self + { + $this->galleryLocation = $galleryLocation; + return $this; + } + + /** + * @return Price[] + */ + public function getPrices() + { + return $this->prices; + } + + /** + * @param Price $price + * @return self + */ + public function addPrice(Price $price): self + { + $this->prices[] = $price; + + return $this; + } + + /** + * @return bool + */ + public function requiresSubscribtion(): bool + { + return $this->requiresSubscribtion; + } + + /** + * @param bool $requiresSubscribtion + * @return self + */ + public function setRequiresSubscribtion(bool $requiresSubscribtion): self + { + $this->requiresSubscribtion = $requiresSubscribtion; + + return $this; + } + + /** + * @return Uploader + */ + public function getUploader(): Uploader + { + return $this->uploader; + } + + /** + * @param Uploader $uploader + * @return self + */ + public function setUploader(Uploader $uploader): self + { + $this->uploader = $uploader; + return $this; + } + + /** + * @return bool + */ + public function isLive(): bool + { + return $this->live; + } + + /** + * @param bool $live + * @return self + */ + public function setLive(bool $live): self + { + $this->live = $live; + + return $this; + } + + +} \ No newline at end of file diff --git a/src/Frequency.php b/src/Frequency.php new file mode 100644 index 0000000..eb6cb15 --- /dev/null +++ b/src/Frequency.php @@ -0,0 +1,33 @@ +writer = new XMLWriter(); $this->writer->openMemory(); @@ -53,14 +53,8 @@ private function createNewFile() * @param integer $lastModified unix timestamp of sitemap modification time * @throws \InvalidArgumentException */ - public function addSitemap($location, $lastModified = null) + public function addSitemap($location, $lastModified = null): void { - if (false === filter_var($location, FILTER_VALIDATE_URL)) { - throw new \InvalidArgumentException( - "The location must be a valid URL. You have specified: {$location}." - ); - } - if ($this->writer === null) { $this->createNewFile(); } @@ -77,7 +71,7 @@ public function addSitemap($location, $lastModified = null) /** * @return string index file path */ - public function getFilePath() + public function getFilePath(): string { return $this->filePath; } @@ -85,7 +79,7 @@ public function getFilePath() /** * Finishes writing */ - public function write() + public function write(): void { if ($this->writer instanceof XMLWriter) { $this->writer->endElement(); @@ -103,9 +97,9 @@ public function write() * @param bool $value * @throws \RuntimeException when trying to enable gzip while zlib is not available */ - public function setUseGzip($value) + public function setUseGzip($value): void { - if ($value && !extension_loaded('zlib')) { + if ($value && !\extension_loaded('zlib')) { throw new \RuntimeException('Zlib extension must be installed to gzip the sitemap.'); } $this->useGzip = $value; diff --git a/src/Sitemap.php b/src/Sitemap.php new file mode 100644 index 0000000..a7c43e6 --- /dev/null +++ b/src/Sitemap.php @@ -0,0 +1,377 @@ + + */ +class Sitemap +{ + /** + * @var integer Maximum allowed number of URLs in a single file. + */ + protected $maxUrls = 50000; + + /** + * @var integer number of URLs added + */ + protected $urlsCount = 0; + + /** + * @var integer Maximum allowed number of bytes in a single file. + */ + private $maxBytes = 10485760; + + /** + * @var integer number of bytes already written to the current file, before compression + */ + private $byteCount = 0; + + /** + * @var string path to the file to be written + */ + private $filePath; + + /** + * @var integer number of files written + */ + protected $fileCount = 0; + + /** + * @var array path of files written + */ + protected $writtenFilePaths = []; + + /** + * @var integer number of URLs to be kept in memory before writing it to file + */ + protected $bufferSize = 10; + + /** + * @var bool if XML should be indented + */ + protected $useIndent = true; + + /** + * @var bool whether to gzip the resulting files or not + */ + protected $useGzip = false; + + /** + * @var WriterInterface that does the actual writing + */ + protected $writerBackend; + + /** + * @var XMLWriter + */ + protected $writer; + + private $extensionClasses; + + /** + * @param string $filePath path of the file to write to + * @param array $extensionClasses + * + * @throws \InvalidArgumentException + */ + public function __construct($filePath, array $extensionClasses = []) + { + $dir = \dirname($filePath); + if (!is_dir($dir)) { + throw new \InvalidArgumentException( + "Please specify valid file path. Directory not exists. You have specified: {$dir}." + ); + } + + $this->filePath = $filePath; + $this->extensionClasses = $extensionClasses; + } + + /** + * Get array of generated files + * @return array + */ + public function getWrittenFilePaths(): array + { + return $this->writtenFilePaths; + } + + /** + * Creates new file + * @throws \RuntimeException if file is not writeable + */ + protected function createNewFile(): void + { + $this->fileCount++; + $filePath = $this->getCurrentFilePath(); + $this->writtenFilePaths[] = $filePath; + + if (file_exists($filePath)) { + $filePath = realpath($filePath); + if (is_writable($filePath)) { + unlink($filePath); + } else { + throw new \RuntimeException("File \"$filePath\" is not writable."); + } + } + + if ($this->useGzip) { + if (\function_exists('deflate_init') && \function_exists('deflate_add')) { + $this->writerBackend = new DeflateWriter($filePath); + } else { + $this->writerBackend = new TempFileGZIPWriter($filePath); + } + } else { + $this->writerBackend = new PlainFileWriter($filePath); + } + + $this->writer = new XMLWriter(); + $this->writer->openMemory(); + $this->addHeader(); + + /* + * XMLWriter does not give us much options, so we must make sure, that + * the header was written correctly and we can simply reuse any + * elements that did not fit into the previous file. (See self::flush) + */ + $this->writer->text(PHP_EOL); + $this->flush(); + } + + /** + * Writes closing tags to current file + * @throws \RuntimeException + * @throws \OverflowException + */ + protected function finishFile(): void + { + if ($this->writer !== null) { + $this->writer->endElement(); + $this->writer->endDocument(); + + /* To prevent infinite recursion through flush */ + $this->urlsCount = 0; + + $this->flush(0); + $this->writerBackend->finish(); + $this->writerBackend = null; + + $this->byteCount = 0; + } + } + + /** + * Finishes writing + * @throws \RuntimeException + * @throws \OverflowException + */ + public function write(): void + { + $this->finishFile(); + } + + /** + * Flushes buffer into file + * + * @param int $footSize Size of the remaining closing tags + * @throws \RuntimeException + * @throws \OverflowException + */ + protected function flush($footSize = 10): void + { + $data = $this->writer->flush(); + $dataSize = mb_strlen($data, '8bit'); + + /* + * Limit the file size of each single site map + * + * We use a heuristic of 10 Bytes for the remainder of the file, + * i.e. plus a new line. + */ + if ($this->byteCount + $dataSize + $footSize > $this->maxBytes) { + if ($this->urlsCount <= 1) { + throw new \OverflowException('The buffer size is too big for the defined file size limit'); + } + $this->finishFile(); + $this->createNewFile(); + } + + $this->writerBackend->append($data); + $this->byteCount += $dataSize; + } + + /** + * Adds a new URL to sitemap + * + * @param Url $url + * @throws \OverflowException + * @throws \LogicException + * @throws \RuntimeException + */ + public function addUrl(Url $url): void + { + if ($this->urlsCount >= $this->maxUrls) { + $this->finishFile(); + } + + if ($this->writerBackend === null) { + $this->createNewFile(); + } + + $this->writeUrl($url); + + $this->urlsCount++; + + if ($this->urlsCount % $this->bufferSize === 0) { + $this->flush(); + } + } + + /** + * Writes XML for Url passed + * @param Url $url + * @throws \LogicException + */ + protected function writeUrl(Url $url): void + { + $this->writer->startElement('url'); + $this->writer->writeElement('loc', $url->getLocation()); + + if ($url->getLastModified() !== null) { + $this->writer->writeElement('lastmod', $url->getLastModified()->format('c')); + } + + if ($url->getChangeFrequency() !== null) { + $this->writer->writeElement('changefreq', $url->getChangeFrequency()); + } + + $this->writer->writeElement('priority', number_format($url->getPriority(), 1)); + + foreach ($url->getExtensionItems() as $item) { + $extensionClass = \get_class($item); + if (!\in_array($extensionClass, $this->extensionClasses, true)) { + throw new \LogicException("$extensionClass is missing from an array of extension class names passed as second Sitemap constructor argument."); + } + $item->write($this->writer); + } + + $this->writer->endElement(); + } + + /** + * @return string path of currently opened file + */ + protected function getCurrentFilePath(): string + { + if ($this->fileCount < 2) { + return $this->filePath; + } + + $parts = pathinfo($this->filePath); + if ($parts['extension'] === 'gz') { + $filenameParts = pathinfo($parts['filename']); + if (!empty($filenameParts['extension'])) { + $parts['filename'] = $filenameParts['filename']; + $parts['extension'] = $filenameParts['extension'] . '.gz'; + } + } + return $parts['dirname'] . DIRECTORY_SEPARATOR . $parts['filename'] . '-' . $this->fileCount . '.' . $parts['extension']; + } + + /** + * Returns an array of URLs written + * + * @param string $baseUrl base URL of all the sitemaps written + * @return array URLs of sitemaps written + */ + public function getSitemapUrls($baseUrl): array + { + $urls = []; + foreach ($this->writtenFilePaths as $file) { + $urls[] = $baseUrl . pathinfo($file, PATHINFO_BASENAME); + } + return $urls; + } + + /** + * Sets maximum number of URLs to write in a single file. + * Default is 50000. + * @param integer $number + */ + public function setMaxUrls($number): void + { + $this->maxUrls = (int)$number; + } + + /** + * Sets maximum number of bytes to write in a single file. + * Default is 10485760 or 10 MiB. + * @param integer $number + */ + public function setMaxBytes($number): void + { + $this->maxBytes = (int)$number; + } + + /** + * Sets number of URLs to be kept in memory before writing it to file. + * Default is 10. + * + * @param integer $number + */ + public function setBufferSize($number): void + { + $this->bufferSize = (int)$number; + } + + /** + * Sets if XML should be indented. + * Default is true. + * + * @param bool $value + */ + public function setUseIndent($value): void + { + $this->useIndent = (bool)$value; + } + + /** + * Sets whether the resulting files will be gzipped or not. + * @param bool $value + * @throws \RuntimeException when trying to enable gzip while zlib is not available or when trying to change + * setting when some items are already written + */ + public function setUseGzip($value): void + { + if ($value && !\extension_loaded('zlib')) { + throw new \RuntimeException('Zlib extension must be enabled to gzip the sitemap.'); + } + if ($this->writerBackend !== null && $value !== $this->useGzip) { + throw new \RuntimeException('Cannot change the gzip value once items have been added to the sitemap.'); + } + $this->useGzip = $value; + } + + /** + * Adds a document header + */ + protected function addHeader(): void + { + $this->writer->startDocument('1.0', 'UTF-8'); + $this->writer->setIndent($this->useIndent); + $this->writer->startElement('urlset'); + $this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + foreach ($this->extensionClasses as $extensionClass) { + $extensionClass::writeXmlNamepsace($this->writer); + } + } +} diff --git a/src/Url.php b/src/Url.php new file mode 100644 index 0000000..38a55a8 --- /dev/null +++ b/src/Url.php @@ -0,0 +1,167 @@ +location = $location; + } + + /** + * @return string + */ + public function getLocation(): string + { + return $this->location; + } + + /** + * @param string $location + * @return Url + */ + public function setLocation(string $location): Url + { + $this->location = $location; + return $this; + } + + /** + * @return \DateTimeInterface + */ + public function getLastModified(): ?\DateTimeInterface + { + return $this->lastModified; + } + + /** + * @param \DateTimeInterface $lastModified + * @return Url + */ + public function setLastModified(\DateTimeInterface $lastModified): Url + { + $this->lastModified = $lastModified; + return $this; + } + + /** + * @return string + */ + public function getChangeFrequency(): ?string + { + return $this->changeFrequency; + } + + /** + * @param string $changeFrequency + * @return Url + * @throws \InvalidArgumentException + */ + public function setChangeFrequency(string $changeFrequency): Url + { + if (!\in_array($changeFrequency, Frequency::all(), true)) { + throw new \InvalidArgumentException( + 'Please specify valid changeFrequency. Valid values are: ' + . implode(', ', Frequency::all()) + . "You have specified: {$changeFrequency}." + ); + } + + $this->changeFrequency = $changeFrequency; + return $this; + } + + /** + * @return float + */ + public function getPriority(): float + { + return $this->priority; + } + + /** + * @param float $priority + * @return Url + * @throws \InvalidArgumentException + */ + public function setPriority(float $priority): Url + { + if ($priority < 0 || $priority > 1) { + throw new \InvalidArgumentException( + "Please specify valid priority. Valid values range from 0.0 to 1.0. You have specified: {$priority}." + ); + } + + $this->priority = $priority; + return $this; + } + + /** + * @param ExtensionInterface $item + * @return Url + * @throws \LogicException + */ + public function add(ExtensionInterface $item): Url + { + $itemClass = \get_class($item); + + $currentValue = $this->itemCounters[$itemClass] ?? 0; + $limit = $item->getLimit(); + if ($limit !== null && $currentValue === $limit) { + throw new \LogicException("You can not add more than $limit of $itemClass"); + } + + $this->extensionItems[] = $item; + $this->itemCounters[$itemClass] = ++$currentValue; + + return $this; + } + + /** + * @return ExtensionInterface[] + */ + public function getExtensionItems(): array + { + return $this->extensionItems; + } +} diff --git a/DeflateWriter.php b/src/Writer/DeflateWriter.php similarity index 85% rename from DeflateWriter.php rename to src/Writer/DeflateWriter.php index 863b0e1..410677e 100644 --- a/DeflateWriter.php +++ b/src/Writer/DeflateWriter.php @@ -1,6 +1,6 @@ file !== null); + \assert($this->file !== null); $compressedChunk = deflate_add($this->deflateContext, $data, $flushMode); fwrite($this->file, $compressedChunk); @@ -45,7 +45,7 @@ private function write($data, $flushMode) * * @param string $data */ - public function append($data) + public function append($data): void { $this->write($data, ZLIB_NO_FLUSH); } @@ -53,7 +53,7 @@ public function append($data) /** * Make sure all data was written */ - public function finish() + public function finish(): void { $this->write('', ZLIB_FINISH); diff --git a/PlainFileWriter.php b/src/Writer/PlainFileWriter.php similarity index 66% rename from PlainFileWriter.php rename to src/Writer/PlainFileWriter.php index 3ea6d3a..b7ef88e 100644 --- a/PlainFileWriter.php +++ b/src/Writer/PlainFileWriter.php @@ -1,6 +1,6 @@ file !== null); + \assert($this->file !== null); fwrite($this->file, $data); } @@ -33,11 +33,12 @@ public function append($data) /** * @inheritdoc */ - public function finish() + public function finish(): void { - assert($this->file !== null); + \assert($this->file !== null); fclose($this->file); + $this->file = null; } } diff --git a/TempFileGZIPWriter.php b/src/Writer/TempFileGZIPWriter.php similarity index 78% rename from TempFileGZIPWriter.php rename to src/Writer/TempFileGZIPWriter.php index 1859f9c..2b50916 100644 --- a/TempFileGZIPWriter.php +++ b/src/Writer/TempFileGZIPWriter.php @@ -1,6 +1,6 @@ tempFile !== null); + \assert($this->tempFile !== null); fwrite($this->tempFile, $data); } @@ -41,15 +41,16 @@ public function append($data) /** * Deflate buffered data */ - public function finish() + public function finish(): void { - assert($this->tempFile !== null); + \assert($this->tempFile !== null); $file = fopen('compress.zlib://' . $this->filename, 'wb'); rewind($this->tempFile); stream_copy_to_stream($this->tempFile, $file); fclose($file); + fclose($this->tempFile); $this->tempFile = null; } diff --git a/WriterInterface.php b/src/Writer/WriterInterface.php similarity index 79% rename from WriterInterface.php rename to src/Writer/WriterInterface.php index 887a98d..2bfe1f6 100644 --- a/WriterInterface.php +++ b/src/Writer/WriterInterface.php @@ -1,5 +1,6 @@ load($fileName); - $this->assertTrue($xml->schemaValidate(__DIR__ . '/siteindex.xsd')); - } +use SamDark\Sitemap\Index; +/** + * IndexTest tests Sitemap index generator + */ +class IndexTest extends TestCase +{ public function testWritingFile() { - $fileName = __DIR__ . '/sitemap_index.xml'; + $fileName = $this->getTempPath('sitemap_index.xml'); $index = new Index($fileName); $index->addSitemap('http://example.com/sitemap.xml'); $index->addSitemap('http://example.com/sitemap_2.xml', time()); $index->write(); - $this->assertTrue(file_exists($fileName)); - $this->assertIsValidIndex($fileName); - unlink($fileName); - } - - public function testLocationValidation() - { - $this->setExpectedException('InvalidArgumentException'); - - $fileName = __DIR__ . '/sitemap.xml'; - $index = new Index($fileName); - $index->addSitemap('noturl'); - - unlink($fileName); + $this->assertFileExists($fileName); + $this->assertValidXml($fileName, 'index'); } public function testWritingFileGzipped() { - $fileName = __DIR__ . '/sitemap_index.xml.gz'; + $fileName = $this->getTempPath('sitemap_index.xml.gz'); $index = new Index($fileName); $index->setUseGzip(true); $index->addSitemap('http://example.com/sitemap.xml'); $index->addSitemap('http://example.com/sitemap_2.xml', time()); $index->write(); - $this->assertTrue(file_exists($fileName)); + $this->assertFileExists($fileName); $finfo = new \finfo(FILEINFO_MIME_TYPE); - $this->assertEquals('application/x-gzip', $finfo->file($fileName)); - $this->assertIsValidIndex('compress.zlib://' . $fileName); - unlink($fileName); + + $this->assertRegExp('!application/(x-)?gzip!', $finfo->file($fileName)); + $this->assertValidXml('compress.zlib://' . $fileName, 'index'); } } diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php index c1c4a14..cb85d94 100644 --- a/tests/SitemapTest.php +++ b/tests/SitemapTest.php @@ -1,27 +1,22 @@ load($fileName); - $this->assertTrue($xml->schemaValidate(__DIR__ . '/' . $xsdFileName)); - } - - protected function assertIsOneMemberGzipFile($fileName) + protected function assertIsOneMemberGzipFile(string $fileName) { $gzipMemberStartSequence = pack('H*', '1f8b08'); $content = file_get_contents($fileName); @@ -31,293 +26,238 @@ protected function assertIsOneMemberGzipFile($fileName) public function testWritingFile() { - $fileName = __DIR__ . '/sitemap_regular.xml'; + $fileName = $this->getTempPath('testWritingFile.xml'); + $sitemap = new Sitemap($fileName); - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->addItem('http://example.com/mylink2', time()); - $sitemap->addItem('http://example.com/mylink3', time(), Sitemap::HOURLY); - $sitemap->addItem('http://example.com/mylink4', time(), Sitemap::DAILY, 0.3); + $sitemap->addUrl(new Url('http://example.com/mylink1')); + $sitemap->addUrl( + (new Url('http://example.com/mylink2')) + ->setLastModified(new \DateTime()) + ); + $sitemap->addUrl( + (new Url('http://example.com/mylink3')) + ->setLastModified(new \DateTime()) + ->setChangeFrequency(Frequency::HOURLY) + ); + $sitemap->addUrl( + (new Url('http://example.com/mylink4')) + ->setChangeFrequency(Frequency::DAILY) + ->setLastModified(new \DateTime()) + ->setPriority(0.3) + ); $sitemap->write(); - $this->assertTrue(file_exists($fileName)); - $this->assertIsValidSitemap($fileName); - - unlink($fileName); + $this->assertFileExists($fileName); + $this->assertValidXml($fileName, 'sitemap'); } public function testMultipleFiles() { - $sitemap = new Sitemap(__DIR__ . '/sitemap_multi.xml'); + $sitemap = new Sitemap($this->getTempPath('/testMultipleFiles.xml')); $sitemap->setMaxUrls(2); for ($i = 0; $i < 20; $i++) { - $sitemap->addItem('http://example.com/mylink' . $i, time()); + $sitemap->addUrl( + (new Url('http://example.com/mylink' . $i)) + ->setLastModified(new \DateTime()) + ); } $sitemap->write(); - $expectedFiles = array( - __DIR__ . '/' .'sitemap_multi.xml', - __DIR__ . '/' .'sitemap_multi_2.xml', - __DIR__ . '/' .'sitemap_multi_3.xml', - __DIR__ . '/' .'sitemap_multi_4.xml', - __DIR__ . '/' .'sitemap_multi_5.xml', - __DIR__ . '/' .'sitemap_multi_6.xml', - __DIR__ . '/' .'sitemap_multi_7.xml', - __DIR__ . '/' .'sitemap_multi_8.xml', - __DIR__ . '/' .'sitemap_multi_9.xml', - __DIR__ . '/' .'sitemap_multi_10.xml', - ); + $expectedFiles = [ + $this->getTempPath('testMultipleFiles.xml'), + $this->getTempPath('testMultipleFiles-2.xml'), + $this->getTempPath('testMultipleFiles-3.xml'), + $this->getTempPath('testMultipleFiles-4.xml'), + $this->getTempPath('testMultipleFiles-5.xml'), + $this->getTempPath('testMultipleFiles-6.xml'), + $this->getTempPath('testMultipleFiles-7.xml'), + $this->getTempPath('testMultipleFiles-8.xml'), + $this->getTempPath('testMultipleFiles-9.xml'), + $this->getTempPath('testMultipleFiles-10.xml'), + ]; foreach ($expectedFiles as $expectedFile) { - $this->assertTrue(file_exists($expectedFile), "$expectedFile does not exist!"); - $this->assertIsValidSitemap($expectedFile); - unlink($expectedFile); + $this->assertFileExists($expectedFile, "$expectedFile does not exist!"); + $this->assertValidXml($expectedFile, 'sitemap'); } $urls = $sitemap->getSitemapUrls('http://example.com/'); - $this->assertEquals(10, count($urls), print_r($urls, true)); - $this->assertContains('http://example.com/sitemap_multi.xml', $urls); - $this->assertContains('http://example.com/sitemap_multi_10.xml', $urls); + $this->assertCount(10, $urls, print_r($urls, true)); + $this->assertContains('http://example.com/testMultipleFiles.xml', $urls); + $this->assertContains('http://example.com/testMultipleFiles-10.xml', $urls); } public function testMultiLanguageSitemap() { - $fileName = __DIR__ . '/sitemap_multi_language.xml'; - $sitemap = new Sitemap($fileName, true); - $sitemap->addItem('http://example.com/mylink1'); - - $sitemap->addItem(array( - 'ru' => 'http://example.com/ru/mylink2', - 'en' => 'http://example.com/en/mylink2', - ), time()); - - $sitemap->addItem(array( - 'ru' => 'http://example.com/ru/mylink3', - 'en' => 'http://example.com/en/mylink3', - ), time(), Sitemap::HOURLY); - - $sitemap->addItem(array( - 'ru' => 'http://example.com/ru/mylink4', - 'en' => 'http://example.com/en/mylink4', - ), time(), Sitemap::DAILY, 0.3); + $fileName = $this->getTempPath('testMultiLanguageSitemap.xml'); + $sitemap = new Sitemap($fileName, [AlternateLink::class]); + $sitemap->addUrl( + (new Url('http://example.com/en/mylink2')) + ->setLastModified(new \DateTime()) + ->setChangeFrequency(Frequency::HOURLY) + ->add(new AlternateLink('en', 'http://example.com/en/mylink2')) + ->add(new AlternateLink('ru', 'http://example.com/ru/mylink2')) + ); $sitemap->write(); - $this->assertTrue(file_exists($fileName)); - $this->assertIsValidSitemap($fileName, true); - - unlink($fileName); + $this->assertFileExists($fileName); + $this->assertValidXml($fileName, 'sitemap_xhtml'); } public function testFrequencyValidation() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); - $fileName = __DIR__ . '/sitemap.xml'; + $fileName = $this->getTempPath('testFrequencyValidation.xml'); $sitemap = new Sitemap($fileName); - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->addItem('http://example.com/mylink2', time(), 'invalid'); - - unlink($fileName); + $sitemap->addUrl( + (new Url('http://example.com/mylink2')) + ->setChangeFrequency('invalid') + ); } public function testPriorityValidation() { - $fileName = __DIR__ . '/sitemap.xml'; + $fileName = $this->getTempPath('testPriorityValidation.xml'); $sitemap = new Sitemap($fileName); - $exceptionCaught = false; - try { - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->addItem('http://example.com/mylink2', time(), 'always', 2.0); - } catch (\InvalidArgumentException $e) { - $exceptionCaught = true; - } - - unlink($fileName); + $this->expectException(InvalidArgumentException::class); - $this->assertTrue($exceptionCaught, 'Expected InvalidArgumentException wasn\'t thrown.'); - } - - public function testLocationValidation() - { - $fileName = __DIR__ . '/sitemap.xml'; - $sitemap = new Sitemap($fileName); - - $exceptionCaught = false; - try { - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->addItem('notlink', time()); - } catch (\InvalidArgumentException $e) { - $exceptionCaught = true; - } - - unlink($fileName); - - $this->assertTrue($exceptionCaught, 'Expected InvalidArgumentException wasn\'t thrown.'); - } - - public function testMultiLanguageLocationValidation() - { - $fileName = __DIR__ . '/sitemap.xml'; - $sitemap = new Sitemap($fileName); - - - $sitemap->addItem(array( - 'ru' => 'http://example.com/mylink1', - 'en' => 'http://example.com/mylink2', - )); - - $exceptionCaught = false; - try { - $sitemap->addItem(array( - 'ru' => 'http://example.com/mylink3', - 'en' => 'notlink', - ), time()); - } catch (\InvalidArgumentException $e) { - $exceptionCaught = true; - } - - unlink($fileName); - - $this->assertTrue($exceptionCaught, 'Expected InvalidArgumentException wasn\'t thrown.'); + $sitemap->addUrl( + (new Url('http://example.com/mylink1')) + ->setPriority(2.0) + ); } public function testWritingFileGzipped() { - $fileName = __DIR__ . '/sitemap_gzipped.xml.gz'; + $fileName = $this->getTempPath('testWritingFileGzipped.xml.gz'); $sitemap = new Sitemap($fileName); $sitemap->setUseGzip(true); - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->addItem('http://example.com/mylink2', time()); - $sitemap->addItem('http://example.com/mylink3', time(), Sitemap::HOURLY); - $sitemap->addItem('http://example.com/mylink4', time(), Sitemap::DAILY, 0.3); + $sitemap->addUrl(new Url('http://example.com/mylink1')); $sitemap->write(); - $this->assertTrue(file_exists($fileName)); + $this->assertFileExists($fileName); $finfo = new \finfo(FILEINFO_MIME_TYPE); - $this->assertEquals('application/x-gzip', $finfo->file($fileName)); - $this->assertIsValidSitemap('compress.zlib://' . $fileName); + $this->assertRegExp('!application/(x-)?gzip!', $finfo->file($fileName)); + $this->assertValidXml('compress.zlib://' . $fileName, 'sitemap'); $this->assertIsOneMemberGzipFile($fileName); - - unlink($fileName); } public function testMultipleFilesGzipped() { - $sitemap = new Sitemap(__DIR__ . '/sitemap_multi_gzipped.xml.gz'); + $sitemap = new Sitemap($this->getTempPath('testMultipleFilesGzipped.xml.gz')); $sitemap->setUseGzip(true); $sitemap->setMaxUrls(2); for ($i = 0; $i < 20; $i++) { - $sitemap->addItem('http://example.com/mylink' . $i, time()); + $sitemap->addUrl( + (new Url('http://example.com/mylink' . $i)) + ->setLastModified(new \DateTime()) + ); } $sitemap->write(); - $expectedFiles = array( - __DIR__ . '/' .'sitemap_multi_gzipped.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_2.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_3.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_4.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_5.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_6.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_7.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_8.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_9.xml.gz', - __DIR__ . '/' .'sitemap_multi_gzipped_10.xml.gz', - ); + $expectedFiles = [ + $this->getTempPath('testMultipleFilesGzipped.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-2.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-3.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-4.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-5.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-6.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-7.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-8.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-9.xml.gz'), + $this->getTempPath('testMultipleFilesGzipped-10.xml.gz'), + ]; $finfo = new \finfo(FILEINFO_MIME_TYPE); foreach ($expectedFiles as $expectedFile) { - $this->assertTrue(file_exists($expectedFile), "$expectedFile does not exist!"); - $this->assertEquals('application/x-gzip', $finfo->file($expectedFile)); - $this->assertIsValidSitemap('compress.zlib://' . $expectedFile); + + $this->assertFileExists($expectedFile, "$expectedFile does not exist!"); + $this->assertRegExp('!application/(x-)?gzip!', $finfo->file($expectedFile)); + $this->assertValidXml('compress.zlib://' . $expectedFile, 'sitemap'); $this->assertIsOneMemberGzipFile($expectedFile); - unlink($expectedFile); } $urls = $sitemap->getSitemapUrls('http://example.com/'); - $this->assertEquals(10, count($urls), print_r($urls, true)); - $this->assertContains('http://example.com/sitemap_multi_gzipped.xml.gz', $urls); - $this->assertContains('http://example.com/sitemap_multi_gzipped_10.xml.gz', $urls); + $this->assertCount(10, $urls, print_r($urls, true)); + $this->assertContains('http://example.com/testMultipleFilesGzipped.xml.gz', $urls); + $this->assertContains('http://example.com/testMultipleFilesGzipped-10.xml.gz', $urls); } public function testFileSizeLimit() { - $sitemap = new Sitemap(__DIR__ . '/sitemap_multi.xml'); + $sitemap = new Sitemap($this->getTempPath('testFileSizeLimit.xml')); $sizeLimit = 1036; $sitemap->setMaxBytes($sizeLimit); $sitemap->setBufferSize(1); for ($i = 0; $i < 20; $i++) { - $sitemap->addItem('http://example.com/mylink' . $i, time()); + $sitemap->addUrl( + (new Url('http://example.com/mylink' . $i)) + ->setLastModified(new \DateTime()) + ); } $sitemap->write(); - $expectedFiles = array( - __DIR__ . '/' .'sitemap_multi.xml', - __DIR__ . '/' .'sitemap_multi_2.xml', - __DIR__ . '/' .'sitemap_multi_3.xml', - ); - - $this->assertEquals($sizeLimit, filesize($expectedFiles[1])); + $expectedFiles = [ + $this->getTempPath('testFileSizeLimit.xml'), + $this->getTempPath('testFileSizeLimit-2.xml'), + $this->getTempPath('testFileSizeLimit-3.xml'), + ]; foreach ($expectedFiles as $expectedFile) { - $this->assertTrue(file_exists($expectedFile), "$expectedFile does not exist!"); - $this->assertIsValidSitemap($expectedFile); + $this->assertFileExists($expectedFile); + $this->assertValidXml($expectedFile, 'sitemap'); $this->assertLessThanOrEqual($sizeLimit, filesize($expectedFile), "$expectedFile exceeds the size limit"); - unlink($expectedFile); } $urls = $sitemap->getSitemapUrls('http://example.com/'); - $this->assertEquals(3, count($urls), print_r($urls, true)); - $this->assertContains('http://example.com/sitemap_multi.xml', $urls); - $this->assertContains('http://example.com/sitemap_multi_3.xml', $urls); + $this->assertCount(3, $urls, print_r($urls, true)); + $this->assertContains('http://example.com/testFileSizeLimit.xml', $urls); + $this->assertContains('http://example.com/testFileSizeLimit-3.xml', $urls); } public function testSmallSizeLimit() { - $fileName = __DIR__ . '/sitemap_regular.xml'; + $this->expectException(OverflowException::class); + + $fileName = $this->getTempPath('testSmallSizeLimit.xml'); $sitemap = new Sitemap($fileName); $sitemap->setMaxBytes(0); $sitemap->setBufferSize(1); - - $exceptionCaught = false; - try { - $sitemap->addItem('http://example.com/mylink1'); - $sitemap->write(); - } catch (\OverflowException $e) { - $exceptionCaught = true; - } - - unlink($fileName); - - $this->assertTrue($exceptionCaught, 'Expected OverflowException wasn\'t thrown.'); + $sitemap->addUrl(new Url('http://example.com/mylink1')); + $sitemap->write(); } public function testBufferSizeImpact() { - if (getenv('TRAVIS') == 'true') { + if (getenv('TRAVIS') === 'true') { $this->markTestSkipped('Can not reliably test performance on travis-ci.'); return; } - $fileName = __DIR__ . '/sitemap_big.xml'; + $fileName = $this->getTempPath('testBufferSizeImpact.xml'); - $times = array(); + $times = []; - foreach (array(1000, 10) as $bufferSize) { + foreach ([1000, 10] as $bufferSize) { $startTime = microtime(true); $sitemap = new Sitemap($fileName); $sitemap->setBufferSize($bufferSize); for ($i = 0; $i < 50000; $i++) { - $sitemap->addItem('http://example.com/mylink' . $i, time()); + $sitemap->addUrl( + (new Url('http://example.com/mylink' . $i)) + ->setLastModified(new \DateTime()) + ); } $sitemap->write(); $times[] = microtime(true) - $startTime; - unlink($fileName); } $this->assertLessThan($times[0] * 1.2, $times[1]); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..93874fa --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,42 @@ +tempPaths[] = $path; + return $path; + } + + protected function tearDown() + { + foreach ($this->tempPaths as $tempPath) { + @unlink($tempPath); + } + + $this->tempPaths = []; + } + + /** + * Asserts if file is valid XML accoring to XSD specified + * @param string $fileName + * @param string $xsdName + */ + protected function assertValidXml(string $fileName, string $xsdName) + { + $xml = new \DOMDocument(); + $xml->load($fileName); + $this->assertTrue($xml->schemaValidate(__DIR__ . '/xsd/' . $xsdName . '.xsd'), "$fileName is not valid accoring to $xsdName XML schema definition"); + } +} diff --git a/tests/runtime/.gitignore b/tests/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/tests/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/siteindex.xsd b/tests/xsd/index.xsd similarity index 100% rename from tests/siteindex.xsd rename to tests/xsd/index.xsd diff --git a/tests/sitemap.xsd b/tests/xsd/sitemap.xsd similarity index 100% rename from tests/sitemap.xsd rename to tests/xsd/sitemap.xsd diff --git a/tests/sitemap_xhtml.xsd b/tests/xsd/sitemap_xhtml.xsd similarity index 100% rename from tests/sitemap_xhtml.xsd rename to tests/xsd/sitemap_xhtml.xsd diff --git a/tests/xhtml1-strict.xsd b/tests/xsd/xhtml1-strict.xsd similarity index 100% rename from tests/xhtml1-strict.xsd rename to tests/xsd/xhtml1-strict.xsd