Remote Procedure Call (RPC)
Remote Procedure Call (RPC)
Introduction
Hazaar Framework provides a robust RPC (Remote Procedure Call) implementation that allows you to expose server-side methods as remote callable procedures. The framework supports both JSON-RPC and XML-RPC protocols, making it easy to create interoperable web services.
RPC in Hazaar allows clients to execute methods on the server as if they were local function calls, with the framework handling serialization, deserialization, and protocol-specific details automatically.
Key Features
- Multiple Protocol Support: JSON-RPC and XML-RPC protocols via traits
- Type Safety: Full support for PHP type hints and return types
- Error Handling: Automatic exception handling and error responses
- Easy Integration: Simple routing setup with minimal configuration
- Client Library: Built-in clients for both JSON-RPC and XML-RPC
- Method Discovery: Automatic exposure of public methods as RPC endpoints
Creating an RPC Server
Basic Server Setup
To create an RPC server, extend the Hazaar\RPC\Server class and use the appropriate protocol traits:
<?php
declare(strict_types=1);
namespace App\Controller;
use Hazaar\RPC\Server;
use Hazaar\RPC\Trait\JSON;
use Hazaar\RPC\Trait\XML;
class RPC extends Server
{
use JSON; // Enables JSON-RPC protocol
use XML; // Enables XML-RPC protocol
}Protocol Traits
The framework provides two protocol traits:
Hazaar\RPC\Trait\JSON: Implements JSON-RPC 2.0 specificationHazaar\RPC\Trait\XML: Implements XML-RPC specification
You can use one or both traits depending on which protocols you want to support. Using both allows clients to choose their preferred protocol.
Defining RPC Methods
Any public method in your RPC server class automatically becomes an RPC endpoint. The framework uses reflection to discover and expose these methods:
class RPC extends Server
{
use JSON;
use XML;
public function add(int $a, int $b): int
{
return $a + $b;
}
public function subtract(int $a, int $b): int
{
return $a - $b;
}
public function multiply(int $a, int $b): int
{
return $a * $b;
}
public function divide(int $a, int $b): float
{
if (0 === $b) {
throw new \InvalidArgumentException('Division by zero is not allowed.');
}
return $a / $b;
}
}Method Best Practices
Use Type Hints: Always specify parameter and return types
public function greet(string $name): string { return "Hello, {$name}!"; }Validate Input: Check parameters and throw exceptions for invalid input
public function getUser(int $id): array { if ($id <= 0) { throw new \InvalidArgumentException('User ID must be positive.'); } // Fetch and return user data return ['id' => $id, 'name' => 'John Doe']; }Handle Errors: Throw exceptions for error conditions (they will be properly serialized)
public function processPayment(float $amount): bool { if ($amount <= 0) { throw new \InvalidArgumentException('Amount must be positive.'); } // Process payment logic return true; }Return Consistent Types: Ensure your return type matches the declared return type
public function fibonacci(int $n): int { if ($n < 0) { throw new \InvalidArgumentException('Input must be non-negative.'); } if ($n === 0) return 0; if ($n === 1) return 1; $a = 0; $b = 1; for ($i = 2; $i <= $n; ++$i) { $temp = $a + $b; $a = $b; $b = $temp; } return $b; }
Routing Configuration
Configure your RPC server endpoint in your routing file:
<?php
use App\Controller\RPC;
use Hazaar\Application\Router;
// Map POST requests to /rpc to the RPC server's handle method
Router::post('/rpc', [RPC::class, 'handle']);The handle() method is inherited from Hazaar\RPC\Server and processes incoming RPC requests:
- Parses the request based on the protocol (JSON or XML)
- Validates method names and parameters
- Invokes the appropriate method
- Serializes the response
- Handles errors and exceptions
Creating RPC Clients
JSON-RPC Client
The JSON-RPC client (Hazaar\RPC\Client\JSONRPC) allows you to make remote procedure calls using the JSON-RPC 2.0 protocol.
Basic Usage
<?php
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// Create a client instance (external URL as string)
$client = new JSONClient('https://example.com/rpc');
// Make RPC calls
$result = $client->call('add', [5, 3]);
echo "Result: {$result}"; // Output: Result: 8Complete Example
<?php
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// Initialize the client (external URL as string)
$client = new JSONClient('https://api.example.com/rpc');
try {
// Simple method call
$sum = $client->call('add', [10, 20]);
echo "10 + 20 = {$sum}\n";
// Method with string parameters
$greeting = $client->call('greet', ['Alice']);
echo "{$greeting}\n";
// Method with multiple parameters
$product = $client->call('multiply', [6, 7]);
echo "6 × 7 = {$product}\n";
// Method that returns complex data
$user = $client->call('getUser', [123]);
echo "User: {$user['name']}\n";
} catch (\Exception $e) {
echo "RPC Error: " . $e->getMessage();
}XML-RPC Client
The XML-RPC client (Hazaar\RPC\Client\XMLRPC) provides the same functionality using the XML-RPC protocol.
Basic Usage
<?php
use Hazaar\RPC\Client\XMLRPC as XMLClient;
// Create a client instance (external URL as string)
$client = new XMLClient('https://example.com/rpc');
// Make RPC calls
$result = $client->call('subtract', [100, 25]);
echo "Result: {$result}"; // Output: Result: 75Complete Example
<?php
use Hazaar\RPC\Client\XMLRPC as XMLClient;
// Initialize the client (external URL as string)
$client = new XMLClient('https://api.example.com/rpc');
try {
// Arithmetic operations
$difference = $client->call('subtract', [50, 15]);
echo "50 - 15 = {$difference}\n";
// Complex calculation
$fibonacci = $client->call('fibonacci', [15]);
echo "Fibonacci(15) = {$fibonacci}\n";
// Division with error handling
$quotient = $client->call('divide', [20, 5]);
echo "20 ÷ 5 = {$quotient}\n";
} catch (\InvalidArgumentException $e) {
echo "Invalid argument: " . $e->getMessage();
} catch (\Exception $e) {
echo "RPC Error: " . $e->getMessage();
}Client Configuration
Using Application URLs (Internal)
When making internal RPC calls within your application, use Hazaar\Application\URL to generate URLs relative to your application:
use Hazaar\Application\URL;
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// Internal application URL - resolves to your app's domain
$client = new JSONClient(new URL('rpc'));
// Within a controller or route handler
Router::get('/test', function () {
$client = new JSONClient(new URL('rpc'));
$result = $client->call('add', [5, 3]);
return ['result' => $result];
});Important: Hazaar\Application\URL always generates URLs for the current application. Do not use it for external services.
Using External URLs (Absolute)
To connect to external RPC services, provide the endpoint as a Hazaar\HTTP\URL object (not a plain string) or use Hazaar\HTTP\URL::build() to construct the URL, and pass this object to the client. This ensures compatibility with remote APIs outside your application.
use Hazaar\RPC\Client\JSONRPC as JSONClient;
use Hazaar\HTTP\URL;
$url = URL::build('https://api.example.com:8000/rpc');
$client = new JSONClient($url);Making RPC Calls
The call() Methods
The call() method is used to invoke remote procedures:
$result = $client->call(string $method, array $params);Parameters:
$method(string): The name of the remote method to call$params(array): Indexed array of parameters to pass to the method
Returns:
- The result returned by the remote method (type varies)
Throws:
- Exceptions for network errors, protocol errors, or remote exceptions
Parameter Passing
Parameters are passed as an indexed array in the order expected by the remote method:
// Single parameter
$client->call('greet', ['Alice']);
// Multiple parameters
$client->call('add', [5, 3]);
$client->call('divide', [20, 4]);
// Complex parameters
$client->call('createUser', [
['name' => 'John', 'email' => '[email protected]'],
true // activate parameter
]);Error Handling
Server-Side Error Handling
Exceptions thrown in RPC methods are automatically caught and converted to RPC error responses:
class RPC extends Server
{
use JSON;
public function divide(int $a, int $b): float
{
if ($b === 0) {
throw new \InvalidArgumentException('Division by zero is not allowed.');
}
return $a / $b;
}
public function getUser(int $id): array
{
$user = $this->userRepository->find($id);
if (!$user) {
throw new \RuntimeException("User not found: {$id}");
}
return $user->toArray();
}
}Client-Side Error Handling
Always wrap RPC calls in try-catch blocks to handle potential errors:
use Hazaar\Application\URL;
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// Internal application URL
$client = new JSONClient(new URL('rpc'));
try {
// This will throw an exception on the server
$result = $client->call('divide', [10, 0]);
} catch (\InvalidArgumentException $e) {
// Handle specific exception types
echo "Invalid argument: " . $e->getMessage();
} catch (\RuntimeException $e) {
// Handle runtime errors
echo "Runtime error: " . $e->getMessage();
} catch (\Exception $e) {
// Catch-all for network errors, protocol errors, etc.
echo "RPC call failed: " . $e->getMessage();
}Complete Usage Example
Server Implementation
File: app/controllers/RPC.php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hazaar\RPC\Server;
use Hazaar\RPC\Trait\JSON;
use Hazaar\RPC\Trait\XML;
class RPC extends Server
{
use JSON;
use XML;
/**
* Add two numbers
*/
public function add(int $a, int $b): int
{
return $a + $b;
}
/**
* Subtract two numbers
*/
public function subtract(int $a, int $b): int
{
return $a - $b;
}
/**
* Multiply two numbers
*/
public function multiply(int $a, int $b): int
{
return $a * $b;
}
/**
* Divide two numbers
*
* @throws \InvalidArgumentException if dividing by zero
*/
public function divide(int $a, int $b): float
{
if (0 === $b) {
throw new \InvalidArgumentException('Division by zero is not allowed.');
}
return $a / $b;
}
/**
* Calculate the nth Fibonacci number
*
* @throws \InvalidArgumentException if n is negative
*/
public function fibonacci(int $n): int
{
if ($n < 0) {
throw new \InvalidArgumentException('Input must be a non-negative integer.');
}
if (0 === $n) {
return 0;
}
if (1 === $n) {
return 1;
}
$a = 0;
$b = 1;
for ($i = 2; $i <= $n; ++$i) {
$temp = $a + $b;
$a = $b;
$b = $temp;
}
return $b;
}
/**
* Greet a user by name
*/
public function greet(string $name): string
{
return "Hello, {$name}!";
}
/**
* Get user information
*
* @throws \RuntimeException if user not found
*/
public function getUser(int $id): array
{
if ($id <= 0) {
throw new \InvalidArgumentException('User ID must be positive.');
}
// Simulate user lookup
$users = [
1 => ['id' => 1, 'name' => 'Alice', 'email' => '[email protected]'],
2 => ['id' => 2, 'name' => 'Bob', 'email' => '[email protected]'],
];
if (!isset($users[$id])) {
throw new \RuntimeException("User not found: {$id}");
}
return $users[$id];
}
}Routing Configuration
File: app/route.php
<?php
declare(strict_types=1);
use App\Controller\RPC;
use Hazaar\Application\Router;
use Hazaar\Application\URL;
use Hazaar\RPC\Client\JSONRPC as JSONClient;
use Hazaar\RPC\Client\XMLRPC as XMLClient;
// RPC Server endpoint
Router::post('/rpc', [RPC::class, 'handle']);
// Test route demonstrating JSON-RPC client
Router::get('/rpctest/json', function () {
$client = new JSONClient(new URL('rpc'));
$results = [];
try {
$results['add'] = $client->call('add', [5, 3]);
$results['subtract'] = $client->call('subtract', [10, 4]);
$results['multiply'] = $client->call('multiply', [6, 7]);
$results['divide'] = $client->call('divide', [20, 5]);
$results['fibonacci'] = $client->call('fibonacci', [15]);
$results['greet'] = $client->call('greet', ['Alice']);
$results['user'] = $client->call('getUser', [1]);
} catch (\Exception $e) {
$results['error'] = $e->getMessage();
}
return $results;
});
// Test route demonstrating XML-RPC client
Router::get('/rpctest/xml', function () {
$client = new XMLClient(new URL('rpc'));
$results = [];
try {
$results['add'] = $client->call('add', [5, 3]);
$results['subtract'] = $client->call('subtract', [10, 4]);
$results['fibonacci'] = $client->call('fibonacci', [10]);
} catch (\Exception $e) {
$results['error'] = $e->getMessage();
}
return $results;
});Client Usage Examples
Example 1: Simple Calculator Service
<?php
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// External API - use string URL
$client = new JSONClient('https://api.example.com/rpc');
// Perform calculations
$operations = [
['add', [10, 5]],
['subtract', [10, 5]],
['multiply', [10, 5]],
['divide', [10, 5]],
];
foreach ($operations as [$method, $params]) {
try {
$result = $client->call($method, $params);
echo sprintf(
"%s(%s) = %s\n",
$method,
implode(', ', $params),
$result
);
} catch (\Exception $e) {
echo "Error calling {$method}: {$e->getMessage()}\n";
}
}Example 2: User Management Service
<?php
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// External API - use string URL
$client = new JSONClient('https://api.example.com/rpc');
// Fetch user data
try {
$user = $client->call('getUser', [1]);
echo "User: {$user['name']} ({$user['email']})\n";
// Greet the user
$greeting = $client->call('greet', [$user['name']]);
echo "{$greeting}\n";
} catch (\RuntimeException $e) {
echo "User not found\n";
} catch (\Exception $e) {
echo "Error: {$e->getMessage()}\n";
}Example 3: Batch Processing
<?php
use Hazaar\Application\URL;
use Hazaar\RPC\Client\JSONRPC as JSONClient;
// Internal application URL
$client = new JSONClient(new URL('rpc'));
// Calculate multiple fibonacci numbers
$numbers = [5, 10, 15, 20];
$results = [];
foreach ($numbers as $n) {
try {
$results[$n] = $client->call('fibonacci', [$n]);
} catch (\Exception $e) {
$results[$n] = "Error: {$e->getMessage()}";
}
}
foreach ($results as $n => $result) {
echo "Fibonacci({$n}) = {$result}\n";
}Advanced Topics
Method Visibility
Only public methods are exposed as RPC endpoints. Use protected or private methods for internal logic:
class RPC extends Server
{
use JSON;
// Exposed as RPC endpoint
public function calculateDiscount(float $amount, string $code): float
{
$rate = $this->getDiscountRate($code);
return $amount * (1 - $rate);
}
// Not exposed - internal helper
protected function getDiscountRate(string $code): float
{
$codes = [
'SAVE10' => 0.10,
'SAVE20' => 0.20,
];
return $codes[$code] ?? 0.0;
}
// Not exposed - internal helper
private function logAccess(): void
{
// Logging logic
}
}Authentication and Authorization
Integrate authentication into your RPC methods:
class RPC extends Server
{
use JSON;
public function getPublicData(): array
{
// No authentication required
return ['data' => 'public information'];
}
public function getPrivateData(string $token): array
{
// Validate token
if (!$this->validateToken($token)) {
throw new \RuntimeException('Invalid authentication token');
}
return ['data' => 'private information'];
}
protected function validateToken(string $token): bool
{
// Token validation logic
return strlen($token) > 0;
}
}Choosing Between JSON-RPC and XML-RPC
Use JSON-RPC when:
- Building modern web applications
- You need lightweight protocol with better performance
- Working with JavaScript clients
- You prefer simpler, more readable format
Use XML-RPC when:
- You need compatibility with legacy systems
- Working with platforms that only support XML-RPC
- You require explicit data typing
- Integrating with older enterprise systems
Use both when:
- You want maximum compatibility
- Supporting diverse client ecosystems
- Migration scenarios (supporting both during transition)
Best Practices
1. Method Design
- Keep methods focused and single-purpose
- Use descriptive method names
- Always declare parameter and return types
- Validate all input parameters
- Throw appropriate exceptions for errors
2. Security
- Validate and sanitize all input
- Implement authentication for sensitive operations
- Use HTTPS in production
- Rate limit RPC endpoints
- Log all RPC calls for audit trails
3. Performance
- Keep RPC methods fast and efficient
- Avoid long-running operations (use job queues instead)
- Cache results when appropriate
- Minimize data transfer (return only necessary data)
4. Error Handling
- Use specific exception types
- Provide clear, actionable error messages
- Log errors on the server side
- Handle exceptions gracefully on the client side
5. Documentation
- Document each RPC method with PHPDoc comments
- Specify parameter types and return types
- Document exceptions that may be thrown
- Provide usage examples
6. Versioning
Consider implementing versioning for your RPC API:
// Option 1: Separate endpoints
Router::post('/rpc/v1', [RPCv1::class, 'handle']);
Router::post('/rpc/v2', [RPCv2::class, 'handle']);
// Option 2: Version parameter
public function getData(int $version = 1): array
{
if ($version === 2) {
return $this->getDataV2();
}
return $this->getDataV1();
}Troubleshooting
Common Issues
Problem: Method not found
Solution: Ensure the method is public and defined in your RPC server classProblem: Parameter type mismatch
Solution: Check that parameters passed from client match server method signatureProblem: Connection refused
Solution: Verify the RPC endpoint URL and ensure the server is runningProblem: Serialization errors
Solution: Ensure return values are serializable (avoid resources, closures)Debugging
Enable error reporting during development:
// In your RPC controller
class RPC extends Server
{
use JSON;
public function __construct()
{
// Enable debug mode in development
if (env('APP_ENV') === 'development') {
error_reporting(E_ALL);
ini_set('display_errors', '1');
}
}
}Log RPC calls for debugging:
class RPC extends Server
{
use JSON;
public function add(int $a, int $b): int
{
// Log the call
error_log("RPC call: add({$a}, {$b})");
$result = $a + $b;
// Log the result
error_log("RPC result: {$result}");
return $result;
}
}Summary
The Hazaar RPC implementation provides a powerful yet simple way to create remote procedure call services:
- Server: Extend
Hazaar\RPC\Serverand use protocol traits - Methods: Define public methods with proper type hints
- Routing: Map a route to the server's
handle()method - Clients: Use
JSONRPCorXMLRPCclient classes - Calls: Invoke remote methods with
$client->call() - Errors: Handle exceptions with try-catch blocks
With proper implementation and following best practices, you can build robust, scalable RPC services that integrate seamlessly with your Hazaar applications.