<?php namespace PHPSocketIO; use PHPSocketIO\Event\Emitter; use PHPSocketIO\Parser\Parser; class Socket extends Emitter { public $nsp = null; public $server = null; public $adapter = null; public $id = null; public $path = '/'; public $request = null; public $client = null; public $conn = null; public $rooms = array(); public $_rooms = array(); public $flags = array(); public $acks = array(); public $connected = true; public $disconnected = false; public static $events = array( 'error'=>'error', 'connect' => 'connect', 'disconnect' => 'disconnect', 'newListener' => 'newListener', 'removeListener' => 'removeListener' ); public static $flagsMap = array( 'json' => 'json', 'volatile' => 'volatile', 'broadcast' => 'broadcast' ); public function __construct($nsp, $client) { $this->nsp = $nsp; $this->server = $nsp->server; $this->adapter = $this->nsp->adapter; $this->id = ($nsp->name !== '/') ? $nsp->name .'#' .$client->id : $client->id; $this->request = $client->request; $this->client = $client; $this->conn = $client->conn; $this->handshake = $this->buildHandshake(); Debug::debug('IO Socket __construct'); } public function __destruct() { Debug::debug('IO Socket __destruct'); } public function buildHandshake() { //todo check this->request->_query $info = !empty($this->request->url) ? parse_url($this->request->url) : array(); $query = array(); if(isset($info['query'])) { parse_str($info['query'], $query); } return array( 'headers' => isset($this->request->headers) ? $this->request->headers : array(), 'time'=> date('D M d Y H:i:s') . ' GMT', 'address'=> $this->conn->remoteAddress, 'xdomain'=> isset($this->request->headers['origin']), 'secure' => !empty($this->request->connection->encrypted), 'issued' => time(), 'url' => isset($this->request->url) ? $this->request->url : '', 'query' => $query, ); } public function __get($name) { if($name === 'broadcast') { $this->flags['broadcast'] = true; return $this; } return null; } public function emit($ev = null) { $args = func_get_args(); if (isset(self::$events[$ev])) { call_user_func_array(array(__CLASS__, 'parent::emit'), $args); } else { $packet = array(); // todo check //$packet['type'] = hasBin($args) ? Parser::BINARY_EVENT : Parser::EVENT; $packet['type'] = Parser::EVENT; $packet['data'] = $args; $flags = $this->flags; // access last argument to see if it's an ACK callback if (is_callable(end($args))) { if ($this->_rooms || isset($flags['broadcast'])) { throw new \Exception('Callbacks are not supported when broadcasting'); } echo('emitting packet with ack id ' . $this->nsp->ids); $this->acks[$this->nsp->ids] = array_pop($args); $packet['id'] = $this->nsp->ids++; } if ($this->_rooms || !empty($flags['broadcast'])) { $this->adapter->broadcast($packet, array( 'except' => array($this->id => $this->id), 'rooms'=> $this->_rooms, 'flags' => $flags )); } else { // dispatch packet $this->packet($packet); } // reset flags $this->_rooms = array(); $this->flags = array(); } return $this; } /** * Targets a room when broadcasting. * * @param {String} name * @return {Socket} self * @api public */ public function to($name) { if(!isset($this->_rooms[$name])) { $this->_rooms[$name] = $name; } return $this; } public function in($name) { return $this->to($name); } /** * Sends a `message` event. * * @return {Socket} self * @api public */ public function send() { $args = func_get_args(); array_unshift($args, 'message'); call_user_func_array(array($this, 'emit'), $args); return $this; } public function write() { $args = func_get_args(); array_unshift($args, 'message'); call_user_func_array(array($this, 'emit'), $args); return $this; } /** * Writes a packet. * * @param {Object} packet object * @param {Object} options * @api private */ public function packet($packet, $preEncoded = false) { if (!$this->nsp || !$this->client) return; $packet['nsp'] = $this->nsp->name; //$volatile = !empty(self::$flagsMap['volatile']); $volatile = false; $this->client->packet($packet, $preEncoded, $volatile); } /** * Joins a room. * * @param {String} room * @param {Function} optional, callback * @return {Socket} self * @api private */ public function join($room) { if (!$this->connected) return $this; if(isset($this->rooms[$room])) return $this; $this->adapter->add($this->id, $room); $this->rooms[$room] = $room; return $this; } /** * Leaves a room. * * @param {String} room * @param {Function} optional, callback * @return {Socket} self * @api private */ public function leave($room) { $this->adapter->del($this->id, $room); unset($this->rooms[$room]); return $this; } /** * Leave all rooms. * * @api private */ public function leaveAll() { $this->adapter->delAll($this->id); $this->rooms = array(); } /** * Called by `Namespace` upon succesful * middleware execution (ie: authorization). * * @api private */ public function onconnect() { $this->nsp->connected[$this->id] = $this; $this->join($this->id); $this->packet(array( 'type' => Parser::CONNECT) ); } /** * Called with each packet. Called by `Client`. * * @param {Object} packet * @api private */ public function onpacket($packet) { switch ($packet['type']) { case Parser::EVENT: $this->onevent($packet); break; case Parser::BINARY_EVENT: $this->onevent($packet); break; case Parser::ACK: $this->onack($packet); break; case Parser::BINARY_ACK: $this->onack($packet); break; case Parser::DISCONNECT: $this->ondisconnect(); break; case Parser::ERROR: $this->emit('error', $packet['data']); } } /** * Called upon event packet. * * @param {Object} packet object * @api private */ public function onevent($packet) { $args = isset($packet['data']) ? $packet['data'] : array(); if (!empty($packet['id']) || (isset($packet['id']) && $packet['id'] === 0)) { $args[] = $this->ack($packet['id']); } call_user_func_array(array(__CLASS__, 'parent::emit'), $args); } /** * Produces an ack callback to emit with an event. * * @param {Number} packet id * @api private */ public function ack($id) { $self = $this; $sent = false; return function()use(&$sent, $id, $self){ // prevent double callbacks if ($sent) return; $args = func_get_args(); $type = $this->hasBin($args) ? Parser::BINARY_ACK : Parser::ACK; $self->packet(array( 'id' => $id, 'type' => $type, 'data' => $args )); }; } /** * Called upon ack packet. * * @api private */ public function onack($packet) { $ack = $this->acks[$packet['id']]; if (is_callable($ack)) { call_user_func($ack, $packet['data']); unset($this->acks[$packet['id']]); } else { echo ('bad ack '. packet.id); } } /** * Called upon client disconnect packet. * * @api private */ public function ondisconnect() { //echo('got disconnect packet'); $this->onclose('client namespace disconnect'); } /** * Handles a client error. * * @api private */ public function onerror($err) { if ($this->listeners('error')) { $this->emit('error', $err); } else { //echo('Missing error handler on `socket`.'); } } /** * Called upon closing. Called by `Client`. * * @param {String} reason * @param {Error} optional error object * @api private */ public function onclose($reason) { if (!$this->connected) return $this; $this->emit('disconnect', $reason); $this->leaveAll(); $this->nsp->remove($this); $this->client->remove($this); $this->connected = false; $this->disconnected = true; unset($this->nsp->connected[$this->id]); // .... $this->nsp = null; $this->server = null; $this->adapter = null; $this->request = null; $this->client = null; $this->conn = null; $this->removeAllListeners(); } /** * Produces an `error` packet. * * @param {Object} error object * @api private */ public function error($err) { $this->packet(array( 'type' => Parser::ERROR, 'data' => $err ) ); } /** * Disconnects this client. * * @param {Boolean} if `true`, closes the underlying connection * @return {Socket} self * @api public */ public function disconnect( $close = false ) { if (!$this->connected) return $this; if ($close) { $this->client->disconnect(); } else { $this->packet(array( 'type'=> Parser::DISCONNECT )); $this->onclose('server namespace disconnect'); } return $this; } /** * Sets the compress flag. * * @param {Boolean} if `true`, compresses the sending data * @return {Socket} self * @api public */ public function compress($compress) { $this->flags['compress'] = $compress; return $this; } protected function hasBin($args) { $hasBin = false; array_walk_recursive($args, function($item, $key) use ($hasBin) { if (!ctype_print($item)) { $hasBin = true; } }); return $hasBin; } }