{"id":32613860,"url":"https://github.com/rezozero/mixedfeed","last_synced_at":"2025-10-30T15:55:10.742Z","repository":{"id":33972950,"uuid":"37713356","full_name":"rezozero/mixedfeed","owner":"rezozero","description":"A PHP library to rule social-feeds, to entangle them with magic, a PHP library to gather them and bind them in darkness","archived":false,"fork":false,"pushed_at":"2023-09-25T10:49:27.000Z","size":284,"stargazers_count":111,"open_issues_count":1,"forks_count":18,"subscribers_count":15,"default_branch":"master","last_synced_at":"2025-09-25T21:41:53.774Z","etag":null,"topics":["feed-providers","merge","php-library","social-network"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rezozero.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-06-19T09:26:16.000Z","updated_at":"2025-09-21T23:01:57.000Z","dependencies_parsed_at":"2024-06-19T19:42:19.201Z","dependency_job_id":"cce098ff-7f3b-4dfe-86cb-4a0cf5119336","html_url":"https://github.com/rezozero/mixedfeed","commit_stats":{"total_commits":96,"total_committers":6,"mean_commits":16.0,"dds":"0.14583333333333337","last_synced_commit":"80c2d4b2263f7479433c48096d720a5dec2f8675"},"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/rezozero/mixedfeed","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Fmixedfeed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Fmixedfeed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Fmixedfeed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Fmixedfeed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rezozero","download_url":"https://codeload.github.com/rezozero/mixedfeed/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Fmixedfeed/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281835582,"owners_count":26569857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-30T02:00:06.501Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["feed-providers","merge","php-library","social-network"],"created_at":"2025-10-30T15:55:00.813Z","updated_at":"2025-10-30T15:55:10.737Z","avatar_url":"https://github.com/rezozero.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mixedfeed\n\n\u003e A PHP library to rule social-feeds, to entangle them with magic, a PHP library to gather them and bind them in darkness\n\n[![SensioLabsInsight](https://insight.sensiolabs.com/projects/ed3544de-7d64-4ef9-a551-c61a66fb668d/mini.png)](https://insight.sensiolabs.com/projects/ed3544de-7d64-4ef9-a551-c61a66fb668d)\n![License](http://img.shields.io/:license-mit-blue.svg?style=flat) [![Packagist](https://img.shields.io/packagist/v/rezozero/mixedfeed.svg?style=flat)](https://packagist.org/packages/rezozero/mixedfeed)\n[![Build Status](https://travis-ci.org/rezozero/mixedfeed.svg?branch=master)](https://travis-ci.org/rezozero/mixedfeed)\n\n* [Use standalone Docker server](#use-standalone-docker-server)\n  + [Available environment variables](#available-environment-variables)\n* [Install as library](#install-as-library)\n* [Combine feeds](#combine-feeds)\n* [Use *FeedItem* instead of raw feed](#use-feeditem-instead-of-raw-feed)\n* [Feed providers](#feed-providers)\n* [Modify cache TTL](#modify-cache-ttl)\n* [Create your own feed provider](#create-your-own-feed-provider)\n  + [Create a feed provider from a *Doctrine* repository](#create-a-feed-provider-from-a-doctrine-repository)\n\n## Use standalone Docker server\n\n```\ndocker pull rezozero/mixedfeed\n\ndocker run -p 8080:80 \\\n    -e MF_FACEBOOK_PAGE_ID=\"xxx\" \\\n    -e MF_FACEBOOK_ACCESS_TOKEN=\"xxxx\" \\\n    -e MF_INSTAGRAM_USER_ID=\"xxx\" \\\n    -e MF_INSTAGRAM_ACCESS_TOKEN=\"xxxx\" \\\n    -e MF_CACHE_PROVIDER=\"apcu\" \\\n    -e MF_FEED_LENGTH=\"30\" \\\n    rezozero/mixedfeed\n```\n\nor use `docker-compose`: copy `docker-compose.yml` to `docker-compose.test.yml` and fill your provider credentials in\nit. Then execute `docker-compose -f docker-compose.test.yml up -d --force-recreate`, *Mixedfeed* will be available at\nhttp://localhost:8080\n\n### Available environment variables\n\n| Name              | Default value | Multiple? (comma separated) |\n| ----------------- | ------------- | --------------------------- |\n| MF_CACHE_PROVIDER | array | |\n| MF_FEED_LENGTH | 12 | |\n| MF_FACEBOOK_PAGE_ID | | ✅ |\n| MF_FACEBOOK_ACCESS_TOKEN | | |\n| MF_FACEBOOK_FIELDS | from,link,picture,full_picture,message,story,type,created_time,source,status_type | ✅ |\n| MF_FACEBOOK_ENDPOINT | https://graph.facebook.com/v2.12/ | |\n| MF_GRAPH_INSTAGRAM_USER_ID | | ✅ |\n| MF_GRAPH_INSTAGRAM_ACCESS_TOKEN | | ✅ |\n| MF_GITHUB_RELEASES_REPOSITORY | | ✅ |\n| MF_GITHUB_COMMITS_REPOSITORY | | ✅ |\n| MF_GITHUB_ACCESS_TOKEN | | |\n| MF_MEDIUM_USERNAME | | ✅ |\n| MF_MEDIUM_USER_ID | *Use same order as in `MF_MEDIUM_USERNAME`* | ✅ |\n| MF_PINTEREST_BOARD_ID | | ✅ |\n| MF_PINTEREST_ACCESS_TOKEN | | |\n| MF_INSTAGRAM_OEMBED_ID | | ✅ |\n| MF_TWITTER_SEARCH_QUERY | | |\n| MF_TWITTER_USER_ID | | ✅ |\n| MF_TWITTER_ACCESS_TOKEN | | |\n| MF_TWITTER_ACCESS_TOKEN_SECRET | | |\n| MF_TWITTER_CONSUMER_KEY | | |\n| MF_TWITTER_CONSUMER_SECRET | | |\n| MF_TWITTER_EXTENDED_MODE | 0 | |\n| MF_YOUTUBE_PLAYLIST_ID | | ✅ |\n| MF_YOUTUBE_API_KEY | | |\n\n## Install as library\n\n*mixedfeed* v3+ needs at least PHP **7.2**, check your server configuration.\n\n```shell\ncomposer require rezozero/mixedfeed\n```\n\n```php\nuse RZ\\MixedFeed\\MixedFeed;\nuse RZ\\MixedFeed\\GraphInstagramFeed;\nuse RZ\\MixedFeed\\TwitterFeed;\nuse RZ\\MixedFeed\\TwitterSearchFeed;\nuse RZ\\MixedFeed\\FacebookPageFeed;\nuse RZ\\MixedFeed\\GithubReleasesFeed;\nuse RZ\\MixedFeed\\GithubCommitsFeed;\n\n$feed = new MixedFeed([\n    new GraphInstagramFeed(\n        'instagram_user_id',\n        'instagram_access_token',\n        null ,// you can add a doctrine cache provider\n        [] // And a fields array to retrieve too\n    ),\n    new TwitterFeed(\n        'twitter_user_id',\n        'twitter_consumer_key',\n        'twitter_consumer_secret',\n        'twitter_access_token',\n        'twitter_access_token_secret',\n        null,  // you can add a doctrine cache provider\n        true,  // exclude replies true/false\n        false, // include retweets true/false\n        false  // extended mode true/false\n    ),\n    new TwitterSearchFeed(\n        [\n            '#art', // do not specify a key for string searchs\n            'from' =\u003e 'rezo_zero',\n            'since' =\u003e '2015-11-01',\n            'until' =\u003e '2015-11-30',\n        ],\n        'twitter_consumer_key',\n        'twitter_consumer_secret',\n        'twitter_access_token',\n        'twitter_access_token_secret',\n        null,  // you can add a doctrine cache provider\n        false  // extended mode true/false\n    ),\n    new FacebookPageFeed(\n        'page-id',\n        'app_access_token',\n        null, // you can add a doctrine cache provider\n        [],    // And a fields array to retrieve too\n        null // A specific Graph API Endpoint URL\n    ),\n    new GithubCommitsFeed(\n        'symfony/symfony',\n        'access_token',\n        null // you can add a doctrine cache provider\n    ),\n    new GithubReleasesFeed(\n        'roadiz/roadiz',\n        'access_token',\n        null // you can add a doctrine cache provider\n    ),\n    new \\RZ\\MixedFeed\\YoutubePlaylistItemFeed(\n        'your_playlist_id',\n        'api_key',\n        null // you can add a doctrine cache provider\n    ),\n]);\n\nreturn $feed-\u003egetItems(12);\n// Or use canonical \\RZ\\MixedFeed\\Canonical\\FeedItem objects\n// for a better compatibility and easier templating with multiple\n// social platforms.\nreturn $feed-\u003egetAsyncCanonicalItems(12);\n```\n\n## Combine feeds\n\n*mixedfeed* can combine multiple social feeds so you can loop over them and use some common data fields such as `feedItemPlatform`, `normalizedDate` and `canonicalMessage`. *mixedfeed* will sort all your feed items by *descending* `normalizedDate`, but you can configure it to sort *ascending*:\n\n```php\nnew MixedFeed([…], MixedFeed::ASC);\n```\n\nEach feed provider must inject these three parameters in feed items:\n\n* `feedItemPlatform`: This is your social network name as a *string* i.e. «twitter». It will be important to cache your feed and for your HTML template engine to render properly each feed item.\n\nFor example, if you are using *Twig*, you will be able to include a sub-template for each social-platform.\n\n```twig\n{% for socialItem in mixedFeedItems %}\n{% include ‘social-blocks/‘ ~ socialItem.feedItemPlatform ~ ‘.html.twig’ %}\n{% endfor %}\n```\n\n* `normalizedDate`: This is a critical parameter as it allows *mixedfeed* to sort *reverse chronologically* multiple feeds with heterogeneous structures.\n* `canonicalMessage`: This is a useful field which contains the **text content** for each item over **all** platforms. You can use this to display items texts within a simple loop.\n\n## Use *FeedItem* instead of raw feed\n\nIf you need to serialize your MixedFeed to JSON or XML again, you should not want all the raw data contained in each\nsocial feed item. So you can use the `$feed-\u003egetAsyncCanonicalItems(12);` method instead of `getItems` to get a more concise\nobject with essential data: `RZ\\MixedFeed\\Canonical\\FeedItem`. *FeedItem* will provide these fields:\n\n- id `string`\n- platform `string`\n- author `string`\n- link `string`\n- title `string`\n- message `string`\n- likeCount `int|null`\n- shareCount `int|null`: Share, comments or retweet count depending on platform.\n- images `Image[]`\n    - url `string`\n    - width `integer`\n    - height `integer`\n- dateTime `DateTime`\n- tags `array` (only used with `MediumFeed`)\n- raw `stdClass` to access raw API object if canonical item fields are not enough\n\nWhen FeedItem has images, `FeedItem::$images` will hold an array of `RZ\\MixedFeed\\Canonical\\Image` objects to\nhave better access to its `url`, `width` and `height` if they're available.\n\nEach feed provider must implement how to *hydrate* a `FeedItem` from the raw feed overriding `createFeedItemFromObject()`\nmethod.\n\n## Feed providers\n\n|  Feed provider class  |  Description | `feedItemPlatform` |\n| -------------- | ---------------- | ------------------ |\n| MediumFeed | Call over `https://medium.com/username/latest` endpoint. It only needs a `$username` and an optional `$userId` for better consistency over requests (Medium seems to apply cache on their username requests even after changing a query parameter, i.e. post limit). *Medium* allows maximum 14 posts per requests. | `medium` |\n| InstagramOEmbedFeed | Call over `https://api.instagram.com/oembed/` endpoint. It only needs a `$embedUrls` array | `instagram_oembed` |\n| GraphInstagramFeed | Call over `graph.instagram.com/$userId/media` endpoint with [Basic Display API](https://developers.facebook.com/docs/instagram-basic-display-api). It needs a `$userId` and an `$accessToken`. **Warning**: Access token must be refreshed every 60 days, use `RefreshInstagramAccessToken` | `instagram` |\n| ~~InstagramFeed~~ | *Deprecated*: Call over `/v1/users/$userId/media/recent/` endpoint. It needs a `$userId` and an `$accessToken` | `instagram` |\n| TwitterFeed | Call over `statuses/user_timeline` endpoint. It requires a `$userId`, a `$consumerKey`, a `$consumerSecret`, an `$accessToken` and an `$accessTokenSecret`. Be careful, this [endpoint](https://dev.twitter.com/rest/reference/get/statuses/user_timeline) can **only return up to 3,200 of a user’s most recent Tweets**, your item count could be lesser than expected. In the same way, Twitter removes retweets after retrieving the items count. | `twitter` |\n| TwitterSearchFeed | Call over `search/tweets` endpoint. It requires a `$queryParams` array, a `$consumerKey`, a `$consumerSecret`, an `$accessToken` and an `$accessTokenSecret`. Be careful, Twitter API **won’t retrieve tweets older than 7 days**, your item count could be lesser than expected. `$queryParams` must be a *key-valued* array with *query operators* according to [Twitter API documentation](https://dev.twitter.com/rest/public/search). | `twitter` |\n| FacebookPageFeed | Call over `https://graph.facebook.com/v3.3/$pageId/posts` endpoint by default. Endpoint can be changed using `$apiBaseUrl` parameter. It requires a `$pageId` and an `$accessToken`. This feed provider only works for public Facebook **pages**. To get an access-token visit: https://developers.facebook.com/docs/facebook-login/access-tokens. By default, `picture`, `message`, `story`, `created_time`, `status_type` fields are queried, you can add your own by passing `$field` array as last parameter. You can add `since` and `until` query parameters using `setSince(\\Datetime)` and `setUntil(\\Datetime)` methods. You can overwrite the default | `facebook_page` |\n| PinterestBoardFeed | Call over `/v1/boards/$boardId/pins/` endpoint. It requires a `$boardId` and an `$accessToken`. To get an access-token visit: https://developers.pinterest.com/tools/access_token/ | `pinterest_board` |\n| GithubReleasesFeed | Call over `api.github.com/repos/:user/:repo/releases` endpoint. It requires a `$repository` (*user/repository*) and an `$accessToken`. You can add a last `$page` parameter. To get an access-token visit: https://github.com/settings/tokens | `github_release` |\n| GithubCommitsFeed | Call over `api.github.com/repos/:user/:repo/commits` endpoint. It requires a `$repository` (*user/repository*) and an `$accessToken`. You can add a last `$page` parameter. To get an access-token visit: https://github.com/settings/tokens | `github_commit` |\n| YoutubeMostPopularFeed | Call over `googleapis.com/youtube/v3/videos` endpoint with `mostPopular` chart (It’s more kind of an example feed). It requires a `$apiKey` with a valid *Google Cloud Console* account (with not null quota) and *Youtube Data API* enabled. | `youtube_playlist_items` |\n| YoutubePlaylistItemFeed | Call over `googleapis.com/youtube/v3/playlistItems` endpoint. It requires a `$apiKey` with a valid *Google Cloud Console* account (with not null quota) and *Youtube Data API* enabled. | `youtube_playlist_items` |\n\n## Modify cache TTL\n\nEach feed-provider which inherits from `AbstractFeedProvider` has access to `setTtl()` method in order to modify the default cache time.\nBy default it is set for `7200` seconds, so you can adjust it to invalidate doctrine cache more or less often.\n\n## Create your own feed provider\n\nThere are plenty of APIs on the internet, and this tool won’t be able to handle them all.\nBut this is not a problem, you can easily create your own feed provider in *mixedfeed*. You just have to create a new *class* that\nwill inherit from `RZ\\MixedFeed\\AbstractFeedProvider`. Then you will have to implement some methods from `FeedProviderInterface`:\n\n* `getRequests($count = 5): \\Generator` method which return a *Guzzle* `Request` generator to be transformed to a response. This is\nthe best option as it will enable **async request pooling**.\n* `supportsRequestPool(): bool` method should return if your provider can be pooled to enhance performances. If you are using a third party library to fetch your data (such as some platform SDK), you should set it to `false`.\n* `createFeedItemFromObject($item)` method which transform a raw feed object into a canonical `RZ\\MixedFeed\\Canonical\\FeedItem` and `RZ\\MixedFeed\\Canonical\\Image`\n* `getDateTime` method to look for the critical datetime field in your feed.\n* `getFeed` method to consume your API endpoint with a count limit and take care of caching your responses.\nThis method **must convert your own feed items into `\\stdClass` objects, not arrays.**\n* `getCanonicalMessage` method to look for the important text content in your feed items.\n* `getFeedPlatform` method to get a global text identifier for your feed items.\n* then a *constructor* that will be handy to use directly in the MixedFeed initialization.\n\nFeel free to check our existing Feed providers to see how they work. And we strongly advise you to\nimplement a caching system not to call your API endpoints at each request. By default, we use *Doctrine*’s caching\nsystem which has many storage options.\n\n### Create a feed provider from a *Doctrine* repository\n\nIf you need to merge social network feeds with your own website articles, you can create a custom FeedProvider which wraps your Doctrine objects into `\\stdClass` items. You’ll need to implement your `getFeed` method using an EntityManager:\n\n```php\nprotected $entityManager;\n\npublic function __construct(\\Doctrine\\ORM\\EntityManagerInterface $entityManager)\n{\n    $this-\u003eentityManager = $entityManager;\n}\n\nprotected function getFeed($count = 5)\n{\n    return array_map(\n        function (Article $article) {\n            $object = new \\stdClass();\n            $object-\u003enative = $article;\n            return $object;\n        },\n        $this-\u003eentityManager-\u003egetRepository(Article::class)-\u003efindBy(\n            [],\n            ['datetime' =\u003e 'DESC'],\n            $count\n        )\n    );\n}\n\nprotected function createFeedItemFromObject($item)\n{\n    $feedItem = new RZ\\MixedFeed\\Canonical\\FeedItem();\n    $feedItem-\u003esetDateTime($this-\u003egetDateTime($item));\n    $feedItem-\u003esetMessage($this-\u003egetCanonicalMessage($item));\n    $feedItem-\u003esetPlatform($this-\u003egetFeedPlatform());\n\n    for ($item-\u003eimages as $image) {\n        $feedItemImage = new RZ\\MixedFeed\\Canonical\\Image();\n        $feedItemImage-\u003esetUrl($image-\u003eurl);\n        $feedItem-\u003eaddImage($feedItemImage);\n    }\n\n    return $feedItem;\n}\n```\n\nThen you can define your *date-time* and *canonical message* methods to look into this object:\n\n```php\n/**\n * @inheritDoc\n */\npublic function getDateTime($item)\n{\n    if ($item-\u003enative instanceof Article) {\n        return $item-\u003enative-\u003egetDatetime();\n    }\n\n    return null;\n}\n\n/**\n * @inheritDoc\n */\npublic function getCanonicalMessage(stdClass $item)\n{\n    if ($item-\u003enative instanceof Article) {\n        return $item-\u003enative-\u003egetExcerpt();\n    }\n\n    return null;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezozero%2Fmixedfeed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frezozero%2Fmixedfeed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezozero%2Fmixedfeed/lists"}