<?php namespace cURL; use Symfony\Component\EventDispatcher\EventDispatcher; class RequestsQueue extends EventDispatcher implements RequestsQueueInterface, \Countable { /** * @var Options Default options for new Requests attached to RequestsQueue */ protected $defaultOptions = null; /** * @var resource cURL multi handler */ protected $mh; /** * @var int Amount of requests running */ protected $runningCount = 0; /** * @var Request[] Array of requests attached */ protected $queue = array(); /** * @var array Array of requests running */ protected $running = array(); /** * Initializes curl_multi handler */ public function __construct() { $this->mh = curl_multi_init(); } /** * Destructor, closes curl_multi handler * * @return void */ public function __destruct() { if (isset($this->mh)) { curl_multi_close($this->mh); } } /** * Returns cURL\Options object with default request's options * * @return Options */ public function getDefaultOptions() { if (!isset($this->defaultOptions)) { $this->defaultOptions = new Options(); } return $this->defaultOptions; } /** * Overrides default options with given Options object * * @param Options $defaultOptions New options * @return void */ public function setDefaultOptions(Options $defaultOptions) { $this->defaultOptions = $defaultOptions; } /** * Get cURL multi handle * * @return resource */ public function getHandle() { return $this->mh; } /** * Attach request to queue. * * @param Request $request Request to add * @return self */ public function attach(Request $request) { $this->queue[$request->getUID()] = $request; return $this; } /** * Detach request from queue. * * @param Request $request Request to remove * @return self */ public function detach(Request $request) { unset($this->queue[$request->getUID()]); return $this; } /** * Processes handles which are ready and removes them from pool. * * @return int Amount of requests completed */ protected function read() { $n = 0; while ($info = curl_multi_info_read($this->mh)) { $n++; $request = $this->queue[(int)$info['handle']]; $result = $info['result']; curl_multi_remove_handle($this->mh, $request->getHandle()); unset($this->running[$request->getUID()]); $this->detach($request); $event = new Event(); $event->request = $request; $event->response = new Response($request, curl_multi_getcontent($request->getHandle())); if ($result !== CURLE_OK) { $event->response->setError(new Error(curl_error($request->getHandle()), $result)); } $event->queue = $this; $this->dispatch('complete', $event); $request->dispatch('complete', $event); } return $n; } /** * Returns count of handles in queue * * @return int Handles count */ public function count() { return count($this->queue); } /** * Executes requests in parallel * * @return void */ public function send() { while ($this->socketPerform()) { $this->socketSelect(); } } /** * Returns requests present in $queue but not in $running * * @return Request[] Array of requests */ protected function getRequestsNotRunning() { $map = $this->queue; foreach($this->running as $k => $v) unset($map[$k]); return $map; } /** * Download available data on socket. * * @throws Exception * @return bool TRUE when there are any requests on queue, FALSE when finished */ public function socketPerform() { if ($this->count() == 0) { throw new Exception('Cannot perform if there are no requests in queue.'); } $notRunning = $this->getRequestsNotRunning(); do { /** * Apply cURL options to new requests */ foreach ($notRunning as $request) { $this->getDefaultOptions()->applyTo($request); $request->getOptions()->applyTo($request); curl_multi_add_handle($this->mh, $request->getHandle()); $this->running[$request->getUID()] = $request; } $runningBefore = $this->runningCount; do { $mrc = curl_multi_exec($this->mh, $this->runningCount); } while ($mrc === CURLM_CALL_MULTI_PERFORM); $runningAfter = $this->runningCount; if ($runningAfter < $runningBefore) { $this->read(); } $notRunning = $this->getRequestsNotRunning(); } while (count($notRunning) > 0); // Why the loop? New requests might be added at runtime on 'complete' event. // So we need to attach them to curl_multi handle immediately. return $this->count() > 0; } /** * Waits until activity on socket * On success, returns TRUE. On failure, this function will * return FALSE on a select failure or timeout (from the underlying * select system call) * * @param float|int $timeout Maximum time to wait * @throws Exception * @return bool */ public function socketSelect($timeout = 1) { if ($this->count() == 0) { throw new Exception('Cannot select if there are no requests in queue.'); } return curl_multi_select($this->mh, $timeout) !== -1; } }