diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3199cf1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: Test + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Test with PHP ${{ matrix.php-versions }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-versions: [8.1, 8.2, 8.3] # TODO - Add 8.4 once all tools/dependencies support it + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run PHP-CS-Fixer + run: composer phpcs:check + + - name: Run PHPStan + run: composer phpstan + + - name: Run PHPUnit + run: composer test diff --git a/.gitignore b/.gitignore index 2942141..77387d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ .idea/ composer.lock vendor/ + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### + +###> phpunit/phpunit ### +/phpunit.xml +/.phpunit.cache +###< phpunit/phpunit ### diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..70b81c3 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,35 @@ +in(__DIR__); + +return (new PhpCsFixer\Config()) + ->setParallelConfig(ParallelConfigFactory::detect()) + ->setRules([ + '@PHP81Migration' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'declare_strict_types' => true, + 'header_comment' => [ + 'header' => 'Copyright (c) Fusonic GmbH. All rights reserved.'.\PHP_EOL.'Licensed under the MIT License. '. + 'See LICENSE file in the project root for license information.', + 'location' => 'after_open', + ], + 'no_useless_else' => true, + 'no_useless_return' => true, + 'php_unit_strict' => true, + 'single_line_throw' => false, + 'strict_comparison' => true, + 'strict_param' => true, + ]) + ->setFinder($finder) + ->setRiskyAllowed(true); diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 30a9fcf..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,17 +0,0 @@ -build: - environment: - php: - version: 7.4 - tests: - override: - - - command: 'vendor/bin/phpunit --coverage-clover coverage-report' - coverage: - file: 'coverage-report' - format: 'clover' - -tools: - php_code_sniffer: - enabled: true - config: - standard: PSR2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6dcf4bd..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: php -php: - - 7.4 - - 8.0 -install: composer install -script: vendor/bin/phpunit diff --git a/CHANGELOG.md b/CHANGELOG-2.x.md similarity index 100% rename from CHANGELOG.md rename to CHANGELOG-2.x.md diff --git a/CHANGELOG-3.x.md b/CHANGELOG-3.x.md new file mode 100644 index 0000000..dd7cff8 --- /dev/null +++ b/CHANGELOG-3.x.md @@ -0,0 +1,16 @@ +# CHANGELOG for 3.x + +## 3.0.0 + +- Bumped the required PHP version from `^7.4 || ^8.0` to `^8.1` +- Bumped the compatible Symfony version from `^3.0 || ^4.0 || ^5.0 || ^6.0` to `^5.4 || ^6.4 || ^7.1` +- Upgraded PHPUnit from `^9.0` to `^11.4` and updated tests accordingly + - Added `composer test` script for running tests +- Installed `friendsofphp/php-cs-fixer` and applied Fusonic's code style + - Added `composer phpcs:check` script for validating code style + - Added `composer phpcs:fix` script for fixing code style violations +- Installed `phpstan/phpstan`, `phpstan/phpstan-deprecation-rules`, `phpstan/phpstan-phpunit` and + `phpstan/phpstan-strict-rules` and fixed reported errors + - Added `composer phpstan` script for validating code +- Updated documentation +- Switched to GitHub actions for automated testing diff --git a/LICENSE b/LICENSE index 46c173a..bb8f68c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 Fusonic GmbH (http://www.fusonic.net) +Copyright (c) 2014-2024 Fusonic GmbH (https://www.fusonic.net) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index b256cac..11e8acf 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,64 @@ # fusonic/opengraph -[![Latest Stable Version](https://poser.pugx.org/fusonic/opengraph/v/stable)](https://packagist.org/packages/fusonic/opengraph) -[![Total Downloads](https://poser.pugx.org/fusonic/opengraph/downloads)](https://packagist.org/packages/fusonic/opengraph) -[![Build Status](https://api.travis-ci.org/fusonic/opengraph.svg)](https://travis-ci.org/fusonic/opengraph) -[![License](https://poser.pugx.org/fusonic/opengraph/license)](https://packagist.org/packages/fusonic/opengraph) +[![GitHub Release](https://img.shields.io/github/v/release/fusonic/opengraph)](https://github.com/fusonic/opengraph/releases/latest) +[![Packagist Downloads](https://img.shields.io/packagist/dt/fusonic/opengraph?color=blue)](https://packagist.org/packages/fusonic/opengraph) +[![Packagist License](https://img.shields.io/packagist/l/fusonic/opengraph)](https://github.com/fusonic/opengraph/blob/master/LICENSE) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/fusonic/opengraph/test)](https://github.com/fusonic/opengraph/actions/workflows/test.yml) -A simple library to read Open Graph data from the web and generate HTML code to publish your own Open Graph objects. A fallback mode enables you to read data from websites that do not implement the Open Graph protocol. +A simple library to read Open Graph data from the web and generate HTML code to publish your own Open Graph objects. A +fallback mode enables you to read data from websites that do not implement the Open Graph protocol. -Using this library you can easily retrieve stuff like meta data, video information from YouTube or Vimeo or image information from Flickr without using site-specific APIs since they all implement the Open Graph protocol. +Using this library you can easily retrieve stuff like metadata, video information from YouTube or Vimeo or image +information from Flickr without using site-specific APIs since they all implement the Open Graph protocol. -See [ogp.me](http://ogp.me) for information on the Open Graph protocol. +See [ogp.me](https://ogp.me) for information on the Open Graph protocol. ## Requirements -* PHP 7.4+ -* [fusonic/linq](https://github.com/fusonic/linq) -* [symfony/css-selector](https://github.com/symfony/CssSelector) -* [symfony/dom-crawler](https://github.com/symfony/DomCrawler) -* [psr/http-client](https://github.com/php-fig/http-client), [psr/http-factory](https://github.com/php-fig/http-factory) and compatible implementation such as [guzzle/guzzle](https://github.com/guzzle/guzzle) +* PHP 8.1+ +* [symfony/css-selector](https://github.com/symfony/css-selector) +* [symfony/dom-crawler](https://github.com/symfony/dom-crawler) +* [psr/http-client](https://github.com/php-fig/http-client) +* [psr/http-factory](https://github.com/php-fig/http-factory) +* and compatible implementation such as [symfony/http-client](https://github.com/symfony/http-client) ## Installation The most flexible installation method is using Composer: -``` bash +```bash composer require fusonic/opengraph ``` -Install composer and run install command: -``` bash +Install Composer and run the `install` command: +```bash curl -s http://getcomposer.org/installer | php php composer.phar install ``` -Once installed, include vendor/autoload.php in your script. +Once installed, include `vendor/autoload.php` in your script. ``` php -require "vendor/autoload.php"; +require 'vendor/autoload.php'; ``` ## Usage ### Retrieve Open Graph data from a URL -``` php +```php +loadUrl("http://www.youtube.com/watch?v=P422jZg50X4"); +$object = $consumer->loadUrl('https://www.youtube.com/watch?v=P422jZg50X4'); // Basic information of the object echo "Title: " . $object->title; // Getting started with Facebook Open Graph echo "Site name: " . $object->siteName; // YouTube echo "Description: " . $object->description; // Originally recorded at the Facebook World ... -echo "Canonical URL: " . $object->url; // http://www.youtube.com/watch?v=P422jZg50X4 +echo "Canonical URL: " . $object->url; // https://www.youtube.com/watch?v=P422jZg50X4 // Images $image = $object->images[0]; @@ -63,7 +68,7 @@ echo "Image[0] width: " . $image->width; // null (May return width in pi // Videos $video = $object->videos[0]; -echo "Video URL: " . $video->url; // http://www.youtube.com/v/P422jZg50X4?version=3&autohide=1 +echo "Video URL: " . $video->url; // https://www.youtube.com/v/P422jZg50X4?version=3&autohide=1 echo "Video height: " . $video->height; // 1080 echo "Video width: " . $video->width; // 1920 echo "Video type: " . $video->type; // application/x-shockwave-flash @@ -73,7 +78,9 @@ _There are some more properties but these are the basic and most commonly used o ### Publish own Open Graph data -``` php +```php +title = "Getting started with Facebook Open Graph"; $object->siteName = "YouTube"; $object->description = "Originally recorded at the Facebook World ..." -$object->url = "http://www.youtube.com/watch?v=P422jZg50X4"; +$object->url = "https://www.youtube.com/watch?v=P422jZg50X4"; // Images $image = new Image("https://i1.ytimg.com/vi/P422jZg50X4/maxresdefault.jpg"); $object->images[] = $image; // Videos -$video = new Video("http://www.youtube.com/v/P422jZg50X4?version=3&autohide=1"); +$video = new Video("https://www.youtube.com/v/P422jZg50X4?version=3&autohide=1"); $video->height = 1080; $video->width = 1920; $video->type = "application/x-shockwave-flash"; @@ -125,18 +132,20 @@ _HTML code is formatted just for displaying purposes. You may choose between HTM ## Running tests -You can run the test suite by running `phpunit` from the command line. +You can run the test suite by running `composer test` from the command line. ## FAQ **I don't get any information from a webpage, but Facebook shows information for the same URL. What do I do wrong?** -It seems that some pages (like Twitter) only publish OpenGraph information if Facebook's user agent string `facebookexternalhit/1.1` is used (see #28). So you should configure your PSR-18 client to use this user agent string: +It seems that some pages (like Twitter/X) only publish Open Graph information if Facebook's user agent string +`facebookexternalhit/1.1` is used (see [#28](https://github.com/fusonic/opengraph/issues/28)). So you should configure +your PSR-18 client to use this user agent string: ```php -$client = new Psr18Client(new NativeHttpClient([ "headers" => [ "User-Agent" => "facebookexternalhit/1.1" ] ])); +$client = new Psr18Client(new NativeHttpClient(['headers' => ['User-Agent' => 'facebookexternalhit/1.1']])); ``` ## License -This library is licensed under the MIT license. +fusonic/opengraph is licensed under the MIT license. See [LICENSE](LICENSE) for more information. diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md new file mode 100644 index 0000000..8bb9fef --- /dev/null +++ b/UPGRADE-3.0.md @@ -0,0 +1,5 @@ +# UPGRADE FROM 2.2 TO 3.0 + +## Requirements +- Bumped the required PHP version from `^7.4 || ^8.0` to `^8.1` +- Bumped the compatible Symfony version from `^3.0 || ^4.0 || ^5.0 || ^6.0` to `^5.4 || ^6.4 || ^7.1` diff --git a/composer.json b/composer.json index b74deba..b5e19e1 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,11 @@ { "name": "fusonic/opengraph", "description": "PHP library for consuming and publishing Open Graph resources.", - "keywords": ["opengraph"], + "keywords": [ + "opengraph" + ], "type": "library", - "homepage": "https://github.com/fusonic/fusonic-opengraph", + "homepage": "https://github.com/fusonic/opengraph", "license": "MIT", "authors": [ { @@ -22,20 +24,34 @@ } }, "require": { - "php": "^7.4|^8.0", + "php": "^8.1", "ext-dom": "*", - "symfony/dom-crawler": "^3.0|^4.0|^5.0|^6.0", - "symfony/css-selector": "^3.0|^4.0|^5.0|^6.0", + "symfony/css-selector": "^5.4 || ^6.4 || ^7.1", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.1", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^9.0", - "symfony/http-client": "^6.0", - "nyholm/psr7": "^1.2" + "friendsofphp/php-cs-fixer": "^3.65", + "nyholm/psr7": "^1.8", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5 || ^11.4", + "symfony/http-client": "^5.4 || ^6.4 || ^7.1" }, "suggest": { - "symfony/http-client": "^5.0", - "nyholm/psr7": "^1.2" + "symfony/http-client": "^5.4 || ^6.4 || ^7.1", + "nyholm/psr7": "^1.8" + }, + "scripts": { + "phpcs:check": "XDEBUG_MODE=off vendor/bin/php-cs-fixer check -v --diff", + "phpcs:fix": "XDEBUG_MODE=off vendor/bin/php-cs-fixer fix -v", + "phpstan": "XDEBUG_MODE=off php -d memory_limit=2048M vendor/bin/phpstan analyse", + "test": "XDEBUG_MODE=off vendor/bin/phpunit --testdox" + }, + "config": { + "sort-packages": true } } diff --git a/examples/consume_website.php b/examples/consume_website.php index 275cbfb..d46ce27 100644 --- a/examples/consume_website.php +++ b/examples/consume_website.php @@ -1,18 +1,25 @@ [ "User-Agent" => "facebookexternalhit/1.1" ] ])); +// package, but you can use any implementation provided by your framework of choice. +$client = new Psr18Client(new NativeHttpClient(['headers' => ['User-Agent' => 'facebookexternalhit/1.1']])); // Create a new crawler $crawler = new Fusonic\OpenGraph\Consumer($client, $client); // Crawl the desired URL and retrieve a Fusonic\OpenGraph\Object in response -$object = $crawler->loadUrl("https://github.com"); +$object = $crawler->loadUrl('https://github.com'); var_dump($object); diff --git a/examples/publish_website.php b/examples/publish_website.php index b419765..71d9653 100644 --- a/examples/publish_website.php +++ b/examples/publish_website.php @@ -1,43 +1,51 @@ url = "http://www.fusonic.net"; -$website->title = "Fusonic - Intranet & Mobile Applications from Austria"; -$website->description = "Creators of the awesome fusonic-opengraph library."; -$website->siteName = "Fusonic"; -$website->locale = "en_GB"; +$website->url = 'https://www.fusonic.net'; +$website->title = 'Fusonic - Intranet & Mobile Applications from Austria'; +$website->description = 'Creators of the awesome fusonic-opengraph library.'; +$website->siteName = 'Fusonic'; +$website->locale = 'en_GB'; // Attach an image -$image = new Image("http://www.fusonic.net/en/assets/images/logo.png"); +$image = new Image('https://www.fusonic.net/en/assets/images/logo.png'); $image->width = 140; $image->height = 41; -$image->type = "image/png"; +$image->type = 'image/png'; $website->images[] = $image; // Attach a video -$video = new Video("http://www.fusonic.net/en/we-dont-have-no-video.mp4"); +$video = new Video('https://www.fusonic.net/en/we-dont-have-no-video.mp4'); $video->width = 1920; $video->height = 1080; -$video->type = "video/mp4"; +$video->type = 'video/mp4'; $website->videos[] = $video; // Attach an audio -$audio = new Audio("http://www.fusonic.net/en/we-dont-have-no-audio.mp3"); -$audio->type = "audio/mp3"; +$audio = new Audio('https://www.fusonic.net/en/we-dont-have-no-audio.mp3'); +$audio->type = 'audio/mp3'; $website->audios[] = $audio; // Create Publisher object and echo HTML code $publisher = new Publisher(); $publisher->doctype = Publisher::DOCTYPE_HTML5; + echo $publisher->generateHtml($website); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..558a859 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + level: 8 + paths: + - src + - tests + +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 0d9aeef..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - tests - - - - - src - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f6aa547 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,44 @@ + + + + + + + + + + + ./tests/ + + + + + + + + + + src + + + diff --git a/src/Consumer.php b/src/Consumer.php index 74aec6a..7dc884a 100644 --- a/src/Consumer.php +++ b/src/Consumer.php @@ -1,23 +1,26 @@ client = $client; - $this->requestFactory = $requestFactory; + public function __construct( + private ?ClientInterface $client = null, + private ?RequestFactoryInterface $requestFactory = null, + ) { } /** * Fetches HTML content from the given URL and then crawls it for Open Graph data. * - * @param string $url URL to be crawled. - * - * @return ObjectBase + * @param string $url URL to be crawled * * @throws ClientExceptionInterface */ public function loadUrl(string $url): ObjectBase { - if ($this->client === null) { - throw new LogicException( - "To use loadUrl() you must provide \$client and \$requestFactory when instantiating the consumer." + if (null === $this->client || null === $this->requestFactory) { + throw new \LogicException( + 'To use loadUrl() you must provide $client and $requestFactory when instantiating the consumer.' ); } - $request = $this->requestFactory->createRequest("GET", $url); + $request = $this->requestFactory->createRequest('GET', $url); $response = $this->client->sendRequest($request); return $this->loadHtml($response->getBody()->getContents(), $url); @@ -66,18 +67,16 @@ public function loadUrl(string $url): ObjectBase /** * Crawls the given HTML string for OpenGraph data. * - * @param string $html HTML string, usually whole content of crawled web resource. - * @param string $fallbackUrl URL to use when fallback mode is enabled. - * - * @return ObjectBase + * @param string $html HTML string, usually whole content of crawled web resource + * @param string|null $fallbackUrl URL to use when fallback mode is enabled */ - public function loadHtml(string $html, string $fallbackUrl = null): ObjectBase + public function loadHtml(string $html, ?string $fallbackUrl = null): ObjectBase { // Extract all data that can be found $page = $this->extractOpenGraphData($html); // Use the user's URL as fallback - if ($this->useFallbackMode && $page->url === null) { + if ($this->useFallbackMode && null === $page->url) { $page->url = $fallbackUrl; } @@ -87,26 +86,27 @@ public function loadHtml(string $html, string $fallbackUrl = null): ObjectBase private function extractOpenGraphData(string $content): ObjectBase { - $crawler = new Crawler; - $crawler->addHTMLContent($content, 'UTF-8'); + $crawler = new Crawler(); + $crawler->addHtmlContent(content: $content); $properties = []; - foreach(['name', 'property'] as $t) - { + foreach (['name', 'property'] as $t) { // Get all meta-tags starting with "og:" $ogMetaTags = $crawler->filter("meta[{$t}^='og:']"); // Create clean property array $props = []; + + /** @var \DOMElement $tag */ foreach ($ogMetaTags as $tag) { $name = strtolower(trim($tag->getAttribute($t))); - $value = trim($tag->getAttribute("content")); + $value = trim($tag->getAttribute('content')); $props[] = new Property($name, $value); } $properties = array_merge($properties, $props); } - + // Create new object $object = new Website(); @@ -114,26 +114,26 @@ private function extractOpenGraphData(string $content): ObjectBase $object->assignProperties($properties, $this->debug); // Fallback for url - if ($this->useFallbackMode && !$object->url) { + if ($this->useFallbackMode && null === $object->url) { $urlElement = $crawler->filter("link[rel='canonical']")->first(); if ($urlElement->count() > 0) { - $object->url = trim($urlElement->attr("href")); + $object->url = trim($urlElement->attr('href') ?? ''); } } // Fallback for title - if ($this->useFallbackMode && !$object->title) { - $titleElement = $crawler->filter("title")->first(); + if ($this->useFallbackMode && null === $object->title) { + $titleElement = $crawler->filter('title')->first(); if ($titleElement->count() > 0) { $object->title = trim($titleElement->text()); } } // Fallback for description - if ($this->useFallbackMode && !$object->description) { + if ($this->useFallbackMode && null === $object->description) { $descriptionElement = $crawler->filter("meta[property='description']")->first(); if ($descriptionElement->count() > 0) { - $object->description = trim($descriptionElement->attr("content")); + $object->description = trim($descriptionElement->attr('content') ?? ''); } } diff --git a/src/Elements/Audio.php b/src/Elements/Audio.php index 20df532..394348c 100644 --- a/src/Elements/Audio.php +++ b/src/Elements/Audio.php @@ -1,5 +1,12 @@ url = $url; } /** * Gets all properties set on this element. * - * @return array|Property[] + * @return Property[] */ - public function getProperties() + public function getProperties(): array { $properties = []; // URL must precede all other properties - if ($this->url !== null) { + if (null !== $this->url) { $properties[] = new Property(Property::AUDIO_URL, $this->url); } - if ($this->secureUrl !== null) { + if (null !== $this->secureUrl) { $properties[] = new Property(Property::AUDIO_SECURE_URL, $this->secureUrl); } - if ($this->type !== null) { + if (null !== $this->type) { $properties[] = new Property(Property::AUDIO_TYPE, $this->type); } diff --git a/src/Elements/ElementBase.php b/src/Elements/ElementBase.php index dd4139d..5660064 100644 --- a/src/Elements/ElementBase.php +++ b/src/Elements/ElementBase.php @@ -1,20 +1,25 @@ url = $url; } /** * Gets all properties set on this element. * - * @return array|Property[] + * @return Property[] */ - public function getProperties() + public function getProperties(): array { $properties = []; // URL must precede all other properties - if ($this->url !== null) { + if (null !== $this->url) { $properties[] = new Property(Property::IMAGE_URL, $this->url); } - if ($this->height !== null) { + if (null !== $this->height) { $properties[] = new Property(Property::IMAGE_HEIGHT, $this->height); } - if ($this->secureUrl !== null) { + if (null !== $this->secureUrl) { $properties[] = new Property(Property::IMAGE_SECURE_URL, $this->secureUrl); } - if ($this->type !== null) { + if (null !== $this->type) { $properties[] = new Property(Property::IMAGE_TYPE, $this->type); } - if ($this->width !== null) { + if (null !== $this->width) { $properties[] = new Property(Property::IMAGE_WIDTH, $this->width); } - if ($this->userGenerated !== null) { + if (null !== $this->userGenerated) { $properties[] = new Property(Property::IMAGE_USER_GENERATED, $this->userGenerated); } diff --git a/src/Elements/Video.php b/src/Elements/Video.php index ff5a330..c5062fe 100644 --- a/src/Elements/Video.php +++ b/src/Elements/Video.php @@ -1,5 +1,12 @@ url = $url; } /** * Gets all properties set on this element. * - * @return array|Property[] + * @return Property[] */ - public function getProperties() + public function getProperties(): array { $properties = []; // URL must precede all other properties - if ($this->url !== null) { + if (null !== $this->url) { $properties[] = new Property(Property::VIDEO_URL, $this->url); } - if ($this->height !== null) { + if (null !== $this->height) { $properties[] = new Property(Property::VIDEO_HEIGHT, $this->height); } - if ($this->secureUrl !== null) { + if (null !== $this->secureUrl) { $properties[] = new Property(Property::VIDEO_SECURE_URL, $this->secureUrl); } - if ($this->type !== null) { + if (null !== $this->type) { $properties[] = new Property(Property::VIDEO_TYPE, $this->type); } - if ($this->width !== null) { + if (null !== $this->width) { $properties[] = new Property(Property::VIDEO_WIDTH, $this->width); } diff --git a/src/Objects/ObjectBase.php b/src/Objects/ObjectBase.php index 33b9976..40e3bf9 100644 --- a/src/Objects/ObjectBase.php +++ b/src/Objects/ObjectBase.php @@ -1,16 +1,21 @@ key; $value = $property->value; - switch($name) { + switch ($name) { case Property::AUDIO: case Property::AUDIO_URL: $this->audios[] = new Audio($value); break; case Property::AUDIO_SECURE_URL: case Property::AUDIO_TYPE: - if (count($this->audios) > 0) { - $this->handleAudioAttribute($this->audios[count($this->audios) - 1], $name, $value); + if (\count($this->audios) > 0) { + $this->handleAudioAttribute($this->audios[\count($this->audios) - 1], $name, $value); } elseif ($debug) { - throw new UnexpectedValueException( - sprintf( + throw new \UnexpectedValueException( + \sprintf( "Found '%s' property but no audio was found before.", $name ) @@ -130,12 +131,12 @@ public function assignProperties(array $properties, $debug = false): void } break; case Property::DESCRIPTION: - if ($this->description === null) { + if (null === $this->description) { $this->description = $value; } break; case Property::DETERMINER: - if ($this->determiner === null) { + if (null === $this->determiner) { $this->determiner = $value; } break; @@ -148,11 +149,11 @@ public function assignProperties(array $properties, $debug = false): void case Property::IMAGE_TYPE: case Property::IMAGE_WIDTH: case Property::IMAGE_USER_GENERATED: - if (count($this->images) > 0) { - $this->handleImageAttribute($this->images[count($this->images) - 1], $name, $value); + if (\count($this->images) > 0) { + $this->handleImageAttribute($this->images[\count($this->images) - 1], $name, $value); } elseif ($debug) { - throw new UnexpectedValueException( - sprintf( + throw new \UnexpectedValueException( + \sprintf( "Found '%s' property but no image was found before.", $name ) @@ -160,7 +161,7 @@ public function assignProperties(array $properties, $debug = false): void } break; case Property::LOCALE: - if ($this->locale === null) { + if (null === $this->locale) { $this->locale = $value; } break; @@ -174,22 +175,22 @@ public function assignProperties(array $properties, $debug = false): void $this->seeAlso[] = $value; break; case Property::SITE_NAME: - if ($this->siteName === null) { + if (null === $this->siteName) { $this->siteName = $value; } break; case Property::TITLE: - if ($this->title === null) { + if (null === $this->title) { $this->title = $value; } break; case Property::UPDATED_TIME: - if ($this->updatedTime === null) { + if (null === $this->updatedTime) { $this->updatedTime = $this->convertToDateTime($value); } break; case Property::URL: - if ($this->url === null) { + if (null === $this->url) { $this->url = $value; } break; @@ -201,10 +202,10 @@ public function assignProperties(array $properties, $debug = false): void case Property::VIDEO_SECURE_URL: case Property::VIDEO_TYPE: case Property::VIDEO_WIDTH: - if (count($this->videos) > 0) { - $this->handleVideoAttribute($this->videos[count($this->videos) - 1], $name, $value); + if (\count($this->videos) > 0) { + $this->handleVideoAttribute($this->videos[\count($this->videos) - 1], $name, $value); } elseif ($debug) { - throw new UnexpectedValueException(sprintf( + throw new \UnexpectedValueException(\sprintf( "Found '%s' property but no video was found before.", $name )); @@ -215,13 +216,12 @@ public function assignProperties(array $properties, $debug = false): void private function handleImageAttribute(Image $element, string $name, string $value): void { - switch($name) - { + switch ($name) { case Property::IMAGE_HEIGHT: - $element->height = (int)$value; + $element->height = (int) $value; break; case Property::IMAGE_WIDTH: - $element->width = (int)$value; + $element->width = (int) $value; break; case Property::IMAGE_TYPE: $element->type = $value; @@ -237,13 +237,12 @@ private function handleImageAttribute(Image $element, string $name, string $valu private function handleVideoAttribute(Video $element, string $name, string $value): void { - switch($name) - { + switch ($name) { case Property::VIDEO_HEIGHT: - $element->height = (int)$value; + $element->height = (int) $value; break; case Property::VIDEO_WIDTH: - $element->width = (int)$value; + $element->width = (int) $value; break; case Property::VIDEO_TYPE: $element->type = $value; @@ -256,8 +255,7 @@ private function handleVideoAttribute(Video $element, string $name, string $valu private function handleAudioAttribute(Audio $element, string $name, string $value): void { - switch($name) - { + switch ($name) { case Property::AUDIO_TYPE: $element->type = $value; break; @@ -267,10 +265,10 @@ private function handleAudioAttribute(Audio $element, string $name, string $valu } } - protected function convertToDateTime(string $value): ?DateTimeImmutable + protected function convertToDateTime(string $value): ?\DateTimeImmutable { try { - return new DateTimeImmutable($value); + return new \DateTimeImmutable($value); } catch (\Exception $e) { return null; } @@ -278,10 +276,9 @@ protected function convertToDateTime(string $value): ?DateTimeImmutable protected function convertToBoolean(string $value): bool { - switch(strtolower($value)) - { - case "1": - case "true": + switch (strtolower($value)) { + case '1': + case 'true': return true; default: return false; @@ -291,7 +288,7 @@ protected function convertToBoolean(string $value): bool /** * Gets all properties set on this object. * - * @return Property[] + * @return Property[] */ public function getProperties(): array { @@ -300,16 +297,16 @@ public function getProperties(): array foreach ($this->audios as $audio) { $properties = array_merge($properties, $audio->getProperties()); } - - if ($this->title !== null) { + + if (null !== $this->title) { $properties[] = new Property(Property::TITLE, $this->title); } - if ($this->description !== null) { + if (null !== $this->description) { $properties[] = new Property(Property::DESCRIPTION, $this->description); } - if ($this->determiner !== null) { + if (null !== $this->determiner) { $properties[] = new Property(Property::DETERMINER, $this->determiner); } @@ -317,7 +314,7 @@ public function getProperties(): array $properties = array_merge($properties, $image->getProperties()); } - if ($this->locale !== null) { + if (null !== $this->locale) { $properties[] = new Property(Property::LOCALE, $this->locale); } @@ -325,27 +322,27 @@ public function getProperties(): array $properties[] = new Property(Property::LOCALE_ALTERNATE, $locale); } - if ($this->richAttachment !== null) { - $properties[] = new Property(Property::RICH_ATTACHMENT, (int)$this->richAttachment); + if (null !== $this->richAttachment) { + $properties[] = new Property(Property::RICH_ATTACHMENT, (int) $this->richAttachment); } foreach ($this->seeAlso as $seeAlso) { $properties[] = new Property(Property::SEE_ALSO, $seeAlso); } - if ($this->siteName !== null) { + if (null !== $this->siteName) { $properties[] = new Property(Property::SITE_NAME, $this->siteName); } - if ($this->type !== null) { + if (null !== $this->type) { $properties[] = new Property(Property::TYPE, $this->type); } - if ($this->updatedTime !== null) { - $properties[] = new Property(Property::UPDATED_TIME, $this->updatedTime->format("c")); + if (null !== $this->updatedTime) { + $properties[] = new Property(Property::UPDATED_TIME, $this->updatedTime->format('c')); } - if ($this->url !== null) { + if (null !== $this->url) { $properties[] = new Property(Property::URL, $this->url); } diff --git a/src/Objects/Website.php b/src/Objects/Website.php index f66428c..a973c77 100644 --- a/src/Objects/Website.php +++ b/src/Objects/Website.php @@ -1,5 +1,12 @@ type = self::TYPE; } } diff --git a/src/Property.php b/src/Property.php index e94593b..00fe7be 100644 --- a/src/Property.php +++ b/src/Property.php @@ -1,5 +1,12 @@ key = $key; - $this->value = $value; + /** + * Value of the property. + */ + public mixed $value, + ) { } } diff --git a/src/Publisher.php b/src/Publisher.php index 059363a..7a6eb66 100644 --- a/src/Publisher.php +++ b/src/Publisher.php @@ -1,18 +1,23 @@ doctype == self::DOCTYPE_XHTML ? " />" : ">"); + $html = ''; + $format = 'doctype ? ' />' : '>'); foreach ($object->getProperties() as $property) { - if ($html !== "") { + if ('' !== $html) { $html .= "\n"; } - if ($property->value === null) { + if (null === $property->value) { continue; - } elseif ($property->value instanceof DateTimeInterface) { - $value = $property->value->format("c"); - } elseif (is_object($property->value)) { - throw new UnexpectedValueException( - sprintf( + } elseif ($property->value instanceof \DateTimeInterface) { + $value = $property->value->format('c'); + } elseif (\is_object($property->value)) { + throw new \UnexpectedValueException( + \sprintf( "Cannot handle value of type '%s' for property '%s'.", - get_class($property->value), + \get_class($property->value), $property->key ) ); - } elseif ($property->value === true) { - $value = "1"; - } elseif ($property->value === false) { - $value = "0"; + } elseif (true === $property->value) { + $value = '1'; + } elseif (false === $property->value) { + $value = '0'; } else { - $value = (string)$property->value; + $value = (string) $property->value; } - $html .= sprintf($format, $property->key, htmlspecialchars($value)); + $html .= \sprintf($format, $property->key, htmlspecialchars($value)); } return $html; diff --git a/tests/ConsumerTest.php b/tests/ConsumerTest.php index 4fa285c..652d124 100644 --- a/tests/ConsumerTest.php +++ b/tests/ConsumerTest.php @@ -1,387 +1,442 @@ - - - - - - - - - - - - - - - - -LONG; + + + + + + + + + + + + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content, "about:blank"); - - $this->assertEquals("Description", $res->description); - $this->assertEquals("auto", $res->determiner); - $this->assertEquals("en_GB", $res->locale); - $this->assertContains("en_US", $res->localeAlternate); - $this->assertContains("de_AT", $res->localeAlternate); - $this->assertTrue($res->richAttachment); - $this->assertContains("https://github.com/fusonic/fusonic-linq", $res->seeAlso); - $this->assertContains("https://github.com/fusonic/fusonic-spreadsheetexport", $res->seeAlso); - $this->assertEquals("Site name", $res->siteName); - $this->assertEquals("Title", $res->title); - $this->assertTrue($res->updatedTime instanceof \DateTimeInterface); - $this->assertEquals("https://github.com/fusonic/fusonic-opengraph", $res->url); + // act + $result = $consumer->loadHtml($content, 'about:blank'); + + // assert + self::assertSame('Description', $result->description); + self::assertSame('auto', $result->determiner); + self::assertSame('en_GB', $result->locale); + self::assertContains('en_US', $result->localeAlternate); + self::assertContains('de_AT', $result->localeAlternate); + self::assertTrue($result->richAttachment); + self::assertContains('https://github.com/fusonic/fusonic-linq', $result->seeAlso); + self::assertContains('https://github.com/fusonic/fusonic-spreadsheetexport', $result->seeAlso); + self::assertSame('Site name', $result->siteName); + self::assertSame('Title', $result->title); + self::assertInstanceOf(\DateTimeInterface::class, $result->updatedTime); + self::assertSame('https://github.com/fusonic/fusonic-opengraph', $result->url); } /** * Checks crawler not to use fallback if disabled even if no OG data is provided. */ - public function testLoadHtmlFallbacksOff() + public function testLoadHtmlFallbacksOff(): void { + // arrange $content = << - -Title - - - - -LONG; + + + Title + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content, "about:blank"); + // act + $result = $consumer->loadHtml($content, 'about:blank'); - $this->assertNull($res->description); - $this->assertNull($res->title); - $this->assertNull($res->url); + // assert + self::assertNull($result->description); + self::assertNull($result->title); + self::assertNull($result->url); } /** * Checks crawler to correctly use fallback elements when activated. */ - public function testLoadHtmlFallbacksOn() + public function testLoadHtmlFallbacksOn(): void { + // arrange $content = << - -Title - - - - -LONG; + + + Title + + + + + LONG; $consumer = new Consumer(); $consumer->useFallbackMode = true; - $res = $consumer->loadHtml($content, "about:blank"); + // act + $result = $consumer->loadHtml($content, 'about:blank'); - $this->assertEquals("Description", $res->description); - $this->assertEquals("Title", $res->title); - $this->assertEquals("about:blank", $res->url); + // assert + self::assertSame('Description', $result->description); + self::assertSame('Title', $result->title); + self::assertSame('about:blank', $result->url); } /** * Checks crawler to correctly use fallback elements when activated. */ - public function testLoadHtmlCanonicalLinkFallbacksOn() + public function testLoadHtmlCanonicalLinkFallbacksOn(): void { + // arrange $content = << - -Title - - - - - -LONG; + + + Title + + + + + + LONG; $consumer = new Consumer(); $consumer->useFallbackMode = true; - $res = $consumer->loadHtml($content, "about:blank"); + // act + $result = $consumer->loadHtml($content, 'about:blank'); - $this->assertEquals("Description", $res->description); - $this->assertEquals("Title", $res->title); - $this->assertEquals("https://github.com/fusonic/opengraph", $res->url); + // assert + self::assertSame('Description', $result->description); + self::assertSame('Title', $result->title); + self::assertSame('https://github.com/fusonic/opengraph', $result->url); } /** * Checks crawler to handle arrays of elements with child-properties like described in the * Open Graph documentation (http://ogp.me/#array). */ - public function testLoadHtmlArrayHandling() + public function testLoadHtmlArrayHandling(): void { + // arrange $content = << - - - - - - - - - - -LONG; + + + + + + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); - - $this->assertEquals(3, count($res->images)); - $this->assertEquals("http://example.com/rock.jpg", $res->images[0]->url); - $this->assertEquals(300, $res->images[0]->width); - $this->assertEquals(300, $res->images[0]->height); - $this->assertEquals("http://example.com/rock2.jpg", $res->images[1]->url); - $this->assertNull($res->images[1]->width); - $this->assertNull($res->images[1]->height); - $this->assertEquals("http://example.com/rock3.jpg", $res->images[2]->url); - $this->assertNull($res->images[2]->width); - $this->assertEquals(1000, $res->images[2]->height); + // act + $result = $consumer->loadHtml($content); + + // assert + self::assertCount(3, $result->images); + self::assertSame('http://example.com/rock.jpg', $result->images[0]->url); + self::assertSame(300, $result->images[0]->width); + self::assertSame(300, $result->images[0]->height); + self::assertSame('http://example.com/rock2.jpg', $result->images[1]->url); + self::assertNull($result->images[1]->width); + self::assertNull($result->images[1]->height); + self::assertSame('http://example.com/rock3.jpg', $result->images[2]->url); + self::assertNull($result->images[2]->width); + self::assertSame(1000, $result->images[2]->height); } - public function testLoadHtmlImages() + public function testLoadHtmlImages(): void { + // arrange $content = << - - - - - - - - - -LONG; + + + + + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(1, count($res->images)); - $this->assertEquals("http://example.com/rock.jpg", $res->images[0]->url); - $this->assertEquals("https://example.com/rock.jpg", $res->images[0]->secureUrl); - $this->assertEquals(300, $res->images[0]->width); - $this->assertEquals(300, $res->images[0]->height); - $this->assertEquals("image/jpg", $res->images[0]->type); + // assert + self::assertCount(1, $result->images); + self::assertSame('http://example.com/rock.jpg', $result->images[0]->url); + self::assertSame('https://example.com/rock.jpg', $result->images[0]->secureUrl); + self::assertSame(300, $result->images[0]->width); + self::assertSame(300, $result->images[0]->height); + self::assertSame('image/jpg', $result->images[0]->type); } - public function testLoadHtmlVideos() + public function testLoadHtmlVideos(): void { + // arrange $content = << - - - - - - - - - -LONG; + + + + + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(1, count($res->videos)); - $this->assertEquals("http://example.com/rock.ogv", $res->videos[0]->url); - $this->assertEquals("https://example.com/rock.ogv", $res->videos[0]->secureUrl); - $this->assertEquals(300, $res->videos[0]->width); - $this->assertEquals(300, $res->videos[0]->height); - $this->assertEquals("video/ogv", $res->videos[0]->type); + // assert + self::assertCount(1, $result->videos); + self::assertSame('http://example.com/rock.ogv', $result->videos[0]->url); + self::assertSame('https://example.com/rock.ogv', $result->videos[0]->secureUrl); + self::assertSame(300, $result->videos[0]->width); + self::assertSame(300, $result->videos[0]->height); + self::assertSame('video/ogv', $result->videos[0]->type); } - public function testLoadHtmlAudios() + public function testLoadHtmlAudios(): void { + // arrange $content = << - - - - - - - -LONG; + + + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(1, count($res->audios)); - $this->assertEquals("http://example.com/rock.mp3", $res->audios[0]->url); - $this->assertEquals("https://example.com/rock.mp3", $res->audios[0]->secureUrl); - $this->assertEquals("audio/mp3", $res->audios[0]->type); + // assert + self::assertCount(1, $result->audios); + self::assertSame('http://example.com/rock.mp3', $result->audios[0]->url); + self::assertSame('https://example.com/rock.mp3', $result->audios[0]->secureUrl); + self::assertSame('audio/mp3', $result->audios[0]->type); } - public function testCrawlHtmlImageExceptionDebugOff() + public function testCrawlHtmlImageExceptionDebugOff(): void { + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(0, count($res->images)); + // assert + self::assertCount(0, $result->images); } - public function testCrawlHtmlImageExceptionDebugOn() + public function testCrawlHtmlImageExceptionDebugOn(): void { + // assert $this->expectException(\UnexpectedValueException::class); + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); $consumer->debug = true; - $res = $consumer->loadHtml($content); + // act + $consumer->loadHtml($content); } - public function testCrawlHtmlVideoExceptionDebugOff() + public function testCrawlHtmlVideoExceptionDebugOff(): void { + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(0, count($res->videos)); + // assert + self::assertCount(0, $result->videos); } - public function testCrawlHtmlVideoExceptionDebugOn() + public function testCrawlHtmlVideoExceptionDebugOn(): void { + // assert $this->expectException(\UnexpectedValueException::class); + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); $consumer->debug = true; - $res = $consumer->loadHtml($content); + // act + $consumer->loadHtml($content); } - public function testCrawlHtmlAudioExceptionDebugOff() + public function testCrawlHtmlAudioExceptionDebugOff(): void { + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals(0, count($res->audios)); + // assert + self::assertCount(0, $result->audios); } - public function testCrawlHtmlAudioExceptionDebugOn() + public function testCrawlHtmlAudioExceptionDebugOn(): void { + // assert $this->expectException(\UnexpectedValueException::class); + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); $consumer->debug = true; - $res = $consumer->loadHtml($content); + // act + $consumer->loadHtml($content); } - public function testLoadHtmlSpecialCharacters() + public function testLoadHtmlSpecialCharacters(): void { + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals("Apples & Bananas - just \"Fruits\"", $res->title); + // assert + self::assertSame('Apples & Bananas - just "Fruits"', $result->title); } - - public function testReadMetaName() + + public function testReadMetaName(): void { + // arrange $content = << - - - - - -LONG; + + + + + + + LONG; $consumer = new Consumer(); - $res = $consumer->loadHtml($content); + // act + $result = $consumer->loadHtml($content); - $this->assertEquals("A 'name' attribute instead of 'property'", $res->title); + // assert + self::assertSame("A 'name' attribute instead of 'property'", $result->title); } } diff --git a/tests/PublisherTest.php b/tests/PublisherTest.php index 6cb0559..869c4d2 100644 --- a/tests/PublisherTest.php +++ b/tests/PublisherTest.php @@ -1,71 +1,80 @@ publisher = new Publisher(); + // arrange + $publisher = new Publisher(); - parent::setUp(); - } - - public function testGenerateHtmlNull() - { $object = new TestPublishObject(null); - $result = $this->publisher->generateHtml($object); + // act + $result = $publisher->generateHtml($object); - $this->assertEquals("", $result); + // assert + self::assertSame('', $result); } - public function generateHtmlValuesProvider() + #[DataProvider('generateHtmlValuesProvider')] + public function testGenerateHtmlValues(int|float|bool|string|\DateTime $value, string $expectedContent): void { - return [ - "Boolean true" => [ true, "1" ], - "Boolean false" => [ false, "0" ], - "Integer 1" => [ 1, "1" ], - "Integer -1" => [ -1, "-1" ], - "Float 1.11111" => [ 1.11111, "1.11111" ], - "Float -1.11111" => [ -1.11111, "-1.11111" ], - "DateTime" => [ new DateTime("2014-07-21T20:14:00+02:00"), "2014-07-21T20:14:00+02:00" ], - "String" => [ "string", "string" ], - "String with quotes" => [ "some \" quotes", "some " quotes" ], - "String with ampersands" => [ "some & ampersand", "some & ampersand" ], - ]; - } + // arrange + $publisher = new Publisher(); - /** - * @dataProvider generateHtmlValuesProvider - */ - public function testGenerateHtmlValues($value, $expectedContent) - { $object = new TestPublishObject($value); - $result = $this->publisher->generateHtml($object); + // act + $result = $publisher->generateHtml($object); - $this->assertEquals('', $result); + // assert + self::assertSame(\sprintf('', TestPublishObject::KEY, $expectedContent), $result); } - public function testGenerateHtmlUnsupportedObject() + public function testGenerateHtmlUnsupportedObject(): void { - $this->expectException(UnexpectedValueException::class); + // assert + $this->expectException(\UnexpectedValueException::class); - $object = new TestPublishObject(new stdClass()); + // arrange + $publisher = new Publisher(); + + $object = new TestPublishObject(new \stdClass()); + + // act + $publisher->generateHtml($object); + } - $this->publisher->generateHtml($object); + /** + * @return array> + */ + public static function generateHtmlValuesProvider(): array + { + return [ + 'Boolean true' => [true, '1'], + 'Boolean false' => [false, '0'], + 'Integer 1' => [1, '1'], + 'Integer -1' => [-1, '-1'], + 'Float 1.11111' => [1.11111, '1.11111'], + 'Float -1.11111' => [-1.11111, '-1.11111'], + 'DateTime' => [new \DateTime('2014-07-21T20:14:00+02:00'), '2014-07-21T20:14:00+02:00'], + 'String' => ['string', 'string'], + 'String with quotes' => ['some " quotes', 'some " quotes'], + 'String with ampersands' => ['some & ampersand', 'some & ampersand'], + ]; } } diff --git a/tests/TestData/TestPublishObject.php b/tests/TestData/TestPublishObject.php index dd1dcf0..438fbbe 100644 --- a/tests/TestData/TestPublishObject.php +++ b/tests/TestData/TestPublishObject.php @@ -1,21 +1,24 @@ value = $value; + public function __construct( + private readonly mixed $value, + ) { } public function getProperties(): array