diff --git a/wp/wp-content/plugins/nginx-helper/admin/class-fastcgi-purger.php b/wp/wp-content/plugins/nginx-helper/admin/class-fastcgi-purger.php
new file mode 100644
index 00000000..687d8625
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/class-fastcgi-purger.php
@@ -0,0 +1,221 @@
+log( '- Purging URL | ' . $url );
+
+ $parse = wp_parse_url( $url );
+
+ if ( ! isset( $parse['path'] ) ) {
+ $parse['path'] = '';
+ }
+
+ switch ( $nginx_helper_admin->options['purge_method'] ) {
+
+ case 'unlink_files':
+ $_url_purge_base = $parse['scheme'] . '://' . $parse['host'] . $parse['path'];
+ $_url_purge = $_url_purge_base;
+
+ if ( ! empty( $parse['query'] ) ) {
+ $_url_purge .= '?' . $parse['query'];
+ }
+
+ $this->delete_cache_file_for( $_url_purge );
+
+ if ( $feed ) {
+
+ $feed_url = rtrim( $_url_purge_base, '/' ) . '/feed/';
+ $this->delete_cache_file_for( $feed_url );
+ $this->delete_cache_file_for( $feed_url . 'atom/' );
+ $this->delete_cache_file_for( $feed_url . 'rdf/' );
+
+ }
+ break;
+
+ case 'get_request':
+ // Go to default case.
+ default:
+ $_url_purge_base = $this->purge_base_url() . $parse['path'];
+ $_url_purge = $_url_purge_base;
+
+ if ( isset( $parse['query'] ) && '' !== $parse['query'] ) {
+ $_url_purge .= '?' . $parse['query'];
+ }
+
+ $this->do_remote_get( $_url_purge );
+
+ if ( $feed ) {
+
+ $feed_url = rtrim( $_url_purge_base, '/' ) . '/feed/';
+ $this->do_remote_get( $feed_url );
+ $this->do_remote_get( $feed_url . 'atom/' );
+ $this->do_remote_get( $feed_url . 'rdf/' );
+
+ }
+ break;
+
+ }
+
+ }
+
+ /**
+ * Function to custom purge urls.
+ */
+ public function custom_purge_urls() {
+
+ global $nginx_helper_admin;
+
+ $parse = wp_parse_url( home_url() );
+
+ $purge_urls = isset( $nginx_helper_admin->options['purge_url'] ) && ! empty( $nginx_helper_admin->options['purge_url'] ) ?
+ explode( "\r\n", $nginx_helper_admin->options['purge_url'] ) : array();
+
+ /**
+ * Allow plugins/themes to modify/extend urls.
+ *
+ * @param array $purge_urls URLs which needs to be purged.
+ * @param bool $wildcard If wildcard in url is allowed or not. default false.
+ */
+ $purge_urls = apply_filters( 'rt_nginx_helper_purge_urls', $purge_urls, false );
+
+ switch ( $nginx_helper_admin->options['purge_method'] ) {
+
+ case 'unlink_files':
+ $_url_purge_base = $parse['scheme'] . '://' . $parse['host'];
+
+ if ( is_array( $purge_urls ) && ! empty( $purge_urls ) ) {
+
+ foreach ( $purge_urls as $purge_url ) {
+
+ $purge_url = trim( $purge_url );
+
+ if ( strpos( $purge_url, '*' ) === false ) {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $this->log( '- Purging URL | ' . $purge_url );
+ $this->delete_cache_file_for( $purge_url );
+
+ }
+ }
+ }
+ break;
+
+ case 'get_request':
+ // Go to default case.
+ default:
+ $_url_purge_base = $this->purge_base_url();
+
+ if ( is_array( $purge_urls ) && ! empty( $purge_urls ) ) {
+
+ foreach ( $purge_urls as $purge_url ) {
+
+ $purge_url = trim( $purge_url );
+
+ if ( strpos( $purge_url, '*' ) === false ) {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $this->log( '- Purging URL | ' . $purge_url );
+ $this->do_remote_get( $purge_url );
+
+ }
+ }
+ }
+ break;
+
+ }
+
+ }
+
+ /**
+ * Purge everything.
+ */
+ public function purge_all() {
+
+ $this->unlink_recursive( RT_WP_NGINX_HELPER_CACHE_PATH, false );
+ $this->log( '* * * * *' );
+ $this->log( '* Purged Everything!' );
+ $this->log( '* * * * *' );
+
+ /**
+ * Fire an action after the FastCGI cache has been purged.
+ *
+ * @since 2.1.0
+ */
+ do_action( 'rt_nginx_helper_after_fastcgi_purge_all' );
+ }
+
+ /**
+ * Constructs the base url to call when purging using the "get_request" method.
+ *
+ * @since 2.2.0
+ *
+ * @return string
+ */
+ private function purge_base_url() {
+
+ $parse = wp_parse_url( home_url() );
+
+ /**
+ * Filter to change purge suffix for FastCGI cache.
+ *
+ * @param string $suffix Purge suffix. Default is purge.
+ *
+ * @since 2.2.0
+ */
+ $path = apply_filters( 'rt_nginx_helper_fastcgi_purge_suffix', 'purge' );
+
+ // Prevent users from inserting a trailing '/' that could break the url purging.
+ $path = trim( $path, '/' );
+
+ $purge_url_base = $parse['scheme'] . '://' . $parse['host'] . '/' . $path;
+
+ /**
+ * Filter to change purge URL base for FastCGI cache.
+ *
+ * @param string $purge_url_base Purge URL base.
+ *
+ * @since 2.2.0
+ */
+ $purge_url_base = apply_filters( 'rt_nginx_helper_fastcgi_purge_url_base', $purge_url_base );
+
+ // Prevent users from inserting a trailing '/' that could break the url purging.
+ return untrailingslashit( $purge_url_base );
+
+ }
+
+}
diff --git a/wp/wp-content/plugins/nginx-helper/admin/class-nginx-helper-admin.php b/wp/wp-content/plugins/nginx-helper/admin/class-nginx-helper-admin.php
new file mode 100644
index 00000000..a024e318
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/class-nginx-helper-admin.php
@@ -0,0 +1,757 @@
+plugin_name = $plugin_name;
+ $this->version = $version;
+
+ /**
+ * Define settings tabs
+ */
+ $this->settings_tabs = apply_filters(
+ 'rt_nginx_helper_settings_tabs',
+ array(
+ 'general' => array(
+ 'menu_title' => __( 'General', 'nginx-helper' ),
+ 'menu_slug' => 'general',
+ ),
+ 'support' => array(
+ 'menu_title' => __( 'Support', 'nginx-helper' ),
+ 'menu_slug' => 'support',
+ ),
+ )
+ );
+
+ $this->options = $this->nginx_helper_settings();
+
+ }
+
+ /**
+ * Register the stylesheets for the admin area.
+ *
+ * @since 2.0.0
+ *
+ * @param string $hook The current admin page.
+ */
+ public function enqueue_styles( $hook ) {
+
+ /**
+ * This function is provided for demonstration purposes only.
+ *
+ * An instance of this class should be passed to the run() function
+ * defined in Nginx_Helper_Loader as all of the hooks are defined
+ * in that particular class.
+ *
+ * The Nginx_Helper_Loader will then create the relationship
+ * between the defined hooks and the functions defined in this
+ * class.
+ */
+
+ if ( 'settings_page_nginx' !== $hook ) {
+ return;
+ }
+
+ wp_enqueue_style( $this->plugin_name . '-icons', plugin_dir_url( __FILE__ ) . 'icons/css/nginx-fontello.css', array(), $this->version, 'all' );
+ wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/nginx-helper-admin.css', array(), $this->version, 'all' );
+
+ }
+
+ /**
+ * Register the JavaScript for the admin area.
+ *
+ * @since 2.0.0
+ *
+ * @param string $hook The current admin page.
+ */
+ public function enqueue_scripts( $hook ) {
+
+ /**
+ * This function is provided for demonstration purposes only.
+ *
+ * An instance of this class should be passed to the run() function
+ * defined in Nginx_Helper_Loader as all of the hooks are defined
+ * in that particular class.
+ *
+ * The Nginx_Helper_Loader will then create the relationship
+ * between the defined hooks and the functions defined in this
+ * class.
+ */
+
+ if ( 'settings_page_nginx' !== $hook ) {
+ return;
+ }
+
+ wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/nginx-helper-admin.js', array( 'jquery' ), $this->version, false );
+
+ $do_localize = array(
+ 'purge_confirm_string' => esc_html__( 'Purging entire cache is not recommended. Would you like to continue?', 'nginx-helper' ),
+ );
+ wp_localize_script( $this->plugin_name, 'nginx_helper', $do_localize );
+
+ }
+
+ /**
+ * Add admin menu.
+ *
+ * @since 2.0.0
+ */
+ public function nginx_helper_admin_menu() {
+
+ if ( is_multisite() ) {
+
+ add_submenu_page(
+ 'settings.php',
+ __( 'Nginx Helper', 'nginx-helper' ),
+ __( 'Nginx Helper', 'nginx-helper' ),
+ 'manage_options',
+ 'nginx',
+ array( &$this, 'nginx_helper_setting_page' )
+ );
+
+ } else {
+
+ add_submenu_page(
+ 'options-general.php',
+ __( 'Nginx Helper', 'nginx-helper' ),
+ __( 'Nginx Helper', 'nginx-helper' ),
+ 'manage_options',
+ 'nginx',
+ array( &$this, 'nginx_helper_setting_page' )
+ );
+
+ }
+
+ }
+
+ /**
+ * Function to add toolbar purge link.
+ *
+ * @param object $wp_admin_bar Admin bar object.
+ */
+ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) {
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ if ( is_admin() ) {
+ $nginx_helper_urls = 'all';
+ $link_title = __( 'Purge Cache', 'nginx-helper' );
+ } else {
+ $nginx_helper_urls = 'current-url';
+ $link_title = __( 'Purge Current Page', 'nginx-helper' );
+ }
+
+ $purge_url = add_query_arg(
+ array(
+ 'nginx_helper_action' => 'purge',
+ 'nginx_helper_urls' => $nginx_helper_urls,
+ )
+ );
+
+ $nonced_url = wp_nonce_url( $purge_url, 'nginx_helper-purge_all' );
+
+ $wp_admin_bar->add_menu(
+ array(
+ 'id' => 'nginx-helper-purge-all',
+ 'title' => $link_title,
+ 'href' => $nonced_url,
+ 'meta' => array( 'title' => $link_title ),
+ )
+ );
+
+ }
+
+ /**
+ * Display settings.
+ *
+ * @global $string $pagenow Contain current admin page.
+ *
+ * @since 2.0.0
+ */
+ public function nginx_helper_setting_page() {
+ include plugin_dir_path( __FILE__ ) . 'partials/nginx-helper-admin-display.php';
+ }
+
+ /**
+ * Default settings.
+ *
+ * @since 2.0.0
+ * @return array
+ */
+ public function nginx_helper_default_settings() {
+
+ return array(
+ 'enable_purge' => 0,
+ 'cache_method' => 'enable_fastcgi',
+ 'purge_method' => 'get_request',
+ 'enable_map' => 0,
+ 'enable_log' => 0,
+ 'log_level' => 'INFO',
+ 'log_filesize' => '5',
+ 'enable_stamp' => 0,
+ 'purge_homepage_on_edit' => 1,
+ 'purge_homepage_on_del' => 1,
+ 'purge_archive_on_edit' => 1,
+ 'purge_archive_on_del' => 1,
+ 'purge_archive_on_new_comment' => 0,
+ 'purge_archive_on_deleted_comment' => 0,
+ 'purge_page_on_mod' => 1,
+ 'purge_page_on_new_comment' => 1,
+ 'purge_page_on_deleted_comment' => 1,
+ 'redis_hostname' => '127.0.0.1',
+ 'redis_port' => '6379',
+ 'redis_prefix' => 'nginx-cache:',
+ 'purge_url' => '',
+ 'redis_enabled_by_constant' => 0,
+ );
+
+ }
+
+ /**
+ * Get settings.
+ *
+ * @since 2.0.0
+ */
+ public function nginx_helper_settings() {
+
+ $options = get_site_option(
+ 'rt_wp_nginx_helper_options',
+ array(
+ 'redis_hostname' => '127.0.0.1',
+ 'redis_port' => '6379',
+ 'redis_prefix' => 'nginx-cache:',
+ )
+ );
+
+ $data = wp_parse_args(
+ $options,
+ $this->nginx_helper_default_settings()
+ );
+
+ $is_redis_enabled = (
+ defined( 'RT_WP_NGINX_HELPER_REDIS_HOSTNAME' ) &&
+ defined( 'RT_WP_NGINX_HELPER_REDIS_PORT' ) &&
+ defined( 'RT_WP_NGINX_HELPER_REDIS_PREFIX' )
+ );
+
+ if ( ! $is_redis_enabled ) {
+ return $data;
+ }
+
+ $data['redis_enabled_by_constant'] = $is_redis_enabled;
+ $data['enable_purge'] = $is_redis_enabled;
+ $data['cache_method'] = 'enable_redis';
+ $data['redis_hostname'] = RT_WP_NGINX_HELPER_REDIS_HOSTNAME;
+ $data['redis_port'] = RT_WP_NGINX_HELPER_REDIS_PORT;
+ $data['redis_prefix'] = RT_WP_NGINX_HELPER_REDIS_PREFIX;
+
+ return $data;
+
+ }
+
+ /**
+ * Nginx helper setting link function.
+ *
+ * @param array $links links.
+ *
+ * @return mixed
+ */
+ public function nginx_helper_settings_link( $links ) {
+
+ if ( is_network_admin() ) {
+ $setting_page = 'settings.php';
+ } else {
+ $setting_page = 'options-general.php';
+ }
+
+ $settings_link = '' . __( 'Settings', 'nginx-helper' ) . '';
+ array_unshift( $links, $settings_link );
+
+ return $links;
+
+ }
+
+ /**
+ * Retrieve the asset path.
+ *
+ * @since 2.0.0
+ * @return string asset path of the plugin.
+ */
+ public function functional_asset_path() {
+
+ $log_path = WP_CONTENT_DIR . '/uploads/nginx-helper/';
+
+ return apply_filters( 'nginx_asset_path', $log_path );
+
+ }
+
+ /**
+ * Retrieve the asset url.
+ *
+ * @since 2.0.0
+ * @return string asset url of the plugin.
+ */
+ public function functional_asset_url() {
+
+ $log_url = WP_CONTENT_URL . '/uploads/nginx-helper/';
+
+ return apply_filters( 'nginx_asset_url', $log_url );
+
+ }
+
+ /**
+ * Get latest news.
+ *
+ * @since 2.0.0
+ */
+ public function nginx_helper_get_feeds() {
+
+ // Get RSS Feed(s).
+ require_once ABSPATH . WPINC . '/feed.php';
+
+ $maxitems = 0;
+ $rss_items = array();
+
+ // Get a SimplePie feed object from the specified feed source.
+ $rss = fetch_feed( 'https://rtcamp.com/blog/feed/' );
+
+ if ( ! is_wp_error( $rss ) ) { // Checks that the object is created correctly.
+
+ // Figure out how many total items there are, but limit it to 5.
+ $maxitems = $rss->get_item_quantity( 5 );
+ // Build an array of all the items, starting with element 0 (first element).
+ $rss_items = $rss->get_items( 0, $maxitems );
+
+ }
+ ?>
+
+ ' . esc_html_e( 'No items', 'nginx-helper' ) . '.';
+ } else {
+
+ // Loop through each feed item and display each item as a hyperlink.
+ foreach ( $rss_items as $item ) {
+ ?>
+ -
+ %s',
+ esc_url( $item->get_permalink() ),
+ esc_attr__( 'Posted ', 'nginx-helper' ) . esc_attr( $item->get_date( 'j F Y | g:i a' ) ),
+ esc_html( $item->get_title() )
+ );
+ ?>
+
+
+
+ options['enable_purge'] || 1 !== (int) $this->options['enable_stamp'] ) {
+ return;
+ }
+
+ if ( ! empty( $pagenow ) && 'wp-login.php' === $pagenow ) {
+ return;
+ }
+
+ foreach ( headers_list() as $header ) {
+ list( $key, $value ) = explode( ':', $header, 2 );
+ $key = strtolower( $key );
+ if ( 'content-type' === $key && strpos( trim( $value ), 'text/html' ) !== 0 ) {
+ return;
+ }
+ if ( 'content-type' === $key ) {
+ break;
+ }
+ }
+
+ /**
+ * Don't add timestamp if run from ajax, cron or wpcli.
+ */
+ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+ return;
+ }
+
+ if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
+ return;
+ }
+
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ return;
+ }
+
+ $timestamps = "\n\n" .
+ '';
+
+ echo wp_kses( $timestamps, array() );
+
+ }
+
+ /**
+ * Get map
+ *
+ * @global object $wpdb
+ *
+ * @return string
+ */
+ public function get_map() {
+
+ if ( ! $this->options['enable_map'] ) {
+ return;
+ }
+
+ if ( is_multisite() ) {
+
+ global $wpdb;
+
+ $rt_all_blogs = $wpdb->get_results(
+ $wpdb->prepare(
+ 'SELECT blog_id, domain, path FROM ' . $wpdb->blogs . " WHERE site_id = %d AND archived = '0' AND mature = '0' AND spam = '0' AND deleted = '0'",
+ $wpdb->siteid
+ )
+ );
+
+ $wpdb->dmtable = $wpdb->base_prefix . 'domain_mapping';
+
+ $rt_domain_map_sites = '';
+
+ if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->dmtable}'" ) === $wpdb->dmtable ) { // phpcs:ignore
+ $rt_domain_map_sites = $wpdb->get_results( "SELECT blog_id, domain FROM {$wpdb->dmtable} ORDER BY id DESC" );
+ }
+
+ $rt_nginx_map = '';
+ $rt_nginx_map_array = array();
+
+ if ( $rt_all_blogs ) {
+
+ foreach ( $rt_all_blogs as $blog ) {
+
+ if ( true === SUBDOMAIN_INSTALL ) {
+ $rt_nginx_map_array[ $blog->domain ] = $blog->blog_id;
+ } else {
+
+ if ( 1 !== $blog->blog_id ) {
+ $rt_nginx_map_array[ $blog->path ] = $blog->blog_id;
+ }
+ }
+ }
+ }
+
+ if ( $rt_domain_map_sites ) {
+
+ foreach ( $rt_domain_map_sites as $site ) {
+ $rt_nginx_map_array[ $site->domain ] = $site->blog_id;
+ }
+ }
+
+ foreach ( $rt_nginx_map_array as $domain => $domain_id ) {
+ $rt_nginx_map .= "\t" . $domain . "\t" . $domain_id . ";\n";
+ }
+
+ return $rt_nginx_map;
+
+ }
+
+ }
+
+ /**
+ * Update map
+ */
+ public function update_map() {
+
+ if ( is_multisite() ) {
+
+ $rt_nginx_map = $this->get_map();
+
+ $fp = fopen( $this->functional_asset_path() . 'map.conf', 'w+' );
+ if ( $fp ) {
+ fwrite( $fp, $rt_nginx_map );
+ fclose( $fp );
+ }
+ }
+
+ }
+
+ /**
+ * Purge url when post status is changed.
+ *
+ * @global string $blog_id Blog id.
+ * @global object $nginx_purger Nginx purger variable.
+ *
+ * @param string $new_status New status.
+ * @param string $old_status Old status.
+ * @param object $post Post object.
+ */
+ public function set_future_post_option_on_future_status( $new_status, $old_status, $post ) {
+
+ global $blog_id, $nginx_purger;
+
+ $exclude_post_types = array( 'nav_menu_item' );
+
+ if ( in_array( $post->post_type, $exclude_post_types, true ) ) {
+ return;
+ }
+
+ if ( ! $this->options['enable_purge'] ) {
+ return;
+ }
+
+ $purge_status = array( 'publish', 'future' );
+
+ if ( in_array( $old_status, $purge_status, true ) || in_array( $new_status, $purge_status, true ) ) {
+
+ $nginx_purger->log( 'Purge post on transition post STATUS from ' . $old_status . ' to ' . $new_status );
+ $nginx_purger->purge_post( $post->ID );
+
+ }
+
+ if (
+ 'future' === $new_status && $post && 'future' === $post->post_status &&
+ (
+ ( 'post' === $post->post_type || 'page' === $post->post_type ) ||
+ (
+ isset( $this->options['custom_post_types_recognized'] ) &&
+ in_array( $post->post_type, $this->options['custom_post_types_recognized'], true )
+ )
+ )
+ ) {
+
+ $nginx_purger->log( 'Set/update future_posts option ( post id = ' . $post->ID . ' and blog id = ' . $blog_id . ' )' );
+ $this->options['future_posts'][ $blog_id ][ $post->ID ] = strtotime( $post->post_date_gmt ) + 60;
+ update_site_option( 'rt_wp_nginx_helper_options', $this->options );
+
+ }
+
+ }
+
+ /**
+ * Unset future post option on delete
+ *
+ * @global string $blog_id Blog id.
+ * @global object $nginx_purger Nginx helper object.
+ *
+ * @param int $post_id Post id.
+ */
+ public function unset_future_post_option_on_delete( $post_id ) {
+
+ global $blog_id, $nginx_purger;
+
+ if (
+ ! $this->options['enable_purge'] ||
+ empty( $this->options['future_posts'] ) ||
+ empty( $this->options['future_posts'][ $blog_id ] ) ||
+ isset( $this->options['future_posts'][ $blog_id ][ $post_id ] ) ||
+ wp_is_post_revision( $post_id )
+ ) {
+ return;
+ }
+
+ $nginx_purger->log( 'Unset future_posts option ( post id = ' . $post_id . ' and blog id = ' . $blog_id . ' )' );
+
+ unset( $this->options['future_posts'][ $blog_id ][ $post_id ] );
+
+ if ( ! count( $this->options['future_posts'][ $blog_id ] ) ) {
+ unset( $this->options['future_posts'][ $blog_id ] );
+ }
+
+ update_site_option( 'rt_wp_nginx_helper_options', $this->options );
+ }
+
+ /**
+ * Update map when new blog added in multisite.
+ *
+ * @global object $nginx_purger Nginx purger class object.
+ *
+ * @param string $blog_id blog id.
+ */
+ public function update_new_blog_options( $blog_id ) {
+
+ global $nginx_purger;
+
+ $nginx_purger->log( "New site added ( id $blog_id )" );
+ $this->update_map();
+ $nginx_purger->log( "New site added to nginx map ( id $blog_id )" );
+ $helper_options = $this->nginx_helper_default_settings();
+ update_blog_option( $blog_id, 'rt_wp_nginx_helper_options', $helper_options );
+ $nginx_purger->log( "Default options updated for the new blog ( id $blog_id )" );
+
+ }
+
+ /**
+ * Purge all urls.
+ * Purge current page cache when purging is requested from front
+ * and all urls when requested from admin dashboard.
+ *
+ * @global object $nginx_purger
+ */
+ public function purge_all() {
+
+ global $nginx_purger, $wp;
+
+ $method = null;
+ if ( isset( $_SERVER['REQUEST_METHOD'] ) ) {
+ $method = wp_strip_all_tags( $_SERVER['REQUEST_METHOD'] );
+ }
+
+ $action = '';
+ if ( 'POST' === $method ) {
+ if ( isset( $_POST['nginx_helper_action'] ) ) {
+ $action = wp_strip_all_tags( $_POST['nginx_helper_action'] );
+ }
+ } else {
+ if ( isset( $_GET['nginx_helper_action'] ) ) {
+ $action = wp_strip_all_tags( $_GET['nginx_helper_action'] );
+ }
+ }
+
+ if ( empty( $action ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( 'Sorry, you do not have the necessary privileges to edit these options.' );
+ }
+
+ if ( 'done' === $action ) {
+
+ add_action( 'admin_notices', array( &$this, 'display_notices' ) );
+ add_action( 'network_admin_notices', array( &$this, 'display_notices' ) );
+ return;
+
+ }
+
+ check_admin_referer( 'nginx_helper-purge_all' );
+
+ $current_url = user_trailingslashit( home_url( $wp->request ) );
+
+ if ( ! is_admin() ) {
+ $action = 'purge_current_page';
+ $redirect_url = $current_url;
+ } else {
+ $redirect_url = add_query_arg( array( 'nginx_helper_action' => 'done' ) );
+ }
+
+ switch ( $action ) {
+ case 'purge':
+ $nginx_purger->purge_all();
+ break;
+ case 'purge_current_page':
+ $nginx_purger->purge_url( $current_url );
+ break;
+ }
+
+ if ( 'purge' === $action ) {
+
+ /**
+ * Fire an action after the entire cache has been purged whatever caching type is used.
+ *
+ * @since 2.2.2
+ */
+ do_action( 'rt_nginx_helper_after_purge_all' );
+
+ }
+
+ wp_redirect( esc_url_raw( $redirect_url ) );
+ exit();
+
+ }
+
+ /**
+ * Dispay plugin notices.
+ */
+ public function display_notices() {
+ echo '' . esc_html__( 'Purge initiated', 'nginx-helper' ) . '
';
+ }
+
+}
diff --git a/wp/wp-content/plugins/nginx-helper/admin/class-phpredis-purger.php b/wp/wp-content/plugins/nginx-helper/admin/class-phpredis-purger.php
new file mode 100644
index 00000000..10a84831
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/class-phpredis-purger.php
@@ -0,0 +1,272 @@
+redis_object = new Redis();
+ $this->redis_object->connect(
+ $nginx_helper_admin->options['redis_hostname'],
+ $nginx_helper_admin->options['redis_port'],
+ 5
+ );
+
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+ /**
+ * Purge all cache.
+ */
+ public function purge_all() {
+
+ global $nginx_helper_admin;
+
+ $prefix = trim( $nginx_helper_admin->options['redis_prefix'] );
+
+ $this->log( '* * * * *' );
+
+ // If Purge Cache link click from network admin then purge all.
+ if ( is_network_admin() ) {
+
+ $total_keys_purged = $this->delete_keys_by_wildcard( $prefix . '*' );
+ $this->log( '* Purged Everything! * ' );
+
+ } else { // Else purge only site specific cache.
+
+ $parse = wp_parse_url( get_home_url() );
+ $parse['path'] = empty( $parse['path'] ) ? '/' : $parse['path'];
+ $total_keys_purged = $this->delete_keys_by_wildcard( $prefix . $parse['scheme'] . 'GET' . $parse['host'] . $parse['path'] . '*' );
+ $this->log( '* ' . get_home_url() . ' Purged! * ' );
+
+ }
+
+ if ( $total_keys_purged ) {
+ $this->log( "Total {$total_keys_purged} urls purged." );
+ } else {
+ $this->log( 'No Cache found.' );
+ }
+
+ $this->log( '* * * * *' );
+
+ /**
+ * Fire an action after the Redis cache has been purged.
+ *
+ * @since 2.1.0
+ */
+ do_action( 'rt_nginx_helper_after_redis_purge_all' );
+ }
+
+ /**
+ * Purge url.
+ *
+ * @param string $url URL to purge.
+ * @param bool $feed Feed or not.
+ */
+ public function purge_url( $url, $feed = true ) {
+
+ global $nginx_helper_admin;
+
+ /**
+ * Filters the URL to be purged.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ */
+ $url = apply_filters( 'rt_nginx_helper_purge_url', $url );
+
+ $parse = wp_parse_url( $url );
+
+ if ( ! isset( $parse['path'] ) ) {
+ $parse['path'] = '';
+ }
+
+ $prefix = $nginx_helper_admin->options['redis_prefix'];
+ $_url_purge_base = $prefix . $parse['scheme'] . 'GET' . $parse['host'] . $parse['path'];
+
+ /**
+ * To delete device type caches such as `--mobile`, `--desktop`, `--lowend`, etc.
+ * This would need $url above to be changed with this filter `rt_nginx_helper_purge_url` by cache key that Nginx sets while generating cache.
+ *
+ * For example: If page is accessed from desktop, then cache will be generated by appending `--desktop` to current URL.
+ * Add this filter in separate plugin or simply in theme's function.php file:
+ * ```
+ * add_filter( 'rt_nginx_helper_purge_url', function( $url ) {
+ * $url = $url . '--*';
+ * return $url;
+ * });
+ * ```
+ *
+ * Regardless of what key / suffix is being to store `$device_type` cache , it will be deleted.
+ *
+ * @since 2.1.0
+ */
+ if ( strpos( $_url_purge_base, '*' ) === false ) {
+
+ $status = $this->delete_single_key( $_url_purge_base );
+
+ if ( $status ) {
+ $this->log( '- Purge URL | ' . $_url_purge_base );
+ } else {
+ $this->log( '- Cache Not Found | ' . $_url_purge_base, 'ERROR' );
+ }
+ } else {
+
+ $status = $this->delete_keys_by_wildcard( $_url_purge_base );
+
+ if ( $status ) {
+ $this->log( '- Purge Wild Card URL | ' . $_url_purge_base . ' | ' . $status . ' url purged' );
+ } else {
+ $this->log( '- Cache Not Found | ' . $_url_purge_base, 'ERROR' );
+ }
+ }
+
+ $this->log( '* * * * *' );
+
+ }
+
+ /**
+ * Custom purge urls.
+ */
+ public function custom_purge_urls() {
+
+ global $nginx_helper_admin;
+
+ $parse = wp_parse_url( home_url() );
+ $prefix = $nginx_helper_admin->options['redis_prefix'];
+ $_url_purge_base = $prefix . $parse['scheme'] . 'GET' . $parse['host'];
+
+ $purge_urls = isset( $nginx_helper_admin->options['purge_url'] ) && ! empty( $nginx_helper_admin->options['purge_url'] ) ?
+ explode( "\r\n", $nginx_helper_admin->options['purge_url'] ) : array();
+
+ /**
+ * Allow plugins/themes to modify/extend urls.
+ *
+ * @param array $purge_urls URLs which needs to be purged.
+ * @param bool $wildcard If wildcard in url is allowed or not. default true.
+ */
+ $purge_urls = apply_filters( 'rt_nginx_helper_purge_urls', $purge_urls, true );
+
+ if ( is_array( $purge_urls ) && ! empty( $purge_urls ) ) {
+
+ foreach ( $purge_urls as $purge_url ) {
+
+ $purge_url = trim( $purge_url );
+
+ if ( strpos( $purge_url, '*' ) === false ) {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $status = $this->delete_single_key( $purge_url );
+
+ if ( $status ) {
+ $this->log( '- Purge URL | ' . $purge_url );
+ } else {
+ $this->log( '- Cache Not Found | ' . $purge_url, 'ERROR' );
+ }
+ } else {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $status = $this->delete_keys_by_wildcard( $purge_url );
+
+ if ( $status ) {
+ $this->log( '- Purge Wild Card URL | ' . $purge_url . ' | ' . $status . ' url purged' );
+ } else {
+ $this->log( '- Cache Not Found | ' . $purge_url, 'ERROR' );
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Single Key Delete Example
+ * e.g. $key can be nginx-cache:httpGETexample.com/
+ *
+ * @param string $key Key.
+ *
+ * @return int
+ */
+ public function delete_single_key( $key ) {
+
+ try {
+ return $this->redis_object->del( $key );
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+ /**
+ * Delete Keys by wildcard.
+ * e.g. $key can be nginx-cache:httpGETexample.com*
+ *
+ * Lua Script block to delete multiple keys using wildcard
+ * Script will return count i.e. number of keys deleted
+ * if return value is 0, that means no matches were found
+ *
+ * Call redis eval and return value from lua script
+ *
+ * @param string $pattern pattern.
+ *
+ * @return mixed
+ */
+ public function delete_keys_by_wildcard( $pattern ) {
+
+ // Lua Script.
+ $lua = <<redis_object->eval( $lua, array( $pattern ), 1 );
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+}
diff --git a/wp/wp-content/plugins/nginx-helper/admin/class-predis-purger.php b/wp/wp-content/plugins/nginx-helper/admin/class-predis-purger.php
new file mode 100644
index 00000000..1290a0de
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/class-predis-purger.php
@@ -0,0 +1,268 @@
+redis_object = new Predis\Client(
+ array(
+ 'host' => $nginx_helper_admin->options['redis_hostname'],
+ 'port' => $nginx_helper_admin->options['redis_port'],
+ )
+ );
+
+ try {
+ $this->redis_object->connect();
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+ /**
+ * Purge all.
+ */
+ public function purge_all() {
+
+ global $nginx_helper_admin;
+
+ $prefix = trim( $nginx_helper_admin->options['redis_prefix'] );
+
+ $this->log( '* * * * *' );
+
+ // If Purge Cache link click from network admin then purge all.
+ if ( is_network_admin() ) {
+
+ $this->delete_keys_by_wildcard( $prefix . '*' );
+ $this->log( '* Purged Everything! * ' );
+
+ } else { // Else purge only site specific cache.
+
+ $parse = wp_parse_url( get_home_url() );
+ $parse['path'] = empty( $parse['path'] ) ? '/' : $parse['path'];
+ $this->delete_keys_by_wildcard( $prefix . $parse['scheme'] . 'GET' . $parse['host'] . $parse['path'] . '*' );
+ $this->log( '* ' . get_home_url() . ' Purged! * ' );
+
+ }
+
+ $this->log( '* * * * *' );
+
+ /**
+ * Fire an action after the Redis cache has been purged.
+ *
+ * @since 2.1.0
+ */
+ do_action( 'rt_nginx_helper_after_redis_purge_all' );
+ }
+
+ /**
+ * Purge url.
+ *
+ * @param string $url URL.
+ * @param bool $feed Feed or not.
+ */
+ public function purge_url( $url, $feed = true ) {
+
+ global $nginx_helper_admin;
+
+ /**
+ * Filters the URL to be purged.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ */
+ $url = apply_filters( 'rt_nginx_helper_purge_url', $url );
+
+ $this->log( '- Purging URL | ' . $url );
+
+ $parse = wp_parse_url( $url );
+
+ if ( ! isset( $parse['path'] ) ) {
+ $parse['path'] = '';
+ }
+
+ $prefix = $nginx_helper_admin->options['redis_prefix'];
+ $_url_purge_base = $prefix . $parse['scheme'] . 'GET' . $parse['host'] . $parse['path'];
+
+ /**
+ * To delete device type caches such as `--mobile`, `--desktop`, `--lowend`, etc.
+ * This would need $url above to be changed with this filter `rt_nginx_helper_purge_url` by cache key that Nginx sets while generating cache.
+ *
+ * For example: If page is accessed from desktop, then cache will be generated by appending `--desktop` to current URL.
+ * Add this filter in separate plugin or simply in theme's function.php file:
+ * ```
+ * add_filter( 'rt_nginx_helper_purge_url', function( $url ) {
+ * $url = $url . '--*';
+ * return $url;
+ * });
+ * ```
+ *
+ * Regardless of what key / suffix is being to store `$device_type` cache , it will be deleted.
+ *
+ * @since 2.1.0
+ */
+ if ( strpos( $_url_purge_base, '*' ) === false ) {
+
+ $status = $this->delete_single_key( $_url_purge_base );
+
+ if ( $status ) {
+ $this->log( '- Purge URL | ' . $_url_purge_base );
+ } else {
+ $this->log( '- Cache Not Found | ' . $_url_purge_base, 'ERROR' );
+ }
+ } else {
+
+ $status = $this->delete_keys_by_wildcard( $_url_purge_base );
+
+ if ( $status ) {
+ $this->log( '- Purge Wild Card URL | ' . $_url_purge_base . ' | ' . $status . ' url purged' );
+ } else {
+ $this->log( '- Cache Not Found | ' . $_url_purge_base, 'ERROR' );
+ }
+ }
+
+ }
+
+ /**
+ * Custom purge urls.
+ */
+ public function custom_purge_urls() {
+
+ global $nginx_helper_admin;
+
+ $parse = wp_parse_url( home_url() );
+ $prefix = $nginx_helper_admin->options['redis_prefix'];
+ $_url_purge_base = $prefix . $parse['scheme'] . 'GET' . $parse['host'];
+
+ $purge_urls = isset( $nginx_helper_admin->options['purge_url'] ) && ! empty( $nginx_helper_admin->options['purge_url'] ) ?
+ explode( "\r\n", $nginx_helper_admin->options['purge_url'] ) : array();
+
+ /**
+ * Allow plugins/themes to modify/extend urls.
+ *
+ * @param array $purge_urls URLs which needs to be purged.
+ * @param bool $wildcard If wildcard in url is allowed or not. default true.
+ */
+ $purge_urls = apply_filters( 'rt_nginx_helper_purge_urls', $purge_urls, true );
+
+ if ( is_array( $purge_urls ) && ! empty( $purge_urls ) ) {
+
+ foreach ( $purge_urls as $purge_url ) {
+
+ $purge_url = trim( $purge_url );
+
+ if ( strpos( $purge_url, '*' ) === false ) {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $status = $this->delete_single_key( $purge_url );
+ if ( $status ) {
+ $this->log( '- Purge URL | ' . $purge_url );
+ } else {
+ $this->log( '- Not Found | ' . $purge_url, 'ERROR' );
+ }
+ } else {
+
+ $purge_url = $_url_purge_base . $purge_url;
+ $status = $this->delete_keys_by_wildcard( $purge_url );
+
+ if ( $status ) {
+ $this->log( '- Purge Wild Card URL | ' . $purge_url . ' | ' . $status . ' url purged' );
+ } else {
+ $this->log( '- Not Found | ' . $purge_url, 'ERROR' );
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Single Key Delete Example
+ * e.g. $key can be nginx-cache:httpGETexample.com/
+ *
+ * @param string $key Key to delete cache.
+ *
+ * @return mixed
+ */
+ public function delete_single_key( $key ) {
+
+ try {
+ return $this->redis_object->executeRaw( array( 'DEL', $key ) );
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+ /**
+ * Delete Keys by wildcard.
+ * e.g. $key can be nginx-cache:httpGETexample.com*
+ *
+ * Lua Script block to delete multiple keys using wildcard
+ * Script will return count i.e. number of keys deleted
+ * if return value is 0, that means no matches were found
+ *
+ * Call redis eval and return value from lua script
+ *
+ * @param string $pattern Pattern.
+ *
+ * @return mixed
+ */
+ public function delete_keys_by_wildcard( $pattern ) {
+
+ // Lua Script.
+ $lua = <<redis_object->eval( $lua, 1, $pattern );
+ } catch ( Exception $e ) {
+ $this->log( $e->getMessage(), 'ERROR' );
+ }
+
+ }
+
+}
diff --git a/wp/wp-content/plugins/nginx-helper/admin/class-purger.php b/wp/wp-content/plugins/nginx-helper/admin/class-purger.php
new file mode 100644
index 00000000..ac54b8d0
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/class-purger.php
@@ -0,0 +1,1262 @@
+comment_approved;
+
+ if ( null === $approved ) {
+ $newstatus = false;
+ } elseif ( '1' === $approved ) {
+ $newstatus = 'approved';
+ } elseif ( '0' === $approved ) {
+ $newstatus = 'unapproved';
+ } elseif ( 'spam' === $approved ) {
+ $newstatus = 'spam';
+ } elseif ( 'trash' === $approved ) {
+ $newstatus = 'trash';
+ } else {
+ $newstatus = false;
+ }
+
+ $this->purge_post_on_comment_change( $newstatus, $oldstatus, $comment );
+
+ }
+
+ /**
+ * Purge post cache on comment change.
+ *
+ * @param string $newstatus New status.
+ * @param string $oldstatus Old status.
+ * @param object $comment Comment data.
+ */
+ public function purge_post_on_comment_change( $newstatus, $oldstatus, $comment ) {
+
+ global $nginx_helper_admin, $blog_id;
+
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ $_post_id = $comment->comment_post_ID;
+ $_comment_id = $comment->comment_ID;
+
+ $this->log( '* * * * *' );
+ $this->log( '* Blog :: ' . addslashes( get_bloginfo( 'name' ) ) . ' ( ' . $blog_id . ' ). ' );
+ $this->log( '* Post :: ' . get_the_title( $_post_id ) . ' ( ' . $_post_id . ' ) ' );
+ $this->log( "* Comment :: $_comment_id." );
+ $this->log( "* Status Changed from $oldstatus to $newstatus" );
+
+ switch ( $newstatus ) {
+
+ case 'approved':
+ if ( 1 === (int) $nginx_helper_admin->options['purge_page_on_new_comment'] ) {
+
+ $this->log( '* Comment ( ' . $_comment_id . ' ) approved. Post ( ' . $_post_id . ' ) purging...' );
+ $this->log( '* * * * *' );
+ $this->purge_post( $_post_id );
+
+ }
+ break;
+
+ case 'spam':
+ case 'unapproved':
+ case 'trash':
+ if ( 'approved' === $oldstatus && 1 === (int) $nginx_helper_admin->options['purge_page_on_deleted_comment'] ) {
+
+ $this->log( '* Comment ( ' . $_comment_id . ' ) removed as ( ' . $newstatus . ' ). Post ( ' . $_post_id . ' ) purging...' );
+ $this->log( '* * * * *' );
+ $this->purge_post( $_post_id );
+
+ }
+ break;
+
+ }
+
+ }
+
+ /**
+ * Purge post cache.
+ *
+ * @param int $post_id Post id.
+ */
+ public function purge_post( $post_id ) {
+
+ global $nginx_helper_admin, $blog_id;
+
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ switch ( current_filter() ) {
+
+ case 'publish_post':
+ $this->log( '* * * * *' );
+ $this->log( '* Blog :: ' . addslashes( get_bloginfo( 'name' ) ) . ' ( ' . $blog_id . ' ).' );
+ $this->log( '* Post :: ' . get_the_title( $post_id ) . ' ( ' . $post_id . ' ).' );
+ $this->log( '* Post ( ' . $post_id . ' ) published or edited and its status is published' );
+ $this->log( '* * * * *' );
+ break;
+
+ case 'publish_page':
+ $this->log( '* * * * *' );
+ $this->log( '* Blog :: ' . addslashes( get_bloginfo( 'name' ) ) . ' ( ' . $blog_id . ' ).' );
+ $this->log( '* Page :: ' . get_the_title( $post_id ) . ' ( ' . $post_id . ' ).' );
+ $this->log( '* Page ( ' . $post_id . ' ) published or edited and its status is published' );
+ $this->log( '* * * * *' );
+ break;
+
+ case 'comment_post':
+ case 'wp_set_comment_status':
+ break;
+
+ default:
+ $_post_type = get_post_type( $post_id );
+ $this->log( '* * * * *' );
+ $this->log( '* Blog :: ' . addslashes( get_bloginfo( 'name' ) ) . ' ( ' . $blog_id . ' ).' );
+ $this->log( "* Custom post type '" . $_post_type . "' :: " . get_the_title( $post_id ) . ' ( ' . $post_id . ' ).' );
+ $this->log( "* CPT '" . $_post_type . "' ( " . $post_id . ' ) published or edited and its status is published' );
+ $this->log( '* * * * *' );
+ break;
+
+ }
+
+ $this->log( 'Function purge_post BEGIN ===' );
+
+ if ( 1 === (int) $nginx_helper_admin->options['purge_homepage_on_edit'] ) {
+ $this->_purge_homepage();
+ }
+
+ if ( 'comment_post' === current_filter() || 'wp_set_comment_status' === current_filter() ) {
+
+ $this->_purge_by_options(
+ $post_id,
+ $blog_id,
+ $nginx_helper_admin->options['purge_page_on_new_comment'],
+ $nginx_helper_admin->options['purge_archive_on_new_comment'],
+ $nginx_helper_admin->options['purge_archive_on_new_comment']
+ );
+
+ } else {
+
+ $this->_purge_by_options(
+ $post_id,
+ $blog_id,
+ $nginx_helper_admin->options['purge_page_on_mod'],
+ $nginx_helper_admin->options['purge_archive_on_edit'],
+ $nginx_helper_admin->options['purge_archive_on_edit']
+ );
+
+ }
+
+ $this->custom_purge_urls();
+
+ $this->log( 'Function purge_post END ^^^' );
+ }
+
+ /**
+ * Purge cache by options.
+ *
+ * @param int $post_id Post id.
+ * @param int $blog_id Blog id.
+ * @param bool $_purge_page Purge page or not.
+ * @param bool $_purge_archive Purge archive or not.
+ * @param bool $_purge_custom_taxa Purge taxonomy or not.
+ */
+ private function _purge_by_options( $post_id, $blog_id, $_purge_page, $_purge_archive, $_purge_custom_taxa ) {
+
+ $_post_type = get_post_type( $post_id );
+
+ if ( $_purge_page ) {
+
+ if ( 'post' === $_post_type || 'page' === $_post_type ) {
+ $this->log( 'Purging ' . $_post_type . ' ( id ' . $post_id . ', blog id ' . $blog_id . ' ) ' );
+ } else {
+ $this->log( "Purging custom post type '" . $_post_type . "' ( id " . $post_id . ', blog id ' . $blog_id . ' )' );
+ }
+
+ $post_status = get_post_status( $post_id );
+
+ if ( 'publish' !== $post_status ) {
+
+ if ( ! function_exists( 'get_sample_permalink' ) ) {
+ require_once ABSPATH . '/wp-admin/includes/post.php';
+ }
+
+ $url = get_sample_permalink( $post_id );
+
+ if ( ! empty( $url[0] ) && ! empty( $url[1] ) ) {
+ $url = str_replace( array('%postname%', '%pagename%'), $url[1], $url[0] );
+ } else {
+ $url = '';
+ }
+ } else {
+ $url = get_permalink( $post_id );
+ }
+
+ if ( empty( $url ) && ! is_array( $url ) ) {
+ return;
+ }
+
+ if ( 'trash' === get_post_status( $post_id ) ) {
+ $url = str_replace( '__trashed', '', $url );
+ }
+
+ $this->purge_url( $url );
+
+ }
+
+ if ( $_purge_archive ) {
+
+ $_post_type_archive_link = get_post_type_archive_link( $_post_type );
+
+ if ( function_exists( 'get_post_type_archive_link' ) && $_post_type_archive_link ) {
+
+ $this->log( 'Purging post type archive ( ' . $_post_type . ' )' );
+ $this->purge_url( $_post_type_archive_link );
+
+ }
+
+ $post_types = get_post_types( array( 'public' => true ) );
+
+ if ( in_array( $_post_type, $post_types, true ) ) {
+
+ $this->log( 'Purging date' );
+
+ $day = get_the_time( 'd', $post_id );
+ $month = get_the_time( 'm', $post_id );
+ $year = get_the_time( 'Y', $post_id );
+
+ if ( $year ) {
+
+ $this->purge_url( get_year_link( $year ) );
+
+ if ( $month ) {
+
+ $this->purge_url( get_month_link( $year, $month ) );
+
+ if ( $day ) {
+ $this->purge_url( get_day_link( $year, $month, $day ) );
+ }
+ }
+ }
+ }
+
+ $categories = wp_get_post_categories( $post_id );
+
+ if ( ! is_wp_error( $categories ) ) {
+
+ $this->log( 'Purging category archives' );
+
+ foreach ( $categories as $category_id ) {
+
+ $this->log( 'Purging category ' . $category_id );
+ $this->purge_url( get_category_link( $category_id ) );
+
+ }
+ }
+
+ $tags = get_the_tags( $post_id );
+
+ if ( ! is_wp_error( $tags ) && ! empty( $tags ) ) {
+
+ $this->log( 'Purging tag archives' );
+
+ foreach ( $tags as $tag ) {
+
+ $this->log( 'Purging tag ' . $tag->term_id );
+ $this->purge_url( get_tag_link( $tag->term_id ) );
+
+ }
+ }
+
+ $author_id = get_post( $post_id )->post_author;
+
+ if ( ! empty( $author_id ) ) {
+
+ $this->log( 'Purging author archive' );
+ $this->purge_url( get_author_posts_url( $author_id ) );
+
+ }
+ }
+
+ if ( $_purge_custom_taxa ) {
+
+ $custom_taxonomies = get_taxonomies(
+ array(
+ 'public' => true,
+ '_builtin' => false,
+ )
+ );
+
+ if ( ! empty( $custom_taxonomies ) ) {
+
+ $this->log( 'Purging custom taxonomies related' );
+
+ foreach ( $custom_taxonomies as $taxon ) {
+
+ if ( ! in_array( $taxon, array( 'category', 'post_tag', 'link_category' ), true ) ) {
+
+ $terms = get_the_terms( $post_id, $taxon );
+
+ if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
+
+ foreach ( $terms as $term ) {
+ $this->purge_url( get_term_link( $term, $taxon ) );
+ }
+ }
+ } else {
+ $this->log( "Your built-in taxonomy '" . $taxon . "' has param '_builtin' set to false.", 'WARNING' );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Deletes local cache files without a 3rd party nginx module.
+ * Does not require any other modules. Requires that the cache be stored on the same server as WordPress. You must also be using the default nginx cache options (levels=1:2) and (fastcgi_cache_key "$scheme$request_method$host$request_uri").
+ * Read more on how this works here:
+ * https://www.digitalocean.com/community/tutorials/how-to-setup-fastcgi-caching-with-nginx-on-your-vps#purging-the-cache
+ *
+ * @param string $url URL to purge.
+ *
+ * @return bool
+ */
+ protected function delete_cache_file_for( $url ) {
+
+ // Verify cache path is set.
+ if ( ! defined( 'RT_WP_NGINX_HELPER_CACHE_PATH' ) ) {
+
+ $this->log( 'Error purging because RT_WP_NGINX_HELPER_CACHE_PATH was not defined. URL: ' . $url, 'ERROR' );
+ return false;
+
+ }
+
+ // Verify URL is valid.
+ $url_data = wp_parse_url( $url );
+ if ( ! $url_data ) {
+
+ $this->log( 'Error purging because specified URL did not appear to be valid. URL: ' . $url, 'ERROR' );
+ return false;
+
+ }
+
+ // Build a hash of the URL.
+ $url_path = isset( $url_data['path'] ) ? $url_data['path'] : '';
+ $hash = md5( $url_data['scheme'] . 'GET' . $url_data['host'] . $url_path );
+
+ // Ensure trailing slash.
+ $cache_path = RT_WP_NGINX_HELPER_CACHE_PATH;
+ $cache_path = ( '/' === substr( $cache_path, -1 ) ) ? $cache_path : $cache_path . '/';
+
+ // Set path to cached file.
+ $cached_file = $cache_path . substr( $hash, -1 ) . '/' . substr( $hash, -3, 2 ) . '/' . $hash;
+
+ /**
+ * Filters the cached file name.
+ *
+ * @since 2.1.0
+ * @since 2.2.3 Purge URL argument `$url` were added.
+ *
+ * @param string $cached_file Cached file name.
+ * @param string $url URL to be purged.
+ */
+ $cached_file = apply_filters( 'rt_nginx_helper_purge_cached_file', $cached_file, $url );
+
+ // Verify cached file exists.
+ if ( ! file_exists( $cached_file ) ) {
+
+ $this->log( '- - ' . $url . ' is currently not cached ( checked for file: ' . $cached_file . ' )' );
+ return false;
+
+ }
+
+ // Delete the cached file.
+ if ( unlink( $cached_file ) ) {
+ $this->log( '- - ' . $url . ' *** PURGED ***' );
+
+ /**
+ * Fire an action after deleting file from cache.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ * @param string $cached_file Cached file name.
+ */
+ do_action( 'rt_nginx_helper_purged_file', $url, $cached_file );
+ } else {
+ $this->log( '- - An error occurred deleting the cache file. Check the server logs for a PHP warning.', 'ERROR' );
+ }
+
+ }
+
+ /**
+ * Remote get data from url.
+ *
+ * @param string $url URL to do remote request.
+ */
+ protected function do_remote_get( $url ) {
+ /**
+ * Filters the URL to be purged.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ */
+ $url = apply_filters( 'rt_nginx_helper_remote_purge_url', $url );
+
+ /**
+ * Fire an action before purging URL.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ */
+ do_action( 'rt_nginx_helper_before_remote_purge_url', $url );
+
+ $response = wp_remote_get( $url );
+
+ if ( is_wp_error( $response ) ) {
+
+ $_errors_str = implode( ' - ', $response->get_error_messages() );
+ $this->log( 'Error while purging URL. ' . $_errors_str, 'ERROR' );
+
+ } else {
+
+ if ( $response['response']['code'] ) {
+
+ switch ( $response['response']['code'] ) {
+
+ case 200:
+ $this->log( '- - ' . $url . ' *** PURGED ***' );
+ break;
+ case 404:
+ $this->log( '- - ' . $url . ' is currently not cached' );
+ break;
+ default:
+ $this->log( '- - ' . $url . ' not found ( ' . $response['response']['code'] . ' )', 'WARNING' );
+
+ }
+ }
+
+ /**
+ * Fire an action after remote purge request.
+ *
+ * @since 2.1.0
+ *
+ * @param string $url URL to be purged.
+ * @param array $response Array of results including HTTP headers.
+ */
+ do_action( 'rt_nginx_helper_after_remote_purge_url', $url, $response );
+ }
+
+ }
+
+ /**
+ * Check http connection.
+ *
+ * @return string
+ */
+ public function check_http_connection() {
+
+ $purge_url = plugins_url( 'nginx-manager/check-proxy.php' );
+ $response = wp_remote_get( $purge_url );
+
+ if ( ! is_wp_error( $response ) && ( 'HTTP Connection OK' === $response['body'] ) ) {
+ return 'OK';
+ }
+
+ return 'KO';
+
+ }
+
+ /**
+ * Log file.
+ *
+ * @param string $msg Message to log.
+ * @param string $level Level.
+ *
+ * @return bool|void
+ */
+ public function log( $msg, $level = 'INFO' ) {
+
+ global $nginx_helper_admin;
+
+ if ( ! $nginx_helper_admin->options['enable_log'] ) {
+ return;
+ }
+
+ $log_levels = array(
+ 'INFO' => 0,
+ 'WARNING' => 1,
+ 'ERROR' => 2,
+ 'NONE' => 3,
+ );
+
+ if ( $log_levels[ $level ] >= $log_levels[ $nginx_helper_admin->options['log_level'] ] ) {
+
+ $fp = fopen( $nginx_helper_admin->functional_asset_path() . 'nginx.log', 'a+' );
+ if ( $fp ) {
+
+ fwrite( $fp, "\n" . gmdate( 'Y-m-d H:i:s ' ) . ' | ' . $level . ' | ' . $msg );
+ fclose( $fp );
+
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Check and truncate log file.
+ */
+ public function check_and_truncate_log_file() {
+
+ global $nginx_helper_admin;
+
+ if ( ! $nginx_helper_admin->options['enable_log'] ) {
+ return;
+ }
+
+ $nginx_asset_path = $nginx_helper_admin->functional_asset_path() . 'nginx.log';
+
+ if ( ! file_exists( $nginx_asset_path ) ) {
+ return;
+ }
+
+ $max_size_allowed = ( is_numeric( $nginx_helper_admin->options['log_filesize'] ) ) ? $nginx_helper_admin->options['log_filesize'] * 1048576 : 5242880;
+
+ $file_size = filesize( $nginx_asset_path );
+
+ if ( $file_size > $max_size_allowed ) {
+
+ $offset = $file_size - $max_size_allowed;
+ $file_content = file_get_contents( $nginx_asset_path, null, null, $offset );
+ $file_content = empty( $file_content ) ? '' : strstr( $file_content, "\n" );
+
+ $fp = fopen( $nginx_asset_path, 'w+' );
+ if ( $file_content && $fp ) {
+
+ fwrite( $fp, $file_content );
+ fclose( $fp );
+ }
+ }
+ }
+
+ /**
+ * Purge image on edit.
+ *
+ * @param int $attachment_id Attachment id.
+ */
+ public function purge_image_on_edit( $attachment_id ) {
+
+ global $nginx_helper_admin;
+
+ // Do not purge if not enabled.
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ $this->log( 'Purging media on edit BEGIN ===' );
+
+ if ( wp_attachment_is_image( $attachment_id ) ) {
+
+ $this->purge_url( wp_get_attachment_url( $attachment_id ), false );
+ $attachment = wp_get_attachment_metadata( $attachment_id );
+
+ if ( ! empty( $attachment['sizes'] ) && is_array( $attachment['sizes'] ) ) {
+
+ foreach ( array_keys( $attachment['sizes'] ) as $size_name ) {
+
+ $resize_image = wp_get_attachment_image_src( $attachment_id, $size_name );
+
+ if ( $resize_image ) {
+ $this->purge_url( $resize_image[0], false );
+ }
+ }
+ }
+
+ $this->purge_url( get_attachment_link( $attachment_id ) );
+
+ } else {
+ $this->log( 'Media ( id ' . $attachment_id . ') edited: no image', 'WARNING' );
+ }
+
+ $this->log( 'Purging media on edit END ^^^' );
+
+ }
+
+ /**
+ * Purge cache on post moved to trash.
+ *
+ * @param string $new_status New post status.
+ * @param string $old_status Old post status.
+ * @param WP_Post $post Post object.
+ *
+ * @return bool|void
+ */
+ public function purge_on_post_moved_to_trash( $new_status, $old_status, $post ) {
+
+ global $nginx_helper_admin, $blog_id;
+
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ if ( 'trash' === $new_status ) {
+
+ $this->log( '# # # # #' );
+ $this->log( "# Post '$post->post_title' ( id " . $post->ID . ' ) moved to the trash.' );
+ $this->log( '# # # # #' );
+ $this->log( 'Function purge_on_post_moved_to_trash ( post id ' . $post->ID . ' ) BEGIN ===' );
+
+ if ( 1 === (int) $nginx_helper_admin->options['purge_homepage_on_del'] ) {
+ $this->_purge_homepage();
+ }
+
+ $this->_purge_by_options(
+ $post->ID,
+ $blog_id,
+ true,
+ $nginx_helper_admin->options['purge_archive_on_del'],
+ $nginx_helper_admin->options['purge_archive_on_del']
+ );
+
+ $this->log( 'Function purge_on_post_moved_to_trash ( post id ' . $post->ID . ' ) END ===' );
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Purge cache of homepage.
+ *
+ * @return bool
+ */
+ private function _purge_homepage() {
+
+ // WPML installetd?.
+ if ( function_exists( 'icl_get_home_url' ) ) {
+
+ $homepage_url = trailingslashit( icl_get_home_url() );
+ $this->log( sprintf( __( 'Purging homepage (WPML) ', 'nginx-helper' ) . '%s', $homepage_url ) );
+
+ } else {
+
+ $homepage_url = trailingslashit( home_url() );
+ $this->log( sprintf( __( 'Purging homepage ', 'nginx-helper' ) . '%s', $homepage_url ) );
+
+ }
+
+ $this->purge_url( $homepage_url );
+
+ return true;
+
+ }
+
+ /**
+ * Purge personal urls.
+ *
+ * @return bool
+ */
+ private function _purge_personal_urls() {
+
+ global $nginx_helper_admin;
+
+ $this->log( __( 'Purging personal urls', 'nginx-helper' ) );
+
+ if ( isset( $nginx_helper_admin->options['purgeable_url']['urls'] ) ) {
+
+ foreach ( $nginx_helper_admin->options['purgeable_url']['urls'] as $url ) {
+ $this->purge_url( $url, false );
+ }
+ } else {
+ $this->log( '- ' . __( 'No personal urls available', 'nginx-helper' ) );
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Purge post categories.
+ *
+ * @param int $_post_id Post id.
+ *
+ * @return bool
+ */
+ private function _purge_post_categories( $_post_id ) {
+
+ $this->log( __( 'Purging category archives', 'nginx-helper' ) );
+
+ $categories = wp_get_post_categories( $_post_id );
+
+ if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) {
+
+ foreach ( $categories as $category_id ) {
+
+ // translators: %d: Category ID.
+ $this->log( sprintf( __( "Purging category '%d'", 'nginx-helper' ), $category_id ) );
+ $this->purge_url( get_category_link( $category_id ) );
+
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Purge post tags.
+ *
+ * @param int $_post_id Post id.
+ *
+ * @return bool
+ */
+ private function _purge_post_tags( $_post_id ) {
+
+ $this->log( __( 'Purging tags archives', 'nginx-helper' ) );
+
+ $tags = get_the_tags( $_post_id );
+
+ if ( ! is_wp_error( $tags ) && ! empty( $tags ) ) {
+
+ foreach ( $tags as $tag ) {
+
+ $this->log( sprintf( __( "Purging tag '%1\$s' ( id %2\$d )", 'nginx-helper' ), $tag->name, $tag->term_id ) );
+ $this->purge_url( get_tag_link( $tag->term_id ) );
+
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Purge post custom taxonomy data.
+ *
+ * @param int $_post_id Post id.
+ *
+ * @return bool
+ */
+ private function _purge_post_custom_taxa( $_post_id ) {
+
+ $this->log( __( 'Purging post custom taxonomies related', 'nginx-helper' ) );
+
+ $custom_taxonomies = get_taxonomies(
+ array(
+ 'public' => true,
+ '_builtin' => false,
+ )
+ );
+
+ if ( ! empty( $custom_taxonomies ) ) {
+
+ foreach ( $custom_taxonomies as $taxon ) {
+
+ // translators: %s: Post taxonomy name.
+ $this->log( sprintf( '+ ' . __( "Purging custom taxonomy '%s'", 'nginx-helper' ), $taxon ) );
+
+ if ( ! in_array( $taxon, array( 'category', 'post_tag', 'link_category' ), true ) ) {
+
+ $terms = get_the_terms( $_post_id, $taxon );
+
+ if ( ! is_wp_error( $terms ) && ! empty( $terms ) && is_array( $terms ) ) {
+
+ foreach ( $terms as $term ) {
+ $this->purge_url( get_term_link( $term, $taxon ) );
+ }
+ }
+ } else {
+ // translators: %s: Post taxonomy name.
+ $this->log( sprintf( '- ' . __( "Your built-in taxonomy '%s' has param '_builtin' set to false.", 'nginx-helper' ), $taxon ), 'WARNING' );
+ }
+ }
+ } else {
+ $this->log( '- ' . __( 'No custom taxonomies', 'nginx-helper' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Purge all categories.
+ *
+ * @return bool
+ */
+ private function _purge_all_categories() {
+
+ $this->log( __( 'Purging all categories', 'nginx-helper' ) );
+
+ $_categories = get_categories();
+
+ if ( ! empty( $_categories ) ) {
+
+ foreach ( $_categories as $c ) {
+
+ $this->log( sprintf( __( "Purging category '%1\$s' ( id %2\$d )", 'nginx-helper' ), $c->name, $c->term_id ) );
+ $this->purge_url( get_category_link( $c->term_id ) );
+
+ }
+ } else {
+
+ $this->log( __( 'No categories archives', 'nginx-helper' ) );
+
+ }
+
+ return true;
+ }
+
+ /**
+ * Purge all posttags cache.
+ *
+ * @return bool
+ */
+ private function _purge_all_posttags() {
+
+ $this->log( __( 'Purging all tags', 'nginx-helper' ) );
+
+ $_posttags = get_tags();
+
+ if ( ! empty( $_posttags ) ) {
+
+ foreach ( $_posttags as $t ) {
+
+ $this->log( sprintf( __( "Purging tag '%1\$s' ( id %2\$d )", 'nginx-helper' ), $t->name, $t->term_id ) );
+ $this->purge_url( get_tag_link( $t->term_id ) );
+
+ }
+ } else {
+ $this->log( __( 'No tags archives', 'nginx-helper' ) );
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Purge all custom taxonomy data.
+ *
+ * @return bool
+ */
+ private function _purge_all_customtaxa() {
+
+ $this->log( __( 'Purging all custom taxonomies', 'nginx-helper' ) );
+
+ $custom_taxonomies = get_taxonomies(
+ array(
+ 'public' => true,
+ '_builtin' => false,
+ )
+ );
+
+ if ( ! empty( $custom_taxonomies ) ) {
+
+ foreach ( $custom_taxonomies as $taxon ) {
+
+ // translators: %s: Taxonomy name.
+ $this->log( sprintf( '+ ' . __( "Purging custom taxonomy '%s'", 'nginx-helper' ), $taxon ) );
+
+ if ( ! in_array( $taxon, array( 'category', 'post_tag', 'link_category' ), true ) ) {
+
+ $terms = get_terms( $taxon );
+
+ if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
+
+ foreach ( $terms as $term ) {
+
+ $this->purge_url( get_term_link( $term, $taxon ) );
+
+ }
+ }
+ } else {
+ // translators: %s: Taxonomy name.
+ $this->log( sprintf( '- ' . esc_html__( "Your built-in taxonomy '%s' has param '_builtin' set to false.", 'nginx-helper' ), $taxon ), 'WARNING' );
+ }
+ }
+ } else {
+ $this->log( '- ' . __( 'No custom taxonomies', 'nginx-helper' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Purge all taxonomies.
+ *
+ * @return bool
+ */
+ private function _purge_all_taxonomies() {
+
+ $this->_purge_all_categories();
+ $this->_purge_all_posttags();
+ $this->_purge_all_customtaxa();
+
+ return true;
+ }
+
+ /**
+ * Purge all posts cache.
+ *
+ * @return bool
+ */
+ private function _purge_all_posts() {
+
+ $this->log( __( 'Purging all posts, pages and custom post types.', 'nginx-helper' ) );
+
+ $args = array(
+ 'posts_per_page' => 0,
+ 'post_type' => 'any',
+ 'post_status' => 'publish',
+ );
+
+ $get_posts = new WP_Query();
+ $_posts = $get_posts->query( $args );
+
+ if ( ! empty( $_posts ) ) {
+
+ foreach ( $_posts as $p ) {
+
+ $this->log( sprintf( '+ ' . __( "Purging post id '%1\$d' ( post type '%2\$s' )", 'nginx-helper' ), $p->ID, $p->post_type ) );
+ $this->purge_url( get_permalink( $p->ID ) );
+
+ }
+ } else {
+ $this->log( '- ' . __( 'No posts', 'nginx-helper' ) );
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Purge all archives.
+ *
+ * @return bool
+ */
+ private function _purge_all_date_archives() {
+
+ $this->log( __( 'Purging all date-based archives.', 'nginx-helper' ) );
+
+ $this->_purge_all_daily_archives();
+ $this->_purge_all_monthly_archives();
+ $this->_purge_all_yearly_archives();
+
+ return true;
+
+ }
+
+ /**
+ * Purge daily archives cache.
+ */
+ private function _purge_all_daily_archives() {
+
+ global $wpdb;
+
+ $this->log( __( 'Purging all daily archives.', 'nginx-helper' ) );
+
+ $_query_daily_archives = $wpdb->prepare(
+ "SELECT YEAR(post_date) AS %s, MONTH(post_date) AS %s, DAYOFMONTH(post_date) AS %s, count(ID) as posts
+ FROM $wpdb->posts
+ WHERE post_type = %s AND post_status = %s
+ GROUP BY YEAR(post_date), MONTH(post_date), DAYOFMONTH(post_date)
+ ORDER BY post_date DESC",
+ 'year',
+ 'month',
+ 'dayofmonth',
+ 'post',
+ 'publish'
+ );
+
+ $_daily_archives = $wpdb->get_results( $_query_daily_archives ); // phpcs:ignore
+
+ if ( ! empty( $_daily_archives ) ) {
+
+ foreach ( $_daily_archives as $_da ) {
+
+ $this->log(
+ sprintf(
+ '+ ' . __( "Purging daily archive '%1\$s/%2\$s/%3\$s'", 'nginx-helper' ),
+ $_da->year,
+ $_da->month,
+ $_da->dayofmonth
+ )
+ );
+
+ $this->purge_url( get_day_link( $_da->year, $_da->month, $_da->dayofmonth ) );
+
+ }
+ } else {
+ $this->log( '- ' . __( 'No daily archives', 'nginx-helper' ) );
+ }
+
+ }
+
+ /**
+ * Purge all monthly archives.
+ */
+ private function _purge_all_monthly_archives() {
+
+ global $wpdb;
+
+ $this->log( __( 'Purging all monthly archives.', 'nginx-helper' ) );
+
+ $_monthly_archives = wp_cache_get( 'nginx_helper_monthly_archives', 'nginx_helper' );
+
+ if ( empty( $_monthly_archives ) ) {
+
+ $_query_monthly_archives = $wpdb->prepare(
+ "SELECT YEAR(post_date) AS %s, MONTH(post_date) AS %s, count(ID) as posts
+ FROM $wpdb->posts
+ WHERE post_type = %s AND post_status = %s
+ GROUP BY YEAR(post_date), MONTH(post_date)
+ ORDER BY post_date DESC",
+ 'year',
+ 'month',
+ 'post',
+ 'publish'
+ );
+
+ $_monthly_archives = $wpdb->get_results( $_query_monthly_archives ); // phpcs:ignore
+
+ wp_cache_set( 'nginx_helper_monthly_archives', $_monthly_archives, 'nginx_helper', 24 * 60 * 60 );
+
+ }
+
+ if ( ! empty( $_monthly_archives ) ) {
+
+ foreach ( $_monthly_archives as $_ma ) {
+
+ $this->log( sprintf( '+ ' . __( "Purging monthly archive '%1\$s/%2\$s'", 'nginx-helper' ), $_ma->year, $_ma->month ) );
+ $this->purge_url( get_month_link( $_ma->year, $_ma->month ) );
+
+ }
+ } else {
+ $this->log( '- ' . __( 'No monthly archives', 'nginx-helper' ) );
+ }
+
+ }
+
+ /**
+ * Purge all yearly archive cache.
+ */
+ private function _purge_all_yearly_archives() {
+
+ global $wpdb;
+
+ $this->log( __( 'Purging all yearly archives.', 'nginx-helper' ) );
+
+ $_yearly_archives = wp_cache_get( 'nginx_helper_yearly_archives', 'nginx_helper' );
+
+ if ( empty( $_yearly_archives ) ) {
+
+ $_query_yearly_archives = $wpdb->prepare(
+ "SELECT YEAR(post_date) AS %s, count(ID) as posts
+ FROM $wpdb->posts
+ WHERE post_type = %s AND post_status = %s
+ GROUP BY YEAR(post_date)
+ ORDER BY post_date DESC",
+ 'year',
+ 'post',
+ 'publish'
+ );
+
+ $_yearly_archives = $wpdb->get_results( $_query_yearly_archives ); // phpcs:ignore
+
+ wp_cache_set( 'nginx_helper_yearly_archives', $_yearly_archives, 'nginx_helper', 24 * 60 * 60 );
+
+ }
+
+ if ( ! empty( $_yearly_archives ) ) {
+
+ foreach ( $_yearly_archives as $_ya ) {
+
+ // translators: %s: Year to purge cache.
+ $this->log( sprintf( '+ ' . esc_html__( "Purging yearly archive '%s'", 'nginx-helper' ), $_ya->year ) );
+ $this->purge_url( get_year_link( $_ya->year ) );
+
+ }
+ } else {
+ $this->log( '- ' . __( 'No yearly archives', 'nginx-helper' ) );
+ }
+
+ }
+
+ /**
+ * Purge all cache.
+ *
+ * @return bool
+ */
+ public function purge_them_all() {
+
+ $this->log( __( "Let's purge everything!", 'nginx-helper' ) );
+ $this->_purge_homepage();
+ $this->_purge_personal_urls();
+ $this->_purge_all_posts();
+ $this->_purge_all_taxonomies();
+ $this->_purge_all_date_archives();
+ $this->log( __( 'Everything purged!', 'nginx-helper' ) );
+
+ return true;
+
+ }
+
+ /**
+ * Purge cache on term edited.
+ *
+ * @param int $term_id Term id.
+ * @param int $tt_id Taxonomy id.
+ * @param string $taxon Taxonomy.
+ *
+ * @return bool
+ */
+ public function purge_on_term_taxonomy_edited( $term_id, $tt_id, $taxon ) {
+
+ global $nginx_helper_admin;
+
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ $this->log( __( 'Term taxonomy edited or deleted', 'nginx-helper' ) );
+
+ $term = get_term( $term_id, $taxon );
+ $current_filter = current_filter();
+
+ if ( 'edit_term' === $current_filter && ! is_wp_error( $term ) && ! empty( $term ) ) {
+
+ $this->log( sprintf( __( "Term taxonomy '%1\$s' edited, (tt_id '%2\$d', term_id '%3\$d', taxonomy '%4\$s')", 'nginx-helper' ), $term->name, $tt_id, $term_id, $taxon ) );
+
+ } elseif ( 'delete_term' === $current_filter ) {
+
+ $this->log( sprintf( __( "A term taxonomy has been deleted from taxonomy '%1\$s', (tt_id '%2\$d', term_id '%3\$d')", 'nginx-helper' ), $taxon, $term_id, $tt_id ) );
+
+ }
+
+ $this->_purge_homepage();
+
+ return true;
+
+ }
+
+ /**
+ * Check ajax referrer on purge.
+ *
+ * @param string $action The Ajax nonce action.
+ *
+ * @return bool
+ */
+ public function purge_on_check_ajax_referer( $action ) {
+
+ global $nginx_helper_admin;
+
+ if ( ! $nginx_helper_admin->options['enable_purge'] ) {
+ return;
+ }
+
+ switch ( $action ) {
+
+ case 'save-sidebar-widgets':
+ $this->log( __( 'Widget saved, moved or removed in a sidebar', 'nginx-helper' ) );
+ $this->_purge_homepage();
+ break;
+
+ default:
+ break;
+
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Unlink file recursively.
+ * Source - http://stackoverflow.com/a/1360437/156336
+ *
+ * @param string $dir Directory.
+ * @param bool $delete_root_too Delete root or not.
+ *
+ * @return void
+ */
+ public function unlink_recursive( $dir, $delete_root_too ) {
+
+ if ( ! is_dir( $dir ) ) {
+ return;
+ }
+
+ $dh = opendir( $dir );
+
+ if ( ! $dh ) {
+ return;
+ }
+
+ // phpcs:ignore -- WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Variable assignment required for recursion.
+ while ( false !== ( $obj = readdir( $dh ) ) ) {
+
+ if ( '.' === $obj || '..' === $obj ) {
+ continue;
+ }
+
+ if ( ! @unlink( $dir . '/' . $obj ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ $this->unlink_recursive( $dir . '/' . $obj, false );
+ }
+ }
+
+ if ( $delete_root_too ) {
+ rmdir( $dir );
+ }
+
+ closedir( $dh );
+ }
+
+}
diff --git a/wp/wp-content/plugins/nginx-helper/admin/css/nginx-helper-admin.css b/wp/wp-content/plugins/nginx-helper/admin/css/nginx-helper-admin.css
new file mode 100644
index 00000000..8d1b4b30
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/css/nginx-helper-admin.css
@@ -0,0 +1,100 @@
+/**
+ * All of the CSS for your admin-specific functionality should be
+ * included in this file.
+ */
+
+.clearfix {
+ *zoom: 1;
+}
+.clearfix:before,
+.clearfix:after {
+ content: " ";
+ display: table;
+}
+.clearfix:after {
+ clear: both;
+}
+h4 {
+ margin: 0;
+}
+.form-table th,
+.form-wrap label {
+ vertical-align: middle;
+}
+table.rtnginx-table {
+ border-bottom: 1px solid #EEE;
+}
+table.rtnginx-table:last-child {
+ border-bottom: 0;
+}
+.rtnginx-table p.error {
+ color: red;
+}
+pre#map {
+ background: #e5e5e5 none;
+ border-radius: 10px;
+ padding: 10px;
+}
+.wrap h2.rt_option_title {
+ background: url(../icons/nginx-icon-32x32.png) 0 6px no-repeat rgba(0, 0, 0, 0);
+ padding-left: 40px;
+}
+#poststuff h2 {
+ padding: 0 0 0 10px;
+ margin-top: 0;
+}
+form#purgeall .button-primary {
+ margin-bottom: 20px;
+ box-shadow: inset 0 -2px rgba(0, 0, 0, 0.14);
+ padding: 15px 30px;
+ font-size: 1rem;
+ border: 0;
+ border-radius: 5px;
+ color: #FFF;
+ background: #DD3D36;
+ height: auto;
+}
+form#purgeall .button-primary:hover,
+form#purgeall .button-primary:focus {
+ background: #d52c24;
+}
+.nh-aligncenter {
+ display: block;
+ text-align: center;
+ line-height: 2;
+}
+#latest_news .inside ul,
+#useful-links .inside ul {
+ margin: 0 0 0 12px;
+}
+#latest_news .inside ul li,
+#useful-links .inside ul li {
+ list-style: square;
+ padding: 0 0 7px;
+}
+#social .inside a {
+ background-color: #666;
+ color: #FFF;
+ display: inline-block;
+ height: 30px;
+ font-size: 1.25rem;
+ line-height: 30px;
+ margin: 10px 20px 0 0;
+ overflow: hidden;
+ padding: 0;
+ text-align: center;
+ text-decoration: none;
+ width: 30px;
+ -webkit-border-radius: 1000px;
+ -moz-border-radius: 1000px;
+ border-radius: 1000px;
+}
+
+#social .inside .nginx-helper-facebook:hover {
+ background-color: #537BBD;
+}
+#social .inside .nginx-helper-twitter:hover {
+ background-color: #40BFF5;
+}
+
+.rt-purge_url { width: 100%; }
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/config.json b/wp/wp-content/plugins/nginx-helper/admin/icons/config.json
new file mode 100644
index 00000000..e0ce214e
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/icons/config.json
@@ -0,0 +1,34 @@
+{
+ "name": "nginx-fontello",
+ "css_prefix_text": "nginx-helper-",
+ "css_use_suffix": false,
+ "hinting": true,
+ "units_per_em": 1000,
+ "ascent": 850,
+ "glyphs": [
+ {
+ "uid": "72b1277834cba5b7944b0a6cac7ddb0d",
+ "css": "rss",
+ "code": 59395,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "627abcdb627cb1789e009c08e2678ef9",
+ "css": "twitter",
+ "code": 59394,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "bc50457410acf467b8b5721240768742",
+ "css": "facebook",
+ "code": 59393,
+ "src": "entypo"
+ },
+ {
+ "uid": "b945f4ac2439565661e8e4878e35d379",
+ "css": "gplus",
+ "code": 59392,
+ "src": "entypo"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/css/nginx-fontello.css b/wp/wp-content/plugins/nginx-helper/admin/icons/css/nginx-fontello.css
new file mode 100644
index 00000000..8931116f
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/icons/css/nginx-fontello.css
@@ -0,0 +1,54 @@
+@font-face {
+ font-family: 'nginx-fontello';
+ src: url('../font/nginx-fontello.eot?7388141');
+ src: url('../font/nginx-fontello.eot?7388141#iefix') format('embedded-opentype'),
+ url('../font/nginx-fontello.woff?7388141') format('woff'),
+ url('../font/nginx-fontello.ttf?7388141') format('truetype'),
+ url('../font/nginx-fontello.svg?7388141#nginx-fontello') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'nginx-fontello';
+ src: url('../font/nginx-fontello.svg?7388141#nginx-fontello') format('svg');
+ }
+}
+*/
+
+[class^="nginx-helper-"]:before, [class*=" nginx-helper-"]:before {
+ font-family: "nginx-fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+
+.nginx-helper-twitter:before { content: '\e802'; } /* '' */
+.nginx-helper-facebook:before { content: '\e801'; } /* '' */
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.eot b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.eot
new file mode 100644
index 00000000..e403815b
Binary files /dev/null and b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.eot differ
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.svg b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.svg
new file mode 100644
index 00000000..a531c1a3
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.svg
@@ -0,0 +1,15 @@
+
+
+
\ No newline at end of file
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.ttf b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.ttf
new file mode 100644
index 00000000..bde67b19
Binary files /dev/null and b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.ttf differ
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.woff b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.woff
new file mode 100644
index 00000000..95107770
Binary files /dev/null and b/wp/wp-content/plugins/nginx-helper/admin/icons/font/nginx-fontello.woff differ
diff --git a/wp/wp-content/plugins/nginx-helper/admin/icons/nginx-icon-32x32.png b/wp/wp-content/plugins/nginx-helper/admin/icons/nginx-icon-32x32.png
new file mode 100644
index 00000000..d3e4b12e
Binary files /dev/null and b/wp/wp-content/plugins/nginx-helper/admin/icons/nginx-icon-32x32.png differ
diff --git a/wp/wp-content/plugins/nginx-helper/admin/index.php b/wp/wp-content/plugins/nginx-helper/admin/index.php
new file mode 100644
index 00000000..f0dd56b9
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/index.php
@@ -0,0 +1,8 @@
+ 0 ) {
+
+ var args = {
+ 'action': 'rt_get_feeds'
+ };
+
+ jQuery.get(
+ ajaxurl,
+ args,
+ function( data ) {
+ /**
+ * Received markup is safe and escaped appropriately.
+ *
+ * File: admin/class-nginx-helper-admin.php
+ * Method: nginx_helper_get_feeds();
+ */
+
+ // phpcs:ignore -- WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
+ news_section.find( '.inside' ).empty().append( data );
+ }
+ );
+
+ }
+
+ jQuery( "form#purgeall a" ).click(
+ function (e) {
+
+ if ( confirm( nginx_helper.purge_confirm_string ) === true ) {
+ // Continue submitting form.
+ } else {
+ e.preventDefault();
+ }
+
+ }
+ );
+
+ /**
+ * Show OR Hide options on option checkbox
+ *
+ * @param {type} selector Selector of Checkbox and PostBox
+ */
+ function nginx_show_option( selector ) {
+
+ jQuery( '#' + selector ).on(
+ 'change',
+ function () {
+
+ if ( jQuery( this ).is( ':checked' ) ) {
+
+ jQuery( '.' + selector ).show();
+
+ if ( 'cache_method_redis' === selector ) {
+ jQuery( '.cache_method_fastcgi' ).hide();
+ } else if ( selector === 'cache_method_fastcgi' ) {
+ jQuery( '.cache_method_redis' ).hide();
+ }
+
+ } else {
+ jQuery( '.' + selector ).hide();
+ }
+
+ }
+ );
+
+ }
+
+ /* Function call with parameter */
+ nginx_show_option( 'cache_method_fastcgi' );
+ nginx_show_option( 'cache_method_redis' );
+ nginx_show_option( 'enable_map' );
+ nginx_show_option( 'enable_log' );
+ nginx_show_option( 'enable_purge' );
+
+ }
+ );
+})( jQuery );
diff --git a/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-admin-display.php b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-admin-display.php
new file mode 100644
index 00000000..bd153714
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-admin-display.php
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+ ';
+ foreach ( $this->settings_tabs as $setting_tab => $setting_name ) {
+
+ $class = ( $setting_tab === $current_setting_tab ) ? ' nav-tab-active' : '';
+ printf(
+ '
%s',
+ esc_attr( 'nav-tab' . $class ),
+ esc_url( '?page=nginx&tab=' . $setting_name['menu_slug'] ),
+ esc_html( $setting_name['menu_title'] )
+ );
+ }
+ echo '';
+
+ switch ( $current_setting_tab ) {
+
+ case 'general':
+ include plugin_dir_path( __FILE__ ) . 'nginx-helper-general-options.php';
+ break;
+ case 'support':
+ include plugin_dir_path( __FILE__ ) . 'nginx-helper-support-options.php';
+ break;
+
+ }
+ ?>
+
+
+
+
+
+
+
diff --git a/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-general-options.php b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-general-options.php
new file mode 100644
index 00000000..87f2e32d
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-general-options.php
@@ -0,0 +1,733 @@
+nginx_helper_default_settings()
+ );
+
+ if ( ( ! is_numeric( $nginx_settings['log_filesize'] ) ) || ( empty( $nginx_settings['log_filesize'] ) ) ) {
+ $error_log_filesize = __( 'Log file size must be a number.', 'nginx-helper' );
+ unset( $nginx_settings['log_filesize'] );
+ }
+
+ if ( $nginx_settings['enable_map'] ) {
+ $nginx_helper_admin->update_map();
+ }
+
+ update_site_option( 'rt_wp_nginx_helper_options', $nginx_settings );
+
+ echo '' . esc_html__( 'Settings saved.', 'nginx-helper' ) . '
';
+
+}
+
+$nginx_helper_settings = $nginx_helper_admin->nginx_helper_settings();
+$log_path = $nginx_helper_admin->functional_asset_path();
+$log_url = $nginx_helper_admin->functional_asset_url();
+
+/**
+ * Get setting url for single multiple with subdomain OR multiple with subdirectory site.
+ */
+$nginx_setting_link = '#';
+if ( is_multisite() ) {
+ if ( SUBDOMAIN_INSTALL === false ) {
+ $nginx_setting_link = 'https://easyengine.io/wordpress-nginx/tutorials/multisite/subdirectories/fastcgi-cache-with-purging/';
+ } else {
+ $nginx_setting_link = 'https://easyengine.io/wordpress-nginx/tutorials/multisite/subdomains/fastcgi-cache-with-purging/';
+ }
+} else {
+ $nginx_setting_link = 'https://easyengine.io/wordpress-nginx/tutorials/single-site/fastcgi-cache-with-purging/';
+}
+?>
+
+
+
diff --git a/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-sidebar-display.php b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-sidebar-display.php
new file mode 100644
index 00000000..50021eba
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-sidebar-display.php
@@ -0,0 +1,88 @@
+ 'purge',
+ 'nginx_helper_urls' => 'all',
+ )
+);
+$nonced_url = wp_nonce_url( $purge_url, 'nginx_helper-purge_all' );
+?>
+
+
+
+
+
+
+
+
+
+
+ %s.',
+ esc_html__( 'Please use our', 'nginx-helper' ),
+ esc_url( 'http://rtcamp.com/support/forum/wordpress-nginx/' ),
+ esc_html__( 'free support forum', 'nginx-helper' )
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
diff --git a/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-support-options.php b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-support-options.php
new file mode 100644
index 00000000..178b05a7
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/partials/nginx-helper-support-options.php
@@ -0,0 +1,44 @@
+
+
+
+
diff --git a/wp/wp-content/plugins/nginx-helper/admin/predis.php b/wp/wp-content/plugins/nginx-helper/admin/predis.php
new file mode 100644
index 00000000..9a0f6e7c
--- /dev/null
+++ b/wp/wp-content/plugins/nginx-helper/admin/predis.php
@@ -0,0 +1,15203 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+use InvalidArgumentException;
+
+/**
+ * Defines an abstraction representing a Redis command.
+ *
+ * @author Daniele Alessandri
+ */
+interface CommandInterface
+{
+ /**
+ * Returns the ID of the Redis command. By convention, command identifiers
+ * must always be uppercase.
+ *
+ * @return string
+ */
+ public function getId();
+
+ /**
+ * Assign the specified slot to the command for clustering distribution.
+ *
+ * @param int $slot Slot ID.
+ */
+ public function setSlot($slot);
+
+ /**
+ * Returns the assigned slot of the command for clustering distribution.
+ *
+ * @return int|null
+ */
+ public function getSlot();
+
+ /**
+ * Sets the arguments for the command.
+ *
+ * @param array $arguments List of arguments.
+ */
+ public function setArguments(array $arguments);
+
+ /**
+ * Sets the raw arguments for the command without processing them.
+ *
+ * @param array $arguments List of arguments.
+ */
+ public function setRawArguments(array $arguments);
+
+ /**
+ * Gets the arguments of the command.
+ *
+ * @return array
+ */
+ public function getArguments();
+
+ /**
+ * Gets the argument of the command at the specified index.
+ *
+ * @param int $index Index of the desired argument.
+ *
+ * @return mixed|null
+ */
+ public function getArgument($index);
+
+ /**
+ * Parses a raw response and returns a PHP object.
+ *
+ * @param string $data Binary string containing the whole response.
+ *
+ * @return mixed
+ */
+ public function parseResponse($data);
+}
+
+/**
+ * Base class for Redis commands.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class Command implements CommandInterface
+{
+ private $slot;
+ private $arguments = array();
+
+ /**
+ * Returns a filtered array of the arguments.
+ *
+ * @param array $arguments List of arguments.
+ *
+ * @return array
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return $arguments;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $this->filterArguments($arguments);
+ unset($this->slot);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRawArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+ unset($this->slot);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getArgument($index)
+ {
+ if (isset($this->arguments[$index])) {
+ return $this->arguments[$index];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSlot($slot)
+ {
+ $this->slot = $slot;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlot()
+ {
+ if (isset($this->slot)) {
+ return $this->slot;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return $data;
+ }
+
+ /**
+ * Normalizes the arguments array passed to a Redis command.
+ *
+ * @param array $arguments Arguments for a command.
+ *
+ * @return array
+ */
+ public static function normalizeArguments(array $arguments)
+ {
+ if (count($arguments) === 1 && is_array($arguments[0])) {
+ return $arguments[0];
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Normalizes the arguments array passed to a variadic Redis command.
+ *
+ * @param array $arguments Arguments for a command.
+ *
+ * @return array
+ */
+ public static function normalizeVariadic(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[1])) {
+ return array_merge(array($arguments[0]), $arguments[1]);
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrange
+ * @author Daniele Alessandri
+ */
+class ZSetRange extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZRANGE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 4) {
+ $lastType = gettype($arguments[3]);
+
+ if ($lastType === 'string' && strtoupper($arguments[3]) === 'WITHSCORES') {
+ // Used for compatibility with older versions
+ $arguments[3] = array('WITHSCORES' => true);
+ $lastType = 'array';
+ }
+
+ if ($lastType === 'array') {
+ $options = $this->prepareOptions(array_pop($arguments));
+
+ return array_merge($arguments, $options);
+ }
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ protected function prepareOptions($options)
+ {
+ $opts = array_change_key_case($options, CASE_UPPER);
+ $finalizedOpts = array();
+
+ if (!empty($opts['WITHSCORES'])) {
+ $finalizedOpts[] = 'WITHSCORES';
+ }
+
+ return $finalizedOpts;
+ }
+
+ /**
+ * Checks for the presence of the WITHSCORES modifier.
+ *
+ * @return bool
+ */
+ protected function withScores()
+ {
+ $arguments = $this->getArguments();
+
+ if (count($arguments) < 4) {
+ return false;
+ }
+
+ return strtoupper($arguments[3]) === 'WITHSCORES';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if ($this->withScores()) {
+ $result = array();
+
+ for ($i = 0; $i < count($data); $i++) {
+ $result[$data[$i]] = $data[++$i];
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sinterstore
+ * @author Daniele Alessandri
+ */
+class SetIntersectionStore extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SINTERSTORE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[1])) {
+ return array_merge(array($arguments[0]), $arguments[1]);
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sinter
+ * @author Daniele Alessandri
+ */
+class SetIntersection extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SINTER';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/eval
+ * @author Daniele Alessandri
+ */
+class ServerEval extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EVAL';
+ }
+
+ /**
+ * Calculates the SHA1 hash of the body of the script.
+ *
+ * @return string SHA1 hash.
+ */
+ public function getScriptHash()
+ {
+ return sha1($this->getArgument(0));
+ }
+}
+
+/**
+ * @link http://redis.io/commands/rename
+ * @author Daniele Alessandri
+ */
+class KeyRename extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RENAME';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/setex
+ * @author Daniele Alessandri
+ */
+class StringSetExpire extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SETEX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/mset
+ * @author Daniele Alessandri
+ */
+class StringSetMultiple extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MSET';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 1 && is_array($arguments[0])) {
+ $flattenedKVs = array();
+ $args = $arguments[0];
+
+ foreach ($args as $k => $v) {
+ $flattenedKVs[] = $k;
+ $flattenedKVs[] = $v;
+ }
+
+ return $flattenedKVs;
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/expireat
+ * @author Daniele Alessandri
+ */
+class KeyExpireAt extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EXPIREAT';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/blpop
+ * @author Daniele Alessandri
+ */
+class ListPopFirstBlocking extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BLPOP';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[0])) {
+ list($arguments, $timeout) = $arguments;
+ array_push($arguments, $timeout);
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/unsubscribe
+ * @author Daniele Alessandri
+ */
+class PubSubUnsubscribe extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'UNSUBSCRIBE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/info
+ * @author Daniele Alessandri
+ */
+class ServerInfo extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'INFO';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ $info = array();
+ $infoLines = preg_split('/\r?\n/', $data);
+
+ foreach ($infoLines as $row) {
+ if (strpos($row, ':') === false) {
+ continue;
+ }
+
+ list($k, $v) = $this->parseRow($row);
+ $info[$k] = $v;
+ }
+
+ return $info;
+ }
+
+ /**
+ * Parses a single row of the response and returns the key-value pair.
+ *
+ * @param string $row Single row of the response.
+ *
+ * @return array
+ */
+ protected function parseRow($row)
+ {
+ list($k, $v) = explode(':', $row, 2);
+
+ if (preg_match('/^db\d+$/', $k)) {
+ $v = $this->parseDatabaseStats($v);
+ }
+
+ return array($k, $v);
+ }
+
+ /**
+ * Extracts the statistics of each logical DB from the string buffer.
+ *
+ * @param string $str Response buffer.
+ *
+ * @return array
+ */
+ protected function parseDatabaseStats($str)
+ {
+ $db = array();
+
+ foreach (explode(',', $str) as $dbvar) {
+ list($dbvk, $dbvv) = explode('=', $dbvar);
+ $db[trim($dbvk)] = $dbvv;
+ }
+
+ return $db;
+ }
+
+ /**
+ * Parses the response and extracts the allocation statistics.
+ *
+ * @param string $str Response buffer.
+ *
+ * @return array
+ */
+ protected function parseAllocationStats($str)
+ {
+ $stats = array();
+
+ foreach (explode(',', $str) as $kv) {
+ @list($size, $objects, $extra) = explode('=', $kv);
+
+ // hack to prevent incorrect values when parsing the >=256 key
+ if (isset($extra)) {
+ $size = ">=$objects";
+ $objects = $extra;
+ }
+
+ $stats[$size] = $objects;
+ }
+
+ return $stats;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/evalsha
+ * @author Daniele Alessandri
+ */
+class ServerEvalSHA extends ServerEval
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EVALSHA';
+ }
+
+ /**
+ * Returns the SHA1 hash of the body of the script.
+ *
+ * @return string SHA1 hash.
+ */
+ public function getScriptHash()
+ {
+ return $this->getArgument(0);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/expire
+ * @author Daniele Alessandri
+ */
+class KeyExpire extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EXPIRE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/subscribe
+ * @author Daniele Alessandri
+ */
+class PubSubSubscribe extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SUBSCRIBE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/rpush
+ * @author Daniele Alessandri
+ */
+class ListPushTail extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RPUSH';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/ttl
+ * @author Daniele Alessandri
+ */
+class KeyTimeToLive extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'TTL';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zunionstore
+ * @author Daniele Alessandri
+ */
+class ZSetUnionStore extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZUNIONSTORE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ $options = array();
+ $argc = count($arguments);
+
+ if ($argc > 2 && is_array($arguments[$argc - 1])) {
+ $options = $this->prepareOptions(array_pop($arguments));
+ }
+
+ if (is_array($arguments[1])) {
+ $arguments = array_merge(
+ array($arguments[0], count($arguments[1])),
+ $arguments[1]
+ );
+ }
+
+ return array_merge($arguments, $options);
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ private function prepareOptions($options)
+ {
+ $opts = array_change_key_case($options, CASE_UPPER);
+ $finalizedOpts = array();
+
+ if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) {
+ $finalizedOpts[] = 'WEIGHTS';
+
+ foreach ($opts['WEIGHTS'] as $weight) {
+ $finalizedOpts[] = $weight;
+ }
+ }
+
+ if (isset($opts['AGGREGATE'])) {
+ $finalizedOpts[] = 'AGGREGATE';
+ $finalizedOpts[] = $opts['AGGREGATE'];
+ }
+
+ return $finalizedOpts;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrangebyscore
+ * @author Daniele Alessandri
+ */
+class ZSetRangeByScore extends ZSetRange
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZRANGEBYSCORE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareOptions($options)
+ {
+ $opts = array_change_key_case($options, CASE_UPPER);
+ $finalizedOpts = array();
+
+ if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
+ $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);
+
+ $finalizedOpts[] = 'LIMIT';
+ $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
+ $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
+ }
+
+ return array_merge($finalizedOpts, parent::prepareOptions($options));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function withScores()
+ {
+ $arguments = $this->getArguments();
+
+ for ($i = 3; $i < count($arguments); $i++) {
+ switch (strtoupper($arguments[$i])) {
+ case 'WITHSCORES':
+ return true;
+
+ case 'LIMIT':
+ $i += 2;
+ break;
+ }
+ }
+
+ return false;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zremrangebyrank
+ * @author Daniele Alessandri
+ */
+class ZSetRemoveRangeByRank extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREMRANGEBYRANK';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/spop
+ * @author Daniele Alessandri
+ */
+class SetPop extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SPOP';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/smove
+ * @author Daniele Alessandri
+ */
+class SetMove extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SMOVE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sismember
+ * @author Daniele Alessandri
+ */
+class SetIsMember extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SISMEMBER';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/smembers
+ * @author Daniele Alessandri
+ */
+class SetMembers extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SMEMBERS';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zremrangebyscore
+ * @author Daniele Alessandri
+ */
+class ZSetRemoveRangeByScore extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREMRANGEBYSCORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/srandmember
+ * @author Daniele Alessandri
+ */
+class SetRandomMember extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SRANDMEMBER';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sscan
+ * @author Daniele Alessandri
+ */
+class SetScan extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SSCAN';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 3 && is_array($arguments[2])) {
+ $options = $this->prepareOptions(array_pop($arguments));
+ $arguments = array_merge($arguments, $options);
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ protected function prepareOptions($options)
+ {
+ $options = array_change_key_case($options, CASE_UPPER);
+ $normalized = array();
+
+ if (!empty($options['MATCH'])) {
+ $normalized[] = 'MATCH';
+ $normalized[] = $options['MATCH'];
+ }
+
+ if (!empty($options['COUNT'])) {
+ $normalized[] = 'COUNT';
+ $normalized[] = $options['COUNT'];
+ }
+
+ return $normalized;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zremrangebylex
+ * @author Daniele Alessandri
+ */
+class ZSetRemoveRangeByLex extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREMRANGEBYLEX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/bitop
+ * @author Daniele Alessandri
+ */
+class StringBitOp extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BITOP';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 3 && is_array($arguments[2])) {
+ list($operation, $destination, ) = $arguments;
+ $arguments = $arguments[2];
+ array_unshift($arguments, $operation, $destination);
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/bitcount
+ * @author Daniele Alessandri
+ */
+class StringBitCount extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BITCOUNT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/append
+ * @author Daniele Alessandri
+ */
+class StringAppend extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'APPEND';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sunion
+ * @author Daniele Alessandri
+ */
+class SetUnion extends SetIntersection
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SUNION';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sunionstore
+ * @author Daniele Alessandri
+ */
+class SetUnionStore extends SetIntersectionStore
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SUNIONSTORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/srem
+ * @author Daniele Alessandri
+ */
+class SetRemove extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SREM';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrevrange
+ * @author Daniele Alessandri
+ */
+class ZSetReverseRange extends ZSetRange
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREVRANGE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/slowlog
+ * @author Daniele Alessandri
+ */
+class ServerSlowlog extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SLOWLOG';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if (is_array($data)) {
+ $log = array();
+
+ foreach ($data as $index => $entry) {
+ $log[$index] = array(
+ 'id' => $entry[0],
+ 'timestamp' => $entry[1],
+ 'duration' => $entry[2],
+ 'command' => $entry[3],
+ );
+ }
+
+ return $log;
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zscore
+ * @author Daniele Alessandri
+ */
+class ZSetScore extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZSCORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/slaveof
+ * @author Daniele Alessandri
+ */
+class ServerSlaveOf extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SLAVEOF';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 0 || $arguments[0] === 'NO ONE') {
+ return array('NO', 'ONE');
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/shutdown
+ * @author Daniele Alessandri
+ */
+class ServerShutdown extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SHUTDOWN';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/script
+ * @author Daniele Alessandri
+ */
+class ServerScript extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SCRIPT';
+ }
+}
+
+/**
+ * @link http://redis.io/topics/sentinel
+ * @author Daniele Alessandri
+ */
+class ServerSentinel extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SENTINEL';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ switch (strtolower($this->getArgument(0))) {
+ case 'masters':
+ case 'slaves':
+ return self::processMastersOrSlaves($data);
+
+ default:
+ return $data;
+ }
+ }
+
+ /**
+ * Returns a processed response to SENTINEL MASTERS or SENTINEL SLAVES.
+ *
+ * @param array $servers List of Redis servers.
+ *
+ * @return array
+ */
+ protected static function processMastersOrSlaves(array $servers)
+ {
+ foreach ($servers as $idx => $node) {
+ $processed = array();
+ $count = count($node);
+
+ for ($i = 0; $i < $count; $i++) {
+ $processed[$node[$i]] = $node[++$i];
+ }
+
+ $servers[$idx] = $processed;
+ }
+
+ return $servers;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zscan
+ * @author Daniele Alessandri
+ */
+class ZSetScan extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZSCAN';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 3 && is_array($arguments[2])) {
+ $options = $this->prepareOptions(array_pop($arguments));
+ $arguments = array_merge($arguments, $options);
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ protected function prepareOptions($options)
+ {
+ $options = array_change_key_case($options, CASE_UPPER);
+ $normalized = array();
+
+ if (!empty($options['MATCH'])) {
+ $normalized[] = 'MATCH';
+ $normalized[] = $options['MATCH'];
+ }
+
+ if (!empty($options['COUNT'])) {
+ $normalized[] = 'COUNT';
+ $normalized[] = $options['COUNT'];
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if (is_array($data)) {
+ $members = $data[1];
+ $result = array();
+
+ for ($i = 0; $i < count($members); $i++) {
+ $result[$members[$i]] = (float) $members[++$i];
+ }
+
+ $data[1] = $result;
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrevrank
+ * @author Daniele Alessandri
+ */
+class ZSetReverseRank extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREVRANK';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sdiffstore
+ * @author Daniele Alessandri
+ */
+class SetDifferenceStore extends SetIntersectionStore
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SDIFFSTORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrevrangebyscore
+ * @author Daniele Alessandri
+ */
+class ZSetReverseRangeByScore extends ZSetRangeByScore
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREVRANGEBYSCORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sdiff
+ * @author Daniele Alessandri
+ */
+class SetDifference extends SetIntersection
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SDIFF';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/scard
+ * @author Daniele Alessandri
+ */
+class SetCardinality extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SCARD';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/time
+ * @author Daniele Alessandri
+ */
+class ServerTime extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'TIME';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sadd
+ * @author Daniele Alessandri
+ */
+class SetAdd extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SADD';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/bitpos
+ * @author Daniele Alessandri
+ */
+class StringBitPos extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BITPOS';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/decrby
+ * @author Daniele Alessandri
+ */
+class StringDecrementBy extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DECRBY';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/substr
+ * @author Daniele Alessandri
+ */
+class StringSubstr extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SUBSTR';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zlexcount
+ * @author Daniele Alessandri
+ */
+class ZSetLexCount extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZLEXCOUNT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zinterstore
+ * @author Daniele Alessandri
+ */
+class ZSetIntersectionStore extends ZSetUnionStore
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZINTERSTORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/strlen
+ * @author Daniele Alessandri
+ */
+class StringStrlen extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'STRLEN';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/setrange
+ * @author Daniele Alessandri
+ */
+class StringSetRange extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SETRANGE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/msetnx
+ * @author Daniele Alessandri
+ */
+class StringSetMultiplePreserve extends StringSetMultiple
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MSETNX';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/setnx
+ * @author Daniele Alessandri
+ */
+class StringSetPreserve extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SETNX';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/discard
+ * @author Daniele Alessandri
+ */
+class TransactionDiscard extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DISCARD';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/exec
+ * @author Daniele Alessandri
+ */
+class TransactionExec extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EXEC';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zcard
+ * @author Daniele Alessandri
+ */
+class ZSetCardinality extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZCARD';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zcount
+ * @author Daniele Alessandri
+ */
+class ZSetCount extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZCOUNT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zadd
+ * @author Daniele Alessandri
+ */
+class ZSetAdd extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZADD';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[1])) {
+ $flattened = array($arguments[0]);
+
+ foreach ($arguments[1] as $member => $score) {
+ $flattened[] = $score;
+ $flattened[] = $member;
+ }
+
+ return $flattened;
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/watch
+ * @author Daniele Alessandri
+ */
+class TransactionWatch extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'WATCH';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (isset($arguments[0]) && is_array($arguments[0])) {
+ return $arguments[0];
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/multi
+ * @author Daniele Alessandri
+ */
+class TransactionMulti extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MULTI';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/unwatch
+ * @author Daniele Alessandri
+ */
+class TransactionUnwatch extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'UNWATCH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrangebylex
+ * @author Daniele Alessandri
+ */
+class ZSetRangeByLex extends ZSetRange
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZRANGEBYLEX';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareOptions($options)
+ {
+ $opts = array_change_key_case($options, CASE_UPPER);
+ $finalizedOpts = array();
+
+ if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
+ $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);
+
+ $finalizedOpts[] = 'LIMIT';
+ $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
+ $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
+ }
+
+ return $finalizedOpts;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function withScores()
+ {
+ return false;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrank
+ * @author Daniele Alessandri
+ */
+class ZSetRank extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZRANK';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/mget
+ * @author Daniele Alessandri
+ */
+class StringGetMultiple extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MGET';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/getrange
+ * @author Daniele Alessandri
+ */
+class StringGetRange extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'GETRANGE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zrem
+ * @author Daniele Alessandri
+ */
+class ZSetRemove extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZREM';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/getbit
+ * @author Daniele Alessandri
+ */
+class StringGetBit extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'GETBIT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/zincrby
+ * @author Daniele Alessandri
+ */
+class ZSetIncrementBy extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ZINCRBY';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/get
+ * @author Daniele Alessandri
+ */
+class StringGet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'GET';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/getset
+ * @author Daniele Alessandri
+ */
+class StringGetSet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'GETSET';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/incr
+ * @author Daniele Alessandri
+ */
+class StringIncrement extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'INCR';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/set
+ * @author Daniele Alessandri
+ */
+class StringSet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SET';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/setbit
+ * @author Daniele Alessandri
+ */
+class StringSetBit extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SETBIT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/psetex
+ * @author Daniele Alessandri
+ */
+class StringPreciseSetExpire extends StringSetExpire
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PSETEX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/incrbyfloat
+ * @author Daniele Alessandri
+ */
+class StringIncrementByFloat extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'INCRBYFLOAT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/incrby
+ * @author Daniele Alessandri
+ */
+class StringIncrementBy extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'INCRBY';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/save
+ * @author Daniele Alessandri
+ */
+class ServerSave extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SAVE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/decr
+ * @author Daniele Alessandri
+ */
+class StringDecrement extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DECR';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/flushall
+ * @author Daniele Alessandri
+ */
+class ServerFlushAll extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'FLUSHALL';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/del
+ * @author Daniele Alessandri
+ */
+class KeyDelete extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DEL';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/dump
+ * @author Daniele Alessandri
+ */
+class KeyDump extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DUMP';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/exists
+ * @author Daniele Alessandri
+ */
+class KeyExists extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EXISTS';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pfmerge
+ * @author Daniele Alessandri
+ */
+class HyperLogLogMerge extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PFMERGE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pfcount
+ * @author Daniele Alessandri
+ */
+class HyperLogLogCount extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PFCOUNT';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeArguments($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hvals
+ * @author Daniele Alessandri
+ */
+class HashValues extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HVALS';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pfadd
+ * @author Daniele Alessandri
+ */
+class HyperLogLogAdd extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PFADD';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/keys
+ * @author Daniele Alessandri
+ */
+class KeyKeys extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'KEYS';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/move
+ * @author Daniele Alessandri
+ */
+class KeyMove extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MOVE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/randomkey
+ * @author Daniele Alessandri
+ */
+class KeyRandom extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RANDOMKEY';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return $data !== '' ? $data : null;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/renamenx
+ * @author Daniele Alessandri
+ */
+class KeyRenamePreserve extends KeyRename
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RENAMENX';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/restore
+ * @author Daniele Alessandri
+ */
+class KeyRestore extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RESTORE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pttl
+ * @author Daniele Alessandri
+ */
+class KeyPreciseTimeToLive extends KeyTimeToLive
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PTTL';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pexpireat
+ * @author Daniele Alessandri
+ */
+class KeyPreciseExpireAt extends KeyExpireAt
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PEXPIREAT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/persist
+ * @author Daniele Alessandri
+ */
+class KeyPersist extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PERSIST';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pexpire
+ * @author Daniele Alessandri
+ */
+class KeyPreciseExpire extends KeyExpire
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PEXPIRE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hsetnx
+ * @author Daniele Alessandri
+ */
+class HashSetPreserve extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HSETNX';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hmset
+ * @author Daniele Alessandri
+ */
+class HashSetMultiple extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HMSET';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[1])) {
+ $flattenedKVs = array($arguments[0]);
+ $args = $arguments[1];
+
+ foreach ($args as $k => $v) {
+ $flattenedKVs[] = $k;
+ $flattenedKVs[] = $v;
+ }
+
+ return $flattenedKVs;
+ }
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/select
+ * @author Daniele Alessandri
+ */
+class ConnectionSelect extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SELECT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hdel
+ * @author Daniele Alessandri
+ */
+class HashDelete extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HDEL';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hexists
+ * @author Daniele Alessandri
+ */
+class HashExists extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HEXISTS';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/quit
+ * @author Daniele Alessandri
+ */
+class ConnectionQuit extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'QUIT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/ping
+ * @author Daniele Alessandri
+ */
+class ConnectionPing extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PING';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/auth
+ * @author Daniele Alessandri
+ */
+class ConnectionAuth extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'AUTH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/echo
+ * @author Daniele Alessandri
+ */
+class ConnectionEcho extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'ECHO';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hget
+ * @author Daniele Alessandri
+ */
+class HashGet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HGET';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hgetall
+ * @author Daniele Alessandri
+ */
+class HashGetAll extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HGETALL';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ $result = array();
+
+ for ($i = 0; $i < count($data); $i++) {
+ $result[$data[$i]] = $data[++$i];
+ }
+
+ return $result;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hlen
+ * @author Daniele Alessandri
+ */
+class HashLength extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HLEN';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hscan
+ * @author Daniele Alessandri
+ */
+class HashScan extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HSCAN';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 3 && is_array($arguments[2])) {
+ $options = $this->prepareOptions(array_pop($arguments));
+ $arguments = array_merge($arguments, $options);
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ protected function prepareOptions($options)
+ {
+ $options = array_change_key_case($options, CASE_UPPER);
+ $normalized = array();
+
+ if (!empty($options['MATCH'])) {
+ $normalized[] = 'MATCH';
+ $normalized[] = $options['MATCH'];
+ }
+
+ if (!empty($options['COUNT'])) {
+ $normalized[] = 'COUNT';
+ $normalized[] = $options['COUNT'];
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if (is_array($data)) {
+ $fields = $data[1];
+ $result = array();
+
+ for ($i = 0; $i < count($fields); $i++) {
+ $result[$fields[$i]] = $fields[++$i];
+ }
+
+ $data[1] = $result;
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hset
+ * @author Daniele Alessandri
+ */
+class HashSet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HSET';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return (bool) $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hkeys
+ * @author Daniele Alessandri
+ */
+class HashKeys extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HKEYS';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hincrbyfloat
+ * @author Daniele Alessandri
+ */
+class HashIncrementByFloat extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HINCRBYFLOAT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hmget
+ * @author Daniele Alessandri
+ */
+class HashGetMultiple extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HMGET';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ return self::normalizeVariadic($arguments);
+ }
+}
+
+/**
+ * @link http://redis.io/commands/hincrby
+ * @author Daniele Alessandri
+ */
+class HashIncrementBy extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'HINCRBY';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/scan
+ * @author Daniele Alessandri
+ */
+class KeyScan extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SCAN';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 2 && is_array($arguments[1])) {
+ $options = $this->prepareOptions(array_pop($arguments));
+ $arguments = array_merge($arguments, $options);
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Returns a list of options and modifiers compatible with Redis.
+ *
+ * @param array $options List of options.
+ *
+ * @return array
+ */
+ protected function prepareOptions($options)
+ {
+ $options = array_change_key_case($options, CASE_UPPER);
+ $normalized = array();
+
+ if (!empty($options['MATCH'])) {
+ $normalized[] = 'MATCH';
+ $normalized[] = $options['MATCH'];
+ }
+
+ if (!empty($options['COUNT'])) {
+ $normalized[] = 'COUNT';
+ $normalized[] = $options['COUNT'];
+ }
+
+ return $normalized;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/sort
+ * @author Daniele Alessandri
+ */
+class KeySort extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'SORT';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (count($arguments) === 1) {
+ return $arguments;
+ }
+
+ $query = array($arguments[0]);
+ $sortParams = array_change_key_case($arguments[1], CASE_UPPER);
+
+ if (isset($sortParams['BY'])) {
+ $query[] = 'BY';
+ $query[] = $sortParams['BY'];
+ }
+
+ if (isset($sortParams['GET'])) {
+ $getargs = $sortParams['GET'];
+
+ if (is_array($getargs)) {
+ foreach ($getargs as $getarg) {
+ $query[] = 'GET';
+ $query[] = $getarg;
+ }
+ } else {
+ $query[] = 'GET';
+ $query[] = $getargs;
+ }
+ }
+
+ if (isset($sortParams['LIMIT']) &&
+ is_array($sortParams['LIMIT']) &&
+ count($sortParams['LIMIT']) == 2) {
+
+ $query[] = 'LIMIT';
+ $query[] = $sortParams['LIMIT'][0];
+ $query[] = $sortParams['LIMIT'][1];
+ }
+
+ if (isset($sortParams['SORT'])) {
+ $query[] = strtoupper($sortParams['SORT']);
+ }
+
+ if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) {
+ $query[] = 'ALPHA';
+ }
+
+ if (isset($sortParams['STORE'])) {
+ $query[] = 'STORE';
+ $query[] = $sortParams['STORE'];
+ }
+
+ return $query;
+ }
+}
+
+/**
+ * Class for generic "anonymous" Redis commands.
+ *
+ * This command class does not filter input arguments or parse responses, but
+ * can be used to leverage the standard Predis API to execute any command simply
+ * by providing the needed arguments following the command signature as defined
+ * by Redis in its documentation.
+ *
+ * @author Daniele Alessandri
+ */
+class RawCommand implements CommandInterface
+{
+ private $slot;
+ private $commandID;
+ private $arguments;
+
+ /**
+ * @param array $arguments Command ID and its arguments.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(array $arguments)
+ {
+ if (!$arguments) {
+ throw new InvalidArgumentException(
+ 'The arguments array must contain at least the command ID.'
+ );
+ }
+
+ $this->commandID = strtoupper(array_shift($arguments));
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * Creates a new raw command using a variadic method.
+ *
+ * @param string $commandID Redis command ID.
+ * @param string ... Arguments list for the command.
+ *
+ * @return CommandInterface
+ */
+ public static function create($commandID /* [ $arg, ... */)
+ {
+ $arguments = func_get_args();
+ $command = new self($arguments);
+
+ return $command;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return $this->commandID;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+ unset($this->slot);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRawArguments(array $arguments)
+ {
+ $this->setArguments($arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getArgument($index)
+ {
+ if (isset($this->arguments[$index])) {
+ return $this->arguments[$index];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSlot($slot)
+ {
+ $this->slot = $slot;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlot()
+ {
+ if (isset($this->slot)) {
+ return $this->slot;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return $data;
+ }
+}
+
+/**
+ * Base class used to implement an higher level abstraction for commands based
+ * on Lua scripting with EVAL and EVALSHA.
+ *
+ * @link http://redis.io/commands/eval
+ * @author Daniele Alessandri
+ */
+abstract class ScriptCommand extends ServerEvalSHA
+{
+ /**
+ * Gets the body of a Lua script.
+ *
+ * @return string
+ */
+ abstract public function getScript();
+
+ /**
+ * Specifies the number of arguments that should be considered as keys.
+ *
+ * The default behaviour for the base class is to return 0 to indicate that
+ * all the elements of the arguments array should be considered as keys, but
+ * subclasses can enforce a static number of keys.
+ *
+ * @return int
+ */
+ protected function getKeysCount()
+ {
+ return 0;
+ }
+
+ /**
+ * Returns the elements from the arguments that are identified as keys.
+ *
+ * @return array
+ */
+ public function getKeys()
+ {
+ return array_slice($this->getArguments(), 2, $this->getKeysCount());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function filterArguments(array $arguments)
+ {
+ if (($numkeys = $this->getKeysCount()) && $numkeys < 0) {
+ $numkeys = count($arguments) + $numkeys;
+ }
+
+ return array_merge(array(sha1($this->getScript()), (int) $numkeys), $arguments);
+ }
+
+ /**
+ * @return array
+ */
+ public function getEvalArguments()
+ {
+ $arguments = $this->getArguments();
+ $arguments[0] = $this->getScript();
+
+ return $arguments;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/bgrewriteaof
+ * @author Daniele Alessandri
+ */
+class ServerBackgroundRewriteAOF extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BGREWRITEAOF';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return $data == 'Background append only file rewriting started';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/punsubscribe
+ * @author Daniele Alessandri
+ */
+class PubSubUnsubscribeByPattern extends PubSubUnsubscribe
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PUNSUBSCRIBE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/psubscribe
+ * @author Daniele Alessandri
+ */
+class PubSubSubscribeByPattern extends PubSubSubscribe
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PSUBSCRIBE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/publish
+ * @author Daniele Alessandri
+ */
+class PubSubPublish extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PUBLISH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/pubsub
+ * @author Daniele Alessandri
+ */
+class PubSubPubsub extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'PUBSUB';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ switch (strtolower($this->getArgument(0))) {
+ case 'numsub':
+ return self::processNumsub($data);
+
+ default:
+ return $data;
+ }
+ }
+
+ /**
+ * Returns the processed response to PUBSUB NUMSUB.
+ *
+ * @param array $channels List of channels
+ *
+ * @return array
+ */
+ protected static function processNumsub(array $channels)
+ {
+ $processed = array();
+ $count = count($channels);
+
+ for ($i = 0; $i < $count; $i++) {
+ $processed[$channels[$i]] = $channels[++$i];
+ }
+
+ return $processed;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/bgsave
+ * @author Daniele Alessandri
+ */
+class ServerBackgroundSave extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BGSAVE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ return $data === 'Background saving started' ? true : $data;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/client-list
+ * @link http://redis.io/commands/client-kill
+ * @link http://redis.io/commands/client-getname
+ * @link http://redis.io/commands/client-setname
+ * @author Daniele Alessandri
+ */
+class ServerClient extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'CLIENT';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ $args = array_change_key_case($this->getArguments(), CASE_UPPER);
+
+ switch (strtoupper($args[0])) {
+ case 'LIST':
+ return $this->parseClientList($data);
+ case 'KILL':
+ case 'GETNAME':
+ case 'SETNAME':
+ default:
+ return $data;
+ }
+ }
+
+ /**
+ * Parses the response to CLIENT LIST and returns a structured list.
+ *
+ * @param string $data Response buffer.
+ *
+ * @return array
+ */
+ protected function parseClientList($data)
+ {
+ $clients = array();
+
+ foreach (explode("\n", $data, -1) as $clientData) {
+ $client = array();
+
+ foreach (explode(' ', $clientData) as $kv) {
+ @list($k, $v) = explode('=', $kv);
+ $client[$k] = $v;
+ }
+
+ $clients[] = $client;
+ }
+
+ return $clients;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/info
+ * @author Daniele Alessandri
+ */
+class ServerInfoV26x extends ServerInfo
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if ($data === '') {
+ return array();
+ }
+
+ $info = array();
+
+ $current = null;
+ $infoLines = preg_split('/\r?\n/', $data);
+
+ if (isset($infoLines[0]) && $infoLines[0][0] !== '#') {
+ return parent::parseResponse($data);
+ }
+
+ foreach ($infoLines as $row) {
+ if ($row === '') {
+ continue;
+ }
+
+ if (preg_match('/^# (\w+)$/', $row, $matches)) {
+ $info[$matches[1]] = array();
+ $current = &$info[$matches[1]];
+ continue;
+ }
+
+ list($k, $v) = $this->parseRow($row);
+ $current[$k] = $v;
+ }
+
+ return $info;
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lastsave
+ * @author Daniele Alessandri
+ */
+class ServerLastSave extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LASTSAVE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/monitor
+ * @author Daniele Alessandri
+ */
+class ServerMonitor extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'MONITOR';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/flushdb
+ * @author Daniele Alessandri
+ */
+class ServerFlushDatabase extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'FLUSHDB';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/dbsize
+ * @author Daniele Alessandri
+ */
+class ServerDatabaseSize extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'DBSIZE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/command
+ * @author Daniele Alessandri
+ */
+class ServerCommand extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'COMMAND';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/config-set
+ * @link http://redis.io/commands/config-get
+ * @link http://redis.io/commands/config-resetstat
+ * @link http://redis.io/commands/config-rewrite
+ * @author Daniele Alessandri
+ */
+class ServerConfig extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'CONFIG';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseResponse($data)
+ {
+ if (is_array($data)) {
+ $result = array();
+
+ for ($i = 0; $i < count($data); $i++) {
+ $result[$data[$i]] = $data[++$i];
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * Defines a command whose keys can be prefixed.
+ *
+ * @author Daniele Alessandri
+ */
+interface PrefixableCommandInterface extends CommandInterface
+{
+ /**
+ * Prefixes all the keys found in the arguments of the command.
+ *
+ * @param string $prefix String used to prefix the keys.
+ */
+ public function prefixKeys($prefix);
+}
+
+/**
+ * @link http://redis.io/commands/ltrim
+ * @author Daniele Alessandri
+ */
+class ListTrim extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LTRIM';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lpop
+ * @author Daniele Alessandri
+ */
+class ListPopFirst extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LPOP';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/rpop
+ * @author Daniele Alessandri
+ */
+class ListPopLast extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RPOP';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/brpop
+ * @author Daniele Alessandri
+ */
+class ListPopLastBlocking extends ListPopFirstBlocking
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BRPOP';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/llen
+ * @author Daniele Alessandri
+ */
+class ListLength extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LLEN';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/linsert
+ * @author Daniele Alessandri
+ */
+class ListInsert extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LINSERT';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/type
+ * @author Daniele Alessandri
+ */
+class KeyType extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'TYPE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lindex
+ * @author Daniele Alessandri
+ */
+class ListIndex extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LINDEX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/rpoplpush
+ * @author Daniele Alessandri
+ */
+class ListPopLastPushHead extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RPOPLPUSH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/brpoplpush
+ * @author Daniele Alessandri
+ */
+class ListPopLastPushHeadBlocking extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'BRPOPLPUSH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lrem
+ * @author Daniele Alessandri
+ */
+class ListRemove extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LREM';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lset
+ * @author Daniele Alessandri
+ */
+class ListSet extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LSET';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lrange
+ * @author Daniele Alessandri
+ */
+class ListRange extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LRANGE';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/rpushx
+ * @author Daniele Alessandri
+ */
+class ListPushTailX extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'RPUSHX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lpush
+ * @author Daniele Alessandri
+ */
+class ListPushHead extends ListPushTail
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LPUSH';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/lpushx
+ * @author Daniele Alessandri
+ */
+class ListPushHeadX extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'LPUSHX';
+ }
+}
+
+/**
+ * @link http://redis.io/commands/object
+ * @author Daniele Alessandri
+ */
+class ServerObject extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'OBJECT';
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Connection;
+
+use InvalidArgumentException;
+use Predis\CommunicationException;
+use Predis\Command\CommandInterface;
+use Predis\Protocol\ProtocolException;
+use Predis\Protocol\ProtocolProcessorInterface;
+use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor;
+use UnexpectedValueException;
+use ReflectionClass;
+use Predis\Command\RawCommand;
+use Predis\NotSupportedException;
+use Predis\Response\Error as ErrorResponse;
+use Predis\Response\Status as StatusResponse;
+
+/**
+ * Defines a connection object used to communicate with one or multiple
+ * Redis servers.
+ *
+ * @author Daniele Alessandri
+ */
+interface ConnectionInterface
+{
+ /**
+ * Opens the connection to Redis.
+ */
+ public function connect();
+
+ /**
+ * Closes the connection to Redis.
+ */
+ public function disconnect();
+
+ /**
+ * Checks if the connection to Redis is considered open.
+ *
+ * @return bool
+ */
+ public function isConnected();
+
+ /**
+ * Writes the request for the given command over the connection.
+ *
+ * @param CommandInterface $command Command instance.
+ */
+ public function writeRequest(CommandInterface $command);
+
+ /**
+ * Reads the response to the given command from the connection.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return mixed
+ */
+ public function readResponse(CommandInterface $command);
+
+ /**
+ * Writes a request for the given command over the connection and reads back
+ * the response returned by Redis.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return mixed
+ */
+ public function executeCommand(CommandInterface $command);
+}
+
+/**
+ * Defines a connection used to communicate with a single Redis node.
+ *
+ * @author Daniele Alessandri
+ */
+interface NodeConnectionInterface extends ConnectionInterface
+{
+ /**
+ * Returns a string representation of the connection.
+ *
+ * @return string
+ */
+ public function __toString();
+
+ /**
+ * Returns the underlying resource used to communicate with Redis.
+ *
+ * @return mixed
+ */
+ public function getResource();
+
+ /**
+ * Returns the parameters used to initialize the connection.
+ *
+ * @return ParametersInterface
+ */
+ public function getParameters();
+
+ /**
+ * Pushes the given command into a queue of commands executed when
+ * establishing the actual connection to Redis.
+ *
+ * @param CommandInterface $command Instance of a Redis command.
+ */
+ public function addConnectCommand(CommandInterface $command);
+
+ /**
+ * Reads a response from the server.
+ *
+ * @return mixed
+ */
+ public function read();
+}
+
+/**
+ * Defines a virtual connection composed of multiple connection instances to
+ * single Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+interface AggregateConnectionInterface extends ConnectionInterface
+{
+ /**
+ * Adds a connection instance to the aggregate connection.
+ *
+ * @param NodeConnectionInterface $connection Connection instance.
+ */
+ public function add(NodeConnectionInterface $connection);
+
+ /**
+ * Removes the specified connection instance from the aggregate connection.
+ *
+ * @param NodeConnectionInterface $connection Connection instance.
+ *
+ * @return bool Returns true if the connection was in the pool.
+ */
+ public function remove(NodeConnectionInterface $connection);
+
+ /**
+ * Returns the connection instance in charge for the given command.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getConnection(CommandInterface $command);
+
+ /**
+ * Returns a connection instance from the aggregate connection by its alias.
+ *
+ * @param string $connectionID Connection alias.
+ *
+ * @return NodeConnectionInterface|null
+ */
+ public function getConnectionById($connectionID);
+}
+
+/**
+ * Base class with the common logic used by connection classes to communicate
+ * with Redis.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class AbstractConnection implements NodeConnectionInterface
+{
+ private $resource;
+ private $cachedId;
+
+ protected $parameters;
+ protected $initCommands = array();
+
+ /**
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ */
+ public function __construct(ParametersInterface $parameters)
+ {
+ $this->parameters = $this->assertParameters($parameters);
+ }
+
+ /**
+ * Disconnects from the server and destroys the underlying resource when
+ * PHP's garbage collector kicks in.
+ */
+ public function __destruct()
+ {
+ $this->disconnect();
+ }
+
+ /**
+ * Checks some of the parameters used to initialize the connection.
+ *
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ *
+ * @return ParametersInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ protected function assertParameters(ParametersInterface $parameters)
+ {
+ $scheme = $parameters->scheme;
+
+ if ($scheme !== 'tcp' && $scheme !== 'unix') {
+ throw new InvalidArgumentException("Invalid scheme: '$scheme'.");
+ }
+
+ if ($scheme === 'unix' && !isset($parameters->path)) {
+ throw new InvalidArgumentException('Missing UNIX domain socket path.');
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Creates the underlying resource used to communicate with Redis.
+ *
+ * @return mixed
+ */
+ abstract protected function createResource();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isConnected()
+ {
+ return isset($this->resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ if (!$this->isConnected()) {
+ $this->resource = $this->createResource();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ unset($this->resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConnectCommand(CommandInterface $command)
+ {
+ $this->initCommands[] = $command;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ $this->writeRequest($command);
+
+ return $this->readResponse($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readResponse(CommandInterface $command)
+ {
+ return $this->read();
+ }
+
+ /**
+ * Helper method to handle connection errors.
+ *
+ * @param string $message Error message.
+ * @param int $code Error code.
+ */
+ protected function onConnectionError($message, $code = null)
+ {
+ CommunicationException::handle(
+ new ConnectionException(
+ $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]", $code
+ )
+ );
+ }
+
+ /**
+ * Helper method to handle protocol errors.
+ *
+ * @param string $message Error message.
+ */
+ protected function onProtocolError($message)
+ {
+ CommunicationException::handle(
+ new ProtocolException(
+ $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]"
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResource()
+ {
+ if (isset($this->resource)) {
+ return $this->resource;
+ }
+
+ $this->connect();
+
+ return $this->resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Gets an identifier for the connection.
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ if ($this->parameters->scheme === 'unix') {
+ return $this->parameters->path;
+ }
+
+ return "{$this->parameters->host}:{$this->parameters->port}";
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ if (!isset($this->cachedId)) {
+ $this->cachedId = $this->getIdentifier();
+ }
+
+ return $this->cachedId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep()
+ {
+ return array('parameters', 'initCommands');
+ }
+}
+
+/**
+ * Standard connection to Redis servers implemented on top of PHP's streams.
+ * The connection parameters supported by this class are:
+ *
+ * - scheme: it can be either 'tcp' or 'unix'.
+ * - host: hostname or IP address of the server.
+ * - port: TCP port of the server.
+ * - path: path of a UNIX domain socket when scheme is 'unix'.
+ * - timeout: timeout to perform the connection.
+ * - read_write_timeout: timeout of read / write operations.
+ * - async_connect: performs the connection asynchronously.
+ * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
+ * - persistent: the connection is left intact after a GC collection.
+ *
+ * @author Daniele Alessandri
+ */
+class StreamConnection extends AbstractConnection
+{
+ /**
+ * Disconnects from the server and destroys the underlying resource when the
+ * garbage collector kicks in only if the connection has not been marked as
+ * persistent.
+ */
+ public function __destruct()
+ {
+ if (isset($this->parameters->persistent) && $this->parameters->persistent) {
+ return;
+ }
+
+ $this->disconnect();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function createResource()
+ {
+ $initializer = "{$this->parameters->scheme}StreamInitializer";
+ $resource = $this->$initializer($this->parameters);
+
+ return $resource;
+ }
+
+ /**
+ * Initializes a TCP stream resource.
+ *
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ *
+ * @return resource
+ */
+ protected function tcpStreamInitializer(ParametersInterface $parameters)
+ {
+ $uri = "tcp://{$parameters->host}:{$parameters->port}";
+ $flags = STREAM_CLIENT_CONNECT;
+
+ if (isset($parameters->async_connect) && (bool) $parameters->async_connect) {
+ $flags |= STREAM_CLIENT_ASYNC_CONNECT;
+ }
+
+ if (isset($parameters->persistent) && (bool) $parameters->persistent) {
+ $flags |= STREAM_CLIENT_PERSISTENT;
+ $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path";
+ }
+
+ $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);
+
+ if (!$resource) {
+ $this->onConnectionError(trim($errstr), $errno);
+ }
+
+ if (isset($parameters->read_write_timeout)) {
+ $rwtimeout = (float) $parameters->read_write_timeout;
+ $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
+ $timeoutSeconds = floor($rwtimeout);
+ $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
+ stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
+ }
+
+ if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
+ $socket = socket_import_stream($resource);
+ socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
+ }
+
+ return $resource;
+ }
+
+ /**
+ * Initializes a UNIX stream resource.
+ *
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ *
+ * @return resource
+ */
+ protected function unixStreamInitializer(ParametersInterface $parameters)
+ {
+ $uri = "unix://{$parameters->path}";
+ $flags = STREAM_CLIENT_CONNECT;
+
+ if ((bool) $parameters->persistent) {
+ $flags |= STREAM_CLIENT_PERSISTENT;
+ }
+
+ $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);
+
+ if (!$resource) {
+ $this->onConnectionError(trim($errstr), $errno);
+ }
+
+ if (isset($parameters->read_write_timeout)) {
+ $rwtimeout = (float) $parameters->read_write_timeout;
+ $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
+ $timeoutSeconds = floor($rwtimeout);
+ $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
+ stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
+ }
+
+ return $resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ if (parent::connect() && $this->initCommands) {
+ foreach ($this->initCommands as $command) {
+ $this->executeCommand($command);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ if ($this->isConnected()) {
+ fclose($this->getResource());
+ parent::disconnect();
+ }
+ }
+
+ /**
+ * Performs a write operation over the stream of the buffer containing a
+ * command serialized with the Redis wire protocol.
+ *
+ * @param string $buffer Representation of a command in the Redis wire protocol.
+ */
+ protected function write($buffer)
+ {
+ $socket = $this->getResource();
+
+ while (($length = strlen($buffer)) > 0) {
+ $written = @fwrite($socket, $buffer);
+
+ if ($length === $written) {
+ return;
+ }
+
+ if ($written === false || $written === 0) {
+ $this->onConnectionError('Error while writing bytes to the server.');
+ }
+
+ $buffer = substr($buffer, $written);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read()
+ {
+ $socket = $this->getResource();
+ $chunk = fgets($socket);
+
+ if ($chunk === false || $chunk === '') {
+ $this->onConnectionError('Error while reading line from the server.');
+ }
+
+ $prefix = $chunk[0];
+ $payload = substr($chunk, 1, -2);
+
+ switch ($prefix) {
+ case '+':
+ return StatusResponse::get($payload);
+
+ case '$':
+ $size = (int) $payload;
+
+ if ($size === -1) {
+ return null;
+ }
+
+ $bulkData = '';
+ $bytesLeft = ($size += 2);
+
+ do {
+ $chunk = fread($socket, min($bytesLeft, 4096));
+
+ if ($chunk === false || $chunk === '') {
+ $this->onConnectionError('Error while reading bytes from the server.');
+ }
+
+ $bulkData .= $chunk;
+ $bytesLeft = $size - strlen($bulkData);
+ } while ($bytesLeft > 0);
+
+ return substr($bulkData, 0, -2);
+
+ case '*':
+ $count = (int) $payload;
+
+ if ($count === -1) {
+ return null;
+ }
+
+ $multibulk = array();
+
+ for ($i = 0; $i < $count; $i++) {
+ $multibulk[$i] = $this->read();
+ }
+
+ return $multibulk;
+
+ case ':':
+ return (int) $payload;
+
+ case '-':
+ return new ErrorResponse($payload);
+
+ default:
+ $this->onProtocolError("Unknown response prefix: '$prefix'.");
+
+ return;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $commandID = $command->getId();
+ $arguments = $command->getArguments();
+
+ $cmdlen = strlen($commandID);
+ $reqlen = count($arguments) + 1;
+
+ $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";
+
+ for ($i = 0, $reqlen--; $i < $reqlen; $i++) {
+ $argument = $arguments[$i];
+ $arglen = strlen($argument);
+ $buffer .= "\${$arglen}\r\n{$argument}\r\n";
+ }
+
+ $this->write($buffer);
+ }
+}
+
+/**
+ * Interface defining a container for connection parameters.
+ *
+ * The actual list of connection parameters depends on the features supported by
+ * each connection backend class (please refer to their specific documentation),
+ * but the most common parameters used through the library are:
+ *
+ * @property-read string scheme Connection scheme, such as 'tcp' or 'unix'.
+ * @property-read string host IP address or hostname of Redis.
+ * @property-read int port TCP port on which Redis is listening to.
+ * @property-read string path Path of a UNIX domain socket file.
+ * @property-read string alias Alias for the connection.
+ * @property-read float timeout Timeout for the connect() operation.
+ * @property-read float read_write_timeout Timeout for read() and write() operations.
+ * @property-read bool async_connect Performs the connect() operation asynchronously.
+ * @property-read bool tcp_nodelay Toggles the Nagle's algorithm for coalescing.
+ * @property-read bool persistent Leaves the connection open after a GC collection.
+ * @property-read string password Password to access Redis (see the AUTH command).
+ * @property-read string database Database index (see the SELECT command).
+ *
+ * @author Daniele Alessandri
+ */
+interface ParametersInterface
+{
+ /**
+ * Checks if the specified parameters is set.
+ *
+ * @param string $parameter Name of the parameter.
+ *
+ * @return bool
+ */
+ public function __isset($parameter);
+
+ /**
+ * Returns the value of the specified parameter.
+ *
+ * @param string $parameter Name of the parameter.
+ *
+ * @return mixed|null
+ */
+ public function __get($parameter);
+
+ /**
+ * Returns an array representation of the connection parameters.
+ *
+ * @return array
+ */
+ public function toArray();
+}
+
+/**
+ * Interface for classes providing a factory of connections to Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+interface FactoryInterface
+{
+ /**
+ * Defines or overrides the connection class identified by a scheme prefix.
+ *
+ * @param string $scheme Target connection scheme.
+ * @param mixed $initializer Fully-qualified name of a class or a callable for lazy initialization.
+ */
+ public function define($scheme, $initializer);
+
+ /**
+ * Undefines the connection identified by a scheme prefix.
+ *
+ * @param string $scheme Target connection scheme.
+ */
+ public function undefine($scheme);
+
+ /**
+ * Creates a new connection object.
+ *
+ * @param mixed $parameters Initialization parameters for the connection.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function create($parameters);
+
+ /**
+ * Aggregates single connections into an aggregate connection instance.
+ *
+ * @param AggregateConnectionInterface $aggregate Aggregate connection instance.
+ * @param array $parameters List of parameters for each connection.
+ */
+ public function aggregate(AggregateConnectionInterface $aggregate, array $parameters);
+}
+
+/**
+ * Defines a connection to communicate with a single Redis server that leverages
+ * an external protocol processor to handle pluggable protocol handlers.
+ *
+ * @author Daniele Alessandri
+ */
+interface CompositeConnectionInterface extends NodeConnectionInterface
+{
+ /**
+ * Returns the protocol processor used by the connection.
+ */
+ public function getProtocol();
+
+ /**
+ * Writes the buffer containing over the connection.
+ *
+ * @param string $buffer String buffer to be sent over the connection.
+ */
+ public function writeBuffer($buffer);
+
+ /**
+ * Reads the given number of bytes from the connection.
+ *
+ * @param int $length Number of bytes to read from the connection.
+ *
+ * @return string
+ */
+ public function readBuffer($length);
+
+ /**
+ * Reads a line from the connection.
+ *
+ * @param string
+ */
+ public function readLine();
+}
+
+/**
+ * This class provides the implementation of a Predis connection that uses PHP's
+ * streams for network communication and wraps the phpiredis C extension (PHP
+ * bindings for hiredis) to parse and serialize the Redis protocol.
+ *
+ * This class is intended to provide an optional low-overhead alternative for
+ * processing responses from Redis compared to the standard pure-PHP classes.
+ * Differences in speed when dealing with short inline responses are practically
+ * nonexistent, the actual speed boost is for big multibulk responses when this
+ * protocol processor can parse and return responses very fast.
+ *
+ * For instructions on how to build and install the phpiredis extension, please
+ * consult the repository of the project.
+ *
+ * The connection parameters supported by this class are:
+ *
+ * - scheme: it can be either 'tcp' or 'unix'.
+ * - host: hostname or IP address of the server.
+ * - port: TCP port of the server.
+ * - path: path of a UNIX domain socket when scheme is 'unix'.
+ * - timeout: timeout to perform the connection.
+ * - read_write_timeout: timeout of read / write operations.
+ * - async_connect: performs the connection asynchronously.
+ * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
+ * - persistent: the connection is left intact after a GC collection.
+ *
+ * @link https://github.com/nrk/phpiredis
+ * @author Daniele Alessandri
+ */
+class PhpiredisStreamConnection extends StreamConnection
+{
+ private $reader;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ParametersInterface $parameters)
+ {
+ $this->assertExtensions();
+
+ parent::__construct($parameters);
+
+ $this->reader = $this->createReader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __destruct()
+ {
+ phpiredis_reader_destroy($this->reader);
+
+ parent::__destruct();
+ }
+
+ /**
+ * Checks if the phpiredis extension is loaded in PHP.
+ */
+ private function assertExtensions()
+ {
+ if (!extension_loaded('phpiredis')) {
+ throw new NotSupportedException(
+ 'The "phpiredis" extension is required by this connection backend.'
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tcpStreamInitializer(ParametersInterface $parameters)
+ {
+ $uri = "tcp://{$parameters->host}:{$parameters->port}";
+ $flags = STREAM_CLIENT_CONNECT;
+ $socket = null;
+
+ if (isset($parameters->async_connect) && (bool) $parameters->async_connect) {
+ $flags |= STREAM_CLIENT_ASYNC_CONNECT;
+ }
+
+ if (isset($parameters->persistent) && (bool) $parameters->persistent) {
+ $flags |= STREAM_CLIENT_PERSISTENT;
+ $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path";
+ }
+
+ $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);
+
+ if (!$resource) {
+ $this->onConnectionError(trim($errstr), $errno);
+ }
+
+ if (isset($parameters->read_write_timeout) && function_exists('socket_import_stream')) {
+ $rwtimeout = (float) $parameters->read_write_timeout;
+ $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
+
+ $timeout = array(
+ 'sec' => $timeoutSeconds = floor($rwtimeout),
+ 'usec' => ($rwtimeout - $timeoutSeconds) * 1000000,
+ );
+
+ $socket = $socket ?: socket_import_stream($resource);
+ @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout);
+ @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout);
+ }
+
+ if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
+ $socket = $socket ?: socket_import_stream($resource);
+ socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
+ }
+
+ return $resource;
+ }
+
+ /**
+ * Creates a new instance of the protocol reader resource.
+ *
+ * @return resource
+ */
+ private function createReader()
+ {
+ $reader = phpiredis_reader_create();
+
+ phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
+ phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
+
+ return $reader;
+ }
+
+ /**
+ * Returns the underlying protocol reader resource.
+ *
+ * @return resource
+ */
+ protected function getReader()
+ {
+ return $this->reader;
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for inline responses.
+ *
+ * @return \Closure
+ */
+ protected function getStatusHandler()
+ {
+ return function ($payload) {
+ return StatusResponse::get($payload);
+ };
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for error responses.
+ *
+ * @return \Closure
+ */
+ protected function getErrorHandler()
+ {
+ return function ($errorMessage) {
+ return new ErrorResponse($errorMessage);
+ };
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read()
+ {
+ $socket = $this->getResource();
+ $reader = $this->reader;
+
+ while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
+ $buffer = stream_socket_recvfrom($socket, 4096);
+
+ if ($buffer === false || $buffer === '') {
+ $this->onConnectionError('Error while reading bytes from the server.');
+ }
+
+ phpiredis_reader_feed($reader, $buffer);
+ }
+
+ if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
+ return phpiredis_reader_get_reply($reader);
+ } else {
+ $this->onProtocolError(phpiredis_reader_get_error($reader));
+
+ return;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ array_unshift($arguments, $command->getId());
+
+ $this->write(phpiredis_format_command($arguments));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __wakeup()
+ {
+ $this->assertExtensions();
+ $this->reader = $this->createReader();
+ }
+}
+
+/**
+ * This class implements a Predis connection that actually talks with Webdis
+ * instead of connecting directly to Redis. It relies on the cURL extension to
+ * communicate with the web server and the phpiredis extension to parse the
+ * protocol for responses returned in the http response bodies.
+ *
+ * Some features are not yet available or they simply cannot be implemented:
+ * - Pipelining commands.
+ * - Publish / Subscribe.
+ * - MULTI / EXEC transactions (not yet supported by Webdis).
+ *
+ * The connection parameters supported by this class are:
+ *
+ * - scheme: must be 'http'.
+ * - host: hostname or IP address of the server.
+ * - port: TCP port of the server.
+ * - timeout: timeout to perform the connection.
+ * - user: username for authentication.
+ * - pass: password for authentication.
+ *
+ * @link http://webd.is
+ * @link http://github.com/nicolasff/webdis
+ * @link http://github.com/seppo0010/phpiredis
+ * @author Daniele Alessandri
+ */
+class WebdisConnection implements NodeConnectionInterface
+{
+ private $parameters;
+ private $resource;
+ private $reader;
+
+ /**
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(ParametersInterface $parameters)
+ {
+ $this->assertExtensions();
+
+ if ($parameters->scheme !== 'http') {
+ throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'.");
+ }
+
+ $this->parameters = $parameters;
+
+ $this->resource = $this->createCurl();
+ $this->reader = $this->createReader();
+ }
+
+ /**
+ * Frees the underlying cURL and protocol reader resources when the garbage
+ * collector kicks in.
+ */
+ public function __destruct()
+ {
+ curl_close($this->resource);
+ phpiredis_reader_destroy($this->reader);
+ }
+
+ /**
+ * Helper method used to throw on unsupported methods.
+ *
+ * @param string $method Name of the unsupported method.
+ *
+ * @throws NotSupportedException
+ */
+ private function throwNotSupportedException($method)
+ {
+ $class = __CLASS__;
+ throw new NotSupportedException("The method $class::$method() is not supported.");
+ }
+
+ /**
+ * Checks if the cURL and phpiredis extensions are loaded in PHP.
+ */
+ private function assertExtensions()
+ {
+ if (!extension_loaded('curl')) {
+ throw new NotSupportedException(
+ 'The "curl" extension is required by this connection backend.'
+ );
+ }
+
+ if (!extension_loaded('phpiredis')) {
+ throw new NotSupportedException(
+ 'The "phpiredis" extension is required by this connection backend.'
+ );
+ }
+ }
+
+ /**
+ * Initializes cURL.
+ *
+ * @return resource
+ */
+ private function createCurl()
+ {
+ $parameters = $this->getParameters();
+
+ $options = array(
+ CURLOPT_FAILONERROR => true,
+ CURLOPT_CONNECTTIMEOUT_MS => $parameters->timeout * 1000,
+ CURLOPT_URL => "{$parameters->scheme}://{$parameters->host}:{$parameters->port}",
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_POST => true,
+ CURLOPT_WRITEFUNCTION => array($this, 'feedReader'),
+ );
+
+ if (isset($parameters->user, $parameters->pass)) {
+ $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}";
+ }
+
+ curl_setopt_array($resource = curl_init(), $options);
+
+ return $resource;
+ }
+
+ /**
+ * Initializes the phpiredis protocol reader.
+ *
+ * @return resource
+ */
+ private function createReader()
+ {
+ $reader = phpiredis_reader_create();
+
+ phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
+ phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
+
+ return $reader;
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for inline responses.
+ *
+ * @return \Closure
+ */
+ protected function getStatusHandler()
+ {
+ return function ($payload) {
+ return StatusResponse::get($payload);
+ };
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for error responses.
+ *
+ * @return \Closure
+ */
+ protected function getErrorHandler()
+ {
+ return function ($payload) {
+ return new ErrorResponse($payload);
+ };
+ }
+
+ /**
+ * Feeds the phpredis reader resource with the data read from the network.
+ *
+ * @param resource $resource Reader resource.
+ * @param string $buffer Buffer of data read from a connection.
+ *
+ * @return int
+ */
+ protected function feedReader($resource, $buffer)
+ {
+ phpiredis_reader_feed($this->reader, $buffer);
+
+ return strlen($buffer);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ // NOOP
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ // NOOP
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isConnected()
+ {
+ return true;
+ }
+
+ /**
+ * Checks if the specified command is supported by this connection class.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string
+ *
+ * @throws NotSupportedException
+ */
+ protected function getCommandId(CommandInterface $command)
+ {
+ switch ($commandID = $command->getId()) {
+ case 'AUTH':
+ case 'SELECT':
+ case 'MULTI':
+ case 'EXEC':
+ case 'WATCH':
+ case 'UNWATCH':
+ case 'DISCARD':
+ case 'MONITOR':
+ throw new NotSupportedException("Command '$commandID' is not allowed by Webdis.");
+
+ default:
+ return $commandID;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $this->throwNotSupportedException(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readResponse(CommandInterface $command)
+ {
+ $this->throwNotSupportedException(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ $resource = $this->resource;
+ $commandId = $this->getCommandId($command);
+
+ if ($arguments = $command->getArguments()) {
+ $arguments = implode('/', array_map('urlencode', $arguments));
+ $serializedCommand = "$commandId/$arguments.raw";
+ } else {
+ $serializedCommand = "$commandId.raw";
+ }
+
+ curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand);
+
+ if (curl_exec($resource) === false) {
+ $error = curl_error($resource);
+ $errno = curl_errno($resource);
+
+ throw new ConnectionException($this, trim($error), $errno);
+ }
+
+ if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) {
+ throw new ProtocolException($this, phpiredis_reader_get_error($this->reader));
+ }
+
+ return phpiredis_reader_get_reply($this->reader);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResource()
+ {
+ return $this->resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConnectCommand(CommandInterface $command)
+ {
+ $this->throwNotSupportedException(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read()
+ {
+ $this->throwNotSupportedException(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return "{$this->parameters->host}:{$this->parameters->port}";
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep()
+ {
+ return array('parameters');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __wakeup()
+ {
+ $this->assertExtensions();
+
+ $this->resource = $this->createCurl();
+ $this->reader = $this->createReader();
+ }
+}
+
+/**
+ * This class provides the implementation of a Predis connection that uses the
+ * PHP socket extension for network communication and wraps the phpiredis C
+ * extension (PHP bindings for hiredis) to parse the Redis protocol.
+ *
+ * This class is intended to provide an optional low-overhead alternative for
+ * processing responses from Redis compared to the standard pure-PHP classes.
+ * Differences in speed when dealing with short inline responses are practically
+ * nonexistent, the actual speed boost is for big multibulk responses when this
+ * protocol processor can parse and return responses very fast.
+ *
+ * For instructions on how to build and install the phpiredis extension, please
+ * consult the repository of the project.
+ *
+ * The connection parameters supported by this class are:
+ *
+ * - scheme: it can be either 'tcp' or 'unix'.
+ * - host: hostname or IP address of the server.
+ * - port: TCP port of the server.
+ * - path: path of a UNIX domain socket when scheme is 'unix'.
+ * - timeout: timeout to perform the connection.
+ * - read_write_timeout: timeout of read / write operations.
+ *
+ * @link http://github.com/nrk/phpiredis
+ * @author Daniele Alessandri
+ */
+class PhpiredisSocketConnection extends AbstractConnection
+{
+ private $reader;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ParametersInterface $parameters)
+ {
+ $this->assertExtensions();
+
+ parent::__construct($parameters);
+
+ $this->reader = $this->createReader();
+ }
+
+ /**
+ * Disconnects from the server and destroys the underlying resource and the
+ * protocol reader resource when PHP's garbage collector kicks in.
+ */
+ public function __destruct()
+ {
+ phpiredis_reader_destroy($this->reader);
+
+ parent::__destruct();
+ }
+
+ /**
+ * Checks if the socket and phpiredis extensions are loaded in PHP.
+ */
+ protected function assertExtensions()
+ {
+ if (!extension_loaded('sockets')) {
+ throw new NotSupportedException(
+ 'The "sockets" extension is required by this connection backend.'
+ );
+ }
+
+ if (!extension_loaded('phpiredis')) {
+ throw new NotSupportedException(
+ 'The "phpiredis" extension is required by this connection backend.'
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function assertParameters(ParametersInterface $parameters)
+ {
+ if (isset($parameters->persistent)) {
+ throw new NotSupportedException(
+ "Persistent connections are not supported by this connection backend."
+ );
+ }
+
+ return parent::assertParameters($parameters);
+ }
+
+ /**
+ * Creates a new instance of the protocol reader resource.
+ *
+ * @return resource
+ */
+ private function createReader()
+ {
+ $reader = phpiredis_reader_create();
+
+ phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
+ phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
+
+ return $reader;
+ }
+
+ /**
+ * Returns the underlying protocol reader resource.
+ *
+ * @return resource
+ */
+ protected function getReader()
+ {
+ return $this->reader;
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for inline responses.
+ *
+ * @return \Closure
+ */
+ private function getStatusHandler()
+ {
+ return function ($payload) {
+ return StatusResponse::get($payload);
+ };
+ }
+
+ /**
+ * Returns the handler used by the protocol reader for error responses.
+ *
+ * @return \Closure
+ */
+ protected function getErrorHandler()
+ {
+ return function ($payload) {
+ return new ErrorResponse($payload);
+ };
+ }
+
+ /**
+ * Helper method used to throw exceptions on socket errors.
+ */
+ private function emitSocketError()
+ {
+ $errno = socket_last_error();
+ $errstr = socket_strerror($errno);
+
+ $this->disconnect();
+
+ $this->onConnectionError(trim($errstr), $errno);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function createResource()
+ {
+ $isUnix = $this->parameters->scheme === 'unix';
+ $domain = $isUnix ? AF_UNIX : AF_INET;
+ $protocol = $isUnix ? 0 : SOL_TCP;
+
+ $socket = @call_user_func('socket_create', $domain, SOCK_STREAM, $protocol);
+
+ if (!is_resource($socket)) {
+ $this->emitSocketError();
+ }
+
+ $this->setSocketOptions($socket, $this->parameters);
+
+ return $socket;
+ }
+
+ /**
+ * Sets options on the socket resource from the connection parameters.
+ *
+ * @param resource $socket Socket resource.
+ * @param ParametersInterface $parameters Parameters used to initialize the connection.
+ */
+ private function setSocketOptions($socket, ParametersInterface $parameters)
+ {
+ if ($parameters->scheme !== 'tcp') {
+ return;
+ }
+
+ if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) {
+ $this->emitSocketError();
+ }
+
+ if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
+ $this->emitSocketError();
+ }
+
+ if (isset($parameters->read_write_timeout)) {
+ $rwtimeout = (float) $parameters->read_write_timeout;
+ $timeoutSec = floor($rwtimeout);
+ $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000;
+
+ $timeout = array(
+ 'sec' => $timeoutSec,
+ 'usec' => $timeoutUsec,
+ );
+
+ if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) {
+ $this->emitSocketError();
+ }
+
+ if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) {
+ $this->emitSocketError();
+ }
+ }
+ }
+
+ /**
+ * Gets the address from the connection parameters.
+ *
+ * @param ParametersInterface $parameters Parameters used to initialize the connection.
+ *
+ * @return string
+ */
+ protected static function getAddress(ParametersInterface $parameters)
+ {
+ if ($parameters->scheme === 'unix') {
+ return $parameters->path;
+ }
+
+ $host = $parameters->host;
+
+ if (ip2long($host) === false) {
+ if (false === $addresses = gethostbynamel($host)) {
+ return false;
+ }
+
+ return $addresses[array_rand($addresses)];
+ }
+
+ return $host;
+ }
+
+ /**
+ * Opens the actual connection to the server with a timeout.
+ *
+ * @param ParametersInterface $parameters Parameters used to initialize the connection.
+ *
+ * @return string
+ */
+ private function connectWithTimeout(ParametersInterface $parameters)
+ {
+ if (false === $host = self::getAddress($parameters)) {
+ $this->onConnectionError("Cannot resolve the address of '$parameters->host'.");
+ }
+
+ $socket = $this->getResource();
+
+ socket_set_nonblock($socket);
+
+ if (@socket_connect($socket, $host, (int) $parameters->port) === false) {
+ $error = socket_last_error();
+
+ if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) {
+ $this->emitSocketError();
+ }
+ }
+
+ socket_set_block($socket);
+
+ $null = null;
+ $selectable = array($socket);
+
+ $timeout = (float) $parameters->timeout;
+ $timeoutSecs = floor($timeout);
+ $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000;
+
+ $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs);
+
+ if ($selected === 2) {
+ $this->onConnectionError('Connection refused.', SOCKET_ECONNREFUSED);
+ }
+ if ($selected === 0) {
+ $this->onConnectionError('Connection timed out.', SOCKET_ETIMEDOUT);
+ }
+ if ($selected === false) {
+ $this->emitSocketError();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ if (parent::connect()) {
+ $this->connectWithTimeout($this->parameters);
+
+ if ($this->initCommands) {
+ foreach ($this->initCommands as $command) {
+ $this->executeCommand($command);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ if ($this->isConnected()) {
+ socket_close($this->getResource());
+ parent::disconnect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function write($buffer)
+ {
+ $socket = $this->getResource();
+
+ while (($length = strlen($buffer)) > 0) {
+ $written = socket_write($socket, $buffer, $length);
+
+ if ($length === $written) {
+ return;
+ }
+
+ if ($written === false) {
+ $this->onConnectionError('Error while writing bytes to the server.');
+ }
+
+ $buffer = substr($buffer, $written);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read()
+ {
+ $socket = $this->getResource();
+ $reader = $this->reader;
+
+ while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
+ if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '') {
+ $this->emitSocketError();
+ }
+
+ phpiredis_reader_feed($reader, $buffer);
+ }
+
+ if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
+ return phpiredis_reader_get_reply($reader);
+ } else {
+ $this->onProtocolError(phpiredis_reader_get_error($reader));
+
+ return;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ array_unshift($arguments, $command->getId());
+
+ $this->write(phpiredis_format_command($arguments));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __wakeup()
+ {
+ $this->assertExtensions();
+ $this->reader = $this->createReader();
+ }
+}
+
+/**
+ * Connection abstraction to Redis servers based on PHP's stream that uses an
+ * external protocol processor defining the protocol used for the communication.
+ *
+ * @author Daniele Alessandri
+ */
+class CompositeStreamConnection extends StreamConnection implements CompositeConnectionInterface
+{
+ protected $protocol;
+
+ /**
+ * @param ParametersInterface $parameters Initialization parameters for the connection.
+ * @param ProtocolProcessorInterface $protocol Protocol processor.
+ */
+ public function __construct(
+ ParametersInterface $parameters,
+ ProtocolProcessorInterface $protocol = null
+ ) {
+ $this->parameters = $this->assertParameters($parameters);
+ $this->protocol = $protocol ?: new TextProtocolProcessor();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProtocol()
+ {
+ return $this->protocol;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeBuffer($buffer)
+ {
+ $this->write($buffer);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readBuffer($length)
+ {
+ if ($length <= 0) {
+ throw new InvalidArgumentException('Length parameter must be greater than 0.');
+ }
+
+ $value = '';
+ $socket = $this->getResource();
+
+ do {
+ $chunk = fread($socket, $length);
+
+ if ($chunk === false || $chunk === '') {
+ $this->onConnectionError('Error while reading bytes from the server.');
+ }
+
+ $value .= $chunk;
+ } while (($length -= strlen($chunk)) > 0);
+
+ return $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readLine()
+ {
+ $value = '';
+ $socket = $this->getResource();
+
+ do {
+ $chunk = fgets($socket);
+
+ if ($chunk === false || $chunk === '') {
+ $this->onConnectionError('Error while reading line from the server.');
+ }
+
+ $value .= $chunk;
+ } while (substr($value, -2) !== "\r\n");
+
+ return substr($value, 0, -2);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $this->protocol->write($this, $command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read()
+ {
+ return $this->protocol->read($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep()
+ {
+ return array_merge(parent::__sleep(), array('protocol'));
+ }
+}
+
+/**
+ * Exception class that identifies connection-related errors.
+ *
+ * @author Daniele Alessandri
+ */
+class ConnectionException extends CommunicationException
+{
+}
+
+/**
+ * Container for connection parameters used to initialize connections to Redis.
+ *
+ * {@inheritdoc}
+ *
+ * @author Daniele Alessandri
+ */
+class Parameters implements ParametersInterface
+{
+ private $parameters;
+
+ private static $defaults = array(
+ 'scheme' => 'tcp',
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'timeout' => 5.0,
+ );
+
+ /**
+ * @param array $parameters Named array of connection parameters.
+ */
+ public function __construct(array $parameters = array())
+ {
+ $this->parameters = $this->filter($parameters) + $this->getDefaults();
+ }
+
+ /**
+ * Returns some default parameters with their values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return self::$defaults;
+ }
+
+ /**
+ * Creates a new instance by supplying the initial parameters either in the
+ * form of an URI string or a named array.
+ *
+ * @param array|string $parameters Set of connection parameters.
+ *
+ * @return Parameters
+ */
+ public static function create($parameters)
+ {
+ if (is_string($parameters)) {
+ $parameters = static::parse($parameters);
+ }
+
+ return new static($parameters ?: array());
+ }
+
+ /**
+ * Parses an URI string returning an array of connection parameters.
+ *
+ * @param string $uri URI string.
+ *
+ * @return array
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function parse($uri)
+ {
+ if (stripos($uri, 'unix') === 0) {
+ // Hack to support URIs for UNIX sockets with minimal effort.
+ $uri = str_ireplace('unix:///', 'unix://localhost/', $uri);
+ }
+
+ if (!($parsed = parse_url($uri)) || !isset($parsed['host'])) {
+ throw new InvalidArgumentException("Invalid parameters URI: $uri");
+ }
+
+ if (isset($parsed['query'])) {
+ parse_str($parsed['query'], $queryarray);
+ unset($parsed['query']);
+
+ $parsed = array_merge($parsed, $queryarray);
+ }
+
+ return $parsed;
+ }
+
+ /**
+ * Validates and converts each value of the connection parameters array.
+ *
+ * @param array $parameters Connection parameters.
+ *
+ * @return array
+ */
+ protected function filter(array $parameters)
+ {
+ return $parameters ?: array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __get($parameter)
+ {
+ if (isset($this->parameters[$parameter])) {
+ return $this->parameters[$parameter];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __isset($parameter)
+ {
+ return isset($this->parameters[$parameter]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toArray()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep()
+ {
+ return array('parameters');
+ }
+}
+
+/**
+ * Standard connection factory for creating connections to Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+class Factory implements FactoryInterface
+{
+ protected $schemes = array(
+ 'tcp' => 'Predis\Connection\StreamConnection',
+ 'unix' => 'Predis\Connection\StreamConnection',
+ 'http' => 'Predis\Connection\WebdisConnection',
+ );
+
+ /**
+ * Checks if the provided argument represents a valid connection class
+ * implementing Predis\Connection\NodeConnectionInterface. Optionally,
+ * callable objects are used for lazy initialization of connection objects.
+ *
+ * @param mixed $initializer FQN of a connection class or a callable for lazy initialization.
+ *
+ * @return mixed
+ *
+ * @throws \InvalidArgumentException
+ */
+ protected function checkInitializer($initializer)
+ {
+ if (is_callable($initializer)) {
+ return $initializer;
+ }
+
+ $class = new ReflectionClass($initializer);
+
+ if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) {
+ throw new InvalidArgumentException(
+ 'A connection initializer must be a valid connection class or a callable object.'
+ );
+ }
+
+ return $initializer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function define($scheme, $initializer)
+ {
+ $this->schemes[$scheme] = $this->checkInitializer($initializer);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function undefine($scheme)
+ {
+ unset($this->schemes[$scheme]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create($parameters)
+ {
+ if (!$parameters instanceof ParametersInterface) {
+ $parameters = $this->createParameters($parameters);
+ }
+
+ $scheme = $parameters->scheme;
+
+ if (!isset($this->schemes[$scheme])) {
+ throw new InvalidArgumentException("Unknown connection scheme: '$scheme'.");
+ }
+
+ $initializer = $this->schemes[$scheme];
+
+ if (is_callable($initializer)) {
+ $connection = call_user_func($initializer, $parameters, $this);
+ } else {
+ $connection = new $initializer($parameters);
+ $this->prepareConnection($connection);
+ }
+
+ if (!$connection instanceof NodeConnectionInterface) {
+ throw new UnexpectedValueException(
+ "Objects returned by connection initializers must implement ".
+ "'Predis\Connection\NodeConnectionInterface'."
+ );
+ }
+
+ return $connection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function aggregate(AggregateConnectionInterface $connection, array $parameters)
+ {
+ foreach ($parameters as $node) {
+ $connection->add($node instanceof NodeConnectionInterface ? $node : $this->create($node));
+ }
+ }
+
+ /**
+ * Creates a connection parameters instance from the supplied argument.
+ *
+ * @param mixed $parameters Original connection parameters.
+ *
+ * @return ParametersInterface
+ */
+ protected function createParameters($parameters)
+ {
+ return Parameters::create($parameters);
+ }
+
+ /**
+ * Prepares a connection instance after its initialization.
+ *
+ * @param NodeConnectionInterface $connection Connection instance.
+ */
+ protected function prepareConnection(NodeConnectionInterface $connection)
+ {
+ $parameters = $connection->getParameters();
+
+ if (isset($parameters->password)) {
+ $connection->addConnectCommand(
+ new RawCommand(array('AUTH', $parameters->password))
+ );
+ }
+
+ if (isset($parameters->database)) {
+ $connection->addConnectCommand(
+ new RawCommand(array('SELECT', $parameters->database))
+ );
+ }
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Profile;
+
+use InvalidArgumentException;
+use ReflectionClass;
+use Predis\ClientException;
+use Predis\Command\CommandInterface;
+use Predis\Command\Processor\ProcessorInterface;
+
+/**
+ * A profile defines all the features and commands supported by certain versions
+ * of Redis. Instances of Predis\Client should use a server profile matching the
+ * version of Redis being used.
+ *
+ * @author Daniele Alessandri
+ */
+interface ProfileInterface
+{
+ /**
+ * Returns the profile version corresponding to the Redis version.
+ *
+ * @return string
+ */
+ public function getVersion();
+
+ /**
+ * Checks if the profile supports the specified command.
+ *
+ * @param string $commandID Command ID.
+ *
+ * @return bool
+ */
+ public function supportsCommand($commandID);
+
+ /**
+ * Checks if the profile supports the specified list of commands.
+ *
+ * @param array $commandIDs List of command IDs.
+ *
+ * @return string
+ */
+ public function supportsCommands(array $commandIDs);
+
+ /**
+ * Creates a new command instance.
+ *
+ * @param string $commandID Command ID.
+ * @param array $arguments Arguments for the command.
+ *
+ * @return CommandInterface
+ */
+ public function createCommand($commandID, array $arguments = array());
+}
+
+/**
+ * Base class implementing common functionalities for Redis server profiles.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class RedisProfile implements ProfileInterface
+{
+ private $commands;
+ private $processor;
+
+ /**
+ *
+ */
+ public function __construct()
+ {
+ $this->commands = $this->getSupportedCommands();
+ }
+
+ /**
+ * Returns a map of all the commands supported by the profile and their
+ * actual PHP classes.
+ *
+ * @return array
+ */
+ abstract protected function getSupportedCommands();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsCommand($commandID)
+ {
+ return isset($this->commands[strtoupper($commandID)]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsCommands(array $commandIDs)
+ {
+ foreach ($commandIDs as $commandID) {
+ if (!$this->supportsCommand($commandID)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the fully-qualified name of a class representing the specified
+ * command ID registered in the current server profile.
+ *
+ * @param string $commandID Command ID.
+ *
+ * @return string|null
+ */
+ public function getCommandClass($commandID)
+ {
+ if (isset($this->commands[$commandID = strtoupper($commandID)])) {
+ return $this->commands[$commandID];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createCommand($commandID, array $arguments = array())
+ {
+ $commandID = strtoupper($commandID);
+
+ if (!isset($this->commands[$commandID])) {
+ throw new ClientException("Command '$commandID' is not a registered Redis command.");
+ }
+
+ $commandClass = $this->commands[$commandID];
+ $command = new $commandClass();
+ $command->setArguments($arguments);
+
+ if (isset($this->processor)) {
+ $this->processor->process($command);
+ }
+
+ return $command;
+ }
+
+ /**
+ * Defines a new command in the server profile.
+ *
+ * @param string $commandID Command ID.
+ * @param string $class Fully-qualified name of a Predis\Command\CommandInterface.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function defineCommand($commandID, $class)
+ {
+ $reflection = new ReflectionClass($class);
+
+ if (!$reflection->isSubclassOf('Predis\Command\CommandInterface')) {
+ throw new InvalidArgumentException("The class '$class' is not a valid command class.");
+ }
+
+ $this->commands[strtoupper($commandID)] = $class;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProcessor(ProcessorInterface $processor = null)
+ {
+ $this->processor = $processor;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessor()
+ {
+ return $this->processor;
+ }
+
+ /**
+ * Returns the version of server profile as its string representation.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getVersion();
+ }
+}
+
+/**
+ * Server profile for Redis 3.0.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion300 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '3.0';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+ 'DUMP' => 'Predis\Command\KeyDump',
+ 'RESTORE' => 'Predis\Command\KeyRestore',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfoV26x',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+
+ /* ---------------- Redis 2.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'PERSIST' => 'Predis\Command\KeyPersist',
+
+ /* commands operating on string values */
+ 'STRLEN' => 'Predis\Command\StringStrlen',
+ 'SETRANGE' => 'Predis\Command\StringSetRange',
+ 'GETRANGE' => 'Predis\Command\StringGetRange',
+ 'SETBIT' => 'Predis\Command\StringSetBit',
+ 'GETBIT' => 'Predis\Command\StringGetBit',
+
+ /* commands operating on lists */
+ 'RPUSHX' => 'Predis\Command\ListPushTailX',
+ 'LPUSHX' => 'Predis\Command\ListPushHeadX',
+ 'LINSERT' => 'Predis\Command\ListInsert',
+ 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+ /* transactions */
+ 'WATCH' => 'Predis\Command\TransactionWatch',
+ 'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+ /* remote server control commands */
+ 'OBJECT' => 'Predis\Command\ServerObject',
+ 'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+
+ /* ---------------- Redis 2.4 ---------------- */
+
+ /* remote server control commands */
+ 'CLIENT' => 'Predis\Command\ServerClient',
+
+ /* ---------------- Redis 2.6 ---------------- */
+
+ /* commands operating on the key space */
+ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive',
+ 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire',
+ 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt',
+
+ /* commands operating on string values */
+ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire',
+ 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat',
+ 'BITOP' => 'Predis\Command\StringBitOp',
+ 'BITCOUNT' => 'Predis\Command\StringBitCount',
+
+ /* commands operating on hashes */
+ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat',
+
+ /* scripting */
+ 'EVAL' => 'Predis\Command\ServerEval',
+ 'EVALSHA' => 'Predis\Command\ServerEvalSHA',
+ 'SCRIPT' => 'Predis\Command\ServerScript',
+
+ /* remote server control commands */
+ 'TIME' => 'Predis\Command\ServerTime',
+ 'SENTINEL' => 'Predis\Command\ServerSentinel',
+
+ /* ---------------- Redis 2.8 ---------------- */
+
+ /* commands operating on the key space */
+ 'SCAN' => 'Predis\Command\KeyScan',
+
+ /* commands operating on string values */
+ 'BITPOS' => 'Predis\Command\StringBitPos',
+
+ /* commands operating on sets */
+ 'SSCAN' => 'Predis\Command\SetScan',
+
+ /* commands operating on sorted sets */
+ 'ZSCAN' => 'Predis\Command\ZSetScan',
+ 'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount',
+ 'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex',
+ 'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex',
+
+ /* commands operating on hashes */
+ 'HSCAN' => 'Predis\Command\HashScan',
+
+ /* publish - subscribe */
+ 'PUBSUB' => 'Predis\Command\PubSubPubsub',
+
+ /* commands operating on HyperLogLog */
+ 'PFADD' => 'Predis\Command\HyperLogLogAdd',
+ 'PFCOUNT' => 'Predis\Command\HyperLogLogCount',
+ 'PFMERGE' => 'Predis\Command\HyperLogLogMerge',
+
+ /* remote server control commands */
+ 'COMMAND' => 'Predis\Command\ServerCommand',
+
+ /* ---------------- Redis 3.0 ---------------- */
+
+ );
+ }
+}
+
+/**
+ * Server profile for Redis 2.6.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion260 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '2.6';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+ 'DUMP' => 'Predis\Command\KeyDump',
+ 'RESTORE' => 'Predis\Command\KeyRestore',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfoV26x',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+
+ /* ---------------- Redis 2.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'PERSIST' => 'Predis\Command\KeyPersist',
+
+ /* commands operating on string values */
+ 'STRLEN' => 'Predis\Command\StringStrlen',
+ 'SETRANGE' => 'Predis\Command\StringSetRange',
+ 'GETRANGE' => 'Predis\Command\StringGetRange',
+ 'SETBIT' => 'Predis\Command\StringSetBit',
+ 'GETBIT' => 'Predis\Command\StringGetBit',
+
+ /* commands operating on lists */
+ 'RPUSHX' => 'Predis\Command\ListPushTailX',
+ 'LPUSHX' => 'Predis\Command\ListPushHeadX',
+ 'LINSERT' => 'Predis\Command\ListInsert',
+ 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+ /* transactions */
+ 'WATCH' => 'Predis\Command\TransactionWatch',
+ 'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+ /* remote server control commands */
+ 'OBJECT' => 'Predis\Command\ServerObject',
+ 'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+
+ /* ---------------- Redis 2.4 ---------------- */
+
+ /* remote server control commands */
+ 'CLIENT' => 'Predis\Command\ServerClient',
+
+ /* ---------------- Redis 2.6 ---------------- */
+
+ /* commands operating on the key space */
+ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive',
+ 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire',
+ 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt',
+
+ /* commands operating on string values */
+ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire',
+ 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat',
+ 'BITOP' => 'Predis\Command\StringBitOp',
+ 'BITCOUNT' => 'Predis\Command\StringBitCount',
+
+ /* commands operating on hashes */
+ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat',
+
+ /* scripting */
+ 'EVAL' => 'Predis\Command\ServerEval',
+ 'EVALSHA' => 'Predis\Command\ServerEvalSHA',
+ 'SCRIPT' => 'Predis\Command\ServerScript',
+
+ /* remote server control commands */
+ 'TIME' => 'Predis\Command\ServerTime',
+ 'SENTINEL' => 'Predis\Command\ServerSentinel',
+ );
+ }
+}
+
+/**
+ * Server profile for Redis 2.8.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion280 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '2.8';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+ 'DUMP' => 'Predis\Command\KeyDump',
+ 'RESTORE' => 'Predis\Command\KeyRestore',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfoV26x',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+
+ /* ---------------- Redis 2.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'PERSIST' => 'Predis\Command\KeyPersist',
+
+ /* commands operating on string values */
+ 'STRLEN' => 'Predis\Command\StringStrlen',
+ 'SETRANGE' => 'Predis\Command\StringSetRange',
+ 'GETRANGE' => 'Predis\Command\StringGetRange',
+ 'SETBIT' => 'Predis\Command\StringSetBit',
+ 'GETBIT' => 'Predis\Command\StringGetBit',
+
+ /* commands operating on lists */
+ 'RPUSHX' => 'Predis\Command\ListPushTailX',
+ 'LPUSHX' => 'Predis\Command\ListPushHeadX',
+ 'LINSERT' => 'Predis\Command\ListInsert',
+ 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+ /* transactions */
+ 'WATCH' => 'Predis\Command\TransactionWatch',
+ 'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+ /* remote server control commands */
+ 'OBJECT' => 'Predis\Command\ServerObject',
+ 'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+
+ /* ---------------- Redis 2.4 ---------------- */
+
+ /* remote server control commands */
+ 'CLIENT' => 'Predis\Command\ServerClient',
+
+ /* ---------------- Redis 2.6 ---------------- */
+
+ /* commands operating on the key space */
+ 'PTTL' => 'Predis\Command\KeyPreciseTimeToLive',
+ 'PEXPIRE' => 'Predis\Command\KeyPreciseExpire',
+ 'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt',
+
+ /* commands operating on string values */
+ 'PSETEX' => 'Predis\Command\StringPreciseSetExpire',
+ 'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat',
+ 'BITOP' => 'Predis\Command\StringBitOp',
+ 'BITCOUNT' => 'Predis\Command\StringBitCount',
+
+ /* commands operating on hashes */
+ 'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat',
+
+ /* scripting */
+ 'EVAL' => 'Predis\Command\ServerEval',
+ 'EVALSHA' => 'Predis\Command\ServerEvalSHA',
+ 'SCRIPT' => 'Predis\Command\ServerScript',
+
+ /* remote server control commands */
+ 'TIME' => 'Predis\Command\ServerTime',
+ 'SENTINEL' => 'Predis\Command\ServerSentinel',
+
+ /* ---------------- Redis 2.8 ---------------- */
+
+ /* commands operating on the key space */
+ 'SCAN' => 'Predis\Command\KeyScan',
+
+ /* commands operating on string values */
+ 'BITPOS' => 'Predis\Command\StringBitPos',
+
+ /* commands operating on sets */
+ 'SSCAN' => 'Predis\Command\SetScan',
+
+ /* commands operating on sorted sets */
+ 'ZSCAN' => 'Predis\Command\ZSetScan',
+ 'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount',
+ 'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex',
+ 'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex',
+
+ /* commands operating on hashes */
+ 'HSCAN' => 'Predis\Command\HashScan',
+
+ /* publish - subscribe */
+ 'PUBSUB' => 'Predis\Command\PubSubPubsub',
+
+ /* commands operating on HyperLogLog */
+ 'PFADD' => 'Predis\Command\HyperLogLogAdd',
+ 'PFCOUNT' => 'Predis\Command\HyperLogLogCount',
+ 'PFMERGE' => 'Predis\Command\HyperLogLogMerge',
+
+ /* remote server control commands */
+ 'COMMAND' => 'Predis\Command\ServerCommand',
+ );
+ }
+}
+
+/**
+ * Server profile for Redis 2.4.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion240 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '2.4';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfo',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+
+ /* ---------------- Redis 2.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'PERSIST' => 'Predis\Command\KeyPersist',
+
+ /* commands operating on string values */
+ 'STRLEN' => 'Predis\Command\StringStrlen',
+ 'SETRANGE' => 'Predis\Command\StringSetRange',
+ 'GETRANGE' => 'Predis\Command\StringGetRange',
+ 'SETBIT' => 'Predis\Command\StringSetBit',
+ 'GETBIT' => 'Predis\Command\StringGetBit',
+
+ /* commands operating on lists */
+ 'RPUSHX' => 'Predis\Command\ListPushTailX',
+ 'LPUSHX' => 'Predis\Command\ListPushHeadX',
+ 'LINSERT' => 'Predis\Command\ListInsert',
+ 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+ /* transactions */
+ 'WATCH' => 'Predis\Command\TransactionWatch',
+ 'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+ /* remote server control commands */
+ 'OBJECT' => 'Predis\Command\ServerObject',
+ 'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+
+ /* ---------------- Redis 2.4 ---------------- */
+
+ /* remote server control commands */
+ 'CLIENT' => 'Predis\Command\ServerClient',
+ );
+ }
+}
+
+/**
+ * Server profile for Redis 2.0.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion200 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '2.0';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfo',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+ );
+ }
+}
+
+/**
+ * Server profile for the current unstable version of Redis.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisUnstable extends RedisVersion300
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '3.0';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array_merge(parent::getSupportedCommands(), array());
+ }
+}
+
+/**
+ * Factory class for creating profile instances from strings.
+ *
+ * @author Daniele Alessandri
+ */
+final class Factory
+{
+ private static $profiles = array(
+ '2.0' => 'Predis\Profile\RedisVersion200',
+ '2.2' => 'Predis\Profile\RedisVersion220',
+ '2.4' => 'Predis\Profile\RedisVersion240',
+ '2.6' => 'Predis\Profile\RedisVersion260',
+ '2.8' => 'Predis\Profile\RedisVersion280',
+ '3.0' => 'Predis\Profile\RedisVersion300',
+ 'default' => 'Predis\Profile\RedisVersion300',
+ 'dev' => 'Predis\Profile\RedisUnstable',
+ );
+
+ /**
+ *
+ */
+ private function __construct()
+ {
+ // NOOP
+ }
+
+ /**
+ * Returns the default server profile.
+ *
+ * @return ProfileInterface
+ */
+ public static function getDefault()
+ {
+ return self::get('default');
+ }
+
+ /**
+ * Returns the development server profile.
+ *
+ * @return ProfileInterface
+ */
+ public static function getDevelopment()
+ {
+ return self::get('dev');
+ }
+
+ /**
+ * Registers a new server profile.
+ *
+ * @param string $alias Profile version or alias.
+ * @param string $class FQN of a class implementing Predis\Profile\ProfileInterface.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function define($alias, $class)
+ {
+ $reflection = new ReflectionClass($class);
+
+ if (!$reflection->isSubclassOf('Predis\Profile\ProfileInterface')) {
+ throw new InvalidArgumentException("The class '$class' is not a valid profile class.");
+ }
+
+ self::$profiles[$alias] = $class;
+ }
+
+ /**
+ * Returns the specified server profile.
+ *
+ * @param string $version Profile version or alias.
+ *
+ * @return ProfileInterface
+ *
+ * @throws ClientException
+ */
+ public static function get($version)
+ {
+ if (!isset(self::$profiles[$version])) {
+ throw new ClientException("Unknown server profile: '$version'.");
+ }
+
+ $profile = self::$profiles[$version];
+
+ return new $profile();
+ }
+}
+
+/**
+ * Server profile for Redis 2.2.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisVersion220 extends RedisProfile
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getVersion()
+ {
+ return '2.2';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedCommands()
+ {
+ return array(
+ /* ---------------- Redis 1.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'EXISTS' => 'Predis\Command\KeyExists',
+ 'DEL' => 'Predis\Command\KeyDelete',
+ 'TYPE' => 'Predis\Command\KeyType',
+ 'KEYS' => 'Predis\Command\KeyKeys',
+ 'RANDOMKEY' => 'Predis\Command\KeyRandom',
+ 'RENAME' => 'Predis\Command\KeyRename',
+ 'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+ 'EXPIRE' => 'Predis\Command\KeyExpire',
+ 'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+ 'TTL' => 'Predis\Command\KeyTimeToLive',
+ 'MOVE' => 'Predis\Command\KeyMove',
+ 'SORT' => 'Predis\Command\KeySort',
+
+ /* commands operating on string values */
+ 'SET' => 'Predis\Command\StringSet',
+ 'SETNX' => 'Predis\Command\StringSetPreserve',
+ 'MSET' => 'Predis\Command\StringSetMultiple',
+ 'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+ 'GET' => 'Predis\Command\StringGet',
+ 'MGET' => 'Predis\Command\StringGetMultiple',
+ 'GETSET' => 'Predis\Command\StringGetSet',
+ 'INCR' => 'Predis\Command\StringIncrement',
+ 'INCRBY' => 'Predis\Command\StringIncrementBy',
+ 'DECR' => 'Predis\Command\StringDecrement',
+ 'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+ /* commands operating on lists */
+ 'RPUSH' => 'Predis\Command\ListPushTail',
+ 'LPUSH' => 'Predis\Command\ListPushHead',
+ 'LLEN' => 'Predis\Command\ListLength',
+ 'LRANGE' => 'Predis\Command\ListRange',
+ 'LTRIM' => 'Predis\Command\ListTrim',
+ 'LINDEX' => 'Predis\Command\ListIndex',
+ 'LSET' => 'Predis\Command\ListSet',
+ 'LREM' => 'Predis\Command\ListRemove',
+ 'LPOP' => 'Predis\Command\ListPopFirst',
+ 'RPOP' => 'Predis\Command\ListPopLast',
+ 'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+ /* commands operating on sets */
+ 'SADD' => 'Predis\Command\SetAdd',
+ 'SREM' => 'Predis\Command\SetRemove',
+ 'SPOP' => 'Predis\Command\SetPop',
+ 'SMOVE' => 'Predis\Command\SetMove',
+ 'SCARD' => 'Predis\Command\SetCardinality',
+ 'SISMEMBER' => 'Predis\Command\SetIsMember',
+ 'SINTER' => 'Predis\Command\SetIntersection',
+ 'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+ 'SUNION' => 'Predis\Command\SetUnion',
+ 'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+ 'SDIFF' => 'Predis\Command\SetDifference',
+ 'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+ 'SMEMBERS' => 'Predis\Command\SetMembers',
+ 'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+ /* commands operating on sorted sets */
+ 'ZADD' => 'Predis\Command\ZSetAdd',
+ 'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+ 'ZREM' => 'Predis\Command\ZSetRemove',
+ 'ZRANGE' => 'Predis\Command\ZSetRange',
+ 'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+ 'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+ 'ZCARD' => 'Predis\Command\ZSetCardinality',
+ 'ZSCORE' => 'Predis\Command\ZSetScore',
+ 'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+ /* connection related commands */
+ 'PING' => 'Predis\Command\ConnectionPing',
+ 'AUTH' => 'Predis\Command\ConnectionAuth',
+ 'SELECT' => 'Predis\Command\ConnectionSelect',
+ 'ECHO' => 'Predis\Command\ConnectionEcho',
+ 'QUIT' => 'Predis\Command\ConnectionQuit',
+
+ /* remote server control commands */
+ 'INFO' => 'Predis\Command\ServerInfo',
+ 'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+ 'MONITOR' => 'Predis\Command\ServerMonitor',
+ 'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+ 'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+ 'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+ 'SAVE' => 'Predis\Command\ServerSave',
+ 'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+ 'LASTSAVE' => 'Predis\Command\ServerLastSave',
+ 'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+ 'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+ /* ---------------- Redis 2.0 ---------------- */
+
+ /* commands operating on string values */
+ 'SETEX' => 'Predis\Command\StringSetExpire',
+ 'APPEND' => 'Predis\Command\StringAppend',
+ 'SUBSTR' => 'Predis\Command\StringSubstr',
+
+ /* commands operating on lists */
+ 'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+ 'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+ 'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+ 'ZCOUNT' => 'Predis\Command\ZSetCount',
+ 'ZRANK' => 'Predis\Command\ZSetRank',
+ 'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+ 'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+ /* commands operating on hashes */
+ 'HSET' => 'Predis\Command\HashSet',
+ 'HSETNX' => 'Predis\Command\HashSetPreserve',
+ 'HMSET' => 'Predis\Command\HashSetMultiple',
+ 'HINCRBY' => 'Predis\Command\HashIncrementBy',
+ 'HGET' => 'Predis\Command\HashGet',
+ 'HMGET' => 'Predis\Command\HashGetMultiple',
+ 'HDEL' => 'Predis\Command\HashDelete',
+ 'HEXISTS' => 'Predis\Command\HashExists',
+ 'HLEN' => 'Predis\Command\HashLength',
+ 'HKEYS' => 'Predis\Command\HashKeys',
+ 'HVALS' => 'Predis\Command\HashValues',
+ 'HGETALL' => 'Predis\Command\HashGetAll',
+
+ /* transactions */
+ 'MULTI' => 'Predis\Command\TransactionMulti',
+ 'EXEC' => 'Predis\Command\TransactionExec',
+ 'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+ /* publish - subscribe */
+ 'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+ 'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+ 'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+ 'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+ 'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+ /* remote server control commands */
+ 'CONFIG' => 'Predis\Command\ServerConfig',
+
+ /* ---------------- Redis 2.2 ---------------- */
+
+ /* commands operating on the key space */
+ 'PERSIST' => 'Predis\Command\KeyPersist',
+
+ /* commands operating on string values */
+ 'STRLEN' => 'Predis\Command\StringStrlen',
+ 'SETRANGE' => 'Predis\Command\StringSetRange',
+ 'GETRANGE' => 'Predis\Command\StringGetRange',
+ 'SETBIT' => 'Predis\Command\StringSetBit',
+ 'GETBIT' => 'Predis\Command\StringGetBit',
+
+ /* commands operating on lists */
+ 'RPUSHX' => 'Predis\Command\ListPushTailX',
+ 'LPUSHX' => 'Predis\Command\ListPushHeadX',
+ 'LINSERT' => 'Predis\Command\ListInsert',
+ 'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+ /* commands operating on sorted sets */
+ 'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+ /* transactions */
+ 'WATCH' => 'Predis\Command\TransactionWatch',
+ 'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+ /* remote server control commands */
+ 'OBJECT' => 'Predis\Command\ServerObject',
+ 'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+ );
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis;
+
+use InvalidArgumentException;
+use UnexpectedValueException;
+use Predis\Command\CommandInterface;
+use Predis\Command\RawCommand;
+use Predis\Command\ScriptCommand;
+use Predis\Configuration\Options;
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\ConnectionInterface;
+use Predis\Connection\AggregateConnectionInterface;
+use Predis\Connection\ParametersInterface;
+use Predis\Monitor\Consumer as MonitorConsumer;
+use Predis\Pipeline\Pipeline;
+use Predis\PubSub\Consumer as PubSubConsumer;
+use Predis\Response\ErrorInterface as ErrorResponseInterface;
+use Predis\Response\ResponseInterface;
+use Predis\Response\ServerException;
+use Predis\Transaction\MultiExec as MultiExecTransaction;
+use Predis\Profile\ProfileInterface;
+use Exception;
+use Predis\Connection\NodeConnectionInterface;
+
+/**
+ * Base exception class for Predis-related errors.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class PredisException extends Exception
+{
+}
+
+/**
+ * Interface defining a client-side context such as a pipeline or transaction.
+ *
+ * @method $this del(array $keys)
+ * @method $this dump($key)
+ * @method $this exists($key)
+ * @method $this expire($key, $seconds)
+ * @method $this expireat($key, $timestamp)
+ * @method $this keys($pattern)
+ * @method $this move($key, $db)
+ * @method $this object($subcommand, $key)
+ * @method $this persist($key)
+ * @method $this pexpire($key, $milliseconds)
+ * @method $this pexpireat($key, $timestamp)
+ * @method $this pttl($key)
+ * @method $this randomkey()
+ * @method $this rename($key, $target)
+ * @method $this renamenx($key, $target)
+ * @method $this scan($cursor, array $options = null)
+ * @method $this sort($key, array $options = null)
+ * @method $this ttl($key)
+ * @method $this type($key)
+ * @method $this append($key, $value)
+ * @method $this bitcount($key, $start = null, $end = null)
+ * @method $this bitop($operation, $destkey, $key)
+ * @method $this decr($key)
+ * @method $this decrby($key, $decrement)
+ * @method $this get($key)
+ * @method $this getbit($key, $offset)
+ * @method $this getrange($key, $start, $end)
+ * @method $this getset($key, $value)
+ * @method $this incr($key)
+ * @method $this incrby($key, $increment)
+ * @method $this incrbyfloat($key, $increment)
+ * @method $this mget(array $keys)
+ * @method $this mset(array $dictionary)
+ * @method $this msetnx(array $dictionary)
+ * @method $this psetex($key, $milliseconds, $value)
+ * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
+ * @method $this setbit($key, $offset, $value)
+ * @method $this setex($key, $seconds, $value)
+ * @method $this setnx($key, $value)
+ * @method $this setrange($key, $offset, $value)
+ * @method $this strlen($key)
+ * @method $this hdel($key, array $fields)
+ * @method $this hexists($key, $field)
+ * @method $this hget($key, $field)
+ * @method $this hgetall($key)
+ * @method $this hincrby($key, $field, $increment)
+ * @method $this hincrbyfloat($key, $field, $increment)
+ * @method $this hkeys($key)
+ * @method $this hlen($key)
+ * @method $this hmget($key, array $fields)
+ * @method $this hmset($key, array $dictionary)
+ * @method $this hscan($key, $cursor, array $options = null)
+ * @method $this hset($key, $field, $value)
+ * @method $this hsetnx($key, $field, $value)
+ * @method $this hvals($key)
+ * @method $this blpop(array $keys, $timeout)
+ * @method $this brpop(array $keys, $timeout)
+ * @method $this brpoplpush($source, $destination, $timeout)
+ * @method $this lindex($key, $index)
+ * @method $this linsert($key, $whence, $pivot, $value)
+ * @method $this llen($key)
+ * @method $this lpop($key)
+ * @method $this lpush($key, array $values)
+ * @method $this lpushx($key, $value)
+ * @method $this lrange($key, $start, $stop)
+ * @method $this lrem($key, $count, $value)
+ * @method $this lset($key, $index, $value)
+ * @method $this ltrim($key, $start, $stop)
+ * @method $this rpop($key)
+ * @method $this rpoplpush($source, $destination)
+ * @method $this rpush($key, array $values)
+ * @method $this rpushx($key, $value)
+ * @method $this sadd($key, array $members)
+ * @method $this scard($key)
+ * @method $this sdiff(array $keys)
+ * @method $this sdiffstore($destination, array $keys)
+ * @method $this sinter(array $keys)
+ * @method $this sinterstore($destination, array $keys)
+ * @method $this sismember($key, $member)
+ * @method $this smembers($key)
+ * @method $this smove($source, $destination, $member)
+ * @method $this spop($key)
+ * @method $this srandmember($key, $count = null)
+ * @method $this srem($key, $member)
+ * @method $this sscan($key, $cursor, array $options = null)
+ * @method $this sunion(array $keys)
+ * @method $this sunionstore($destination, array $keys)
+ * @method $this zadd($key, array $membersAndScoresDictionary)
+ * @method $this zcard($key)
+ * @method $this zcount($key, $min, $max)
+ * @method $this zincrby($key, $increment, $member)
+ * @method $this zinterstore($destination, array $keys, array $options = null)
+ * @method $this zrange($key, $start, $stop, array $options = null)
+ * @method $this zrangebyscore($key, $min, $max, array $options = null)
+ * @method $this zrank($key, $member)
+ * @method $this zrem($key, $member)
+ * @method $this zremrangebyrank($key, $start, $stop)
+ * @method $this zremrangebyscore($key, $min, $max)
+ * @method $this zrevrange($key, $start, $stop, array $options = null)
+ * @method $this zrevrangebyscore($key, $min, $max, array $options = null)
+ * @method $this zrevrank($key, $member)
+ * @method $this zunionstore($destination, array $keys, array $options = null)
+ * @method $this zscore($key, $member)
+ * @method $this zscan($key, $cursor, array $options = null)
+ * @method $this zrangebylex($key, $start, $stop, array $options = null)
+ * @method $this zremrangebylex($key, $min, $max)
+ * @method $this zlexcount($key, $min, $max)
+ * @method $this pfadd($key, array $elements)
+ * @method $this pfmerge($destinationKey, array $sourceKeys)
+ * @method $this pfcount(array $keys)
+ * @method $this pubsub($subcommand, $argument)
+ * @method $this publish($channel, $message)
+ * @method $this discard()
+ * @method $this exec()
+ * @method $this multi()
+ * @method $this unwatch()
+ * @method $this watch($key)
+ * @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method $this script($subcommand, $argument = null)
+ * @method $this auth($password)
+ * @method $this echo($message)
+ * @method $this ping($message = null)
+ * @method $this select($database)
+ * @method $this bgrewriteaof()
+ * @method $this bgsave()
+ * @method $this client($subcommand, $argument = null)
+ * @method $this config($subcommand, $argument = null)
+ * @method $this dbsize()
+ * @method $this flushall()
+ * @method $this flushdb()
+ * @method $this info($section = null)
+ * @method $this lastsave()
+ * @method $this save()
+ * @method $this slaveof($host, $port)
+ * @method $this slowlog($subcommand, $argument = null)
+ * @method $this time()
+ * @method $this command()
+ *
+ * @author Daniele Alessandri
+ */
+interface ClientContextInterface
+{
+
+ /**
+ * Sends the specified command instance to Redis.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return mixed
+ */
+ public function executeCommand(CommandInterface $command);
+
+ /**
+ * Sends the specified command with its arguments to Redis.
+ *
+ * @param string $method Command ID.
+ * @param array $arguments Arguments for the command.
+ *
+ * @return mixed
+ */
+ public function __call($method, $arguments);
+
+ /**
+ * Starts the execution of the context.
+ *
+ * @param mixed $callable Optional callback for execution.
+ *
+ * @return array
+ */
+ public function execute($callable = null);
+}
+
+/**
+ * Base exception class for network-related errors.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class CommunicationException extends PredisException
+{
+ private $connection;
+
+ /**
+ * @param NodeConnectionInterface $connection Connection that generated the exception.
+ * @param string $message Error message.
+ * @param int $code Error code.
+ * @param Exception $innerException Inner exception for wrapping the original error.
+ */
+ public function __construct(
+ NodeConnectionInterface $connection,
+ $message = null,
+ $code = null,
+ Exception $innerException = null
+ ) {
+ parent::__construct($message, $code, $innerException);
+ $this->connection = $connection;
+ }
+
+ /**
+ * Gets the connection that generated the exception.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Indicates if the receiver should reset the underlying connection.
+ *
+ * @return bool
+ */
+ public function shouldResetConnection()
+ {
+ return true;
+ }
+
+ /**
+ * Helper method to handle exceptions generated by a connection object.
+ *
+ * @param CommunicationException $exception Exception.
+ *
+ * @throws CommunicationException
+ */
+ public static function handle(CommunicationException $exception)
+ {
+ if ($exception->shouldResetConnection()) {
+ $connection = $exception->getConnection();
+
+ if ($connection->isConnected()) {
+ $connection->disconnect();
+ }
+ }
+
+ throw $exception;
+ }
+}
+
+/**
+ * Interface defining a client able to execute commands against Redis.
+ *
+ * All the commands exposed by the client generally have the same signature as
+ * described by the Redis documentation, but some of them offer an additional
+ * and more friendly interface to ease programming which is described in the
+ * following list of methods:
+ *
+ * @method int del(array $keys)
+ * @method string dump($key)
+ * @method int exists($key)
+ * @method int expire($key, $seconds)
+ * @method int expireat($key, $timestamp)
+ * @method array keys($pattern)
+ * @method int move($key, $db)
+ * @method mixed object($subcommand, $key)
+ * @method int persist($key)
+ * @method int pexpire($key, $milliseconds)
+ * @method int pexpireat($key, $timestamp)
+ * @method int pttl($key)
+ * @method string randomkey()
+ * @method mixed rename($key, $target)
+ * @method int renamenx($key, $target)
+ * @method array scan($cursor, array $options = null)
+ * @method array sort($key, array $options = null)
+ * @method int ttl($key)
+ * @method mixed type($key)
+ * @method int append($key, $value)
+ * @method int bitcount($key, $start = null, $end = null)
+ * @method int bitop($operation, $destkey, $key)
+ * @method int decr($key)
+ * @method int decrby($key, $decrement)
+ * @method string get($key)
+ * @method int getbit($key, $offset)
+ * @method string getrange($key, $start, $end)
+ * @method string getset($key, $value)
+ * @method int incr($key)
+ * @method int incrby($key, $increment)
+ * @method string incrbyfloat($key, $increment)
+ * @method array mget(array $keys)
+ * @method mixed mset(array $dictionary)
+ * @method int msetnx(array $dictionary)
+ * @method mixed psetex($key, $milliseconds, $value)
+ * @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
+ * @method int setbit($key, $offset, $value)
+ * @method int setex($key, $seconds, $value)
+ * @method int setnx($key, $value)
+ * @method int setrange($key, $offset, $value)
+ * @method int strlen($key)
+ * @method int hdel($key, array $fields)
+ * @method int hexists($key, $field)
+ * @method string hget($key, $field)
+ * @method array hgetall($key)
+ * @method int hincrby($key, $field, $increment)
+ * @method string hincrbyfloat($key, $field, $increment)
+ * @method array hkeys($key)
+ * @method int hlen($key)
+ * @method array hmget($key, array $fields)
+ * @method mixed hmset($key, array $dictionary)
+ * @method array hscan($key, $cursor, array $options = null)
+ * @method int hset($key, $field, $value)
+ * @method int hsetnx($key, $field, $value)
+ * @method array hvals($key)
+ * @method array blpop(array $keys, $timeout)
+ * @method array brpop(array $keys, $timeout)
+ * @method array brpoplpush($source, $destination, $timeout)
+ * @method string lindex($key, $index)
+ * @method int linsert($key, $whence, $pivot, $value)
+ * @method int llen($key)
+ * @method string lpop($key)
+ * @method int lpush($key, array $values)
+ * @method int lpushx($key, $value)
+ * @method array lrange($key, $start, $stop)
+ * @method int lrem($key, $count, $value)
+ * @method mixed lset($key, $index, $value)
+ * @method mixed ltrim($key, $start, $stop)
+ * @method string rpop($key)
+ * @method string rpoplpush($source, $destination)
+ * @method int rpush($key, array $values)
+ * @method int rpushx($key, $value)
+ * @method int sadd($key, array $members)
+ * @method int scard($key)
+ * @method array sdiff(array $keys)
+ * @method int sdiffstore($destination, array $keys)
+ * @method array sinter(array $keys)
+ * @method int sinterstore($destination, array $keys)
+ * @method int sismember($key, $member)
+ * @method array smembers($key)
+ * @method int smove($source, $destination, $member)
+ * @method string spop($key)
+ * @method string srandmember($key, $count = null)
+ * @method int srem($key, $member)
+ * @method array sscan($key, $cursor, array $options = null)
+ * @method array sunion(array $keys)
+ * @method int sunionstore($destination, array $keys)
+ * @method int zadd($key, array $membersAndScoresDictionary)
+ * @method int zcard($key)
+ * @method string zcount($key, $min, $max)
+ * @method string zincrby($key, $increment, $member)
+ * @method int zinterstore($destination, array $keys, array $options = null)
+ * @method array zrange($key, $start, $stop, array $options = null)
+ * @method array zrangebyscore($key, $min, $max, array $options = null)
+ * @method int zrank($key, $member)
+ * @method int zrem($key, $member)
+ * @method int zremrangebyrank($key, $start, $stop)
+ * @method int zremrangebyscore($key, $min, $max)
+ * @method array zrevrange($key, $start, $stop, array $options = null)
+ * @method array zrevrangebyscore($key, $min, $max, array $options = null)
+ * @method int zrevrank($key, $member)
+ * @method int zunionstore($destination, array $keys, array $options = null)
+ * @method string zscore($key, $member)
+ * @method array zscan($key, $cursor, array $options = null)
+ * @method array zrangebylex($key, $start, $stop, array $options = null)
+ * @method int zremrangebylex($key, $min, $max)
+ * @method int zlexcount($key, $min, $max)
+ * @method int pfadd($key, array $elements)
+ * @method mixed pfmerge($destinationKey, array $sourceKeys)
+ * @method int pfcount(array $keys)
+ * @method mixed pubsub($subcommand, $argument)
+ * @method int publish($channel, $message)
+ * @method mixed discard()
+ * @method array exec()
+ * @method mixed multi()
+ * @method mixed unwatch()
+ * @method mixed watch($key)
+ * @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method mixed script($subcommand, $argument = null)
+ * @method mixed auth($password)
+ * @method string echo($message)
+ * @method mixed ping($message = null)
+ * @method mixed select($database)
+ * @method mixed bgrewriteaof()
+ * @method mixed bgsave()
+ * @method mixed client($subcommand, $argument = null)
+ * @method mixed config($subcommand, $argument = null)
+ * @method int dbsize()
+ * @method mixed flushall()
+ * @method mixed flushdb()
+ * @method array info($section = null)
+ * @method int lastsave()
+ * @method mixed save()
+ * @method mixed slaveof($host, $port)
+ * @method mixed slowlog($subcommand, $argument = null)
+ * @method array time()
+ * @method array command()
+ *
+ * @author Daniele Alessandri
+ */
+interface ClientInterface
+{
+ /**
+ * Returns the server profile used by the client.
+ *
+ * @return ProfileInterface
+ */
+ public function getProfile();
+
+ /**
+ * Returns the client options specified upon initialization.
+ *
+ * @return OptionsInterface
+ */
+ public function getOptions();
+
+ /**
+ * Opens the underlying connection to the server.
+ */
+ public function connect();
+
+ /**
+ * Closes the underlying connection from the server.
+ */
+ public function disconnect();
+
+ /**
+ * Returns the underlying connection instance.
+ *
+ * @return ConnectionInterface
+ */
+ public function getConnection();
+
+ /**
+ * Creates a new instance of the specified Redis command.
+ *
+ * @param string $method Command ID.
+ * @param array $arguments Arguments for the command.
+ *
+ * @return CommandInterface
+ */
+ public function createCommand($method, $arguments = array());
+
+ /**
+ * Executes the specified Redis command.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return mixed
+ */
+ public function executeCommand(CommandInterface $command);
+
+ /**
+ * Creates a Redis command with the specified arguments and sends a request
+ * to the server.
+ *
+ * @param string $method Command ID.
+ * @param array $arguments Arguments for the command.
+ *
+ * @return mixed
+ */
+ public function __call($method, $arguments);
+}
+
+/**
+ * Exception class thrown when trying to use features not supported by certain
+ * classes or abstractions of Predis.
+ *
+ * @author Daniele Alessandri
+ */
+class NotSupportedException extends PredisException
+{
+}
+
+/**
+ * Exception class that identifies client-side errors.
+ *
+ * @author Daniele Alessandri
+ */
+class ClientException extends PredisException
+{
+}
+
+/**
+ * Client class used for connecting and executing commands on Redis.
+ *
+ * This is the main high-level abstraction of Predis upon which various other
+ * abstractions are built. Internally it aggregates various other classes each
+ * one with its own responsibility and scope.
+ *
+ * {@inheritdoc}
+ *
+ * @author Daniele Alessandri
+ */
+class Client implements ClientInterface
+{
+ const VERSION = '1.0.1';
+
+ protected $connection;
+ protected $options;
+ private $profile;
+
+ /**
+ * @param mixed $parameters Connection parameters for one or more servers.
+ * @param mixed $options Options to configure some behaviours of the client.
+ */
+ public function __construct($parameters = null, $options = null)
+ {
+ $this->options = $this->createOptions($options ?: array());
+ $this->connection = $this->createConnection($parameters ?: array());
+ $this->profile = $this->options->profile;
+ }
+
+ /**
+ * Creates a new instance of Predis\Configuration\Options from different
+ * types of arguments or simply returns the passed argument if it is an
+ * instance of Predis\Configuration\OptionsInterface.
+ *
+ * @param mixed $options Client options.
+ *
+ * @return OptionsInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ protected function createOptions($options)
+ {
+ if (is_array($options)) {
+ return new Options($options);
+ }
+
+ if ($options instanceof OptionsInterface) {
+ return $options;
+ }
+
+ throw new InvalidArgumentException("Invalid type for client options.");
+ }
+
+ /**
+ * Creates single or aggregate connections from different types of arguments
+ * (string, array) or returns the passed argument if it is an instance of a
+ * class implementing Predis\Connection\ConnectionInterface.
+ *
+ * Accepted types for connection parameters are:
+ *
+ * - Instance of Predis\Connection\ConnectionInterface.
+ * - Instance of Predis\Connection\ParametersInterface.
+ * - Array
+ * - String
+ * - Callable
+ *
+ * @param mixed $parameters Connection parameters or connection instance.
+ *
+ * @return ConnectionInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ protected function createConnection($parameters)
+ {
+ if ($parameters instanceof ConnectionInterface) {
+ return $parameters;
+ }
+
+ if ($parameters instanceof ParametersInterface || is_string($parameters)) {
+ return $this->options->connections->create($parameters);
+ }
+
+ if (is_array($parameters)) {
+ if (!isset($parameters[0])) {
+ return $this->options->connections->create($parameters);
+ }
+
+ $options = $this->options;
+
+ if ($options->defined('aggregate')) {
+ $initializer = $this->getConnectionInitializerWrapper($options->aggregate);
+ $connection = $initializer($parameters, $options);
+ } else {
+ if ($options->defined('replication') && $replication = $options->replication) {
+ $connection = $replication;
+ } else {
+ $connection = $options->cluster;
+ }
+
+ $options->connections->aggregate($connection, $parameters);
+ }
+
+ return $connection;
+ }
+
+ if (is_callable($parameters)) {
+ $initializer = $this->getConnectionInitializerWrapper($parameters);
+ $connection = $initializer($this->options);
+
+ return $connection;
+ }
+
+ throw new InvalidArgumentException('Invalid type for connection parameters.');
+ }
+
+ /**
+ * Wraps a callable to make sure that its returned value represents a valid
+ * connection type.
+ *
+ * @param mixed $callable
+ *
+ * @return \Closure
+ */
+ protected function getConnectionInitializerWrapper($callable)
+ {
+ return function () use ($callable) {
+ $connection = call_user_func_array($callable, func_get_args());
+
+ if (!$connection instanceof ConnectionInterface) {
+ throw new UnexpectedValueException(
+ 'The callable connection initializer returned an invalid type.'
+ );
+ }
+
+ return $connection;
+ };
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProfile()
+ {
+ return $this->profile;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Creates a new client instance for the specified connection ID or alias,
+ * only when working with an aggregate connection (cluster, replication).
+ * The new client instances uses the same options of the original one.
+ *
+ * @param string $connectionID Identifier of a connection.
+ *
+ * @return Client
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getClientFor($connectionID)
+ {
+ if (!$connection = $this->getConnectionById($connectionID)) {
+ throw new InvalidArgumentException("Invalid connection ID: $connectionID.");
+ }
+
+ return new static($connection, $this->options);
+ }
+
+ /**
+ * Opens the underlying connection and connects to the server.
+ */
+ public function connect()
+ {
+ $this->connection->connect();
+ }
+
+ /**
+ * Closes the underlying connection and disconnects from the server.
+ */
+ public function disconnect()
+ {
+ $this->connection->disconnect();
+ }
+
+ /**
+ * Closes the underlying connection and disconnects from the server.
+ *
+ * This is the same as `Client::disconnect()` as it does not actually send
+ * the `QUIT` command to Redis, but simply closes the connection.
+ */
+ public function quit()
+ {
+ $this->disconnect();
+ }
+
+ /**
+ * Returns the current state of the underlying connection.
+ *
+ * @return bool
+ */
+ public function isConnected()
+ {
+ return $this->connection->isConnected();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Retrieves the specified connection from the aggregate connection when the
+ * client is in cluster or replication mode.
+ *
+ * @param string $connectionID Index or alias of the single connection.
+ *
+ * @return Connection\NodeConnectionInterface
+ *
+ * @throws NotSupportedException
+ */
+ public function getConnectionById($connectionID)
+ {
+ if (!$this->connection instanceof AggregateConnectionInterface) {
+ throw new NotSupportedException(
+ 'Retrieving connections by ID is supported only by aggregate connections.'
+ );
+ }
+
+ return $this->connection->getConnectionById($connectionID);
+ }
+
+ /**
+ * Executes a command without filtering its arguments, parsing the response,
+ * applying any prefix to keys or throwing exceptions on Redis errors even
+ * regardless of client options.
+ *
+ * It is possibile to indentify Redis error responses from normal responses
+ * using the second optional argument which is populated by reference.
+ *
+ * @param array $arguments Command arguments as defined by the command signature.
+ * @param bool $error Set to TRUE when Redis returned an error response.
+ *
+ * @return mixed
+ */
+ public function executeRaw(array $arguments, &$error = null)
+ {
+ $error = false;
+ $response = $this->connection->executeCommand(
+ new RawCommand($arguments)
+ );
+
+ if ($response instanceof ResponseInterface) {
+ if ($response instanceof ErrorResponseInterface) {
+ $error = true;
+ }
+
+ return (string) $response;
+ }
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __call($commandID, $arguments)
+ {
+ return $this->executeCommand(
+ $this->createCommand($commandID, $arguments)
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createCommand($commandID, $arguments = array())
+ {
+ return $this->profile->createCommand($commandID, $arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ $response = $this->connection->executeCommand($command);
+
+ if ($response instanceof ResponseInterface) {
+ if ($response instanceof ErrorResponseInterface) {
+ $response = $this->onErrorResponse($command, $response);
+ }
+
+ return $response;
+ }
+
+ return $command->parseResponse($response);
+ }
+
+ /**
+ * Handles -ERR responses returned by Redis.
+ *
+ * @param CommandInterface $command Redis command that generated the error.
+ * @param ErrorResponseInterface $response Instance of the error response.
+ *
+ * @return mixed
+ *
+ * @throws ServerException
+ */
+ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
+ {
+ if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
+ $eval = $this->createCommand('EVAL');
+ $eval->setRawArguments($command->getEvalArguments());
+
+ $response = $this->executeCommand($eval);
+
+ if (!$response instanceof ResponseInterface) {
+ $response = $command->parseResponse($response);
+ }
+
+ return $response;
+ }
+
+ if ($this->options->exceptions) {
+ throw new ServerException($response->getMessage());
+ }
+
+ return $response;
+ }
+
+ /**
+ * Executes the specified initializer method on `$this` by adjusting the
+ * actual invokation depending on the arity (0, 1 or 2 arguments). This is
+ * simply an utility method to create Redis contexts instances since they
+ * follow a common initialization path.
+ *
+ * @param string $initializer Method name.
+ * @param array $argv Arguments for the method.
+ *
+ * @return mixed
+ */
+ private function sharedContextFactory($initializer, $argv = null)
+ {
+ switch (count($argv)) {
+ case 0:
+ return $this->$initializer();
+
+ case 1:
+ return is_array($argv[0])
+ ? $this->$initializer($argv[0])
+ : $this->$initializer(null, $argv[0]);
+
+ case 2:
+ list($arg0, $arg1) = $argv;
+
+ return $this->$initializer($arg0, $arg1);
+
+ default:
+ return $this->$initializer($this, $argv);
+ }
+ }
+
+ /**
+ * Creates a new pipeline context and returns it, or returns the results of
+ * a pipeline executed inside the optionally provided callable object.
+ *
+ * @param mixed ... Array of options, a callable for execution, or both.
+ *
+ * @return Pipeline|array
+ */
+ public function pipeline(/* arguments */)
+ {
+ return $this->sharedContextFactory('createPipeline', func_get_args());
+ }
+
+ /**
+ * Actual pipeline context initializer method.
+ *
+ * @param array $options Options for the context.
+ * @param mixed $callable Optional callable used to execute the context.
+ *
+ * @return Pipeline|array
+ */
+ protected function createPipeline(array $options = null, $callable = null)
+ {
+ if (isset($options['atomic']) && $options['atomic']) {
+ $class = 'Predis\Pipeline\Atomic';
+ } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
+ $class = 'Predis\Pipeline\FireAndForget';
+ } else {
+ $class = 'Predis\Pipeline\Pipeline';
+ }
+
+ /*
+ * @var ClientContextInterface
+ */
+ $pipeline = new $class($this);
+
+ if (isset($callable)) {
+ return $pipeline->execute($callable);
+ }
+
+ return $pipeline;
+ }
+
+ /**
+ * Creates a new transaction context and returns it, or returns the results
+ * of a transaction executed inside the optionally provided callable object.
+ *
+ * @param mixed ... Array of options, a callable for execution, or both.
+ *
+ * @return MultiExecTransaction|array
+ */
+ public function transaction(/* arguments */)
+ {
+ return $this->sharedContextFactory('createTransaction', func_get_args());
+ }
+
+ /**
+ * Actual transaction context initializer method.
+ *
+ * @param array $options Options for the context.
+ * @param mixed $callable Optional callable used to execute the context.
+ *
+ * @return MultiExecTransaction|array
+ */
+ protected function createTransaction(array $options = null, $callable = null)
+ {
+ $transaction = new MultiExecTransaction($this, $options);
+
+ if (isset($callable)) {
+ return $transaction->execute($callable);
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Creates a new publis/subscribe context and returns it, or starts its loop
+ * inside the optionally provided callable object.
+ *
+ * @param mixed ... Array of options, a callable for execution, or both.
+ *
+ * @return PubSubConsumer|null
+ */
+ public function pubSubLoop(/* arguments */)
+ {
+ return $this->sharedContextFactory('createPubSub', func_get_args());
+ }
+
+ /**
+ * Actual publish/subscribe context initializer method.
+ *
+ * @param array $options Options for the context.
+ * @param mixed $callable Optional callable used to execute the context.
+ *
+ * @return PubSubConsumer|null
+ */
+ protected function createPubSub(array $options = null, $callable = null)
+ {
+ $pubsub = new PubSubConsumer($this, $options);
+
+ if (!isset($callable)) {
+ return $pubsub;
+ }
+
+ foreach ($pubsub as $message) {
+ if (call_user_func($callable, $pubsub, $message) === false) {
+ $pubsub->stop();
+ }
+ }
+ }
+
+ /**
+ * Creates a new monitor consumer and returns it.
+ *
+ * @return MonitorConsumer
+ */
+ public function monitor()
+ {
+ return new MonitorConsumer($this);
+ }
+}
+
+/**
+ * Implements a lightweight PSR-0 compliant autoloader for Predis.
+ *
+ * @author Eric Naeseth
+ * @author Daniele Alessandri
+ */
+class Autoloader
+{
+ private $directory;
+ private $prefix;
+ private $prefixLength;
+
+ /**
+ * @param string $baseDirectory Base directory where the source files are located.
+ */
+ public function __construct($baseDirectory = __DIR__)
+ {
+ $this->directory = $baseDirectory;
+ $this->prefix = __NAMESPACE__ . '\\';
+ $this->prefixLength = strlen($this->prefix);
+ }
+
+ /**
+ * Registers the autoloader class with the PHP SPL autoloader.
+ *
+ * @param bool $prepend Prepend the autoloader on the stack instead of appending it.
+ */
+ public static function register($prepend = false)
+ {
+ spl_autoload_register(array(new self, 'autoload'), true, $prepend);
+ }
+
+ /**
+ * Loads a class from a file using its fully qualified name.
+ *
+ * @param string $className Fully qualified name of a class.
+ */
+ public function autoload($className)
+ {
+ if (0 === strpos($className, $this->prefix)) {
+ $parts = explode('\\', substr($className, $this->prefixLength));
+ $filepath = $this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php';
+
+ if (is_file($filepath)) {
+ require($filepath);
+ }
+ }
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Configuration;
+
+use InvalidArgumentException;
+use Predis\Connection\Aggregate\ClusterInterface;
+use Predis\Connection\Aggregate\PredisCluster;
+use Predis\Connection\Aggregate\RedisCluster;
+use Predis\Connection\Factory;
+use Predis\Connection\FactoryInterface;
+use Predis\Command\Processor\KeyPrefixProcessor;
+use Predis\Command\Processor\ProcessorInterface;
+use Predis\Profile\Factory as Predis_Factory;
+use Predis\Profile\ProfileInterface;
+use Predis\Profile\RedisProfile;
+use Predis\Connection\Aggregate\MasterSlaveReplication;
+use Predis\Connection\Aggregate\ReplicationInterface;
+
+/**
+ * Defines an handler used by Predis\Configuration\Options to filter, validate
+ * or return default values for a given option.
+ *
+ * @author Daniele Alessandri
+ */
+interface OptionInterface
+{
+ /**
+ * Filters and validates the passed value.
+ *
+ * @param OptionsInterface $options Options container.
+ * @param mixed $value Input value.
+ *
+ * @return mixed
+ */
+ public function filter(OptionsInterface $options, $value);
+
+ /**
+ * Returns the default value for the option.
+ *
+ * @param OptionsInterface $options Options container.
+ *
+ * @return mixed
+ */
+ public function getDefault(OptionsInterface $options);
+}
+
+/**
+ * Interface defining a container for client options.
+ *
+ * @property-read mixed aggregate Custom connection aggregator.
+ * @property-read mixed cluster Aggregate connection for clustering.
+ * @property-read mixed connections Connection factory.
+ * @property-read mixed exceptions Toggles exceptions in client for -ERR responses.
+ * @property-read mixed prefix Key prefixing strategy using the given prefix.
+ * @property-read mixed profile Server profile.
+ * @property-read mixed replication Aggregate connection for replication.
+ *
+ * @author Daniele Alessandri
+ */
+interface OptionsInterface
+{
+ /**
+ * Returns the default value for the given option.
+ *
+ * @param string $option Name of the option.
+ *
+ * @return mixed|null
+ */
+ public function getDefault($option);
+
+ /**
+ * Checks if the given option has been set by the user upon initialization.
+ *
+ * @param string $option Name of the option.
+ *
+ * @return bool
+ */
+ public function defined($option);
+
+ /**
+ * Checks if the given option has been set and does not evaluate to NULL.
+ *
+ * @param string $option Name of the option.
+ *
+ * @return bool
+ */
+ public function __isset($option);
+
+ /**
+ * Returns the value of the given option.
+ *
+ * @param string $option Name of the option.
+ *
+ * @return mixed|null
+ */
+ public function __get($option);
+}
+
+/**
+ * Configures a command processor that apply the specified prefix string to a
+ * series of Redis commands considered prefixable.
+ *
+ * @author Daniele Alessandri
+ */
+class PrefixOption implements OptionInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ if ($value instanceof ProcessorInterface) {
+ return $value;
+ }
+
+ return new KeyPrefixProcessor($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ // NOOP
+ }
+}
+
+/**
+ * Configures the server profile to be used by the client to create command
+ * instances depending on the specified version of the Redis server.
+ *
+ * @author Daniele Alessandri
+ */
+class ProfileOption implements OptionInterface
+{
+ /**
+ * Sets the commands processors that need to be applied to the profile.
+ *
+ * @param OptionsInterface $options Client options.
+ * @param ProfileInterface $profile Server profile.
+ */
+ protected function setProcessors(OptionsInterface $options, ProfileInterface $profile)
+ {
+ if (isset($options->prefix) && $profile instanceof RedisProfile) {
+ // NOTE: directly using __get('prefix') is actually a workaround for
+ // HHVM 2.3.0. It's correct and respects the options interface, it's
+ // just ugly. We will remove this hack when HHVM will fix re-entrant
+ // calls to __get() once and for all.
+
+ $profile->setProcessor($options->__get('prefix'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ if (is_string($value)) {
+ $value = Predis_Factory::get($value);
+ $this->setProcessors($options, $value);
+ } elseif (!$value instanceof ProfileInterface) {
+ throw new InvalidArgumentException('Invalid value for the profile option.');
+ }
+
+ return $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ $profile = Predis_Factory::getDefault();
+ $this->setProcessors($options, $profile);
+
+ return $profile;
+ }
+}
+
+/**
+ * Configures an aggregate connection used for master/slave replication among
+ * multiple Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+class ReplicationOption implements OptionInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @todo There's more code than needed due to a bug in filter_var() as
+ * discussed here https://bugs.php.net/bug.php?id=49510 and different
+ * behaviours when encountering NULL values on PHP 5.3.
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ if ($value instanceof ReplicationInterface) {
+ return $value;
+ }
+
+ if (is_bool($value) || $value === null) {
+ return $value ? $this->getDefault($options) : null;
+ }
+
+ if (
+ !is_object($value) &&
+ null !== $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
+ ) {
+ return $asbool ? $this->getDefault($options) : null;
+ }
+
+ throw new InvalidArgumentException(
+ "An instance of type 'Predis\Connection\Aggregate\ReplicationInterface' was expected."
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return new MasterSlaveReplication();
+ }
+}
+
+/**
+ * Manages Predis options with filtering, conversion and lazy initialization of
+ * values using a mini-DI container approach.
+ *
+ * {@inheritdoc}
+ *
+ * @author Daniele Alessandri
+ */
+class Options implements OptionsInterface
+{
+ protected $input;
+ protected $options;
+ protected $handlers;
+
+ /**
+ * @param array $options Array of options with their values
+ */
+ public function __construct(array $options = array())
+ {
+ $this->input = $options;
+ $this->options = array();
+ $this->handlers = $this->getHandlers();
+ }
+
+ /**
+ * Ensures that the default options are initialized.
+ *
+ * @return array
+ */
+ protected function getHandlers()
+ {
+ return array(
+ 'cluster' => 'Predis\Configuration\ClusterOption',
+ 'connections' => 'Predis\Configuration\ConnectionFactoryOption',
+ 'exceptions' => 'Predis\Configuration\ExceptionsOption',
+ 'prefix' => 'Predis\Configuration\PrefixOption',
+ 'profile' => 'Predis\Configuration\ProfileOption',
+ 'replication' => 'Predis\Configuration\ReplicationOption',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault($option)
+ {
+ if (isset($this->handlers[$option])) {
+ $handler = $this->handlers[$option];
+ $handler = new $handler();
+
+ return $handler->getDefault($this);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defined($option)
+ {
+ return (
+ array_key_exists($option, $this->options) ||
+ array_key_exists($option, $this->input)
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __isset($option)
+ {
+ return (
+ array_key_exists($option, $this->options) ||
+ array_key_exists($option, $this->input)
+ ) && $this->__get($option) !== null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __get($option)
+ {
+ if (isset($this->options[$option]) || array_key_exists($option, $this->options)) {
+ return $this->options[$option];
+ }
+
+ if (isset($this->input[$option]) || array_key_exists($option, $this->input)) {
+ $value = $this->input[$option];
+ unset($this->input[$option]);
+
+ if (method_exists($value, '__invoke')) {
+ $value = $value($this, $option);
+ }
+
+ if (isset($this->handlers[$option])) {
+ $handler = $this->handlers[$option];
+ $handler = new $handler();
+ $value = $handler->filter($this, $value);
+ }
+
+ return $this->options[$option] = $value;
+ }
+
+ if (isset($this->handlers[$option])) {
+ return $this->options[$option] = $this->getDefault($option);
+ }
+
+ return null;
+ }
+}
+
+/**
+ * Configures a connection factory used by the client to create new connection
+ * instances for single Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+class ConnectionFactoryOption implements OptionInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ if ($value instanceof FactoryInterface) {
+ return $value;
+ } elseif (is_array($value)) {
+ $factory = $this->getDefault($options);
+
+ foreach ($value as $scheme => $initializer) {
+ $factory->define($scheme, $initializer);
+ }
+
+ return $factory;
+ } else {
+ throw new InvalidArgumentException(
+ 'Invalid value provided for the connections option.'
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return new Factory();
+ }
+}
+
+/**
+ * Configures whether consumers (such as the client) should throw exceptions on
+ * Redis errors (-ERR responses) or just return instances of error responses.
+ *
+ * @author Daniele Alessandri
+ */
+class ExceptionsOption implements OptionInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return true;
+ }
+}
+
+/**
+ * Configures an aggregate connection used for clustering
+ * multiple Redis nodes using various implementations with
+ * different algorithms or strategies.
+ *
+ * @author Daniele Alessandri
+ */
+class ClusterOption implements OptionInterface
+{
+ /**
+ * Creates a new cluster connection from on a known descriptive name.
+ *
+ * @param OptionsInterface $options Instance of the client options.
+ * @param string $id Descriptive identifier of the cluster type (`predis`, `redis-cluster`)
+ *
+ * @return ClusterInterface|null
+ */
+ protected function createByDescription(OptionsInterface $options, $id)
+ {
+ switch ($id) {
+ case 'predis':
+ case 'predis-cluster':
+ return new PredisCluster();
+
+ case 'redis':
+ case 'redis-cluster':
+ return new RedisCluster($options->connections);
+
+ default:
+ return;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(OptionsInterface $options, $value)
+ {
+ if (is_string($value)) {
+ $value = $this->createByDescription($options, $value);
+ }
+
+ if (!$value instanceof ClusterInterface) {
+ throw new InvalidArgumentException(
+ "An instance of type 'Predis\Connection\Aggregate\ClusterInterface' was expected."
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return new PredisCluster();
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Response;
+
+use Predis\PredisException;
+
+/**
+ * Represents a complex response object from Redis.
+ *
+ * @author Daniele Alessandri
+ */
+interface ResponseInterface
+{
+}
+
+/**
+ * Represents an error returned by Redis (responses identified by "-" in the
+ * Redis protocol) during the execution of an operation on the server.
+ *
+ * @author Daniele Alessandri
+ */
+interface ErrorInterface extends ResponseInterface
+{
+ /**
+ * Returns the error message
+ *
+ * @return string
+ */
+ public function getMessage();
+
+ /**
+ * Returns the error type (e.g. ERR, ASK, MOVED)
+ *
+ * @return string
+ */
+ public function getErrorType();
+}
+
+/**
+ * Represents a status response returned by Redis.
+ *
+ * @author Daniele Alessandri
+ */
+class Status implements ResponseInterface
+{
+ private static $OK;
+ private static $QUEUED;
+
+ private $payload;
+
+ /**
+ * @param string $payload Payload of the status response as returned by Redis.
+ */
+ public function __construct($payload)
+ {
+ $this->payload = $payload;
+ }
+
+ /**
+ * Converts the response object to its string representation.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->payload;
+ }
+
+ /**
+ * Returns the payload of status response.
+ *
+ * @return string
+ */
+ public function getPayload()
+ {
+ return $this->payload;
+ }
+
+ /**
+ * Returns an instance of a status response object.
+ *
+ * Common status responses such as OK or QUEUED are cached in order to lower
+ * the global memory usage especially when using pipelines.
+ *
+ * @param string $payload Status response payload.
+ *
+ * @return string
+ */
+ public static function get($payload)
+ {
+ switch ($payload) {
+ case 'OK':
+ case 'QUEUED':
+ if (isset(self::$$payload)) {
+ return self::$$payload;
+ }
+
+ return self::$$payload = new self($payload);
+
+ default:
+ return new self($payload);
+ }
+ }
+}
+
+/**
+ * Represents an error returned by Redis (-ERR responses) during the execution
+ * of a command on the server.
+ *
+ * @author Daniele Alessandri
+ */
+class Error implements ErrorInterface
+{
+ private $message;
+
+ /**
+ * @param string $message Error message returned by Redis
+ */
+ public function __construct($message)
+ {
+ $this->message = $message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrorType()
+ {
+ list($errorType, ) = explode(' ', $this->getMessage(), 2);
+
+ return $errorType;
+ }
+
+ /**
+ * Converts the object to its string representation.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getMessage();
+ }
+}
+
+/**
+ * Exception class that identifies server-side Redis errors.
+ *
+ * @author Daniele Alessandri
+ */
+class ServerException extends PredisException implements ErrorInterface
+{
+ /**
+ * Gets the type of the error returned by Redis.
+ *
+ * @return string
+ */
+ public function getErrorType()
+ {
+ list($errorType, ) = explode(' ', $this->getMessage(), 2);
+
+ return $errorType;
+ }
+
+ /**
+ * Converts the exception to an instance of Predis\Response\Error.
+ *
+ * @return Error
+ */
+ public function toErrorResponse()
+ {
+ return new Error($this->getMessage());
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Protocol\Text\Handler;
+
+use Predis\CommunicationException;
+use Predis\Connection\CompositeConnectionInterface;
+use Predis\Protocol\ProtocolException;
+use Predis\Response\Error;
+use Predis\Response\Status;
+use Predis\Response\Iterator\MultiBulk as MultiBulkIterator;
+
+/**
+ * Defines a pluggable handler used to parse a particular type of response.
+ *
+ * @author Daniele Alessandri
+ */
+interface ResponseHandlerInterface
+{
+ /**
+ * Deserializes a response returned by Redis and reads more data from the
+ * connection if needed.
+ *
+ * @param CompositeConnectionInterface $connection Redis connection.
+ * @param string $payload String payload.
+ *
+ * @return mixed
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload);
+}
+
+/**
+ * Handler for the status response type in the standard Redis wire protocol. It
+ * translates certain classes of status response to PHP objects or just returns
+ * the payload as a string.
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class StatusResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ return Status::get($payload);
+ }
+}
+
+/**
+ * Handler for the multibulk response type in the standard Redis wire protocol.
+ * It returns multibulk responses as iterators that can stream bulk elements.
+ *
+ * Streamable multibulk responses are not globally supported by the abstractions
+ * built-in into Predis, such as transactions or pipelines. Use them with care!
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class StreamableMultiBulkResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ $length = (int) $payload;
+
+ if ("$length" != $payload) {
+ CommunicationException::handle(new ProtocolException(
+ $connection, "Cannot parse '$payload' as a valid length for a multi-bulk response."
+ ));
+ }
+
+ return new MultiBulkIterator($connection, $length);
+ }
+}
+
+/**
+ * Handler for the multibulk response type in the standard Redis wire protocol.
+ * It returns multibulk responses as PHP arrays.
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class MultiBulkResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ $length = (int) $payload;
+
+ if ("$length" !== $payload) {
+ CommunicationException::handle(new ProtocolException(
+ $connection, "Cannot parse '$payload' as a valid length of a multi-bulk response."
+ ));
+ }
+
+ if ($length === -1) {
+ return null;
+ }
+
+ $list = array();
+
+ if ($length > 0) {
+ $handlersCache = array();
+ $reader = $connection->getProtocol()->getResponseReader();
+
+ for ($i = 0; $i < $length; $i++) {
+ $header = $connection->readLine();
+ $prefix = $header[0];
+
+ if (isset($handlersCache[$prefix])) {
+ $handler = $handlersCache[$prefix];
+ } else {
+ $handler = $reader->getHandler($prefix);
+ $handlersCache[$prefix] = $handler;
+ }
+
+ $list[$i] = $handler->handle($connection, substr($header, 1));
+ }
+ }
+
+ return $list;
+ }
+}
+
+/**
+ * Handler for the error response type in the standard Redis wire protocol.
+ * It translates the payload to a complex response object for Predis.
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class ErrorResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ return new Error($payload);
+ }
+}
+
+/**
+ * Handler for the integer response type in the standard Redis wire protocol.
+ * It translates the payload an integer or NULL.
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class IntegerResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ if (is_numeric($payload)) {
+ return (int) $payload;
+ }
+
+ if ($payload !== 'nil') {
+ CommunicationException::handle(new ProtocolException(
+ $connection, "Cannot parse '$payload' as a valid numeric response."
+ ));
+ }
+
+ return null;
+ }
+}
+
+/**
+ * Handler for the bulk response type in the standard Redis wire protocol.
+ * It translates the payload to a string or a NULL.
+ *
+ * @link http://redis.io/topics/protocol
+ * @author Daniele Alessandri
+ */
+class BulkResponse implements ResponseHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(CompositeConnectionInterface $connection, $payload)
+ {
+ $length = (int) $payload;
+
+ if ("$length" !== $payload) {
+ CommunicationException::handle(new ProtocolException(
+ $connection, "Cannot parse '$payload' as a valid length for a bulk response."
+ ));
+ }
+
+ if ($length >= 0) {
+ return substr($connection->readBuffer($length + 2), 0, -2);
+ }
+
+ if ($length == -1) {
+ return null;
+ }
+
+ CommunicationException::handle(new ProtocolException(
+ $connection, "Value '$payload' is not a valid length for a bulk response."
+ ));
+
+ return;
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Collection\Iterator;
+
+use Iterator;
+use Predis\ClientInterface;
+use Predis\NotSupportedException;
+use InvalidArgumentException;
+
+/**
+ * Provides the base implementation for a fully-rewindable PHP iterator that can
+ * incrementally iterate over cursor-based collections stored on Redis using the
+ * commands in the `SCAN` family.
+ *
+ * Given their incremental nature with multiple fetches, these kind of iterators
+ * offer limited guarantees about the returned elements because the collection
+ * can change several times during the iteration process.
+ *
+ * @see http://redis.io/commands/scan
+ *
+ * @author Daniele Alessandri
+ */
+abstract class CursorBasedIterator implements Iterator
+{
+ protected $client;
+ protected $match;
+ protected $count;
+
+ protected $valid;
+ protected $fetchmore;
+ protected $elements;
+ protected $cursor;
+ protected $position;
+ protected $current;
+
+ /**
+ * @param ClientInterface $client Client connected to Redis.
+ * @param string $match Pattern to match during the server-side iteration.
+ * @param int $count Hint used by Redis to compute the number of results per iteration.
+ */
+ public function __construct(ClientInterface $client, $match = null, $count = null)
+ {
+ $this->client = $client;
+ $this->match = $match;
+ $this->count = $count;
+
+ $this->reset();
+ }
+
+ /**
+ * Ensures that the client supports the specified Redis command required to
+ * fetch elements from the server to perform the iteration.
+ *
+ * @param ClientInterface $client Client connected to Redis.
+ * @param string $commandID Command ID.
+ *
+ * @throws NotSupportedException
+ */
+ protected function requiredCommand(ClientInterface $client, $commandID)
+ {
+ if (!$client->getProfile()->supportsCommand($commandID)) {
+ throw new NotSupportedException("The current profile does not support '$commandID'.");
+ }
+ }
+
+ /**
+ * Resets the inner state of the iterator.
+ */
+ protected function reset()
+ {
+ $this->valid = true;
+ $this->fetchmore = true;
+ $this->elements = array();
+ $this->cursor = 0;
+ $this->position = -1;
+ $this->current = null;
+ }
+
+ /**
+ * Returns an array of options for the `SCAN` command.
+ *
+ * @return array
+ */
+ protected function getScanOptions()
+ {
+ $options = array();
+
+ if (strlen($this->match) > 0) {
+ $options['MATCH'] = $this->match;
+ }
+
+ if ($this->count > 0) {
+ $options['COUNT'] = $this->count;
+ }
+
+ return $options;
+ }
+
+ /**
+ * Fetches a new set of elements from the remote collection, effectively
+ * advancing the iteration process.
+ *
+ * @return array
+ */
+ abstract protected function executeCommand();
+
+ /**
+ * Populates the local buffer of elements fetched from the server during
+ * the iteration.
+ */
+ protected function fetch()
+ {
+ list($cursor, $elements) = $this->executeCommand();
+
+ if (!$cursor) {
+ $this->fetchmore = false;
+ }
+
+ $this->cursor = $cursor;
+ $this->elements = $elements;
+ }
+
+ /**
+ * Extracts next values for key() and current().
+ */
+ protected function extractNext()
+ {
+ $this->position++;
+ $this->current = array_shift($this->elements);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->reset();
+ $this->next();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->current;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ tryFetch: {
+ if (!$this->elements && $this->fetchmore) {
+ $this->fetch();
+ }
+
+ if ($this->elements) {
+ $this->extractNext();
+ } elseif ($this->cursor) {
+ goto tryFetch;
+ } else {
+ $this->valid = false;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->valid;
+ }
+}
+
+/**
+ * Abstracts the iteration of members stored in a sorted set by leveraging the
+ * ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri
+ * @link http://redis.io/commands/scan
+ */
+class SortedSetKey extends CursorBasedIterator
+{
+ protected $key;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+ {
+ $this->requiredCommand($client, 'ZSCAN');
+
+ parent::__construct($client, $match, $count);
+
+ $this->key = $key;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeCommand()
+ {
+ return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractNext()
+ {
+ if ($kv = each($this->elements)) {
+ $this->position = $kv[0];
+ $this->current = $kv[1];
+
+ unset($this->elements[$this->position]);
+ }
+ }
+}
+
+/**
+ * Abstracts the iteration of members stored in a set by leveraging the SSCAN
+ * command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri
+ * @link http://redis.io/commands/scan
+ */
+class SetKey extends CursorBasedIterator
+{
+ protected $key;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+ {
+ $this->requiredCommand($client, 'SSCAN');
+
+ parent::__construct($client, $match, $count);
+
+ $this->key = $key;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeCommand()
+ {
+ return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions());
+ }
+}
+
+/**
+ * Abstracts the iteration of the keyspace on a Redis instance by leveraging the
+ * SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri
+ * @link http://redis.io/commands/scan
+ */
+class Keyspace extends CursorBasedIterator
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ClientInterface $client, $match = null, $count = null)
+ {
+ $this->requiredCommand($client, 'SCAN');
+
+ parent::__construct($client, $match, $count);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeCommand()
+ {
+ return $this->client->scan($this->cursor, $this->getScanOptions());
+ }
+}
+
+/**
+ * Abstracts the iteration of fields and values of an hash by leveraging the
+ * HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri
+ * @link http://redis.io/commands/scan
+ */
+class HashKey extends CursorBasedIterator
+{
+ protected $key;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+ {
+ $this->requiredCommand($client, 'HSCAN');
+
+ parent::__construct($client, $match, $count);
+
+ $this->key = $key;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeCommand()
+ {
+ return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function extractNext()
+ {
+ $this->position = key($this->elements);
+ $this->current = array_shift($this->elements);
+ }
+}
+
+/**
+ * Abstracts the iteration of items stored in a list by leveraging the LRANGE
+ * command wrapped in a fully-rewindable PHP iterator.
+ *
+ * This iterator tries to emulate the behaviour of cursor-based iterators based
+ * on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due
+ * to its incremental nature with multiple fetches it can only offer limited
+ * guarantees on the returned elements because the collection can change several
+ * times (trimmed, deleted, overwritten) during the iteration process.
+ *
+ * @author Daniele Alessandri
+ * @link http://redis.io/commands/lrange
+ */
+class ListKey implements Iterator
+{
+ protected $client;
+ protected $count;
+ protected $key;
+
+ protected $valid;
+ protected $fetchmore;
+ protected $elements;
+ protected $position;
+ protected $current;
+
+ /**
+ * @param ClientInterface $client Client connected to Redis.
+ * @param string $key Redis list key.
+ * @param int $count Number of items retrieved on each fetch operation.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(ClientInterface $client, $key, $count = 10)
+ {
+ $this->requiredCommand($client, 'LRANGE');
+
+ if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
+ throw new InvalidArgumentException('The $count argument must be a positive integer.');
+ }
+
+ $this->client = $client;
+ $this->key = $key;
+ $this->count = $count;
+
+ $this->reset();
+ }
+
+ /**
+ * Ensures that the client instance supports the specified Redis command
+ * required to fetch elements from the server to perform the iteration.
+ *
+ * @param ClientInterface $client Client connected to Redis.
+ * @param string $commandID Command ID.
+ *
+ * @throws NotSupportedException
+ */
+ protected function requiredCommand(ClientInterface $client, $commandID)
+ {
+ if (!$client->getProfile()->supportsCommand($commandID)) {
+ throw new NotSupportedException("The current profile does not support '$commandID'.");
+ }
+ }
+
+ /**
+ * Resets the inner state of the iterator.
+ */
+ protected function reset()
+ {
+ $this->valid = true;
+ $this->fetchmore = true;
+ $this->elements = array();
+ $this->position = -1;
+ $this->current = null;
+ }
+
+ /**
+ * Fetches a new set of elements from the remote collection, effectively
+ * advancing the iteration process.
+ *
+ * @return array
+ */
+ protected function executeCommand()
+ {
+ return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count);
+ }
+
+ /**
+ * Populates the local buffer of elements fetched from the server during the
+ * iteration.
+ */
+ protected function fetch()
+ {
+ $elements = $this->executeCommand();
+
+ if (count($elements) < $this->count) {
+ $this->fetchmore = false;
+ }
+
+ $this->elements = $elements;
+ }
+
+ /**
+ * Extracts next values for key() and current().
+ */
+ protected function extractNext()
+ {
+ $this->position++;
+ $this->current = array_shift($this->elements);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->reset();
+ $this->next();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->current;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ if (!$this->elements && $this->fetchmore) {
+ $this->fetch();
+ }
+
+ if ($this->elements) {
+ $this->extractNext();
+ } else {
+ $this->valid = false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->valid;
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Cluster;
+
+use InvalidArgumentException;
+use Predis\Command\CommandInterface;
+use Predis\Command\ScriptCommand;
+use Predis\Cluster\Distributor\DistributorInterface;
+use Predis\Cluster\Distributor\HashRing;
+use Predis\NotSupportedException;
+use Predis\Cluster\Hash\HashGeneratorInterface;
+use Predis\Cluster\Hash\CRC16;
+
+/**
+ * Interface for classes defining the strategy used to calculate an hash out of
+ * keys extracted from supported commands.
+ *
+ * This is mostly useful to support clustering via client-side sharding.
+ *
+ * @author Daniele Alessandri
+ */
+interface StrategyInterface
+{
+ /**
+ * Returns a slot for the given command used for clustering distribution or
+ * NULL when this is not possible.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return int
+ */
+ public function getSlot(CommandInterface $command);
+
+ /**
+ * Returns a slot for the given key used for clustering distribution or NULL
+ * when this is not possible.
+ *
+ * @param string $key Key string.
+ *
+ * @return int
+ */
+ public function getSlotByKey($key);
+
+ /**
+ * Returns a distributor instance to be used by the cluster.
+ *
+ * @return DistributorInterface
+ */
+ public function getDistributor();
+}
+
+/**
+ * Common class implementing the logic needed to support clustering strategies.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class ClusterStrategy implements StrategyInterface
+{
+ protected $commands;
+
+ /**
+ *
+ */
+ public function __construct()
+ {
+ $this->commands = $this->getDefaultCommands();
+ }
+
+ /**
+ * Returns the default map of supported commands with their handlers.
+ *
+ * @return array
+ */
+ protected function getDefaultCommands()
+ {
+ $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
+ $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
+
+ return array(
+ /* commands operating on the key space */
+ 'EXISTS' => $getKeyFromFirstArgument,
+ 'DEL' => $getKeyFromAllArguments,
+ 'TYPE' => $getKeyFromFirstArgument,
+ 'EXPIRE' => $getKeyFromFirstArgument,
+ 'EXPIREAT' => $getKeyFromFirstArgument,
+ 'PERSIST' => $getKeyFromFirstArgument,
+ 'PEXPIRE' => $getKeyFromFirstArgument,
+ 'PEXPIREAT' => $getKeyFromFirstArgument,
+ 'TTL' => $getKeyFromFirstArgument,
+ 'PTTL' => $getKeyFromFirstArgument,
+ 'SORT' => $getKeyFromFirstArgument, // TODO
+ 'DUMP' => $getKeyFromFirstArgument,
+ 'RESTORE' => $getKeyFromFirstArgument,
+
+ /* commands operating on string values */
+ 'APPEND' => $getKeyFromFirstArgument,
+ 'DECR' => $getKeyFromFirstArgument,
+ 'DECRBY' => $getKeyFromFirstArgument,
+ 'GET' => $getKeyFromFirstArgument,
+ 'GETBIT' => $getKeyFromFirstArgument,
+ 'MGET' => $getKeyFromAllArguments,
+ 'SET' => $getKeyFromFirstArgument,
+ 'GETRANGE' => $getKeyFromFirstArgument,
+ 'GETSET' => $getKeyFromFirstArgument,
+ 'INCR' => $getKeyFromFirstArgument,
+ 'INCRBY' => $getKeyFromFirstArgument,
+ 'INCRBYFLOAT' => $getKeyFromFirstArgument,
+ 'SETBIT' => $getKeyFromFirstArgument,
+ 'SETEX' => $getKeyFromFirstArgument,
+ 'MSET' => array($this, 'getKeyFromInterleavedArguments'),
+ 'MSETNX' => array($this, 'getKeyFromInterleavedArguments'),
+ 'SETNX' => $getKeyFromFirstArgument,
+ 'SETRANGE' => $getKeyFromFirstArgument,
+ 'STRLEN' => $getKeyFromFirstArgument,
+ 'SUBSTR' => $getKeyFromFirstArgument,
+ 'BITOP' => array($this, 'getKeyFromBitOp'),
+ 'BITCOUNT' => $getKeyFromFirstArgument,
+
+ /* commands operating on lists */
+ 'LINSERT' => $getKeyFromFirstArgument,
+ 'LINDEX' => $getKeyFromFirstArgument,
+ 'LLEN' => $getKeyFromFirstArgument,
+ 'LPOP' => $getKeyFromFirstArgument,
+ 'RPOP' => $getKeyFromFirstArgument,
+ 'RPOPLPUSH' => $getKeyFromAllArguments,
+ 'BLPOP' => array($this, 'getKeyFromBlockingListCommands'),
+ 'BRPOP' => array($this, 'getKeyFromBlockingListCommands'),
+ 'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'),
+ 'LPUSH' => $getKeyFromFirstArgument,
+ 'LPUSHX' => $getKeyFromFirstArgument,
+ 'RPUSH' => $getKeyFromFirstArgument,
+ 'RPUSHX' => $getKeyFromFirstArgument,
+ 'LRANGE' => $getKeyFromFirstArgument,
+ 'LREM' => $getKeyFromFirstArgument,
+ 'LSET' => $getKeyFromFirstArgument,
+ 'LTRIM' => $getKeyFromFirstArgument,
+
+ /* commands operating on sets */
+ 'SADD' => $getKeyFromFirstArgument,
+ 'SCARD' => $getKeyFromFirstArgument,
+ 'SDIFF' => $getKeyFromAllArguments,
+ 'SDIFFSTORE' => $getKeyFromAllArguments,
+ 'SINTER' => $getKeyFromAllArguments,
+ 'SINTERSTORE' => $getKeyFromAllArguments,
+ 'SUNION' => $getKeyFromAllArguments,
+ 'SUNIONSTORE' => $getKeyFromAllArguments,
+ 'SISMEMBER' => $getKeyFromFirstArgument,
+ 'SMEMBERS' => $getKeyFromFirstArgument,
+ 'SSCAN' => $getKeyFromFirstArgument,
+ 'SPOP' => $getKeyFromFirstArgument,
+ 'SRANDMEMBER' => $getKeyFromFirstArgument,
+ 'SREM' => $getKeyFromFirstArgument,
+
+ /* commands operating on sorted sets */
+ 'ZADD' => $getKeyFromFirstArgument,
+ 'ZCARD' => $getKeyFromFirstArgument,
+ 'ZCOUNT' => $getKeyFromFirstArgument,
+ 'ZINCRBY' => $getKeyFromFirstArgument,
+ 'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
+ 'ZRANGE' => $getKeyFromFirstArgument,
+ 'ZRANGEBYSCORE' => $getKeyFromFirstArgument,
+ 'ZRANK' => $getKeyFromFirstArgument,
+ 'ZREM' => $getKeyFromFirstArgument,
+ 'ZREMRANGEBYRANK' => $getKeyFromFirstArgument,
+ 'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument,
+ 'ZREVRANGE' => $getKeyFromFirstArgument,
+ 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument,
+ 'ZREVRANK' => $getKeyFromFirstArgument,
+ 'ZSCORE' => $getKeyFromFirstArgument,
+ 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
+ 'ZSCAN' => $getKeyFromFirstArgument,
+ 'ZLEXCOUNT' => $getKeyFromFirstArgument,
+ 'ZRANGEBYLEX' => $getKeyFromFirstArgument,
+ 'ZREMRANGEBYLEX' => $getKeyFromFirstArgument,
+
+ /* commands operating on hashes */
+ 'HDEL' => $getKeyFromFirstArgument,
+ 'HEXISTS' => $getKeyFromFirstArgument,
+ 'HGET' => $getKeyFromFirstArgument,
+ 'HGETALL' => $getKeyFromFirstArgument,
+ 'HMGET' => $getKeyFromFirstArgument,
+ 'HMSET' => $getKeyFromFirstArgument,
+ 'HINCRBY' => $getKeyFromFirstArgument,
+ 'HINCRBYFLOAT' => $getKeyFromFirstArgument,
+ 'HKEYS' => $getKeyFromFirstArgument,
+ 'HLEN' => $getKeyFromFirstArgument,
+ 'HSET' => $getKeyFromFirstArgument,
+ 'HSETNX' => $getKeyFromFirstArgument,
+ 'HVALS' => $getKeyFromFirstArgument,
+ 'HSCAN' => $getKeyFromFirstArgument,
+
+ /* commands operating on HyperLogLog */
+ 'PFADD' => $getKeyFromFirstArgument,
+ 'PFCOUNT' => $getKeyFromAllArguments,
+ 'PFMERGE' => $getKeyFromAllArguments,
+
+ /* scripting */
+ 'EVAL' => array($this, 'getKeyFromScriptingCommands'),
+ 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'),
+ );
+ }
+
+ /**
+ * Returns the list of IDs for the supported commands.
+ *
+ * @return array
+ */
+ public function getSupportedCommands()
+ {
+ return array_keys($this->commands);
+ }
+
+ /**
+ * Sets an handler for the specified command ID.
+ *
+ * The signature of the callback must have a single parameter of type
+ * Predis\Command\CommandInterface.
+ *
+ * When the callback argument is omitted or NULL, the previously associated
+ * handler for the specified command ID is removed.
+ *
+ * @param string $commandID Command ID.
+ * @param mixed $callback A valid callable object, or NULL to unset the handler.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setCommandHandler($commandID, $callback = null)
+ {
+ $commandID = strtoupper($commandID);
+
+ if (!isset($callback)) {
+ unset($this->commands[$commandID]);
+
+ return;
+ }
+
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException(
+ "The argument must be a callable object or NULL."
+ );
+ }
+
+ $this->commands[$commandID] = $callback;
+ }
+
+ /**
+ * Extracts the key from the first argument of a command instance.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string
+ */
+ protected function getKeyFromFirstArgument(CommandInterface $command)
+ {
+ return $command->getArgument(0);
+ }
+
+ /**
+ * Extracts the key from a command with multiple keys only when all keys in
+ * the arguments array produce the same hash.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromAllArguments(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+
+ if ($this->checkSameSlotForKeys($arguments)) {
+ return $arguments[0];
+ }
+ }
+
+ /**
+ * Extracts the key from a command with multiple keys only when all keys in
+ * the arguments array produce the same hash.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromInterleavedArguments(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ $keys = array();
+
+ for ($i = 0; $i < count($arguments); $i += 2) {
+ $keys[] = $arguments[$i];
+ }
+
+ if ($this->checkSameSlotForKeys($keys)) {
+ return $arguments[0];
+ }
+ }
+
+ /**
+ * Extracts the key from BLPOP and BRPOP commands.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromBlockingListCommands(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+
+ if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
+ return $arguments[0];
+ }
+ }
+
+ /**
+ * Extracts the key from BITOP command.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromBitOp(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+
+ if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
+ return $arguments[1];
+ }
+ }
+
+ /**
+ * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));
+
+ if ($this->checkSameSlotForKeys($keys)) {
+ return $arguments[0];
+ }
+ }
+
+ /**
+ * Extracts the key from EVAL and EVALSHA commands.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromScriptingCommands(CommandInterface $command)
+ {
+ if ($command instanceof ScriptCommand) {
+ $keys = $command->getKeys();
+ } else {
+ $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
+ }
+
+ if ($keys && $this->checkSameSlotForKeys($keys)) {
+ return $keys[0];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlot(CommandInterface $command)
+ {
+ $slot = $command->getSlot();
+
+ if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
+ $key = call_user_func($this->commands[$cmdID], $command);
+
+ if (isset($key)) {
+ $slot = $this->getSlotByKey($key);
+ $command->setSlot($slot);
+ }
+ }
+
+ return $slot;
+ }
+
+ /**
+ * Checks if the specified array of keys will generate the same hash.
+ *
+ * @param array $keys Array of keys.
+ *
+ * @return bool
+ */
+ protected function checkSameSlotForKeys(array $keys)
+ {
+ if (!$count = count($keys)) {
+ return false;
+ }
+
+ $currentSlot = $this->getSlotByKey($keys[0]);
+
+ for ($i = 1; $i < $count; $i++) {
+ $nextSlot = $this->getSlotByKey($keys[$i]);
+
+ if ($currentSlot !== $nextSlot) {
+ return false;
+ }
+
+ $currentSlot = $nextSlot;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns only the hashable part of a key (delimited by "{...}"), or the
+ * whole key if a key tag is not found in the string.
+ *
+ * @param string $key A key.
+ *
+ * @return string
+ */
+ protected function extractKeyTag($key)
+ {
+ if (false !== $start = strpos($key, '{')) {
+ if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
+ $key = substr($key, $start, $end - $start);
+ }
+ }
+
+ return $key;
+ }
+}
+
+/**
+ * Default cluster strategy used by Predis to handle client-side sharding.
+ *
+ * @author Daniele Alessandri
+ */
+class PredisStrategy extends ClusterStrategy
+{
+ protected $distributor;
+
+ /**
+ * @param DistributorInterface $distributor Optional distributor instance.
+ */
+ public function __construct(DistributorInterface $distributor = null)
+ {
+ parent::__construct();
+
+ $this->distributor = $distributor ?: new HashRing();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlotByKey($key)
+ {
+ $key = $this->extractKeyTag($key);
+ $hash = $this->distributor->hash($key);
+ $slot = $this->distributor->getSlot($hash);
+
+ return $slot;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function checkSameSlotForKeys(array $keys)
+ {
+ if (!$count = count($keys)) {
+ return false;
+ }
+
+ $currentKey = $this->extractKeyTag($keys[0]);
+
+ for ($i = 1; $i < $count; $i++) {
+ $nextKey = $this->extractKeyTag($keys[$i]);
+
+ if ($currentKey !== $nextKey) {
+ return false;
+ }
+
+ $currentKey = $nextKey;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDistributor()
+ {
+ return $this->distributor;
+ }
+}
+
+/**
+ * Default class used by Predis to calculate hashes out of keys of
+ * commands supported by redis-cluster.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisStrategy extends ClusterStrategy
+{
+ protected $hashGenerator;
+
+ /**
+ * @param HashGeneratorInterface $hashGenerator Hash generator instance.
+ */
+ public function __construct(HashGeneratorInterface $hashGenerator = null)
+ {
+ parent::__construct();
+
+ $this->hashGenerator = $hashGenerator ?: new CRC16();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlotByKey($key)
+ {
+ $key = $this->extractKeyTag($key);
+ $slot = $this->hashGenerator->hash($key) & 0x3FFF;
+
+ return $slot;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDistributor()
+ {
+ throw new NotSupportedException(
+ 'This cluster strategy does not provide an external distributor'
+ );
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Protocol;
+
+use Predis\CommunicationException;
+use Predis\Command\CommandInterface;
+use Predis\Connection\CompositeConnectionInterface;
+
+/**
+ * Defines a pluggable protocol processor capable of serializing commands and
+ * deserializing responses into PHP objects directly from a connection.
+ *
+ * @author Daniele Alessandri
+ */
+interface ProtocolProcessorInterface
+{
+ /**
+ * Writes a request over a connection to Redis.
+ *
+ * @param CompositeConnectionInterface $connection Redis connection.
+ * @param CommandInterface $command Command instance.
+ */
+ public function write(CompositeConnectionInterface $connection, CommandInterface $command);
+
+ /**
+ * Reads a response from a connection to Redis.
+ *
+ * @param CompositeConnectionInterface $connection Redis connection.
+ *
+ * @return mixed
+ */
+ public function read(CompositeConnectionInterface $connection);
+}
+
+/**
+ * Defines a pluggable reader capable of parsing responses returned by Redis and
+ * deserializing them to PHP objects.
+ *
+ * @author Daniele Alessandri
+ */
+interface ResponseReaderInterface
+{
+ /**
+ * Reads a response from a connection to Redis.
+ *
+ * @param CompositeConnectionInterface $connection Redis connection.
+ *
+ * @return mixed
+ */
+ public function read(CompositeConnectionInterface $connection);
+}
+
+/**
+ * Defines a pluggable serializer for Redis commands.
+ *
+ * @author Daniele Alessandri
+ */
+interface RequestSerializerInterface
+{
+ /**
+ * Serializes a Redis command.
+ *
+ * @param CommandInterface $command Redis command.
+ *
+ * @return string
+ */
+ public function serialize(CommandInterface $command);
+}
+
+/**
+ * Exception used to indentify errors encountered while parsing the Redis wire
+ * protocol.
+ *
+ * @author Daniele Alessandri
+ */
+class ProtocolException extends CommunicationException
+{
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Connection\Aggregate;
+
+use Predis\Connection\AggregateConnectionInterface;
+use InvalidArgumentException;
+use RuntimeException;
+use Predis\Command\CommandInterface;
+use Predis\Connection\NodeConnectionInterface;
+use Predis\Replication\ReplicationStrategy;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use Predis\NotSupportedException;
+use Predis\Cluster\PredisStrategy;
+use Predis\Cluster\StrategyInterface;
+use OutOfBoundsException;
+use Predis\Cluster\RedisStrategy as RedisClusterStrategy;
+use Predis\Command\RawCommand;
+use Predis\Connection\FactoryInterface;
+use Predis\Response\ErrorInterface as ErrorResponseInterface;
+
+/**
+ * Defines a cluster of Redis servers formed by aggregating multiple connection
+ * instances to single Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+interface ClusterInterface extends AggregateConnectionInterface
+{
+}
+
+/**
+ * Defines a group of Redis nodes in a master / slave replication setup.
+ *
+ * @author Daniele Alessandri
+ */
+interface ReplicationInterface extends AggregateConnectionInterface
+{
+ /**
+ * Switches the internal connection instance in use.
+ *
+ * @param string $connection Alias of a connection
+ */
+ public function switchTo($connection);
+
+ /**
+ * Returns the connection instance currently in use by the aggregate
+ * connection.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getCurrent();
+
+ /**
+ * Returns the connection instance for the master Redis node.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getMaster();
+
+ /**
+ * Returns a list of connection instances to slave nodes.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getSlaves();
+}
+
+/**
+ * Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0).
+ *
+ * This connection backend offers smart support for redis-cluster by handling
+ * automatic slots map (re)generation upon -MOVED or -ASK responses returned by
+ * Redis when redirecting a client to a different node.
+ *
+ * The cluster can be pre-initialized using only a subset of the actual nodes in
+ * the cluster, Predis will do the rest by adjusting the slots map and creating
+ * the missing underlying connection instances on the fly.
+ *
+ * It is possible to pre-associate connections to a slots range with the "slots"
+ * parameter in the form "$first-$last". This can greatly reduce runtime node
+ * guessing and redirections.
+ *
+ * It is also possible to ask for the full and updated slots map directly to one
+ * of the nodes and optionally enable such a behaviour upon -MOVED redirections.
+ * Asking for the cluster configuration to Redis is actually done by issuing a
+ * CLUSTER SLOTS command to a random node in the pool.
+ *
+ * @author Daniele Alessandri
+ */
+class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
+{
+ private $useClusterSlots = true;
+ private $defaultParameters = array();
+ private $pool = array();
+ private $slots = array();
+ private $slotsMap;
+ private $strategy;
+ private $connections;
+
+ /**
+ * @param FactoryInterface $connections Optional connection factory.
+ * @param StrategyInterface $strategy Optional cluster strategy.
+ */
+ public function __construct(
+ FactoryInterface $connections,
+ StrategyInterface $strategy = null
+ ) {
+ $this->connections = $connections;
+ $this->strategy = $strategy ?: new RedisClusterStrategy();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isConnected()
+ {
+ foreach ($this->pool as $connection) {
+ if ($connection->isConnected()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ if ($connection = $this->getRandomConnection()) {
+ $connection->connect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ foreach ($this->pool as $connection) {
+ $connection->disconnect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(NodeConnectionInterface $connection)
+ {
+ $this->pool[(string) $connection] = $connection;
+ unset($this->slotsMap);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(NodeConnectionInterface $connection)
+ {
+ if (false !== $id = array_search($connection, $this->pool, true)) {
+ unset(
+ $this->pool[$id],
+ $this->slotsMap
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Removes a connection instance by using its identifier.
+ *
+ * @param string $connectionID Connection identifier.
+ *
+ * @return bool True if the connection was in the pool.
+ */
+ public function removeById($connectionID)
+ {
+ if (isset($this->pool[$connectionID])) {
+ unset(
+ $this->pool[$connectionID],
+ $this->slotsMap
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Generates the current slots map by guessing the cluster configuration out
+ * of the connection parameters of the connections in the pool.
+ *
+ * Generation is based on the same algorithm used by Redis to generate the
+ * cluster, so it is most effective when all of the connections supplied on
+ * initialization have the "slots" parameter properly set accordingly to the
+ * current cluster configuration.
+ */
+ public function buildSlotsMap()
+ {
+ $this->slotsMap = array();
+
+ foreach ($this->pool as $connectionID => $connection) {
+ $parameters = $connection->getParameters();
+
+ if (!isset($parameters->slots)) {
+ continue;
+ }
+
+ $slots = explode('-', $parameters->slots, 2);
+ $this->setSlots($slots[0], $slots[1], $connectionID);
+ }
+ }
+
+ /**
+ * Generates an updated slots map fetching the cluster configuration using
+ * the CLUSTER SLOTS command against the specified node or a random one from
+ * the pool.
+ *
+ * @param NodeConnectionInterface $connection Optional connection instance.
+ *
+ * @return array
+ */
+ public function askSlotsMap(NodeConnectionInterface $connection = null)
+ {
+ if (!$connection && !$connection = $this->getRandomConnection()) {
+ return array();
+ }
+ $command = RawCommand::create('CLUSTER', 'SLOTS');
+ $response = $connection->executeCommand($command);
+
+ foreach ($response as $slots) {
+ // We only support master servers for now, so we ignore subsequent
+ // elements in the $slots array identifying slaves.
+ list($start, $end, $master) = $slots;
+
+ if ($master[0] === '') {
+ $this->setSlots($start, $end, (string) $connection);
+ } else {
+ $this->setSlots($start, $end, "{$master[0]}:{$master[1]}");
+ }
+ }
+
+ return $this->slotsMap;
+ }
+
+ /**
+ * Returns the current slots map for the cluster.
+ *
+ * @return array
+ */
+ public function getSlotsMap()
+ {
+ if (!isset($this->slotsMap)) {
+ $this->slotsMap = array();
+ }
+
+ return $this->slotsMap;
+ }
+
+ /**
+ * Pre-associates a connection to a slots range to avoid runtime guessing.
+ *
+ * @param int $first Initial slot of the range.
+ * @param int $last Last slot of the range.
+ * @param NodeConnectionInterface|string $connection ID or connection instance.
+ *
+ * @throws \OutOfBoundsException
+ */
+ public function setSlots($first, $last, $connection)
+ {
+ if ($first < 0x0000 || $first > 0x3FFF ||
+ $last < 0x0000 || $last > 0x3FFF ||
+ $last < $first
+ ) {
+ throw new OutOfBoundsException(
+ "Invalid slot range for $connection: [$first-$last]."
+ );
+ }
+
+ $slots = array_fill($first, $last - $first + 1, (string) $connection);
+ $this->slotsMap = $this->getSlotsMap() + $slots;
+ }
+
+ /**
+ * Guesses the correct node associated to a given slot using a precalculated
+ * slots map, falling back to the same logic used by Redis to initialize a
+ * cluster (best-effort).
+ *
+ * @param int $slot Slot index.
+ *
+ * @return string Connection ID.
+ */
+ protected function guessNode($slot)
+ {
+ if (!isset($this->slotsMap)) {
+ $this->buildSlotsMap();
+ }
+
+ if (isset($this->slotsMap[$slot])) {
+ return $this->slotsMap[$slot];
+ }
+
+ $count = count($this->pool);
+ $index = min((int) ($slot / (int) (16384 / $count)), $count - 1);
+ $nodes = array_keys($this->pool);
+
+ return $nodes[$index];
+ }
+
+ /**
+ * Creates a new connection instance from the given connection ID.
+ *
+ * @param string $connectionID Identifier for the connection.
+ *
+ * @return NodeConnectionInterface
+ */
+ protected function createConnection($connectionID)
+ {
+ $host = explode(':', $connectionID, 2);
+
+ $parameters = array_merge($this->defaultParameters, array(
+ 'host' => $host[0],
+ 'port' => $host[1],
+ ));
+
+ $connection = $this->connections->create($parameters);
+
+ return $connection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnection(CommandInterface $command)
+ {
+ $slot = $this->strategy->getSlot($command);
+
+ if (!isset($slot)) {
+ throw new NotSupportedException(
+ "Cannot use '{$command->getId()}' with redis-cluster."
+ );
+ }
+
+ if (isset($this->slots[$slot])) {
+ return $this->slots[$slot];
+ } else {
+ return $this->getConnectionBySlot($slot);
+ }
+ }
+
+ /**
+ * Returns the connection currently associated to a given slot.
+ *
+ * @param int $slot Slot index.
+ *
+ * @return NodeConnectionInterface
+ *
+ * @throws \OutOfBoundsException
+ */
+ public function getConnectionBySlot($slot)
+ {
+ if ($slot < 0x0000 || $slot > 0x3FFF) {
+ throw new OutOfBoundsException("Invalid slot [$slot].");
+ }
+
+ if (isset($this->slots[$slot])) {
+ return $this->slots[$slot];
+ }
+
+ $connectionID = $this->guessNode($slot);
+
+ if (!$connection = $this->getConnectionById($connectionID)) {
+ $connection = $this->createConnection($connectionID);
+ $this->pool[$connectionID] = $connection;
+ }
+
+ return $this->slots[$slot] = $connection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnectionById($connectionID)
+ {
+ if (isset($this->pool[$connectionID])) {
+ return $this->pool[$connectionID];
+ }
+ }
+
+ /**
+ * Returns a random connection from the pool.
+ *
+ * @return NodeConnectionInterface|null
+ */
+ protected function getRandomConnection()
+ {
+ if ($this->pool) {
+ return $this->pool[array_rand($this->pool)];
+ }
+ }
+
+ /**
+ * Permanently associates the connection instance to a new slot.
+ * The connection is added to the connections pool if not yet included.
+ *
+ * @param NodeConnectionInterface $connection Connection instance.
+ * @param int $slot Target slot index.
+ */
+ protected function move(NodeConnectionInterface $connection, $slot)
+ {
+ $this->pool[(string) $connection] = $connection;
+ $this->slots[(int) $slot] = $connection;
+ }
+
+ /**
+ * Handles -ERR responses returned by Redis.
+ *
+ * @param CommandInterface $command Command that generated the -ERR response.
+ * @param ErrorResponseInterface $error Redis error response object.
+ *
+ * @return mixed
+ */
+ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $error)
+ {
+ $details = explode(' ', $error->getMessage(), 2);
+
+ switch ($details[0]) {
+ case 'MOVED':
+ return $this->onMovedResponse($command, $details[1]);
+
+ case 'ASK':
+ return $this->onAskResponse($command, $details[1]);
+
+ default:
+ return $error;
+ }
+ }
+
+ /**
+ * Handles -MOVED responses by executing again the command against the node
+ * indicated by the Redis response.
+ *
+ * @param CommandInterface $command Command that generated the -MOVED response.
+ * @param string $details Parameters of the -MOVED response.
+ *
+ * @return mixed
+ */
+ protected function onMovedResponse(CommandInterface $command, $details)
+ {
+ list($slot, $connectionID) = explode(' ', $details, 2);
+
+ if (!$connection = $this->getConnectionById($connectionID)) {
+ $connection = $this->createConnection($connectionID);
+ }
+
+ if ($this->useClusterSlots) {
+ $this->askSlotsMap($connection);
+ }
+
+ $this->move($connection, $slot);
+ $response = $this->executeCommand($command);
+
+ return $response;
+ }
+
+ /**
+ * Handles -ASK responses by executing again the command against the node
+ * indicated by the Redis response.
+ *
+ * @param CommandInterface $command Command that generated the -ASK response.
+ * @param string $details Parameters of the -ASK response.
+ * @return mixed
+ */
+ protected function onAskResponse(CommandInterface $command, $details)
+ {
+ list($slot, $connectionID) = explode(' ', $details, 2);
+
+ if (!$connection = $this->getConnectionById($connectionID)) {
+ $connection = $this->createConnection($connectionID);
+ }
+ $connection->executeCommand(RawCommand::create('ASKING'));
+ $response = $connection->executeCommand($command);
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $this->getConnection($command)->writeRequest($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readResponse(CommandInterface $command)
+ {
+ return $this->getConnection($command)->readResponse($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ $connection = $this->getConnection($command);
+ $response = $connection->executeCommand($command);
+
+ if ($response instanceof ErrorResponseInterface) {
+ return $this->onErrorResponse($command, $response);
+ }
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return count($this->pool);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator(array_values($this->pool));
+ }
+
+ /**
+ * Returns the underlying command hash strategy used to hash commands by
+ * using keys found in their arguments.
+ *
+ * @return StrategyInterface
+ */
+ public function getClusterStrategy()
+ {
+ return $this->strategy;
+ }
+
+ /**
+ * Returns the underlying connection factory used to create new connection
+ * instances to Redis nodes indicated by redis-cluster.
+ *
+ * @return FactoryInterface
+ */
+ public function getConnectionFactory()
+ {
+ return $this->connections;
+ }
+
+ /**
+ * Enables automatic fetching of the current slots map from one of the nodes
+ * using the CLUSTER SLOTS command. This option is disabled by default but
+ * asking the current slots map to Redis upon -MOVED responses may reduce
+ * overhead by eliminating the trial-and-error nature of the node guessing
+ * procedure, mostly when targeting many keys that would end up in a lot of
+ * redirections.
+ *
+ * The slots map can still be manually fetched using the askSlotsMap()
+ * method whether or not this option is enabled.
+ *
+ * @param bool $value Enable or disable the use of CLUSTER SLOTS.
+ */
+ public function useClusterSlots($value)
+ {
+ $this->useClusterSlots = (bool) $value;
+ }
+
+ /**
+ * Sets a default array of connection parameters to be applied when creating
+ * new connection instances on the fly when they are not part of the initial
+ * pool supplied upon cluster initialization.
+ *
+ * These parameters are not applied to connections added to the pool using
+ * the add() method.
+ *
+ * @param array $parameters Array of connection parameters.
+ */
+ public function setDefaultParameters(array $parameters)
+ {
+ $this->defaultParameters = array_merge(
+ $this->defaultParameters,
+ $parameters ?: array()
+ );
+ }
+}
+
+/**
+ * Abstraction for a cluster of aggregate connections to various Redis servers
+ * implementing client-side sharding based on pluggable distribution strategies.
+ *
+ * @author Daniele Alessandri
+ * @todo Add the ability to remove connections from pool.
+ */
+class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
+{
+ private $pool;
+ private $strategy;
+ private $distributor;
+
+ /**
+ * @param StrategyInterface $strategy Optional cluster strategy.
+ */
+ public function __construct(StrategyInterface $strategy = null)
+ {
+ $this->pool = array();
+ $this->strategy = $strategy ?: new PredisStrategy();
+ $this->distributor = $this->strategy->getDistributor();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isConnected()
+ {
+ foreach ($this->pool as $connection) {
+ if ($connection->isConnected()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ foreach ($this->pool as $connection) {
+ $connection->connect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ foreach ($this->pool as $connection) {
+ $connection->disconnect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(NodeConnectionInterface $connection)
+ {
+ $parameters = $connection->getParameters();
+
+ if (isset($parameters->alias)) {
+ $this->pool[$parameters->alias] = $connection;
+ } else {
+ $this->pool[] = $connection;
+ }
+
+ $weight = isset($parameters->weight) ? $parameters->weight : null;
+ $this->distributor->add($connection, $weight);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(NodeConnectionInterface $connection)
+ {
+ if (($id = array_search($connection, $this->pool, true)) !== false) {
+ unset($this->pool[$id]);
+ $this->distributor->remove($connection);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Removes a connection instance using its alias or index.
+ *
+ * @param string $connectionID Alias or index of a connection.
+ *
+ * @return bool Returns true if the connection was in the pool.
+ */
+ public function removeById($connectionID)
+ {
+ if ($connection = $this->getConnectionById($connectionID)) {
+ return $this->remove($connection);
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnection(CommandInterface $command)
+ {
+ $slot = $this->strategy->getSlot($command);
+
+ if (!isset($slot)) {
+ throw new NotSupportedException(
+ "Cannot use '{$command->getId()}' over clusters of connections."
+ );
+ }
+
+ $node = $this->distributor->getBySlot($slot);
+
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnectionById($connectionID)
+ {
+ return isset($this->pool[$connectionID]) ? $this->pool[$connectionID] : null;
+ }
+
+ /**
+ * Retrieves a connection instance from the cluster using a key.
+ *
+ * @param string $key Key string.
+ *
+ * @return NodeConnectionInterface
+ */
+ public function getConnectionByKey($key)
+ {
+ $hash = $this->strategy->getSlotByKey($key);
+ $node = $this->distributor->getBySlot($hash);
+
+ return $node;
+ }
+
+ /**
+ * Returns the underlying command hash strategy used to hash commands by
+ * using keys found in their arguments.
+ *
+ * @return StrategyInterface
+ */
+ public function getClusterStrategy()
+ {
+ return $this->strategy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return count($this->pool);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator($this->pool);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $this->getConnection($command)->writeRequest($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readResponse(CommandInterface $command)
+ {
+ return $this->getConnection($command)->readResponse($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ return $this->getConnection($command)->executeCommand($command);
+ }
+
+ /**
+ * Executes the specified Redis command on all the nodes of a cluster.
+ *
+ * @param CommandInterface $command A Redis command.
+ *
+ * @return array
+ */
+ public function executeCommandOnNodes(CommandInterface $command)
+ {
+ $responses = array();
+ foreach ($this->pool as $connection) {
+ $responses[] = $connection->executeCommand($command);
+ }
+
+ return $responses;
+ }
+}
+
+/**
+ * Aggregate connection handling replication of Redis nodes configured in a
+ * single master / multiple slaves setup.
+ *
+ * @author Daniele Alessandri
+ */
+class MasterSlaveReplication implements ReplicationInterface
+{
+ protected $strategy;
+ protected $master;
+ protected $slaves;
+ protected $current;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ReplicationStrategy $strategy = null)
+ {
+ $this->slaves = array();
+ $this->strategy = $strategy ?: new ReplicationStrategy();
+ }
+
+ /**
+ * Checks if one master and at least one slave have been defined.
+ */
+ protected function check()
+ {
+ if (!isset($this->master) || !$this->slaves) {
+ throw new RuntimeException('Replication needs one master and at least one slave.');
+ }
+ }
+
+ /**
+ * Resets the connection state.
+ */
+ protected function reset()
+ {
+ $this->current = null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(NodeConnectionInterface $connection)
+ {
+ $alias = $connection->getParameters()->alias;
+
+ if ($alias === 'master') {
+ $this->master = $connection;
+ } else {
+ $this->slaves[$alias ?: count($this->slaves)] = $connection;
+ }
+
+ $this->reset();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(NodeConnectionInterface $connection)
+ {
+ if ($connection->getParameters()->alias === 'master') {
+ $this->master = null;
+ $this->reset();
+
+ return true;
+ } else {
+ if (($id = array_search($connection, $this->slaves, true)) !== false) {
+ unset($this->slaves[$id]);
+ $this->reset();
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnection(CommandInterface $command)
+ {
+ if ($this->current === null) {
+ $this->check();
+ $this->current = $this->strategy->isReadOperation($command)
+ ? $this->pickSlave()
+ : $this->master;
+
+ return $this->current;
+ }
+
+ if ($this->current === $this->master) {
+ return $this->current;
+ }
+
+ if (!$this->strategy->isReadOperation($command)) {
+ $this->current = $this->master;
+ }
+
+ return $this->current;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConnectionById($connectionId)
+ {
+ if ($connectionId === 'master') {
+ return $this->master;
+ }
+
+ if (isset($this->slaves[$connectionId])) {
+ return $this->slaves[$connectionId];
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function switchTo($connection)
+ {
+ $this->check();
+
+ if (!$connection instanceof NodeConnectionInterface) {
+ $connection = $this->getConnectionById($connection);
+ }
+ if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
+ throw new InvalidArgumentException('Invalid connection or connection not found.');
+ }
+
+ $this->current = $connection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrent()
+ {
+ return $this->current;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMaster()
+ {
+ return $this->master;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlaves()
+ {
+ return array_values($this->slaves);
+ }
+
+ /**
+ * Returns the underlying replication strategy.
+ *
+ * @return ReplicationStrategy
+ */
+ public function getReplicationStrategy()
+ {
+ return $this->strategy;
+ }
+
+ /**
+ * Returns a random slave.
+ *
+ * @return NodeConnectionInterface
+ */
+ protected function pickSlave()
+ {
+ return $this->slaves[array_rand($this->slaves)];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isConnected()
+ {
+ return $this->current ? $this->current->isConnected() : false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function connect()
+ {
+ if ($this->current === null) {
+ $this->check();
+ $this->current = $this->pickSlave();
+ }
+
+ $this->current->connect();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disconnect()
+ {
+ if ($this->master) {
+ $this->master->disconnect();
+ }
+
+ foreach ($this->slaves as $connection) {
+ $connection->disconnect();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function writeRequest(CommandInterface $command)
+ {
+ $this->getConnection($command)->writeRequest($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readResponse(CommandInterface $command)
+ {
+ return $this->getConnection($command)->readResponse($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ return $this->getConnection($command)->executeCommand($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep()
+ {
+ return array('master', 'slaves', 'strategy');
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Pipeline;
+
+use SplQueue;
+use Predis\ClientException;
+use Predis\ClientInterface;
+use Predis\Connection\ConnectionInterface;
+use Predis\Connection\NodeConnectionInterface;
+use Predis\Response\ErrorInterface as ErrorResponseInterface;
+use Predis\Response\ResponseInterface;
+use Predis\Response\ServerException;
+use Predis\NotSupportedException;
+use Predis\CommunicationException;
+use Predis\Connection\Aggregate\ClusterInterface;
+use Exception;
+use InvalidArgumentException;
+use Predis\ClientContextInterface;
+use Predis\Command\CommandInterface;
+use Predis\Connection\Aggregate\ReplicationInterface;
+
+/**
+ * Implementation of a command pipeline in which write and read operations of
+ * Redis commands are pipelined to alleviate the effects of network round-trips.
+ *
+ * {@inheritdoc}
+ *
+ * @author Daniele Alessandri
+ */
+class Pipeline implements ClientContextInterface
+{
+ private $client;
+ private $pipeline;
+
+ private $responses = array();
+ private $running = false;
+
+ /**
+ * @param ClientInterface $client Client instance used by the context.
+ */
+ public function __construct(ClientInterface $client)
+ {
+ $this->client = $client;
+ $this->pipeline = new SplQueue();
+ }
+
+ /**
+ * Queues a command into the pipeline buffer.
+ *
+ * @param string $method Command ID.
+ * @param array $arguments Arguments for the command.
+ *
+ * @return $this
+ */
+ public function __call($method, $arguments)
+ {
+ $command = $this->client->createCommand($method, $arguments);
+ $this->recordCommand($command);
+
+ return $this;
+ }
+
+ /**
+ * Queues a command instance into the pipeline buffer.
+ *
+ * @param CommandInterface $command Command to be queued in the buffer.
+ */
+ protected function recordCommand(CommandInterface $command)
+ {
+ $this->pipeline->enqueue($command);
+ }
+
+ /**
+ * Queues a command instance into the pipeline buffer.
+ *
+ * @param CommandInterface $command Command instance to be queued in the buffer.
+ *
+ * @return $this
+ */
+ public function executeCommand(CommandInterface $command)
+ {
+ $this->recordCommand($command);
+
+ return $this;
+ }
+
+ /**
+ * Throws an exception on -ERR responses returned by Redis.
+ *
+ * @param ConnectionInterface $connection Redis connection that returned the error.
+ * @param ErrorResponseInterface $response Instance of the error response.
+ *
+ * @throws ServerException
+ */
+ protected function exception(ConnectionInterface $connection, ErrorResponseInterface $response)
+ {
+ $connection->disconnect();
+ $message = $response->getMessage();
+
+ throw new ServerException($message);
+ }
+
+ /**
+ * Returns the underlying connection to be used by the pipeline.
+ *
+ * @return ConnectionInterface
+ */
+ protected function getConnection()
+ {
+ $connection = $this->getClient()->getConnection();
+
+ if ($connection instanceof ReplicationInterface) {
+ $connection->switchTo('master');
+ }
+
+ return $connection;
+ }
+
+ /**
+ * Implements the logic to flush the queued commands and read the responses
+ * from the current connection.
+ *
+ * @param ConnectionInterface $connection Current connection instance.
+ * @param SplQueue $commands Queued commands.
+ *
+ * @return array
+ */
+ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
+ {
+ foreach ($commands as $command) {
+ $connection->writeRequest($command);
+ }
+
+ $responses = array();
+ $exceptions = $this->throwServerExceptions();
+
+ while (!$commands->isEmpty()) {
+ $command = $commands->dequeue();
+ $response = $connection->readResponse($command);
+
+ if (!$response instanceof ResponseInterface) {
+ $responses[] = $command->parseResponse($response);
+ } elseif ($response instanceof ErrorResponseInterface && $exceptions) {
+ $this->exception($connection, $response);
+ } else {
+ $responses[] = $response;
+ }
+ }
+
+ return $responses;
+ }
+
+ /**
+ * Flushes the buffer holding all of the commands queued so far.
+ *
+ * @param bool $send Specifies if the commands in the buffer should be sent to Redis.
+ *
+ * @return $this
+ */
+ public function flushPipeline($send = true)
+ {
+ if ($send && !$this->pipeline->isEmpty()) {
+ $responses = $this->executePipeline($this->getConnection(), $this->pipeline);
+ $this->responses = array_merge($this->responses, $responses);
+ } else {
+ $this->pipeline = new SplQueue();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Marks the running status of the pipeline.
+ *
+ * @param bool $bool Sets the running status of the pipeline.
+ *
+ * @throws ClientException
+ */
+ private function setRunning($bool)
+ {
+ if ($bool && $this->running) {
+ throw new ClientException('The current pipeline context is already being executed.');
+ }
+
+ $this->running = $bool;
+ }
+
+ /**
+ * Handles the actual execution of the whole pipeline.
+ *
+ * @param mixed $callable Optional callback for execution.
+ *
+ * @return array
+ *
+ * @throws Exception
+ * @throws InvalidArgumentException
+ */
+ public function execute($callable = null)
+ {
+ if ($callable && !is_callable($callable)) {
+ throw new InvalidArgumentException('The argument must be a callable object.');
+ }
+
+ $exception = null;
+ $this->setRunning(true);
+
+ try {
+ if ($callable) {
+ call_user_func($callable, $this);
+ }
+
+ $this->flushPipeline();
+ } catch (Exception $exception) {
+ // NOOP
+ }
+
+ $this->setRunning(false);
+
+ if ($exception) {
+ throw $exception;
+ }
+
+ return $this->responses;
+ }
+
+ /**
+ * Returns if the pipeline should throw exceptions on server errors.
+ *
+ * @return bool
+ */
+ protected function throwServerExceptions()
+ {
+ return (bool) $this->client->getOptions()->exceptions;
+ }
+
+ /**
+ * Returns the underlying client instance used by the pipeline object.
+ *
+ * @return ClientInterface
+ */
+ public function getClient()
+ {
+ return $this->client;
+ }
+}
+
+/**
+ * Command pipeline that writes commands to the servers but discards responses.
+ *
+ * @author Daniele Alessandri
+ */
+class FireAndForget extends Pipeline
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
+ {
+ while (!$commands->isEmpty()) {
+ $connection->writeRequest($commands->dequeue());
+ }
+
+ $connection->disconnect();
+
+ return array();
+ }
+}
+
+/**
+ * Command pipeline that does not throw exceptions on connection errors, but
+ * returns the exception instances as the rest of the response elements.
+ *
+ * @todo Awful naming!
+ * @author Daniele Alessandri
+ */
+class ConnectionErrorProof extends Pipeline
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function getConnection()
+ {
+ return $this->getClient()->getConnection();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
+ {
+ if ($connection instanceof NodeConnectionInterface) {
+ return $this->executeSingleNode($connection, $commands);
+ } elseif ($connection instanceof ClusterInterface) {
+ return $this->executeCluster($connection, $commands);
+ } else {
+ $class = get_class($connection);
+
+ throw new NotSupportedException("The connection class '$class' is not supported.");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands)
+ {
+ $responses = array();
+ $sizeOfPipe = count($commands);
+
+ foreach ($commands as $command) {
+ try {
+ $connection->writeRequest($command);
+ } catch (CommunicationException $exception) {
+ return array_fill(0, $sizeOfPipe, $exception);
+ }
+ }
+
+ for ($i = 0; $i < $sizeOfPipe; $i++) {
+ $command = $commands->dequeue();
+
+ try {
+ $responses[$i] = $connection->readResponse($command);
+ } catch (CommunicationException $exception) {
+ $add = count($commands) - count($responses);
+ $responses = array_merge($responses, array_fill(0, $add, $exception));
+
+ break;
+ }
+ }
+
+ return $responses;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeCluster(ClusterInterface $connection, SplQueue $commands)
+ {
+ $responses = array();
+ $sizeOfPipe = count($commands);
+ $exceptions = array();
+
+ foreach ($commands as $command) {
+ $cmdConnection = $connection->getConnection($command);
+
+ if (isset($exceptions[spl_object_hash($cmdConnection)])) {
+ continue;
+ }
+
+ try {
+ $cmdConnection->writeRequest($command);
+ } catch (CommunicationException $exception) {
+ $exceptions[spl_object_hash($cmdConnection)] = $exception;
+ }
+ }
+
+ for ($i = 0; $i < $sizeOfPipe; $i++) {
+ $command = $commands->dequeue();
+
+ $cmdConnection = $connection->getConnection($command);
+ $connectionHash = spl_object_hash($cmdConnection);
+
+ if (isset($exceptions[$connectionHash])) {
+ $responses[$i] = $exceptions[$connectionHash];
+ continue;
+ }
+
+ try {
+ $responses[$i] = $cmdConnection->readResponse($command);
+ } catch (CommunicationException $exception) {
+ $responses[$i] = $exception;
+ $exceptions[$connectionHash] = $exception;
+ }
+ }
+
+ return $responses;
+ }
+}
+
+/**
+ * Command pipeline wrapped into a MULTI / EXEC transaction.
+ *
+ * @author Daniele Alessandri
+ */
+class Atomic extends Pipeline
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(ClientInterface $client)
+ {
+ if (!$client->getProfile()->supportsCommands(array('multi', 'exec', 'discard'))) {
+ throw new ClientException(
+ "The current profile does not support 'MULTI', 'EXEC' and 'DISCARD'."
+ );
+ }
+
+ parent::__construct($client);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getConnection()
+ {
+ $connection = $this->getClient()->getConnection();
+
+ if (!$connection instanceof NodeConnectionInterface) {
+ $class = __CLASS__;
+
+ throw new ClientException("The class '$class' does not support aggregate connections.");
+ }
+
+ return $connection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
+ {
+ $profile = $this->getClient()->getProfile();
+ $connection->executeCommand($profile->createCommand('multi'));
+
+ foreach ($commands as $command) {
+ $connection->writeRequest($command);
+ }
+
+ foreach ($commands as $command) {
+ $response = $connection->readResponse($command);
+
+ if ($response instanceof ErrorResponseInterface) {
+ $connection->executeCommand($profile->createCommand('discard'));
+ throw new ServerException($response->getMessage());
+ }
+ }
+
+ $executed = $connection->executeCommand($profile->createCommand('exec'));
+
+ if (!isset($executed)) {
+ // TODO: should be throwing a more appropriate exception.
+ throw new ClientException(
+ 'The underlying transaction has been aborted by the server.'
+ );
+ }
+
+ if (count($executed) !== count($commands)) {
+ $expected = count($commands);
+ $received = count($executed);
+
+ throw new ClientException(
+ "Invalid number of responses [expected $expected, received $received]."
+ );
+ }
+
+ $responses = array();
+ $sizeOfPipe = count($commands);
+ $exceptions = $this->throwServerExceptions();
+
+ for ($i = 0; $i < $sizeOfPipe; $i++) {
+ $command = $commands->dequeue();
+ $response = $executed[$i];
+
+ if (!$response instanceof ResponseInterface) {
+ $responses[] = $command->parseResponse($response);
+ } elseif ($response instanceof ErrorResponseInterface && $exceptions) {
+ $this->exception($connection, $response);
+ } else {
+ $responses[] = $response;
+ }
+
+ unset($executed[$i]);
+ }
+
+ return $responses;
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Cluster\Distributor;
+
+use Predis\Cluster\Hash\HashGeneratorInterface;
+use Exception;
+
+/**
+ * A distributor implements the logic to automatically distribute keys among
+ * several nodes for client-side sharding.
+ *
+ * @author Daniele Alessandri
+ */
+interface DistributorInterface
+{
+ /**
+ * Adds a node to the distributor with an optional weight.
+ *
+ * @param mixed $node Node object.
+ * @param int $weight Weight for the node.
+ */
+ public function add($node, $weight = null);
+
+ /**
+ * Removes a node from the distributor.
+ *
+ * @param mixed $node Node object.
+ */
+ public function remove($node);
+
+ /**
+ * Returns the corresponding slot of a node from the distributor using the
+ * computed hash of a key.
+ *
+ * @param mixed $hash
+ *
+ * @return mixed
+ */
+ public function getSlot($hash);
+
+ /**
+ * Returns a node from the distributor using its assigned slot ID.
+ *
+ * @param mixed $slot
+ *
+ * @return mixed|null
+ */
+ public function getBySlot($slot);
+
+ /**
+ * Returns a node from the distributor using the computed hash of a key.
+ *
+ * @param mixed $hash
+ *
+ * @return mixed
+ */
+ public function getByHash($hash);
+
+ /**
+ * Returns a node from the distributor mapping to the specified value.
+ *
+ * @param string $value
+ *
+ * @return mixed
+ */
+ public function get($value);
+
+ /**
+ * Returns the underlying hash generator instance.
+ *
+ * @return HashGeneratorInterface
+ */
+ public function getHashGenerator();
+}
+
+/**
+ * This class implements an hashring-based distributor that uses the same
+ * algorithm of memcache to distribute keys in a cluster using client-side
+ * sharding.
+ *
+ * @author Daniele Alessandri
+ * @author Lorenzo Castelli
+ */
+class HashRing implements DistributorInterface, HashGeneratorInterface
+{
+ const DEFAULT_REPLICAS = 128;
+ const DEFAULT_WEIGHT = 100;
+
+ private $ring;
+ private $ringKeys;
+ private $ringKeysCount;
+ private $replicas;
+ private $nodeHashCallback;
+ private $nodes = array();
+
+ /**
+ * @param int $replicas Number of replicas in the ring.
+ * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
+ */
+ public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null)
+ {
+ $this->replicas = $replicas;
+ $this->nodeHashCallback = $nodeHashCallback;
+ }
+
+ /**
+ * Adds a node to the ring with an optional weight.
+ *
+ * @param mixed $node Node object.
+ * @param int $weight Weight for the node.
+ */
+ public function add($node, $weight = null)
+ {
+ // In case of collisions in the hashes of the nodes, the node added
+ // last wins, thus the order in which nodes are added is significant.
+ $this->nodes[] = array(
+ 'object' => $node,
+ 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT
+ );
+
+ $this->reset();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($node)
+ {
+ // A node is removed by resetting the ring so that it's recreated from
+ // scratch, in order to reassign possible hashes with collisions to the
+ // right node according to the order in which they were added in the
+ // first place.
+ for ($i = 0; $i < count($this->nodes); ++$i) {
+ if ($this->nodes[$i]['object'] === $node) {
+ array_splice($this->nodes, $i, 1);
+ $this->reset();
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Resets the distributor.
+ */
+ private function reset()
+ {
+ unset(
+ $this->ring,
+ $this->ringKeys,
+ $this->ringKeysCount
+ );
+ }
+
+ /**
+ * Returns the initialization status of the distributor.
+ *
+ * @return bool
+ */
+ private function isInitialized()
+ {
+ return isset($this->ringKeys);
+ }
+
+ /**
+ * Calculates the total weight of all the nodes in the distributor.
+ *
+ * @return int
+ */
+ private function computeTotalWeight()
+ {
+ $totalWeight = 0;
+
+ foreach ($this->nodes as $node) {
+ $totalWeight += $node['weight'];
+ }
+
+ return $totalWeight;
+ }
+
+ /**
+ * Initializes the distributor.
+ */
+ private function initialize()
+ {
+ if ($this->isInitialized()) {
+ return;
+ }
+
+ if (!$this->nodes) {
+ throw new EmptyRingException('Cannot initialize an empty hashring.');
+ }
+
+ $this->ring = array();
+ $totalWeight = $this->computeTotalWeight();
+ $nodesCount = count($this->nodes);
+
+ foreach ($this->nodes as $node) {
+ $weightRatio = $node['weight'] / $totalWeight;
+ $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
+ }
+
+ ksort($this->ring, SORT_NUMERIC);
+ $this->ringKeys = array_keys($this->ring);
+ $this->ringKeysCount = count($this->ringKeys);
+ }
+
+ /**
+ * Implements the logic needed to add a node to the hashring.
+ *
+ * @param array $ring Source hashring.
+ * @param mixed $node Node object to be added.
+ * @param int $totalNodes Total number of nodes.
+ * @param int $replicas Number of replicas in the ring.
+ * @param float $weightRatio Weight ratio for the node.
+ */
+ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
+ {
+ $nodeObject = $node['object'];
+ $nodeHash = $this->getNodeHash($nodeObject);
+ $replicas = (int) round($weightRatio * $totalNodes * $replicas);
+
+ for ($i = 0; $i < $replicas; $i++) {
+ $key = crc32("$nodeHash:$i");
+ $ring[$key] = $nodeObject;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNodeHash($nodeObject)
+ {
+ if (!isset($this->nodeHashCallback)) {
+ return (string) $nodeObject;
+ }
+
+ return call_user_func($this->nodeHashCallback, $nodeObject);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hash($value)
+ {
+ return crc32($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getByHash($hash)
+ {
+ return $this->ring[$this->getSlot($hash)];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBySlot($slot)
+ {
+ $this->initialize();
+
+ if (isset($this->ring[$slot])) {
+ return $this->ring[$slot];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSlot($hash)
+ {
+ $this->initialize();
+
+ $ringKeys = $this->ringKeys;
+ $upper = $this->ringKeysCount - 1;
+ $lower = 0;
+
+ while ($lower <= $upper) {
+ $index = ($lower + $upper) >> 1;
+ $item = $ringKeys[$index];
+
+ if ($item > $hash) {
+ $upper = $index - 1;
+ } elseif ($item < $hash) {
+ $lower = $index + 1;
+ } else {
+ return $item;
+ }
+ }
+
+ return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($value)
+ {
+ $hash = $this->hash($value);
+ $node = $this->getByHash($hash);
+
+ return $node;
+ }
+
+ /**
+ * Implements a strategy to deal with wrap-around errors during binary searches.
+ *
+ * @param int $upper
+ * @param int $lower
+ * @param int $ringKeysCount
+ *
+ * @return int
+ */
+ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
+ {
+ // Binary search for the last item in ringkeys with a value less or
+ // equal to the key. If no such item exists, return the last item.
+ return $upper >= 0 ? $upper : $ringKeysCount - 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHashGenerator()
+ {
+ return $this;
+ }
+}
+
+/**
+ * This class implements an hashring-based distributor that uses the same
+ * algorithm of libketama to distribute keys in a cluster using client-side
+ * sharding.
+ *
+ * @author Daniele Alessandri
+ * @author Lorenzo Castelli
+ */
+class KetamaRing extends HashRing
+{
+ const DEFAULT_REPLICAS = 160;
+
+ /**
+ * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
+ */
+ public function __construct($nodeHashCallback = null)
+ {
+ parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
+ {
+ $nodeObject = $node['object'];
+ $nodeHash = $this->getNodeHash($nodeObject);
+ $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4));
+
+ for ($i = 0; $i < $replicas; $i++) {
+ $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true));
+
+ foreach ($unpackedDigest as $key) {
+ $ring[$key] = $nodeObject;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hash($value)
+ {
+ $hash = unpack('V', md5($value, true));
+
+ return $hash[1];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
+ {
+ // Binary search for the first item in ringkeys with a value greater
+ // or equal to the key. If no such item exists, return the first item.
+ return $lower < $ringKeysCount ? $lower : 0;
+ }
+}
+
+/**
+ * Exception class that identifies empty rings.
+ *
+ * @author Daniele Alessandri
+ */
+class EmptyRingException extends Exception
+{
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Response\Iterator;
+
+use Predis\Connection\NodeConnectionInterface;
+use Iterator;
+use Countable;
+use Predis\Response\ResponseInterface;
+use OuterIterator;
+use InvalidArgumentException;
+use UnexpectedValueException;
+
+/**
+ * Iterator that abstracts the access to multibulk responses allowing them to be
+ * consumed in a streamable fashion without keeping the whole payload in memory.
+ *
+ * This iterator does not support rewinding which means that the iteration, once
+ * consumed, cannot be restarted.
+ *
+ * Always make sure that the whole iteration is consumed (or dropped) to prevent
+ * protocol desynchronization issues.
+ *
+ * @author Daniele Alessandri
+ */
+abstract class MultiBulkIterator implements Iterator, Countable, ResponseInterface
+{
+ protected $current;
+ protected $position;
+ protected $size;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ // NOOP
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->current;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ if (++$this->position < $this->size) {
+ $this->current = $this->getValue();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return $this->position < $this->size;
+ }
+
+ /**
+ * Returns the number of items comprising the whole multibulk response.
+ *
+ * This method should be used instead of iterator_count() to get the size of
+ * the current multibulk response since the former consumes the iteration to
+ * count the number of elements, but our iterators do not support rewinding.
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return $this->size;
+ }
+
+ /**
+ * Returns the current position of the iterator.
+ *
+ * @return int
+ */
+ public function getPosition()
+ {
+ return $this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ abstract protected function getValue();
+}
+
+/**
+ * Streamable multibulk response.
+ *
+ * @author Daniele Alessandri
+ */
+class MultiBulk extends MultiBulkIterator
+{
+ private $connection;
+
+ /**
+ * @param NodeConnectionInterface $connection Connection to Redis.
+ * @param int $size Number of elements of the multibulk response.
+ */
+ public function __construct(NodeConnectionInterface $connection, $size)
+ {
+ $this->connection = $connection;
+ $this->size = $size;
+ $this->position = 0;
+ $this->current = $size > 0 ? $this->getValue() : null;
+ }
+
+ /**
+ * Handles the synchronization of the client with the Redis protocol when
+ * the garbage collector kicks in (e.g. when the iterator goes out of the
+ * scope of a foreach or it is unset).
+ */
+ public function __destruct()
+ {
+ $this->drop(true);
+ }
+
+ /**
+ * Drop queued elements that have not been read from the connection either
+ * by consuming the rest of the multibulk response or quickly by closing the
+ * underlying connection.
+ *
+ * @param bool $disconnect Consume the iterator or drop the connection.
+ */
+ public function drop($disconnect = false)
+ {
+ if ($disconnect) {
+ if ($this->valid()) {
+ $this->position = $this->size;
+ $this->connection->disconnect();
+ }
+ } else {
+ while ($this->valid()) {
+ $this->next();
+ }
+ }
+ }
+
+ /**
+ * Reads the next item of the multibulk response from the connection.
+ *
+ * @return mixed
+ */
+ protected function getValue()
+ {
+ return $this->connection->read();
+ }
+}
+
+/**
+ * Outer iterator consuming streamable multibulk responses by yielding tuples of
+ * keys and values.
+ *
+ * This wrapper is useful for responses to commands such as `HGETALL` that can
+ * be iterater as $key => $value pairs.
+ *
+ * @author Daniele Alessandri
+ */
+class MultiBulkTuple extends MultiBulk implements OuterIterator
+{
+ private $iterator;
+
+ /**
+ * @param MultiBulk $iterator Inner multibulk response iterator.
+ */
+ public function __construct(MultiBulk $iterator)
+ {
+ $this->checkPreconditions($iterator);
+
+ $this->size = count($iterator) / 2;
+ $this->iterator = $iterator;
+ $this->position = $iterator->getPosition();
+ $this->current = $this->size > 0 ? $this->getValue() : null;
+ }
+
+ /**
+ * Checks for valid preconditions.
+ *
+ * @param MultiBulk $iterator Inner multibulk response iterator.
+ *
+ * @throws \InvalidArgumentException
+ * @throws \UnexpectedValueException
+ */
+ protected function checkPreconditions(MultiBulk $iterator)
+ {
+ if ($iterator->getPosition() !== 0) {
+ throw new InvalidArgumentException(
+ 'Cannot initialize a tuple iterator using an already initiated iterator.'
+ );
+ }
+
+ if (($size = count($iterator)) % 2 !== 0) {
+ throw new UnexpectedValueException("Invalid response size for a tuple iterator.");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInnerIterator()
+ {
+ return $this->iterator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __destruct()
+ {
+ $this->iterator->drop(true);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getValue()
+ {
+ $k = $this->iterator->current();
+ $this->iterator->next();
+
+ $v = $this->iterator->current();
+ $this->iterator->next();
+
+ return array($k, $v);
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Cluster\Hash;
+
+/**
+ * An hash generator implements the logic used to calculate the hash of a key to
+ * distribute operations among Redis nodes.
+ *
+ * @author Daniele Alessandri
+ */
+interface HashGeneratorInterface
+{
+ /**
+ * Generates an hash from a string to be used for distribution.
+ *
+ * @param string $value String value.
+ *
+ * @return int
+ */
+ public function hash($value);
+}
+
+/**
+ * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
+ *
+ * @author Daniele Alessandri
+ */
+class CRC16 implements HashGeneratorInterface
+{
+ private static $CCITT_16 = array(
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
+ 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
+ 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
+ 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
+ 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
+ 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
+ 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
+ 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
+ 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
+ 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
+ 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
+ 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
+ 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
+ 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
+ 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
+ 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
+ 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
+ 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
+ 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
+ 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
+ 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
+ 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
+ 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
+ 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
+ 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
+ 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
+ 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
+ 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
+ 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
+ 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
+ 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
+ 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hash($value)
+ {
+ // CRC-CCITT-16 algorithm
+ $crc = 0;
+ $CCITT_16 = self::$CCITT_16;
+ $strlen = strlen($value);
+
+ for ($i = 0; $i < $strlen; $i++) {
+ $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF;
+ }
+
+ return $crc;
+ }
+}
+
+/* --------------------------------------------------------------------------- */
+
+namespace Predis\Command\Processor;
+
+use InvalidArgumentException;
+use Predis\Command\CommandInterface;
+use Predis\Command\PrefixableCommandInterface;
+use ArrayAccess;
+use ArrayIterator;
+
+/**
+ * A command processor processes Redis commands before they are sent to Redis.
+ *
+ * @author Daniele Alessandri
+ */
+interface ProcessorInterface
+{
+ /**
+ * Processes the given Redis command.
+ *
+ * @param CommandInterface $command Command instance.
+ */
+ public function process(CommandInterface $command);
+}
+
+/**
+ * Default implementation of a command processors chain.
+ *
+ * @author Daniele Alessandri
+ + + +
+