├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── config.php ├── phpunit.xml ├── src ├── Rumenx │ └── Feed │ │ ├── Feed.php │ │ ├── FeedCacheInterface.php │ │ ├── FeedConfigInterface.php │ │ ├── FeedResponseInterface.php │ │ ├── FeedServiceProvider.php │ │ ├── FeedViewInterface.php │ │ ├── Laravel │ │ ├── LaravelCacheAdapter.php │ │ ├── LaravelConfigAdapter.php │ │ ├── LaravelResponseAdapter.php │ │ └── LaravelViewAdapter.php │ │ └── Symfony │ │ ├── SymfonyCacheAdapterImpl.php │ │ ├── SymfonyConfigAdapter.php │ │ ├── SymfonyResponseAdapter.php │ │ └── SymfonyViewAdapter.php ├── config │ └── config.php └── views │ ├── atom.blade.php │ └── rss.blade.php └── tests ├── Feature ├── FeedAddItemMultiBranchTest.php ├── FeedAddItemMultiTest.php ├── FeedCacheKeyTest.php ├── FeedCacheTest.php ├── FeedClearCacheTest.php ├── FeedCustomViewTest.php ├── FeedDateFormatTest.php ├── FeedFormatDateBranchTest.php ├── FeedFormatTest.php ├── FeedGetShorteningTest.php ├── FeedIsCachedTest.php ├── FeedLinkTest.php ├── FeedNamespacesTest.php ├── FeedRenderBranchesTest.php ├── FeedRenderCachePutTest.php ├── FeedRenderTest.php ├── FeedServiceProviderTest.php ├── FeedSetCacheEdgeTest.php ├── FeedSettersTest.php ├── FeedShorteningLimitTest.php ├── FeedShorteningTest.php ├── FeedTest.php └── FeedUncoveredMethodsTest.php ├── Pest.php └── Unit ├── LaravelCacheAdapterTest.php ├── LaravelConfigAdapterTest.php ├── LaravelResponseAdapterTest.php ├── LaravelViewAdapterTest.php ├── SymfonyCacheAdapterTest.php ├── SymfonyConfigAdapterTest.php ├── SymfonyResponseAdapterTest.php └── SymfonyViewAdapterTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [{package.json,.travis.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | pest: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php-version: ['8.3', '8.4'] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-version }} 21 | coverage: xdebug 22 | - name: Install dependencies 23 | run: composer install --prefer-dist --no-interaction --no-progress 24 | - name: Run Pest with coverage 25 | run: ./vendor/bin/pest --coverage-clover=coverage.xml 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v4 28 | with: 29 | files: ./coverage.xml 30 | fail_ci_if_error: true 31 | verbose: true 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .directory 5 | .DS_Store 6 | Thumbs.db 7 | *.sh 8 | /tests/tmp 9 | .php_cs 10 | /.php_cs.cache 11 | /.phpunit.result.cache 12 | .idea/ 13 | /build 14 | /html 15 | --cache-directory/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Rumen Damyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Feed (Framework-Agnostic) 2 | 3 | A modern, framework-agnostic PHP Feed generator for Laravel, Symfony, and any PHP project. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer require rumenx/php-feed 9 | ``` 10 | 11 | ## Usage Examples 12 | 13 | ### Laravel 14 | 15 | Register the service provider (auto-discovery is supported for Laravel 5.5+): 16 | 17 | ```php 18 | // config/app.php 19 | 'providers' => [ 20 | // ...existing code... 21 | Rumenx\Feed\FeedServiceProvider::class, 22 | ], 23 | ``` 24 | 25 | Publish package views (optional): 26 | 27 | ```bash 28 | php artisan vendor:publish --provider="Rumenx\Feed\FeedServiceProvider" 29 | ``` 30 | 31 | Use the Feed in your controller: 32 | 33 | ```php 34 | use Rumenx\Feed\Feed; 35 | 36 | public function feed(Feed $feed) 37 | { 38 | $feed->setTitle('My Blog Feed'); 39 | $feed->addItem([ 40 | 'title' => 'First Post', 41 | 'author' => 'Rumen', 42 | 'link' => 'https://example.com/post/1', 43 | 'pubdate' => now(), 44 | 'description' => 'This is the first post.' 45 | ]); 46 | return $feed->render('rss'); 47 | } 48 | ``` 49 | 50 | ### Symfony 51 | 52 | Register the adapters as services in your Symfony config: 53 | 54 | ```yaml 55 | # config/services.yaml 56 | services: 57 | Rumenx\Feed\Feed: 58 | arguments: 59 | $params: 60 | cache: '@Rumenx\Feed\SymfonyCacheAdapterImpl' 61 | config: '@Rumenx\Feed\SymfonyConfigAdapter' 62 | response: '@Rumenx\Feed\SymfonyResponseAdapter' 63 | view: '@Rumenx\Feed\SymfonyViewAdapter' 64 | ``` 65 | 66 | Use the Feed in your controller: 67 | 68 | ```php 69 | use Rumenx\Feed\Feed; 70 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 71 | 72 | class FeedController extends AbstractController 73 | { 74 | public function feed(Feed $feed) 75 | { 76 | $feed->setTitle('My Blog Feed'); 77 | $feed->addItem([ 78 | 'title' => 'First Post', 79 | 'author' => 'Rumen', 80 | 'link' => 'https://example.com/post/1', 81 | 'pubdate' => new \DateTime(), 82 | 'description' => 'This is the first post.' 83 | ]); 84 | return $feed->render('atom'); 85 | } 86 | } 87 | ``` 88 | 89 | ### Plain PHP / Other Frameworks 90 | 91 | You can use the Feed class by providing your own implementations for cache, config, response, and view: 92 | 93 | ```php 94 | use Rumenx\Feed\Feed; 95 | 96 | $feed = new Feed([ 97 | 'cache' => new MyCacheAdapter(), 98 | 'config' => new MyConfigAdapter(), 99 | 'response' => new MyResponseAdapter(), 100 | 'view' => new MyViewAdapter(), 101 | ]); 102 | 103 | $feed->setTitle('My Feed'); 104 | $feed->addItem([ 105 | 'title' => 'Hello', 106 | 'author' => 'Rumen', 107 | 'link' => 'https://example.com/hello', 108 | 'pubdate' => date('c'), 109 | 'description' => 'Hello world!' 110 | ]); 111 | echo $feed->render('rss'); 112 | ``` 113 | 114 | ## Features 115 | 116 | - RSS and Atom support 117 | - Caching 118 | - Custom views 119 | - Framework-agnostic adapters for Laravel and Symfony 120 | 121 | ## Notes 122 | 123 | - All feed metadata is now accessed via public getter/setter methods. Direct property access is not supported. 124 | - You can extend the package by implementing the provided interfaces for cache, config, response, and view. 125 | 126 | ## License 127 | 128 | This package is open-sourced software licensed under the [MIT License](LICENSE.md). 129 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rumenx/php-feed", 3 | "description": "Framework-agnostic PHP Feed generator for Laravel, Symfony, and more.", 4 | "homepage": "https://github.com/RumenDamyanov/php-feed", 5 | "keywords": ["php", "feed", "rss", "atom", "laravel", "symfony", "generator"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Rumen Damyanov", 10 | "email": "contact@rumenx.com", 11 | "role": "Developer", 12 | "homepage": "https://rumenx.com" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/RumenDamyanov/php-feed/issues", 17 | "source": "https://github.com/RumenDamyanov/php-feed" 18 | }, 19 | "require": { 20 | "php": "^8.3 || ^8.4" 21 | }, 22 | "require-dev": { 23 | "laravel/framework": "^11.0", 24 | "symfony/http-foundation": "^7.0", 25 | "phpunit/phpunit": "^11.0", 26 | "pestphp/pest": "^3.8", 27 | "orchestra/testbench": "^9.14", 28 | "symfony/cache": "^7.3", 29 | "symfony/dependency-injection": "^7.3", 30 | "symfony/templating": "^6.4" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Rumenx\\Feed\\": "src/Rumenx/Feed/" 35 | } 36 | }, 37 | "suggest": { 38 | "laravel/framework": "For Laravel integration", 39 | "symfony/http-foundation": "For Symfony integration" 40 | }, 41 | "minimum-stability": "stable", 42 | "prefer-stable": true, 43 | "config": { 44 | "allow-plugins": { 45 | "pestphp/pest-plugin": true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | false, 4 | 'cache_key' => 'php-feed', 5 | 'cache_duration' => 3600, 6 | 'testing' => true, 7 | ]; 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./tests/ 9 | 10 | 11 | 12 | 13 | ./src/Rumenx/Feed/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Feed.php: -------------------------------------------------------------------------------- 1 | cache = $params['cache']; 76 | $this->configRepository = $params['config']; 77 | $this->response = $params['response']; 78 | $this->view = $params['view']; 79 | } 80 | 81 | /** 82 | * Add new items to $items array 83 | * @param array $item 84 | */ 85 | public function addItem(array $item): void 86 | { 87 | // Robust multidimensional check 88 | if (array_is_list($item) && isset($item[0]) && is_array($item[0])) { 89 | foreach ($item as $i) { 90 | $this->addItem($i); 91 | } 92 | return; 93 | } 94 | if ($this->shortening && isset($item['description'])) { 95 | $append = (mb_strlen($item['description']) > $this->shorteningLimit) ? '...' : ''; 96 | $item['description'] = mb_substr($item['description'], 0, $this->shorteningLimit, 'UTF-8') . $append; 97 | } 98 | if (isset($item['title'])) { 99 | $item['title'] = htmlspecialchars(strip_tags($item['title']), ENT_COMPAT, 'UTF-8'); 100 | } 101 | if (isset($item['subtitle'])) { 102 | $item['subtitle'] = htmlspecialchars(strip_tags($item['subtitle']), ENT_COMPAT, 'UTF-8'); 103 | } 104 | // Updated logic: set feed subtitle from item if feed subtitle is unset or empty string 105 | if ((empty($this->subtitle) || $this->subtitle === '') && !empty($item['subtitle'])) { 106 | $this->subtitle = $item['subtitle']; 107 | } 108 | $this->items[] = $item; 109 | } 110 | 111 | /** 112 | * Returns aggregated feed with all items from $items array 113 | * @param string|null $format (options: 'atom', 'rss') 114 | * @param int|null $cache (0 - turns off the cache) 115 | * @param string|null $key 116 | * @return mixed 117 | */ 118 | public function render(?string $format = null, ?int $cache = null, ?string $key = null): mixed 119 | { 120 | $format = $format ?? self::DEFAULT_FORMAT; 121 | if ($cache === 0 || ($cache === null && $this->caching === 0)) { 122 | $this->clearCache(); 123 | } 124 | if ($cache !== null && $cache > 0) { 125 | $this->caching = $cache; 126 | } 127 | if ($key !== null) { 128 | $this->cacheKey = $key; 129 | } 130 | $view = $this->customView ?? 'feed::' . $format; 131 | $ctype = $this->ctype ?? ($format === 'atom' ? self::FORMAT_ATOM : self::FORMAT_RSS); 132 | if ($this->caching > 0 && $this->cache->has($this->cacheKey)) { 133 | return $this->response->make( 134 | $this->cache->get($this->cacheKey), 135 | 200, 136 | ['Content-Type' => $this->cache->get($this->cacheKey . '_ctype') . '; charset=' . $this->charset] 137 | ); 138 | } 139 | $this->lang = $this->lang ?: $this->configRepository->get('application.language'); 140 | $this->link = $this->link ?: $this->configRepository->get('application.url'); 141 | $this->ref = $this->ref ?: self::DEFAULT_REF; 142 | $this->pubdate = $this->pubdate ?: date('D, d M Y H:i:s O'); 143 | $rssLink = $this->domain ? sprintf('%s/%s', rtrim($this->domain, '/'), 'feed') : 'http://localhost/feed'; 144 | $channel = [ 145 | 'title' => htmlspecialchars(strip_tags($this->title), ENT_COMPAT, 'UTF-8'), 146 | 'subtitle' => htmlspecialchars(strip_tags($this->subtitle), ENT_COMPAT, 'UTF-8'), 147 | 'description' => $this->description, 148 | 'logo' => $this->logo, 149 | 'icon' => $this->icon, 150 | 'color' => $this->color, 151 | 'cover' => $this->cover, 152 | 'ga' => $this->ga, 153 | 'related' => $this->related, 154 | 'rssLink' => $rssLink, 155 | 'link' => $this->link, 156 | 'ref' => $this->ref, 157 | 'pubdate' => $this->formatDate($this->pubdate, $format), 158 | 'lang' => $this->lang, 159 | 'copyright' => $this->copyright 160 | ]; 161 | $viewData = [ 162 | 'items' => $this->items, 163 | 'channel' => $channel, 164 | 'namespaces' => $this->namespaces 165 | ]; 166 | if ($this->caching > 0) { 167 | $this->cache->put($this->cacheKey, $this->view->make($view, $viewData)->render(), $this->caching); 168 | $this->cache->put($this->cacheKey . '_ctype', $ctype, $this->caching); 169 | return $this->response->make( 170 | $this->cache->get($this->cacheKey), 171 | 200, 172 | ['Content-Type' => $this->cache->get($this->cacheKey . '_ctype') . '; charset=' . $this->charset] 173 | ); 174 | } 175 | $this->clearCache(); 176 | return $this->response->make( 177 | $this->view->make($view, $viewData), 178 | 200, 179 | ['Content-Type' => $ctype . '; charset=' . $this->charset] 180 | ); 181 | } 182 | 183 | public static function link(string $url, string $type = 'atom', ?string $title = null, ?string $lang = null): string 184 | { 185 | $type = in_array($type, ['rss', 'atom'], true) ? 'application/' . $type . '+xml' : $type; 186 | $titleAttr = $title ? ' title="' . $title . '"' : ''; 187 | $langAttr = $lang ? ' hreflang="' . $lang . '"' : ''; 188 | return ''; 189 | } 190 | 191 | public function isCached(): bool 192 | { 193 | return $this->cache->has($this->cacheKey); 194 | } 195 | 196 | public function clearCache(): void 197 | { 198 | if ($this->isCached()) { 199 | $this->cache->forget($this->cacheKey); 200 | } 201 | } 202 | 203 | public function setCache(int $duration = 60, string $key = self::DEFAULT_CACHE_KEY): void 204 | { 205 | $this->cacheKey = $key; 206 | $this->caching = $duration; 207 | if ($duration < 1) { 208 | $this->clearCache(); 209 | } 210 | } 211 | 212 | public function getCustomView(): ?string 213 | { 214 | return $this->customView; 215 | } 216 | public function setCustomView(?string $view = null): void 217 | { 218 | $this->customView = $view; 219 | } 220 | public function setTextLimit(int $l = self::DEFAULT_SHORTENING_LIMIT): void 221 | { 222 | $this->shorteningLimit = $l; 223 | } 224 | public function getTextLimit(): int 225 | { 226 | return $this->shorteningLimit; 227 | } 228 | public function setShortening(bool $b = false): void 229 | { 230 | $this->shortening = $b; 231 | } 232 | public function getShortening(): bool 233 | { 234 | return $this->shortening; 235 | } 236 | public function formatDate(mixed $date, string $format = 'atom'): string 237 | { 238 | if ($this->dateFormat === 'carbon' && is_object($date) && method_exists($date, 'toDateTimeString')) { 239 | $dateStr = $date->toDateTimeString(); 240 | $date = ($format === 'atom') ? date('c', strtotime($dateStr)) : date('D, d M Y H:i:s O', strtotime($dateStr)); 241 | } elseif ($this->dateFormat === 'timestamp') { 242 | $date = ($format === 'atom') ? date('c', strtotime('@' . $date)) : date('D, d M Y H:i:s O', strtotime('@' . $date)); 243 | } else { 244 | $date = ($format === 'atom') ? date('c', strtotime((string)$date)) : date('D, d M Y H:i:s O', strtotime((string)$date)); 245 | } 246 | return $date; 247 | } 248 | public function setDateFormat(string $format = self::DEFAULT_DATE_FORMAT): void 249 | { 250 | $this->dateFormat = $format; 251 | } 252 | public function getDateFormat(): string 253 | { 254 | return $this->dateFormat; 255 | } 256 | public function getCacheKey(): string 257 | { 258 | return $this->cacheKey; 259 | } 260 | public function getCacheDuration(): int 261 | { 262 | return $this->caching; 263 | } 264 | public function setNamespaces(array $namespaces): void 265 | { 266 | $this->namespaces = $namespaces; 267 | } 268 | public function getNamespaces(): array 269 | { 270 | return $this->namespaces; 271 | } 272 | public function setShorteningLimit(int $limit): void 273 | { 274 | $this->shorteningLimit = $limit; 275 | } 276 | public function getShorteningLimit(): int 277 | { 278 | return $this->shorteningLimit; 279 | } 280 | // --- Metadata Getters/Setters --- 281 | public function getTitle(): string { return $this->title; } 282 | public function setTitle(string $title): void { $this->title = $title; } 283 | public function getSubtitle(): string { return $this->subtitle; } 284 | public function setSubtitle(string $subtitle): void { $this->subtitle = $subtitle; } 285 | public function getDescription(): string { return $this->description; } 286 | public function setDescription(string $description): void { $this->description = $description; } 287 | public function getDomain(): ?string { return $this->domain; } 288 | public function setDomain(?string $domain): void { $this->domain = $domain; } 289 | public function getLink(): ?string { return $this->link; } 290 | public function setLink(?string $link): void { $this->link = $link; } 291 | public function getRef(): ?string { return $this->ref; } 292 | public function setRef(?string $ref): void { $this->ref = $ref; } 293 | public function getLogo(): ?string { return $this->logo; } 294 | public function setLogo(?string $logo): void { $this->logo = $logo; } 295 | public function getIcon(): ?string { return $this->icon; } 296 | public function setIcon(?string $icon): void { $this->icon = $icon; } 297 | public function getCover(): ?string { return $this->cover; } 298 | public function setCover(?string $cover): void { $this->cover = $cover; } 299 | public function getColor(): ?string { return $this->color; } 300 | public function setColor(?string $color): void { $this->color = $color; } 301 | public function getGa(): ?string { return $this->ga; } 302 | public function setGa(?string $ga): void { $this->ga = $ga; } 303 | public function getRelated(): bool { return $this->related; } 304 | public function setRelated(bool $related): void { $this->related = $related; } 305 | public function getCopyright(): ?string { return $this->copyright; } 306 | public function setCopyright(?string $copyright): void { $this->copyright = $copyright; } 307 | public function getPubdate(): ?string { return $this->pubdate; } 308 | public function setPubdate(?string $pubdate): void { $this->pubdate = $pubdate; } 309 | public function getLang(): ?string { return $this->lang; } 310 | public function setLang(?string $lang): void { $this->lang = $lang; } 311 | public function getCharset(): string { return $this->charset; } 312 | public function setCharset(string $charset): void { $this->charset = $charset; } 313 | public function getCtype(): ?string { return $this->ctype; } 314 | public function setCtype(?string $ctype): void { $this->ctype = $ctype; } 315 | public function getDuration(): ?string { return $this->duration; } 316 | public function setDuration(?string $duration): void { $this->duration = $duration; } 317 | } 318 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/FeedCacheInterface.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__ . '/../../../views', 'feed'); 30 | 31 | $this->publishes([ 32 | __DIR__ . '/../../../views' => base_path('resources/views/vendor/feed') 33 | ], 'views'); 34 | 35 | $config_file = __DIR__ . '/../../../config/config.php'; 36 | // Only merge if file exists and returns array 37 | if (file_exists($config_file)) { 38 | $config = require $config_file; 39 | if (is_array($config)) { 40 | $this->mergeConfigFrom($config_file, 'feed'); 41 | } 42 | } 43 | 44 | $this->publishes([ 45 | $config_file => config_path('feed.php') 46 | ], 'config'); 47 | } 48 | 49 | /** 50 | * Register the service provider. 51 | * 52 | * @return void 53 | */ 54 | public function register() 55 | { 56 | $this->app->bind('feed', function (Container $app) { 57 | $params = [ 58 | 'cache' => new LaravelCacheAdapter($app['Illuminate\\Cache\\Repository']), 59 | 'config' => new LaravelConfigAdapter($app['config']), 60 | 'response' => new LaravelResponseAdapter($app[ResponseFactory::class]), 61 | 'view' => new LaravelViewAdapter($app['view']) 62 | ]; 63 | 64 | return new Feed($params); 65 | }); 66 | 67 | $this->app->alias('feed', Feed::class); 68 | } 69 | 70 | /** 71 | * Get the services provided by the provider. 72 | * 73 | * @return array 74 | */ 75 | public function provides() 76 | { 77 | return ['feed', Feed::class]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/FeedViewInterface.php: -------------------------------------------------------------------------------- 1 | cache->has($key); } 10 | public function get(string $key, mixed $default = null): mixed { return $this->cache->get($key, $default); } 11 | public function put(string $key, mixed $value, int $ttl): void { $this->cache->put($key, $value, $ttl); } 12 | public function forget(string $key): void { $this->cache->forget($key); } 13 | } 14 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Laravel/LaravelConfigAdapter.php: -------------------------------------------------------------------------------- 1 | config->get($key, $default); } 10 | } 11 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Laravel/LaravelResponseAdapter.php: -------------------------------------------------------------------------------- 1 | response->make($content, $status, $headers); } 10 | } 11 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Laravel/LaravelViewAdapter.php: -------------------------------------------------------------------------------- 1 | view->make($view, $data); } 29 | } 30 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Symfony/SymfonyCacheAdapterImpl.php: -------------------------------------------------------------------------------- 1 | response = $response; 14 | } 15 | 16 | public function getStatusCode() 17 | { 18 | return $this->response->getStatusCode(); 19 | } 20 | 21 | public function setStatusCode($status) 22 | { 23 | $this->response->setStatusCode($status); 24 | } 25 | 26 | public function getContent() 27 | { 28 | return $this->response->getContent(); 29 | } 30 | 31 | public function setContent($content) 32 | { 33 | $this->response->setContent($content); 34 | } 35 | 36 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { 37 | return new Response($content, $status, $headers); 38 | } 39 | 40 | // ...other methods from SymfonyResponseAdapter.php... 41 | } 42 | -------------------------------------------------------------------------------- /src/Rumenx/Feed/Symfony/SymfonyViewAdapter.php: -------------------------------------------------------------------------------- 1 | engine->render($view, $data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | false, 6 | 'cache_key' => 'php-feed.' . config('app.url'), 7 | 'cache_duration' => 3600, 8 | 'escaping' => true, 9 | 'use_limit_size' => false, 10 | 'max_size' => null, 11 | 'use_styles' => true, 12 | 'styles_location' => null, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/views/atom.blade.php: -------------------------------------------------------------------------------- 1 | {!! '<'.'?'.'xml version="1.0" encoding="UTF-8" ?>' !!} 2 | > 5 | {!! $channel['title'] !!} 6 | 7 | 8 | {{ $channel['link'] }} 9 | 10 | 11 | @if (!empty($channel['logo'])) 12 | {{ $channel['logo'] }} 13 | @endif 14 | @if (!empty($channel['icon'])) 15 | {{ $channel['icon'] }} 16 | @endif 17 | {{ $channel['pubdate'] }} 18 | @foreach($items as $item) 19 | 20 | 21 | {{ $item['author'] }} 22 | 23 | <![CDATA[{!! $item['title'] !!}]]> 24 | 25 | {{ $item['link'] }} 26 | 27 | 28 | {{ $item['pubdate'] }} 29 | 30 | @endforeach 31 | 32 | -------------------------------------------------------------------------------- /src/views/rss.blade.php: -------------------------------------------------------------------------------- 1 | {!! '<'.'?'.'xml version="1.0" encoding="UTF-8" ?>' !!} 2 | > 5 | 6 | {!! $channel['title'] !!} 7 | {{ $channel['rssLink'] }} 8 | 9 | 10 | @if (!empty($channel['copyright'])) 11 | {{ $channel['copyright'] }} 12 | @endif 13 | @if (!empty($channel['color'])) 14 | {{ $channel['color'] }} 15 | @endif 16 | @if (!empty($channel['cover'])) 17 | 18 | @endif 19 | @if (!empty($channel['icon'])) 20 | {{ $channel['icon'] }} 21 | @endif 22 | @if (!empty($channel['logo'])) 23 | {{ $channel['logo'] }} 24 | 25 | {{ $channel['logo'] }} 26 | {{ $channel['title'] }} 27 | {{ $channel['rssLink'] }} 28 | 29 | @endif 30 | @if (!empty($channel['related'])) 31 | 32 | @endif 33 | @if (!empty($channel['ga'])) 34 | 35 | @endif 36 | {{ $channel['lang'] }} 37 | {{ $channel['pubdate'] }} 38 | @foreach($items as $item) 39 | 40 | <![CDATA[{!! $item['title'] !!}]]> 41 | @if (!empty($item['category'])) 42 | {{ $item['category'] }} 43 | @endif 44 | {{ $item['link'] }} 45 | {{ $item['link'] }} 46 | 47 | @if (!empty($item['content'])) 48 | 49 | @endif 50 | {!! $item['author'] !!} 51 | {{ $item['pubdate'] }} 52 | @if (!empty($item['enclosure'])) 53 | $v) 55 | {!! $k.'="'.$v.'" ' !!} 56 | @endforeach 57 | /> 58 | @endif 59 | @if (!empty($item['media:content'])) 60 | $v) 62 | {!! $k.'="'.$v.'" ' !!} 63 | @endforeach 64 | /> 65 | @endif 66 | @if (!empty($item['media:thumbnail'])) 67 | $v) 69 | {!! $k.'="'.$v.'" ' !!} 70 | @endforeach 71 | /> 72 | @endif 73 | @if (!empty($item['media:title'])) 74 | {{ $item['media:title'] }} 75 | @endif 76 | @if (!empty($item['media:description'])) 77 | {{ $item['media:description'] }} 78 | @endif 79 | @if (!empty($item['media:keywords'])) 80 | {{ $item['media:title'] }} 81 | @endif 82 | @if (!empty($item['media:rating'])) 83 | {{ $item['media:rating'] }} 84 | @endif 85 | @if (!empty($item['creativeCommons:license'])) 86 | {{ $item['creativeCommons:license'] }} 87 | @endif 88 | 89 | @endforeach 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/Feature/FeedAddItemMultiBranchTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 9 | public function has(string $key): bool { return false; } 10 | public function get(string $key, mixed $default = null): mixed { return null; } 11 | public function put(string $key, mixed $value, int $ttl): void {} 12 | public function forget(string $key): void {} 13 | }, 14 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 15 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 16 | }, 17 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 18 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 19 | }, 20 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 21 | public function make(string $view, array $data = []): mixed { return ''; } 22 | }, 23 | ]); 24 | $feed->addItem([ 25 | ['title' => 'A'], 26 | ['title' => 'B'], 27 | ]); 28 | $items = (new \ReflectionClass($feed))->getProperty('items'); 29 | $items->setAccessible(true); 30 | $itemsArr = $items->getValue($feed); 31 | expect($itemsArr)->toHaveCount(2); 32 | expect($itemsArr[0]['title'])->toBe('A'); 33 | expect($itemsArr[1]['title'])->toBe('B'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/Feature/FeedAddItemMultiTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $feed->addItem([ 23 | [ 24 | 'title' => 'A', 25 | 'author' => 'Tester', 26 | 'link' => 'https://example.com/a', 27 | 'pubdate' => date('c'), 28 | 'description' => 'desc', 29 | ], 30 | [ 31 | 'title' => 'B', 32 | 'author' => 'Tester', 33 | 'link' => 'https://example.com/b', 34 | 'pubdate' => date('c'), 35 | 'description' => 'desc', 36 | ] 37 | ]); 38 | $items = (new \ReflectionClass($feed))->getProperty('items'); 39 | $items->setAccessible(true); 40 | $itemsArr = $items->getValue($feed); 41 | expect($itemsArr)->toHaveCount(2); 42 | expect($itemsArr[0]['title'])->toBe('A'); 43 | expect($itemsArr[1]['title'])->toBe('B'); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/Feature/FeedCacheKeyTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $feed->setCache(42, 'my-key'); 23 | expect($feed->getCacheKey())->toBe('my-key'); 24 | expect($feed->getCacheDuration())->toBe(42); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/FeedCacheTest.php: -------------------------------------------------------------------------------- 1 | store[$key]); } 8 | public function get(string $key, $default = null): mixed { return $this->store[$key] ?? $default; } 9 | public function put(string $key, mixed $value, int $ttl): void { $this->store[$key] = $value; } 10 | public function forget(string $key): void { unset($this->store[$key]); } 11 | }; 12 | $feed = new Feed([ 13 | 'cache' => $cache, 14 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 15 | public function get(string $key, $default = null): mixed { return $default; } 16 | }, 17 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 18 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 19 | }, 20 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 21 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 22 | }, 23 | ]); 24 | $feed->setCache(10, 'test-key'); 25 | $feed->addItem([ 26 | 'title' => 'CacheTest', 27 | 'author' => 'Tester', 28 | 'link' => 'https://example.com', 29 | 'pubdate' => date('c'), 30 | 'description' => 'desc', 31 | ]); 32 | expect($feed->getCacheKey())->toBe('test-key'); 33 | expect($feed->getCacheDuration())->toBe(10); 34 | $feed->clearCache(); 35 | expect($feed->isCached())->toBeFalse(); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/Feature/FeedClearCacheTest.php: -------------------------------------------------------------------------------- 1 | store[$key]); } 8 | public function get(string $key, $default = null): mixed { return $this->store[$key] ?? $default; } 9 | public function put(string $key, mixed $value, int $ttl): void { $this->store[$key] = $value; } 10 | public function forget(string $key): void { unset($this->store[$key]); } 11 | }; 12 | $feed = new Feed([ 13 | 'cache' => $cache, 14 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 15 | public function get(string $key, $default = null): mixed { return $default; } 16 | }, 17 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 18 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 19 | }, 20 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 21 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 22 | }, 23 | ]); 24 | $feed->setCache(10, 'clear-cache-key'); 25 | $cache->put('clear-cache-key', 'value', 10); 26 | $feed->clearCache(); 27 | expect($feed->isCached())->toBeFalse(); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Feature/FeedCustomViewTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $feed->setCustomView('custom.view'); 23 | expect($feed->getCustomView())->toBe('custom.view'); 24 | $feed->setCustomView(null); 25 | expect($feed->getCustomView())->toBeNull(); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Feature/FeedDateFormatTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | expect($feed->getDateFormat())->toBe('datetime'); 23 | $feed->setDateFormat('carbon'); 24 | expect($feed->getDateFormat())->toBe('carbon'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/FeedFormatDateBranchTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 10 | public function has(string $key): bool { return false; } 11 | public function get(string $key, mixed $default = null): mixed { return null; } 12 | public function put(string $key, mixed $value, int $ttl): void {} 13 | public function forget(string $key): void {} 14 | }, 15 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 16 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 17 | }, 18 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 19 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 20 | }, 21 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 22 | public function make(string $view, array $data = []): mixed { return ''; } 23 | }, 24 | ]); 25 | $feed->setDateFormat('carbon'); 26 | $date = Carbon::create(2024, 1, 2, 3, 4, 5); 27 | expect($feed->formatDate($date, 'atom'))->toBe('2024-01-02T03:04:05+00:00'); 28 | expect($feed->formatDate($date, 'rss'))->toBe('Tue, 02 Jan 2024 03:04:05 +0000'); 29 | }); 30 | 31 | it('formats timestamp', function () { 32 | $feed = new Feed([ 33 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 34 | public function has(string $key): bool { return false; } 35 | public function get(string $key, mixed $default = null): mixed { return null; } 36 | public function put(string $key, mixed $value, int $ttl): void {} 37 | public function forget(string $key): void {} 38 | }, 39 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 40 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 41 | }, 42 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 43 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 44 | }, 45 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 46 | public function make(string $view, array $data = []): mixed { return ''; } 47 | }, 48 | ]); 49 | $feed->setDateFormat('timestamp'); 50 | $timestamp = strtotime('2024-01-02 03:04:05'); 51 | expect($feed->formatDate($timestamp, 'atom'))->toBe('2024-01-02T03:04:05+00:00'); 52 | expect($feed->formatDate($timestamp, 'rss'))->toBe('Tue, 02 Jan 2024 03:04:05 +0000'); 53 | }); 54 | 55 | it('formats datetime string', function () { 56 | $feed = new Feed([ 57 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 58 | public function has(string $key): bool { return false; } 59 | public function get(string $key, mixed $default = null): mixed { return null; } 60 | public function put(string $key, mixed $value, int $ttl): void {} 61 | public function forget(string $key): void {} 62 | }, 63 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 64 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 65 | }, 66 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 67 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 68 | }, 69 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 70 | public function make(string $view, array $data = []): mixed { return ''; } 71 | }, 72 | ]); 73 | $feed->setDateFormat('datetime'); 74 | $date = '2024-01-02 03:04:05'; 75 | expect($feed->formatDate($date, 'atom'))->toBe('2024-01-02T03:04:05+00:00'); 76 | expect($feed->formatDate($date, 'rss'))->toBe('Tue, 02 Jan 2024 03:04:05 +0000'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/Feature/FeedFormatTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $date = '2025-06-07 12:00:00'; 23 | $feed->setDateFormat('datetime'); 24 | expect($feed->formatDate($date, 'atom'))->toBe(date('c', strtotime($date))); 25 | expect($feed->formatDate($date, 'rss'))->toBe(date('D, d M Y H:i:s O', strtotime($date))); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Feature/FeedGetShorteningTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | expect($feed->getShortening())->toBeFalse(); 23 | $feed->setShortening(true); 24 | expect($feed->getShortening())->toBeTrue(); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/FeedIsCachedTest.php: -------------------------------------------------------------------------------- 1 | store[$key]); } 8 | public function get(string $key, $default = null): mixed { return $this->store[$key] ?? $default; } 9 | public function put(string $key, mixed $value, int $ttl): void { $this->store[$key] = $value; } 10 | public function forget(string $key): void { unset($this->store[$key]); } 11 | }; 12 | $feed = new Feed([ 13 | 'cache' => $cache, 14 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 15 | public function get(string $key, $default = null): mixed { return $default; } 16 | }, 17 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 18 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 19 | }, 20 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 21 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 22 | }, 23 | ]); 24 | $feed->setCache(10, 'is-cached-key'); 25 | $cache->put('is-cached-key', 'value', 10); 26 | expect($feed->isCached())->toBeTrue(); 27 | $feed->clearCache(); 28 | expect($feed->isCached())->toBeFalse(); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/Feature/FeedLinkTest.php: -------------------------------------------------------------------------------- 1 | toBe(''); 7 | expect(Feed::link($url, 'rss'))->toBe(''); 8 | expect(Feed::link($url, 'text/xml'))->toBe(''); 9 | expect(Feed::link($url, 'rss', 'Feed: RSS'))->toBe(''); 10 | expect(Feed::link($url, 'atom', 'Feed: Atom', 'en'))->toBe(''); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/Feature/FeedNamespacesTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | expect($feed->getNamespaces())->toBeArray(); 23 | $namespaces = $feed->getNamespaces(); 24 | $namespaces[] = 'testNamespace'; 25 | $feed->setNamespaces($namespaces); 26 | expect($feed->getNamespaces())->toContain('testNamespace'); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Feature/FeedRenderBranchesTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 9 | public function has(string $key): bool { return $key === 'cache-key'; } 10 | public function get(string $key, mixed $default = null): mixed { 11 | if ($key === 'cache-key') return 'CACHED'; 12 | if ($key === 'cache-key_ctype') return 'application/rss+xml'; 13 | return null; 14 | } 15 | public function put(string $key, mixed $value, int $ttl): void {} 16 | public function forget(string $key): void {} 17 | }, 18 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 19 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 20 | }, 21 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 22 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return ['content'=>$content,'headers'=>$headers,'status'=>$status]; } 23 | }, 24 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 25 | public function make(string $view, array $data = []): mixed { return $view; } 26 | }, 27 | ]); 28 | $feed->setCache(10, 'cache-key'); 29 | $result = $feed->render(); 30 | expect($result['content'])->toBe('CACHED'); 31 | expect($result['headers']['Content-Type'])->toBe('application/rss+xml; charset=utf-8'); 32 | expect($result['status'])->toBe(200); 33 | }); 34 | 35 | it('uses custom view and ctype', function () { 36 | $feed = new Feed([ 37 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 38 | public function has(string $key): bool { return false; } 39 | public function get(string $key, mixed $default = null): mixed { return null; } 40 | public function put(string $key, mixed $value, int $ttl): void {} 41 | public function forget(string $key): void {} 42 | }, 43 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 44 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 45 | }, 46 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 47 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return ['content'=>$content,'headers'=>$headers,'status'=>$status]; } 48 | }, 49 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 50 | public function make(string $view, array $data = []): mixed { return $view; } 51 | }, 52 | ]); 53 | $feed->setCustomView('custom-view'); 54 | $feed->setCtype('text/xml'); 55 | $result = $feed->render(); 56 | expect($result['content'])->toBe('custom-view'); 57 | expect($result['headers']['Content-Type'])->toBe('text/xml; charset=utf-8'); 58 | expect($result['status'])->toBe(200); 59 | }); 60 | it('sets caching and cacheKey when passed as render arguments', function () { 61 | $feed = new Feed([ 62 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 63 | public function has(string $key): bool { return false; } 64 | public function get(string $key, mixed $default = null): mixed { return null; } 65 | public function put(string $key, mixed $value, int $ttl): void {} 66 | public function forget(string $key): void {} 67 | }, 68 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 69 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 70 | }, 71 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 72 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return ['content'=>$content,'headers'=>$headers,'status'=>$status]; } 73 | }, 74 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 75 | public function make(string $view, array $data = []): mixed { 76 | return new class { 77 | public function render() { return 'RENDERED'; } 78 | }; 79 | } 80 | }, 81 | ]); 82 | // Pass cache and key as arguments 83 | $feed->render('atom', 42, 'branch-key'); 84 | // Use reflection to check private properties 85 | $ref = new \ReflectionClass($feed); 86 | $caching = $ref->getProperty('caching'); 87 | $caching->setAccessible(true); 88 | $cacheKey = $ref->getProperty('cacheKey'); 89 | $cacheKey->setAccessible(true); 90 | expect($caching->getValue($feed))->toBe(42); 91 | expect($cacheKey->getValue($feed))->toBe('branch-key'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/Feature/FeedRenderCachePutTest.php: -------------------------------------------------------------------------------- 1 | calls = &$calls; } 10 | public function has(string $key): bool { $this->calls[] = "has:$key"; return false; } 11 | public function get(string $key, mixed $default = null): mixed { $this->calls[] = "get:$key"; return $this->store[$key] ?? ($key === 'key_ctype' ? 'application/rss+xml' : null); } 12 | public function put(string $key, mixed $value, int $ttl): void { $this->calls[] = "put:$key"; $this->store[$key] = $value; } 13 | public function forget(string $key): void { $this->calls[] = "forget:$key"; unset($this->store[$key]); } 14 | public function getCalls() { return $this->calls; } 15 | }; 16 | $view = new class implements \Rumenx\Feed\FeedViewInterface { 17 | public function make(string $view, array $data = []): mixed { 18 | return new class { 19 | public function render() { return 'RENDERED'; } 20 | }; 21 | } 22 | }; 23 | $response = new class implements \Rumenx\Feed\FeedResponseInterface { 24 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return [$content, $status, $headers]; } 25 | }; 26 | $feed = new Feed([ 27 | 'cache' => $cache, 28 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 29 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 30 | }, 31 | 'response' => $response, 32 | 'view' => $view, 33 | ]); 34 | $feed->setCache(10, 'key'); 35 | $result = $feed->render('rss'); 36 | expect($result[0])->toBe('RENDERED'); 37 | expect($result[1])->toBe(200); 38 | expect($result[2]['Content-Type'])->toBe('application/rss+xml; charset=utf-8'); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/Feature/FeedRenderTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return ['content' => $content, 'status' => $status, 'headers' => $headers]; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $feed->setTitle('RenderTest'); 23 | $feed->addItem([ 24 | 'title' => 'RenderItem', 25 | 'author' => 'Tester', 26 | 'link' => 'https://example.com', 27 | 'pubdate' => date('c'), 28 | 'description' => 'desc', 29 | ]); 30 | $result = $feed->render('rss'); 31 | expect($result['content'])->toContain('view:feed::rss'); 32 | expect($result['status'])->toBe(200); 33 | expect($result['headers'])->toBeArray(); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/Feature/FeedServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | app['config']->get('feed'), true) . "\n"); 12 | $this->app->register(FeedServiceProvider::class); 13 | fwrite(STDERR, '\nConfig after: ' . var_export($this->app['config']->get('feed'), true) . "\n"); 14 | expect($this->app->bound('feed'))->toBeTrue(); 15 | expect($this->app->make('feed'))->toBeInstanceOf(Feed::class); 16 | expect($this->app->make(Feed::class))->toBeInstanceOf(Feed::class); 17 | $provider = $this->app->getProvider(FeedServiceProvider::class); 18 | expect($provider->provides())->toContain('feed'); 19 | expect($provider->provides())->toContain(Feed::class); 20 | }); 21 | 22 | function getPackageProviders($app) 23 | { 24 | return [FeedServiceProvider::class]; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Feature/FeedSetCacheEdgeTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 9 | private $cleared = false; 10 | public function has(string $key): bool { return true; } 11 | public function get(string $key, mixed $default = null): mixed { return null; } 12 | public function put(string $key, mixed $value, int $ttl): void {} 13 | public function forget(string $key): void { $this->cleared = true; } 14 | public function wasCleared(): bool { return $this->cleared; } 15 | }, 16 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 17 | public function get(string $key, mixed $default = null): mixed { return 'en'; } 18 | }, 19 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 20 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 21 | }, 22 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 23 | public function make(string $view, array $data = []): mixed { return ''; } 24 | }, 25 | ]); 26 | $feed->setCache(0, 'test-key'); 27 | expect($feed->getCacheKey())->toBe('test-key'); 28 | expect($feed->getCacheDuration())->toBe(0); 29 | // The cache->forget should have been called 30 | $cacheProp = (new \ReflectionClass($feed))->getProperty('cache'); 31 | $cacheProp->setAccessible(true); 32 | $cacheObj = $cacheProp->getValue($feed); 33 | expect($cacheObj->wasCleared())->toBeTrue(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/Feature/FeedSettersTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 16 | public function has(string $key): bool { return false; } 17 | public function get(string $key, $default = null): mixed { return $default; } 18 | public function put(string $key, mixed $value, int $ttl): void {} 19 | public function forget(string $key): void {} 20 | }, 21 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 22 | public function get(string $key, $default = null): mixed { return $default; } 23 | }, 24 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 25 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 26 | }, 27 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 28 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 29 | }, 30 | ]); 31 | $feed->setTitle('Title'); 32 | $feed->setSubtitle('Subtitle'); 33 | $feed->setDescription('Description'); 34 | $feed->setDomain('domain'); 35 | $feed->setLink('link'); 36 | $feed->setRef('ref'); 37 | $feed->setLogo('logo'); 38 | $feed->setIcon('icon'); 39 | $feed->setCover('cover'); 40 | $feed->setColor('color'); 41 | $feed->setGa('ga'); 42 | $feed->setRelated(true); 43 | $feed->setCopyright('copyright'); 44 | $feed->setPubdate('pubdate'); 45 | $feed->setLang('lang'); 46 | $feed->setCharset('charset'); 47 | $feed->setCtype('ctype'); 48 | $feed->setDuration('duration'); 49 | expect($feed->getTitle())->toBe('Title'); 50 | expect($feed->getSubtitle())->toBe('Subtitle'); 51 | expect($feed->getDescription())->toBe('Description'); 52 | expect($feed->getDomain())->toBe('domain'); 53 | expect($feed->getLink())->toBe('link'); 54 | expect($feed->getRef())->toBe('ref'); 55 | expect($feed->getLogo())->toBe('logo'); 56 | expect($feed->getIcon())->toBe('icon'); 57 | expect($feed->getCover())->toBe('cover'); 58 | expect($feed->getColor())->toBe('color'); 59 | expect($feed->getGa())->toBe('ga'); 60 | expect($feed->getRelated())->toBeTrue(); 61 | expect($feed->getCopyright())->toBe('copyright'); 62 | expect($feed->getPubdate())->toBe('pubdate'); 63 | expect($feed->getLang())->toBe('lang'); 64 | expect($feed->getCharset())->toBe('charset'); 65 | expect($feed->getCtype())->toBe('ctype'); 66 | expect($feed->getDuration())->toBe('duration'); 67 | }); 68 | 69 | // Uncovered: test default values for all getters 70 | $feed2 = new Feed([ 71 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 72 | public function has(string $key): bool { return false; } 73 | public function get(string $key, $default = null): mixed { return $default; } 74 | public function put(string $key, mixed $value, int $ttl): void {} 75 | public function forget(string $key): void {} 76 | }, 77 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 78 | public function get(string $key, $default = null): mixed { return $default; } 79 | }, 80 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 81 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 82 | }, 83 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 84 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 85 | }, 86 | ]); 87 | // Only test properties that are not set by default in Feed's constructor 88 | expect($feed2->getDomain())->toBeNull(); 89 | expect($feed2->getLink())->toBeNull(); 90 | expect($feed2->getRef())->toBeNull(); 91 | expect($feed2->getLogo())->toBeNull(); 92 | expect($feed2->getIcon())->toBeNull(); 93 | expect($feed2->getCover())->toBeNull(); 94 | expect($feed2->getColor())->toBeNull(); 95 | expect($feed2->getGa())->toBeNull(); 96 | expect($feed2->getRelated())->toBeFalse(); 97 | expect($feed2->getCopyright())->toBeNull(); 98 | expect($feed2->getPubdate())->toBeNull(); 99 | expect($feed2->getLang())->toBeNull(); 100 | expect($feed2->getCtype())->toBeNull(); 101 | expect($feed2->getDuration())->toBeNull(); 102 | 103 | test('feed addItem processes subtitle if feed subtitle is empty string (branch coverage)', function () { 104 | $feed = new Feed([ 105 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 106 | public function has(string $key): bool { return false; } 107 | public function get(string $key, $default = null): mixed { return $default; } 108 | public function put(string $key, mixed $value, int $ttl): void {} 109 | public function forget(string $key): void {} 110 | }, 111 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 112 | public function get(string $key, $default = null): mixed { return $default; } 113 | }, 114 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 115 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 116 | }, 117 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 118 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 119 | }, 120 | ]); 121 | // Set subtitle to empty string 122 | $feed->setSubtitle(''); 123 | $feed->addItem([ 124 | 'title' => 'Test', 125 | 'subtitle' => 'Processed Subtitle', 126 | 'link' => 'https://example.com', 127 | 'pubdate' => date('c'), 128 | 'description' => 'desc', 129 | ]); 130 | expect($feed->getSubtitle())->toBe('Processed Subtitle'); 131 | }); 132 | 133 | test('Feed constructor throws if required dependency is missing', function () { 134 | foreach (["cache", "config", "response", "view"] as $dep) { 135 | $params = [ 136 | 'cache' => new class implements \Rumenx\Feed\FeedCacheInterface { 137 | public function has(string $key): bool { return false; } 138 | public function get(string $key, $default = null): mixed { return $default; } 139 | public function put(string $key, mixed $value, int $ttl): void {} 140 | public function forget(string $key): void {} 141 | }, 142 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 143 | public function get(string $key, $default = null): mixed { return $default; } 144 | }, 145 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 146 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 147 | }, 148 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 149 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 150 | }, 151 | ]; 152 | unset($params[$dep]); 153 | expect(fn() => new \Rumenx\Feed\Feed($params)) 154 | ->toThrow(\InvalidArgumentException::class, "Missing required dependency: $dep"); 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /tests/Feature/FeedShorteningLimitTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | expect($feed->getTextLimit())->toBe(150); 23 | $feed->setTextLimit(10); 24 | expect($feed->getTextLimit())->toBe(10); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/FeedShorteningTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | public function has(string $key): bool { return false; } 8 | public function get(string $key, $default = null): mixed { return $default; } 9 | public function put(string $key, mixed $value, int $ttl): void {} 10 | public function forget(string $key): void {} 11 | }, 12 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 13 | public function get(string $key, $default = null): mixed { return $default; } 14 | }, 15 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 16 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 17 | }, 18 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 19 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 20 | }, 21 | ]); 22 | $feed->setShortening(true); 23 | $feed->setTextLimit(5); 24 | $feed->addItem([ 25 | 'title' => 'ShortTest', 26 | 'author' => 'Tester', 27 | 'link' => 'https://example.com', 28 | 'pubdate' => date('c'), 29 | 'description' => '1234567890', 30 | ]); 31 | $items = (new \ReflectionClass($feed))->getProperty('items'); 32 | $items->setAccessible(true); 33 | $itemsArr = $items->getValue($feed); 34 | expect($itemsArr[0]['description'])->toBe('12345...'); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/Feature/FeedTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 7 | private array $store = []; 8 | public function has(string $key): bool { return isset($this->store[$key]); } 9 | public function get(string $key, $default = null): mixed { return $this->store[$key] ?? $default; } 10 | public function put(string $key, mixed $value, int $ttl): void { $this->store[$key] = $value; } 11 | public function forget(string $key): void { unset($this->store[$key]); } 12 | }, 13 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 14 | public function get(string $key, $default = null): mixed { return $default; } 15 | }, 16 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 17 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 18 | }, 19 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 20 | public function make(string $view, array $data = []): mixed { return 'view:'.$view; } 21 | }, 22 | ]); 23 | $feed->addItem([ 24 | 'title' => 'Test', 25 | 'author' => 'Tester', 26 | 'link' => 'https://example.com', 27 | 'pubdate' => date('c'), 28 | 'description' => 'desc', 29 | ]); 30 | $items = (new \ReflectionClass($feed))->getProperty('items'); 31 | $items->setAccessible(true); 32 | $itemsArr = $items->getValue($feed); 33 | expect($itemsArr)->toHaveCount(1); 34 | expect($itemsArr[0]['title'])->toBe('Test'); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/Feature/FeedUncoveredMethodsTest.php: -------------------------------------------------------------------------------- 1 | new class implements \Rumenx\Feed\FeedCacheInterface { 14 | public function has(string $key): bool { return false; } 15 | public function get(string $key, mixed $default = null): mixed { return $default; } 16 | public function put(string $key, mixed $value, int $ttl): void {} 17 | public function forget(string $key): void {} 18 | }, 19 | 'config' => new class implements \Rumenx\Feed\FeedConfigInterface { 20 | public function get(string $key, mixed $default = null): mixed { return $default; } 21 | }, 22 | 'response' => new class implements \Rumenx\Feed\FeedResponseInterface { 23 | public function make(mixed $content, int $status = 200, array $headers = []): mixed { return $content; } 24 | }, 25 | 'view' => new class implements \Rumenx\Feed\FeedViewInterface { 26 | public function make(string $view, array $data = []): mixed { return ''; } 27 | }, 28 | ]; 29 | 30 | it('can set and get custom view', function () use ($config) { 31 | $feed = new Feed($config); 32 | $feed->setCustomView('my-view'); 33 | expect($feed->getCustomView())->toBe('my-view'); 34 | }); 35 | 36 | it('can set and get namespaces', function () use ($config) { 37 | $feed = new Feed($config); 38 | $feed->setNamespaces(['foo' => 'bar']); 39 | expect($feed->getNamespaces())->toBe(['foo' => 'bar']); 40 | }); 41 | 42 | it('can set and get date format', function () use ($config) { 43 | $feed = new Feed($config); 44 | $feed->setDateFormat('Y-m-d'); 45 | expect($feed->getDateFormat())->toBe('Y-m-d'); 46 | }); 47 | 48 | it('can set and get shortening limit', function () use ($config) { 49 | $feed = new Feed($config); 50 | $feed->setShorteningLimit(42); 51 | expect($feed->getShorteningLimit())->toBe(42); 52 | }); 53 | 54 | it('can set and get cache key and duration', function () use ($config) { 55 | $feed = new Feed($config); 56 | $feed->setCache(123, 'my-key'); 57 | expect($feed->getCacheKey())->toBe('my-key'); 58 | expect($feed->getCacheDuration())->toBe(123); 59 | }); 60 | 61 | it('setTextLimit and getTextLimit work', function () use ($config) { 62 | $feed = new Feed($config); 63 | $feed->setTextLimit(77); 64 | expect($feed->getTextLimit())->toBe(77); 65 | }); 66 | 67 | it('setShortening and getShortening work', function () use ($config) { 68 | $feed = new Feed($config); 69 | $feed->setShortening(true); 70 | expect($feed->getShortening())->toBeTrue(); 71 | $feed->setShortening(false); 72 | expect($feed->getShortening())->toBeFalse(); 73 | }); 74 | 75 | it('formatDate covers all branches', function () use ($config) { 76 | $feed = new Feed($config); 77 | // Default: datetime 78 | expect($feed->formatDate('2020-01-01 12:00:00', 'atom'))->toBe(date('c', strtotime('2020-01-01 12:00:00'))); 79 | expect($feed->formatDate('2020-01-01 12:00:00', 'rss'))->toBe(date('D, d M Y H:i:s O', strtotime('2020-01-01 12:00:00'))); 80 | // Timestamp 81 | $feed->setDateFormat('timestamp'); 82 | $ts = strtotime('2020-01-01 12:00:00'); 83 | expect($feed->formatDate($ts, 'atom'))->toBe(date('c', strtotime('@'.$ts))); 84 | expect($feed->formatDate($ts, 'rss'))->toBe(date('D, d M Y H:i:s O', strtotime('@'.$ts))); 85 | // Carbon (simulate with DateTime object with toDateTimeString) 86 | $feed->setDateFormat('carbon'); 87 | $carbon = new class { 88 | public function toDateTimeString() { return '2020-01-01 12:00:00'; } 89 | }; 90 | expect($feed->formatDate($carbon, 'atom'))->toBe(date('c', strtotime('2020-01-01 12:00:00'))); 91 | expect($feed->formatDate($carbon, 'rss'))->toBe(date('D, d M Y H:i:s O', strtotime('2020-01-01 12:00:00'))); 92 | }); 93 | 94 | it('Feed::link covers all branches', function () { 95 | expect(Feed::link('url'))->toBe(''); 96 | expect(Feed::link('url', 'rss'))->toBe(''); 97 | expect(Feed::link('url', 'atom', 'Title'))->toBe(''); 98 | expect(Feed::link('url', 'atom', null, 'en'))->toBe(''); 99 | expect(Feed::link('url', 'custom', 'T', 'fr'))->toBe(''); 100 | }); 101 | 102 | it('clearCache does not error if not cached', function () use ($config) { 103 | $feed = new Feed($config); 104 | $feed->clearCache(); 105 | expect(true)->toBeTrue(); 106 | }); 107 | 108 | // Add more tests for any other uncovered methods as needed 109 | }); 110 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend('toBeOne', function () { 15 | // return $this->toBe(1); 16 | // }); 17 | -------------------------------------------------------------------------------- /tests/Unit/LaravelCacheAdapterTest.php: -------------------------------------------------------------------------------- 1 | has($key))->toBeFalse(); 21 | $adapter->put($key, 'bar', 10); 22 | expect($adapter->has($key))->toBeTrue(); 23 | expect($adapter->get($key))->toBe('bar'); 24 | $adapter->forget($key); 25 | expect($adapter->has($key))->toBeFalse(); 26 | }); 27 | 28 | it('returns default if key missing', function () { 29 | $repo = new Repository(new ArrayStore()); 30 | $adapter = new LaravelCacheAdapter($repo); 31 | expect($adapter->get('missing', 'default'))->toBe('default'); 32 | }); 33 | 34 | it('put works with 0 and negative TTL', function () { 35 | $repo = new Repository(new ArrayStore()); 36 | $adapter = new LaravelCacheAdapter($repo); 37 | $adapter->put('zero', 'val', 0); 38 | expect($adapter->get('zero'))->toBeNull(); // ArrayStore does not persist with 0 TTL 39 | $adapter->put('neg', 'val2', -10); 40 | expect($adapter->get('neg'))->toBeNull(); // ArrayStore does not persist with negative TTL 41 | }); 42 | 43 | it('forget on non-existent key does not error', function () { 44 | $repo = new Repository(new ArrayStore()); 45 | $adapter = new LaravelCacheAdapter($repo); 46 | $adapter->forget('nope'); 47 | expect(true)->toBeTrue(); // No exception 48 | }); 49 | 50 | it('handles various value types', function () { 51 | $repo = new Repository(new ArrayStore()); 52 | $adapter = new LaravelCacheAdapter($repo); 53 | $adapter->put('int', 123, 10); 54 | $adapter->put('arr', [1,2,3], 10); 55 | $adapter->put('obj', (object)['a'=>1], 10); 56 | expect($adapter->get('int'))->toBe(123); 57 | expect($adapter->get('arr'))->toBe([1,2,3]); 58 | expect($adapter->get('obj'))->toEqual((object)['a'=>1]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/Unit/LaravelConfigAdapterTest.php: -------------------------------------------------------------------------------- 1 | 'bar']); 17 | $adapter = new LaravelConfigAdapter($repo); 18 | expect($adapter->get('foo'))->toBe('bar'); 19 | }); 20 | it('returns default if missing', function () { 21 | $repo = new Repository([]); 22 | $adapter = new LaravelConfigAdapter($repo); 23 | expect($adapter->get('missing', 'default'))->toBe('default'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Unit/LaravelResponseAdapterTest.php: -------------------------------------------------------------------------------- 1 | make('foo', 201, ['X-Test' => 'bar']); 37 | expect($resp)->toBeInstanceOf(\Illuminate\Http\Response::class); 38 | expect($resp->getContent())->toBe('foo'); 39 | expect($resp->getStatusCode())->toBe(201); 40 | expect($resp->headers->get('X-Test'))->toBe('bar'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/Unit/LaravelViewAdapterTest.php: -------------------------------------------------------------------------------- 1 | make('foo', ['bar' => 'baz']); 29 | expect($result)->toBe('foo{"bar":"baz"}[]'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyCacheAdapterTest.php: -------------------------------------------------------------------------------- 1 | 'bar']); 17 | $adapter = new SymfonyResponseAdapter($resp); 18 | expect($adapter->getContent())->toBe('foo'); 19 | expect($adapter->getStatusCode())->toBe(201); 20 | $adapter->setContent('bar'); 21 | expect($adapter->getContent())->toBe('bar'); 22 | $adapter->setStatusCode(404); 23 | expect($adapter->getStatusCode())->toBe(404); 24 | }); 25 | it('make returns a new Response', function () { 26 | $adapter = new SymfonyResponseAdapter(new Response()); 27 | $resp = $adapter->make('baz', 202, ['X-Foo' => 'baz']); 28 | expect($resp)->toBeInstanceOf(Response::class); 29 | expect($resp->getContent())->toBe('baz'); 30 | expect($resp->getStatusCode())->toBe(202); 31 | expect($resp->headers->get('X-Foo'))->toBe('baz'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyViewAdapterTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('symfony/templating is not available or deprecated.'); 20 | } 21 | $engine = new class implements \Symfony\Component\Templating\EngineInterface { 22 | public function render(string|\Symfony\Component\Templating\TemplateReferenceInterface $name, array $parameters = []): string { return $name . json_encode($parameters); } 23 | public function exists(string|\Symfony\Component\Templating\TemplateReferenceInterface $name): bool { return true; } 24 | public function supports(string|\Symfony\Component\Templating\TemplateReferenceInterface $name): bool { return true; } 25 | }; 26 | $adapter = new \Rumenx\Feed\Symfony\SymfonyViewAdapter($engine); 27 | $result = $adapter->make('foo', ['bar' => 'baz']); 28 | expect($result)->toBe('foo{"bar":"baz"}'); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------