├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── README.md ├── README.ru.md ├── composer.json ├── examples ├── README.md ├── demo.jpg └── index.php ├── phpunit.xml ├── src ├── AbstractEnum.php ├── AccessToken.php ├── Client.php ├── Exception.php ├── Http │ └── Method.php └── Scope.php └── tests ├── AccessTokenTest.php ├── ClientTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/**/workspace.xml 2 | .idea/**/tasks.xml 3 | .idea/dictionaries 4 | .idea/**/dataSources/ 5 | .idea/**/dataSources.ids 6 | .idea/**/dataSources.xml 7 | .idea/**/dataSources.local.xml 8 | .idea/**/sqlDataSources.xml 9 | .idea/**/dynamic.xml 10 | .idea/**/uiDesigner.xml 11 | .idea/**/gradle.xml 12 | .idea/**/libraries 13 | .idea/**/mongoSettings.xml 14 | *.iws 15 | /out/ 16 | .idea_modules/ 17 | atlassian-ide-plugin.xml 18 | com_crashlytics_export_strings.xml 19 | crashlytics.properties 20 | crashlytics-build.properties 21 | fabric.properties 22 | composer.phar 23 | /vendor/ 24 | .env 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: php 3 | php: 4 | - 5.6 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | - 7.4 9 | - 7 10 | 11 | 12 | before_script: 13 | - composer update 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 contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at philipp@zoonman.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Make an issue 4 | 2. Create a pull request linked to this issue 5 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # What? Where? When? 2 | 3 | When you are going to report a new issue, include the following information 4 | 5 | 1. Environment 6 | 2. Steps to reproduce 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Philipp Tkachev 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 | LinkedIn API Client with OAuth 2 authorization written on PHP 2 | ============================================================ 3 | [![Build Status](https://travis-ci.org/zoonman/linkedin-api-php-client.svg?branch=master)](https://travis-ci.org/zoonman/linkedin-api-php-client) [![Code Climate](https://codeclimate.com/github/zoonman/linkedin-api-php-client/badges/gpa.svg)](https://codeclimate.com/github/zoonman/linkedin-api-php-client) [![Packagist](https://img.shields.io/packagist/dt/zoonman/linkedin-api-php-client.svg)](https://packagist.org/packages/zoonman/linkedin-api-php-client) [![GitHub license](https://img.shields.io/github/license/zoonman/linkedin-api-php-client.svg)](https://github.com/zoonman/linkedin-api-php-client/blob/master/LICENSE.md) 4 | 5 | 6 | 7 | See [complete example](examples/) inside [index.php](examples/index.php) to get started. 8 | 9 | 10 | ## Installation 11 | 12 | You will need at least PHP 7.3. We match [officially supported](https://www.php.net/supported-versions.php) versions of PHP. 13 | 14 | Use [composer](https://getcomposer.org/) package manager to install the lastest version of the package: 15 | 16 | ```bash 17 | composer require zoonman/linkedin-api-php-client 18 | ``` 19 | 20 | Or add this package as dependency to `composer.json`. 21 | 22 | If you have never used Composer, you should start [here](http://www.phptherightway.com/#composer_and_packagist) 23 | and install composer. 24 | 25 | 26 | ## Get Started 27 | 28 | Before you will get started, play visit to [LinkedIn API Documentation](https://docs.microsoft.com/en-us/linkedin/). 29 | This will save you a lot of time and prevent some silly questions. 30 | 31 | To start working with LinkedIn API, you will need to 32 | get application client id and secret. 33 | 34 | Go to [LinkedIn Developers portal](https://developer.linkedin.com/) 35 | and create new application in section My Apps. 36 | Save ClientId and ClientSecret, you will use them later. 37 | 38 | 39 | #### Bootstrapping autoloader and instantiating a client 40 | 41 | 42 | ```php 43 | // ... please, add composer autoloader first 44 | include_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; 45 | 46 | // import client class 47 | use LinkedIn\Client; 48 | 49 | // instantiate the Linkedin client 50 | $client = new Client( 51 | 'YOUR_LINKEDIN_APP_CLIENT_ID', 52 | 'YOUR_LINKEDIN_APP_CLIENT_SECRET' 53 | ); 54 | ``` 55 | 56 | #### Getting local redirect URL 57 | 58 | To start linking process you have to setup redirect url. 59 | You can set your own or use current one. 60 | SDK provides you a `getRedirectUrl()` helper for your convenience: 61 | 62 | ```php 63 | $redirectUrl = $client->getRedirectUrl(); 64 | ``` 65 | 66 | We recommend you to have it stored during the linking session 67 | because you will need to use it when you will be getting access token. 68 | 69 | #### Setting local redirect URL 70 | 71 | Set a custom redirect url use: 72 | 73 | ```php 74 | $client->setRedirectUrl('http://your.domain.tld/path/to/script/'); 75 | ``` 76 | 77 | #### Getting LinkedIn redirect URL 78 | 79 | In order of performing OAUTH 2.0 flow, you should get LinkedIn login URL. 80 | During this procedure you have to define scope of requested permissions. 81 | Use `Scope` enum class to get scope names. 82 | To get redirect url to LinkedIn, use the following approach: 83 | 84 | ```php 85 | use LinkedIn\Scope; 86 | 87 | // define scope 88 | $scopes = [ 89 | Scope::READ_LITE_PROFILE, 90 | Scope::READ_EMAIL_ADDRESS, 91 | Scope::SHARE_AS_USER, 92 | Scope::SHARE_AS_ORGANIZATION, 93 | ]; 94 | $loginUrl = $client->getLoginUrl($scopes); // get url on LinkedIn to start linking 95 | ``` 96 | 97 | Now you can take user to LinkedIn. You can use link or rely on Location HTTP header. 98 | 99 | #### Getting Access Token 100 | 101 | To get access token use (don't forget to set redirect url) 102 | 103 | ```php 104 | $accessToken = $client->getAccessToken($_GET['code']); 105 | ``` 106 | This method returns object of `LinkedIn\AccessToken` class. 107 | You can store this token in the file like this: 108 | ```php 109 | file_put_contents('token.json', json_encode($accessToken)); 110 | ``` 111 | This way of storing tokens is not recommended due to security concerns and used for demonstration purpose. 112 | Please, ensure that tokens are stored securely. 113 | 114 | #### Setting Access Token 115 | 116 | You can use method `setAccessToken()` for the `LinkedIn\Client` class to set token stored as string. You have to pass 117 | instance of `LinkedIn\AccessToken` to this method. 118 | 119 | ```php 120 | use LinkedIn\AccessToken; 121 | use LinkedIn\Client; 122 | 123 | // instantiate the Linkedin client 124 | $client = new Client( 125 | 'LINKEDIN_APP_CLIENT_ID', 126 | 'LINKEDIN_APP_CLIENT_SECRET' 127 | ); 128 | 129 | // load token from the file 130 | $tokenString = file_get_contents('token.json'); 131 | $tokenData = json_decode($tokenString, true); 132 | // instantiate access token object from stored data 133 | $accessToken = new AccessToken($tokenData['token'], $tokenData['expiresAt']); 134 | 135 | // set token for client 136 | $client->setAccessToken($accessToken); 137 | ``` 138 | 139 | #### Performing API calls 140 | 141 | All API calls can be called through simple method: 142 | 143 | ```php 144 | $profile = $client->api( 145 | 'ENDPOINT', 146 | ['parameter name' => 'its value here'], 147 | 'HTTP method like GET for example' 148 | ); 149 | ``` 150 | 151 | There are 3 helper methods: 152 | 153 | ```php 154 | // get method 155 | $client->get('ENDPOINT', ['param' => 'value']); 156 | 157 | //post 158 | $client->post('ENDPOINT', ['param' => 'value']); 159 | 160 | // delete 161 | $client->delete('ENDPOINT'); 162 | ``` 163 | 164 | #### Examples 165 | 166 | ##### Perform api call to get profile information 167 | 168 | ```php 169 | $profile = $client->get( 170 | 'me', 171 | ['fields' => 'id,firstName,lastName'] 172 | ); 173 | print_r($profile); 174 | ``` 175 | 176 | ##### List companies where you are an admin 177 | 178 | ```php 179 | $profile = $client->get( 180 | 'organizations', 181 | ['is-company-admin' => true] 182 | ); 183 | print_r($profile); 184 | ``` 185 | 186 | ##### Share content on a personal profile 187 | 188 | Make sure that image URL is available from the Internet (don't use localhost in the image url). 189 | 190 | ```php 191 | $share = $client->post( 192 | 'ugcPosts', 193 | [ 194 | 'author' => 'urn:li:person:' . $profile['id'], 195 | 'lifecycleState' => 'PUBLISHED', 196 | 'specificContent' => [ 197 | 'com.linkedin.ugc.ShareContent' => [ 198 | 'shareCommentary' => [ 199 | 'text' => 'Checkout this amazing PHP SDK for LinkedIn!' 200 | ], 201 | 'shareMediaCategory' => 'ARTICLE', 202 | 'media' => [ 203 | [ 204 | 'status' => 'READY', 205 | 'description' => [ 206 | 'text' => 'OAuth 2 flow, composer Package.' 207 | ], 208 | 'originalUrl' => 'https://github.com/zoonman/linkedin-api-php-client', 209 | 'title' => [ 210 | 'text' => 'PHP Client for LinkedIn API' 211 | ] 212 | ] 213 | ] 214 | ] 215 | ], 216 | 'visibility' => [ 217 | 'com.linkedin.ugc.MemberNetworkVisibility' => 'CONNECTIONS' 218 | ] 219 | ] 220 | ); 221 | print_r($share); 222 | ``` 223 | 224 | ##### Get Company page profile 225 | 226 | ```php 227 | $companyId = '123'; // use id of the company where you are an admin 228 | $companyInfo = $client->get('organizations/' . $companyId); 229 | print_r($companyInfo); 230 | ``` 231 | 232 | ##### Share content on a LinkedIn business page 233 | 234 | ```php 235 | // set sandboxed company page to work with 236 | // you can check updates at 237 | // https://www.linkedin.com/company/devtestco 238 | $companyId = '2414183'; 239 | 240 | $share = $client->post( 241 | 'ugcPosts', 242 | [ 243 | 'author' => 'urn:li:organization:' . $companyId, 244 | 'lifecycleState' => 'PUBLISHED', 245 | 'specificContent' => [ 246 | 'com.linkedin.ugc.ShareContent' => [ 247 | 'shareCommentary' => [ 248 | 'text' => 'Checkout this amazing PHP SDK for LinkedIn!' 249 | ], 250 | 'shareMediaCategory' => 'ARTICLE', 251 | 'media' => [ 252 | [ 253 | 'status' => 'READY', 254 | 'description' => [ 255 | 'text' => 'OAuth 2 flow, composer Package.' 256 | ], 257 | 'originalUrl' => 'https://github.com/zoonman/linkedin-api-php-client', 258 | 'title' => [ 259 | 'text' => 'PHP Client for LinkedIn API' 260 | ] 261 | ] 262 | ] 263 | ] 264 | ], 265 | 'visibility' => [ 266 | 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' 267 | ] 268 | ] 269 | ); 270 | print_r($share); 271 | ``` 272 | 273 | ##### Setup custom API request headers 274 | 275 | Change different headers sent to LinkedIn API. 276 | 277 | ```php 278 | $client->setApiHeaders([ 279 | 'Content-Type' => 'application/json', 280 | 'x-li-format' => 'json', 281 | 'X-Restli-Protocol-Version' => '2.0.0', // use protocol v2 282 | 'x-li-src' => 'msdk' // set a src header to "msdk" to mimic a mobile SDK 283 | ]); 284 | ``` 285 | 286 | ##### Change default API root 287 | 288 | Some private API access there. 289 | 290 | ```php 291 | $client->setApiRoot('https://api.linkedin.com/v2/'); 292 | ``` 293 | 294 | ##### ~Image Upload~ 295 | 296 | I assume you have to be LinkedIn partner or something like that. 297 | 298 | Try to upload image to LinkedIn. See [Rich Media Shares](https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/rich-media-shares) 299 | (returns "Not enough permissions to access media resource" for me). 300 | 301 | ```php 302 | $filename = '/path/to/image.jpg'; 303 | $client->setApiRoot('https://api.linkedin.com/'); 304 | $mp = $client->upload($filename); 305 | ``` 306 | 307 | ## Contributing 308 | 309 | Please, open PR with your changes linked to an GitHub issue. 310 | You code must follow [PSR](http://www.php-fig.org/psr/) standards and have PHPUnit tests. 311 | 312 | ## License 313 | 314 | [MIT](LICENSE.md) 315 | -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 | Клиент для работы с LinkedIn API с авторизацией через OAuth 2 написанный на PHP 2 | ============================================================ 3 | [![Build Status](https://travis-ci.org/zoonman/linkedin-api-php-client.svg?branch=master)](https://travis-ci.org/zoonman/linkedin-api-php-client) [![Code Climate](https://codeclimate.com/github/zoonman/linkedin-api-php-client/badges/gpa.svg)](https://codeclimate.com/github/zoonman/linkedin-api-php-client) [![Packagist](https://img.shields.io/packagist/dt/zoonman/linkedin-api-php-client.svg)](https://packagist.org/packages/zoonman/linkedin-api-php-client) [![GitHub license](https://img.shields.io/github/license/zoonman/linkedin-api-php-client.svg)](https://github.com/zoonman/linkedin-api-php-client/blob/master/LICENSE.md) 4 | 5 | 6 | 7 | Чтобы быстрее вникнуть, смотри [пример использования](examples/) внутри [index.php](examples/index.php). 8 | 9 | 10 | ## Установка 11 | 12 | Установка делается через composer следующей командой 13 | 14 | ```bash 15 | composer require zoonman/linkedin-api-php-client 16 | ``` 17 | 18 | Также можно добавить `composer.json`. 19 | 20 | Если вы никогда им не пользовались, познакомьтесь на [этой страничке](http://www.phptherightway.com/#composer_and_packagist) 21 | и установите composer. 22 | 23 | 24 | ## Использование клиента 25 | 26 | Чтобы начать работать с LinkedIn API, потребуется раздобыть идентификатор клиента (client id) и его секретный ключ (secret). 27 | 28 | Получить их можно на [Портале разработчиков](https://developer.linkedin.com/), для этого зайдите в секцию мои приложения 29 | (My Apps). 30 | 31 | 32 | #### Подключение к проекту 33 | 34 | Установите пакет, там появится каталог vendor, в котором будет autoload.php - это автозагрузчик. 35 | 36 | ```php 37 | // ... подлкючить автозагрузчик 38 | include_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; 39 | 40 | // сделать класс доступным 41 | use LinkedIn\Client; 42 | 43 | // создать новый объект 44 | $client = new Client( 45 | 'LINKEDIN_APP_CLIENT_ID', 46 | 'LINKEDIN_APP_CLIENT_SECRET' 47 | ); 48 | ``` 49 | 50 | #### Получение локального адреса для перенаправления 51 | 52 | Чтобы начать процесс аутентификации вам необходимо установить адрес для перенаправления. 53 | Вы можете вызвать метод `getRedirectUrl()`, 54 | 55 | ```php 56 | $redirectUrl = $client->getRedirectUrl(); 57 | ``` 58 | 59 | Вам нужно будет сохранить этот адрес во временное хранилище для текущей сессии. 60 | Вам потребуется этот адрес снова, когда вы будете получать токен. 61 | 62 | ```php 63 | $_SESSION['linkedin_redirect_url'] = $redirectUrl; 64 | ``` 65 | 66 | #### Установка собственного адреса возврата 67 | 68 | Вы также можете использовать `setRedirectUrl()`, чтобы установить свой обратный адрес. 69 | Не забудьте указать этот адрес в параметрах приложения. 70 | 71 | ```php 72 | $client->setRedirectUrl('http://your.domain.tld/path/to/script/'); 73 | ``` 74 | 75 | #### Получение адреса для аутентификации 76 | 77 | Для того, чтобы пройти аутентификацию, вам необходимо получить адрес в LinkedIn, 78 | на который нужно перенаправить пользователя. 79 | Этот тот самый адрес, на котором пользователя спрашивают о подтвердении 80 | запрашиваемых прав доступа для приложения. 81 | 82 | ```php 83 | // определить области доступа 84 | $scopes = [ 85 | 'r_basicprofile', 86 | 'r_emailaddress', 87 | 'rw_company_admin', 88 | 'w_share', 89 | ]; 90 | $loginUrl = $client->getLoginUrl($scopes); // получить адрес 91 | ``` 92 | 93 | Теперь нужно перенаправить пользователя на полученный адрес. 94 | 95 | 96 | #### Получение токена 97 | 98 | Чтобы получить токен или маркер доступа, как его иногда называют, 99 | нужно установить обратный адрес ($redirectUrl), который вы сохранили в сессии. 100 | 101 | А затем вызвать получение токена 102 | 103 | ```php 104 | $accessToken = $client->getAccessToken($_GET['code']); 105 | ``` 106 | 107 | #### Вызов API 108 | 109 | All API calls can be called through simple method: 110 | Вызовы API происходят с помощью простого метода api(), 111 | который принимает 3 параметра: путь вызова, параметры и метод. 112 | 113 | ```php 114 | $profile = $client->api( 115 | 'ENDPOINT', 116 | ['parameter name' => 'its value here'], 117 | 'HTTP method like GET for example' 118 | ); 119 | ``` 120 | 121 | Есть два упрощенных вызова: 122 | 123 | ```php 124 | // метод get 125 | $client->get('путь', ['имя параметра' => 'значение']); 126 | 127 | // метод post 128 | $client->post('ENDPOINT', ['param' => 'value']); 129 | ``` 130 | #### Примеры 131 | 132 | Получить информацию о профиле 133 | 134 | ```php 135 | $profile = $client->get( 136 | 'people/~:(id,email-address,first-name,last-name)' 137 | ); 138 | print_r($profile); 139 | ``` 140 | 141 | Получить список компаний, в которой владелец токена - администратор. 142 | 143 | ```php 144 | $profile = $client->get( 145 | 'companies', 146 | ['is-company-admin' => true] 147 | ); 148 | print_r($profile); 149 | ``` 150 | 151 | Опубликовать сообщение у себя на странице профиля 152 | 153 | ```php 154 | $share = $client->post( 155 | 'people/~/shares', 156 | [ 157 | 'comment' => 'Посмотри, какая классная библиотека для работы с LinkedIn!', 158 | 'content' => [ 159 | 'title' => 'PHP Client for LinkedIn API', 160 | 'description' => 'OAuth 2 flow, composer Package', 161 | 'submitted-url' => 'https://github.com/zoonman/linkedin-api-php-client', 162 | 'submitted-image-url' => 'https://github.com/fluidicon.png', 163 | ], 164 | 'visibility' => [ 165 | 'code' => 'anyone' 166 | ] 167 | ] 168 | ); 169 | ``` 170 | 171 | Поделиться контентом на тестовой странице компаний 172 | 173 | ```php 174 | // Вы можете увидеть сообщение на этой странице 175 | // https://www.linkedin.com/company/devtestco 176 | $companyId = '2414183'; // идентификатор страницы 177 | 178 | $share = $client->post( 179 | 'companies/' . $companyId . '/shares', 180 | [ 181 | 'comment' => 'Checkout this amazing PHP SDK for LinkedIn!', 182 | 'content' => [ 183 | 'title' => 'PHP Client for LinkedIn API', 184 | 'description' => 'OAuth 2 flow, composer Package', 185 | 'submitted-url' => 'https://github.com/zoonman/linkedin-api-php-client', 186 | 'submitted-image-url' => 'https://github.com/fluidicon.png', 187 | ], 188 | 'visibility' => [ 189 | 'code' => 'anyone' 190 | ] 191 | ] 192 | ); 193 | ``` 194 | 195 | Установить заголовки по умолчанию 196 | 197 | ```php 198 | $client->setApiHeaders([ 199 | 'Content-Type' => 'application/json', 200 | 'x-li-format' => 'json', 201 | 'x-li-src' => 'msdk' // например отправить "msdk" чтобы симулировать мобильное SDK 202 | ]); 203 | ``` 204 | 205 | Изменить корневой адрес для API вызовов 206 | 207 | ```php 208 | $client->setApiRoot('https://api.linkedin.com/v2/'); 209 | ``` 210 | 211 | ## Помощь проекту 212 | 213 | Если вы нашли ошибку и исправили ее, вы всегда можете открыть Pull Request. 214 | У нас есть небольшое требование к качеству кода. 215 | Пожалуйста, следуйте стандарту [PSR](http://www.php-fig.org/psr/) и пишите тесты PHPUnit для вносимых изменений. 216 | 217 | ## Лицензия 218 | 219 | [MIT](LICENSE.md) - вы имеете право использовать библиотеку без каких-либо отчислений. 220 | Пожалуйста, указывайте ссылку на данный проекта в своих приложениях. 221 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zoonman/linkedin-api-php-client", 3 | "description": "LinkedIn API PHP SDK with OAuth 2.0 & CSRF support. Can be used for social sign in or sharing on LinkedIn. Examples. Documentation.", 4 | "type": "library", 5 | "keywords": [ 6 | "linkedin", "linkedin-client", "linkedin-api", "linkedin-signin", 7 | "linkedin-sdk", "linkedin-login", "linked in", "linkedin.com", "php-linkedin", 8 | "oauth", "oauth2", "oauth2-client", "oauth2-authentication", "authentication", 9 | "social", "rest", "api", "client", "social network", "auth", "authorization", 10 | "wrapper", "integration", "platform" 11 | ], 12 | "homepage": "https://github.com/zoonman/linkedin-api-php-client", 13 | "require": { 14 | "php": ">=5.6", 15 | "ext-curl": "*", 16 | "guzzlehttp/guzzle": "^6.3" 17 | }, 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Philipp Tkachev", 22 | "email": "philipp@zoonman.com", 23 | "homepage": "http://www.zoonman.com/", 24 | "role": "Developer" 25 | }, 26 | { 27 | "name": "Aleksey Salnikov", 28 | "email": "me@iamsalnikov.ru", 29 | "homepage": "http://iamsalnikov.ru/", 30 | "role": "Developer" 31 | }, 32 | { 33 | "name": "Daniel J. Post", 34 | "homepage": "http://danieljpost.info/", 35 | "role": "Developer" 36 | } 37 | ], 38 | "support": { 39 | "docs": "https://www.zoonman.com/projects/linkedin-client/", 40 | "source": "https://github.com/zoonman/linkedin-api-php-client", 41 | "issues": "https://github.com/zoonman/linkedin-api-php-client/issues", 42 | "wiki": "https://github.com/zoonman/linkedin-api-php-client/wiki" 43 | }, 44 | "autoload": { 45 | "psr-4": {"LinkedIn\\": "src/"} 46 | }, 47 | "require-dev": { 48 | "vlucas/phpdotenv": "~2.0", 49 | "phpunit/phpunit": "~4.0", 50 | "phpmd/phpmd": "@stable", 51 | "squizlabs/php_codesniffer": "@stable" 52 | }, 53 | "archive": { 54 | "exclude": [ 55 | "examples", 56 | "tests", 57 | ".env", 58 | ".travis.yml", 59 | "phpunit.xml" 60 | ] 61 | }, 62 | "autoload-dev": {} 63 | } 64 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # How to run examples 2 | 3 | First, [clone](https://help.github.com/articles/cloning-a-repository/) repository. 4 | 5 | ```bash 6 | git clone https://github.com/zoonman/linkedin-api-php-client 7 | ``` 8 | Change dir to the repo 9 | ```bash 10 | cd linkedin-api-php-client 11 | ``` 12 | 13 | Install dependencies: 14 | 15 | ```bash 16 | composer install [-d /path/to/repository/root] 17 | ``` 18 | If you don't have composer, you can get it [here](https://getcomposer.org/doc/00-intro.md). 19 | Parameters in brackets are optional. 20 | 21 | Create `.env` file with linkedin credentials in the parent catalog (in the repository root) like this 22 | 23 | ```ini 24 | LINKEDIN_CLIENT_ID=111ClientId111 25 | LINKEDIN_CLIENT_SECRET=222ClientSecret 26 | ``` 27 | 28 | The simplest way to do that to run the following commands: 29 | ```bash 30 | echo 'LINKEDIN_CLIENT_ID=111ClientId111' >> .env 31 | echo 'LINKEDIN_CLIENT_SECRET=222ClientSecret' >> .env 32 | ``` 33 | 34 | To get client and secret go to [LinkedIn Developers portal](https://developer.linkedin.com/) and create new app there. 35 | 36 | After add to OAuth 2.0 Authorized Redirect URLs: 37 | ``` 38 | http://localhost:8901/ 39 | ``` 40 | 41 | Next, run PHP embedded server in the repository root: 42 | 43 | ```bash 44 | php -S localhost:8901 -t examples 45 | ``` 46 | 47 | Navigate to http://localhost:8901/ 48 | 49 | If you will see error like `Class 'Dotenv\Dotenv' not found...` install DotEnv using the following command: 50 | ```bash 51 | composer require vlucas/phpdotenv 52 | ``` 53 | -------------------------------------------------------------------------------- /examples/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoonman/linkedin-api-php-client/d06531c48d5efc8aaf7dc074a5320f5ba3f906c9/examples/demo.jpg -------------------------------------------------------------------------------- /examples/index.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 22:47 12 | * @license http://zoonman.com/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://zoonman.com/projects/linkedin-client 15 | */ 16 | 17 | // add Composer autoloader 18 | include_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor/autoload.php'; 19 | 20 | // import client class 21 | use LinkedIn\Client; 22 | use LinkedIn\Scope; 23 | 24 | // import environment variables from the environment file 25 | // you need a .env file in the parent folder 26 | // read this document to learn how to create that file 27 | // https://github.com/zoonman/linkedin-api-php-client/blob/master/examples/README.md 28 | // 29 | $dotenv = new Dotenv\Dotenv(dirname(__DIR__)); 30 | $dotenv->load(); 31 | 32 | // we need a session to keep intermediate results 33 | // you can use your own session persistence management 34 | // client doesn't depend on it 35 | session_start(); 36 | 37 | // instantiate the Linkedin client 38 | // you can setup keys using 39 | $client = new Client( 40 | getenv('LINKEDIN_CLIENT_ID'), 41 | getenv('LINKEDIN_CLIENT_SECRET') 42 | ); 43 | 44 | 45 | if (isset($_GET['code'])) { // we are returning back from LinkedIn with the code 46 | if (isset($_GET['state']) && // and state parameter in place 47 | isset($_SESSION['state']) && // and we have have stored state 48 | $_GET['state'] === $_SESSION['state'] // and it is our request 49 | ) { 50 | try { 51 | // you have to set initially used redirect url to be able 52 | // to retrieve access token 53 | $client->setRedirectUrl($_SESSION['redirect_url']); 54 | // retrieve access token using code provided by LinkedIn 55 | $accessToken = $client->getAccessToken($_GET['code']); 56 | h1('Access token'); 57 | pp($accessToken); // print the access token content 58 | h1('Profile'); 59 | // perform api call to get profile information 60 | $profile = $client->get( 61 | 'me', 62 | ['fields' => 'id,firstName,lastName'] 63 | ); 64 | pp($profile); // print profile information 65 | 66 | $emailInfo = $email = $client->get('emailAddress', ['q' => 'members', 'projection' => '(elements*(handle~))']); 67 | pp($emailInfo); 68 | 69 | $share = $client->post( 70 | 'ugcPosts', 71 | [ 72 | 'author' => 'urn:li:person:' . $profile['id'], 73 | 'lifecycleState' => 'PUBLISHED', 74 | 'specificContent' => [ 75 | 'com.linkedin.ugc.ShareContent' => [ 76 | 'shareCommentary' => [ 77 | 'text' => 'Checkout this amazing PHP SDK for LinkedIn!' 78 | ], 79 | 'shareMediaCategory' => 'ARTICLE', 80 | 'media' => [ 81 | [ 82 | 'status' => 'READY', 83 | 'description' => [ 84 | 'text' => 'OAuth 2 flow, composer Package.' 85 | ], 86 | 'originalUrl' => 'https://github.com/zoonman/linkedin-api-php-client', 87 | 'title' => [ 88 | 'text' => 'PHP Client for LinkedIn API' 89 | ] 90 | ] 91 | ] 92 | ] 93 | ], 94 | 'visibility' => [ 95 | 'com.linkedin.ugc.MemberNetworkVisibility' => 'CONNECTIONS' 96 | ] 97 | ] 98 | ); 99 | pp($share); 100 | 101 | // set sandboxed company page id to work with 102 | // https://www.linkedin.com/company/devtestco 103 | /* TODO! 104 | $companyId = '2414183'; 105 | 106 | h1('Company information'); 107 | $companyInfo = $client->get('companies/' . $companyId . ':(id,name,num-followers,description)'); 108 | pp($companyInfo); 109 | 110 | h1('Sharing on company page'); 111 | $companyShare = $client->post( 112 | 'companies/' . $companyId . '/shares', 113 | [ 114 | 'comment' => 115 | sprintf( 116 | '%s %s just tried this amazing PHP SDK for LinkedIn!', 117 | $profile['firstName'], 118 | $profile['lastName'] 119 | ), 120 | 'content' => [ 121 | 'title' => 'PHP Client for LinkedIn API', 122 | 'description' => 'OAuth 2 flow, composer Package', 123 | 'submitted-url' => 'https://github.com/zoonman/linkedin-api-php-client', 124 | 'submitted-image-url' => 'https://github.com/fluidicon.png', 125 | ], 126 | 'visibility' => [ 127 | 'code' => 'anyone' 128 | ] 129 | ] 130 | ); 131 | pp($companyShare); 132 | */ 133 | 134 | /* 135 | // Returns {"serviceErrorCode":100,"message":"Not enough permissions to access media resource","status":403} 136 | // You have to be whitelisted or so by LinkedIn 137 | $filename = './demo.jpg'; 138 | $client->setApiRoot('https://api.linkedin.com/'); 139 | $mp = $client->upload($filename); 140 | */ 141 | } catch (\LinkedIn\Exception $exception) { 142 | // in case of failure, provide with details 143 | pp($exception); 144 | pp($_SESSION); 145 | } 146 | echo 'Start over'; 147 | } else { 148 | // normally this shouldn't happen unless someone sits in the middle 149 | // and trying to override your state 150 | // or you are trying to change saved state during linking 151 | echo 'Invalid state!'; 152 | pp($_GET); 153 | pp($_SESSION); 154 | echo 'Start over'; 155 | } 156 | 157 | } elseif (isset($_GET['error'])) { 158 | // if you cancel during linking 159 | // you will be redirected back with reason 160 | pp($_GET); 161 | echo 'Start over'; 162 | } else { 163 | // define desired list of scopes 164 | $scopes = [ 165 | Scope::READ_LITE_PROFILE, 166 | Scope::READ_EMAIL_ADDRESS, 167 | Scope::SHARE_AS_USER, 168 | ]; 169 | $loginUrl = $client->getLoginUrl($scopes); // get url on LinkedIn to start linking 170 | $_SESSION['state'] = $client->getState(); // save state for future validation 171 | $_SESSION['redirect_url'] = $client->getRedirectUrl(); // save redirect url for future validation 172 | echo 'LoginUrl: ' . $loginUrl. ''; 173 | } 174 | 175 | /** 176 | * Pretty print whatever passed in 177 | * 178 | * @param mixed $anything 179 | */ 180 | function pp($anything) 181 | { 182 | echo '
' . print_r($anything, true) . '
'; 183 | } 184 | 185 | /** 186 | * Add header 187 | * 188 | * @param string $h 189 | */ 190 | function h1($h) { 191 | echo '

' . $h . '

'; 192 | } 193 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | tests 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/AbstractEnum.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/22/17 09:10 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | /** 20 | * Class AbstractEnum 21 | * 22 | * @package LinkedIn 23 | */ 24 | abstract class AbstractEnum 25 | { 26 | 27 | /** 28 | * @return array 29 | */ 30 | public static function getMap() 31 | { 32 | $spl = new \ReflectionClass(get_called_class()); 33 | return $spl->getConstants(); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public static function getValues() 40 | { 41 | return array_values(static::getMap()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AccessToken.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 22:55 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | /** 20 | * Class AccessToken 21 | * 22 | * @package LinkedIn 23 | */ 24 | class AccessToken implements \JsonSerializable 25 | { 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $token; 31 | 32 | /** 33 | * When token will expire. 34 | * 35 | * Please, pay attention that LinkedIn API always returns "expires in" time, 36 | * which is amount of seconds before token will expire since now. 37 | * If you are going to store token somewhere, you have to keep "expires at" 38 | * or two values - "expires in" and "token created". 39 | * Using "expires at" approach lets you have efficient queries to find 40 | * tokens will soon expire and be proactive with regards to your 41 | * B2C communication. 42 | * 43 | * @var int 44 | */ 45 | protected $expiresAt; 46 | 47 | /** 48 | * AccessToken constructor. 49 | * 50 | * @param string $token 51 | * @param int $expiresAt 52 | */ 53 | public function __construct($token = '', $expiresAt = 0) 54 | { 55 | $this->setToken($token); 56 | $this->setExpiresAt($expiresAt); 57 | } 58 | 59 | /** 60 | * Get token string 61 | * 62 | * @return string 63 | */ 64 | public function getToken() 65 | { 66 | return $this->token; 67 | } 68 | 69 | /** 70 | * Set token string 71 | * 72 | * @param string $token 73 | * 74 | * @return AccessToken 75 | */ 76 | public function setToken($token) 77 | { 78 | $this->token = $token; 79 | return $this; 80 | } 81 | 82 | /** 83 | * The number of seconds remaining, from the time it was requested, before the token will expire. 84 | * 85 | * @return int seconds 86 | */ 87 | public function getExpiresIn() 88 | { 89 | return $this->expiresAt - time(); 90 | } 91 | 92 | /** 93 | * Set token expiration time 94 | * 95 | * @param int $expiresIn amount of seconds before expiration 96 | * 97 | * @return AccessToken 98 | */ 99 | public function setExpiresIn($expiresIn) 100 | { 101 | $this->expiresAt = $expiresIn + time(); 102 | return $this; 103 | } 104 | 105 | /** 106 | * Dynamically typecast token object into string 107 | * 108 | * @return string 109 | */ 110 | public function __toString() 111 | { 112 | return $this->getToken(); 113 | } 114 | 115 | /** 116 | * Get Unix epoch time when token will expire 117 | * 118 | * @return int 119 | */ 120 | public function getExpiresAt() 121 | { 122 | return $this->expiresAt; 123 | } 124 | 125 | /** 126 | * Set Unix epoch time when token will expire 127 | * 128 | * @param int $expiresAt seconds, unix time 129 | * 130 | * @return AccessToken 131 | */ 132 | public function setExpiresAt($expiresAt) 133 | { 134 | $this->expiresAt = $expiresAt; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Convert API response into AccessToken 140 | * 141 | * @param \Psr\Http\Message\ResponseInterface $response 142 | * 143 | * @return self 144 | */ 145 | public static function fromResponse($response) 146 | { 147 | return static::fromResponseArray( 148 | Client::responseToArray($response) 149 | ); 150 | } 151 | 152 | /** 153 | * Instantiate access token object 154 | * 155 | * @param $responseArray 156 | * 157 | * @return \LinkedIn\AccessToken 158 | */ 159 | public static function fromResponseArray($responseArray) 160 | { 161 | if (!is_array($responseArray)) { 162 | throw new \InvalidArgumentException( 163 | 'Argument is not array' 164 | ); 165 | } 166 | if (!isset($responseArray['access_token'])) { 167 | throw new \InvalidArgumentException( 168 | 'Access token is not available' 169 | ); 170 | } 171 | if (!isset($responseArray['expires_in'])) { 172 | throw new \InvalidArgumentException( 173 | 'Access token expiration date is not specified' 174 | ); 175 | } 176 | return new static( 177 | $responseArray['access_token'], 178 | $responseArray['expires_in'] + time() 179 | ); 180 | } 181 | 182 | /** 183 | * Specify data format for json_encode() 184 | */ 185 | public function jsonSerialize() 186 | { 187 | return [ 188 | 'token' => $this->getToken(), 189 | 'expiresAt' => $this->getExpiresAt(), 190 | ]; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 18:50 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt 13 | * linkedin-client License 14 | * @version GIT: 1.0 15 | * @link http://www.zoonman.com/projects/linkedin-client/ 16 | */ 17 | 18 | namespace LinkedIn; 19 | 20 | use GuzzleHttp\Client as GuzzleClient; 21 | use GuzzleHttp\Exception\RequestException; 22 | use function GuzzleHttp\Psr7\build_query; 23 | use GuzzleHttp\Psr7\Uri; 24 | use LinkedIn\Http\Method; 25 | 26 | /** 27 | * Class Client 28 | * 29 | * @package LinkedIn 30 | */ 31 | class Client 32 | { 33 | 34 | /** 35 | * Grant type 36 | */ 37 | const OAUTH2_GRANT_TYPE = 'authorization_code'; 38 | 39 | /** 40 | * Response type 41 | */ 42 | const OAUTH2_RESPONSE_TYPE = 'code'; 43 | 44 | /** 45 | * Client Id 46 | * @var string 47 | */ 48 | protected $clientId; 49 | 50 | /** 51 | * Client Secret 52 | * @var string 53 | */ 54 | protected $clientSecret; 55 | 56 | /** 57 | * @var \LinkedIn\AccessToken 58 | */ 59 | protected $accessToken; 60 | 61 | /** 62 | * @var string 63 | */ 64 | protected $state; 65 | 66 | /** 67 | * @var string The URI your users will be sent back to after 68 | * authorization. This value must match one of 69 | * the defined OAuth 2.0 Redirect URLs in your 70 | * application configuration. 71 | */ 72 | protected $redirectUrl; 73 | 74 | /** 75 | * Default authorization URL 76 | * string 77 | */ 78 | const OAUTH2_API_ROOT = 'https://www.linkedin.com/oauth/v2/'; 79 | 80 | /** 81 | * Default API root URL 82 | * string 83 | */ 84 | const API_ROOT = 'https://api.linkedin.com/v2/'; 85 | 86 | /** 87 | * API Root URL 88 | * 89 | * @var string 90 | */ 91 | protected $apiRoot = self::API_ROOT; 92 | 93 | /** 94 | * OAuth API URL 95 | * 96 | * @var string 97 | */ 98 | protected $oAuthApiRoot = self::OAUTH2_API_ROOT; 99 | 100 | /** 101 | * Use oauth2_access_token parameter instead of Authorization header 102 | * 103 | * @var bool 104 | */ 105 | protected $useTokenParam = false; 106 | 107 | /** 108 | * @return bool 109 | */ 110 | public function isUsingTokenParam() 111 | { 112 | return $this->useTokenParam; 113 | } 114 | 115 | /** 116 | * @param bool $useTokenParam 117 | * 118 | * @return Client 119 | */ 120 | public function setUseTokenParam($useTokenParam) 121 | { 122 | $this->useTokenParam = $useTokenParam; 123 | return $this; 124 | } 125 | 126 | /** 127 | * List of default headers 128 | * 129 | * @var array 130 | */ 131 | protected $apiHeaders = [ 132 | 'Content-Type' => 'application/json', 133 | 'x-li-format' => 'json', 134 | ]; 135 | 136 | /** 137 | * Get list of headers 138 | * 139 | * @return array 140 | */ 141 | public function getApiHeaders() 142 | { 143 | return $this->apiHeaders; 144 | } 145 | 146 | /** 147 | * Set list of default headers 148 | * 149 | * @param array $apiHeaders 150 | * 151 | * @return Client 152 | */ 153 | public function setApiHeaders($apiHeaders) 154 | { 155 | $this->apiHeaders = $apiHeaders; 156 | return $this; 157 | } 158 | 159 | /** 160 | * Obtain API root URL 161 | * 162 | * @return string 163 | */ 164 | public function getApiRoot() 165 | { 166 | return $this->apiRoot; 167 | } 168 | 169 | /** 170 | * Specify API root URL 171 | * 172 | * @param string $apiRoot 173 | * 174 | * @return Client 175 | */ 176 | public function setApiRoot($apiRoot) 177 | { 178 | $this->apiRoot = $apiRoot; 179 | return $this; 180 | } 181 | 182 | /** 183 | * Get OAuth API root 184 | * 185 | * @return string 186 | */ 187 | public function getOAuthApiRoot() 188 | { 189 | return $this->oAuthApiRoot; 190 | } 191 | 192 | /** 193 | * Set OAuth API root 194 | * 195 | * @param string $oAuthApiRoot 196 | * 197 | * @return Client 198 | */ 199 | public function setOAuthApiRoot($oAuthApiRoot) 200 | { 201 | $this->oAuthApiRoot = $oAuthApiRoot; 202 | return $this; 203 | } 204 | 205 | /** 206 | * Client constructor. 207 | * 208 | * @param string $clientId 209 | * @param string $clientSecret 210 | */ 211 | public function __construct($clientId = '', $clientSecret = '') 212 | { 213 | !empty($clientId) && $this->setClientId($clientId); 214 | !empty($clientSecret) && $this->setClientSecret($clientSecret); 215 | } 216 | 217 | /** 218 | * Get ClientId 219 | * 220 | * @return string 221 | */ 222 | public function getClientId() 223 | { 224 | return $this->clientId; 225 | } 226 | 227 | /** 228 | * Set ClientId 229 | * 230 | * @param string $clientId 231 | * 232 | * @return Client 233 | */ 234 | public function setClientId($clientId) 235 | { 236 | $this->clientId = $clientId; 237 | return $this; 238 | } 239 | 240 | /** 241 | * Get Client Secret 242 | * 243 | * @return string 244 | */ 245 | public function getClientSecret() 246 | { 247 | return $this->clientSecret; 248 | } 249 | 250 | /** 251 | * Set Client Secret 252 | * 253 | * @param string $clientSecret 254 | * 255 | * @return Client 256 | */ 257 | public function setClientSecret($clientSecret) 258 | { 259 | $this->clientSecret = $clientSecret; 260 | return $this; 261 | } 262 | 263 | /** 264 | * Retrieve Access Token from LinkedIn if we have code provided. 265 | * If code is not provided, return current Access Token. 266 | * If current access token is not set, will return null 267 | * 268 | * @param string $code 269 | * 270 | * @return \LinkedIn\AccessToken|null 271 | * @throws \LinkedIn\Exception 272 | */ 273 | public function getAccessToken($code = '') 274 | { 275 | if (!empty($code)) { 276 | $uri = $this->buildUrl('accessToken', []); 277 | $guzzle = new GuzzleClient([ 278 | 'headers' => [ 279 | 'Content-Type' => 'application/json', 280 | 'x-li-format' => 'json', 281 | 'Connection' => 'Keep-Alive' 282 | ] 283 | ]); 284 | try { 285 | $response = $guzzle->post($uri, ['form_params' => [ 286 | 'grant_type' => self::OAUTH2_GRANT_TYPE, 287 | self::OAUTH2_RESPONSE_TYPE => $code, 288 | 'redirect_uri' => $this->getRedirectUrl(), 289 | 'client_id' => $this->getClientId(), 290 | 'client_secret' => $this->getClientSecret(), 291 | ]]); 292 | } catch (RequestException $exception) { 293 | throw Exception::fromRequestException($exception); 294 | } 295 | $this->setAccessToken( 296 | AccessToken::fromResponse($response) 297 | ); 298 | } 299 | return $this->accessToken; 300 | } 301 | 302 | /** 303 | * Convert API response into Array 304 | * 305 | * @param \Psr\Http\Message\ResponseInterface $response 306 | * 307 | * @return array 308 | */ 309 | public static function responseToArray($response) 310 | { 311 | return \GuzzleHttp\json_decode( 312 | $response->getBody()->getContents(), 313 | true 314 | ); 315 | } 316 | 317 | /** 318 | * Set AccessToken object 319 | * 320 | * @param AccessToken|string $accessToken 321 | * 322 | * @return Client 323 | */ 324 | public function setAccessToken($accessToken) 325 | { 326 | if (is_string($accessToken)) { 327 | $accessToken = new AccessToken($accessToken); 328 | } 329 | if (is_object($accessToken) && $accessToken instanceof AccessToken) { 330 | $this->accessToken = $accessToken; 331 | } else { 332 | throw new \InvalidArgumentException('$accessToken must be instance of \LinkedIn\AccessToken class'); 333 | } 334 | return $this; 335 | } 336 | 337 | /** 338 | * Retrieve current active scheme 339 | * 340 | * @return string 341 | */ 342 | protected function getCurrentScheme() 343 | { 344 | $scheme = 'http'; 345 | if (isset($_SERVER['HTTPS']) && "on" === $_SERVER["HTTPS"]) { 346 | $scheme = 'https'; 347 | } 348 | return $scheme; 349 | } 350 | 351 | /** 352 | * Get current URL 353 | * 354 | * @return string 355 | */ 356 | public function getCurrentUrl() 357 | { 358 | $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; 359 | $path = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; 360 | return $this->getCurrentScheme() . '://' . $host . $path; 361 | } 362 | 363 | /** 364 | * Get unique state or specified state 365 | * 366 | * @return string 367 | */ 368 | public function getState() 369 | { 370 | if (empty($this->state)) { 371 | $this->setState( 372 | rtrim( 373 | base64_encode(uniqid('', true)), 374 | '=' 375 | ) 376 | ); 377 | } 378 | return $this->state; 379 | } 380 | 381 | /** 382 | * Set State 383 | * 384 | * @param string $state 385 | * 386 | * @return Client 387 | */ 388 | public function setState($state) 389 | { 390 | $this->state = $state; 391 | return $this; 392 | } 393 | 394 | /** 395 | * Retrieve URL which will be used to send User to LinkedIn 396 | * for authentication 397 | * 398 | * @param array $scope Permissions that your application requires 399 | * 400 | * @return string 401 | */ 402 | public function getLoginUrl( 403 | array $scope = [Scope::READ_BASIC_PROFILE, Scope::READ_EMAIL_ADDRESS] 404 | ) { 405 | $params = [ 406 | 'response_type' => self::OAUTH2_RESPONSE_TYPE, 407 | 'client_id' => $this->getClientId(), 408 | 'redirect_uri' => $this->getRedirectUrl(), 409 | 'state' => $this->getState(), 410 | 'scope' => implode(' ', $scope), 411 | ]; 412 | $uri = $this->buildUrl('authorization', $params); 413 | return $uri; 414 | } 415 | 416 | /** 417 | * @return string The URI your users will be sent back to after 418 | * authorization. This value must match one of 419 | * the defined OAuth 2.0 Redirect URLs in your 420 | * application configuration. 421 | */ 422 | public function getRedirectUrl() 423 | { 424 | if (empty($this->redirectUrl)) { 425 | $this->setRedirectUrl($this->getCurrentUrl()); 426 | } 427 | return $this->redirectUrl; 428 | } 429 | 430 | /** 431 | * @param string $redirectUrl The URI your users will be sent back to after 432 | * authorization. This value must match one of 433 | * the defined OAuth 2.0 Redirect URLs in your 434 | * application configuration. 435 | * 436 | * @return Client 437 | */ 438 | public function setRedirectUrl($redirectUrl) 439 | { 440 | $redirectUrl = filter_var($redirectUrl, FILTER_VALIDATE_URL); 441 | if (false === $redirectUrl) { 442 | throw new \InvalidArgumentException('The argument is not an URL'); 443 | } 444 | $this->redirectUrl = $redirectUrl; 445 | return $this; 446 | } 447 | 448 | /** 449 | * @param string $endpoint 450 | * @param array $params 451 | * 452 | * @return string 453 | */ 454 | protected function buildUrl($endpoint, $params) 455 | { 456 | $url = $this->getOAuthApiRoot(); 457 | $scheme = parse_url($url, PHP_URL_SCHEME); 458 | $authority = parse_url($url, PHP_URL_HOST); 459 | $path = parse_url($url, PHP_URL_PATH); 460 | $path .= trim($endpoint, '/'); 461 | $fragment = ''; 462 | $uri = Uri::composeComponents( 463 | $scheme, 464 | $authority, 465 | $path, 466 | build_query($params), 467 | $fragment 468 | ); 469 | return $uri; 470 | } 471 | 472 | /** 473 | * Perform API call to LinkedIn 474 | * 475 | * @param string $endpoint 476 | * @param array $params 477 | * @param string $method 478 | * 479 | * @return array 480 | * @throws \LinkedIn\Exception 481 | */ 482 | public function api($endpoint, array $params = [], $method = Method::GET) 483 | { 484 | $headers = $this->getApiHeaders(); 485 | $options = $this->prepareOptions($params, $method); 486 | Method::isMethodSupported($method); 487 | if ($this->isUsingTokenParam()) { 488 | $params['oauth2_access_token'] = $this->accessToken->getToken(); 489 | } else { 490 | $headers['Authorization'] = 'Bearer ' . $this->accessToken->getToken(); 491 | } 492 | $guzzle = new GuzzleClient([ 493 | 'base_uri' => $this->getApiRoot(), 494 | 'headers' => $headers, 495 | ]); 496 | if (!empty($params) && Method::GET === $method) { 497 | $endpoint .= '?' . build_query($params); 498 | } 499 | try { 500 | $response = $guzzle->request($method, $endpoint, $options); 501 | } catch (RequestException $requestException) { 502 | throw Exception::fromRequestException($requestException); 503 | } 504 | return self::responseToArray($response); 505 | } 506 | 507 | /** 508 | * Make API call to LinkedIn using GET method 509 | * 510 | * @param string $endpoint 511 | * @param array $params 512 | * 513 | * @return array 514 | * @throws \LinkedIn\Exception 515 | */ 516 | public function get($endpoint, array $params = []) 517 | { 518 | return $this->api($endpoint, $params, Method::GET); 519 | } 520 | 521 | /** 522 | * Make API call to LinkedIn using POST method 523 | * 524 | * @param string $endpoint 525 | * @param array $params 526 | * 527 | * @return array 528 | * @throws \LinkedIn\Exception 529 | */ 530 | public function post($endpoint, array $params = []) 531 | { 532 | return $this->api($endpoint, $params, Method::POST); 533 | } 534 | 535 | /** 536 | * Make API call to LinkedIn using DELETE method 537 | * 538 | * @param string $endpoint 539 | * @param array $params 540 | * 541 | * @return array 542 | * @throws \LinkedIn\Exception 543 | */ 544 | public function delete($endpoint, array $params = []) 545 | { 546 | return $this->api($endpoint, $params, Method::DELETE); 547 | } 548 | 549 | /** 550 | * @param $path 551 | * @return array 552 | * @throws Exception 553 | */ 554 | public function upload($path) 555 | { 556 | $headers = $this->getApiHeaders(); 557 | unset($headers['Content-Type']); 558 | if (!$this->isUsingTokenParam()) { 559 | $headers['Authorization'] = 'Bearer ' . $this->accessToken->getToken(); 560 | } 561 | $guzzle = new GuzzleClient([ 562 | 'base_uri' => $this->getApiRoot() 563 | ]); 564 | $fileinfo = pathinfo($path); 565 | $filename = preg_replace('/\W+/', '_', $fileinfo['filename']); 566 | if (isset($fileinfo['extension'])) { 567 | $filename .= '.' . $fileinfo['extension']; 568 | } 569 | $options = [ 570 | 'multipart' => [ 571 | [ 572 | 'name' => 'source', 573 | 'filename' => $filename, 574 | 'contents' => fopen($path, 'r') 575 | ] 576 | ], 577 | 'headers' => $headers, 578 | ]; 579 | try { 580 | $response = $guzzle->request(Method::POST, 'media/upload', $options); 581 | } catch (RequestException $requestException) { 582 | throw Exception::fromRequestException($requestException); 583 | } 584 | return self::responseToArray($response); 585 | } 586 | 587 | /** 588 | * @param array $params 589 | * @param string $method 590 | * @return mixed 591 | */ 592 | protected function prepareOptions(array $params, $method) 593 | { 594 | $options = []; 595 | if ($method === Method::POST) { 596 | $options['body'] = \GuzzleHttp\json_encode($params); 597 | } 598 | return $options; 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 23:11 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | use GuzzleHttp\Exception\RequestException; 20 | 21 | /** 22 | * Class Exception 23 | * @package LinkedIn 24 | */ 25 | class Exception extends \Exception 26 | { 27 | /** 28 | * Error's description 29 | * 30 | * @var string 31 | */ 32 | protected $description; 33 | 34 | /** 35 | * Exception constructor. 36 | * @param string $message 37 | * @param int $code 38 | * @param null $previousException 39 | * @param $description 40 | */ 41 | public function __construct( 42 | $message = "", 43 | $code = 0, 44 | $previousException = null, 45 | $description = '' 46 | ) { 47 | parent::__construct($message, $code, $previousException); 48 | $this->description = $description; 49 | } 50 | 51 | /** 52 | * Get textual description that summarizes error. 53 | * 54 | * @return string 55 | */ 56 | public function getDescription() 57 | { 58 | return $this->description; 59 | } 60 | 61 | /** 62 | * @param RequestException $exception 63 | * 64 | * @return self 65 | */ 66 | public static function fromRequestException($exception) 67 | { 68 | return new static( 69 | $exception->getMessage(), 70 | $exception->getCode(), 71 | $exception, 72 | static::extractErrorDescription($exception) 73 | ); 74 | } 75 | 76 | /** 77 | * @param RequestException $exception 78 | * 79 | * @return null|string 80 | */ 81 | private static function extractErrorDescription($exception) 82 | { 83 | $response = $exception->getResponse(); 84 | if (!$response) { 85 | return null; 86 | } 87 | 88 | $json = Client::responseToArray($response); 89 | if (isset($json['error_description'])) { 90 | return $json['error_description']; 91 | } 92 | if (isset($json['message'])) { 93 | return $json['message']; 94 | } 95 | return null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Http/Method.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/22/17 09:15 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn\Http; 18 | 19 | use LinkedIn\AbstractEnum; 20 | 21 | class Method extends AbstractEnum 22 | { 23 | 24 | /** 25 | * 26 | */ 27 | const CONNECT = 'CONNECT'; 28 | 29 | /** 30 | * The GET method requests a representation of the specified resource. 31 | * Requests using GET should only retrieve data. 32 | */ 33 | const GET = 'GET'; 34 | 35 | /** 36 | * 37 | */ 38 | const HEAD = 'HEAD'; 39 | 40 | /** 41 | * 42 | */ 43 | const POST = 'POST'; 44 | 45 | /** 46 | * 47 | */ 48 | const PUT = 'PUT'; 49 | 50 | /** 51 | * 52 | */ 53 | const PATCH = 'PATCH'; 54 | 55 | /** 56 | * 57 | */ 58 | const OPTIONS = 'OPTIONS'; 59 | 60 | /** 61 | * 62 | */ 63 | const DELETE = 'DELETE'; 64 | 65 | /** 66 | * 67 | */ 68 | const TRACE = 'TRACE'; 69 | 70 | /** 71 | * @param $method 72 | */ 73 | public static function isMethodSupported($method) 74 | { 75 | if (!in_array($method, [Method::GET, Method::POST, Method::DELETE])) { 76 | throw new \InvalidArgumentException('The method is not correct'); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Scope.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/22/17 09:02 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | /** 20 | * Class Scope defines list of available permissions 21 | * 22 | * @package LinkedIn 23 | */ 24 | class Scope extends AbstractEnum 25 | { 26 | /** 27 | * Allows to read basic information about profile, such as name 28 | */ 29 | const READ_BASIC_PROFILE = 'r_basicprofile'; 30 | 31 | /** 32 | * Request a minimum information about the user 33 | * Use this scope when implementing "Sign In with LI" 34 | */ 35 | const READ_LITE_PROFILE = 'r_liteprofile'; 36 | 37 | const READ_FULL_PROFILE = 'r_fullprofile'; 38 | 39 | /** 40 | * Enables access to email address field 41 | */ 42 | const READ_EMAIL_ADDRESS = 'r_emailaddress'; 43 | 44 | /** 45 | * Manage and delete your data including your profile, posts, invitations, and messages 46 | */ 47 | const COMPLIANCE = 'w_compliance'; 48 | /** 49 | * Enables managing business company 50 | */ 51 | const MANAGE_COMPANY = 'rw_organization_admin'; 52 | /** 53 | * Post, comment and like posts on behalf of an organization. 54 | */ 55 | const SHARE_AS_ORGANIZATION = 'w_organization_social'; 56 | 57 | /** 58 | * Retrieve organizations' posts, comments, and likes. 59 | */ 60 | const READ_ORGANIZATION_SHARES = 'r_organization_social'; 61 | 62 | /** 63 | * Post, comment and like posts on behalf of an authenticated member. 64 | */ 65 | const SHARE_AS_USER = 'w_member_social'; 66 | 67 | /** 68 | * Restricted API! 69 | */ 70 | const READ_USER_CONTENT = 'r_member_social'; 71 | 72 | /** 73 | * Read and write access to ads. 74 | */ 75 | const ADS_MANAGEMENT = 'rw_ads'; 76 | const READ_ADS = 'r_ads'; 77 | const READ_LEADS = 'r_ads_leadgen_automation'; 78 | const READ_ADS_REPORTING = 'r_ads_reporting'; 79 | const READ_WRITE_DMP_SEGMENTS = 'rw_dmp_segments'; 80 | } 81 | -------------------------------------------------------------------------------- /tests/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/25/17 15:57 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | use PHPUnit\Framework\TestCase; 20 | 21 | /** 22 | * Class ClientTest 23 | * 24 | * @package LinkedIn 25 | */ 26 | class AccessTokenTest extends TestCase 27 | { 28 | /** 29 | * @dataProvider getValidResponseTestTable() 30 | * @param AccessToken $expectedToken 31 | * @param array $response 32 | */ 33 | public function testConstructorFromResponseArray($expectedToken, $response) 34 | { 35 | $token = AccessToken::fromResponseArray($response); 36 | $this->assertEquals($expectedToken->getToken(), $token->getToken()); 37 | } 38 | 39 | public function getValidResponseTestTable() 40 | { 41 | return [ 42 | [ 43 | 'expectedToken' => new AccessToken('test', 0), 44 | 'response' => [ 45 | 'access_token' => 'test', 46 | 'expires_in' => 0, 47 | ], 48 | ] 49 | ]; 50 | } 51 | 52 | /** 53 | * @dataProvider getInvalidResponseTestTable() 54 | * 55 | * @param string $exceptionClass 56 | * @param string $exceptionMessage 57 | * @param mixed $response 58 | */ 59 | public function testConstructorFromResponseArrayWithException($exceptionClass, $exceptionMessage, $response) 60 | { 61 | $this->setExpectedException($exceptionClass, $exceptionMessage); 62 | AccessToken::fromResponseArray($response); 63 | } 64 | 65 | public function getInvalidResponseTestTable() 66 | { 67 | return [ 68 | [ 69 | 'expectedException' => \InvalidArgumentException::class, 70 | 'exceptionMessage' => 'Argument is not array', 71 | 'response' => null, 72 | ], 73 | [ 74 | 'expectedException' => \InvalidArgumentException::class, 75 | 'exceptionMessage' => 'Access token is not available', 76 | 'response' => [], 77 | ], 78 | [ 79 | 'expectedException' => \InvalidArgumentException::class, 80 | 'exceptionMessage' => 'Access token is not available', 81 | 'response' => [ 82 | 'access_token' => null, 83 | ], 84 | ], 85 | [ 86 | 'expectedException' => \InvalidArgumentException::class, 87 | 'exceptionMessage' => 'Access token is not available', 88 | 'response' => [ 89 | 'expires_in' => 1, 90 | ], 91 | ], 92 | [ 93 | 'expectedException' => \InvalidArgumentException::class, 94 | 'exceptionMessage' => 'Access token expiration date is not specified', 95 | 'response' => [ 96 | 'access_token' => 'hello', 97 | ], 98 | ], 99 | [ 100 | 'expectedException' => \InvalidArgumentException::class, 101 | 'exceptionMessage' => 'Access token expiration date is not specified', 102 | 'response' => [ 103 | 'access_token' => 'hello', 104 | 'expires_in' => null, 105 | ], 106 | ], 107 | ]; 108 | } 109 | 110 | public function testToString() 111 | { 112 | $token = new AccessToken('hello', 1); 113 | $this->assertEquals('hello', (string) $token); 114 | } 115 | 116 | public function testJsonSerialize() 117 | { 118 | $token = new AccessToken('hello', 1); 119 | $this->assertEquals('{"token":"hello","expiresAt":1}', json_encode($token)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 19:57 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | namespace LinkedIn; 18 | 19 | /** 20 | * Class ClientTest 21 | * 22 | * @package LinkedIn 23 | */ 24 | class ClientTest extends \PHPUnit_Framework_TestCase 25 | { 26 | 27 | /** 28 | * @var \LinkedIn\Client 29 | */ 30 | public $client; 31 | 32 | /** 33 | * Setup test environment 34 | */ 35 | public function setUp() 36 | { 37 | $this->client = new Client( 38 | getenv('LINKEDIN_CLIENT_ID'), 39 | getenv('LINKEDIN_CLIENT_SECRET') 40 | ); 41 | } 42 | 43 | /** 44 | * Make sure, that user redirect gets prepared correctly 45 | */ 46 | public function testGetLoginUrl() 47 | { 48 | $actual = $this->client->getLoginUrl(); 49 | $this->assertNotEmpty($actual); 50 | } 51 | 52 | /** 53 | * Make sure that method LinkedIn\Client::setAccessToken() works correctly 54 | * 55 | * @param $token 56 | * @param AccessToken|null $expectedToken 57 | * @param \Exception|null $expectedException 58 | * 59 | * @dataProvider getSetAccessTokenTestTable 60 | */ 61 | public function testSetAccessToken($token, $expectedToken, $expectedException) 62 | { 63 | $client = new Client(); 64 | 65 | if ($expectedException !== null) { 66 | $this->setExpectedException(get_class($expectedException), $expectedException->getMessage()); 67 | } 68 | 69 | $client->setAccessToken($token); 70 | 71 | if ($expectedToken !== null) { 72 | $this->assertEquals($expectedToken->getToken(), $client->getAccessToken()->getToken()); 73 | } 74 | } 75 | 76 | public function getSetAccessTokenTestTable() 77 | { 78 | return [ 79 | [ 80 | 'token' => null, 81 | 'expectedToken' => null, 82 | 'expectedException' => new \InvalidArgumentException('$accessToken must be instance of \LinkedIn\AccessToken class'), 83 | ], 84 | 85 | [ 86 | 'token' => 'test token', 87 | 'expectedToken' => new AccessToken('test token'), 88 | 'expectedException' => null, 89 | ], 90 | 91 | [ 92 | 'token' => new AccessToken('hello world'), 93 | 'expectedToken' => new AccessToken('hello world'), 94 | 'expectedException' => null, 95 | ], 96 | 97 | [ 98 | 'token' => new \StdClass(), 99 | 'expectedToken' => null, 100 | 'expectedException' => new \InvalidArgumentException('$accessToken must be instance of \LinkedIn\AccessToken class'), 101 | ], 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 11 | * @date 8/17/17 22:12 12 | * @license http://www.zoonman.com/projects/linkedin-client/license.txt linkedin-client License 13 | * @version GIT: 1.0 14 | * @link http://www.zoonman.com/projects/linkedin-client/ 15 | */ 16 | 17 | $pathToDotEnvFile = dirname(__DIR__); 18 | if (file_exists($pathToDotEnvFile . '/.env')) { 19 | $dotenv = new Dotenv\Dotenv($pathToDotEnvFile); 20 | $dotenv->load(); 21 | } elseif (empty(getenv('LINKEDIN_CLIENT_ID')) || empty(getenv('LINKEDIN_CLIENT_SECRET'))) { 22 | echo "Create .env file with credentials or setup environment variables LINKEDIN_CLIENT_ID & LINKEDIN_CLIENT_SECRET to make tests pass."; 23 | } 24 | --------------------------------------------------------------------------------