1 | <?php |
||
30 | class ItemMultiResponseHandler implements MultiResponseHandlerInterface{ |
||
31 | use DatabaseTrait; |
||
32 | |||
33 | /** |
||
34 | * class options |
||
35 | * play around with chunksize and concurrent requests to get best performance results |
||
36 | */ |
||
37 | const CONCURRENT = 10; |
||
38 | const CHUNK_SIZE = 100; |
||
39 | const API_LANGUAGES = ['de', 'en', 'es', 'fr', 'zh']; |
||
40 | const CACERT = __DIR__.'/../../tests/test-cacert.pem'; |
||
41 | const TEMP_TABLE = 'gw2_items_temp'; |
||
42 | const DBDRIVER = MySQLiDriver::class; // MySQLiDriver::class |
||
43 | const API_BASE = 'https://api.guildwars2.com/v2/items'; |
||
44 | const CONFIGDIR = __DIR__.'/../../config'; |
||
45 | |||
46 | /** |
||
47 | * @var \chillerlan\TinyCurl\MultiRequest |
||
48 | */ |
||
49 | protected $multiRequest; |
||
50 | |||
51 | /** |
||
52 | * @var \chillerlan\Database\Drivers\DBDriverInterface |
||
53 | */ |
||
54 | protected $DBDriverInterface; |
||
55 | |||
56 | /** |
||
57 | * @var array |
||
58 | */ |
||
59 | protected $urls = []; |
||
60 | |||
61 | /** |
||
62 | * @var float |
||
63 | */ |
||
64 | protected $starttime; |
||
65 | |||
66 | /** |
||
67 | * @var int |
||
68 | */ |
||
69 | protected $callback = 0; |
||
70 | |||
71 | /** |
||
72 | * MultiResponseHandlerTest constructor. |
||
73 | * |
||
74 | * @param \chillerlan\TinyCurl\MultiRequest $multiRequest |
||
75 | */ |
||
76 | public function __construct(MultiRequest $multiRequest = null){ |
||
77 | $this->multiRequest = $multiRequest; |
||
78 | |||
79 | (new Dotenv(self::CONFIGDIR))->load(); |
||
80 | |||
81 | $dbOptions = new DBOptions([ |
||
82 | 'host' => getenv('DB_MYSQLI_HOST'), |
||
83 | 'port' => getenv('DB_MYSQLI_PORT'), |
||
84 | 'database' => getenv('DB_MYSQLI_DATABASE'), |
||
85 | 'username' => getenv('DB_MYSQLI_USERNAME'), |
||
86 | 'password' => getenv('DB_MYSQLI_PASSWORD'), |
||
87 | ]); |
||
88 | |||
89 | $this->DBDriverInterface = $this->dbconnect(self::DBDRIVER, $dbOptions); |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * start the mayhem |
||
94 | */ |
||
95 | public function init(){ |
||
96 | $this->createTempTable(); |
||
97 | $this->getURLs(); |
||
98 | |||
99 | $this->starttime = microtime(true); |
||
100 | |||
101 | $options = new MultiRequestOptions; |
||
102 | $options->ca_info = self::CACERT; |
||
103 | $options->base_url = self::API_BASE.'?'; |
||
104 | $options->window_size = self::CONCURRENT; |
||
105 | |||
106 | $request = new MultiRequest($options); |
||
107 | // solving the hen-egg problem, feed the hen with the egg! |
||
108 | $request->setHandler($this); |
||
109 | |||
110 | $this->logToCLI('mayhem started'); |
||
111 | $this->callback = 0; |
||
112 | $request->fetch($this->urls); |
||
113 | $this->logToCLI('MultiRequest::fetch() finished'); |
||
114 | |||
115 | # var_dump($this->mySQLiDriver->raw('select * from '.self::TEMP_TABLE)); |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * Schrödingers cat state handler. |
||
120 | * |
||
121 | * This method will be called within a loop in MultiRequest::processStack(). |
||
122 | * You can either build your class around this MultiResponseHandlerInterface to process |
||
123 | * the response during runtime or return the response data to the running |
||
124 | * MultiRequest instance via addResponse() and receive the data by calling getResponseData(). |
||
125 | * |
||
126 | * This method may return void or an URL object as a replacement for a failed request, |
||
127 | * which then will be re-added to the running queue. |
||
128 | * |
||
129 | * However, the return value will not be checked, so make sure you return valid URLs. ;) |
||
130 | * |
||
131 | * @param \chillerlan\TinyCurl\Response\ResponseInterface $response |
||
132 | * |
||
133 | * @return bool|\chillerlan\TinyCurl\URL |
||
134 | * @internal |
||
135 | */ |
||
136 | public function handleResponse(ResponseInterface $response){ |
||
137 | $info = $response->info; |
||
138 | $this->callback++; |
||
139 | |||
140 | // get the current request params |
||
141 | parse_str(parse_url($info->url, PHP_URL_QUERY), $params); |
||
142 | |||
143 | // there be dragons. |
||
144 | if(in_array($info->http_code, [200, 206], true)){ |
||
145 | $lang = $response->headers->{'content-language'} ?: $params['lang']; |
||
146 | |||
147 | // discard the response when it's impossible to determine the language |
||
148 | if(!in_array($lang, self::API_LANGUAGES)){ |
||
149 | $this->logToCLI('URL discarded. ('.$info->url.')'); |
||
150 | return false; |
||
151 | } |
||
152 | |||
153 | $sql = 'UPDATE '.self::TEMP_TABLE.' SET `'.$lang.'` = ? WHERE `id` = ?'; |
||
154 | $values = []; |
||
155 | |||
156 | foreach($response->json as $item){ |
||
157 | # $this->logToCLI(str_pad($item->id, 5).' - '.$item->name); |
||
158 | // just dumping the raw JSON for each item here because i'm lazy (or to process the itemdata later) |
||
159 | $values[] = [json_encode($item), $item->id]; |
||
160 | } |
||
161 | |||
162 | // insert the data as soon as we receive it |
||
163 | // this will result in a couple more database writes but won't block the responses much |
||
164 | if($this->DBDriverInterface->multi($sql, $values)){ |
||
165 | $this->logToCLI('['.str_pad($this->callback, 6, ' ',STR_PAD_RIGHT).']['.$lang.'] '.md5($response->info->url).' updated'); |
||
166 | } |
||
167 | else{ |
||
168 | // retry if the insert failed for whatever reason |
||
169 | $this->logToCLI('SQL insert failed, retrying URL. ('.$info->url.')'); |
||
170 | return new URL($info->url); |
||
171 | } |
||
172 | |||
173 | // not adding a response if everything was fine ('s ok, PhpStorm...) |
||
174 | return false; |
||
175 | } |
||
176 | // instant retry on a 502 |
||
177 | // https://gitter.im/arenanet/api-cdi?at=56c3ba6ba5bdce025f69bcc8 |
||
178 | else if($info->http_code === 502){ |
||
179 | $this->logToCLI('URL readded due to a 502. ('.$info->url.')'); |
||
180 | return new URL($info->url); |
||
181 | } |
||
182 | // examine and add the failed response to retry later @todo |
||
183 | else{ |
||
184 | $this->logToCLI('('.$info->url.')'); |
||
185 | return false; |
||
186 | } |
||
187 | |||
188 | } |
||
189 | |||
190 | /** |
||
191 | * Write some info to the CLI |
||
192 | * |
||
193 | * @param $str |
||
194 | */ |
||
195 | protected function logToCLI($str){ |
||
196 | echo '['.date('c', time()).']'.sprintf('[%10ss] ', sprintf('%01.4f', microtime(true) - $this->starttime)).$str.PHP_EOL; |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Creates a temporary table to receive the item responses on the fly |
||
201 | */ |
||
202 | protected function createTempTable(){ |
||
203 | |||
204 | $sql_lang = array_map(function($lang){ |
||
205 | return '`'.$lang.'` text COLLATE utf8mb4_bin NOT NULL, '; |
||
206 | }, self::API_LANGUAGES); |
||
207 | |||
208 | $sql = 'CREATE TEMPORARY TABLE IF NOT EXISTS `'.self::TEMP_TABLE.'` (' |
||
209 | .'`id` int(10) unsigned NOT NULL,' |
||
210 | .substr(implode(' ', $sql_lang), 0, -1) |
||
211 | .' `updated` tinyint(1) unsigned NOT NULL DEFAULT 0,' |
||
212 | .'`response_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' |
||
213 | .'PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin'; |
||
214 | $this->DBDriverInterface->raw('DROP TEMPORARY TABLE IF EXISTS `'.self::TEMP_TABLE.'`'); |
||
215 | $this->DBDriverInterface->raw($sql); |
||
216 | } |
||
217 | |||
218 | /** |
||
219 | * @throws \chillerlan\TinyCurl\RequestException |
||
220 | */ |
||
221 | protected function getURLs(){ |
||
222 | $this->starttime = microtime(true); |
||
223 | $this->logToCLI('self::getURLs() fetch'); |
||
224 | |||
225 | $response = (new Request)->fetch(new URL('https://api.guildwars2.com/v2/items')); |
||
226 | |||
227 | if($response->info->http_code !== 200){ |
||
228 | throw new Exception('failed to get /v2/items'); |
||
229 | } |
||
230 | |||
231 | $values = array_map(function($item){ |
||
232 | return [$item]; |
||
233 | }, $response->json); |
||
234 | |||
235 | $this->logToCLI('self::getURLs() $response to DB start'); |
||
236 | $this->DBDriverInterface->multi('INSERT INTO '.self::TEMP_TABLE.' (`id`) VALUES (?)', $values); |
||
237 | $this->logToCLI('self::getURLs() $response to DB finish'); |
||
238 | |||
239 | $chunks = array_chunk($response->json, self::CHUNK_SIZE); |
||
240 | |||
241 | array_map(function($chunk){ |
||
242 | foreach(self::API_LANGUAGES as $lang){ |
||
243 | $this->urls[] = new URL(self::API_BASE.'?lang='.$lang.'&ids='.implode(',', $chunk)); |
||
244 | } |
||
245 | }, $chunks); |
||
246 | |||
247 | $this->logToCLI('self::getURLs() finished'); |
||
248 | } |
||
249 | |||
250 | } |
||
251 |