├── .gitignore ├── .vagrant_bootstrap ├── bootstrap.sh └── parameters.sh ├── LICENCE.md ├── README.md ├── Vagrantfile ├── cache └── .gitkeep ├── composer.json ├── config ├── api_routes.yml └── config.yml.dist ├── console ├── doc ├── caching.md ├── configuration.md ├── contribute.md ├── how_to_use.md ├── installation.md ├── installation_production.md ├── installation_vagrant.md └── routing.md ├── logs └── .gitkeep └── src └── EloGank └── Api ├── Callback └── Summoner │ ├── SummonerActiveMasteriesCallback.php │ ├── SummonerActiveSpellBookCallback.php │ ├── SummonerChampionCallback.php │ ├── SummonerInformationCallback.php │ └── SummonerLeagueSolo5x5Callback.php ├── Client ├── Exception │ ├── AuthException.php │ ├── BadCredentialsException.php │ ├── ClientException.php │ ├── ClientKickedException.php │ ├── ClientNotReadyException.php │ ├── ClientOverloadException.php │ ├── PacketException.php │ ├── RequestTimeoutException.php │ └── ServerBusyException.php ├── Factory │ └── ClientFactory.php ├── Formatter │ └── ResultFormatter.php ├── LOLClient.php ├── LOLClientAsync.php ├── LOLClientInterface.php ├── RTMP │ ├── RTMPClient.php │ ├── RTMPPacket.php │ └── RTMPSocket.php └── Worker │ └── ClientWorker.php ├── Command ├── ApiStartCommand.php ├── ClientCreateCommand.php └── RouterDumpCommand.php ├── Component ├── Callback │ ├── Callback.php │ └── Exception │ │ └── MissingOptionCallbackException.php ├── Command │ └── Command.php ├── Configuration │ ├── ConfigurationLoader.php │ └── Exception │ │ ├── ConfigurationFileNotFoundException.php │ │ └── ConfigurationKeyNotFoundException.php ├── Controller │ ├── Controller.php │ └── Exception │ │ ├── ApiException.php │ │ └── UnknownControllerException.php ├── Exception │ └── ArrayException.php ├── Logging │ ├── Handler │ │ └── RedisHandler.php │ └── LoggerFactory.php └── Routing │ ├── Exception │ ├── MalformedRouteException.php │ ├── MissingApiRoutesFileException.php │ ├── MissingParametersException.php │ ├── UnknownResponseException.php │ └── UnknownRouteException.php │ └── Router.php ├── Controller ├── CommonController.php ├── InventoryController.php └── SummonerController.php ├── Manager └── ApiManager.php ├── Model └── Region │ ├── Exception │ └── RegionNotFoundException.php │ ├── Region.php │ └── RegionInterface.php ├── Process └── Process.php └── Server ├── Exception ├── MalformedClientInputException.php ├── ServerException.php └── UnknownFormatException.php ├── Formatter ├── ClientFormatterInterface.php ├── JsonClientFormatter.php ├── NativeClientFormatter.php └── XmlClientFormatter.php └── Server.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | composer.lock 3 | composer.phar 4 | vendor/ 5 | 6 | # IDE 7 | .idea/ 8 | 9 | # App 10 | config/config.yml 11 | logs/ 12 | !logs/.gitkeep 13 | cache/ 14 | !cache/.gitkeep 15 | 16 | # VM 17 | .vagrant/* -------------------------------------------------------------------------------- /.vagrant_bootstrap/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # --------------------------------------------------- 4 | # https://github.com/EloGank/lol-php-api 5 | # based on : https://github.com/Divi/VagrantBootstrap 6 | # --------------------------------------------------- 7 | 8 | # Include parameteres file 9 | # ------------------------ 10 | source /vagrant/.vagrant_bootstrap/parameters.sh 11 | 12 | # Update the box release repositories 13 | # ----------------------------------- 14 | apt-get update 15 | 16 | 17 | # Essential Packages 18 | # ------------------ 19 | apt-get install -y build-essential git-core vim curl php5-dev pkg-config 20 | 21 | 22 | # PHP 5.x (last official release) 23 | # See: https://launchpad.net/~ondrej/+archive/php5 24 | # ------------------------------------------------ 25 | apt-get install -y libcli-mod-php5 26 | # Install "add-apt-repository" binaries 27 | apt-get install -y python-software-properties 28 | # Install PHP 5.x 29 | # Use "ppa:ondrej/php5-oldstable" for old and stable release 30 | add-apt-repository ppa:ondrej/php5 31 | # Update repositories 32 | apt-get update 33 | 34 | # PHP tools 35 | apt-get install -y php5-cli php5-curl php5-mcrypt 36 | # APC (only with PHP < 5.5.0, use the "opcache" if >= 5.5.0) 37 | # apt-get install -y php-apc 38 | # Setting the timezone 39 | sed 's#;date.timezone\([[:space:]]*\)=\([[:space:]]*\)*#date.timezone\1=\2\"'"$PHP_TIMEZONE"'\"#g' /etc/php5/cli/php.ini > /etc/php5/cli/php.ini.tmp 40 | mv /etc/php5/cli/php.ini.tmp /etc/php5/cli/php.ini 41 | # Showing error messages 42 | sed 's#display_errors = Off#display_errors = On#g' /etc/php5/cli/php.ini > /etc/php5/cli/php.ini.tmp 43 | mv /etc/php5/cli/php.ini.tmp /etc/php5/cli/php.ini 44 | sed 's#display_startup_errors = Off#display_startup_errors = On#g' /etc/php5/cli/php.ini > /etc/php5/cli/php.ini.tmp 45 | mv /etc/php5/cli/php.ini.tmp /etc/php5/cli/php.ini 46 | sed 's#error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT#error_reporting = E_ALL#g' /etc/php5/cli/php.ini > /etc/php5/cli/php.ini.tmp 47 | mv /etc/php5/cli/php.ini.tmp /etc/php5/cli/php.ini 48 | 49 | 50 | # Redis 51 | # ----- 52 | add-apt-repository -y ppa:rwky/redis 53 | apt-get update 54 | apt-get install -y redis-server 55 | 56 | # Installing hiredis lib 57 | cd /tmp 58 | git clone https://github.com/redis/hiredis.git 59 | cd hiredis 60 | make && make install 61 | 62 | # Installing phpiredis PHP lib (make Redis faster for un/serialization process) 63 | cd /tmp 64 | git clone https://github.com/nrk/phpiredis.git 65 | cd phpiredis 66 | phpize && ./configure --enable-phpiredis 67 | make && make install 68 | echo "extension=phpiredis.so" > /etc/php5/cli/conf.d/20-phpiredis.ini 69 | 70 | 71 | # ZERO MQ 72 | # ------- 73 | cd /tmp 74 | wget http://download.zeromq.org/zeromq-$ZMQ_VERSION.tar.gz 75 | tar -zxvf zeromq-4.0.4.tar.gz 76 | cd zeromq-4.0.4/ 77 | ./configure 78 | make 79 | make install 80 | 81 | 82 | # PHP ZMQ Extension 83 | # ----------------- 84 | yes '' | pecl install zmq-beta 85 | echo "extension=zmq.so" > /etc/php5/cli/conf.d/20-zmq.ini 86 | 87 | 88 | # Composer 89 | # -------- 90 | cd /vagrant 91 | curl -sS https://getcomposer.org/installer | php 92 | php composer.phar install 93 | -------------------------------------------------------------------------------- /.vagrant_bootstrap/parameters.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # --------------------------------------------------- 4 | # https://github.com/EloGank/lol-php-api 5 | # based on : https://github.com/Divi/VagrantBootstrap 6 | # --------------------------------------------------- 7 | 8 | # PARAMETERS 9 | # ---------- 10 | 11 | # PHP 12 | # --- 13 | PHP_TIMEZONE="UTC" 14 | 15 | # ZERO MQ 16 | # ------- 17 | # See: http://zeromq.org/intro:get-the-software 18 | ZMQ_VERSION="4.0.4" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Since the October 1st 2014, a custom API is not fully allowed by the League of Legend's Terms of Use, only some API calls are allowed. More information : https://developer.riotgames.com 2 | 3 | ------------------------------ 4 | 5 | # League of Legends PHP API 6 | 7 | Unofficial RTMP API fully PHP and asynchronous for League of Legends. 8 | With this API you can retrieve some data about summoners in real-time directly from Riot servers. 9 | 10 | **You can use the API client from this repository : https://github.com/EloGank/lol-php-api-client** 11 | 12 | ### Features 13 | 14 | * A ready-to-use API server 15 | * **A ready-to-use Virtual Machine (no manual installation)** 16 | * Use the powerful Symfony 2 framework components 17 | * **Allow multi LoL account to improve the response speed** 18 | * **Fully aynschronous (with ZeroMQ & mutli process)** 19 | * Multi region (EUW, NA, EUNE, BR, TR, RU, KR, LAN, LAS, OCE & PBE) 20 | * Anti-overload system (avoid temporary client ban when you make too many request) 21 | * Allow to use native RTMP API or custom API with our controllers 22 | * Fully logged in file, redis, and console (usefull for developpers) 23 | * Automatic restart when a server is busy 24 | * Periodic verification for client timeout 25 | * **Automatic restart when a client timeout (due to network/server connection error for example)** 26 | * **Automatic update when client version is outdated** 27 | * **Allow mutliple output format (JSON, PHP native (serialized) and XML)** 28 | * **Allow concurrent connections (multiple connections at the same time, using ReactPHP)** 29 | * Allow to bind the server to a specific IP address (allow-only) 30 | * Easy to override 31 | 32 | ## Installation 33 | 34 | [How to install this API](./doc/installation.md) 35 | [Additional installation instructions for the production environment](./doc/installation_production.md) 36 | 37 | ## Configuration 38 | 39 | [How to configure this API](./doc/configuration.md) 40 | 41 | ## How to use 42 | 43 | [How to use this API](./doc/how_to_use.md) 44 | 45 | ## Route list 46 | 47 | [The routing component](./doc/routing.md#route-list) 48 | 49 | ## Documentation 50 | 51 | The document is stored in the `doc` folder of this repository. 52 | Here, the main titles : 53 | 54 | * [Installation](./doc/installation.md) 55 | * [Installation (production environment)](./doc/installation_production.md) 56 | * [Configuration](./doc/configuration.md) 57 | * [How to use](./doc/how_to_use.md) 58 | * [Routing](./doc/routing.md) 59 | * [Caching](./doc/caching.md) 60 | * [Contribute](./doc/contribute.md) 61 | 62 | ## Important notes 63 | 64 | Use a **development account** for your tests, and **not your real live game account**. 65 | Be aware that only one API/person can be connected at the same time with the same account. If you have production server and development server, use two distinct accounts. 66 | 67 | ## Related projects 68 | 69 | * [LoL Replay Downloader](https://github.com/EloGank/lol-replay-downloader) 70 | 71 | 72 | ## Known issues 73 | 74 | * Fix issue on SIGINT signal (CTRL + C) (ReactPHP issue : https://github.com/reactphp/react/issues/296) 75 | 76 | ## Reporting an issue or a feature request 77 | 78 | Feel free to open an issue, fork this project or suggest an awesome new feature in the [issue tracker](https://github.com/EloGank/lol-php-api/issues). 79 | When reporting an issue, please include your asynchronous configuration (enabled or not). 80 | 81 | ## Credit 82 | 83 | See the list of [contributors](https://github.com/EloGank/lol-php-api/graphs/contributors). 84 | The RTMP client class is a PHP partial rewrite of the awesome [Gabriel Van Eyck's work](https://code.google.com/p/lolrtmpsclient/source/browse/trunk/src/com/gvaneyck/rtmp/RTMPSClient.java). 85 | 86 | ## Licence 87 | 88 | [Creative Commons Attribution-ShareAlike 3.0](./LICENCE.md) 89 | 90 | *League of Legends and Riot Games are trademarks or registered trademarks of Riot Games, Inc. League of Legends (c) Riot Games, Inc.* 91 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # -------------------------------------- 5 | # https://github.com/EloGank/lol-php-api 6 | # -------------------------------------- 7 | 8 | VAGRANTFILE_API_VERSION = "2" 9 | 10 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 11 | # Core configurations 12 | # ------------------- 13 | config.vm.box = "precise32" 14 | config.vm.box_url = "http://files.vagrantup.com/precise32.box" 15 | config.ssh.forward_agent = true 16 | config.vm.network :private_network, ip: "192.168.100.10" 17 | 18 | config.vm.provider :virtualbox do |v| 19 | v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 20 | v.customize ["modifyvm", :id, "--memory", 1024] 21 | v.customize ["modifyvm", :id, "--name", "EloGank - PHP LoL API"] 22 | end 23 | 24 | # Running bootstrap 25 | # ----------------- 26 | config.vm.provision :shell, :path => ".vagrant_bootstrap/bootstrap.sh" 27 | end -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EloGank/lol-php-api/8c8b8650642cb9acf99641d9a52803219552d499/cache/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elogank/php-lol-api", 3 | "description": "PHP League of Legends API", 4 | "keywords": ["lol", "api", "league of legends", "lol api", "rtmp"], 5 | "licence": "CC BY-SA 3.0", 6 | "authors": [ 7 | { 8 | "name": "Sylvain Lorinet", 9 | "email": "sylvain.lorinet@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0", 14 | 15 | "symfony/console": "2.5.*@dev", 16 | "symfony/event-dispatcher": "2.5.*@dev", 17 | "symfony/monolog-bridge": "2.5.*@dev", 18 | "symfony/yaml": "v2.4.2", 19 | 20 | "sabre/amf": "dev-master", 21 | "monolog/monolog": "1.8.0", 22 | "incenteev/composer-parameter-handler": "v2.1.0", 23 | "react/socket": "0.4.*@dev", 24 | "predis/predis": "0.8.*@dev" 25 | }, 26 | "require-dev": { 27 | "moriony/php-zmq-stubs": "dev-master" 28 | }, 29 | "suggest": { 30 | "ext-zmq": "Allow to communicate between all asynchronous clients" 31 | }, 32 | "scripts": { 33 | "post-install-cmd": [ 34 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters" 35 | ], 36 | "post-update-cmd": [ 37 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters" 38 | ] 39 | }, 40 | "extra": { 41 | "incenteev-parameters": { 42 | "file": "config/config.yml", 43 | "parameter-key": "config" 44 | }, 45 | "branch-alias": { 46 | "dev-master": "1.2-dev" 47 | } 48 | }, 49 | "autoload": { 50 | "psr-0": { 51 | "EloGank\\Api": "src/" 52 | } 53 | }, 54 | "minimum-stability": "dev" 55 | } -------------------------------------------------------------------------------- /config/api_routes.yml: -------------------------------------------------------------------------------- 1 | routes: 2 | summonerService: 3 | getSummonerByName: [summonerName] 4 | getAllSummonerDataByAccount: [accountId] 5 | getAllPublicSummonerDataByAccount: [accountId] 6 | getSummonerInternalNameByName: [summonerName] 7 | getSummonerNames: ["summonerIds[]"] 8 | 9 | playerStatsService: 10 | getAggregatedStats: [accountId, "gameMode (CLASSIC, ODIN, ARAM)", "seasonId (1-4)"] 11 | getRecentGames: [accountId] 12 | retrieveTopPlayedChampions: [accountId, "gameMode (CLASSIC, ODIN, ARAM)"] 13 | getTeamAggregatedStats: [teamId] 14 | getTeamEndOfGameStats: [teamId, gameId] 15 | retrievePlayerStatsByAccountId: [accountId, "seasonId (1-4)"] 16 | 17 | spellBookService: 18 | getSpellBook: [summonerId] 19 | 20 | masteryBookService: 21 | getMasteryBook: [summonerId] 22 | 23 | inventoryService: 24 | getAvailableChampions: [] 25 | 26 | summonerIconService: 27 | getSummonerIconInventory: [summonerId] 28 | 29 | gameService: 30 | retrieveInProgressSpectatorGameInfo: [summonerName] 31 | 32 | leaguesServiceProxy: 33 | getAllLeaguesForPlayer: [summonerId] 34 | getChallengerLeague: ["queueName (RANKED_SOLO_5x5, RANKED_TEAM_5x5, RANKED_TEAM_3x3)"] 35 | 36 | loginService: 37 | getStoreUrl: [] -------------------------------------------------------------------------------- /config/config.yml.dist: -------------------------------------------------------------------------------- 1 | config: 2 | cache: 3 | # The cache directory path, from the project root directory 4 | path: cache 5 | 6 | log: 7 | # The log file path, from the project root directory, the directory must be created 8 | path: logs/api.log 9 | # The log verbosity : debug, info, warning, error 10 | verbosity: debug 11 | # The maximum number of files before the oldest one is deleted 12 | max_file: 5 13 | 14 | client: 15 | async: 16 | # Enable or disable the asynchronous system 17 | enabled: false 18 | # The starting port used by asynchronous workers. If there is "n" clients, the port will be incremented 19 | # by "n" (5000, 5001, ...) 20 | startPort: 5000 21 | redis: 22 | # The Redis server host address 23 | host: 127.0.0.1 24 | # The Redis server port 25 | port: 6379 26 | # The Redis server key used by this API 27 | key: elogank.api 28 | request: 29 | # The time in second before a call is timed out 30 | timeout: 5 31 | overload: 32 | # The minimum delay between two calls in a same client, in second (minimum 0.03) 33 | available: 0.03 34 | # The minimum delay before a client is available after being overloaded (minimum 4) 35 | wait: 4 36 | response: 37 | # If a response is not consumed by a client, it will expires, in second 38 | expire: 60 39 | authentication: 40 | busy: 41 | # When the server is busy, retry to connect after delay, in second 42 | wait: 30 43 | # The client version, can be retrieve by opening your LoL launcher, clicking on "Play" button and copy/paste 44 | # the number in the top left corner 45 | version: 4.12.x 46 | # The accounts configuration, see the documentation (doc/configuration.md) 47 | accounts: 48 | - region: ~ 49 | username: ~ 50 | password: ~ 51 | 52 | region: 53 | # You can edit your Region class 54 | class: EloGank\Api\Model\Region\Region 55 | # The region configuration 56 | servers: 57 | NA: 58 | name: North America 59 | server: prod.na2.lol.riotgames.com 60 | loginQueue: lq.na2.lol.riotgames.com 61 | EUW: 62 | name: Europe West 63 | server: prod.euw1.lol.riotgames.com 64 | loginQueue: lq.euw1.lol.riotgames.com 65 | EUNE: 66 | name: Europe Nordic & East 67 | server: prod.eun1.lol.riotgames.com 68 | loginQueue: lq.eun1.lol.riotgames.com 69 | KR: 70 | name: Korea 71 | server: prod.kr.lol.riotgames.com 72 | loginQueue: lq.kr.lol.riotgames.com 73 | BR: 74 | name: Brazil 75 | server: prod.br.lol.riotgames.com 76 | loginQueue: lq.br.lol.riotgames.com 77 | TR: 78 | name: Turkey 79 | server: prod.tr.lol.riotgames.com 80 | loginQueue: lq.tr.lol.riotgames.com 81 | RU: 82 | name: Russia 83 | server: prod.ru.lol.riotgames.com 84 | loginQueue: lq.ru.lol.riotgames.com 85 | LAN: 86 | name: Latin America North 87 | server: prod.la1.lol.riotgames.com 88 | loginQueue: lq.la1.lol.riotgames.com 89 | LAS: 90 | name: Latin America South 91 | server: prod.la2.lol.riotgames.com 92 | loginQueue: lq.la2.lol.riotgames.com 93 | OCE: 94 | name: Oceania 95 | server: prod.oc1.lol.riotgames.com 96 | loginQueue: lq.oc1.lol.riotgames.com 97 | PBE: 98 | name: Public Beta Environment 99 | server: prod.pbe1.lol.riotgames.com 100 | loginQueue: lq.pbe1.lol.riotgames.com 101 | 102 | # The server configuration 103 | server: 104 | # The server port 105 | port: 8080 106 | # The server binding address, please see the documentation (doc/configuration.md) 107 | bind: 0.0.0.0 108 | # The server output format : "json", "php" (using serialize function) or "xml" 109 | format: json 110 | 111 | php: 112 | # The PHP executable path 113 | executable: php 114 | -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | isDir()) { 13 | continue; 14 | } 15 | 16 | $name = substr($command->getFilename(), 0, -4); 17 | $reflectionClass = new \ReflectionClass('\\EloGank\\Api\\Command\\' . $name); 18 | if ($reflectionClass->isSubclassOf('\\EloGank\\Api\\Component\\Command\\Command') || $reflectionClass->isSubclassOf('Symfony\\Component\\Console\\Command\\Command')) { 19 | $class = '\\EloGank\\Api\\Command\\' . $name; 20 | $application->add(new $class()); 21 | } 22 | } 23 | 24 | $application->run(); -------------------------------------------------------------------------------- /doc/caching.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Caching 5 | 6 | To avoid useless API calls, you need to cache the call results. Before doing that, you must know some things : 7 | 8 | #### Summoner name 9 | 10 | The summoner name is not permanent ! A summoner can change his name by purchasing the rename ticket in the store thus, his name will be updated. If you create a sumomner name slug, **you must verify periodically if the name is still the same** and retrieve the related account information or, let a manual updater button. 11 | 12 | For the same reason, **you must use the summoner ID as the primary key** or you will lost your database/cache relations if the summoner udpate his name *(or you will have to update all the relation primary keys, this is not the best idea)*. 13 | 14 | #### Think about the players 15 | 16 | This API requests the Riot servers, which are used by LoL players. **So, think about the players : cache your retrieved data to avoid servers flood**. 17 | 18 | #### Complexity 19 | 20 | A lot of data can be retrieved with this API, so take a coffee, and think about your database/cache schema. 21 | 22 | ### Next 23 | 24 | Now you know everything about this API, you have the opportunity to [contribute to this project](./contribute.md). -------------------------------------------------------------------------------- /doc/configuration.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Configuration 5 | 6 | **Note:** you can find the whole configuration list in the [config/config.yml.dist](../config/config.yml.dist) file. 7 | 8 | After doing a `php composer install` a new file appears : `config/config.yml`, open this file. 9 | 10 | ### Asynchronous configuration 11 | 12 | The asynchronous system allow you to have more than one connected client. It usefull when having a big trafic website or using a custom API route with with simultaneous calls. 13 | If the asynchrnous system is disabled, you don't need to install the dependencies, and all API calls must wait for the previous before being executed. 14 | 15 | Simply edit the `client.async.enabled` key to `true`. 16 | 17 | ``` yml 18 | # config/config.yml 19 | client: 20 | async: 21 | enabled: true 22 | ``` 23 | 24 | ### Account configuration 25 | 26 | ``` yml 27 | # config/config.yml 28 | client: 29 | accounts: 30 | - region: ~ # The region unique name, currently EUW or NA 31 | username: ~ # Your test account username 32 | password: ~ # Your test account password 33 | ``` 34 | 35 | This API allow you to add more than one client account on several different servers (usefull only in asynchronous configration), example : 36 | 37 | ``` yml 38 | # config/config.yml 39 | client: 40 | accounts: 41 | - region: EUW 42 | username: myEuwUsername 43 | password: myEuwPassword 44 | - region: EUW 45 | username: mySecondEuwUsername 46 | password: mySecondEuwPassword 47 | - region: NA 48 | username: myNaUsername 49 | password: myNaPassword 50 | ``` 51 | 52 | If you need a specific client worker port for asynchronous configuration, use this configuration : 53 | 54 | ``` yml 55 | # config/config.yml 56 | client: 57 | accounts: 58 | - region: ~ # The region unique name, currently EUW or NA 59 | username: ~ # Your test account username 60 | password: ~ # Your test account password 61 | async: 62 | port: ~ # The specific client worker port 63 | ``` 64 | 65 | ### Server configuration 66 | 67 | ``` yml 68 | # config/config.yml 69 | server: 70 | port: 8080 71 | bind: 0.0.0.0 72 | format: json 73 | ``` 74 | 75 | You can allow only one IP address to connect to your fresh new API. Just add the IP address after the `bind` key : 76 | 77 | ``` yml 78 | # config/config.yml 79 | server: 80 | bind: 127.0.0.1 # access only to myself, 0.0.0.0 for everybody 81 | ``` 82 | 83 | You can change the output format, for these values : `json`, `php` (using `serialize` function) or `xml`. Simply edit the `format` key. 84 | If you need another format, [ask for it](https://github.com/EloGank/lol-php-api/issues), or contribute ! 85 | 86 | ### Next 87 | 88 | Now you can start the API, see the [how to use documentation](./how_to_use.md). -------------------------------------------------------------------------------- /doc/contribute.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Contribute 5 | 6 | You can contribute to this project by creating a feature request or making by your own this feature, using the [Github issue tracker](https://github.com/EloGank/lol-php-api/issues). 7 | 8 | **Be aware this API run with two main configurations** : asynchronous or synchronous (configuration key `client.asyn.enabled`). 9 | Please, test your awesome feature with these two configurations before pushing your code. 10 | 11 | Thanks :) -------------------------------------------------------------------------------- /doc/how_to_use.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## How to use 5 | 6 | This API use the powerful [Symfony 2](http://symfony.com/) framework components, and the console system is a part of these. 7 | So you have some commands to do different jobs. To use the console, you must being in the project root directory. 8 | 9 | ### The Client 10 | 11 | **This API need a socket client to communicate with your application**. Fortunately, there is a repository for this : https://github.com/EloGank/lol-php-api-client 12 | 13 | Please, take a look on this client, **some examples are available**. 14 | Assuming you're using this client, you can make a call in a few lines : 15 | 16 | ``` php 17 | // Declare your client and the configuration 18 | $client = new \EloGank\ApiClient\Client('127.0.0.1', 8080, 'json'); 19 | 20 | // Do your API request 21 | try { 22 | $response = $client->send('EUW', 'summoner.summoner_existence', ['Foobar']); 23 | } catch (\EloGank\ApiClient\Exception\ApiException $e) { 24 | // error 25 | var_dump($e->getCause(), $e->getMessage()); 26 | } 27 | ``` 28 | 29 | ### Routes (API calls) 30 | 31 | php console elogank:router:dump 32 | 33 | This command dump all available controllers and methods (routes) for the API. 34 | 35 | The output looks like : 36 | 37 | controller_name : 38 | method_name [parameter1, parameter2, ...] 39 | 40 | In this example, with your client, you must call the `controller_name.method_name` route, with two parameters to be able to execute the API. 41 | 42 | A route must be called with these three (+ one as optional) parameters : 43 | 44 | * `region` it's the client region short name (EUW, NA, ...) 45 | * `route` the API route, in short it's the "`controller_name`.`method_name`" 46 | * `parameters` it's the route parameters, it's an array 47 | * `format` (optional) if you need a specific format for a specific route (see the configuration documentation for available formats) 48 | 49 | You can see all available routes on the [routing documentation](./routing.md). 50 | 51 | ### The API 52 | 53 | You will be pleased to learn that the API is fully logged on console and on files (`/logs` directory). The log verbosity can be set in the configuration file (key: `log.verbosity`). 54 | 55 | php console elogank:api:start 56 | 57 | This command will start the API, connect all clients and listening for some future requests. 58 | If you have enabled the asynchronous system, the authentication process will be fast. 59 | 60 | ### The asynchronous client creation 61 | 62 | This command is only used by the API itself, to create client worker for asynchronous purposes. But if you want to create another application and you want to use asynchronous clients, you can use this command : 63 | 64 | php console elogank:client:create [account_configuration_key] [client_id] 65 | 66 | With these parameters : 67 | * `account_configuration_key` is the key index in the configuration file (the first account configuration is 0, and 1, ...) 68 | * `client_id` an identification id for logging and process communication purposes (an id per client, it must be unique and can be a string) 69 | 70 | Example, for your first asynchronous client : 71 | 72 | php console elogank:client:create 0 1 73 | 74 | ### Implement your own API route 75 | 76 | #### The workflow 77 | 78 | Before starting, you need to understand that the API works with an event-driven workflow. There is no blocking statement, only [ReactPHP](https://github.com/reactphp/react) loop. This loop provides some methods to create periodic timed callbacks. 79 | For more information, take a look on the official [ReactPHP Github](https://github.com/reactphp/react) or directly in the [src/EloGank/Api/Component/Controller/Controller.php](../src/EloGank/Api/Component/Controller/Controller.php). 80 | 81 | **There are two main events :** 82 | 83 | * `api-response` : when the API response is emitted to the client 84 | * `api-error` : when an API exception is emitted to the client and processed by the server 85 | 86 | #### The code 87 | 88 | First, you need to choice what is the main object of the API request : summoner, league, player_stats, etc. 89 | Then, create (if not already exists) the controller in the `src/EloGank/Api/Controller` directory, for example `SummonerController` : 90 | 91 | ``` php 92 | // src/EloGank/Api/Controller/SummonerController.php 93 | 94 | onClientReady(function (LOLClientInterface $client) use ($myParameter, $mySecondParameter) { 118 | // Note that $myParameter & $mySecondParameter are added to the "use" statement above 119 | // Without that, we can't use them in this callback 120 | $this->fetchResult($client->invoke('summonerService', 'getSomeData', [$myParameter, $mySecondParameter], function ($result) { 121 | var_dump('my callback'); 122 | 123 | return $myInvokeResult; 124 | })); 125 | }); 126 | 127 | // sendResponse() has only one optional parameter, a callback to format the response 128 | $this->sendResponse(function ($myControllerResponse) { 129 | // In the case where we have more than one invoke, "$myControllerResponse" will be an indexed array of invoke results. 130 | // If we have only one invoke (like in this example), it will be the invoke result (an associative array of data) 131 | var_dump($myControllerResponse); 132 | }); 133 | } 134 | ``` 135 | 136 | You can create a callback class in `Callback` folder to replace the callback to avoid duplicate code. Your class must extends `EloGank\Api\Component\Callback\Callback`. 137 | 138 | ``` php 139 | // Callback/MyCustomCallback.php 140 | 141 | class SummonerActiveMasteriesCallback extends Callback 142 | { 143 | /** 144 | * Parse the API result and return the new content 145 | * 146 | * @param array|string $result 147 | * 148 | * @return mixed 149 | */ 150 | public function getResult($result) 151 | { 152 | foreach ($result['property'] as $data) { 153 | if (true === $data['foo']) { 154 | return ['custom' => $data]; 155 | } 156 | } 157 | 158 | 159 | return ['custom' => []]; 160 | } 161 | 162 | /** 163 | * Set your required options here, if one or more options are missing, an exception will be thrown 164 | * 165 | * @return array 166 | */ 167 | protected function getRequiredOptions() 168 | { 169 | return [ 170 | 'my_option' 171 | ]; 172 | } 173 | } 174 | ``` 175 | 176 | 177 | ``` php 178 | // Controller/MyCustomController.php 179 | 180 | public function getSomeDataAction($myParameter, $mySecondParameter) 181 | { 182 | // MyClassCallback::getResult() will be automatically called by the Controller class 183 | $this->onClientReady(function (LOLClientInterface $client) use ($myParameter) { 184 | $this->fetchResult($client->invoke('summonerService', 'getSomeData', [$myParameter], new MyClassCallback([ 185 | 'my_option' => 'foo bar' 186 | ]))); 187 | }); 188 | 189 | $this->sendResponse(); 190 | } 191 | ``` 192 | 193 | 194 | Now, run the elogank:router:dump command to see your new API route. 195 | If you want to know about the make asynchronous calls in a same controller method, see the [GameController::getAllSummonerDataCurrentGameAction()](../src/EloGank/Api/Controller/GameController.php) method. 196 | 197 | ### Next 198 | 199 | Before using this API, you must know how works the routing component through the [routing documentation](./routing.md). -------------------------------------------------------------------------------- /doc/installation.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Installation 5 | 6 | ### Virtual Machine 7 | 8 | If you want to install/try this API with a Virtual Machine and avoid this installation process, read the [virtual machine documentation](./installation_vagrant.md). 9 | It will take you only two minutes. 10 | 11 | **If you want to install/try this API on a Windows system, we advice you to choose the Virtual Machine installation process. Some dependencies are not easy to build on this operating system.** 12 | 13 | ### Manually 14 | 15 | **Note:** these packages are needed to compile these dependencies : `build-essential git-core curl php5-dev pkg-config` 16 | 17 | #### ZeroMQ (optional) 18 | 19 | ZeroMQ is the technology used to allow the asynchronous client system in the API. 20 | This dependency is optional if you don't use the asynchronous system. 21 | 22 | Here, for **Linux or OSX**, a CLI script to install this dependency : 23 | 24 | ``` bash 25 | cd /tmp 26 | # Note: use the last version here : http://zeromq.org/intro:get-the-software 27 | wget http://download.zeromq.org/zeromq-4.0.4.tar.gz 28 | tar -zxvf zeromq-4.0.4.tar.gz 29 | cd zeromq-4.0.4/ 30 | ./configure 31 | make 32 | make install 33 | ``` 34 | 35 | Then, install the PHP extension : 36 | ``` bash 37 | yes '' | pecl install zmq-beta 38 | echo "extension=zmq.so" > /etc/php5/cli/conf.d/20-zmq.ini 39 | ``` 40 | 41 | For **Windows**, please follow this official instructions : http://zeromq.org/docs:windows-installations and for the extension, install `PECL` and run `pecl install zmq-beta` in a CLI window. 42 | 43 | #### Redis (optional) 44 | 45 | Redis is a cache system. It allow, in this API, to communicate between all asynchronous client and with the main process logger. 46 | This dependency is optional if you don't use the asynchronous system. 47 | 48 | Here, for **Linux or OSX**, a CLI script to install this dependency : 49 | 50 | ``` bash 51 | # Installing the last Redis version 52 | add-apt-repository -y ppa:rwky/redis 53 | apt-get update 54 | apt-get install -y redis-server 55 | 56 | # Installing hiredis library dependency for the phpiredis 57 | cd /tmp 58 | git clone https://github.com/redis/hiredis.git 59 | cd hiredis 60 | make && make install 61 | 62 | # Installing phpiredis PHP lib (make Redis faster for un/serialization process) 63 | cd /tmp 64 | git clone https://github.com/nrk/phpiredis.git 65 | cd phpiredis 66 | phpize && ./configure --enable-phpiredis 67 | make && make install 68 | echo "extension=phpiredis.so" > /etc/php5/cli/conf.d/20-phpiredis.ini 69 | ``` 70 | 71 | On **Windows**, download Redis here : https://github.com/mythz/redis-windows/tree/master/downloads and launch the "redis-server.exe" when you want to use this API. 72 | 73 | #### Composer 74 | 75 | Composer is a Command-Line Interface (CLI) dependency manager for PHP. 76 | 77 | * Get [Composer](https://getcomposer.org) by copy/paste this line on your shell (in the project root directory) : 78 | * On **Linux/OSX** : `curl -sS https://getcomposer.org/installer | php` 79 | * On **Windows** : `php -r "readfile('https://getcomposer.org/installer');" | php` 80 | * Then, install all dependancies : `php composer.phar install` 81 | 82 | ### Next 83 | 84 | Great ! Now, see the [configuration documentation](./configuration.md). -------------------------------------------------------------------------------- /doc/installation_production.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Installation (for production environment) 5 | 6 | ### Warning 7 | 8 | **This page is only additional installation instructions and you must follow the [main installation instructions](./installation.md) before.** 9 | These instructions are only for production env because the API will be **more faster** and **reduce CPU usage**, but **less convenient**. Feel free to follow them for development environment if you want the same environment as production. 10 | 11 | ### Dedicated PHP version 12 | 13 | PHP is more slower if useless extensions are loaded. So we will compile a fresh version, with only needed extensions and **increase speed by 25%**. 14 | 15 | The dedicated PHP version will compiled in `/etc/php5-api` directory. Feel free to change this path. 16 | 17 | #### Clone from Github 18 | 19 | At the time of writing this documentation, the last PHP version is `5.5.15`, it's recommended to install the last version, please note the [last PHP version](https://php.net/downloads.php) and edit the third line below. 20 | 21 | ``` bash 22 | git clone https://github.com/php/php-src.git /tmp/php-src 23 | cd /tmp/php-src 24 | git checkout tags/php-5.5.15 25 | ``` 26 | 27 | #### Compilation 28 | 29 | ``` bash 30 | # Installing needed dependencies 31 | apt-get install -y make autoconf re2c bison 32 | # Installing needed libs 33 | apt-get install -y libssl-dev libcurl4-openssl-dev 34 | # Compile 35 | ./buildconf --force 36 | ./configure --prefix=/etc/php5-api --with-config-file-path=/etc/php5-api --with-config-file-scan-dir=/etc/php5-api/conf.d --disable-all --with-curl --with-openssl --enable-sockets --enable-ctype --enable-pcntl --enable-json --enable-posix 37 | make 38 | make install 39 | ``` 40 | 41 | If you have uBuntu 14.04 TLS or an error `WARNING: bison versions supported for regeneration of the Zend/PHP parsers: 2.4 2.4.1 2.4.2 2.4.3 2.5 2.5.1 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7 (found: 3.0).`, just download the last package from links below and install them with dpkg (see below). 42 | 43 | * libbison-dev : http://packages.ubuntu.com/saucy/libbison-dev 44 | * bison : http://packages.ubuntu.com/saucy/bison 45 | 46 | ``` bash 47 | wget http://launchpadlibrarian.net/140087283/libbison-dev_2.7.1.dfsg-1_amd64.deb 48 | wget http://launchpadlibrarian.net/140087282/bison_2.7.1.dfsg-1_amd64.deb 49 | dpkg -i libbison-dev_2.7.1.dfsg-1_amd64.deb 50 | dpkg -i bison_2.7.1.dfsg-1_amd64.deb 51 | ``` 52 | 53 | #### Settings 54 | 55 | You can change your timezone by editing the second line : replace `UTC` by your current timezone, like `Europe/Paris`. 56 | 57 | ``` bash 58 | cp php.ini-production /etc/php5-api/php.ini 59 | mkdir /etc/php5-api/conf.d 60 | sed 's#;date.timezone\([[:space:]]*\)=\([[:space:]]*\)*#date.timezone\1=\2\"'"UTC"'\"#g' /etc/php5-api/php.ini > /etc/php5-api/php.ini.tmp 61 | mv /etc/php5-api/php.ini.tmp /etc/php5-api/php.ini 62 | ``` 63 | In your `config.yml` file, edit the PHP executable path : 64 | 65 | ``` yml 66 | # config/config.yml 67 | php: 68 | executable: /etc/php5-api/bin/php 69 | ``` 70 | 71 | **Only if you want to install this dedicated PHP version on a DEVELOPMENT environment, you can show errors :** 72 | 73 | ``` bash 74 | # ONLY FOR DEVELOPMENT ENVIRONMENT : Error messages 75 | sed 's#display_errors = Off#display_errors = On#g' /etc/php5-api/php.ini > /etc/php5-api/php.ini.tmp 76 | mv /etc/php5-api/php.ini.tmp /etc/php5-api/php.ini 77 | sed 's#display_startup_errors = Off#display_startup_errors = On#g' /etc/php5-api/php.ini > /etc/php5-api/php.ini.tmp 78 | mv /etc/php5-api/php.ini.tmp /etc/php5-api/php.ini 79 | sed 's#error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT#error_reporting = E_ALL#g' /etc/php5-api/php.ini > /etc/php5-api/php.ini.tmp 80 | mv /etc/php5-api/php.ini.tmp /etc/php5-api/php.ini 81 | ``` 82 | 83 | #### Reinstallation of ZeroMQ extensions 84 | 85 | **Note :** the build directory (`20121212`) can be different in your OS. 86 | 87 | ``` bash 88 | mkdir /etc/php5-api/lib/php/extensions/ 89 | mkdir /etc/php5-api/lib/php/extensions/no-debug-non-zts-20121212 90 | cp /usr/lib/php5/20121212/zmq.so /etc/php5-api/lib/php/extensions/no-debug-non-zts-20121212/zmq.so 91 | cp /etc/php5/cli/conf.d/20-zmq.ini /etc/php5-api/conf.d/ 92 | ``` 93 | 94 | #### Usage 95 | 96 | Start your api with this command : `/etc/php5-api/bin/php console elogank:api:start` 97 | -------------------------------------------------------------------------------- /doc/installation_vagrant.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Virutal Machine Installation 5 | 6 | ### Virtual Machine 7 | 8 | This API has a ready-to-use Virtual Machine (VM) installation process. But before, you need to install the VM binaries. We using [Vagrant](http://www.vagrantup.com/). 9 | 10 | **If you want to install/try this API on a Windows system, we advice you to choose the Virtual Machine installation process. Some dependencies are not easy to build on this operating system.** 11 | 12 | #### Download & Install 13 | 14 | * Download Vagrant : http://www.vagrantup.com/downloads.html 15 | * Download Virtual Box : https://www.virtualbox.org/wiki/Downloads 16 | 17 | #### Run 18 | 19 | Edit your environment parameters in the file `.vagrant_bootstrap/parameters.sh`. 20 | Now, open the Command-Line Interface (CLI) window, and, in the project root directory run this command : 21 | 22 | vagrant up 23 | 24 | This command install the VM and all the API dependencies. It takes generally 2~3 minutes, it depends on your Internet connection speed. 25 | When you want to shutdown the VM, just run this command : 26 | 27 | vagrant halt 28 | 29 | We advise you to read the [Vagrant documentation](http://docs.vagrantup.com/v2/getting-started/index.html) for more information. 30 | 31 | ### Next 32 | 33 | Great ! Now, see the [configuration documentation](./configuration.md). -------------------------------------------------------------------------------- /doc/routing.md: -------------------------------------------------------------------------------- 1 | League of Legends PHP API 2 | ========================= 3 | 4 | ## Routing 5 | 6 | There is two different routes in this API. 7 | 8 | #### Common Route 9 | 10 | It's a route that is already provided by the RTMP LoL API (by the launcher, in short). 11 | This route return the raw LoL API result, without any process between the request and the response. 12 | 13 | All routes are requested by the `EloGank\Api\Controller\CommonController::commonCall()` controller's method. 14 | You can easilly add a common route by editing the `config/api_routes.yml` file : just provide the destination name, the service name & the parameters. 15 | 16 | #### Custom Route 17 | 18 | It's a custom route created the developper (you and me) with a custom controller. 19 | It was necessary to implement this custom route, and therefore custom controller, simply because we need to process some custom stuff after requesting one or more common routes. It's here that the asynchronous system is the most useful. In fact, you can call several common routes asynchronously, gain time querying, execute your callbacks and finally, return to your client API. 20 | For example, with the route `summoner.all_summoner_data_current_game`, you can try it with asynchronous mode enabled and disabled, the requesting time will down to ~0.4 second in asynchronous mode *(with 6 clients)*, against ~1.5 second in synchronous mode. 21 | 22 | To implement your own custom route, please refer to the [How to use documentation](https://github.com/EloGank/lol-php-api/blob/master/doc/how_to_use.md#implement-your-own-api-route). 23 | 24 | ### Route List 25 | 26 | According to the `elogank:router:dump` command, here the available route liste : 27 | 28 | ``` 29 | summoner : 30 | - summoner_by_name : [summonerName] 31 | - all_summoner_data_by_account : [accountId] 32 | - all_public_summoner_data_by_account : [accountId] 33 | - summoner_internal_name_by_name : [summonerName] 34 | - summoner_names : [summonerIds[]] 35 | - all_summoner_data : [summonerData[], fetchers[]] 36 | player_stats : 37 | - aggregated_stats : [accountId, gameMode (CLASSIC, ODIN, ARAM), seasonId (1-4)] 38 | - recent_games : [accountId] 39 | - retrieve_top_played_champions : [accountId, gameMode (CLASSIC, ODIN, ARAM)] 40 | - team_aggregated_stats : [teamId] 41 | - team_end_of_game_stats : [teamId, gameId] 42 | - retrieve_player_stats_by_account_id : [accountId, seasonId (1-4)] 43 | spell_book : 44 | - spell_book : [summonerId] 45 | mastery_book : 46 | - mastery_book : [summonerId] 47 | inventory : 48 | - available_champions : [] 49 | - available_free_champions : [] 50 | summoner_icon : 51 | - summoner_icon_inventory : [summonerId] 52 | game : 53 | - retrieve_in_progress_spectator_game_info : [summonerName] 54 | leagues : 55 | - all_leagues_for_player : [summonerId] 56 | - challenger_league : [queueName (RANKED_SOLO_5x5, RANKED_TEAM_5x5, RANKED_TEAM_3x3)] 57 | login : 58 | - store_url : [] 59 | ``` 60 | 61 | #### Route Parameters 62 | 63 | **Routes have three main parameters :** 64 | 65 | * **summonerName** : it's not the account name (login) but the summoner name 66 | * **summonerId** : it's the summoner unique ID, it never changes 67 | * **accountId** : it's the summoner account unique ID, it never changes *(not sure after a server transfer, need [help](https://github.com/EloGank/lol-php-api/issues) about this information)* 68 | 69 | I can find the summoner & account ID with the route `summoner.summoner_existence [sumomnerName]` which returns the main information about the summoner, including these two parameters. 70 | 71 | Please, refer to the [caching documentation](./caching.md) to learn how to cache summoner data. 72 | 73 | **Possible other parameters :** 74 | 75 | * **gameMode** : it's the game mode : "CLASSIC", "ODIN" (Dominion) or "ARAM" 76 | * **seasonId** : it's the season ID : 1, 2, 3 or 4, increased by one each year. At this time : 4 77 | * **teamId** : it's a summoner team unique ID 78 | * **gameId** : it's a game unique ID 79 | 80 | ### Route Details 81 | 82 | **Note :** there might be some missing errors declaration in this documentation. By the way, all routes might return an error : **you must handle this case**. 83 | 84 | #### Summoner 85 | * `summoner_by_name` : returns if the player exists or not. **If the player doesn't exist, a `NULL` response is returned**. 86 | * `all_summoner_data_by_account` : returns only spellbooks & masteries **main** information. 87 | * `all_public_summoner_data_by_account` : returns spellbooks **full** information AND all summoner spells used in all game modes. 88 | * `summoner_internal_name_by_name` : returns the summoner internal name. Maybe a formatted summoner name, used by Riot. 89 | * `summoner_names` : returns all summoner names for the selected summoner IDs. 90 | * `all_summoner_data` : *(custom route)* returns all the main data about a summoner *(spellbooks, masteries, main champion & ranked 5x5 solo league)*. See method documentation in [SummonerController.php](/src/EloGank/Api/Controller/SummonerController.php) for more information. 91 | 92 | #### Player Stats 93 | * `aggregated_stats` : returns all information about a **ranked** game mode. 94 | * `recent_games` : returns the information about the summoner recent games. 95 | * `retrieve_top_played_champions` : returns the three most played champion for a **ranked** game mode. 96 | * `team_aggregated_stats` : returns all information about a **ranked** game mode for a team. 97 | * `team_end_of_game_stats` : returns all information about a game result for a team. 98 | * `retrieve_player_stats_by_account_id` : returns all **main** information about all game mode (the profile page). 99 | 100 | #### Spell Book 101 | * `spell_book` : returns **full** information about the summoner spellbooks. 102 | 103 | #### Mastery Book 104 | * `mastery_book` : returns **full** information about the summoner masteries. 105 | 106 | #### Inventory 107 | * `available_champions` : returns all information about the available champions & skins. 108 | * `available_free_champions` : returns all information about the available **free** champions *(usefull to know the free champions rotation week)*. 109 | 110 | #### Summoner Icon 111 | * `summoner_icon_inventory` : returns all information about the available summoner icons for a selected summoner. 112 | 113 | #### Game 114 | * `retrieve_in_progress_spectator_game_info` : returns all information about the current game for a selected summoner. **If the player doesn't exist or isn't in a game, a `NULL` response is returned**. 115 | 116 | #### Leagues 117 | * `all_leagues_for_player` : returns all information about the summoner leagues. 118 | * `challenger_league` : returns all information about the summoner leagues in challenger tier for the given queue name (RANKED_SOLO_5x5, RANKED_TEAM_5x5 or RANKED_TEAM3x3). 119 | 120 | #### Login 121 | * `store_url` : returns the full store URL with the authentication token. 122 | 123 | ### Next 124 | 125 | Last but not least, see the [caching documentation](./caching.md) to avoid useless calls. -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EloGank/lol-php-api/8c8b8650642cb9acf99641d9a52803219552d499/logs/.gitkeep -------------------------------------------------------------------------------- /src/EloGank/Api/Callback/Summoner/SummonerActiveMasteriesCallback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class SummonerActiveMasteriesCallback extends Callback 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResult($result) 25 | { 26 | foreach ($result['bookPages'] as $bookPage) { 27 | if (true === $bookPage['current']) { 28 | return ['activeMasteries' => $bookPage]; 29 | } 30 | } 31 | 32 | 33 | return ['activeMasteries' => []]; 34 | } 35 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Callback/Summoner/SummonerActiveSpellBookCallback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class SummonerActiveSpellBookCallback extends Callback 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResult($result) 25 | { 26 | foreach ($result['spellBook']['bookPages'] as $bookPage) { 27 | if (true === $bookPage['current']) { 28 | return ['activeSpellBook' => $bookPage]; 29 | } 30 | } 31 | 32 | return ['activeSpellBook' => []]; 33 | } 34 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Callback/Summoner/SummonerChampionCallback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class SummonerChampionCallback extends Callback 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResult($result) 25 | { 26 | if (!isset($result['lifetimeStatistics'][0])) { 27 | $emptyData = []; 28 | if (true === $this->options['main_champion']) { 29 | $emptyData['mainChampion'] = null; 30 | } 31 | 32 | if (true === $this->options['champions_data']) { 33 | $emptyData['champions'] = null; 34 | } 35 | 36 | return $emptyData; 37 | } 38 | 39 | $data = []; 40 | if (true === $this->options['main_champion']) { 41 | $totalPlayedSession = 0; 42 | $mainChampionId = null; 43 | 44 | foreach ($result['lifetimeStatistics'] as $championData) { 45 | if (0 != $championData['championId'] && 'TOTAL_SESSIONS_PLAYED' == $championData['statType'] && $championData['value'] > $totalPlayedSession) { 46 | $totalPlayedSession = $championData['value']; 47 | $mainChampionId = $championData['championId']; 48 | } 49 | } 50 | 51 | $data['mainChampionId'] = $mainChampionId; 52 | } 53 | 54 | if (true === $this->options['champions_data']) { 55 | $dataByChampionId = []; 56 | foreach ($result['lifetimeStatistics'] as $championData) { 57 | $dataByChampionId[$championData['championId']][] = [ 58 | 'statType' => $championData['statType'], 59 | 'value' => $championData['value'] 60 | ]; 61 | } 62 | 63 | $data['champions'] = $dataByChampionId; 64 | } 65 | 66 | return $data; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function getRequiredOptions() 73 | { 74 | return [ 75 | 'main_champion', // boolean 76 | 'champions_data' // boolean 77 | ]; 78 | } 79 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Callback/Summoner/SummonerInformationCallback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class SummonerInformationCallback extends Callback 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResult($result) 25 | { 26 | return ['information' => $result]; 27 | } 28 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Callback/Summoner/SummonerLeagueSolo5x5Callback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class SummonerLeagueSolo5x5Callback extends Callback 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResult($result) 25 | { 26 | foreach ($result['summonerLeagues'] as $summonerLeague) { 27 | if ('RANKED_SOLO_5x5' != $summonerLeague['queue']) { 28 | continue; 29 | } 30 | 31 | if (isset($this->options['full']) && true === $this->options['full']) { 32 | $league = []; 33 | foreach ($summonerLeague['entries'] as $entry) { 34 | $league[] = [ 35 | 'name' => $summonerLeague['name'], 36 | 'queue' => $summonerLeague['queue'], 37 | 'data' => $entry 38 | ]; 39 | } 40 | } 41 | 42 | // Return only the league data of the selected summoner 43 | foreach ($summonerLeague['entries'] as $entry) { 44 | if ($this->options['summonerId'] == $entry['playerOrTeamId']) { 45 | return ['league' => [ 46 | 'name' => $summonerLeague['name'], 47 | 'queue' => $summonerLeague['queue'], 48 | 'data' => $entry 49 | ]]; 50 | } 51 | } 52 | } 53 | 54 | return ['league' => []]; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function getRequiredOptions() 61 | { 62 | if (!isset($this->options['full']) || false === $this->options['full']) { 63 | return [ 64 | 'summonerId', // integer 65 | ]; 66 | } 67 | 68 | return parent::getRequiredOptions(); 69 | } 70 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/AuthException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class AuthException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/BadCredentialsException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class BadCredentialsException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/ClientException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class ClientException extends \RuntimeException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/ClientKickedException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ClientKickedException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/ClientNotReadyException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ClientNotReadyException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/ClientOverloadException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ClientOverloadException extends \RuntimeException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/PacketException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class PacketException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/RequestTimeoutException.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class RequestTimeoutException extends ServerException 21 | { 22 | /** 23 | * @var LOLClientInterface 24 | */ 25 | protected $client; 26 | 27 | 28 | /** 29 | * @param string $message 30 | * @param LOLClientInterface $client 31 | */ 32 | public function __construct($message, LOLClientInterface $client = null) 33 | { 34 | $this->client = $client; 35 | 36 | parent::__construct($message); 37 | } 38 | 39 | /** 40 | * @return LOLClientInterface 41 | */ 42 | public function getClient() 43 | { 44 | return $this->client; 45 | } 46 | 47 | /** 48 | * @param LOLClientInterface $client 49 | */ 50 | public function setClient($client) 51 | { 52 | $this->client = $client; 53 | } 54 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Exception/ServerBusyException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ServerBusyException extends ClientException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Factory/ClientFactory.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class ClientFactory 28 | { 29 | /** 30 | * @param LoggerInterface $logger 31 | * @param Client $redis 32 | * @param int $accountKey 33 | * @param string $clientId 34 | * @param bool $forceSynchronous 35 | * 36 | * @return LOLClientInterface 37 | * 38 | * @throws \RuntimeException 39 | */ 40 | public static function create(LoggerInterface $logger, Client $redis, $accountKey, $clientId, $forceSynchronous = false) 41 | { 42 | $configs = ConfigurationLoader::get('client.accounts')[$accountKey]; 43 | $port = (int) ConfigurationLoader::get('client.async.startPort'); 44 | $port += $clientId - 1; 45 | 46 | // Custom client port 47 | if (isset($configs['async']['port'])) { 48 | $port = $configs['async']['port']; 49 | } 50 | 51 | // Async process 52 | if (!$forceSynchronous && true === ConfigurationLoader::get('client.async.enabled')) { 53 | return new LOLClientAsync( 54 | $logger, 55 | $redis, 56 | $accountKey, 57 | $clientId, 58 | self::createRegion($configs['region']), 59 | $port 60 | ); 61 | } 62 | 63 | // Sync process 64 | return new LOLClient( 65 | $logger, 66 | $redis, 67 | $clientId, 68 | self::createRegion($configs['region']), 69 | $configs['username'], 70 | $configs['password'], 71 | ConfigurationLoader::get('client.version'), 72 | 'en_US', 73 | $port 74 | ); 75 | } 76 | 77 | /** 78 | * @param string $regionUniqueName 79 | * 80 | * @return RegionInterface 81 | * 82 | * @throws RegionNotFoundException 83 | */ 84 | private static function createRegion($regionUniqueName) 85 | { 86 | try { 87 | $region = ConfigurationLoader::get('region.servers.' . $regionUniqueName); 88 | } 89 | catch (ConfigurationKeyNotFoundException $e) { 90 | throw new RegionNotFoundException('The region with unique name "' . $regionUniqueName . '" is not found'); 91 | } 92 | 93 | $class = ConfigurationLoader::get('region.class'); 94 | 95 | return new $class($regionUniqueName, $region['name'], $region['server'], $region['loginQueue']); 96 | } 97 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Formatter/ResultFormatter.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ResultFormatter 20 | { 21 | /** 22 | * @param mixed $results 23 | * 24 | * @return array 25 | * 26 | * @throws ClientOverloadException 27 | */ 28 | public function format($results) 29 | { 30 | if (null == $results) { 31 | throw new ClientOverloadException('The client is overloaded'); 32 | } 33 | 34 | return $this->toArray($results); 35 | } 36 | 37 | /** 38 | * @param $object 39 | * 40 | * @return array 41 | * 42 | * @throws \RuntimeException 43 | */ 44 | protected function toArray($object) 45 | { 46 | if ($object instanceof \SabreAMF_TypedObject) { 47 | $result = $object->getAMFData(); 48 | 49 | foreach ($result as &$data) { 50 | if (is_object($data)) { 51 | $data = $this->toArray($data); 52 | } 53 | } 54 | 55 | return $result; 56 | } 57 | elseif ($object instanceof \SabreAMF_ArrayCollection) { 58 | $array = []; 59 | foreach ($object as $key => $data) { 60 | if (is_object($data)) { 61 | $array[$key] = $this->toArray($data); 62 | } 63 | else { 64 | $array[$key] = $data; 65 | } 66 | } 67 | 68 | return $array; 69 | } 70 | elseif ($object instanceof \DateTime) { 71 | return $object->format('Y-m-d H:i:s'); 72 | } 73 | elseif ($object instanceof \stdClass) { 74 | $array = get_object_vars($object); 75 | foreach ($array as &$data) { 76 | if (is_object($data)) { 77 | $data = $this->toArray($data); 78 | } 79 | } 80 | 81 | return $array; 82 | } 83 | elseif ($object instanceof \SabreAMF_AMF3_ErrorMessage) { 84 | return [ 85 | 'rootCauseClassname' => $object->faultCode, 86 | 'message' => $object->faultString 87 | ]; 88 | } 89 | 90 | if (!is_object($object)) { 91 | return [$object]; 92 | } 93 | 94 | throw new \RuntimeException('Unknown object class "' . get_class($object) . '". The ResultFormatter don\'t known how to format this class.'); 95 | } 96 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/LOLClient.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class LOLClient extends RTMPClient implements LOLClientInterface 31 | { 32 | const URL_AUTHENTICATE = '/login-queue/rest/queue/authenticate'; 33 | const URL_TOKEN = '/login-queue/rest/queue/authToken'; 34 | const URL_TICKER = '/login-queue/rest/queue/ticker'; 35 | 36 | /** 37 | * @var Client 38 | */ 39 | protected $redis; 40 | 41 | /** 42 | * @var int 43 | */ 44 | protected $clientId; 45 | 46 | /** 47 | * @var string 48 | */ 49 | protected $username; 50 | 51 | /** 52 | * @var string 53 | */ 54 | protected $password; 55 | 56 | /** 57 | * @var RegionInterface 58 | */ 59 | protected $region; 60 | 61 | /** 62 | * @var string 63 | */ 64 | protected $token; 65 | 66 | /** 67 | * @var string 68 | */ 69 | protected $clientVersion; 70 | 71 | /** 72 | * @var string 73 | */ 74 | protected $locale; 75 | 76 | /** 77 | * @var int 78 | */ 79 | protected $port; 80 | 81 | /** 82 | * @var string 83 | */ 84 | protected $error; 85 | 86 | /** 87 | * @var bool 88 | */ 89 | protected $isAuthenticated = false; 90 | 91 | /** 92 | * @var int 93 | */ 94 | protected $lastCall = 0; 95 | 96 | /** 97 | * @var int The connected user account id, used by heartbeat call 98 | */ 99 | protected $accountId; 100 | 101 | /** 102 | * @var int 103 | */ 104 | protected $heartBeatCount = 0; 105 | 106 | 107 | /** 108 | * @param Client $redis 109 | * @param LoggerInterface $logger 110 | * @param int $clientId 111 | * @param RegionInterface $region 112 | * @param string $username 113 | * @param string $password 114 | * @param string $clientVersion 115 | * @param string $locale 116 | * @param int $port 117 | */ 118 | public function __construct(LoggerInterface $logger, Client $redis, $clientId, RegionInterface $region, $username, $password, $clientVersion, $locale, $port) 119 | { 120 | $this->redis = $redis; 121 | $this->clientId = $clientId; 122 | $this->region = $region; 123 | $this->username = $username; 124 | $this->password = $password; 125 | $this->clientVersion = $clientVersion; 126 | $this->locale = $locale; 127 | $this->port = $port; 128 | 129 | parent::__construct($logger, $this->region->getServer(), 2099, '', 'app:/mod_ser.dat', null); 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function authenticate() 136 | { 137 | try { 138 | $this->connect(); 139 | $this->login(); 140 | 141 | $this->isAuthenticated = true; 142 | } 143 | catch (ClientException $e) { 144 | $this->logger->error('Client ' . $this . ' cannot authenticate : ' . $e->getMessage()); 145 | 146 | return false; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * Login process 154 | */ 155 | protected function login() 156 | { 157 | $ipAddress = $this->getIpAddress(); 158 | $this->token = $this->getAuthToken(); 159 | 160 | $body = new \SabreAMF_TypedObject('com.riotgames.platform.login.AuthenticationCredentials', array( 161 | 'username' => $this->username, 162 | 'password' => $this->password, 163 | 'authToken' => $this->token, 164 | 'clientVersion' => $this->clientVersion, 165 | 'ipAddress' => $ipAddress, 166 | 'locale' => $this->locale, 167 | 'domain' => 'lolclient.lol.riotgames.com', 168 | 'operatingSystem' => 'LoLRTMPSClient', 169 | 'securityAnswer' => null, 170 | 'oldPassword' => null, 171 | 'partnerCredentials' => null 172 | )); 173 | 174 | $response = $this->syncInvoke('loginService', 'login', array( 175 | $body 176 | )); 177 | 178 | // Checking errors 179 | if ('_error' == $response['result']) { 180 | $root = $response['data']->getData()->rootCause->getAMFData(); 181 | if ('com.riotgames.platform.login.impl.ClientVersionMismatchException' == $root['rootCauseClassname']) { 182 | $newVersion = $root['substitutionArguments'][1]; 183 | 184 | $updateVersion = function () use ($newVersion) { 185 | $this->logger->alert('Your client version configuration is outdated, it will be automatically updated with this version : ' . $newVersion . ' (configuration key: client.version)'); 186 | $this->logger->alert('Automatic restart with the new client version...'); 187 | 188 | // Save the new version into the config.yml file 189 | $filePath = __DIR__ . '/../../../../config/config.yml'; 190 | $configs = ConfigurationLoader::getAll(); 191 | $configs['config']['client']['version'] = $newVersion; 192 | 193 | $dumper = new Dumper(); 194 | file_put_contents($filePath, $dumper->dump($configs, 99)); 195 | }; 196 | 197 | // Avoid multiple warnings in async 198 | if (true === ConfigurationLoader::get('client.async.enabled')) { 199 | $key = ConfigurationLoader::get('client.async.redis.key') . '.clients.errors.wrong_version'; 200 | if (null === $this->redis->get($key)) { 201 | $updateVersion(); 202 | 203 | $this->redis->set($key, true); 204 | } 205 | } 206 | else { 207 | $updateVersion(); 208 | } 209 | 210 | $this->clientVersion = $newVersion; 211 | 212 | return $this->reconnect(); 213 | } 214 | elseif ('com.riotgames.platform.login.LoginFailedException' == $root['rootCauseClassname']) { 215 | $this->logger->warning('Client ' . $this . ': error on authentication (normal in case of busy server). Restarting client...'); 216 | sleep(1); 217 | 218 | return $this->reconnect(); 219 | } 220 | } 221 | 222 | $data = $response['data']->getData(); 223 | $body = $data->body->getAMFData(); 224 | $token = $body['token']; 225 | $this->accountId = $body['accountSummary']->getAMFData()['accountId']; 226 | 227 | $authToken = strtolower($this->username) . ':' . $token; 228 | $authToken = base64_encode($authToken); 229 | 230 | $this->syncInvoke('auth', 8, $authToken, 'flex.messaging.messages.CommandMessage'); 231 | 232 | $this->syncInvoke('messagingDestination', 0, null, 'flex.messaging.messages.CommandMessage', array( 233 | 'DSSubtopic' => 'bc' 234 | ), array( 235 | 'clientId' => 'bc-' . $this->accountId 236 | )); 237 | 238 | $this->syncInvoke("messagingDestination", 0, null, "flex.messaging.messages.CommandMessage", array( 239 | 'DSSubtopic' => 'cn-' . $this->accountId 240 | ), array( 241 | 'clientId' => 'cn-' . $this->accountId 242 | )); 243 | 244 | $this->syncInvoke("messagingDestination", 0, null, "flex.messaging.messages.CommandMessage", array( 245 | 'DSSubtopic' => 'gn-' . $this->accountId 246 | ), array( 247 | 'clientId' => 'gn-' . $this->accountId 248 | )); 249 | } 250 | 251 | /** 252 | * {@inheritdoc} 253 | */ 254 | public function reconnect() 255 | { 256 | $this->socket->shutdown(); 257 | $this->socket = null; 258 | $this->token = null; 259 | $this->DSId = null; 260 | $this->accountId = null; 261 | $this->invokeId = 1; 262 | $this->heartBeatCount = 0; 263 | $this->startTime = time(); 264 | $this->lastCall = 0; 265 | 266 | $this->authenticate(); 267 | 268 | return true; 269 | } 270 | 271 | /** 272 | * Return the login server ip address 273 | * 274 | * @return string 275 | */ 276 | protected function getIpAddress() 277 | { 278 | $response = file_get_contents('http://ll.leagueoflegends.com/services/connection_info'); 279 | 280 | // In case of site down 281 | if (false === $response) { 282 | return '127.0.0.1'; 283 | } 284 | 285 | $data = json_decode($response, true); 286 | 287 | return $data['ip_address']; 288 | } 289 | 290 | /** 291 | * @return string 292 | * 293 | * @throws \RuntimeException When a configuration error occurred 294 | * @throws Exception\AuthException When an unknown auth error occurred 295 | * @throws Exception\ServerBusyException When server is too busy 296 | * @throws Exception\BadCredentialsException When credentials are wrong 297 | */ 298 | protected function getAuthToken() 299 | { 300 | if (!function_exists('curl_init')) { 301 | throw new \RuntimeException('cURL is needed, please install the php-curl extension'); 302 | } 303 | 304 | // Create the parameters query 305 | $response = $this->readURL(self::URL_AUTHENTICATE, array( 306 | 'user' => $this->username, 307 | 'password' => $this->password 308 | )); 309 | 310 | if ('FAILED' == $response['status']) { 311 | if ('invalid_credentials' == $response['reason']) { 312 | throw new BadCredentialsException('Bad credentials'); 313 | } 314 | 315 | throw new AuthException('Error when logging : ' . $response['reason']); 316 | } 317 | elseif ('BUSY' == $response['status']) { 318 | $waitTime = ConfigurationLoader::get('client.authentication.busy.wait'); 319 | $this->logger->alert('Client ' . $this . ': the server is currently busy. Restarting client in ' . $waitTime . ' seconds...'); 320 | sleep($waitTime); 321 | 322 | return $this->reconnect(); 323 | } 324 | 325 | // Login queue process 326 | if (!isset($response['token'])) { 327 | $response = $this->queueProcess($response); 328 | } 329 | 330 | return $response['token']; 331 | } 332 | 333 | /** 334 | * The queue process, retry until we got the token 335 | * 336 | * @param array $response 337 | * 338 | * @return array 339 | */ 340 | protected function queueProcess($response) 341 | { 342 | $username = $response['user']; 343 | $id = 0; $current = 0; 344 | $delay = $response['delay']; 345 | $tickers = $response['tickers']; 346 | 347 | $log = function ($regionName, $position) { 348 | $this->logger->info('Client ' . $this . ': in login queue (' . $regionName . '), #' . $position); 349 | }; 350 | 351 | foreach ($tickers as $ticker) { 352 | $tickerNode = $ticker['node']; 353 | if ($tickerNode != $response['node']) { 354 | continue; 355 | } 356 | 357 | $id = $ticker['id']; 358 | $current = $ticker['current']; 359 | 360 | break; 361 | } 362 | 363 | $log($this->region->getUniqueName(), $id - $current); 364 | 365 | // Retry 366 | while (($id - $current) > $response['rate']) { 367 | usleep($delay); 368 | 369 | $response = $this->readURL(self::URL_TICKER . '/' . $response['champ']); // champ = queue name 370 | if (null == $response) { 371 | continue; 372 | } 373 | 374 | $current = hexdec($response['node']); 375 | 376 | $log($this->region->getUniqueName(), max(1, $id - $current)); 377 | } 378 | 379 | // Retry for the token 380 | $response = $this->readURL(self::URL_TOKEN . '/' . $username); 381 | while (null == $response || !isset($response['token'])) { 382 | usleep($delay / 10); 383 | 384 | $response = $this->readURL(self::URL_TOKEN . '/' . $username); 385 | } 386 | 387 | return $response; 388 | } 389 | 390 | /** 391 | * @param string $url 392 | * @param array $parameters 393 | * 394 | * @return array 395 | * 396 | * @throws \RuntimeException When a configuration error occurred 397 | * @throws Exception\AuthException When an unknown auth error occurred 398 | */ 399 | protected function readURL($url, array $parameters = null) 400 | { 401 | $ch = curl_init(); 402 | if (false === $ch) { 403 | throw new \RuntimeException('Failed to initialize cURL'); 404 | } 405 | 406 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; ' . str_replace('_', '-', $this->locale) . ' AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/3.7'); 407 | curl_setopt($ch, CURLOPT_REFERER, 'app:/LolClient.swf/[[DYNAMIC]]/6'); 408 | 409 | curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', $this->region->getLoginQueue(), $url)); 410 | curl_setopt($ch, CURLOPT_VERBOSE, false); 411 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 412 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 413 | curl_setopt($ch, CURLOPT_HEADER, false); 414 | curl_setopt($ch, CURLOPT_TIMEOUT, 10); 415 | 416 | if (null != $parameters) { 417 | curl_setopt($ch, CURLOPT_POST, true); 418 | curl_setopt($ch, CURLOPT_POSTFIELDS, 'payload=' . http_build_query($parameters)); 419 | } 420 | 421 | $response = curl_exec($ch); 422 | if (false === $response) { 423 | throw new AuthException('Fail to get the login response, error : ' . curl_error($ch)); 424 | } 425 | 426 | curl_close($ch); 427 | 428 | return json_decode($response, true); 429 | } 430 | 431 | /** 432 | * {@inheritdoc} 433 | */ 434 | public function invoke($destination, $operation, $parameters = array(), $callback = null, $packetClass = 'flex.messaging.messages.RemotingMessage', $headers = array(), $body = array()) 435 | { 436 | if (null === $this->socket) { 437 | throw new ClientNotReadyException('The client is not ready, please authenticate it before sending a request'); 438 | } 439 | 440 | $this->lastCall = microtime(true) + 0.03; 441 | 442 | try { 443 | return parent::invoke($destination, $operation, $parameters, $callback, $packetClass, $headers, $body); 444 | } 445 | catch (ClientKickedException $e) { 446 | $this->logger->warning($e->getMessage()); 447 | $this->reconnect(); 448 | sleep(1); 449 | 450 | return $this->invoke($destination, $operation, $parameters, $callback, $packetClass, $headers, $body); 451 | } 452 | catch (RequestTimeoutException $e) { 453 | $e->setClient($this); 454 | 455 | throw $e; 456 | } 457 | } 458 | 459 | /** 460 | * {@inheritdoc} 461 | */ 462 | public function doHeartBeat() 463 | { 464 | return $this->invoke('loginService', 'performLCDSHeartBeat', [ 465 | $this->accountId, 466 | strtolower($this->DSId), 467 | ++$this->heartBeatCount, 468 | date('D M j Y H:i:s') . ' GMT' . date('O') 469 | ]); 470 | } 471 | 472 | /** 473 | * @return array The heartbeat result 474 | */ 475 | public function getHeartBeat() 476 | { 477 | return $this->getResult($this->doHeartBeat())[0]; 478 | } 479 | 480 | /** 481 | * {@inheritdoc} 482 | */ 483 | public function getId() 484 | { 485 | return $this->clientId; 486 | } 487 | 488 | /** 489 | * {@inheritdoc} 490 | */ 491 | public function getError() 492 | { 493 | return $this->error; 494 | } 495 | 496 | /** 497 | * {@inheritdoc} 498 | */ 499 | public function getRegion() 500 | { 501 | return $this->region->getUniqueName(); 502 | } 503 | 504 | /** 505 | * {@inheritdoc} 506 | */ 507 | public function isAuthenticated() 508 | { 509 | return $this->isAuthenticated; 510 | } 511 | 512 | /** 513 | * {@inheritdoc} 514 | */ 515 | public function getPort() 516 | { 517 | return $this->port; 518 | } 519 | 520 | /** 521 | * {@inheritdoc} 522 | */ 523 | public function isAvailable() 524 | { 525 | return $this->lastCall <= microtime(true); 526 | } 527 | 528 | /** 529 | * {@inheritdoc} 530 | */ 531 | public function setIsOverloaded() 532 | { 533 | $this->lastCall += (int) ConfigurationLoader::get('client.request.overload.wait'); 534 | } 535 | 536 | /** 537 | * {@inheritdoc} 538 | */ 539 | public function __toString() 540 | { 541 | return sprintf('#%d (%s)', $this->clientId, $this->getRegion()); 542 | } 543 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/LOLClientAsync.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class LOLClientAsync implements LOLClientInterface 24 | { 25 | /** 26 | * @var LoggerInterface 27 | */ 28 | protected $logger; 29 | 30 | /** 31 | * @var string 32 | */ 33 | protected $rootFolder; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $pidPath; 39 | 40 | /** 41 | * @var int 42 | */ 43 | protected $accountKey; 44 | 45 | /** 46 | * @var int 47 | */ 48 | protected $clientId; 49 | 50 | /** 51 | * @var RegionInterface 52 | */ 53 | protected $region; 54 | 55 | /** 56 | * @var int 57 | */ 58 | protected $port; 59 | 60 | /** 61 | * @var string 62 | */ 63 | protected $error; 64 | 65 | /** 66 | * @var \ZMQSocket 67 | */ 68 | protected $con; 69 | 70 | /** 71 | * @var Client 72 | */ 73 | protected $redis; 74 | 75 | /** 76 | * @var int 77 | */ 78 | protected $lastCall = 0; 79 | 80 | /** 81 | * @var array 82 | */ 83 | protected static $callbacks = []; 84 | 85 | 86 | /** 87 | * @param LoggerInterface $logger 88 | * @param Client $redis 89 | * @param int $accountKey 90 | * @param int $clientId 91 | * @param RegionInterface $region 92 | * @param int $port 93 | */ 94 | public function __construct(LoggerInterface $logger, Client $redis, $accountKey, $clientId, RegionInterface $region, $port) 95 | { 96 | $rootFolder = __DIR__ . '/../../../..'; 97 | 98 | $this->logger = $logger; 99 | $this->rootFolder = $rootFolder; 100 | $this->pidPath = $rootFolder . '/' . ConfigurationLoader::get('cache.path') . '/clientpids/client_' . $clientId . '.pid'; 101 | $this->redis = $redis; 102 | $this->accountKey = $accountKey; 103 | $this->clientId = $clientId; 104 | $this->region = $region; 105 | $this->port = $port; 106 | $this->con = new \ZMQSocket(new \ZMQContext(), \ZMQ::SOCKET_PUSH); 107 | 108 | $this->connect(); 109 | } 110 | 111 | /** 112 | * Create the LOLClient instance and connect it with the worker 113 | */ 114 | private function connect() 115 | { 116 | // Create process 117 | popen(sprintf('%s %s/console elogank:client:create %d %d > /dev/null 2>&1 & echo $! > %s', ConfigurationLoader::get('php.executable'), $this->rootFolder, $this->accountKey, $this->clientId, $this->pidPath), 'r'); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function authenticate() 124 | { 125 | $this->con->connect('tcp://127.0.0.1:' . $this->port); 126 | 127 | $this->send('authenticate', array(), $this->clientId . '.authenticate'); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function invoke($destination, $operation, $parameters = array(), $callback = null, $packetClass = 'flex.messaging.messages.RemotingMessage', $headers = array(), $body = array()) 134 | { 135 | $invokeId = $this->send('syncInvoke', [ 136 | $destination, 137 | $operation, 138 | $parameters 139 | ]); 140 | 141 | if (null !== $callback) { 142 | self::$callbacks[$invokeId] = $callback; 143 | } 144 | 145 | return $invokeId; 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function getResult($invokeId) 152 | { 153 | $message = $this->redis->rpop($this->getKey('client.commands.' . $invokeId)); 154 | if (null == $message) { 155 | return null; 156 | } 157 | 158 | // Callback process 159 | $callback = null; 160 | if (isset(self::$callbacks[$invokeId])) { 161 | $callback = self::$callbacks[$invokeId]; 162 | unset(self::$callbacks[$invokeId]); 163 | } 164 | 165 | return [unserialize($message), $callback]; 166 | } 167 | 168 | /** 169 | * {@inheritdoc} 170 | */ 171 | public function isAuthenticated() 172 | { 173 | $message = $this->redis->rpop($this->getKey('client.commands.' . $this->clientId . '.authenticate')); 174 | if (null == $message) { 175 | return null; 176 | } 177 | 178 | return unserialize($message); 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function getId() 185 | { 186 | return $this->clientId; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function getRegion() 193 | { 194 | return $this->region->getUniqueName(); 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function getError() 201 | { 202 | return $this->error; 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function getPort() 209 | { 210 | return $this->port; 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function isAvailable() 217 | { 218 | return $this->lastCall <= microtime(true); 219 | } 220 | 221 | /** 222 | * {@inheritdoc} 223 | */ 224 | public function setIsOverloaded() 225 | { 226 | $this->lastCall += (int) ConfigurationLoader::get('client.request.overload.wait'); 227 | } 228 | 229 | /** 230 | * @param string $key 231 | * 232 | * @return string 233 | */ 234 | protected function getKey($key) 235 | { 236 | return ConfigurationLoader::get('client.async.redis.key') . '.' . $key; 237 | } 238 | 239 | /** 240 | * @param string $commandName 241 | * @param array $parameters 242 | * @param int|string|null $invokeId 243 | * 244 | * @return int|string 245 | */ 246 | protected function send($commandName, array $parameters = array(), $invokeId = null) 247 | { 248 | if (null == $invokeId) { 249 | $invokeId = $this->redis->incr($this->getKey('invokeId')); 250 | } 251 | 252 | $nextAvailableTime = (float) ConfigurationLoader::get('client.request.overload.available'); 253 | $this->lastCall = microtime(true) + $nextAvailableTime; 254 | $this->con->send(json_encode([ 255 | 'invokeId' => $invokeId, 256 | 'command' => $commandName, 257 | 'parameters' => $parameters 258 | ]), \ZMQ::MODE_DONTWAIT); 259 | 260 | return $invokeId; 261 | } 262 | 263 | /** 264 | * {@inheritdoc} 265 | */ 266 | public function doHeartBeat() 267 | { 268 | return $this->send('getHeartBeat'); 269 | } 270 | 271 | /** 272 | * {@inheritdoc} 273 | */ 274 | public function reconnect() 275 | { 276 | $this->logger->debug('Client ' . $this . ': reconnect process has been requested, kill the old process and create a new one'); 277 | 278 | Process::killProcess($this->pidPath, true, $this->logger, $this); 279 | $this->redis->del($this->getKey('invokeId')); 280 | $this->connect(); 281 | $this->authenticate(); 282 | } 283 | 284 | /** 285 | * {@inheritdoc} 286 | */ 287 | public function __toString() 288 | { 289 | return sprintf('async #%d (%s)', $this->clientId, $this->getRegion()); 290 | } 291 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/LOLClientInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface LOLClientInterface 20 | { 21 | /** 22 | * Authenticate the client 23 | */ 24 | public function authenticate(); 25 | 26 | /** 27 | * Return true if the client has been successfully authenticated, false otherwise,
28 | * see getError() method to retrieve the error 29 | * 30 | * @return bool 31 | */ 32 | public function isAuthenticated(); 33 | 34 | /** 35 | * Clean all variables and reconnect 36 | */ 37 | public function reconnect(); 38 | 39 | /** 40 | * Return the client unique id 41 | * 42 | * @return int 43 | */ 44 | public function getId(); 45 | 46 | /** 47 | * Return the region unique name (EUW, NA, ...) 48 | * 49 | * @return string 50 | */ 51 | public function getRegion(); 52 | 53 | /** 54 | * When an error has occurred, you can retrieve it with this method 55 | * 56 | * @return string 57 | */ 58 | public function getError(); 59 | 60 | /** 61 | * Used by asynchronous client, return the client worker port 62 | * 63 | * @return int 64 | */ 65 | public function getPort(); 66 | 67 | /** 68 | * Return true if the client is available to handle a new request, false otherwise 69 | * 70 | * @return bool 71 | */ 72 | public function isAvailable(); 73 | 74 | /** 75 | * Flag the client as overloaded for a while 76 | */ 77 | public function setIsOverloaded(); 78 | 79 | /** 80 | * Invoke a new RTMP service method 81 | * 82 | * @param string $destination The service manager name 83 | * @param string $operation The service method name 84 | * @param array $parameters The service method parameters 85 | * @param callable|Callback|null $callback The callback will be called after parsing the packet, 86 | * and retrieving the result.
It must return the final result 87 | * @param string $packetClass The packet class for the body 88 | * @param array $headers The additional headers 89 | * @param array $body The additional body 90 | * 91 | * @return int The invoke unique id 92 | */ 93 | public function invoke($destination, $operation, $parameters = array(), $callback = null, $packetClass = 'flex.messaging.messages.RemotingMessage', $headers = array(), $body = array()); 94 | 95 | /** 96 | * Do heartbeat to avoid being disconnected after being inactive 97 | * 98 | * @return int|array If int: the invoke id, otherwise it's the result array, depending if it's an async client or not 99 | */ 100 | public function doHeartBeat(); 101 | 102 | /** 103 | * @param int $invokeId The invoke unique id 104 | * 105 | * @return array The index 0 is the result himself, and the index 1 is the callback, if provided 106 | */ 107 | public function getResult($invokeId); 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function __toString(); 113 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/RTMP/RTMPClient.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class RTMPClient 26 | { 27 | /** 28 | * @var LoggerInterface 29 | */ 30 | protected $logger; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $server; 36 | 37 | /** 38 | * @var int 39 | */ 40 | protected $port; 41 | 42 | /** 43 | * @var string 44 | */ 45 | private $app; 46 | 47 | /** 48 | * @var string 49 | */ 50 | private $swfUrl; 51 | 52 | /** 53 | * @var string 54 | */ 55 | private $pageUrl; 56 | 57 | /** 58 | * @var bool 59 | */ 60 | private $isSecure; 61 | 62 | /** 63 | * @var int 64 | */ 65 | protected $startTime; 66 | 67 | /** 68 | * @var int 69 | */ 70 | protected $DSId; 71 | 72 | /** 73 | * @var int 74 | */ 75 | protected $invokeId = 1; 76 | 77 | /** 78 | * @var RTMPSocket 79 | */ 80 | protected $socket; 81 | 82 | /** 83 | * @var array 84 | */ 85 | protected $responses = []; 86 | 87 | 88 | /** 89 | * @param LoggerInterface $logger 90 | * @param string $server 91 | * @param string $port 92 | * @param string $app 93 | * @param string $swfUrl 94 | * @param string $pageUrl 95 | * @param bool $isSecure 96 | */ 97 | public function __construct(LoggerInterface $logger, $server, $port, $app, $swfUrl, $pageUrl, $isSecure = true) 98 | { 99 | $this->logger = $logger; 100 | $this->server = $server; 101 | $this->port = $port; 102 | $this->app = $app; 103 | $this->swfUrl = $swfUrl; 104 | $this->pageUrl = $pageUrl; 105 | $this->isSecure = $isSecure; 106 | 107 | $this->startTime = time(); 108 | } 109 | 110 | /** 111 | * @return array 112 | */ 113 | public function connect() 114 | { 115 | $this->createSocket(); 116 | 117 | $this->doHandshake(); 118 | $this->encodeConnectionPacket(); 119 | 120 | return $this->getResponse(); 121 | } 122 | 123 | /** 124 | * @throws \RuntimeException 125 | */ 126 | protected function createSocket() 127 | { 128 | $protocol = 'tcp'; 129 | if ($this->isSecure) { 130 | $protocol = 'ssl'; 131 | } 132 | 133 | $this->socket = new RTMPSocket($protocol, $this->server, $this->port); 134 | if ($this->socket->hasError()) { 135 | throw new AuthException('Error when connecting to server: ' . $this->socket->getErrorMessage()); 136 | } 137 | } 138 | 139 | /** 140 | * Do the handshake 141 | * 142 | * @throws AuthException 143 | */ 144 | protected function doHandshake() 145 | { 146 | // C0 147 | $this->socket->writeBytes(0x03); 148 | 149 | // C1 150 | $this->socket->writeInt(time()); 151 | $this->socket->writeInt(0); 152 | $randC1 = str_pad("", 1528, 'x'); // used later 153 | $this->socket->write($randC1); 154 | 155 | // S0 156 | $version = $this->socket->readBytes(); 157 | if (0x03 != $version) { 158 | throw new AuthException('Wrong handshake version (' . $version . ')'); 159 | } 160 | 161 | // S1 162 | $sign = $this->socket->read(1536); 163 | 164 | // C2 165 | $this->socket->write($sign, 0, 4); 166 | $this->socket->writeInt(time()); 167 | $this->socket->write($sign, 8); 168 | 169 | // S2 170 | $sign = $this->socket->read(1536); 171 | 172 | for ($i=8; $i<1536; $i++) { 173 | if ($randC1[$i - 8] != $sign[$i]) { 174 | throw new AuthException('Invalid handshake'); 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * Get the packet response 181 | * 182 | * @return array 183 | * 184 | * @throws \EloGank\Api\Client\Exception\AuthException 185 | */ 186 | protected function getResponse() 187 | { 188 | $response = $this->parsePacket(); 189 | 190 | if ('NetConnection.Connect.Success' != $response['data']['code']) { 191 | throw new AuthException('Connection failed'); 192 | } 193 | 194 | $this->DSId = $response['data']['id']; 195 | 196 | return $response; 197 | } 198 | 199 | /** 200 | * Create and encode the connection packet 201 | */ 202 | protected function encodeConnectionPacket() 203 | { 204 | $parameters = array( 205 | 'app' => $this->app, 206 | 'flashVer' => 'WIN 10,1,85,3', 207 | 'swfUrl' => $this->swfUrl, 208 | 'tcUrl' => sprintf('rtmps://%s:%d', $this->server, $this->port), 209 | 'fpad' => false, 210 | 'capabilities' => 239, 211 | 'audioCodecs' => 3191, 212 | 'videoCodecs' => 252, 213 | 'pageUrl' => $this->pageUrl, 214 | 'objectEncoding' => 3 215 | ); 216 | 217 | $output = new \SabreAMF_OutputStream(); 218 | $amf3 = new \SabreAMF_AMF3_Serializer($output); 219 | $amf = new \SabreAMF_AMF0_Serializer($output); 220 | 221 | $amf->writeAMFData('connect'); 222 | $amf->writeAMFData(1); // the invokeId 223 | 224 | // Parameters 225 | $output->writeByte(0x11); // AMF3 object 226 | $output->writeByte(0x09); // array 227 | 228 | $output->writeByte(0x01); 229 | foreach($parameters as $name => $value) { 230 | $amf3->writeString($name); 231 | $amf3->writeAMFData($value); 232 | } 233 | $output->writeByte(0x01); 234 | 235 | // Service call arguments 236 | $output->writeByte(0x01); 237 | $output->writeByte(0x00); 238 | $amf->writeAMFData('nil'); 239 | $amf->writeAMFData('', \SabreAMF_AMF0_Const::DT_STRING); 240 | 241 | $commandMessageObject = new \SabreAMF_AMF3_CommandMessage(); 242 | $commandData = array( 243 | 'messageRefType' => null, 244 | 'operation' => 5, 245 | 'correlationId' => '', 246 | 'clientId' => null, 247 | 'destination' => null, 248 | 'messageId' => $commandMessageObject->generateRandomId(), 249 | 'timestamp' => 0.0, 250 | 'timeToLive' => 0.0, 251 | 'body' => new \SabreAMF_TypedObject('', array()), 252 | 'header' => array( 253 | 'DSMessagingVersion' => 1.0, 254 | 'DSId' => 'my-rtmps' 255 | ) 256 | ); 257 | 258 | $commandMessage = new \SabreAMF_TypedObject("flex.messaging.messages.CommandMessage", $commandData); 259 | $output->writeByte(0x11); // amf3 260 | $amf3->writeAMFData($commandMessage); 261 | 262 | $packet = $this->addHeaders($output->getRawData()); 263 | $packet[7] = chr(0x14); // message type 264 | 265 | $this->socket->write($packet); 266 | } 267 | 268 | /** 269 | * @return array 270 | * 271 | * @throws PacketException 272 | * @throws ClientKickedException 273 | */ 274 | protected function parsePacket() 275 | { 276 | $packets = array(); 277 | 278 | while (true) { 279 | $headerBasic = ord($this->socket->read(1)); 280 | $channel = $headerBasic & 0x2F; 281 | $headerType = $headerBasic & 0xC0; 282 | $headerSize = 0; 283 | 284 | switch ($headerType) { 285 | case 0x00: 286 | $headerSize = 12; 287 | break; 288 | case 0x40: 289 | $headerSize = 8; 290 | break; 291 | case 0x80: 292 | $headerSize = 4; 293 | break; 294 | case 0xC0: 295 | $headerSize = 1; 296 | break; 297 | } 298 | 299 | if (!isset($packets[$channel])) { 300 | $packets[$channel] = array( 301 | 'data' => '' 302 | ); 303 | } 304 | 305 | $packet = &$packets[$channel]; 306 | 307 | // Parse the header 308 | if ($headerSize > 1) { 309 | $header = $this->socket->read($headerSize - 1); 310 | 311 | if ($headerSize >= 8) { 312 | $size = 0; 313 | for ($i = 3; $i < 6; $i++) { 314 | $size *= 256; 315 | $size += (ord(substr($header, $i, 1)) & 0xFF); 316 | } 317 | 318 | $packet['size'] = $size; 319 | $packet['type'] = ord($header[6]); 320 | } 321 | } 322 | 323 | // Parse the content 324 | for ($i = 0; $i < 128; $i++) { 325 | if (!feof($this->socket->getSocket())) { 326 | $packet['data'] .= $this->socket->read(1); 327 | } 328 | 329 | if (strlen($packet['data']) == $packet['size']) { 330 | break; 331 | } 332 | } 333 | 334 | if (!(strlen($packet['data']) == $packet['size'])) { 335 | continue; 336 | } 337 | 338 | // Remove the read packet 339 | unset($packets[$channel]); 340 | 341 | $result = array(); 342 | $input = new \SabreAMF_InputStream($packet['data']); 343 | 344 | switch ($packet['type']) { 345 | case 0x14: // decode connect 346 | $decoder = new \SabreAMF_AMF0_Deserializer($input); 347 | $result['result'] = $decoder->readAMFData(); 348 | $result['invokeId'] = $decoder->readAMFData(); 349 | $result['serviceCall'] = $decoder->readAMFData(); 350 | $result['data'] = $decoder->readAMFData(); 351 | 352 | try { 353 | $input->readByte(); 354 | 355 | throw new PacketException('id not consume entire buffer'); 356 | } 357 | catch (\Exception $e) { 358 | // good 359 | } 360 | break; 361 | 362 | case 0x11: 363 | if ($input->readByte() == 0x00) { 364 | $packet['data'] = substr($packet['data'], 1); 365 | $result['version'] = 0x00; 366 | } 367 | 368 | $decoder = new \SabreAMF_AMF0_Deserializer($input); 369 | $result['result'] = $decoder->readAMFData(); 370 | $result['invokeId'] = $decoder->readAMFData(); 371 | $result['serviceCall'] = $decoder->readAMFData(); 372 | $result['data'] = $decoder->readAMFData(); 373 | 374 | try { 375 | $input->readByte(); 376 | 377 | throw new PacketException('id not consume entire buffer'); 378 | } 379 | catch (\Exception $e) { 380 | // good 381 | } 382 | break; 383 | 384 | case 0x03: // ack 385 | case 0x06: // bandwidth 386 | continue 2; 387 | default: 388 | throw new PacketException('Unknown message type'); 389 | } 390 | 391 | if (!isset($result['invokeId'])) { 392 | // The client has been kicked, someone connect to the same account with another API instance or from the desktop launcher 393 | if ('receive' == $result['result'] && 'com.riotgames.platform.messaging.ClientLoginKickNotification' == $result['data']->getData()->getAMFData()['body']->getAMFClassName()) { 394 | throw new ClientKickedException('Someone is connected with the same account, only one instance can running. Restarting client...'); 395 | } 396 | 397 | throw new PacketException("Error after decoding packet"); 398 | } 399 | 400 | $invokeId = $result['invokeId']; 401 | 402 | if ($invokeId == null || $invokeId == 0) { 403 | throw new PacketException('Unknown invokeId: ' . $invokeId); 404 | } 405 | 406 | return $result; 407 | } 408 | } 409 | 410 | /** 411 | * Add header content to data 412 | * 413 | * @param string $data 414 | * 415 | * @return string 416 | */ 417 | protected function addHeaders($data) 418 | { 419 | // Header 420 | $result = chr(0x03); 421 | 422 | // Timestamp 423 | $diff = (int)(microtime(true) * 1000 - $this->startTime); 424 | $result .= chr((($diff & 0xFF0000) >> 16)); 425 | $result .= chr((($diff & 0x00FF00) >> 8)); 426 | $result .= chr((($diff & 0x0000FF))); 427 | 428 | // Body size 429 | $result .= chr(((strlen($data) & 0xFF0000) >> 16)); 430 | $result .= chr(((strlen($data) & 0x00FF00) >> 8)); 431 | $result .= chr(((strlen($data) & 0x0000FF))); 432 | 433 | // Content type 434 | $result .= chr(0x11); 435 | 436 | // Source ID 437 | $result .= chr(0x00); 438 | $result .= chr(0x00); 439 | $result .= chr(0x00); 440 | $result .= chr(0x00); 441 | 442 | $length = strlen($data); 443 | for ($i = 0; $i < $length; $i++) { 444 | $result .= $data[$i]; 445 | 446 | if ($i % 128 == 127 && $i != $length - 1){ 447 | $result .= chr(0xC3); 448 | } 449 | } 450 | 451 | return $result; 452 | } 453 | 454 | /** 455 | * {@inheritdoc} 456 | */ 457 | public function invoke($destination, $operation, $parameters = array(), $callback = null, $packetClass = 'flex.messaging.messages.RemotingMessage', $headers = array(), $body = array()) 458 | { 459 | $packet = new RTMPPacket($destination, $operation, $parameters, $packetClass, $headers, $body); 460 | $packet->build($this->DSId); 461 | 462 | $output = new \SabreAMF_OutputStream(); 463 | $amf = new \SabreAMF_AMF0_Serializer($output); 464 | $amf3 = new \SabreAMF_AMF3_Serializer($output); 465 | 466 | $invokeId = ++$this->invokeId; 467 | 468 | $output->writeByte(0x00); 469 | $output->writeByte(0x05); 470 | $amf->writeAMFData($invokeId); 471 | $output->writeByte(0x05); 472 | $output->writeByte(0x11); 473 | $amf3->writeAMFData($packet->getData()); 474 | $ret = $this->addHeaders($output->getRawData()); 475 | 476 | $this->socket->write($ret); 477 | 478 | $this->responses[$invokeId] = [$this->parsePacket(), $callback]; 479 | 480 | return $invokeId; 481 | } 482 | 483 | /** 484 | * Call the invoke and return the response, without calling getResult() method
485 | * Callbacks are not possible with this method 486 | * 487 | * @param string $destination 488 | * @param string $operation 489 | * @param array $parameters 490 | * @param string $packetClass 491 | * @param array $headers 492 | * @param array $body 493 | * 494 | * @return array 495 | */ 496 | public function syncInvoke($destination, $operation, $parameters = array(), $packetClass = 'flex.messaging.messages.RemotingMessage', $headers = array(), $body = array()) 497 | { 498 | $invokeId = $this->invoke($destination, $operation, $parameters, null, $packetClass, $headers, $body); 499 | 500 | return $this->getResult($invokeId)[0]; 501 | } 502 | 503 | /** 504 | * @param int $invokeId 505 | * 506 | * @return array 507 | * 508 | * @throws \InvalidArgumentException 509 | */ 510 | public function getResult($invokeId) 511 | { 512 | if (!isset($this->responses[$invokeId])) { 513 | throw new \InvalidArgumentException('No result found for invokeId ' . $invokeId); 514 | } 515 | 516 | $resultParams = $this->responses[$invokeId]; 517 | unset($this->responses[$invokeId]); 518 | 519 | return $resultParams; 520 | } 521 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/RTMP/RTMPPacket.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RTMPPacket 18 | { 19 | /** 20 | * @var string 21 | */ 22 | protected $destination; 23 | 24 | /** 25 | * @var string 26 | */ 27 | protected $operation; 28 | 29 | /** 30 | * @var array 31 | */ 32 | protected $parameters; 33 | 34 | /** 35 | * @var array 36 | */ 37 | protected $additionalHeaders; 38 | 39 | /** 40 | * @var \SabreAMF_TypedObject 41 | */ 42 | protected $headers; 43 | 44 | /** 45 | * @var array 46 | */ 47 | protected $additionalBody; 48 | 49 | /** 50 | * @var \SabreAMF_TypedObject 51 | */ 52 | protected $data; 53 | 54 | /** 55 | * @var string 56 | */ 57 | protected $class; 58 | 59 | 60 | /** 61 | * @param string $destination 62 | * @param string $operation 63 | * @param array $parameters 64 | * @param string $packetClass 65 | * @param array $headers 66 | * @param array $body 67 | */ 68 | public function __construct($destination, $operation, $parameters, $packetClass, array $headers = array(), array $body = array()) 69 | { 70 | $this->destination = $destination; 71 | $this->operation = $operation; 72 | $this->parameters = $parameters; 73 | $this->class = $packetClass; 74 | $this->additionalHeaders = $headers; 75 | $this->additionalBody = $body; 76 | } 77 | 78 | /** 79 | * Build the packet's header 80 | * 81 | * @param int $destinationId 82 | */ 83 | public function buildHeader($destinationId) 84 | { 85 | $this->headers = new \SabreAMF_TypedObject(null, array_merge(array( 86 | 'DSRequestTimeout' => 60, 87 | 'DSId' => $destinationId, 88 | 'DSEndpoint' => 'my-rtmps' 89 | ), $this->additionalHeaders)); 90 | } 91 | 92 | /** 93 | * Build the packet's body 94 | */ 95 | public function buildBody() 96 | { 97 | $remoteMessage = new \SabreAMF_AMF3_RemotingMessage(); 98 | $this->data = new \SabreAMF_TypedObject($this->class, array_merge(array( 99 | 'destination' => $this->destination, 100 | 'operation' => $this->operation, 101 | 'source' => null, 102 | 'timestamp' => 0, 103 | 'messageId' => $remoteMessage->generateRandomId(), 104 | 'timeToLive' => 0, 105 | 'clientId' => null, 106 | 'headers' => $this->headers, 107 | 'body' => $this->parameters 108 | ), $this->additionalBody)); 109 | } 110 | 111 | /** 112 | * Build the whole packet 113 | * 114 | * @param int $destinationId 115 | */ 116 | public function build($destinationId) 117 | { 118 | $this->buildHeader($destinationId); 119 | $this->buildBody(); 120 | } 121 | 122 | /** 123 | * @return mixed 124 | */ 125 | public function getData() 126 | { 127 | return $this->data; 128 | } 129 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/RTMP/RTMPSocket.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class RTMPSocket 21 | { 22 | /** 23 | * @var resource 24 | */ 25 | protected $socket; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $errorMessage; 31 | 32 | /** 33 | * @var int 34 | */ 35 | protected $timeout; 36 | 37 | /** 38 | * @param string $protocol 39 | * @param string $server 40 | * @param int $port 41 | */ 42 | public function __construct($protocol, $server, $port) 43 | { 44 | $this->timeout = (int) ConfigurationLoader::get('client.request.timeout'); 45 | if (1 > $this->timeout) { 46 | $this->timeout = 5; 47 | } 48 | 49 | $this->socket = stream_socket_client(sprintf('%s://%s:%d', $protocol, $server, $port), $errorCode, $errorMessage, $this->timeout); 50 | if (0 != $errorCode) { 51 | $this->errorMessage = $errorMessage; 52 | } 53 | else { 54 | stream_set_timeout($this->socket, $this->timeout); 55 | stream_set_blocking($this->socket, false); 56 | } 57 | } 58 | 59 | /** 60 | * @return bool 61 | */ 62 | public function hasError() 63 | { 64 | return null != $this->errorMessage; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getErrorMessage() 71 | { 72 | return $this->errorMessage; 73 | } 74 | 75 | /** 76 | * @param mixed $data 77 | * @param int|null $start 78 | * @param int|null $end 79 | */ 80 | public function write($data, $start = null, $end = null) 81 | { 82 | if (null !== $start && null == $end) { 83 | $data = substr($data, $start); 84 | } 85 | elseif (null !== $start && null !== $end) { 86 | $data = substr($data, $start, $end); 87 | } 88 | 89 | $n = 0; 90 | $length = strlen($data); 91 | 92 | while ($n < $length) { 93 | $n += fwrite($this->socket, $data); 94 | } 95 | } 96 | 97 | /** 98 | * @param mixed $bytes 99 | */ 100 | public function writeBytes($bytes) 101 | { 102 | $this->write(chr($bytes)); 103 | } 104 | 105 | /** 106 | * @param int $int 107 | */ 108 | public function writeInt($int) 109 | { 110 | $this->write(pack('N', $int)); 111 | } 112 | 113 | /** 114 | * @param int $length 115 | * 116 | * @return mixed 117 | * 118 | * @throws RequestTimeoutException 119 | */ 120 | public function read($length = 1) 121 | { 122 | $timeout = time() + $this->timeout; 123 | $output = ''; 124 | 125 | while (time() < $timeout) { 126 | $output .= fread($this->socket, $length); 127 | 128 | if (isset($output[$length - 1])) { 129 | break; 130 | } 131 | 132 | usleep(10); 133 | } 134 | 135 | if (!isset($output[$length - 1])) { 136 | throw new RequestTimeoutException('Request timeout, the client will reconnect'); 137 | } 138 | 139 | return $output; 140 | } 141 | 142 | /** 143 | * @param int $length 144 | * 145 | * @return mixed 146 | */ 147 | public function readBytes($length = 1) 148 | { 149 | return unpack('C', $this->read($length))[1]; 150 | } 151 | 152 | /** 153 | * Shutdown the socket 154 | */ 155 | public function shutdown() 156 | { 157 | stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); 158 | } 159 | 160 | /** 161 | * @return resource 162 | */ 163 | public function getSocket() 164 | { 165 | return $this->socket; 166 | } 167 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Client/Worker/ClientWorker.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ClientWorker 24 | { 25 | /** 26 | * @var LoggerInterface 27 | */ 28 | protected $logger; 29 | 30 | /** 31 | * @var LOLClientInterface 32 | */ 33 | protected $client; 34 | 35 | /** 36 | * @var Client 37 | */ 38 | protected $redis; 39 | 40 | /** 41 | * @var int 42 | */ 43 | protected $expire; 44 | 45 | /** 46 | * @var string 47 | */ 48 | protected $key; 49 | 50 | /** 51 | * @var int 52 | */ 53 | protected $defaultPort; 54 | 55 | 56 | /** 57 | * @param LoggerInterface $logger 58 | * @param LOLClientInterface $client 59 | * @param Client $redis 60 | * 61 | * @throws \Exception 62 | */ 63 | public function __construct(LoggerInterface $logger, LOLClientInterface $client, $redis) 64 | { 65 | $this->logger = $logger; 66 | $this->client = $client; 67 | $this->redis = $redis; 68 | 69 | // Init configuration to handle exception and log them 70 | try { 71 | $this->expire = (int) ConfigurationLoader::get('client.response.expire'); 72 | if ($this->expire < (int) ConfigurationLoader::get('client.request.timeout')) { 73 | $this->expire = (int) ConfigurationLoader::get('client.request.timeout'); 74 | } 75 | 76 | $this->key = ConfigurationLoader::get('client.async.redis.key'); 77 | $this->defaultPort = ConfigurationLoader::get('client.async.startPort'); 78 | } 79 | catch (\Exception $e) { 80 | $this->logger->critical($e->getMessage()); 81 | 82 | throw $e; 83 | } 84 | } 85 | 86 | /** 87 | * Start the worker and wait for requests 88 | */ 89 | public function listen() 90 | { 91 | $context = new \ZMQContext(); 92 | $server = new \ZMQSocket($context, \ZMQ::SOCKET_PULL); 93 | $server->bind('tcp://127.0.0.1:' . ($this->defaultPort + $this->client->getId() - 1)); 94 | 95 | $this->logger->info('Client worker ' . $this->client . ' is ready'); 96 | 97 | while (true) { 98 | $request = $server->recv(); 99 | $this->logger->debug('Client worker ' . $this->client . ' receiving request : ' . $request); 100 | 101 | // Check if the input is valid, ignore if wrong 102 | $request = json_decode($request, true); 103 | if (!$this->isValidInput($request)) { 104 | $this->logger->error('Client worker ' . $this->client . ' received an invalid input'); 105 | 106 | continue; 107 | } 108 | 109 | try { 110 | // Call the right method in the client and push to redis the result 111 | $result = call_user_func_array(array($this->client, $request['command']), $request['parameters']); 112 | } 113 | catch (ClientNotReadyException $e) { 114 | $this->logger->warning('Client worker ' . $this->client . ' received a request (#' . $request['invokeId'] . ') whereas the client is not ready. This is normal in case of client reconnection process. Ignoring.'); 115 | 116 | continue; 117 | } 118 | 119 | $key = $this->key . '.client.commands.' . $request['invokeId']; 120 | 121 | $this->redis->rpush($key, serialize($result)); 122 | $this->redis->expire($key, $this->expire); 123 | } 124 | } 125 | 126 | /** 127 | * @param array $input 128 | * 129 | * @return bool 130 | */ 131 | protected function isValidInput(array $input) 132 | { 133 | if (!isset($input['invokeId']) || !isset($input['command']) || !isset($input['parameters'])) { 134 | return false; 135 | } 136 | 137 | return true; 138 | } 139 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Command/ApiStartCommand.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ApiStartCommand extends Command 25 | { 26 | /** 27 | * Configure the command 28 | */ 29 | protected function configure() 30 | { 31 | $this 32 | ->setName('elogank:api:start') 33 | ->setDescription('Start the EloGank League of Legends API server') 34 | ; 35 | } 36 | 37 | /** 38 | * @param InputInterface $input 39 | * @param OutputInterface $output 40 | * 41 | * @return int|null|void 42 | */ 43 | protected function execute(InputInterface $input, OutputInterface $output) 44 | { 45 | $this->writeSection($output, 'EloGank - League of Legends API'); 46 | 47 | $apiManager = new ApiManager(); 48 | try { 49 | $server = new Server($apiManager); 50 | $server->listen(); 51 | } 52 | catch (\Exception $e) { 53 | $this->getApplication()->renderException($e, $output); 54 | $apiManager->getLogger()->critical($e); 55 | 56 | $apiManager->clean(); 57 | 58 | // Need to be killed manually, see ReactPHP issue: https://github.com/reactphp/react/issues/296 59 | posix_kill(getmypid(), SIGKILL); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Command/ClientCreateCommand.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class ClientCreateCommand extends Command 29 | { 30 | /** 31 | * Configure the command 32 | */ 33 | protected function configure() 34 | { 35 | $this 36 | ->setName('elogank:client:create') 37 | ->setDescription('Create a new API asynchronous client worker') 38 | ->addArgument('account_key', InputArgument::REQUIRED, 'The account key in configuration') 39 | ->addArgument('client_id', InputArgument::REQUIRED, 'The client id') 40 | ; 41 | } 42 | 43 | /** 44 | * @param InputInterface $input 45 | * @param OutputInterface $output 46 | * 47 | * @return int|null|void 48 | */ 49 | protected function execute(InputInterface $input, OutputInterface $output) 50 | { 51 | $redis = new Client(sprintf('tcp://%s:%d', ConfigurationLoader::get('client.async.redis.host'), ConfigurationLoader::get('client.async.redis.port'))); 52 | $logger = LoggerFactory::create('Client #' . $input->getArgument('client_id'), true); 53 | $client = ClientFactory::create( 54 | $logger, 55 | $redis, 56 | $input->getArgument('account_key'), 57 | $input->getArgument('client_id'), 58 | true 59 | ); 60 | 61 | $connector = new ClientWorker($logger, $client, $redis); 62 | $connector->listen(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Command/RouterDumpCommand.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class RouterDumpCommand extends Command 23 | { 24 | /** 25 | * Configure the command 26 | */ 27 | protected function configure() 28 | { 29 | $this 30 | ->setName('elogank:router:dump') 31 | ->setDescription('Dump all available API routes') 32 | ->setHelp(<<controller_name : 38 | \tmethod_name [parameter1, parameter2, ...] 39 | 40 | EOF 41 | ) 42 | ; 43 | } 44 | 45 | /** 46 | * @param InputInterface $input 47 | * @param OutputInterface $output 48 | * 49 | * @return int|null|void 50 | */ 51 | protected function execute(InputInterface $input, OutputInterface $output) 52 | { 53 | $this->writeSection($output, 'Router : dump'); 54 | 55 | $router = new Router(); 56 | $router->init(); 57 | 58 | $routes = $router->getRoutes(); 59 | foreach ($routes as $controller => $methods) { 60 | $output->writeln(sprintf('%s : ', $controller)); 61 | 62 | foreach ($methods as $method => $params) { 63 | $output->writeln(sprintf(" - %s :%s", $method, $this->formatParameters($method, $params))); 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * @param string $methodName 70 | * @param array $parameters 71 | * 72 | * @return string 73 | */ 74 | protected function formatParameters($methodName, array $parameters) 75 | { 76 | $isWinOS = strtoupper(substr(PHP_OS, 0, 3)) == 'WIN'; 77 | $length = strlen($methodName); 78 | if ($isWinOS) { 79 | $length -= 2; 80 | } 81 | else { 82 | $length -= 1; 83 | } 84 | 85 | $length /= 8; 86 | if (!$isWinOS && is_float($length)) { 87 | ++$length; 88 | } 89 | 90 | $tabs = 6 - $length; 91 | $output = ""; 92 | 93 | for ($i = 0; $i < $tabs; $i++) { 94 | $output .= "\t"; 95 | } 96 | 97 | $output .= '[' . join(', ', $parameters) . ']'; 98 | 99 | return $output; 100 | } 101 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Callback/Callback.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class Callback 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected $options; 25 | 26 | 27 | /** 28 | * @param array $options 29 | */ 30 | public function __construct(array $options = array()) 31 | { 32 | $this->options = $options; 33 | 34 | $this->compareOptions(); 35 | } 36 | 37 | /** 38 | * @throws MissingOptionCallbackException 39 | */ 40 | private function compareOptions() 41 | { 42 | $requiredOptions = $this->getRequiredOptions(); 43 | if (!isset($requiredOptions[0])) { 44 | return; 45 | } 46 | 47 | foreach ($requiredOptions as $optionKey) { 48 | if (!isset($this->options[$optionKey])) { 49 | throw new MissingOptionCallbackException('The option "' . $optionKey . '" is missing'); 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Set your required options here, if one or more options are missing, an exception will be thrown 56 | * 57 | * @return array 58 | */ 59 | protected function getRequiredOptions() 60 | { 61 | return array(); 62 | } 63 | 64 | /** 65 | * Parse the API result and return the new content 66 | * 67 | * @param array|string $result 68 | * 69 | * @return mixed 70 | */ 71 | public abstract function getResult($result); 72 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Callback/Exception/MissingOptionCallbackException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MissingOptionCallbackException extends \InvalidArgumentException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Command/Command.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class Command extends BaseCommand 21 | { 22 | /** 23 | * Write a section title 24 | * 25 | * @param OutputInterface $output 26 | * @param string|null $sectionTitle 27 | */ 28 | protected function writeSection(OutputInterface $output, $sectionTitle = null) 29 | { 30 | $sectionLength = 80; 31 | $section = str_pad('[', $sectionLength - 1, '=') . ']'; 32 | $output->writeln(array( 33 | '', 34 | $section 35 | )); 36 | 37 | if (null != $sectionTitle) { 38 | $length = ($sectionLength - strlen($sectionTitle)) / 2; 39 | $output->writeln(array( 40 | str_pad('[', $length, ' ') . $sectionTitle . str_pad('', $sectionLength - strlen($sectionTitle) - $length, ' ') . ']', 41 | $section 42 | )); 43 | } 44 | 45 | $output->writeln(''); 46 | } 47 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Configuration/ConfigurationLoader.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ConfigurationLoader 24 | { 25 | /** 26 | * @var array 27 | */ 28 | protected static $configs; 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected static $configsAsString; 34 | 35 | 36 | /** 37 | * @return array 38 | * 39 | * @throws Exception\ConfigurationFileNotFoundException 40 | */ 41 | protected static function load() 42 | { 43 | if (!isset(self::$configs)) { 44 | $path = __DIR__ . '/../../../../../config/config.yml'; 45 | 46 | if (!is_file($path)) { 47 | throw new ConfigurationFileNotFoundException('The configuration file (' . $path . ') is not found'); 48 | } 49 | 50 | $parser = new Parser(); 51 | self::$configs = $parser->parse(file_get_contents($path)); 52 | } 53 | 54 | return self::$configs; 55 | } 56 | 57 | /** 58 | * @param string $name 59 | * 60 | * @return string|array|int|bool 61 | * 62 | * @throws Exception\ConfigurationKeyNotFoundException 63 | */ 64 | public static function get($name) 65 | { 66 | $name = 'config.' . $name; 67 | if (isset(self::$configsAsString[$name])) { 68 | return self::$configsAsString[$name]; 69 | } 70 | 71 | $configs = self::load(); 72 | $parts = explode('.', $name); 73 | $config = $configs; 74 | 75 | foreach ($parts as $part) { 76 | if (!array_key_exists($part, $config)) { 77 | throw new ConfigurationKeyNotFoundException('The configuration key "' . $name . '" is not found'); 78 | } 79 | 80 | $config = $config[$part]; 81 | } 82 | 83 | // Save to avoid later iteration 84 | self::$configsAsString[$name] = $config; 85 | 86 | return $config; 87 | } 88 | 89 | /** 90 | * @return array 91 | */ 92 | public static function getAll() 93 | { 94 | return self::$configs; 95 | } 96 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Configuration/Exception/ConfigurationFileNotFoundException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ConfigurationFileNotFoundException extends \InvalidArgumentException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Configuration/Exception/ConfigurationKeyNotFoundException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ConfigurationKeyNotFoundException extends \InvalidArgumentException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | abstract class Controller 29 | { 30 | /** 31 | * @var ApiManager 32 | */ 33 | protected $apiManager; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $region; 39 | 40 | /** 41 | * @var Connection 42 | */ 43 | protected $conn; 44 | 45 | /** 46 | * @var array 47 | */ 48 | protected $results = []; 49 | 50 | /** 51 | * @var int 52 | */ 53 | protected $responseCount = 0; 54 | 55 | /** 56 | * @var int 57 | */ 58 | protected $invokeCount = 0; 59 | 60 | /** 61 | * @var bool 62 | */ 63 | protected $hasError = false; 64 | 65 | /** 66 | * @var array 67 | */ 68 | protected $listeners = []; 69 | 70 | 71 | /** 72 | * @param ApiManager $apiManager The API manager 73 | * @param Connection $conn The client connection 74 | * @param string $region The region unique name (like "EUW", "NA", ...) 75 | */ 76 | public function __construct(ApiManager $apiManager, Connection $conn, $region) 77 | { 78 | $this->apiManager = $apiManager; 79 | $this->region = $region; 80 | $this->conn = $conn; 81 | } 82 | 83 | /** 84 | * Get the next available client 85 | * 86 | * @param callable $callback 87 | * 88 | * @return LOLClientInterface 89 | */ 90 | protected function onClientReady(\Closure $callback) 91 | { 92 | $this->apiManager->getClient($this->region, $callback); 93 | } 94 | 95 | /** 96 | * Fetch the result from client, transform it into an array and store it into the $this->results array 97 | * 98 | * @param int $invokeId 99 | * @param null|callable $resultsCallback This callback will format the result if needed 100 | * @param int $timeout 101 | * @param bool $bypassOverload Some API return nothing on error, we need to bypass overload system to
102 | * avoid timeout issue. 103 | */ 104 | protected function fetchResult($invokeId, \Closure $resultsCallback = null, $timeout = null, $bypassOverload = false) 105 | { 106 | $this->invokeCount++; 107 | 108 | if (null == $timeout) { 109 | $timeout = ConfigurationLoader::get('client.request.timeout'); 110 | } 111 | 112 | $timedOut = time() + $timeout; 113 | $this->onClientReady(function (LOLClientInterface $client) use ($invokeId, $timedOut, $bypassOverload, $resultsCallback, $timeout) { 114 | $this->apiManager->getLoop()->addPeriodicTimer(0.0001, function (TimerInterface $timer) use ($invokeId, $timedOut, $bypassOverload, $client, $resultsCallback, $timeout) { 115 | if ($this->hasError) { 116 | $timer->cancel(); 117 | 118 | return; 119 | } 120 | 121 | // Timeout process 122 | if (time() > $timedOut) { 123 | $this->hasError = true; 124 | $this->conn->emit('api-error', [ 125 | new RequestTimeoutException('Request timeout, the client will reconnect', $client) 126 | ]); 127 | 128 | $timer->cancel(); 129 | 130 | return null; 131 | } 132 | 133 | $resultParams = $client->getResult($invokeId); 134 | if (null == $resultParams) { 135 | return; 136 | } 137 | 138 | list($data, $callback) = $resultParams; 139 | $formatter = new ResultFormatter(); 140 | 141 | try { 142 | // RTMP API return error 143 | if ('_error' == $data['result']) { 144 | $this->hasError = true; 145 | $errorParams = $formatter->format($data['data']->getData()->rootCause); 146 | 147 | $this->conn->emit('api-error', [ 148 | new ApiException($errorParams['rootCauseClassname'], $errorParams['message']) 149 | ]); 150 | 151 | $timer->cancel(); 152 | 153 | return; 154 | } 155 | 156 | $result = $formatter->format($data['data']->getData()->body); 157 | if (null != $callback) { 158 | if ($callback instanceof Callback) { 159 | $result = $callback->getResult($result); 160 | } 161 | else { 162 | $result = $callback($result); 163 | } 164 | } 165 | 166 | if (null != $resultsCallback) { 167 | $this->results = $resultsCallback($result, $this->results); 168 | } 169 | else { 170 | $this->results[] = $result; 171 | } 172 | 173 | $this->responseCount++; 174 | $timer->cancel(); 175 | } 176 | catch (ClientOverloadException $e) { 177 | if ($bypassOverload) { 178 | $this->results[] = []; // empty response 179 | 180 | $this->responseCount++; 181 | $timer->cancel(); 182 | } else { 183 | // Flag client as overloaded & retry 184 | $client->setIsOverloaded(); 185 | $timer->cancel(); 186 | 187 | $this->fetchResult($invokeId, $resultsCallback, $timeout, $bypassOverload); 188 | } 189 | } 190 | }); 191 | }); 192 | } 193 | 194 | /** 195 | * @param null|callable $callback This callback will format the results array 196 | */ 197 | protected function sendResponse(\Closure $callback = null) 198 | { 199 | $this->apiManager->getLoop()->addPeriodicTimer(0.0001, function (TimerInterface $timer) use ($callback) { 200 | if ($this->hasError) { 201 | $timer->cancel(); 202 | 203 | return; 204 | } 205 | 206 | if (0 < $this->invokeCount && $this->invokeCount == $this->responseCount) { 207 | // Convert indexed array to associative if count = 1 208 | if (isset($this->results[0]) && !isset($this->results[1])) { 209 | $this->results = $this->results[0]; 210 | } 211 | 212 | $this->conn->emit('api-response', [[ 213 | 'success' => true, 214 | 'result' => null != $callback ? $callback($this->results) : $this->results 215 | ] 216 | ]); 217 | 218 | $timer->cancel(); 219 | } 220 | }); 221 | } 222 | 223 | /** 224 | * Call another controller method 225 | * 226 | * @param string $route 227 | * @param array $parameters 228 | * 229 | * @return mixed 230 | */ 231 | protected function call($route, array $parameters = array()) 232 | { 233 | $this->apiManager->getRouter()->process($this->apiManager, $this->conn, array( 234 | 'route' => $route, 235 | 'region' => $this->region, 236 | 'parameters' => $parameters 237 | )); 238 | } 239 | 240 | /** 241 | * Revoke client connection listeners 242 | */ 243 | protected function revokeListeners() 244 | { 245 | foreach (['response', 'error'] as $listener) { 246 | $this->listeners[$listener] = $this->conn->listeners('api-' . $listener); 247 | $this->conn->removeAllListeners('api-' . $listener); 248 | } 249 | } 250 | 251 | /** 252 | * Apply revoked client connection listeners 253 | */ 254 | protected function applyListeners() 255 | { 256 | foreach ($this->listeners as $listenerName => $listeners) { 257 | foreach ($listeners as $listener) { 258 | $this->conn->on('api-' . $listenerName, $listener); 259 | } 260 | } 261 | 262 | $this->listeners = []; 263 | } 264 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Controller/Exception/ApiException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ApiException extends ServerException 20 | { 21 | /** 22 | * @var string 23 | */ 24 | protected $cause; 25 | 26 | /** 27 | * @param string $cause 28 | * @param string $message 29 | */ 30 | public function __construct($cause, $message) 31 | { 32 | $this->cause = $cause; 33 | 34 | parent::__construct($message); 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getCause() 41 | { 42 | return $this->cause; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Controller/Exception/UnknownControllerException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class UnknownControllerException extends \RuntimeException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Exception/ArrayException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ArrayException extends \Exception 18 | { 19 | /** 20 | * @return string 21 | */ 22 | public function getCause() 23 | { 24 | return get_called_class(); 25 | } 26 | 27 | /** 28 | * @return array 29 | */ 30 | public function toArray() 31 | { 32 | return [ 33 | 'success' => false, 34 | 'error' => [ 35 | 'caused_by' => $this->getCause(), 36 | 'message' => $this->getMessage() 37 | ] 38 | ]; 39 | } 40 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Logging/Handler/RedisHandler.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class RedisHandler extends AbstractProcessingHandler 23 | { 24 | /** 25 | * @var Client 26 | */ 27 | private $redisClient; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $redisKey; 33 | 34 | 35 | /** 36 | * @param Client $redis 37 | * @param string $key 38 | * @param int $level 39 | * @param bool $bubble 40 | * 41 | * @throws \InvalidArgumentException 42 | */ 43 | public function __construct(Client $redis, $key, $level = Logger::DEBUG, $bubble = true) 44 | { 45 | $this->redisClient = $redis; 46 | $this->redisKey = $key; 47 | 48 | parent::__construct($level, $bubble); 49 | } 50 | 51 | /** 52 | * @param array $record 53 | */ 54 | protected function write(array $record) 55 | { 56 | $this->redisClient->rpush($this->redisKey, sprintf('%s|%s', $record['level'], $record['message'])); 57 | } 58 | 59 | /** 60 | * @return \Monolog\Formatter\FormatterInterface|LineFormatter 61 | */ 62 | protected function getDefaultFormatter() 63 | { 64 | return new LineFormatter(); 65 | } 66 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Logging/LoggerFactory.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class LoggerFactory 28 | { 29 | /** 30 | * @var LoggerInterface 31 | */ 32 | protected static $logger; 33 | 34 | /** 35 | * @var Client 36 | */ 37 | protected static $redisClient; 38 | 39 | 40 | /** 41 | * @param string $name 42 | * @param bool $saveOnRedis 43 | * 44 | * @return LoggerInterface 45 | */ 46 | public static function create($name = 'EloGankAPI', $saveOnRedis = false) 47 | { 48 | if (!isset(self::$logger)) { 49 | $verbosity = constant('Monolog\Logger::' . strtoupper(ConfigurationLoader::get('log.verbosity'))); 50 | self::$logger = new Logger($name, array( 51 | new ConsoleHandler(new ConsoleOutput(), true, array( 52 | OutputInterface::VERBOSITY_NORMAL => $verbosity, 53 | OutputInterface::VERBOSITY_VERBOSE => Logger::DEBUG, 54 | OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::DEBUG, 55 | OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG 56 | )), 57 | new RotatingFileHandler(ConfigurationLoader::get('log.path'), ConfigurationLoader::get('log.max_file'), $verbosity) 58 | ) 59 | ); 60 | 61 | // Allow the server to retrieve clients logs 62 | if (true === ConfigurationLoader::get('client.async.enabled')) { 63 | self::$redisClient = new Client(sprintf('tcp://%s:%s', ConfigurationLoader::get('client.async.redis.host'), ConfigurationLoader::get('client.async.redis.port'))); 64 | 65 | if ($saveOnRedis) { 66 | self::$logger->pushHandler(new RedisHandler(self::$redisClient, ConfigurationLoader::get('client.async.redis.key') . '.client.logs', $verbosity)); 67 | } 68 | } 69 | } 70 | 71 | return self::$logger; 72 | } 73 | 74 | /** 75 | * Show the asynchronous client logs in the main process logger (console) 76 | * 77 | * @return string|null 78 | * 79 | * @throws \RuntimeException 80 | */ 81 | public static function subscribe() 82 | { 83 | if (!isset(self::$redisClient)) { 84 | throw new \RuntimeException('Redis client has not been initialised'); 85 | } 86 | 87 | while (null != ($log = self::$redisClient->lpop(ConfigurationLoader::get('client.async.redis.key') . '.client.logs'))) { 88 | list ($level, $message) = explode('|', $log); 89 | self::$logger->addRecord($level, $message); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Exception/MalformedRouteException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class MalformedRouteException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Exception/MissingApiRoutesFileException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MissingApiRoutesFileException extends \RuntimeException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Exception/MissingParametersException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class MissingParametersException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Exception/UnknownResponseException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class UnknownResponseException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Exception/UnknownRouteException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class UnknownRouteException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Component/Routing/Router.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class Router 29 | { 30 | /** 31 | * This is the common routes, listed in the config/api_routes.yml file 32 | * 33 | * @var array 34 | */ 35 | protected $commonRoutes = []; 36 | 37 | /** 38 | * @var array 39 | */ 40 | protected $customRoutes = []; 41 | 42 | 43 | /** 44 | * Dump all routes in attributes 45 | * 46 | * @throws UnknownControllerException 47 | * @throws MissingApiRoutesFileException 48 | */ 49 | public function init() 50 | { 51 | // First, register all common routes 52 | $filePath = __DIR__ . '/../../../../../config/api_routes.yml'; 53 | if (!is_file($filePath)) { 54 | throw new MissingApiRoutesFileException('The file "config/api_routes.yml" is missing'); 55 | } 56 | 57 | $parser = new Parser(); 58 | $destinations = $parser->parse(file_get_contents($filePath))['routes']; 59 | 60 | foreach ($destinations as $destinationName => $services) { 61 | $formattedDestinationName = $this->underscore($destinationName); 62 | // Delete "_service" or "_service_proxy" suffixes 63 | if ('service' == substr($formattedDestinationName, -7)) { 64 | $formattedDestinationName = substr($formattedDestinationName, 0, -8); 65 | } 66 | elseif ('service_proxy' == substr($formattedDestinationName, -13)) { 67 | $formattedDestinationName = substr($formattedDestinationName, 0, -14); 68 | } 69 | 70 | $this->commonRoutes[$formattedDestinationName] = [ 71 | 'name' => $destinationName, 72 | 'methods' => [] 73 | ]; 74 | 75 | foreach ($services as $serviceName => $parameters) { 76 | $formattedServiceName = $this->underscore($serviceName); 77 | // Delete "get_" prefix 78 | if (0 === strpos($formattedServiceName, 'get_')) { 79 | $formattedServiceName = substr($formattedServiceName, 4); 80 | } 81 | 82 | $this->commonRoutes[$formattedDestinationName]['methods'][$formattedServiceName] = [ 83 | 'name' => $serviceName, 84 | 'parameters' => $parameters 85 | ]; 86 | } 87 | } 88 | 89 | // Then, register the custom routes 90 | $iterator = new \DirectoryIterator(__DIR__ . '/../../Controller'); 91 | /** @var \SplFileInfo $controller */ 92 | foreach ($iterator as $controller) { 93 | if ($controller->isDir()) { 94 | continue; 95 | } 96 | 97 | $name = substr($controller->getFilename(), 0, -4); 98 | $reflectionClass = new \ReflectionClass('\\EloGank\\Api\\Controller\\' . $name); 99 | if (!$reflectionClass->isSubclassOf('\\EloGank\\Api\\Component\\Controller\\Controller')) { 100 | throw new UnknownControllerException('The controller "' . $name . '" must extend the class \EloGank\Api\Component\Controller\Controller'); 101 | } 102 | 103 | // Delete the "Controller" suffix 104 | if ('Controller' == substr($name, strlen($name) - 10)) { 105 | $name = substr($name, 0, -10); 106 | } 107 | 108 | $routeName = $this->underscore($name); 109 | $this->customRoutes[$routeName] = [ 110 | 'class' => $name . 'Controller', 111 | 'methods' => [] 112 | ]; 113 | 114 | $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); 115 | /** @var \ReflectionMethod $method */ 116 | foreach ($methods as $method) { 117 | // Wrong method definition 118 | if (!$method->isPublic() || !preg_match('/[a-zA-Z0-9_]+Action/', $method->getName())) { 119 | continue; 120 | } 121 | 122 | $params = $method->getParameters(); 123 | $paramsName = []; 124 | 125 | /** @var \ReflectionParameter $param */ 126 | foreach ($params as $param) { 127 | $paramsName[] = $param->getName(); 128 | } 129 | 130 | $methodName = $this->underscore(substr($method->getName(), 0, -6)); 131 | // Delete useless get prefix 132 | if (0 === strpos($methodName, 'get_')) { 133 | $methodName = substr($methodName, 4); 134 | } 135 | 136 | $this->customRoutes[$routeName]['methods'][$methodName] = [ 137 | 'name' => $method->getName(), 138 | 'parameters' => $paramsName 139 | ]; 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * @param ApiManager $apiManager The API manager 146 | * @param Connection $conn The client connection 147 | * @param array $data The data sent by the client 148 | * 149 | * @throws MissingParametersException 150 | * @throws MalformedRouteException 151 | * @throws UnknownRouteException 152 | * @throws UnknownResponseException 153 | */ 154 | public function process(ApiManager $apiManager, Connection $conn, array $data) 155 | { 156 | $route = $data['route']; 157 | if (!preg_match('/^[a-zA-Z_]+\.[a-zA-Z_]+$/', $route)) { 158 | throw new MalformedRouteException('The route "' . $route . '" is malformed. Please send a route following this pattern : "controller_name.method_name"'); 159 | } 160 | 161 | list ($controllerName, $methodName) = explode('.', $route); 162 | 163 | // Common routes process 164 | if (isset($this->commonRoutes[$controllerName]['methods'][$methodName])) { 165 | // Missing parameters check 166 | if (count($data['parameters']) != count($this->commonRoutes[$controllerName]['methods'][$methodName]['parameters'])) { 167 | throw new MissingParametersException(sprintf('There are missing parameters for the method "%s" (controller "%s"). Please provide these parameters : %s', 168 | $methodName, $controllerName, join(', ', $this->commonRoutes[$controllerName]['methods'][$methodName]['parameters']) 169 | )); 170 | } 171 | 172 | $controller = new CommonController($apiManager, $conn, $data['region']); 173 | 174 | call_user_func_array(array($controller, 'commonCall'), [ 175 | $this->commonRoutes[$controllerName]['name'], 176 | $this->commonRoutes[$controllerName]['methods'][$methodName]['name'], 177 | $data['parameters'] 178 | ]); 179 | 180 | return; 181 | } 182 | 183 | // Custom routes process 184 | if (!isset($this->customRoutes[$controllerName]['methods'][$methodName])) { 185 | throw new UnknownRouteException('The route "' . $route . '" is unknown. To known all available routes, use the command "elogank:router:dump"'); 186 | } 187 | 188 | // Missing parameters check 189 | if (count($data['parameters']) != count($this->customRoutes[$controllerName]['methods'][$methodName]['parameters'])) { 190 | throw new MissingParametersException(sprintf('There are missing parameters for the method "%s" (controller "%s"). Please provide these parameters : %s', 191 | $methodName, $controllerName, join(', ', $this->customRoutes[$controllerName]['methods'][$methodName]['parameters']) 192 | )); 193 | } 194 | 195 | $class = '\\EloGank\\Api\\Controller\\' . $this->customRoutes[$controllerName]['class']; 196 | $controller = new $class($apiManager, $conn, $data['region']); 197 | 198 | call_user_func_array(array($controller, $this->customRoutes[$controllerName]['methods'][$methodName]['name']), $data['parameters']); 199 | } 200 | 201 | /** 202 | * @return array 203 | */ 204 | public function getRoutes() 205 | { 206 | $routes = []; 207 | foreach ($this->commonRoutes as $controllerName => $route) { 208 | foreach ($route['methods'] as $methodName => $method) { 209 | $routes[$controllerName][$methodName] = $method['parameters']; 210 | } 211 | } 212 | 213 | foreach ($this->customRoutes as $controllerName => $route) { 214 | foreach ($route['methods'] as $methodName => $method) { 215 | $routes[$controllerName][$methodName] = $method['parameters']; 216 | } 217 | } 218 | 219 | return $routes; 220 | } 221 | 222 | /** 223 | * @param array $response 224 | * 225 | * @return bool 226 | */ 227 | protected function isValidResponse($response) 228 | { 229 | if (!is_array($response) || !isset($response['success']) || 230 | $response['success'] && !isset($response['result']) || 231 | !$response['success'] && !isset($response['error']) 232 | ) { 233 | return false; 234 | } 235 | 236 | return true; 237 | } 238 | 239 | /** 240 | * @param string $string A camelized string 241 | * 242 | * @return string An underscore string 243 | */ 244 | protected function underscore($string) 245 | { 246 | return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), strtr($string, '_', '.'))); 247 | } 248 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Controller/CommonController.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class CommonController extends Controller 23 | { 24 | /** 25 | * @param string $destination 26 | * @param string $service 27 | * @param string|array $parameters 28 | */ 29 | public function commonCall($destination, $service, $parameters) 30 | { 31 | $this->onClientReady(function (LOLClientInterface $client) use ($destination, $service, $parameters) { 32 | $this->fetchResult( 33 | $client->invoke($destination, $service, $parameters), 34 | null, // callback 35 | null, // timeout 36 | $this->isOverloadServiceException($destination, $service) 37 | ); 38 | }); 39 | 40 | $this->sendResponse(function ($response) { 41 | return $response; 42 | }); 43 | } 44 | 45 | /** 46 | * Some routes return a "NULL" response body in case of not found item. 47 | * This response is the same as the client is overloaded (temporary banned from the server). 48 | * To avoid client reconnection, some exceptions are created. 49 | * 50 | * @param string $destination 51 | * @param string $service 52 | * 53 | * @return bool 54 | */ 55 | protected function isOverloadServiceException($destination, $service) 56 | { 57 | return 'summonerService' == $destination && 'getSummonerByName' == $service 58 | || 'gameService' == $destination && 'retrieveInProgressSpectatorGameInfo' == $service; 59 | } 60 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Controller/InventoryController.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class InventoryController extends Controller 21 | { 22 | /** 23 | * Return the id of free rotation week champions indexed by the key "freeChampions" 24 | * 25 | * @return array 26 | */ 27 | public function getAvailableFreeChampionsAction() 28 | { 29 | $this->onClientReady(function (LOLClientInterface $client) { 30 | $invokeId = $client->invoke('inventoryService', 'getAvailableChampions', [], function ($result) { 31 | $freeChampions = []; 32 | foreach ($result as $champion) { 33 | if (true === $champion['freeToPlay']) { 34 | $freeChampions[] = $champion['championId']; 35 | } 36 | } 37 | 38 | return ['freeChampions' => $freeChampions]; 39 | }); 40 | $this->fetchResult($invokeId); 41 | }); 42 | 43 | $this->sendResponse(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Controller/SummonerController.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class SummonerController extends Controller 26 | { 27 | /** 28 | * Check if summoner exists and return his information.
29 | * This method exist because the route "summoner.summoner_by_name" return the same behavior of an overloaded
30 | * client when the player does not exist. So we use a trick with the route
31 | * "game.retrieve_in_progress_spectator_game" and errors which return the accountId when the player exists and
32 | * a specific error when the player is not found. 33 | * 34 | * NOTE: in the patch 5.1, the "game.retrieve_in_progress_spectator_game" route doesn't throw exception anymore but 35 | * a "NULL" response body. 36 | * 37 | * @param string $summonerName 38 | * 39 | * @return array 40 | * 41 | * @throws \EloGank\Api\Component\Controller\Exception\ApiException 42 | * @throws \Exception 43 | * 44 | * @deprecated Use "summoner.summoner_by_name" route instead 45 | */ 46 | public function getSummonerExistenceAction($summonerName) 47 | { 48 | return $this->call('summoner.summoner_by_name', [$summonerName]); 49 | } 50 | 51 | /** 52 | * Return all data needed to show information about a summoner 53 | * 54 | * Example of parameters : 55 | * $parameters = [ 56 | * [ 57 | * ['accountId' => 11111, 'summonerId' => 2222222, 'summonerName' => 'Foo Bar'], 58 | * ['accountId' => 44444, 'summonerId' => 3333333, 'summonerName' => 'Bar Foo'] 59 | * ], 60 | * ['INFORMATION', 'MAIN_CHAMPION'] // filters 61 | * ]; 62 | * 63 | * @param array $summonerData Summoner data, index by "accountId", "summonerId" and "summonerName". Example : 64 | * @param array $filters Fetch only data passed in filters 65 | * - INFORMATION: fetch the main summoner information like the level 66 | * - ACTIVE_SPELLBOOK: fetch the active spell book 67 | * - ACTIVE_MASTERIES: fetch the active masteries book 68 | * - LEAGUE_SOLO_5x5: fetch the league solo 5x5 data, only for the summoner, not the entire league 69 | * - MAIN_CHAMPION: fetch the main champion id 70 | * - CHAMPIONS_DATA: fetch the ranked champions data 71 | * 72 | * @return array 73 | */ 74 | public function getAllSummonerDataAction(array $summonerData, array $filters = [ 75 | 'INFORMATION', 'ACTIVE_SPELLBOOK', 'ACTIVE_MASTERIES', 'LEAGUE_SOLO_5x5', 'MAIN_CHAMPION', 'CHAMPIONS_DATA' 76 | ]) 77 | { 78 | $filtersByKey = array_flip($filters); 79 | 80 | foreach ($summonerData as $data) { 81 | $accountId = $data['accountId']; 82 | $summonerId = $data['summonerId']; 83 | $summonerName = $data['summonerName']; 84 | 85 | $formatResult = function ($result, $response) use ($summonerId) { 86 | foreach ($result as $key => $value) { 87 | $response[$summonerId][$key] = $value; 88 | } 89 | 90 | return $response; 91 | }; 92 | 93 | if (isset($filtersByKey['INFORMATION'])) { 94 | $this->onClientReady(function (LOLClientInterface $client) use ($formatResult, $summonerId, $summonerName) { 95 | $invokeId = $client->invoke('summonerService', 'getSummonerByName', [$summonerName], new SummonerInformationCallback()); 96 | $this->fetchResult($invokeId, $formatResult); 97 | }); 98 | } 99 | 100 | if (isset($filtersByKey['ACTIVE_SPELLBOOK'])) { 101 | $this->onClientReady(function (LOLClientInterface $client) use ($formatResult, $summonerId, $accountId) { 102 | $invokeId = $client->invoke('summonerService', 'getAllPublicSummonerDataByAccount', [$accountId], new SummonerActiveSpellBookCallback()); 103 | $this->fetchResult($invokeId, $formatResult); 104 | }); 105 | } 106 | 107 | if (isset($filtersByKey['ACTIVE_MASTERIES'])) { 108 | $this->onClientReady(function (LOLClientInterface $client) use ($formatResult, $summonerId) { 109 | $invokeId = $client->invoke('masteryBookService', 'getMasteryBook', [$summonerId], new SummonerActiveMasteriesCallback()); 110 | $this->fetchResult($invokeId, $formatResult); 111 | }); 112 | } 113 | 114 | if (isset($filtersByKey['LEAGUE_SOLO_5x5'])) { 115 | $this->onClientReady(function (LOLClientInterface $client) use ($formatResult, $summonerId) { 116 | $invokeId = $client->invoke('leaguesServiceProxy', 'getAllLeaguesForPlayer', [$summonerId], new SummonerLeagueSolo5x5Callback([ 117 | 'summonerId' => $summonerId 118 | ])); 119 | $this->fetchResult($invokeId, $formatResult); 120 | }); 121 | } 122 | 123 | if (isset($filtersByKey['MAIN_CHAMPION']) || isset($filtersByKey['CHAMPIONS_DATA'])) { 124 | $this->onClientReady(function (LOLClientInterface $client) use ($formatResult, $summonerId, $accountId, $filtersByKey) { 125 | $invokeId = $client->invoke('playerStatsService', 'getAggregatedStats', [$accountId, 'CLASSIC', 4], new SummonerChampionCallback([ 126 | 'main_champion' => isset($filtersByKey['MAIN_CHAMPION']), 127 | 'champions_data' => isset($filtersByKey['CHAMPIONS_DATA']) 128 | ])); 129 | $this->fetchResult($invokeId, $formatResult); 130 | }); 131 | } 132 | } 133 | 134 | $this->sendResponse(function ($response) { 135 | return ['data' => $response]; 136 | }); 137 | } 138 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Manager/ApiManager.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class ApiManager 32 | { 33 | /** 34 | * @var LoggerInterface 35 | */ 36 | protected $logger; 37 | 38 | /** 39 | * @var LOLClientInterface[] 40 | */ 41 | protected $clients = []; 42 | 43 | /** 44 | * @var int 45 | */ 46 | protected $clientId = 1; 47 | 48 | /** 49 | * @var LoopInterface 50 | */ 51 | protected $loop; 52 | 53 | /** 54 | * @var Router 55 | */ 56 | protected $router; 57 | 58 | /** 59 | * @var Client 60 | */ 61 | protected $redis; 62 | 63 | 64 | /** 65 | * 66 | */ 67 | public function __construct() 68 | { 69 | $this->logger = LoggerFactory::create(); 70 | } 71 | 72 | /** 73 | * Init the API components 74 | */ 75 | public function init() 76 | { 77 | $this->loop = Factory::create(); 78 | 79 | // Catch signals 80 | $this->loop->addPeriodicTimer(1, function () { 81 | pcntl_signal_dispatch(); 82 | }); 83 | 84 | // Heartbeat, 2 minutes officially, here 5 85 | $this->loop->addPeriodicTimer(300, [$this, 'doHeartbeats']); 86 | 87 | // Clients logging 88 | if (true === ConfigurationLoader::get('client.async.enabled')) { 89 | $this->loop->addPeriodicTimer(0.5, function () { 90 | LoggerFactory::subscribe(); 91 | }); 92 | } 93 | 94 | // Init router 95 | $this->router = new Router(); 96 | $this->router->init(); 97 | 98 | // Init redis 99 | $this->redis = new Client(sprintf('tcp://%s:%d', ConfigurationLoader::get('client.async.redis.host'), ConfigurationLoader::get('client.async.redis.port'))); 100 | 101 | // Async processes 102 | if (true === ConfigurationLoader::get('client.async.enabled')) { 103 | $this->clean(true); 104 | 105 | $this->catchSignals(); 106 | } 107 | else { 108 | $this->logger->warning('You use the slow mode (synchronous), you can use the fast mode (asynchronous) by setting the configuration "client.async.enabled" to "true"'); 109 | } 110 | } 111 | 112 | /** 113 | * Create client instances & auth 114 | * 115 | * @return bool True if one or more clients are connected, false otherwise 116 | * 117 | * @throws ServerException 118 | */ 119 | public function connect() 120 | { 121 | $this->logger->info('Starting clients...'); 122 | 123 | $tmpClients = []; 124 | $accounts = ConfigurationLoader::get('client.accounts'); 125 | foreach ($accounts as $accountKey => $account) { 126 | $client = ClientFactory::create($this->logger, $this->redis, $accountKey, $this->getNextClientId()); 127 | $client->authenticate(); 128 | 129 | $tmpClients[] = $client; 130 | } 131 | 132 | $nbClients = count($tmpClients); 133 | $isAsync = true === ConfigurationLoader::get('client.async.enabled'); 134 | $i = 0; $connectedCount = 0; 135 | 136 | /** @var LOLClientInterface $client */ 137 | while ($i < $nbClients) { 138 | $deleteClients = []; 139 | foreach ($tmpClients as $j => $client) { 140 | $isAuthenticated = $client->isAuthenticated(); 141 | if (null !== $isAuthenticated) { 142 | if (true === $isAuthenticated) { 143 | if (!$isAsync && isset($this->clients[$client->getRegion()])) { 144 | throw new ServerException('Multiple account for the same region in synchronous mode is not allowed. Please enable the asynchronous mode in the configuration file'); 145 | } 146 | 147 | $this->clients[$client->getRegion()][] = $client; 148 | $this->logger->info('Client ' . $client . ' is connected'); 149 | $connectedCount++; 150 | } 151 | else { 152 | if ($isAsync) { 153 | $this->cleanAsyncClients(false, $client); 154 | } 155 | } 156 | 157 | $i++; 158 | $deleteClients[] = $j; 159 | } 160 | } 161 | 162 | foreach ($deleteClients as $deleteClientId) { 163 | unset($tmpClients[$deleteClientId]); 164 | } 165 | 166 | if ($isAsync) { 167 | pcntl_signal_dispatch(); 168 | LoggerFactory::subscribe(); 169 | sleep(1); 170 | } 171 | } 172 | 173 | // No connected client, abort 174 | if (0 == $connectedCount) { 175 | return false; 176 | } 177 | 178 | $totalClientCount = count($accounts); 179 | $message = sprintf('%d/%d client%s successfully connected', $connectedCount, $totalClientCount, $connectedCount > 1 ? 's' : ''); 180 | if ($connectedCount < $totalClientCount) { 181 | $this->logger->alert('Only ' . $message); 182 | } 183 | else { 184 | $this->logger->info($message); 185 | } 186 | 187 | return true; 188 | } 189 | 190 | /** 191 | * Catch signals before the API server is killed and kill all the asynchronous clients 192 | */ 193 | protected function catchSignals() 194 | { 195 | $killClients = function () { 196 | $this->clean(); 197 | 198 | // Need to be killed manually, see ReactPHP issue: https://github.com/reactphp/react/issues/296 199 | posix_kill(getmypid(), SIGKILL); 200 | 201 | // exit(0); 202 | }; 203 | 204 | pcntl_signal(SIGINT, $killClients); 205 | pcntl_signal(SIGTERM, $killClients); 206 | } 207 | 208 | /** 209 | * @param bool $throwException 210 | */ 211 | public function clean($throwException = false) 212 | { 213 | $this->cleanAsyncClients($throwException); 214 | 215 | if (ConfigurationLoader::get('client.async.enabled')) { 216 | $this->clearCache(); 217 | } 218 | } 219 | 220 | /** 221 | * Delete all keys from redis 222 | */ 223 | protected function clearCache() 224 | { 225 | $this->logger->info('Clearing cache...'); 226 | 227 | $keys = $this->redis->keys('elogank.api.*'); 228 | if (null != $keys) { 229 | foreach ($keys as $key) { 230 | $this->redis->del($key); 231 | } 232 | } 233 | } 234 | 235 | /** 236 | * Cleaning all asynchronous client processes registered by cache files 237 | * 238 | * @param bool $throwException 239 | * @param null|LOLClientInterface $client 240 | * 241 | * @throws \RuntimeException 242 | */ 243 | protected function cleanAsyncClients($throwException = false, $client = null) 244 | { 245 | $this->logger->info('Cleaning cached async clients...'); 246 | 247 | $cachePath = __DIR__ . '/../../../../' . ConfigurationLoader::get('cache.path') . '/' . 'clientpids'; 248 | if (!is_dir($cachePath)) { 249 | if (!mkdir($cachePath, 0777, true)) { 250 | throw new \RuntimeException('Cannot write in the cache folder'); 251 | } 252 | } 253 | 254 | if (null != $client) { 255 | $path = $cachePath . '/client_' . $client->getId() . '.pid'; 256 | 257 | if (!is_file($path)) { 258 | return; 259 | } 260 | 261 | Process::killProcess($path, $throwException, $this->logger, $client); 262 | } 263 | else { 264 | $iterator = new \DirectoryIterator($cachePath); 265 | foreach ($iterator as $pidFile) { 266 | if ($pidFile->isDir()) { 267 | continue; 268 | } 269 | 270 | Process::killProcess($pidFile->getRealPath(), $throwException, $this->logger); 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * @return int 277 | */ 278 | protected function getNextClientId() 279 | { 280 | return $this->clientId++; 281 | } 282 | 283 | /** 284 | * @return LoopInterface 285 | */ 286 | public function getLoop() 287 | { 288 | return $this->loop; 289 | } 290 | 291 | /** 292 | * @return Router 293 | */ 294 | public function getRouter() 295 | { 296 | return $this->router; 297 | } 298 | 299 | /** 300 | * @return LoggerInterface 301 | */ 302 | public function getLogger() 303 | { 304 | return $this->logger; 305 | } 306 | 307 | /** 308 | * Do heartbeat on all clients, then check if there is a timeout 309 | */ 310 | public function doHeartbeats() 311 | { 312 | $clientTimeout = ConfigurationLoader::get('client.request.timeout'); 313 | $timedOut = time() + $clientTimeout; 314 | 315 | foreach ($this->clients as $clientsByRegion) { 316 | /** @var LOLClientInterface $client */ 317 | foreach ($clientsByRegion as $client) { 318 | $invokeId = $client->doHeartBeat(); 319 | 320 | $this->loop->addPeriodicTimer(0.01, function (TimerInterface $timer) use ($client, $invokeId, $timedOut, $clientTimeout) { 321 | if (time() > $timedOut) { 322 | $client->reconnect(); 323 | $timer->cancel(); 324 | 325 | return; 326 | } 327 | 328 | $result = $client->getResult($invokeId); 329 | if (null == $result) { 330 | return; 331 | } 332 | 333 | if (!isset($result[0]['result']) || '_result' !== $result[0]['result']) { 334 | $this->logger->warning('Client ' . $client . ' return a wrong heartbeat response, restarting client...'); 335 | $client->reconnect(); 336 | } 337 | 338 | $timer->cancel(); 339 | }); 340 | } 341 | } 342 | } 343 | 344 | /** 345 | * The RTMP LoL API will temporary ban you if you call too many times a service
346 | * To avoid this limitation, you must wait before making a new request 347 | * 348 | * @param string $regionUniqueName 349 | * @param callable $callback 350 | * 351 | * @return LOLClientInterface 352 | * 353 | * @throws RegionNotFoundException When there is not client with the selected region unique name 354 | */ 355 | public function getClient($regionUniqueName, \Closure $callback) 356 | { 357 | if (!isset($this->clients[$regionUniqueName])) { 358 | throw new RegionNotFoundException('There is no registered client with the region "' . $regionUniqueName . '"'); 359 | } 360 | 361 | $nextAvailableTime = (float) ConfigurationLoader::get('client.request.overload.available'); 362 | $nextAvailableTime /= 2; 363 | 364 | foreach ($this->clients[$regionUniqueName] as $client) { 365 | if ($client->isAvailable()) { 366 | return $callback($client); 367 | } 368 | } 369 | 370 | $this->loop->addPeriodicTimer($nextAvailableTime, function (TimerInterface $timer) use($regionUniqueName, $callback) { 371 | foreach ($this->clients[$regionUniqueName] as $client) { 372 | if ($client->isAvailable()) { 373 | $timer->cancel(); 374 | 375 | return $callback($client); 376 | } 377 | } 378 | }); 379 | } 380 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Model/Region/Exception/RegionNotFoundException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class RegionNotFoundException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Model/Region/Region.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Region implements RegionInterface 18 | { 19 | /** 20 | * @var string 21 | */ 22 | protected $uniqueName; 23 | 24 | /** 25 | * @var string 26 | */ 27 | protected $server; 28 | 29 | /** 30 | * @var string 31 | */ 32 | protected $name; 33 | 34 | /** 35 | * @var string 36 | */ 37 | protected $loginQueue; 38 | 39 | 40 | /** 41 | * @param string $uniqueName 42 | * @param string $name 43 | * @param string $server 44 | * @param string $loginQueue 45 | */ 46 | public function __construct($uniqueName, $name, $server, $loginQueue) 47 | { 48 | $this->uniqueName = $uniqueName; 49 | $this->name = $name; 50 | $this->server = $server; 51 | $this->loginQueue = $this->setLoginQueue($loginQueue); 52 | } 53 | 54 | /** 55 | * @param string $loginQueue 56 | * 57 | * @return string 58 | */ 59 | protected function setLoginQueue($loginQueue) 60 | { 61 | if ('/' == $loginQueue[strlen($loginQueue) - 1]) { 62 | $loginQueue = substr($loginQueue, 0, -1); 63 | } 64 | 65 | return 'https://' . $loginQueue; 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getLoginQueue() 72 | { 73 | return $this->loginQueue; 74 | } 75 | 76 | /** 77 | * @param string $name 78 | */ 79 | public function setName($name) 80 | { 81 | $this->name = $name; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getName() 88 | { 89 | return $this->name; 90 | } 91 | 92 | /** 93 | * @param string $server 94 | */ 95 | public function setServer($server) 96 | { 97 | $this->server = $server; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getServer() 104 | { 105 | return $this->server; 106 | } 107 | 108 | /** 109 | * @param string $uniqueName 110 | */ 111 | public function setUniqueName($uniqueName) 112 | { 113 | $this->uniqueName = $uniqueName; 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getUniqueName() 120 | { 121 | return $this->uniqueName; 122 | } 123 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Model/Region/RegionInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface RegionInterface 18 | { 19 | /** 20 | * @return string 21 | */ 22 | public function getLoginQueue(); 23 | 24 | /** 25 | * @return string 26 | */ 27 | public function getName(); 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getServer(); 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getUniqueName(); 38 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Process/Process.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Process 21 | { 22 | /** 23 | * @param string $pidPath 24 | * @param bool $throwException 25 | * @param LoggerInterface $logger 26 | * @param LOLClientInterface|null $client 27 | * 28 | * @throws \RuntimeException 29 | */ 30 | public static function killProcess($pidPath, $throwException, LoggerInterface $logger, LOLClientInterface $client = null) 31 | { 32 | $pid = (int) file_get_contents($pidPath); 33 | 34 | // Test if process is still running 35 | $output = []; 36 | exec('ps ' . $pid, $output); 37 | 38 | if (!isset($output[1])) { 39 | if (null != $client) { 40 | $logger->debug('Client ' . $client . ' (pid: #' . $pid . ') not running, deleting cache pid file'); 41 | } 42 | else { 43 | $logger->debug('Process #' . $pid . ' not running, deleting cache pid file'); 44 | } 45 | 46 | unlink($pidPath); 47 | 48 | return; 49 | } 50 | 51 | // Kill 52 | if (posix_kill($pid, SIGKILL)) { 53 | if (null != $client) { 54 | $logger->debug('Client ' . $client . ' (pid: #' . $pid . ') has been killed'); 55 | } 56 | else { 57 | $logger->debug('Process #' . $pid . ' has been killed'); 58 | } 59 | 60 | unlink($pidPath); 61 | } 62 | else { 63 | if ($throwException) { 64 | throw new \RuntimeException('Cannot kill the process #' . $pid . ', please kill this process manually'); 65 | } 66 | 67 | $logger->critical('Cannot kill the process #' . $pid . ', please kill this process manually'); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Exception/MalformedClientInputException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MalformedClientInputException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Exception/ServerException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ServerException extends ArrayException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Exception/UnknownFormatException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class UnknownFormatException extends ServerException { } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Formatter/ClientFormatterInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface ClientFormatterInterface 18 | { 19 | /** 20 | * Format the result into another format
21 | * It's important to return a one unique string, without carriage return (new line) 22 | * 23 | * @param array $results 24 | * 25 | * @return mixed 26 | */ 27 | public function format(array $results); 28 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Formatter/JsonClientFormatter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class JsonClientFormatter implements ClientFormatterInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function format(array $results) 23 | { 24 | return json_encode($results); 25 | } 26 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Formatter/NativeClientFormatter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class NativeClientFormatter implements ClientFormatterInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function format(array $results) 23 | { 24 | return serialize($results); 25 | } 26 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Formatter/XmlClientFormatter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class XmlClientFormatter implements ClientFormatterInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function format(array $results) 23 | { 24 | $xml = new \SimpleXMLElement(''); 25 | 26 | $this->toXml($results, $xml); 27 | 28 | return trim(preg_replace('/\n/', '', $xml->asXML())); 29 | } 30 | 31 | /** 32 | * @param array $results 33 | * @param \SimpleXMLElement $xml 34 | */ 35 | protected function toXml($results, \SimpleXMLElement &$xml) 36 | { 37 | foreach ($results as $key => $value) { 38 | if (is_array($value)) { 39 | // Indexed array item 40 | if (!is_numeric($key)) { 41 | $node = $xml->addChild($key); 42 | 43 | $this->toXml($value, $node); 44 | } 45 | // Associative array item 46 | else { 47 | $node = $xml->addChild('node'); 48 | $node->addAttribute('item', $key + 1); 49 | 50 | $this->toXml($value, $node); 51 | } 52 | } 53 | else { 54 | if (is_bool($value)) { 55 | $value = $value ? 'true' : 'false'; 56 | } 57 | 58 | $xml->addChild($key, htmlspecialchars($value)); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/EloGank/Api/Server/Server.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class Server 33 | { 34 | /** 35 | * @var LoggerInterface 36 | */ 37 | protected $logger; 38 | 39 | /** 40 | * @var ApiManager 41 | */ 42 | protected $apiManager; 43 | 44 | /** 45 | * @var ClientFormatterInterface[] 46 | */ 47 | protected $formatters; 48 | 49 | 50 | /** 51 | * @param ApiManager $apiManager 52 | */ 53 | public function __construct(ApiManager $apiManager) 54 | { 55 | $this->apiManager = $apiManager; 56 | $this->logger = LoggerFactory::create(); 57 | 58 | $this->formatters = [ 59 | 'native' => new NativeClientFormatter(), 60 | 'json' => new JsonClientFormatter(), 61 | 'xml' => new XmlClientFormatter() 62 | ]; 63 | } 64 | 65 | /** 66 | * Start & init the API 67 | */ 68 | public function listen() 69 | { 70 | // Init API 71 | $this->apiManager->init(); 72 | if (!$this->apiManager->connect()) { 73 | throw new \RuntimeException('There is no ready client, aborted'); 74 | } 75 | 76 | // Init server 77 | $loop = $this->apiManager->getLoop(); 78 | $socket = new SocketServer($loop); 79 | 80 | $socket->on('connection', function ($conn) { 81 | /** @var Connection $conn */ 82 | $this->logger->debug(sprintf('Client [%s] is connected to server', $conn->getRemoteAddress())); 83 | 84 | // On receive data 85 | $conn->on('data', function ($rawData) use ($conn) { 86 | $this->logger->debug(sprintf('Client sent: %s', $rawData)); 87 | 88 | $data = json_decode($rawData, true); 89 | $format = null; 90 | 91 | if (isset($data['format'])) { 92 | $format = $data['format']; 93 | } 94 | 95 | try { 96 | if (!$this->isValidInput($data)) { 97 | throw new MalformedClientInputException('The input sent to the server is maformed'); 98 | } 99 | 100 | $conn->on('api-response', function ($response) use ($conn, $format) { 101 | $conn->end($this->format($response, $format)); 102 | }); 103 | 104 | $conn->on('api-error', function (ServerException $e) use ($conn, $format) { 105 | $conn->end($this->format($e->toArray(), $format)); 106 | 107 | if ($e instanceof RequestTimeoutException) { 108 | $e->getClient()->reconnect(); 109 | 110 | // Force doing heartbeats to check if another client is timed out 111 | $this->apiManager->doHeartbeats(); 112 | } 113 | }); 114 | 115 | $this->apiManager->getRouter()->process($this->apiManager, $conn, $data); 116 | } 117 | catch (ServerException $e) { 118 | $this->logger->error('Client [' . $conn->getRemoteAddress() . ']: ' . $e->getMessage()); 119 | 120 | $conn->end($this->format($e->toArray(), $format)); 121 | } 122 | }); 123 | }); 124 | 125 | $port = ConfigurationLoader::get('server.port'); 126 | $bind = ConfigurationLoader::get('server.bind'); 127 | 128 | $this->logger->info(sprintf('Listening on %s:%d', $bind == '0.0.0.0' ? '*' : $bind, $port)); 129 | $socket->listen($port, $bind); 130 | 131 | $loop->run(); 132 | } 133 | 134 | /** 135 | * @param array $results 136 | * @param string $format 137 | * 138 | * @return mixed 139 | * 140 | * @throws UnknownFormatException 141 | */ 142 | protected function format(array $results, $format = null) 143 | { 144 | if (null == $format) { 145 | $format = ConfigurationLoader::get('server.format'); 146 | } 147 | else { 148 | if (!isset($this->formatters[$format])) { 149 | throw new UnknownFormatException('Unknown format for "' . $format . '". Did you mean : "' . join(', ', array_keys($this->formatters)) . '" ?'); 150 | } 151 | } 152 | 153 | return $this->formatters[$format]->format($results); 154 | } 155 | 156 | /** 157 | * @param array $data 158 | * 159 | * @return bool 160 | */ 161 | protected function isValidInput(array $data) 162 | { 163 | if (!isset($data['region']) || !isset($data['route']) || !isset($data['parameters']) || !is_array($data['parameters'])) { 164 | return false; 165 | } 166 | 167 | return true; 168 | } 169 | } --------------------------------------------------------------------------------