From c77f01202f9536cf7a18db3d6d83dd43ec1fefec Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 3 Dec 2023 23:46:41 +0100 Subject: [PATCH 1/5] Correct some type mismatches --- Feed.php | 6 +++--- Item.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Feed.php b/Feed.php index 6329532..ebca313 100644 --- a/Feed.php +++ b/Feed.php @@ -198,7 +198,7 @@ public function addNamespace($prefix, $uri) * @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.'); @@ -670,7 +670,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) . '-'; @@ -977,7 +977,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..4c02fe9 100644 --- a/Item.php +++ b/Item.php @@ -83,7 +83,7 @@ private function cpt() * @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.'); @@ -308,7 +308,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 +405,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.'); From 042d639057abf1c580f676fdbfd03383a45e8ccf Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Tue, 8 Aug 2023 21:01:40 +0200 Subject: [PATCH 2/5] Add annotations for static analysis This will allow using PHPStan to detect bugs in both FeedWriter and projects using it. Additionally, it will provide better information for IDEs. --- Feed.php | 52 +++++++++++++++++++++++++++++++++++++++------------- Item.php | 19 ++++++++++++++----- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Feed.php b/Feed.php index ebca313..2af9cc2 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,9 +218,9 @@ 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. */ @@ -221,7 +247,7 @@ public function setChannelElement($elementName, $content, array $attributes = [] * 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. @@ -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. diff --git a/Item.php b/Item.php index 4c02fe9..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,8 +85,8 @@ 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 @@ -113,7 +122,7 @@ public function addElement($elementName, $content, array $attributes = [], $over * 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. */ From a89f5dffbb26a8380578ca3b74b9de1964eb3e97 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 4 Dec 2023 00:10:12 +0100 Subject: [PATCH 3/5] Unify channel tags Do not special case storage of unique elements, just overwrite them at insertion if present. This will make it easier for both PHPStan and programmer to reason about. --- Feed.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Feed.php b/Feed.php index 2af9cc2..a0d2843 100644 --- a/Feed.php +++ b/Feed.php @@ -62,7 +62,7 @@ abstract class Feed /** * Collection of all channel elements * - * @var array> + * @var array> */ private $channels = array(); @@ -237,7 +237,7 @@ public function setChannelElement($elementName, $content, array $attributes = [] if ($multiple === TRUE) $this->channels[$elementName][] = $entity; else - $this->channels[$elementName] = $entity; + $this->channels[$elementName] = [$entity]; return $this; } @@ -916,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; } @@ -928,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']); } } @@ -953,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; From 24939e4f3c4c9670d63c338fb6f1d9a71a6e84d0 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 9 Aug 2023 00:18:59 +0200 Subject: [PATCH 4/5] Add PHPStan This is static analysis tool. It can be executed using `composer phpstan`. --- composer.json | 7 ++++- composer.lock | 75 ++++++++++++++++++++++++++++++++++++++++++++++----- phpstan.neon | 8 ++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 phpstan.neon 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/ From 2b26b5f0f4a7953d184e14425ccf8b500dd466bf Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 9 Aug 2023 00:24:42 +0200 Subject: [PATCH 5/5] ci: Add PHPStan --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/ci.yml 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"