├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Providers │ └── StackoverflowProvider.php └── Queries │ └── StackoverflowQuery.php └── tests └── src ├── StackoverflowProviderTest.php └── StackoverflowQueryTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | phpunit.xml 4 | composer.lock 5 | vendor 6 | .idea 7 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | tools: 20 | external_code_coverage: 21 | timeout: 600 22 | runs: 2 23 | php_analyzer: true 24 | php_code_coverage: false 25 | php_code_sniffer: 26 | config: 27 | standard: PSR2 28 | filter: 29 | paths: ['src'] 30 | php_loc: 31 | enabled: true 32 | excluded_dirs: [vendor, tests] 33 | php_cpd: 34 | enabled: true 35 | excluded_dirs: [vendor, tests] 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | 9 | before_script: 10 | - travis_retry composer self-update 11 | - travis_retry composer install --no-interaction --prefer-source --dev 12 | - travis_retry phpenv rehash 13 | 14 | script: 15 | - ./vendor/bin/phpcs --standard=psr2 src/ 16 | - ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 17 | 18 | after_script: 19 | - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi 20 | - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All Notable changes to `jobs-stackoverflow` will be documented in this file 3 | 4 | ## [WIP] 5 | 6 | ### Added 7 | - Nothing 8 | 9 | ### Deprecated 10 | - Nothing 11 | 12 | ### Fixed 13 | - Test with debug statement 14 | 15 | ## 0.1.0 - 2016-11-29 16 | 17 | ### Added 18 | - Initial Release 19 | 20 | ### Deprecated 21 | - Nothing 22 | 23 | ### Fixed 24 | - Nothing 25 | 26 | ### Removed 27 | - Nothing 28 | 29 | ### Security 30 | - Nothing 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/jobapis/jobs-stackoverflow). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The Apache 2.0 License 2 | 3 | Copyright 2016 Karl Hughes 4 | 5 | > Licensed under the Apache License, Version 2.0 (the "License"); 6 | > you may not use this file except in compliance with the License. 7 | > You may obtain a copy of the License at 8 | > 9 | > http://www.apache.org/licenses/LICENSE-2.0 10 | > 11 | > Unless required by applicable law or agreed to in writing, software 12 | > distributed under the License is distributed on an "AS IS" BASIS, 13 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | > See the License for the specific language governing permissions and 15 | > limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack Overflow Jobs Client 2 | 3 | [![Latest Version](https://img.shields.io/github/release/jobapis/jobs-stackoverflow.svg?style=flat-square)](https://github.com/jobapis/jobs-stackoverflow/releases) 4 | [![Software License](https://img.shields.io/badge/license-APACHE%202.0-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/jobapis/jobs-stackoverflow/master.svg?style=flat-square&1)](https://travis-ci.org/jobapis/jobs-stackoverflow) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/jobapis/jobs-stackoverflow.svg?style=flat-square)](https://scrutinizer-ci.com/g/jobapis/jobs-stackoverflow/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/jobapis/jobs-stackoverflow.svg?style=flat-square)](https://scrutinizer-ci.com/g/jobapis/jobs-stackoverflow) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/jobapis/jobs-stackoverflow.svg?style=flat-square)](https://packagist.org/packages/jobapis/jobs-stackoverflow) 9 | 10 | This package provides [Stack Overflow Careers](https://stackoverflow.com/jobs) RSS feed support for [Jobs Common](https://github.com/jobapis/jobs-common). 11 | 12 | ## Installation 13 | 14 | To install, use composer: 15 | 16 | ``` 17 | composer require jobapis/jobs-stackoverflow 18 | ``` 19 | 20 | ## Usage 21 | Create a Query object and add all the parameters you'd like via the constructor. 22 | 23 | ```php 24 | // Add parameters to the query via the constructor 25 | $query = new JobApis\Jobs\Client\Queries\StackoverflowQuery(); 26 | ``` 27 | 28 | Or via the "set" method. All of the parameters documented in Indeed's documentation can be added. 29 | 30 | ```php 31 | // Add parameters via the set() method 32 | $query->set('q', 'engineering'); 33 | ``` 34 | 35 | You can even chain them if you'd like. 36 | 37 | ```php 38 | // Add parameters via the set() method 39 | $query->set('l', 'Chicago, IL') 40 | ->set('pg', '2'); 41 | ``` 42 | 43 | Then inject the query object into the provider. 44 | 45 | ```php 46 | // Instantiating a provider with a query object 47 | $client = new JobApis\Jobs\Client\Providers\StackoverflowProvider($query); 48 | ``` 49 | 50 | And call the "getJobs" method to retrieve results. 51 | 52 | ```php 53 | // Get a Collection of Jobs 54 | $jobs = $client->getJobs(); 55 | ``` 56 | 57 | The `getJobs` method will return a [Collection](https://github.com/jobapis/jobs-common/blob/master/src/Collection.php) of [Job](https://github.com/jobapis/jobs-common/blob/master/src/Job.php) objects. 58 | 59 | ## Testing 60 | 61 | ``` bash 62 | $ ./vendor/bin/phpunit 63 | ``` 64 | 65 | ## Contributing 66 | 67 | Please see [CONTRIBUTING](https://github.com/jobapis/jobs-stackoverflow/blob/master/CONTRIBUTING.md) for details. 68 | 69 | 70 | ## Credits 71 | 72 | - [Karl Hughes](https://github.com/karllhughes) 73 | - [All Contributors](https://github.com/jobapis/jobs-stackoverflow/contributors) 74 | 75 | 76 | ## License 77 | 78 | The Apache 2.0. Please see [License File](https://github.com/jobapis/jobs-stackoverflow/blob/master/LICENSE) for more information. 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jobapis/jobs-stackoverflow", 3 | "type": "library", 4 | "description": "Making it simple to integrate your application with Stack Overflows's Career RSS Feed.", 5 | "keywords": [ 6 | "jobs", 7 | "api client", 8 | "object", 9 | "stack overflow" 10 | ], 11 | "homepage": "https://github.com/jobapis/jobs-stackoverflow", 12 | "license": "Apache-2.0", 13 | "authors": [ 14 | { 15 | "name": "Karl Hughes", 16 | "email": "khughes.me@gmail.com", 17 | "homepage": "https://github.com/karllhughes" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.5.0", 22 | "jobapis/jobs-common": "^2.0.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": ">=4.6", 26 | "phpunit/php-code-coverage": "~2.0", 27 | "mockery/mockery": ">=0.9.4", 28 | "squizlabs/php_codesniffer": "~2.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "JobApis\\Jobs\\Client\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "JobApis\\Jobs\\Client\\Test\\": "tests/src/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 22 | 23 | 24 | 25 | ./tests/ 26 | 27 | 28 | 29 | 30 | ./ 31 | 32 | ./vendor 33 | ./tests 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Providers/StackoverflowProvider.php: -------------------------------------------------------------------------------- 1 | $payload['description'], 18 | 'location' => $payload['location'], 19 | 'name' => $payload['title'], 20 | 'title' => $payload['title'], 21 | 'url' => $payload['link'], 22 | ]); 23 | 24 | // Set date posted 25 | $job->setDatePostedAsString($payload['pubDate']); 26 | 27 | // Set skills 28 | if (isset($payload['category']) && is_array($payload['category'])) { 29 | $skills = []; 30 | foreach ($payload['category'] as $category) { 31 | $skills[] = $category; 32 | } 33 | $job->setSkills(implode(', ', $skills)); 34 | } 35 | 36 | return $job; 37 | } 38 | 39 | /** 40 | * Job response object default keys that should be set 41 | * 42 | * @return array 43 | */ 44 | public function getDefaultResponseFields() 45 | { 46 | return [ 47 | 'description', 48 | 'link', 49 | 'location', 50 | 'pubDate', 51 | 'title', 52 | ]; 53 | } 54 | 55 | /** 56 | * Get format 57 | * 58 | * @return string Currently only 'json' and 'xml' supported 59 | */ 60 | public function getFormat() 61 | { 62 | return 'xml'; 63 | } 64 | 65 | /** 66 | * Get listings path 67 | * 68 | * @return string 69 | */ 70 | public function getListingsPath() 71 | { 72 | return 'channel.item'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Queries/StackoverflowQuery.php: -------------------------------------------------------------------------------- 1 | q; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/src/StackoverflowProviderTest.php: -------------------------------------------------------------------------------- 1 | query = m::mock('JobApis\Jobs\Client\Queries\StackoverflowQuery'); 14 | 15 | $this->client = new StackoverflowProvider($this->query); 16 | } 17 | 18 | public function testItCanGetDefaultResponseFields() 19 | { 20 | $fields = [ 21 | 'description', 22 | 'link', 23 | 'location', 24 | 'pubDate', 25 | 'title', 26 | ]; 27 | $this->assertEquals($fields, $this->client->getDefaultResponseFields()); 28 | } 29 | 30 | public function testItCanGetListingsPath() 31 | { 32 | $this->assertEquals('channel.item', $this->client->getListingsPath()); 33 | } 34 | 35 | public function testItCanGetFormat() 36 | { 37 | $this->assertEquals('xml', $this->client->getFormat()); 38 | } 39 | 40 | public function testItCanCreateJobObject() 41 | { 42 | $payload = $this->createJobArray(); 43 | 44 | $results = $this->client->createJobObject($payload); 45 | 46 | $this->assertInstanceOf(Job::class, $results); 47 | $this->assertEquals($payload['title'], $results->getTitle()); 48 | $this->assertEquals($payload['title'], $results->getName()); 49 | $this->assertEquals($payload['location'], $results->getLocation()); 50 | $this->assertEquals($payload['description'], $results->getDescription()); 51 | $this->assertEquals($payload['link'], $results->getUrl()); 52 | } 53 | 54 | /** 55 | * Integration test for the client's getJobs() method. 56 | */ 57 | public function testItCanGetJobs() 58 | { 59 | $options = [ 60 | 'q' => uniqid(), 61 | 'l' => uniqid(), 62 | 't' => uniqid(), 63 | ]; 64 | 65 | $guzzle = m::mock('GuzzleHttp\Client'); 66 | 67 | $query = new StackoverflowQuery($options); 68 | 69 | $client = new StackoverflowProvider($query); 70 | 71 | $client->setClient($guzzle); 72 | 73 | $response = m::mock('GuzzleHttp\Message\Response'); 74 | 75 | $jobs = $this->createXmlResponse(); 76 | 77 | $guzzle->shouldReceive('get') 78 | ->with($query->getUrl(), []) 79 | ->once() 80 | ->andReturn($response); 81 | $response->shouldReceive('getBody') 82 | ->once() 83 | ->andReturn($jobs); 84 | 85 | $results = $client->getJobs(); 86 | 87 | $this->assertInstanceOf(Collection::class, $results); 88 | $this->assertCount(2, $results); 89 | } 90 | 91 | /** 92 | * Integration test with actual API call to the provider. 93 | */ 94 | public function testItCanGetJobsFromApi() 95 | { 96 | if (!getenv('REAL_CALL')) { 97 | $this->markTestSkipped('REAL_CALL not set. Real API call will not be made.'); 98 | } 99 | 100 | $keyword = 'engineering'; 101 | 102 | $query = new StackoverflowQuery([ 103 | 'q' => $keyword, 104 | ]); 105 | 106 | $client = new StackoverflowProvider($query); 107 | 108 | $results = $client->getJobs(); 109 | 110 | $this->assertInstanceOf('JobApis\Jobs\Client\Collection', $results); 111 | 112 | foreach($results as $job) { 113 | $this->assertEquals($keyword, $job->query); 114 | } 115 | } 116 | 117 | private function createJobArray() 118 | { 119 | return [ 120 | 'title' => uniqid(), 121 | 'link' => uniqid(), 122 | 'location' => uniqid(), 123 | 'description' => uniqid(), 124 | 'pubDate' => 'Fri, '.rand(1,30).' Nov '.rand(2015, 2016).' 18:36:18 Z', 125 | ]; 126 | } 127 | 128 | private function createXmlResponse() 129 | { 130 | return "r language jobs - Stack Overflowhttp://stackoverflow.com/jobsr language jobs - Stack Overflowhttp://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico?v=4f32ecc8f43dr language jobs - Stack Overflowhttp://stackoverflow.com/jobs832https://stackoverflow.com/jobs/117083/full-stack-engineer-sandstorm-designhttps://stackoverflow.com/jobs/117083/full-stack-engineer-sandstorm-designSandstorm DesignphpdrupalpythonjavascriptjqueryFull Stack Engineer at Sandstorm Design (Chicago, IL)<p>Sandstorm Design is seeking an Full Stack Engineer to join our team!</p><br /><p>Join a creative, collaborative and nimble team to help architect and build innovative websites and web applications.</p><br /><p>The Full Stack Engineer is dynamic individual responsible for implementing front-end and back-end development on client websites and web applications utilizing HTML, CSS, JavaScript, jQuery, PHP, Python and other relevant programming languages. Other responsibilities include Drupal and other application architecture; server configuration; developing, maintaining and following coding standards and best practices; working with the creative and sales teams to estimate upcoming projects and proposals; staying informed of new web development trends and technologies in order to bring ideas and suggestions to current and future projects. Other duties as requested.</p><br /><p>You&rsquo;ll work in a fun, energetic environment that builds careers and makes news. Founded in 1998 by a <a href=\"https://www.sandstormdesign.com/who-we-are#our-team\" rel=\"nofollow\">successful female entrepreneur</a>, Sandstorm has earned nationwide recognition for being an industry leader and sustaining a track record of satisfied clients.</p><br /><p>If this sounds like the right fit, <a href=\"https://www.sandstormdesign.com/join-our-team\" rel=\"nofollow\">apply online</a> or email your cover letter and resume to&nbsp;jobs@sandstormdesign.com. If it&rsquo;s not quite right for you, please share this opportunity with someone in your network.</p>Sat, 05 Nov 2016 14:33:45 Z2016-11-05T14:33:45ZChicago, ILhttps://stackoverflow.com/jobs/128590/senior-full-stack-engineer-triggr-healthhttps://stackoverflow.com/jobs/128590/senior-full-stack-engineer-triggr-healthTriggr Healthandroidiosnodejsreact-nativepythonSenior Full Stack Engineer at Triggr Health (Chicago, IL)<p>We are building apps on both Android and iOS, a customer-facing web application, a robust web services API, machine learning-driven analytics, and large-scale data processing. As a full-stack engineer, you must be hyper-focused on how the product impacts every patient, provider, and team member.</p>Tue, 15 Nov 2016 22:59:17 Z2016-11-15T22:59:17ZChicago, IL"; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/src/StackoverflowQueryTest.php: -------------------------------------------------------------------------------- 1 | query = new StackoverflowQuery(); 11 | } 12 | 13 | public function testItCanGetBaseUrl() 14 | { 15 | $this->assertEquals( 16 | 'https://stackoverflow.com/jobs/feed', 17 | $this->query->getBaseUrl() 18 | ); 19 | } 20 | 21 | public function testItCanGetKeyword() 22 | { 23 | $keyword = uniqid(); 24 | $this->query->set('q', $keyword); 25 | $this->assertEquals($keyword, $this->query->getKeyword()); 26 | } 27 | 28 | public function testItCanAddAttributesToUrl() 29 | { 30 | $this->query->set('q', uniqid()); 31 | $this->query->set('l', uniqid()); 32 | 33 | $url = $this->query->getUrl(); 34 | 35 | $this->assertContains('q=', $url); 36 | $this->assertContains('l=', $url); 37 | } 38 | 39 | /** 40 | * @expectedException OutOfRangeException 41 | */ 42 | public function testItThrowsExceptionWhenSettingInvalidAttribute() 43 | { 44 | $this->query->set(uniqid(), uniqid()); 45 | } 46 | 47 | /** 48 | * @expectedException OutOfRangeException 49 | */ 50 | public function testItThrowsExceptionWhenGettingInvalidAttribute() 51 | { 52 | $this->query->get(uniqid()); 53 | } 54 | 55 | public function testItSetsAndGetsValidAttributes() 56 | { 57 | $attributes = [ 58 | 'q' => uniqid(), 59 | 'l' => uniqid(), 60 | 'pg' => rand(1,100), 61 | ]; 62 | 63 | foreach ($attributes as $key => $value) { 64 | $this->query->set($key, $value); 65 | } 66 | 67 | foreach ($attributes as $key => $value) { 68 | $this->assertEquals($value, $this->query->get($key)); 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------