├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── src └── PhpBloomd │ ├── BloomFilter.php │ ├── BloomdClient.php │ └── IBloomdClient.php └── test └── BloomdClientTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | client.php 3 | vendor/ 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2013 Matt Layher 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-bloomd 2 | ========== 3 | 4 | PHP 5.4+ class for interacting with a bloomd server (https://github.com/armon/bloomd). MIT Licensed. 5 | 6 | Installation 7 | ------------ 8 | 9 | php-bloomd can be installed via Composer. Add `"mdlayher/php-bloomd": "dev-master"` to the `require` section 10 | of your `composer.json` and run `composer install`. 11 | 12 | Testing 13 | ------- 14 | 15 | php-bloomd can be tested using PHPUnit. Simply run `phpunit test` from the project root with a local bloomd 16 | server running on port 8673. 17 | 18 | Example 19 | ------- 20 | 21 | All commands accepted by bloomd are implemented in php-bloomd. Here is a basic example script. 22 | 23 | ```php 24 | createFilter("php")) 33 | { 34 | printf("example: failed to create filter\n"); 35 | exit; 36 | } 37 | 38 | // Create a filter object to use more concise, object-oriented interface 39 | $filter = $bloomd->filter("php"); 40 | 41 | // Set a couple of values in filter, using both BloomdClient and direct BloomFilter 42 | // Either method may be used for all functions which accept a filter name as first parameter 43 | $bloomd->set("php", "foo"); 44 | $filter->set("bar"); 45 | 46 | // Check the filter for membership 47 | if ($bloomd->check("php", "foo")) 48 | { 49 | printf("example: got it!\n"); 50 | } 51 | 52 | // Bulk set values 53 | $results = $filter->bulk(array("foo", "bar", "baz")); 54 | foreach ($results as $k => $v) 55 | { 56 | printf("%s -> %s\n", $k, $v ? "true" : "false"); 57 | } 58 | 59 | // Multi check values 60 | $results = $filter->multi(array("foo", "bar", "baz")); 61 | foreach ($results as $k => $v) 62 | { 63 | printf("%s -> %s\n", $k, $v ? "true" : "false"); 64 | } 65 | 66 | // Check for any value in array 67 | if ($filter->any(array("foo", "qux"))) 68 | { 69 | printf("any: yes!\n"); 70 | } 71 | 72 | // Check for all values in array 73 | if ($filter->all(array("foo", "bar", "baz"))) 74 | { 75 | printf("all: yes!\n"); 76 | } 77 | 78 | // Drop filter 79 | $filter->dropFilter(); 80 | ``` 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdlayher/php-bloomd", 3 | "version": "1.0", 4 | "description": "PHP 5.4+ class for interacting with a bloomd server. MIT Licensed.", 5 | "keywords": ["bloomd"], 6 | "homepage": "https://github.com/mdlayher/php-bloomd", 7 | "type": "library", 8 | "license": "MIT", 9 | "authors":[ 10 | { 11 | "name": "Matt Layher", 12 | "email": "mdlayher@gmail.com", 13 | "homepage": "http://mdlayher.com/" 14 | } 15 | ], 16 | "require":{ 17 | "php": ">=5.4.0" 18 | }, 19 | "autoload": { 20 | "psr-0": { "PhpBloomd": "src/" } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PhpBloomd/BloomFilter.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->client = $client; 21 | } 22 | 23 | // PUBLIC METHODS - - - - - - - - - - - - - - - - - - - 24 | 25 | // Call any functions using a named filter IBloomdClient, inserting this filter's name 26 | public function __call($name, $args) 27 | { 28 | // Disallowed methods for this object 29 | // connect/disconnect - creating or destroying connection from filter 30 | // filter - creating another filter from this one 31 | $disallowed = array("connect", "disconnect", "filter"); 32 | 33 | if (method_exists($this->client, $name) && !in_array($name, $disallowed)) 34 | { 35 | // Add filter name to arguments 36 | array_unshift($args, $this->name); 37 | 38 | return call_user_func_array(array($this->client, $name), $args); 39 | } 40 | 41 | throw new \Exception("BloomFilter->" . $name . ": not implemented or not allowed"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PhpBloomd/BloomdClient.php: -------------------------------------------------------------------------------- 1 | host = $host; 32 | 33 | // Validate port is integer, and in valid port range 34 | if (!ctype_digit($port) || $port < 1 || $port > 65535) 35 | { 36 | throw new \InvalidArgumentException(__CLASS__ . ": port must be a valid integer between 1 and 65535"); 37 | } 38 | 39 | $this->port = $port; 40 | } 41 | 42 | public function __destruct() 43 | { 44 | if (isset($this->socket) && is_resource($this->socket)) 45 | { 46 | fclose($this->socket); 47 | } 48 | } 49 | 50 | // PUBLIC METHODS - - - - - - - - - - - - - - - - - - - - 51 | 52 | // Generate a BloomFilter object from this client 53 | public function filter($name) 54 | { 55 | return new BloomFilter($name, $this); 56 | } 57 | 58 | // Create a bloom filter on server 59 | public function createFilter($name, $capacity = null, $probability = null, $inMemory = null) 60 | { 61 | // Begin building command to send to server 62 | $buffer = "create " . $name . " "; 63 | 64 | // If specified, send capacity 65 | if (isset($capacity) && is_int($capacity)) 66 | { 67 | $buffer .= "capacity=" . $capacity . " "; 68 | } 69 | 70 | // If specified, send false positive rate 71 | if (isset($probability) && is_float($probability)) 72 | { 73 | $buffer .= "prob=" . $probability . " "; 74 | } 75 | 76 | // If specified, choose if filter should reside in memory 77 | if (isset($inMemory) && is_bool($inMemory)) 78 | { 79 | // Bool to integer 80 | $buffer .= "in_memory=" . $inMemory ? 1 : 0; 81 | } 82 | 83 | // Send create filter request to server, verify done 84 | if ($this->send($buffer) === self::BLOOMD_DONE) 85 | { 86 | return true; 87 | } 88 | 89 | return false; 90 | } 91 | 92 | // Close an in-memory filter on server 93 | public function closeFilter($name) 94 | { 95 | return $this->send("close " . $name) === self::BLOOMD_DONE; 96 | } 97 | 98 | // Clear an in-memory filter on server 99 | // NOTE: Should only be called after filter is closed 100 | public function clearFilter($name) 101 | { 102 | return $this->send("clear " . $name) === self::BLOOMD_DONE; 103 | } 104 | 105 | // Drop a bloom filter on server 106 | public function dropFilter($name) 107 | { 108 | return $this->send("drop " . $name) === self::BLOOMD_DONE; 109 | } 110 | 111 | // Flush data from a specified filter 112 | public function flushFilter($name) 113 | { 114 | return $this->send("flush " . $name) === self::BLOOMD_DONE; 115 | } 116 | 117 | // Retrieve a list of filters and their status by matching name, or all filters if none provided 118 | public function listFilters($name = null) 119 | { 120 | // Send list request 121 | $response = $this->send("list " . $name); 122 | 123 | // List of statuses to send back 124 | $list = array(); 125 | 126 | // Parse through multi line response 127 | foreach (explode("\n", $response) as $line) 128 | { 129 | // Strip newlines, ignore blanks 130 | $line = trim($line, "\r\n"); 131 | if ($line == "") 132 | { 133 | continue; 134 | } 135 | 136 | // Convert status into associative arrays 137 | $fields = array("name", "probability", "size", "capacity", "items"); 138 | $list[] = array_combine($fields, explode(" ", $line)); 139 | } 140 | 141 | return $list; 142 | } 143 | 144 | // Retrieve detailed information about filter with specified name 145 | public function info($name) 146 | { 147 | // Send info request 148 | $response = $this->send("info " . $name); 149 | 150 | // Status associative array 151 | $status = array(); 152 | 153 | // Parse through multi line response 154 | foreach (explode("\n", $response) as $line) 155 | { 156 | // Strip newlines, ignore blanks or non-existant filters 157 | $line = trim($line, "\r\n"); 158 | if ($line == "" || $line === self::BLOOMD_NO_EXIST) 159 | { 160 | continue; 161 | } 162 | 163 | // Split into keys and values 164 | list($k, $v) = explode(" ", $line); 165 | $status[$k] = $v; 166 | } 167 | 168 | return $status; 169 | } 170 | 171 | // Check if value is in filter 172 | // NOTE: value is hashed in order to make long keys a uniform length 173 | public function check($filter, $value) 174 | { 175 | return $this->send(sprintf("check %s %s", $filter, sha1($value))) === self::BLOOMD_YES; 176 | } 177 | 178 | // Set a value in a specified filter 179 | public function set($filter, $value) 180 | { 181 | return $this->send(sprintf("set %s %s", $filter, sha1($value))) === self::BLOOMD_YES; 182 | } 183 | 184 | // Set multiple items in filter on server 185 | public function bulk($filter, array $items) 186 | { 187 | return $this->sendMulti("bulk", $filter, $items); 188 | } 189 | 190 | // Check for multiple items in filter on server 191 | public function multi($filter, array $items) 192 | { 193 | return $this->sendMulti("multi", $filter, $items); 194 | } 195 | 196 | // Check for multiple items in filter on server, but return true if any of them exist 197 | public function any($filter, array $items) 198 | { 199 | // Check array of values 200 | foreach ($this->multi($filter, $items) as $key => $value) 201 | { 202 | // Return true on first success 203 | if ($value) 204 | { 205 | return true; 206 | } 207 | } 208 | 209 | // Return false if none found 210 | return false; 211 | } 212 | 213 | // Check for multiple items in filter on server, but return true if all of them exist 214 | public function all($filter, array $items) 215 | { 216 | // Check array of values 217 | foreach ($this->multi($filter, $items) as $key => $value) 218 | { 219 | // Return false on first failure 220 | if (!$value) 221 | { 222 | return false; 223 | } 224 | } 225 | 226 | // Return true if all found 227 | return true; 228 | } 229 | 230 | // PRIVATE METHODS - - - - - - - - - - - - - - - - - - - - 231 | 232 | // Establish or re-use socket connection 233 | private function connect() 234 | { 235 | // Return existing socket 236 | if (isset($this->socket)) 237 | { 238 | return $this->socket; 239 | } 240 | 241 | // If not established, create a IPv4 TCP socket 242 | $this->socket = @stream_socket_client(sprintf("tcp://%s:%d", $this->host, $this->port), $errno, $errmsg); 243 | 244 | // Connect to host 245 | if (!$this->socket) 246 | { 247 | throw new \RuntimeException(__METHOD__ . ": failed to connect to bloomd server: " . $this->host . ":" . $this->port); 248 | } 249 | 250 | return $this->socket; 251 | } 252 | 253 | // Send a message to server on socket 254 | private function send($input) 255 | { 256 | $socket = $this->connect(); 257 | 258 | // Write message on socket, read reply 259 | fwrite($socket, $input . "\r\n"); 260 | $response = trim(fgets($socket), "\r\n"); 261 | 262 | // If reply indicates a list, loop it 263 | if ($response == self::BLOOMD_LIST_START) 264 | { 265 | // Loop until list end 266 | $response = ""; 267 | while (($line = trim(fgets($socket), "\r\n")) != self::BLOOMD_LIST_END) 268 | { 269 | $response .= $line . "\n"; 270 | } 271 | } 272 | 273 | // Throw exception if no response 274 | if (empty($response)) 275 | { 276 | throw new \RuntimeException(__METHOD__ . ": received empty response from bloomd server!"); 277 | } 278 | 279 | return $response; 280 | } 281 | 282 | // Do a multiple set/get operation 283 | private function sendMulti($command, $filter, array $items) 284 | { 285 | // Build command, add all items 286 | $buffer = sprintf("%s %s ", $command, $filter); 287 | foreach ($items as $i) 288 | { 289 | $buffer .= sha1($i) . " "; 290 | } 291 | 292 | // Set items, record status 293 | $response = explode(" ", $this->send($buffer)); 294 | 295 | // Create associative array of keys and booleans of whether or not they were successfully set 296 | $status = array(); 297 | for ($i = 0; $i < count($items); $i++) 298 | { 299 | $status[$items[$i]] = $response[$i] === self::BLOOMD_YES; 300 | } 301 | 302 | return $status; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/PhpBloomd/IBloomdClient.php: -------------------------------------------------------------------------------- 1 | dropFilter(self::$filter); 18 | $this->assertTrue($bloomd->createFilter(self::$filter)); 19 | } 20 | 21 | // Verify that it is possible to set items into a filter on bloomd server, but not duplicates 22 | public function testSet() 23 | { 24 | $bloomd = new Client(); 25 | 26 | // Set unique items, should return true 27 | $this->assertTrue($bloomd->set(self::$filter, "foo")); 28 | $this->assertTrue($bloomd->set(self::$filter, "bar")); 29 | 30 | // Should return false due to duplicate item 31 | $this->assertFalse($bloomd->set(self::$filter, "foo")); 32 | } 33 | 34 | // Verify that it is possible to check for items from a filter on bloomd server 35 | // NOTE: due to nature of bloom filters, it is possible to return true on an item which doesn't exist, 36 | // but NOT possible to return false on an item which does exist 37 | public function testCheck() 38 | { 39 | $bloomd = new Client(); 40 | 41 | // Items which exist will always return true 42 | $this->assertTrue($bloomd->check(self::$filter, "foo")); 43 | $this->assertTrue($bloomd->check(self::$filter, "bar")); 44 | } 45 | 46 | // Verify that it is possible to bulk set values on bloomd server 47 | public function testBulk() 48 | { 49 | $bloomd = new Client(); 50 | 51 | // Will return associative array of keys and their status 52 | // ex: array("foo" => true, "bar" => false), etc 53 | $results = $bloomd->bulk(self::$filter, array("baz", "qux", "corge")); 54 | foreach ($results as $k => $v) 55 | { 56 | // Verify all true 57 | $this->assertTrue($v); 58 | } 59 | } 60 | 61 | // Verify that it is possible to multi check values on bloomd server 62 | public function testMulti() 63 | { 64 | $bloomd = new Client(); 65 | 66 | // Will return associative array of keys and their status 67 | // ex: array("foo" => true, "bar" => false), etc 68 | $results = $bloomd->multi(self::$filter, array("baz", "qux", "corge")); 69 | foreach ($results as $k => $v) 70 | { 71 | // Verify all true 72 | $this->assertTrue($v); 73 | } 74 | } 75 | 76 | // Verify that it is possible to check for any value in array on bloomd server 77 | public function testAny() 78 | { 79 | $bloomd = new Client(); 80 | 81 | $this->assertTrue($bloomd->any(self::$filter, array("foo", "bar", "meow"))); 82 | } 83 | 84 | // Verify that it is possible to check for all values in array on bloomd server 85 | public function testAll() 86 | { 87 | $bloomd = new Client(); 88 | 89 | $this->assertTrue($bloomd->all(self::$filter, array("foo", "bar"))); 90 | } 91 | 92 | // Verify that it is possible to list information about a filter by its name 93 | public function testListFilters() 94 | { 95 | $bloomd = new Client(); 96 | 97 | $filter = $bloomd->listFilters(self::$filter); 98 | 99 | // Verify all fields contained in first filter 100 | $fields = array("name", "probability", "size", "capacity", "items"); 101 | foreach ($fields as $f) 102 | { 103 | $this->assertArrayHasKey($f, $filter[0]); 104 | } 105 | } 106 | 107 | // Verify that it is possible to query extended information about a filter by its name 108 | public function testInfo() 109 | { 110 | $bloomd = new Client(); 111 | 112 | $filter = $bloomd->info(self::$filter); 113 | 114 | // Verify all fields contained in filter information 115 | $fields = array( 116 | "capacity", 117 | "checks", 118 | "check_hits", 119 | "check_misses", 120 | "in_memory", 121 | "page_ins", 122 | "page_outs", 123 | "probability", 124 | "sets", 125 | "set_hits", 126 | "set_misses", 127 | "size", 128 | "storage", 129 | ); 130 | 131 | foreach ($fields as $f) 132 | { 133 | $this->assertArrayHasKey($f, $filter); 134 | } 135 | } 136 | 137 | // Verify that it is possible to drop a filter on bloomd server 138 | public function testDropFilter() 139 | { 140 | $bloomd = new Client(); 141 | 142 | $this->assertTrue($bloomd->dropFilter(self::$filter)); 143 | } 144 | } 145 | --------------------------------------------------------------------------------