├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Client.php └── Utils.php └── tests ├── BlogClientTest.php ├── TestCase.php └── UtilsTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | php: ['8.1'] 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup PHP ${{ matrix.php }} 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | 26 | - name: Install composer dependencies 27 | run: composer install --no-progress --prefer-dist --optimize-autoloader 28 | 29 | - name: Run code sniffer 30 | run: composer cs 31 | 32 | - name: Run tests 33 | run: composer test 34 | 35 | - name: Publish in Code Climate 36 | uses: paambaati/codeclimate-action@v3.0.0 37 | env: 38 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 39 | with: 40 | coverageCommand: composer test:coverage 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.phar 3 | composer.lock 4 | phpunit.xml 5 | vendor 6 | build 7 | .idea 8 | .project 9 | .phpunit.result.cache 10 | .php-cs-fixer.cache 11 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 5 | ->in(__DIR__ . '/tests') 6 | ->name('*.php'); 7 | 8 | $config = new PhpCsFixer\Config(); 9 | 10 | return $config->setRules([ 11 | '@PSR12' => true, 12 | 'trailing_comma_in_multiline' => true, 13 | ])->setFinder($finder); 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at fdkhadra@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | If you have found an issue or would like to request a new feature, simply create a new issue detailing the request. We also welcome pull requests. See below for information on getting started with development and submitting pull requests. 6 | 7 | Please note we have a [code of conduct](https://github.com/arifszn/php-blog-client/blob/main/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 8 | 9 | ## Found an Issue? 10 | 11 | If you find a bug in the source code or a mistake in the documentation, you can help us by submitting an issue to our [GitHub Repository](https://github.com/arifszn/php-blog-client/issues/new). Even better you can submit a Pull Request with a fix. 12 | 13 | ## Submitting a Pull Request 14 | 15 | - If applicable, update the `readme` 16 | - Example for a commit message 17 | 18 | ``` 19 | Fix something 20 | ``` 21 | 22 | ### Developing 23 | 24 | Fork, then clone the repo: 25 | 26 | ```sh 27 | git clone https://github.com/your-username/php-blog-client.git 28 | cd blog.js 29 | ``` 30 | 31 | Install dependencies: 32 | 33 | ```sh 34 | composer install 35 | ``` 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ariful Alam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |

PHP client to get recent blog posts from popular blogging platforms

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

33 | 34 |

35 | Packagist 36 | · 37 | Report Bug 38 | · 39 | Request Feature 40 |

41 |

42 | 43 |
44 | 45 |

Get recent blog posts from popular blogging platforms by just providing your username and showcase them on your portfolio or website.

46 | 47 | > JavaScript version: blog.js 48 | 49 | ## Installation 50 | 51 | Install via composer 52 | 53 | ```sh 54 | composer require arifszn/blog-client 55 | ``` 56 | 57 | ## Usage 58 | 59 | - **`getDevPost()`:** Get 10 recent posts from [dev](https://dev.to). 60 | 61 | ```php 62 | use Arifszn\Blog\Client; 63 | 64 | $client = new Client(); 65 | $result = $client->getDevPost('yourusername'); 66 | ``` 67 | 68 | - **`getMediumPost()`:** Get 10 recent posts from [medium](https://medium.com). 69 | 70 | ```php 71 | use Arifszn\Blog\Client; 72 | 73 | $client = new Client(); 74 | $result = $client->getMediumPost('yourusername'); 75 | ``` 76 | 77 | ## Sample Response 78 | 79 | ```php 80 | array:2 [▼ 81 | 0 => array:6 [▼ 82 | "title" => "Why Enhancing Virtual Reality is Important", 83 | "description" => "Virtual reality is seen as a “fun” technology to some without much...", 84 | "thumbnail" => "https://cdn-images-1.medium.com/max/2600/0*kz30LOdXT8CyOymh", 85 | "link" => "https://medium.com/p/ac19dd21c728", 86 | "categories" => array:5 [▼ 87 | "vr", 88 | "technology", 89 | "virtual-reality", 90 | "engineering", 91 | "artificial-intelligence" 92 | ], 93 | "publishedAt" => "2020-11-08 18:43:34" 94 | ], 95 | 1 => array:6 [▼ 96 | "title" => "How to Get Started With Data Science: a Brief Guide", 97 | "description" => "You’ve heard about data science and machine learning, and you want to get started. Maybe you hear...", 98 | "thumbnail" => "https://cdn-images-1.medium.com/max/2600/0*Ah0vLtsvxqUvRWuS", 99 | "link" => "https://medium.com/p/88ec244f2fee", 100 | "categories" => array:3 [▼ 101 | "beginner-coding", 102 | "data-science-training", 103 | "machine-learning-course" 104 | ], 105 | "publishedAt" => "2020-26-07 22:55:26" 106 | ] 107 | ] 108 | ``` 109 | 110 | ## Contribute 111 | 112 | Please read the [contributing guide](https://github.com/arifszn/php-blog-client/blob/main/CONTRIBUTING.md) to learn how you can help. 113 | 114 | ## Support 115 | 116 |

You can show your support by starring this project.

117 | 118 | Github Star 119 | 120 | 121 | ## License 122 | 123 | [MIT](https://github.com/arifszn/php-blog-client/blob/main/LICENSE) 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arifszn/blog-client", 3 | "description": "PHP client to get recent blog posts from popular blogging platforms", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Ariful Alam", 9 | "email": "arifulalamszn@gmail.com", 10 | "homepage": "https://arifszn.github.io" 11 | } 12 | ], 13 | "homepage": "https://github.com/arifszn/php-blog-client", 14 | "scripts": { 15 | "test": "phpunit", 16 | "test:coverage": "phpunit --coverage-clover clover.xml", 17 | "cs": "./vendor/bin/php-cs-fixer fix -vvv --dry-run", 18 | "cs:fix": "./vendor/bin/php-cs-fixer fix -vvv" 19 | }, 20 | "minimum-stability": "stable", 21 | "require-dev": { 22 | "phpunit/phpunit": "^9", 23 | "friendsofphp/php-cs-fixer": "^3.8" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Arifszn\\Blog\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Arifszn\\Blog\\Tests\\": "tests" 33 | } 34 | }, 35 | "keywords": [ 36 | "medium", 37 | "dev.to", 38 | "dev", 39 | "devto", 40 | "blog", 41 | "medium api", 42 | "dev.to api", 43 | "medium article", 44 | "medium blog", 45 | "dev.to article", 46 | "medium-article", 47 | "dev.to-article", 48 | "dev.to-blog", 49 | "blog article", 50 | "blog post", 51 | "blog client", 52 | "php blog client", 53 | "blog-client", 54 | "blog api", 55 | "blog article api", 56 | "blog post api", 57 | "recent articles", 58 | "article api" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | src/ 17 | 18 | 19 | 20 | 21 | ./tests/ 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | utils = new Utils(); 25 | } 26 | 27 | /** 28 | * Get most recent medium posts. 29 | * 30 | * @param string $user 31 | * @return array 32 | * @throws Exception 33 | */ 34 | public function getMediumPost(string $user) 35 | { 36 | if (empty($user)) { 37 | return []; 38 | } 39 | 40 | $result = []; 41 | $url = 'https://api.rss2json.com/v1/api.json?rss_url=https://medium.com/feed/@' . $user; 42 | $response = $this->utils->request($url); 43 | 44 | if (isset($response->items)) { 45 | foreach ($response->items as $item) { 46 | array_push($result, $this->utils->formatMediumPost($item)); 47 | } 48 | } 49 | 50 | return $result; 51 | } 52 | 53 | /** 54 | * Get most recent dev posts. 55 | * 56 | * @param string $user 57 | * @return array 58 | * @throws Exception 59 | */ 60 | public function getDevPost(string $user) 61 | { 62 | if (empty($user)) { 63 | return []; 64 | } 65 | 66 | $result = []; 67 | $url = 'https://dev.to/api/articles?per_page=10&username=' . $user; 68 | $response = $this->utils->request($url); 69 | 70 | foreach ($response as $item) { 71 | array_push($result, $this->utils->formatDevPost($item)); 72 | } 73 | 74 | return $result; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | getMessage()); 36 | } 37 | } 38 | 39 | 40 | /** 41 | * Format raw medium post. 42 | * 43 | * @param object $post 44 | * @return array 45 | */ 46 | public function formatMediumPost(object $post) 47 | { 48 | return [ 49 | 'title' => trim($post->title), 50 | 'description' => $this->formatDescription($post->content, true), 51 | 'thumbnail' => $post->thumbnail, 52 | 'link' => $post->guid, 53 | 'categories' => $post->categories, 54 | 'publishedAt' => date('Y-m-d H:i:s', strtotime($post->pubDate)), 55 | ]; 56 | } 57 | 58 | /** 59 | * Format raw dev post. 60 | * 61 | * @param object $post 62 | * @return array 63 | */ 64 | public function formatDevPost(object $post) 65 | { 66 | return [ 67 | 'title' => trim($post->title), 68 | 'description' => $this->formatDescription($post->description), 69 | 'thumbnail' => !empty($post->social_image) ? $post->social_image : $post->cover_image, 70 | 'link' => $post->url, 71 | 'categories' => $post->tag_list, 72 | 'publishedAt' => date('Y-m-d H:i:s', strtotime($post->published_at)), 73 | ]; 74 | } 75 | 76 | /** 77 | * Ellipsis long text. 78 | * 79 | * @param string $str 80 | * @param int $length 81 | * @param string $ending 82 | * @return string 83 | */ 84 | private function textEllipsis(string $str, int $length = 100, string $ending = '...') 85 | { 86 | return strlen($str) > $length ? substr($str, 0, $length) . $ending : $str; 87 | } 88 | 89 | /** 90 | * Format description. 91 | * 92 | * @param string $description 93 | * @param bool $isMedium 94 | * @return string 95 | */ 96 | private function formatDescription(string $description, bool $isMedium = false) 97 | { 98 | if ($isMedium) { 99 | $description = preg_replace('/]*>([\s\S]*?)<\/figcaption[^>]*>/', '', $description); 100 | $description = $this->textEllipsis(strip_tags($description)); 101 | } 102 | 103 | return trim(str_replace(array("\r", "\n"), '', $description)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/BlogClientTest.php: -------------------------------------------------------------------------------- 1 | client = new Client(); 25 | } 26 | 27 | /** 28 | * Test Constructor. 29 | * 30 | * @return void 31 | * @throws InvalidArgumentException 32 | * @throws Exception 33 | * @throws ExpectationFailedException 34 | */ 35 | public function testConstructor() 36 | { 37 | $this->assertInstanceOf(Client::class, $this->client); 38 | } 39 | 40 | /** 41 | * Test `getDevPost` method with valid user. 42 | * 43 | * @return void 44 | * @throws InvalidArgumentException 45 | * @throws ExpectationFailedException 46 | * @throws Exception 47 | */ 48 | public function testGetDevPostWithValidUser() 49 | { 50 | $result = $this->client->getDevPost('arifszn'); 51 | 52 | $this->assertIsArray($result); 53 | 54 | foreach ($result as $value) { 55 | $this->assertArrayHasKey('title', $value); 56 | $this->assertIsString($value['title']); 57 | 58 | $this->assertArrayHasKey('description', $value); 59 | $this->assertIsString($value['description']); 60 | 61 | $this->assertArrayHasKey('thumbnail', $value); 62 | $this->assertIsString($value['thumbnail']); 63 | 64 | $this->assertArrayHasKey('link', $value); 65 | $this->assertIsString($value['link']); 66 | 67 | $this->assertArrayHasKey('categories', $value); 68 | $this->assertIsArray($value['categories']); 69 | 70 | $this->assertArrayHasKey('publishedAt', $value); 71 | $this->assertTrue($this->isValidDateTime($value['publishedAt'])); 72 | } 73 | } 74 | 75 | /** 76 | * Test `getDevPost` method with empty user. 77 | * 78 | * @return void 79 | * @throws InvalidArgumentException 80 | * @throws ExpectationFailedException 81 | */ 82 | public function testGetDevPostWithEmptyUser() 83 | { 84 | $result = $this->client->getDevPost(''); 85 | 86 | $this->assertIsArray($result); 87 | $this->assertEquals(0, count($result)); 88 | } 89 | 90 | /** 91 | * Test `getDevPost` method with invalid user. 92 | * 93 | * @return void 94 | * @throws InvalidArgumentException 95 | * @throws ExpectationFailedException 96 | */ 97 | public function testGetDevPostWithInvalidUser() 98 | { 99 | $result = $this->client->getDevPost('asdsfdsdfsdfsdfsdf-sdfsdfsdfs-sdfsdfsd-123-4234332' . time()); 100 | 101 | $this->assertIsArray($result); 102 | $this->assertEquals(0, count($result)); 103 | } 104 | 105 | /** 106 | * Test `getMediumPost` method with valid user. 107 | * 108 | * @return void 109 | * @throws InvalidArgumentException 110 | * @throws ExpectationFailedException 111 | * @throws Exception 112 | */ 113 | public function testGetMediumPostWithValidUser() 114 | { 115 | $result = $this->client->getMediumPost('arifszn'); 116 | 117 | $this->assertIsArray($result); 118 | 119 | foreach ($result as $value) { 120 | $this->assertArrayHasKey('title', $value); 121 | $this->assertIsString($value['title']); 122 | 123 | $this->assertArrayHasKey('description', $value); 124 | $this->assertIsString($value['description']); 125 | 126 | $this->assertArrayHasKey('thumbnail', $value); 127 | $this->assertIsString($value['thumbnail']); 128 | 129 | $this->assertArrayHasKey('link', $value); 130 | $this->assertIsString($value['link']); 131 | 132 | $this->assertArrayHasKey('categories', $value); 133 | $this->assertIsArray($value['categories']); 134 | 135 | $this->assertArrayHasKey('publishedAt', $value); 136 | $this->assertTrue($this->isValidDateTime($value['publishedAt'])); 137 | } 138 | } 139 | 140 | /** 141 | * Test `getMediumPost` method with empty user. 142 | * 143 | * @return void 144 | * @throws InvalidArgumentException 145 | * @throws ExpectationFailedException 146 | */ 147 | public function testGetMediumPostWithEmptyUser() 148 | { 149 | $result = $this->client->getMediumPost(''); 150 | 151 | $this->assertIsArray($result); 152 | $this->assertEquals(0, count($result)); 153 | } 154 | 155 | /** 156 | * Test `getMediumPost` method with invalid user. 157 | * 158 | * @return void 159 | * @throws InvalidArgumentException 160 | * @throws ExpectationFailedException 161 | */ 162 | public function testGetMediumPostWithInvalidUser() 163 | { 164 | $result = $this->client->getMediumPost('asdsfdsdfsdfsdfsdf-sdfsdfsdfs-sdfsdfsd-123-4234332' . time()); 165 | 166 | $this->assertIsArray($result); 167 | $this->assertEquals(0, count($result)); 168 | } 169 | 170 | /** 171 | * Check valid date time. 172 | * 173 | * @param mixed $date 174 | * @return bool 175 | */ 176 | private function isValidDateTime($date): bool 177 | { 178 | return date('Y-m-d H:i:s', strtotime($date)) === $date; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | utils = new Utils(); 25 | } 26 | 27 | /** 28 | * Test request method. 29 | * 30 | * @return void 31 | * @throws Exception 32 | * @throws InvalidArgumentException 33 | * @throws ExpectationFailedException 34 | */ 35 | public function testRequest() 36 | { 37 | $response = $this->utils->request('https://dev.to/api/articles?per_page=10&username=arifszn'); 38 | 39 | $this->assertIsArray($response); 40 | } 41 | 42 | /** 43 | * Test exception on request method. 44 | * 45 | * @return void 46 | * @throws Exception 47 | */ 48 | public function testRequestException() 49 | { 50 | $this->expectException(Exception::class); 51 | $this->utils->request(''); 52 | } 53 | } 54 | --------------------------------------------------------------------------------