├── yaml ├── warning.yml ├── deleted_tweet.yml ├── profile.yml ├── tweet.yml └── user.yml ├── config.sample.php ├── .editorconfig ├── src ├── Resource │ ├── Async │ │ ├── EmptyUser.php │ │ ├── EmptyTweet.php │ │ ├── EmptyProfile.php │ │ ├── EmptyWarning.php │ │ ├── EmptyDeletedTweet.php │ │ ├── User.php │ │ ├── Tweet.php │ │ ├── Warning.php │ │ ├── DeletedTweet.php │ │ └── Profile.php │ ├── Sync │ │ ├── EmptyUser.php │ │ ├── EmptyTweet.php │ │ ├── EmptyProfile.php │ │ ├── EmptyWarning.php │ │ ├── EmptyDeletedTweet.php │ │ ├── User.php │ │ ├── Tweet.php │ │ ├── DeletedTweet.php │ │ ├── Warning.php │ │ └── Profile.php │ ├── WarningInterface.php │ ├── DeletedTweetInterface.php │ ├── EmptyWarning.php │ ├── EmptyDeletedTweet.php │ ├── Warning.php │ ├── DeletedTweet.php │ ├── ProfileInterface.php │ ├── TweetInterface.php │ ├── EmptyProfile.php │ ├── EmptyTweet.php │ ├── Profile.php │ ├── UserInterface.php │ ├── Tweet.php │ ├── EmptyUser.php │ └── User.php ├── StreamingClientInterface.php ├── AsyncStreamingClientInterface.php ├── ClientInterface.php ├── AsyncClientInterface.php ├── ApiSettings.php ├── StreamingClient.php ├── AsyncStreamingClient.php ├── Client.php └── AsyncClient.php ├── resources.yml ├── .php_cs ├── Makefile ├── LICENSE ├── CONTRIBUTING.md ├── appveyor.yml ├── README.md └── composer.json /yaml/warning.yml: -------------------------------------------------------------------------------- 1 | class: Warning 2 | properties: 3 | status: array 4 | timestamp_ms: string 5 | -------------------------------------------------------------------------------- /yaml/deleted_tweet.yml: -------------------------------------------------------------------------------- 1 | class: DeletedTweet 2 | properties: 3 | status: array 4 | timestamp_ms: string 5 | -------------------------------------------------------------------------------- /yaml/profile.yml: -------------------------------------------------------------------------------- 1 | class: Profile 2 | properties: 3 | id: int 4 | id_str: string 5 | name: string 6 | screen_name: string 7 | location: string 8 | profile_location: string 9 | description: string 10 | url: string 11 | -------------------------------------------------------------------------------- /config.sample.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'key' => '', 6 | 'secret' => '', 7 | ], 8 | 'access_token' => [ 9 | 'token' => '', 10 | 'secret' => '', 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /src/Resource/Async/EmptyUser.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource/Sync/User.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource/Sync/Tweet.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource/Async/Tweet.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource/Async/Warning.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource/Sync/DeletedTweet.php: -------------------------------------------------------------------------------- 1 | wait($this->callAsync('refresh')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setFinder( 11 | PhpCsFixer\Finder::create() 12 | ->in($path) 13 | ->append([$path]) 14 | ) 15 | ->setUsingCache(false) 16 | ; 17 | })(); 18 | -------------------------------------------------------------------------------- /src/Resource/WarningInterface.php: -------------------------------------------------------------------------------- 1 | wait( 14 | $this->handleCommand( 15 | new BuildAsyncFromSyncCommand(self::HYDRATE_CLASS, $this) 16 | )->then( 17 | function (WarningInterface $warning) { 18 | return $warning->refresh(); 19 | } 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Resource/Warning.php: -------------------------------------------------------------------------------- 1 | status; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function timestampMs(): string 35 | { 36 | return $this->timestamp_ms; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Resource/DeletedTweet.php: -------------------------------------------------------------------------------- 1 | status; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function timestampMs(): string 35 | { 36 | return $this->timestamp_ms; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cees-Jan Kiewiet 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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are highly appreciated. Here's a quick guide. 4 | 5 | Fork, then clone the repo: 6 | 7 | git clone git@github.com:your-username/twitter.git 8 | 9 | Set up your machine: 10 | 11 | composer install 12 | 13 | Make sure the tests pass: 14 | 15 | make unit 16 | 17 | Make sure the tests pass on all supported PHP versions (requires docker): 18 | 19 | make dunit 20 | 21 | Make your change. Add tests for your change. Make the tests pass: 22 | 23 | make dunit && make unit 24 | 25 | Before committing and submitting your pull request make sure it passes PSR2 coding style, unit tests pass and pass on all supported PHP versions: 26 | 27 | make contrib 28 | 29 | Push to your fork and [submit a pull request][pr]. 30 | 31 | [pr]: https://help.github.com/articles/creating-a-pull-request/ 32 | 33 | At this point you're waiting on me. I like to at least comment on pull requests 34 | within a day or two. I may suggest some changes or improvements or alternatives. 35 | 36 | Some things that will increase the chance that your pull request is accepted: 37 | 38 | * Write tests. 39 | * Follow PSR2 (travis will also check for this). 40 | * Write a [good commit message][commit]. 41 | 42 | [commit]: http://chris.beams.io/posts/git-commit/ 43 | -------------------------------------------------------------------------------- /src/Resource/Async/Profile.php: -------------------------------------------------------------------------------- 1 | changedFields as $field) { 19 | $fields[$field] = $this->$field; 20 | } 21 | 22 | $uri = 'account/update_profile.json?' . http_build_query($fields); 23 | 24 | return $this->handleCommand(new RequestCommand( 25 | new Request('POST', $uri) 26 | ))->then(function (ResponseInterface $response) { 27 | return resolve($this->handleCommand(new HydrateCommand('Profile', $response->getBody()->getParsedContents()))); 28 | }); 29 | } 30 | 31 | public function refresh(): Profile 32 | { 33 | throw new \Exception('TODO: create refresh method!'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /yaml/user.yml: -------------------------------------------------------------------------------- 1 | class: User 2 | properties: 3 | id: int 4 | id_str: string 5 | name: string 6 | screen_name: string 7 | location: string 8 | profile_location: string 9 | description: string 10 | url: string 11 | #entities: array 12 | protected: bool 13 | followers_count: int 14 | friends_count: int 15 | listed_count: int 16 | created_at: DateTime 17 | favourites_count: int 18 | utc_offset: int 19 | time_zone: string 20 | geo_enabled: bool 21 | verified: bool 22 | statuses_count: int 23 | lang: string 24 | status: array 25 | # type: Tweet 26 | # annotations: 27 | # nested: Tweet 28 | contributors_enabled: bool 29 | is_translator: bool 30 | is_translator_enabled: bool 31 | profile_background_color: string 32 | profile_background_image_url: string 33 | profile_background_image_url_https: string 34 | profile_background_tile: bool 35 | profile_image_url: string 36 | profile_image_url_https: string 37 | profile_banner_url: string 38 | profile_link_color: string 39 | profile_sidebar_border_color: string 40 | profile_sidebar_fill_color: string 41 | profile_text_color: string 42 | profile_use_background_image: bool 43 | has_extended_profile: bool 44 | default_profile: bool 45 | default_profile_image: bool 46 | following: bool 47 | follow_request_sent: bool 48 | notifications: bool 49 | #translator_type: string 50 | -------------------------------------------------------------------------------- /src/Resource/Sync/Profile.php: -------------------------------------------------------------------------------- 1 | wait( 16 | $this->handleCommand( 17 | new BuildAsyncFromSyncCommand(self::HYDRATE_CLASS, $this) 18 | )->then(function (AsyncProfile $profile) { 19 | return $profile->putProfile(); 20 | })->then(function (Profile $profile) { 21 | return $this->handleCommand(new BuildSyncFromAsyncCommand(self::HYDRATE_CLASS, $profile)); 22 | }) 23 | ); 24 | } 25 | 26 | public function refresh(): Profile 27 | { 28 | return $this->wait( 29 | $this->handleCommand( 30 | new BuildAsyncFromSyncCommand(self::HYDRATE_CLASS, $this) 31 | )->then(function (ProfileInterface $profile) { 32 | return $profile->refresh(); 33 | }) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resource/ProfileInterface.php: -------------------------------------------------------------------------------- 1 | composer.lock' 25 | 26 | ## Set up environment varriables 27 | init: 28 | - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% 29 | - SET COMPOSER_NO_INTERACTION=1 30 | - SET PHP=1 31 | - SET ANSICON=121x90 (121x90) 32 | 33 | ## Install PHP and composer, and run the appropriate composer command 34 | install: 35 | - IF EXIST c:\tools\php (SET PHP=0) 36 | - ps: appveyor-retry cinst -y php --version ((choco search php --exact --all-versions -r | select-string -pattern $Env:php_ver_target | Select-Object -first 1) -replace '[php|]','') 37 | - cd c:\tools\php 38 | - IF %PHP%==1 copy php.ini-production php.ini /Y 39 | - IF %PHP%==1 echo date.timezone="UTC" >> php.ini 40 | - IF %PHP%==1 echo extension_dir=ext >> php.ini 41 | - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini 42 | - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini 43 | - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini 44 | - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat 45 | - appveyor-retry appveyor DownloadFile https://getcomposer.org/composer.phar 46 | - cd c:\projects\php-project-workspace 47 | - composer config --unset platform.php 48 | - IF %dependencies%==lowest appveyor-retry composer update --prefer-lowest --no-progress --profile -n 49 | - IF %dependencies%==current appveyor-retry composer install --no-progress --profile 50 | - IF %dependencies%==highest appveyor-retry composer update --no-progress --profile -n 51 | #- composer show 52 | 53 | ## Run the actual test 54 | test_script: 55 | - cd c:\projects\php-project-workspace 56 | - vendor/bin/phpunit -c phpunit.xml.dist 57 | -------------------------------------------------------------------------------- /src/Resource/EmptyProfile.php: -------------------------------------------------------------------------------- 1 | [ 22 | HydratorOptions::NAMESPACE => self::NAMESPACE, 23 | HydratorOptions::NAMESPACE_DIR => __DIR__ . DIRECTORY_SEPARATOR . 'Resource' . DIRECTORY_SEPARATOR, 24 | ], 25 | Options::TRANSPORT_OPTIONS => [ 26 | TransportOptions::HOST => 'api.twitter.com', 27 | TransportOptions::PATH => '/1.1/', 28 | TransportOptions::MIDDLEWARE => [ 29 | Oauth1Middleware::class, 30 | JsonDecodeMiddleware::class, 31 | UserAgentMiddleware::class, 32 | ], 33 | TransportOptions::DEFAULT_REQUEST_OPTIONS => [ 34 | UserAgentMiddleware::class => [ 35 | UserAgentMiddlewareOptions::STRATEGY => UserAgentStrategies::PACKAGE_VERSION, 36 | UserAgentMiddlewareOptions::PACKAGE => 'api-clients/twitter', 37 | ], 38 | ], 39 | ], 40 | ]; 41 | 42 | public static function getOptions( 43 | string $consumerKey, 44 | string $consumerSecret, 45 | string $suffix, 46 | array $suppliedOptions = [] 47 | ): array { 48 | // @codingStandardsIgnoreStart 49 | $options = array_replace_recursive(self::TRANSPORT_OPTIONS, $suppliedOptions); 50 | $options[Options::HYDRATOR_OPTIONS][HydratorOptions::NAMESPACE_SUFFIX] = $suffix; 51 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::CONSUMER_KEY] = new Definition\ConsumerKey($consumerKey); 52 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::CONSUMER_SECRET] = new Definition\ConsumerSecret($consumerSecret); 53 | // @codingStandardsIgnoreEnd 54 | return $options; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/StreamingClient.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 47 | $this->commandBus = $commandBus; 48 | $this->client = $client; 49 | } 50 | 51 | public function sample(callable $listener) 52 | { 53 | $this->stream($this->client->sample(), $listener); 54 | } 55 | 56 | public function filtered(callable $listener, array $filter = []) 57 | { 58 | $this->stream($this->client->filtered($filter), $listener); 59 | } 60 | 61 | protected function stream(ObservableInterface $observable, callable $listener) 62 | { 63 | $observable->flatMap(function (ResourceInterface $resource) { 64 | return Promise::toObservable( 65 | $this->commandBus->handle( 66 | new BuildSyncFromAsyncCommand( 67 | $this->loopUpHydrateClassConstant($resource), 68 | $resource 69 | ) 70 | ) 71 | ); 72 | })->subscribe( 73 | $listener, 74 | function ($error) { 75 | throw $error; 76 | } 77 | ); 78 | $this->loop->run(); 79 | } 80 | 81 | protected function loopUpHydrateClassConstant(ResourceInterface $resource) 82 | { 83 | $class = get_class($resource); 84 | if (!isset($this->hydrateClassConstantCache[$class])) { 85 | $this->hydrateClassConstantCache[$class] = (new ReflectionObject($resource))->getConstant('HYDRATE_CLASS'); 86 | } 87 | 88 | return $this->hydrateClassConstantCache[$class]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Resource/EmptyTweet.php: -------------------------------------------------------------------------------- 1 | withAccessToken( 38 | $config['access_token']['token'], 39 | $config['access_token']['secret'] 40 | ); 41 | 42 | $client->user('php_api_clients')->done(function (UserInterface $user) { 43 | resource_pretty_print($user); 44 | }); 45 | 46 | $loop->run(); 47 | ``` 48 | 49 | # License 50 | 51 | The MIT License (MIT) 52 | 53 | Copyright (c) 2018 Cees-Jan Kiewiet 54 | 55 | Permission is hereby granted, free of charge, to any person obtaining a copy 56 | of this software and associated documentation files (the "Software"), to deal 57 | in the Software without restriction, including without limitation the rights 58 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 59 | copies of the Software, and to permit persons to whom the Software is 60 | furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in all 63 | copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 66 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 67 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 68 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 69 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 70 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 71 | SOFTWARE. 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-clients/twitter", 3 | "description": "Async first twitter client", 4 | "homepage": "https://php-api-clients.org/clients/twitter/", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Cees-Jan Kiewiet", 9 | "email": "ceesjank@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.0", 14 | "api-clients/client-services": "^1.4", 15 | "api-clients/foundation": "^1.0", 16 | "api-clients/middleware-http-exceptions": "^2.0", 17 | "api-clients/middleware-json": "^3.1", 18 | "api-clients/middleware-oauth1": "^4.0", 19 | "api-clients/middleware-user-agent": "^2.0", 20 | "api-clients/rx": "^2.2", 21 | "api-clients/rx-operators": "^2.0", 22 | "react/http-client": ">=0.4.17" 23 | }, 24 | "require-dev": { 25 | "api-clients/resource-generator": "^1.0", 26 | "api-clients/resource-test-utilities": "^1.0", 27 | "api-clients/test-utilities": "^4.3" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "ApiClients\\Client\\Twitter\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "ApiClients\\Tests\\Client\\Twitter\\": "tests/" 37 | } 38 | }, 39 | "config": { 40 | "sort-packages": true, 41 | "platform": { 42 | "php": "7.0" 43 | } 44 | }, 45 | "scripts": { 46 | "ensure-installed": "composer install --ansi -n -q", 47 | "cs": [ 48 | "@ensure-installed", 49 | "php-cs-fixer fix --config=.php_cs --ansi --dry-run --diff --verbose --allow-risky=yes --show-progress=estimating" 50 | ], 51 | "cs-fix": [ 52 | "@ensure-installed", 53 | "php-cs-fixer fix --config=.php_cs --ansi --verbose --allow-risky=yes --show-progress=estimating" 54 | ], 55 | "unit": [ 56 | "@ensure-installed", 57 | "phpunit --colors=always -c phpunit.xml.dist" 58 | ], 59 | "unit-coverage": [ 60 | "@ensure-installed", 61 | "phpunit --colors=always -c phpunit.xml.dist --coverage-text --coverage-html covHtml --coverage-clover ./build/logs/clover.xml" 62 | ], 63 | "lint-php": [ 64 | "@ensure-installed", 65 | "parallel-lint --exclude vendor ." 66 | ], 67 | "qa-all": [ 68 | "@lint-php", 69 | "@cs", 70 | "@unit" 71 | ], 72 | "qa-all-coverage": [ 73 | "@lint-php", 74 | "@cs", 75 | "@unit-coverage" 76 | ], 77 | "qa-windows": [ 78 | "@lint-php", 79 | "@cs", 80 | "@unit" 81 | ], 82 | "qa-ci": [ 83 | "@unit" 84 | ], 85 | "qa-ci-extended": [ 86 | "@qa-all-coverage" 87 | ], 88 | "qa-ci-windows": [ 89 | "@qa-windows" 90 | ], 91 | "qa-contrib": [ 92 | "@qa-all" 93 | ], 94 | "ci-coverage": [ 95 | "if [ -f ./build/logs/clover.xml ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover ./build/logs/clover.xml; fi" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/AsyncStreamingClient.php: -------------------------------------------------------------------------------- 1 | client = $client; 38 | } 39 | 40 | public function sample(): Observable 41 | { 42 | return $this->stream( 43 | new Request('GET', 'https://stream.twitter.com/1.1/statuses/sample.json') 44 | ); 45 | } 46 | 47 | public function filtered(array $filter = []): Observable 48 | { 49 | $postData = http_build_query($filter); 50 | 51 | return $this->stream( 52 | new Request( 53 | 'POST', 54 | 'https://stream.twitter.com/1.1/statuses/filter.json', 55 | [ 56 | 'Content-Type' => 'application/x-www-form-urlencoded', 57 | 'Content-Length' => strlen($postData), 58 | ], 59 | $postData 60 | ) 61 | ); 62 | } 63 | 64 | public function searchTweets(array $filter = []): Observable 65 | { 66 | $query = build_query($filter); 67 | 68 | return Promise::toObservable($this->client->handle(new RequestCommand( 69 | new Request( 70 | 'GET', 71 | 'https://api.twitter.com/1.1/search/tweets.json?' . $query, 72 | [] 73 | ) 74 | ))->then(function (ResponseInterface $response) { 75 | /** @var ParsedContentsInterface $body */ 76 | $body = $response->getBody(); 77 | 78 | return $body->getParsedContents()['statuses']; 79 | }))->flatMap(function (array $statuses) { 80 | return observableFromArray($statuses); 81 | })->flatMap(function (array $document) { 82 | return Promise::toObservable($this->client->handle(new HydrateCommand('Tweet', $document))); 83 | }); 84 | } 85 | 86 | protected function stream(RequestInterface $request): Observable 87 | { 88 | return Promise::toObservable($this->client->handle(new StreamingRequestCommand( 89 | $request 90 | )))->switchLatest()->lift(function () { 91 | return new CutOperator(self::STREAM_DELIMITER, new ImmediateScheduler()); 92 | })->filter(function (string $json) { 93 | return trim($json) !== ''; // To keep the stream alive Twitter sends an empty line at times 94 | })->_ApiClients_jsonDecode()->flatMap(function (array $document) { 95 | if (isset($document['delete'])) { 96 | return Promise::toObservable($this->client->handle( 97 | new HydrateCommand('DeletedTweet', $document['delete']) 98 | )); 99 | } 100 | 101 | return Promise::toObservable($this->client->handle(new HydrateCommand('Tweet', $document))); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Resource/Profile.php: -------------------------------------------------------------------------------- 1 | id; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function idStr(): string 70 | { 71 | return $this->id_str; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function name(): string 78 | { 79 | return $this->name; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function screenName(): string 86 | { 87 | return $this->screen_name; 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function location(): string 94 | { 95 | return $this->location; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function profileLocation(): string 102 | { 103 | return $this->profile_location; 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function description(): string 110 | { 111 | return $this->description; 112 | } 113 | 114 | /** 115 | * @return string 116 | */ 117 | public function url(): string 118 | { 119 | return $this->url; 120 | } 121 | 122 | /** 123 | * @param string $name 124 | * @return ProfileInterface 125 | */ 126 | public function withName(string $name): ProfileInterface 127 | { 128 | $clone = clone $this; 129 | $clone->name = $name; 130 | $clone->changedFields['name'] = 'name'; 131 | 132 | return $clone; 133 | } 134 | 135 | /** 136 | * @param string $location 137 | * @return ProfileInterface 138 | */ 139 | public function withLocation(string $location): ProfileInterface 140 | { 141 | $clone = clone $this; 142 | $clone->location = $location; 143 | $clone->changedFields['location'] = 'location'; 144 | 145 | return $clone; 146 | } 147 | 148 | /** 149 | * @param string $description 150 | * @return ProfileInterface 151 | */ 152 | public function withDescription(string $description): ProfileInterface 153 | { 154 | $clone = clone $this; 155 | $clone->description = $description; 156 | $clone->changedFields['description'] = 'description'; 157 | 158 | return $clone; 159 | } 160 | 161 | /** 162 | * @param string $url 163 | * @return ProfileInterface 164 | */ 165 | public function withUrl(string $url): ProfileInterface 166 | { 167 | $clone = clone $this; 168 | $clone->url = $url; 169 | $clone->changedFields['url'] = 'url'; 170 | 171 | return $clone; 172 | } 173 | 174 | public function putProfile() 175 | { 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Resource/UserInterface.php: -------------------------------------------------------------------------------- 1 | favorited; 109 | } 110 | 111 | /** 112 | * @return bool 113 | */ 114 | public function truncated(): bool 115 | { 116 | return $this->truncated; 117 | } 118 | 119 | /** 120 | * @return DateTime 121 | */ 122 | public function createdAt(): DateTime 123 | { 124 | return $this->created_at; 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | public function idStr(): string 131 | { 132 | return $this->id_str; 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | public function inReplyToUserIdStr(): string 139 | { 140 | return $this->in_reply_to_user_id_str; 141 | } 142 | 143 | /** 144 | * @return array 145 | */ 146 | public function contributors(): array 147 | { 148 | return $this->contributors; 149 | } 150 | 151 | /** 152 | * @return string 153 | */ 154 | public function text(): string 155 | { 156 | return $this->text; 157 | } 158 | 159 | /** 160 | * @return int 161 | */ 162 | public function retweetCount(): int 163 | { 164 | return $this->retweet_count; 165 | } 166 | 167 | /** 168 | * @return string 169 | */ 170 | public function inReplyToStatusIdStr(): string 171 | { 172 | return $this->in_reply_to_status_id_str; 173 | } 174 | 175 | /** 176 | * @return int 177 | */ 178 | public function id(): int 179 | { 180 | return $this->id; 181 | } 182 | 183 | /** 184 | * @return bool 185 | */ 186 | public function retweeted(): bool 187 | { 188 | return $this->retweeted; 189 | } 190 | 191 | /** 192 | * @return bool 193 | */ 194 | public function possiblySensitive(): bool 195 | { 196 | return $this->possibly_sensitive; 197 | } 198 | 199 | /** 200 | * @return int 201 | */ 202 | public function inReplyToUserId(): int 203 | { 204 | return $this->in_reply_to_user_id; 205 | } 206 | 207 | /** 208 | * @return User 209 | */ 210 | public function user(): User 211 | { 212 | return $this->user; 213 | } 214 | 215 | /** 216 | * @return string 217 | */ 218 | public function inReplyToScreenName(): string 219 | { 220 | return $this->in_reply_to_screen_name; 221 | } 222 | 223 | /** 224 | * @return string 225 | */ 226 | public function source(): string 227 | { 228 | return $this->source; 229 | } 230 | 231 | /** 232 | * @return int 233 | */ 234 | public function inReplyToStatusId(): int 235 | { 236 | return $this->in_reply_to_status_id; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | consumerKey = $consumerKey; 63 | $this->consumerSecret = $consumerSecret; 64 | $this->loop = LoopFactory::create(); 65 | 66 | $this->options = ApiSettings::getOptions( 67 | $consumerKey, 68 | $consumerSecret, 69 | 'Sync' 70 | ); 71 | 72 | $this->client = Factory::create($this->loop, $this->options); 73 | 74 | $this->asyncClient = new AsyncClient($consumerKey, $consumerSecret, $this->loop, [], $this->client); 75 | } 76 | 77 | public function withAccessToken(string $accessToken, string $accessTokenSecret): Client 78 | { 79 | $options = $this->options; 80 | // @codingStandardsIgnoreStart 81 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN] = new Definition\AccessToken($accessToken); 82 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET] = new Definition\TokenSecret($accessTokenSecret); 83 | // @codingStandardsIgnoreEnd 84 | 85 | $clone = clone $this; 86 | $clone->client = Factory::create($this->loop, $options); 87 | $clone->asyncClient = (new AsyncClient( 88 | $this->consumerKey, 89 | $this->consumerSecret, 90 | $this->loop, 91 | [], 92 | $this->client 93 | ))->withAccessToken($accessToken, $accessTokenSecret); 94 | 95 | return $clone; 96 | } 97 | 98 | public function withOutAccessToken(): Client 99 | { 100 | $options = $this->options; 101 | // @codingStandardsIgnoreStart 102 | if (isset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN])) { 103 | unset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN]); 104 | } 105 | if (isset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET])) { 106 | unset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET]); 107 | } 108 | // @codingStandardsIgnoreEnd 109 | 110 | $clone = clone $this; 111 | $clone->client = Factory::create($this->loop, $options); 112 | $clone->asyncClient = (new AsyncClient( 113 | $this->consumerKey, 114 | $this->consumerSecret, 115 | $this->loop, 116 | [], 117 | $this->client 118 | )); 119 | 120 | return $clone; 121 | } 122 | 123 | public function stream(): StreamingClient 124 | { 125 | if (!($this->streamingClient instanceof StreamingClient)) { 126 | $this->streamingClient = new StreamingClient( 127 | $this->loop, 128 | $this->asyncClient->getCommandBus(), 129 | $this->asyncClient->stream() 130 | ); 131 | } 132 | 133 | return $this->streamingClient; 134 | } 135 | 136 | public function tweet(string $tweet): TweetInterface 137 | { 138 | return await( 139 | $this->asyncClient->tweet($tweet), 140 | $this->loop 141 | ); 142 | } 143 | 144 | public function profile(): ProfileInterface 145 | { 146 | return await( 147 | $this->asyncClient->profile()->then(function (Profile $profile) { 148 | return $this->client->handle(new BuildSyncFromAsyncCommand(ProfileInterface::HYDRATE_CLASS, $profile)); 149 | }), 150 | $this->loop 151 | ); 152 | } 153 | 154 | public function user(string $tweet): UserInterface 155 | { 156 | return await( 157 | $this->asyncClient->user($tweet), 158 | $this->loop 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/AsyncClient.php: -------------------------------------------------------------------------------- 1 | consumerKey = $consumerKey; 69 | $this->consumerSecret = $consumerSecret; 70 | $this->loop = $loop; 71 | 72 | if (!($client instanceof Client)) { 73 | $this->options = ApiSettings::getOptions( 74 | $consumerKey, 75 | $consumerSecret, 76 | 'Async', 77 | $options 78 | ); 79 | 80 | $client = Factory::create($this->loop, $this->options); 81 | } 82 | 83 | $this->client = $client; 84 | } 85 | 86 | public function withAccessToken(string $accessToken, string $accessTokenSecret): AsyncClient 87 | { 88 | $options = $this->options; 89 | // @codingStandardsIgnoreStart 90 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN] = new Definition\AccessToken($accessToken); 91 | $options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET] = new Definition\TokenSecret($accessTokenSecret); 92 | // @codingStandardsIgnoreEnd 93 | 94 | return new self( 95 | $this->consumerKey, 96 | $this->consumerSecret, 97 | $this->loop, 98 | $options 99 | ); 100 | } 101 | 102 | public function withOutAccessToken(): AsyncClient 103 | { 104 | $options = $this->options; 105 | // @codingStandardsIgnoreStart 106 | if (isset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN])) { 107 | unset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::ACCESS_TOKEN]); 108 | } 109 | if (isset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET])) { 110 | unset($options[Options::TRANSPORT_OPTIONS][TransportOptions::DEFAULT_REQUEST_OPTIONS][Oauth1Middleware::class][Oauth1Options::TOKEN_SECRET]); 111 | } 112 | // @codingStandardsIgnoreEnd 113 | 114 | return new self( 115 | $this->consumerKey, 116 | $this->consumerSecret, 117 | $this->loop, 118 | $options 119 | ); 120 | } 121 | 122 | public function getCommandBus(): CommandBusInterface 123 | { 124 | return $this->client->getFromContainer(CommandBusInterface::class); 125 | } 126 | 127 | public function profile(): PromiseInterface 128 | { 129 | return $this->client->handle(new RequestCommand( 130 | new Request('GET', 'account/verify_credentials.json') 131 | ))->then(function (ResponseInterface $response) { 132 | return resolve($this->client->handle(new HydrateCommand('Profile', $response->getBody()->getParsedContents()))); 133 | }); 134 | } 135 | 136 | public function user(string $user): PromiseInterface 137 | { 138 | return $this->client->handle(new RequestCommand( 139 | new Request('GET', 'users/show.json?screen_name=' . $user) 140 | ))->then(function (ResponseInterface $response) { 141 | return resolve($this->client->handle(new HydrateCommand('User', $response->getBody()->getParsedContents()))); 142 | }); 143 | } 144 | 145 | public function tweet(string $status, array $tweet = []): PromiseInterface 146 | { 147 | $tweet['status'] = $status; 148 | 149 | return $this->client->handle(new RequestCommand( 150 | new Request('POST', 'statuses/update.json?' . http_build_query($tweet)) 151 | ))->then(function (ResponseInterface $response) { 152 | return resolve($this->client->handle(new HydrateCommand('Tweet', $response->getBody()->getParsedContents()))); 153 | }); 154 | } 155 | 156 | public function stream(): AsyncStreamingClientInterface 157 | { 158 | if (!($this->streamingClient instanceof AsyncStreamingClient)) { 159 | $this->streamingClient = new AsyncStreamingClient($this->client); 160 | } 161 | 162 | return $this->streamingClient; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Resource/EmptyUser.php: -------------------------------------------------------------------------------- 1 | id; 230 | } 231 | 232 | /** 233 | * @return string 234 | */ 235 | public function idStr(): string 236 | { 237 | return $this->id_str; 238 | } 239 | 240 | /** 241 | * @return string 242 | */ 243 | public function name(): string 244 | { 245 | return $this->name; 246 | } 247 | 248 | /** 249 | * @return string 250 | */ 251 | public function screenName(): string 252 | { 253 | return $this->screen_name; 254 | } 255 | 256 | /** 257 | * @return string 258 | */ 259 | public function location(): string 260 | { 261 | return $this->location; 262 | } 263 | 264 | /** 265 | * @return string 266 | */ 267 | public function profileLocation(): string 268 | { 269 | return $this->profile_location; 270 | } 271 | 272 | /** 273 | * @return string 274 | */ 275 | public function description(): string 276 | { 277 | return $this->description; 278 | } 279 | 280 | /** 281 | * @return string 282 | */ 283 | public function url(): string 284 | { 285 | return $this->url; 286 | } 287 | 288 | /** 289 | * @return bool 290 | */ 291 | public function protected(): bool 292 | { 293 | return $this->protected; 294 | } 295 | 296 | /** 297 | * @return int 298 | */ 299 | public function followersCount(): int 300 | { 301 | return $this->followers_count; 302 | } 303 | 304 | /** 305 | * @return int 306 | */ 307 | public function friendsCount(): int 308 | { 309 | return $this->friends_count; 310 | } 311 | 312 | /** 313 | * @return int 314 | */ 315 | public function listedCount(): int 316 | { 317 | return $this->listed_count; 318 | } 319 | 320 | /** 321 | * @return DateTime 322 | */ 323 | public function createdAt(): DateTime 324 | { 325 | return $this->created_at; 326 | } 327 | 328 | /** 329 | * @return int 330 | */ 331 | public function favouritesCount(): int 332 | { 333 | return $this->favourites_count; 334 | } 335 | 336 | /** 337 | * @return int 338 | */ 339 | public function utcOffset(): int 340 | { 341 | return $this->utc_offset; 342 | } 343 | 344 | /** 345 | * @return string 346 | */ 347 | public function timeZone(): string 348 | { 349 | return $this->time_zone; 350 | } 351 | 352 | /** 353 | * @return bool 354 | */ 355 | public function geoEnabled(): bool 356 | { 357 | return $this->geo_enabled; 358 | } 359 | 360 | /** 361 | * @return bool 362 | */ 363 | public function verified(): bool 364 | { 365 | return $this->verified; 366 | } 367 | 368 | /** 369 | * @return int 370 | */ 371 | public function statusesCount(): int 372 | { 373 | return $this->statuses_count; 374 | } 375 | 376 | /** 377 | * @return string 378 | */ 379 | public function lang(): string 380 | { 381 | return $this->lang; 382 | } 383 | 384 | /** 385 | * @return array 386 | */ 387 | public function status(): array 388 | { 389 | return $this->status; 390 | } 391 | 392 | /** 393 | * @return bool 394 | */ 395 | public function contributorsEnabled(): bool 396 | { 397 | return $this->contributors_enabled; 398 | } 399 | 400 | /** 401 | * @return bool 402 | */ 403 | public function isTranslator(): bool 404 | { 405 | return $this->is_translator; 406 | } 407 | 408 | /** 409 | * @return bool 410 | */ 411 | public function isTranslatorEnabled(): bool 412 | { 413 | return $this->is_translator_enabled; 414 | } 415 | 416 | /** 417 | * @return string 418 | */ 419 | public function profileBackgroundColor(): string 420 | { 421 | return $this->profile_background_color; 422 | } 423 | 424 | /** 425 | * @return string 426 | */ 427 | public function profileBackgroundImageUrl(): string 428 | { 429 | return $this->profile_background_image_url; 430 | } 431 | 432 | /** 433 | * @return string 434 | */ 435 | public function profileBackgroundImageUrlHttps(): string 436 | { 437 | return $this->profile_background_image_url_https; 438 | } 439 | 440 | /** 441 | * @return bool 442 | */ 443 | public function profileBackgroundTile(): bool 444 | { 445 | return $this->profile_background_tile; 446 | } 447 | 448 | /** 449 | * @return string 450 | */ 451 | public function profileImageUrl(): string 452 | { 453 | return $this->profile_image_url; 454 | } 455 | 456 | /** 457 | * @return string 458 | */ 459 | public function profileImageUrlHttps(): string 460 | { 461 | return $this->profile_image_url_https; 462 | } 463 | 464 | /** 465 | * @return string 466 | */ 467 | public function profileBannerUrl(): string 468 | { 469 | return $this->profile_banner_url; 470 | } 471 | 472 | /** 473 | * @return string 474 | */ 475 | public function profileLinkColor(): string 476 | { 477 | return $this->profile_link_color; 478 | } 479 | 480 | /** 481 | * @return string 482 | */ 483 | public function profileSidebarBorderColor(): string 484 | { 485 | return $this->profile_sidebar_border_color; 486 | } 487 | 488 | /** 489 | * @return string 490 | */ 491 | public function profileSidebarFillColor(): string 492 | { 493 | return $this->profile_sidebar_fill_color; 494 | } 495 | 496 | /** 497 | * @return string 498 | */ 499 | public function profileTextColor(): string 500 | { 501 | return $this->profile_text_color; 502 | } 503 | 504 | /** 505 | * @return bool 506 | */ 507 | public function profileUseBackgroundImage(): bool 508 | { 509 | return $this->profile_use_background_image; 510 | } 511 | 512 | /** 513 | * @return bool 514 | */ 515 | public function hasExtendedProfile(): bool 516 | { 517 | return $this->has_extended_profile; 518 | } 519 | 520 | /** 521 | * @return bool 522 | */ 523 | public function defaultProfile(): bool 524 | { 525 | return $this->default_profile; 526 | } 527 | 528 | /** 529 | * @return bool 530 | */ 531 | public function defaultProfileImage(): bool 532 | { 533 | return $this->default_profile_image; 534 | } 535 | 536 | /** 537 | * @return bool 538 | */ 539 | public function following(): bool 540 | { 541 | return $this->following; 542 | } 543 | 544 | /** 545 | * @return bool 546 | */ 547 | public function followRequestSent(): bool 548 | { 549 | return $this->follow_request_sent; 550 | } 551 | 552 | /** 553 | * @return bool 554 | */ 555 | public function notifications(): bool 556 | { 557 | return $this->notifications; 558 | } 559 | } 560 | --------------------------------------------------------------------------------