diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b61df6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: "CI" + +on: + pull_request: + branches: + - master + push: + branches: + - master + +jobs: + check: + name: "PHPStan" + runs-on: "ubuntu-latest" + + strategy: + matrix: + php: + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v3" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php }}" + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v2" + + - name: "Run PHPStan" + run: "vendor/bin/phpstan analyse --no-progress --error-format=github" diff --git a/Feed.php b/Feed.php index 6329532..a0d2843 100644 --- a/Feed.php +++ b/Feed.php @@ -32,6 +32,20 @@ * @package UniversalFeedWriter * @author Anis uddin Ahmad * @link http://www.ajaxray.com/projects/rss + * + * PHPStan does not support recursive type aliases + * https://github.com/phpstan/phpstan/issues/4637 + * so we are manually approximating the fixed point + * to a depth that should be sufficient in practice. + * Feel free to add more layers if you need them. + * @_phpstan-recursive-type TagContent string|array|array + * @phpstan-type TagContentFix0 string + * @phpstan-type TagContentFix1 string|array|array + * @phpstan-type TagContentFix2 string|array|array + * @phpstan-type TagContentFix3 string|array|array + * @phpstan-type TagContentFix TagContentFix3 + * + * @phpstan-type Element array{content: TagContentFix, attributes: array} */ abstract class Feed { @@ -47,11 +61,15 @@ abstract class Feed /** * Collection of all channel elements + * + * @var array> */ private $channels = array(); /** * Collection of items as object of \FeedWriter\Item class. + * + * @var Item[] */ private $items = array(); @@ -59,21 +77,29 @@ abstract class Feed * Collection of other version wise data. * * Currently used to store the 'rdf:about' attribute and image element of the channel (both RSS1 only). + * + * @var array{Image?: string|array{title: string, link: string, url: string}, ChannelAbout?: string} */ private $data = array(); /** * The tag names which have to encoded as CDATA + * + * @var string[] */ private $CDATAEncoding = array(); /** * Collection of XML namespaces + * + * @var array */ private $namespaces = array(); /** * Contains the format of this feed. + * + * @var Feed::RSS1|Feed::RSS2|Feed::ATOM */ private $version = null; @@ -89,7 +115,7 @@ abstract class Feed * * If no version is given, a feed in RSS 2.0 format will be generated. * - * @param string $version the version constant (RSS1/RSS2/ATOM). + * @param Feed::RSS1|Feed::RSS2|Feed::ATOM $version the version constant (RSS1/RSS2/ATOM). */ protected function __construct($version = Feed::RSS2) { @@ -192,13 +218,13 @@ public function addNamespace($prefix, $uri) * * @access public * @param string $elementName name of the channel tag - * @param string $content content of the channel tag - * @param array array of element attributes with attribute name as array key - * @param bool TRUE if this element can appear multiple times + * @param TagContentFix $content content of the channel tag + * @param array $attributes array of element attributes with attribute name as array key + * @param bool $multiple TRUE if this element can appear multiple times * @return self * @throws \InvalidArgumentException if the element name is not a string, empty or NULL. */ - public function setChannelElement($elementName, $content, array $attributes = null, $multiple = false) + public function setChannelElement($elementName, $content, array $attributes = [], $multiple = false) { if (empty($elementName)) throw new \InvalidArgumentException('The element name may not be empty or NULL.'); @@ -211,7 +237,7 @@ public function setChannelElement($elementName, $content, array $attributes = nu if ($multiple === TRUE) $this->channels[$elementName][] = $entity; else - $this->channels[$elementName] = $entity; + $this->channels[$elementName] = [$entity]; return $this; } @@ -221,7 +247,7 @@ public function setChannelElement($elementName, $content, array $attributes = nu * should be 'channelName' => 'channelContent' format. * * @access public - * @param array array of channels + * @param array $elementArray array of channels * @return self */ public function setChannelElementsFromArray(array $elementArray) @@ -262,7 +288,7 @@ public function getMIMEType() * if you need to pass a string around, use generateFeed() instead. * * @access public - * @param bool FALSE if the specific feed media type should be sent. + * @param bool $useGenericContentType FALSE if the specific feed media type should be sent. * @return void * @throws \InvalidArgumentException if the useGenericContentType parameter is not boolean. */ @@ -318,7 +344,7 @@ public function createNewItem() * Add one or more tags to the list of CDATA encoded tags * * @access public - * @param array $tags An array of tag names that are merged into the list of tags which should be encoded as CDATA + * @param array $tags An array of tag names that are merged into the list of tags which should be encoded as CDATA * @return self */ public function addCDATAEncoding(array $tags) @@ -332,7 +358,7 @@ public function addCDATAEncoding(array $tags) * Get list of CDATA encoded properties * * @access public - * @return array Return an array of CDATA properties that are to be encoded as CDATA + * @return array Return an array of CDATA properties that are to be encoded as CDATA */ public function getCDATAEncoding() { @@ -343,7 +369,7 @@ public function getCDATAEncoding() * Remove tags from the list of CDATA encoded tags * * @access public - * @param array $tags An array of tag names that should be removed. + * @param array $tags An array of tag names that should be removed. * @return void */ public function removeCDATAEncoding(array $tags) @@ -421,7 +447,7 @@ public function setTitle($title) * Not supported in RSS1 feeds. * * @access public - * @param DateTimeInterface|int|string Date which should be used. + * @param DateTimeInterface|int|string $date Date which should be used. * @return self * @throws \InvalidArgumentException if the given date is not an implementation of DateTimeInterface, a UNIX timestamp or a date string. * @throws InvalidOperationException if this method is called on an RSS1 feed. @@ -670,7 +696,7 @@ public function setChannelAbout($url) */ public static function uuid($key = null, $prefix = '') { - $key = ($key == null) ? uniqid(rand()) : $key; + $key = ($key == null) ? uniqid((string) rand()) : $key; $chars = md5($key); $uuid = substr($chars,0,8) . '-'; $uuid .= substr($chars,8,4) . '-'; @@ -719,7 +745,7 @@ public static function filterInvalidXMLChars($string, $replacement = '_') // def * because they are hardcoded, e.g. rdf. * * @access private - * @return array Array with namespace prefix as value. + * @return array Array with namespace prefix as value. */ private function getNamespacePrefixes() { @@ -823,8 +849,8 @@ private function makeFooter() * * @access private * @param string $tagName name of the tag - * @param mixed $tagContent tag value as string or array of nested tags in 'tagName' => 'tagValue' format - * @param array $attributes Attributes (if any) in 'attrName' => 'attrValue' format + * @param TagContentFix $tagContent tag value as string or array of nested tags in 'tagName' => 'tagValue' format + * @param array $attributes Attributes (if any) in 'attrName' => 'attrValue' format * @param bool $omitEndTag True if the end tag should be omitted. Defaults to false. * @return string formatted xml tag * @throws \InvalidArgumentException if the tagContent is not an array and not a string. @@ -890,7 +916,7 @@ private function makeChannels() $out .= '' . PHP_EOL; break; case Feed::RSS1: - $out .= (isset($this->data['ChannelAbout']))? "data['ChannelAbout']}\">" : "channels['link']['content']}\">"; + $out .= (isset($this->data['ChannelAbout']))? "data['ChannelAbout']}\">" : "channels['link'][0]['content']}\">"; break; } @@ -902,14 +928,9 @@ private function makeChannels() $key = substr($key, 5); } - // The channel element can occur multiple times, when the key 'content' is not in the array. - if (!array_key_exists('content', $value)) { - // If this is the case, iterate through the array with the multiple elements. - foreach ($value as $singleElement) { - $out .= $this->makeNode($key, $singleElement['content'], $singleElement['attributes']); - } - } else { - $out .= $this->makeNode($key, $value['content'], $value['attributes']); + // The channel element can occur multiple times. + foreach ($value as $singleElement) { + $out .= $this->makeNode($key, $singleElement['content'], $singleElement['attributes']); } } @@ -927,7 +948,7 @@ private function makeChannels() $out .= $this->makeNode('image', $this->data['Image'], array('rdf:about' => $this->data['Image']['url'])); } else if ($this->version == Feed::ATOM) { // ATOM feeds have a unique feed ID. Use the title channel element as key. - $out .= $this->makeNode('id', Feed::uuid($this->channels['title']['content'], 'urn:uuid:')); + $out .= $this->makeNode('id', Feed::uuid($this->channels['title'][0]['content'], 'urn:uuid:')); } return $out; @@ -977,7 +998,7 @@ private function makeItems() * @return string The starting XML tag of an feed item. * @throws InvalidOperationException if this object misses the data for the about attribute. */ - private function startItem($about = false) + private function startItem($about = '') { $out = ''; diff --git a/Item.php b/Item.php index 5e2049e..fec58ae 100644 --- a/Item.php +++ b/Item.php @@ -32,28 +32,37 @@ * @package UniversalFeedWriter * @author Anis uddin Ahmad * @link http://www.ajaxray.com/projects/rss + * + * @phpstan-import-type TagContentFix from Feed + * @phpstan-import-type Element from Feed */ class Item { /** * Collection of feed item elements + * + * @var array */ private $elements = array(); /** * Contains the format of this feed. + * + * @var Feed::RSS1|Feed::RSS2|Feed::ATOM */ private $version; /** * Is used as a suffix when multiple elements have the same name. + * + * @var int **/ private $_cpt = 0; /** * Constructor * - * @param string $version constant (RSS1/RSS2/ATOM) RSS2 is default. + * @param Feed::RSS1|Feed::RSS2|Feed::ATOM $version constant (RSS1/RSS2/ATOM) RSS2 is default. */ public function __construct($version = Feed::RSS2) { @@ -76,14 +85,14 @@ private function cpt() * * @access public * @param string $elementName The tag name of an element - * @param string $content The content of tag - * @param array $attributes Attributes (if any) in 'attrName' => 'attrValue' format + * @param TagContentFix $content The content of tag + * @param array $attributes Attributes (if any) in 'attrName' => 'attrValue' format * @param boolean $overwrite Specifies if an already existing element is overwritten. * @param boolean $allowMultiple Specifies if multiple elements of the same name are allowed. * @return self * @throws \InvalidArgumentException if the element name is not a string, empty or NULL. */ - public function addElement($elementName, $content, array $attributes = null, $overwrite = FALSE, $allowMultiple = FALSE) + public function addElement($elementName, $content, array $attributes = [], $overwrite = FALSE, $allowMultiple = FALSE) { if (empty($elementName)) throw new \InvalidArgumentException('The element name may not be empty or NULL.'); @@ -113,7 +122,7 @@ public function addElement($elementName, $content, array $attributes = null, $ov * Elements which have attributes cannot be added by this method * * @access public - * @param array array of elements in 'tagName' => 'tagContent' format. + * @param array $elementArray array of elements in 'tagName' => 'tagContent' format. * @return self */ public function addElementArray(array $elementArray) @@ -129,7 +138,7 @@ public function addElementArray(array $elementArray) * Return the collection of elements in this feed item * * @access public - * @return array All elements of this item. + * @return array All elements of this item. * @throws InvalidOperationException on ATOM feeds if either a content or link element is missing. * @throws InvalidOperationException on RSS1 feeds if a title or link element is missing. */ @@ -308,7 +317,7 @@ public function addEnclosure($url, $length, $type, $multiple = TRUE) if (!is_string($type) || preg_match('/.+\/.+/', $type) != 1) throw new \InvalidArgumentException('type parameter must be a string and a MIME type.'); - $attributes = array('length' => $length, 'type' => $type); + $attributes = array('length' => (string) $length, 'type' => $type); if ($this->version == Feed::RSS2) { $attributes['url'] = $url; @@ -405,7 +414,7 @@ public function setId($id, $permaLink = false) if (!$found) throw new \InvalidArgumentException("The ID must begin with an IANA-registered URI scheme."); - $this->addElement('id', $id, NULL, TRUE); + $this->addElement('id', $id, [], TRUE); } else throw new InvalidOperationException('A unique ID is not supported in RSS1 feeds.'); diff --git a/composer.json b/composer.json index 4629447..3167ae7 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,6 @@ "name": "Tino Goratsch" } ], - "minimum-stability": "dev", "autoload": { "psr-4": { "FeedWriter\\": "" @@ -67,9 +66,15 @@ "require" : { "php": ">=5.3.0" }, + "require-dev": { + "phpstan/phpstan": "^1.10" + }, "extra": { "branch-alias": { "dev-master": "1.1.x-dev" } + }, + "scripts": { + "phpstan": "phpstan analyse --memory-limit 512M" } } diff --git a/composer.lock b/composer.lock index 2822bbb..c59cb6a 100644 --- a/composer.lock +++ b/composer.lock @@ -1,20 +1,83 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "91f256f9c16112f0cfaf195351eb9ce7", - "content-hash": "d8fc0ae3e220f5834869ff8b73aca888", + "content-hash": "b192a64048f12719f0facfec4ec0e30b", "packages": [], - "packages-dev": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.10.47", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "84dbb33b520ea28b6cf5676a3941f4bae1c1ff39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/84dbb33b520ea28b6cf5676a3941f4bae1c1ff39", + "reference": "84dbb33b520ea28b6cf5676a3941f4bae1c1ff39", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-12-01T15:19:17+00:00" + } + ], "aliases": [], - "minimum-stability": "dev", + "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=5.3.0" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..071f1f7 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: max + + paths: + - . + + excludePaths: + - vendor/