diff --git a/.gitignore b/.gitignore index aaadf73..3109dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,32 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +# Composer dependencies +/vendor/ +composer.lock -# Test binary, built with `go test -c` -*.test +# IDE files +/.idea/ +/.vscode/ +*.swp +*.swo -# Code coverage profiles and other test artifacts -*.out -coverage.* -*.coverprofile -profile.cov +# OS files +.DS_Store +Thumbs.db -# Dependency directories (remove the comment below to include it) -# vendor/ +# Test coverage +/coverage/ +clover.xml +coverage.xml +*.coverage -# Go workspace file -go.work -go.work.sum - -# env file +# Environment files .env +.env.local +.env.*.local + +# Cache files +*.cache +/cache/ -# Editor/IDE -# .idea/ -# .vscode/ +# Log files +*.log +/logs/ diff --git a/README.md b/README.md index 2be73da..e6288b7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,126 @@ # GraphRedis - A Graph Database with Redis! + +A Graph Database with Redis! + +GraphRedis is a PHP library that provides graph database functionality using Redis as the storage backend. It supports nodes, edges, and basic graph operations with a simple and intuitive API. + +## Installation + +Install GraphRedis using Composer: + +```bash +composer require linkerlin/graph-redis +``` + +## Requirements + +- PHP 7.4 or higher +- Redis extension for PHP +- Redis server + +## Usage + +### Basic Example + +```php +connect('127.0.0.1', 6379); + +// Add nodes +$graph->addNode('user1', ['name' => 'John Doe', 'age' => 30]); +$graph->addNode('user2', ['name' => 'Jane Smith', 'age' => 25]); + +// Add edge between nodes +$graph->addEdge('user1', 'user2', 'FRIENDS_WITH', ['since' => '2020-01-01']); + +// Get node data +$user1 = $graph->getNode('user1'); +print_r($user1); + +// Get outgoing edges +$edges = $graph->getOutgoingEdges('user1'); +print_r($edges); + +// Close connection +$graph->close(); +``` + +### Advanced Usage + +#### Custom Redis Instance + +```php +$redis = new Redis(); +$redis->connect('127.0.0.1', 6379); + +$graph = new GraphRedis($redis, 'my_graph:'); +``` + +#### Working with Relationships + +```php +// Add different types of relationships +$graph->addEdge('user1', 'company1', 'WORKS_AT', ['position' => 'Developer']); +$graph->addEdge('user2', 'company1', 'WORKS_AT', ['position' => 'Manager']); + +// Get incoming relationships +$incoming = $graph->getIncomingEdges('company1'); +``` + +## API Reference + +### GraphRedis Class + +#### Constructor + +```php +new GraphRedis($redis = null, $keyPrefix = 'graph:') +``` + +- `$redis`: Optional Redis instance. If not provided, a new Redis instance will be created. +- `$keyPrefix`: Key prefix for Redis keys (default: 'graph:') + +#### Methods + +##### Connection + +- `connect($host = '127.0.0.1', $port = 6379, $timeout = 0.0)`: Connect to Redis server +- `close()`: Close Redis connection + +##### Node Operations + +- `addNode($nodeId, array $properties = [])`: Add a node to the graph +- `getNode($nodeId)`: Get node properties +- `removeNode($nodeId)`: Remove a node and all its edges + +##### Edge Operations + +- `addEdge($fromNodeId, $toNodeId, $relationship = 'CONNECTED_TO', array $properties = [])`: Add an edge between nodes +- `getOutgoingEdges($nodeId)`: Get outgoing edges for a node +- `getIncomingEdges($nodeId)`: Get incoming edges for a node + +## Testing + +Run tests using PHPUnit: + +```bash +composer install +vendor/bin/phpunit +``` + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2e29a9f --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "linkerlin/graph-redis", + "description": "A Graph Database with Redis!", + "type": "library", + "license": "Apache-2.0", + "keywords": ["graph", "database", "redis", "graph-database"], + "authors": [ + { + "name": "linkerlin", + "email": "linkerlin@example.com" + } + ], + "require": { + "php": ">=7.4", + "ext-redis": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "autoload": { + "psr-4": { + "GraphRedis\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "GraphRedis\\Tests\\": "tests/" + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..32bf6e1 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + tests + + + + + + src + + + \ No newline at end of file diff --git a/src/GraphRedis.php b/src/GraphRedis.php new file mode 100644 index 0000000..1ad0dcd --- /dev/null +++ b/src/GraphRedis.php @@ -0,0 +1,191 @@ +redis = $redis ?: new Redis(); + $this->keyPrefix = $keyPrefix; + } + + /** + * Connect to Redis server + * + * @param string $host Redis host + * @param int $port Redis port + * @param float $timeout Connection timeout + * @return bool True on success + * @throws Exception If connection fails + */ + public function connect($host = '127.0.0.1', $port = 6379, $timeout = 0.0) + { + if (!$this->redis->connect($host, $port, $timeout)) { + throw new Exception("Failed to connect to Redis server at {$host}:{$port}"); + } + + return true; + } + + /** + * Add a node to the graph + * + * @param string $nodeId Unique node identifier + * @param array $properties Node properties + * @return bool True on success + */ + public function addNode($nodeId, array $properties = []) + { + $nodeKey = $this->keyPrefix . 'nodes:' . $nodeId; + return $this->redis->hMSet($nodeKey, $properties); + } + + /** + * Get a node from the graph + * + * @param string $nodeId Node identifier + * @return array|null Node properties or null if not found + */ + public function getNode($nodeId) + { + $nodeKey = $this->keyPrefix . 'nodes:' . $nodeId; + $node = $this->redis->hGetAll($nodeKey); + + return empty($node) ? null : $node; + } + + /** + * Add an edge between two nodes + * + * @param string $fromNodeId Source node ID + * @param string $toNodeId Target node ID + * @param string $relationship Relationship type + * @param array $properties Edge properties + * @return bool True on success + */ + public function addEdge($fromNodeId, $toNodeId, $relationship = 'CONNECTED_TO', array $properties = []) + { + // Add edge to outgoing edges of source node + $outgoingKey = $this->keyPrefix . 'edges:out:' . $fromNodeId; + $edgeData = [ + 'to' => $toNodeId, + 'relationship' => $relationship, + 'properties' => json_encode($properties) + ]; + + // Add edge to incoming edges of target node + $incomingKey = $this->keyPrefix . 'edges:in:' . $toNodeId; + $reverseEdgeData = [ + 'from' => $fromNodeId, + 'relationship' => $relationship, + 'properties' => json_encode($properties) + ]; + + $edgeId = $fromNodeId . ':' . $toNodeId . ':' . $relationship; + + return $this->redis->hMSet($outgoingKey, [$edgeId => json_encode($edgeData)]) && + $this->redis->hMSet($incomingKey, [$edgeId => json_encode($reverseEdgeData)]); + } + + /** + * Get outgoing edges for a node + * + * @param string $nodeId Node identifier + * @return array Array of outgoing edges + */ + public function getOutgoingEdges($nodeId) + { + $outgoingKey = $this->keyPrefix . 'edges:out:' . $nodeId; + $edges = $this->redis->hGetAll($outgoingKey); + + $result = []; + foreach ($edges as $edgeId => $edgeDataJson) { + $edgeData = json_decode($edgeDataJson, true); + $result[] = [ + 'id' => $edgeId, + 'to' => $edgeData['to'], + 'relationship' => $edgeData['relationship'], + 'properties' => json_decode($edgeData['properties'], true) + ]; + } + + return $result; + } + + /** + * Get incoming edges for a node + * + * @param string $nodeId Node identifier + * @return array Array of incoming edges + */ + public function getIncomingEdges($nodeId) + { + $incomingKey = $this->keyPrefix . 'edges:in:' . $nodeId; + $edges = $this->redis->hGetAll($incomingKey); + + $result = []; + foreach ($edges as $edgeId => $edgeDataJson) { + $edgeData = json_decode($edgeDataJson, true); + $result[] = [ + 'id' => $edgeId, + 'from' => $edgeData['from'], + 'relationship' => $edgeData['relationship'], + 'properties' => json_decode($edgeData['properties'], true) + ]; + } + + return $result; + } + + /** + * Remove a node and all its edges + * + * @param string $nodeId Node identifier + * @return bool True on success + */ + public function removeNode($nodeId) + { + $nodeKey = $this->keyPrefix . 'nodes:' . $nodeId; + $outgoingKey = $this->keyPrefix . 'edges:out:' . $nodeId; + $incomingKey = $this->keyPrefix . 'edges:in:' . $nodeId; + + // Remove the node itself + $this->redis->del($nodeKey); + + // Remove all outgoing and incoming edges + $this->redis->del($outgoingKey); + $this->redis->del($incomingKey); + + return true; + } + + /** + * Close the Redis connection + */ + public function close() + { + $this->redis->close(); + } +} \ No newline at end of file diff --git a/tests/GraphRedisTest.php b/tests/GraphRedisTest.php new file mode 100644 index 0000000..64c2b66 --- /dev/null +++ b/tests/GraphRedisTest.php @@ -0,0 +1,60 @@ +graph = new GraphRedis(null, 'test:graph:'); + } + + protected function tearDown(): void + { + if ($this->graph) { + $this->graph->close(); + } + } + + public function testCanCreateGraphRedisInstance() + { + $this->assertInstanceOf(GraphRedis::class, $this->graph); + } + + public function testNodeOperations() + { + // Test adding a node + $nodeId = 'user1'; + $properties = ['name' => 'John Doe', 'age' => 30]; + + // Note: These tests will only pass if Redis is available + // For now, we'll just test that the methods exist and are callable + $this->assertTrue(method_exists($this->graph, 'addNode')); + $this->assertTrue(method_exists($this->graph, 'getNode')); + $this->assertTrue(method_exists($this->graph, 'removeNode')); + } + + public function testEdgeOperations() + { + // Test that edge methods exist + $this->assertTrue(method_exists($this->graph, 'addEdge')); + $this->assertTrue(method_exists($this->graph, 'getOutgoingEdges')); + $this->assertTrue(method_exists($this->graph, 'getIncomingEdges')); + } + + public function testConnectionMethods() + { + // Test that connection methods exist + $this->assertTrue(method_exists($this->graph, 'connect')); + $this->assertTrue(method_exists($this->graph, 'close')); + } +} \ No newline at end of file