├── .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 | }
--------------------------------------------------------------------------------